本人博客原文
google原生Android中,MiniThumbFile.java里存储图片/视频的缩略图的算法有问题。
该算法的漏洞造成微缩略图文件(DCIM\.thumbnails\.thumbdata4--1967290299)非常庞大和臃肿,多达1G,理论上可以无限大,直到填满SD卡
重现步骤:
第一步:插入一张拥有10万张图片的外部SD卡,
第二步:等待手机扫描完整个SD卡,这个过程大概30分钟。至于扫描是否完成,你可以看TAG为MediaProvider的日志。
第三步:拔出外部SD卡,再次等手机进行扫描大概10分钟。
第四步:按下手机的电源key +音量down key来截屏。
第五步:通过电脑查看内部SD卡的DCIM\.thumbnails\.thumbdata4--1967290299文件。这时你会发现,该文件多达1G.
对于该问题,大概的逻辑是这样的:
MediaProvider会对SD卡上的文件的进行扫描,得到一些基本的信息放入数据库的files这个表中。
对于一些图片(比如手机截屏图片),手机会以这些图片文件在files表中的_id的值,作为一个定位标志,把其缩略文件存放在.thumbdata4--1967290299中。
比如:_id为1,那么该文件的缩略图就存放在.thumbdata4--1967290299文件的_id * BYTES_PER_MINTHUMB开始的位置,其中BYTES_PER_MINTHUMB = 17000,
对此可以参照MiniThumbFile.java文件中的saveMiniThumbToFile(byte[] data, long id, long magic)等函数
对于Android的Sqlite3数据库,_id是自增的。比如,最开始我插入了10001记录,这时最后一条记录的_id为10001,这时如果删除1到10000条记录,
再插入一条记录,那么这条的记录的_id值为10002.
因此,随着用户日常中不停的向SD卡反复地添加删除文件,那么files表中的_id的值会不停的增大,
当该值为10万时,即使我们截屏时,系统只把该截屏图片这样一张图的缩略图保存在.thumbdata4--1967290299文件,
10万*BYTES_PER_MINTHUMB>1G,也就是说.thumbdata4--1967290299文件就会占用了1G多的SD空间。随着时间的推移,该值会继续变大,直到填满SD卡。
我们的方案就是把.thumbdata4--1967290299文件分成多个文件来存储缩略图。
比如.thumbdata4--1967290299-0存_id 为1到1024的文件的缩略图,.thumbdata4--1967290299-1存_id 为1025到2048的文件的缩略图
同时对缩略图文件做一些维护,即如果一个缩略图文件本身对应原文件不在时,清理掉该微缩略图文件。
另外在极端情况下(剩余容量小于100M下),清理掉所有的微缩略图文件。
在MTK 6575平台上对该问题的修改包含2个文件的修改:
modified: frameworks/base/media/java/android/media/MediaScanner.javamodified: frameworks/base/media/java/android/media/MiniThumbFile.java
MiniThumbFile.java文件的主要修改如下:
/**@@ -85,16 +89,34 @@ public class MiniThumbFile {/*add 1 on google's version.*//*this version add check code for thumbdata.*/private static final int MINI_THUMB_DATA_FILE_VERSION = 3 + 1;- public static final int BYTES_PER_MINTHUMB = 17000;+ public static final int BYTES_PER_MINTHUMB = 17000;private static final int HEADER_SIZE = 1 + 8 + 4 + 8;private Uri mUri;- private RandomAccessFile mMiniThumbFile;+ Map<Long,RandomAccessFile> mMiniThumbFilesMap = Collections.synchronizedMap(new HashMap<Long,RandomAccessFile>(5));private FileChannel mChannel;private ByteBuffer mBuffer;private static Hashtable<String, MiniThumbFile> sThumbFiles =new Hashtable<String, MiniThumbFile>();private static java.util.zip.Adler32 sChecker = new Adler32();private static final long UNKNOWN_CHECK_CODE = -1;+ final static char SEPERATE_CHAR='_';+ public static String[] parseFileName(String fileName)+ {+ String str=fileName;+ if(fileName==null)+ return null;+ int index=fileName.lastIndexOf("/");+ if(index!=-1)+ {+ str=fileName.substring(index+1);+ }+ String strs[]=str.split(SEPERATE_CHAR+"");+ if(strs==null||strs.length!=4||(!str.startsWith(".thumbdata")))+ {+ return null;+ }+ return strs;+ }/*** We store different types of thumbnails in different files. To remain backward compatibility,* we should hashcode of content://media/external/images/media remains the same.@@ -105,7 +127,18 @@ public class MiniThumbFile {}sThumbFiles.clear();}-+ private final static long getMiniThumbFileId(long id)+ {+ return (id>>8);+ }+ public final static int getMiniThumbDataFileBlockNum()+ {+ return (1<<8);+ }+ private final static long getPositionInMiniThumbFile(long id)+ {+ return (id&0xff)*BYTES_PER_MINTHUMB;+ }public static synchronized MiniThumbFile instance(Uri uri) {String type = uri.getPathSegments().get(1);MiniThumbFile file = sThumbFiles.get(type);@@ -120,10 +153,11 @@ public class MiniThumbFile {}private String randomAccessFilePath(int version) {+ String type = mUri.getPathSegments().get(1);String directoryName =Environment.getExternalStorageDirectory().toString() + "/DCIM/.thumbnails";- return directoryName + "/.thumbdata" + version + "-" + mUri.hashCode();+ return directoryName + "/.thumbdata" + version + SEPERATE_CHAR + mUri.hashCode()+SEPERATE_CHAR+type;}/**@@ -131,18 +165,18 @@ public class MiniThumbFile {* @param uri the Uri same as instance(Uri uri).* @return*/- public static String getThumbdataPath(Uri uri) {+ /*public static String getThumbdataPath(Uri uri) {String type = uri.getPathSegments().get(1);Uri thumbFileUri = Uri.parse("content://media/external/" + type + "/media");String directoryName = Environment.getExternalStorageDirectory().toString()+ "/DCIM/.thumbnails";- String path = directoryName + "/.thumbdata" + MINI_THUMB_DATA_FILE_VERSION + "-" + thumbFileUri.hashCode();+ String path = directoryName + "/.thumbdata" + MINI_THUMB_DATA_FILE_VERSION +SEPERATE_CHAR + thumbFileUri.hashCode()+SEPERATE_CHAR+type;if (LOG) Log.i(TAG, "getThumbdataPath(" + uri + ") return " + path);return path;- }+ }*/- private void removeOldFile() {- String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1);+ private void removeOldMiniThumbDataFile(long miniThumbDataFileId) {+ String oldPath =getMiniThumbDataFilePath(MINI_THUMB_DATA_FILE_VERSION - 1,miniThumbDataFileId);File oldFile = new File(oldPath);if (oldFile.exists()) {try {@@ -152,35 +186,43 @@ public class MiniThumbFile {}}}-- private RandomAccessFile miniThumbDataFile() {- if (mMiniThumbFile == null) {- removeOldFile();- String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION);- File directory = new File(path).getParentFile();- if (!directory.isDirectory()) {- if (!directory.mkdirs()) {- Log.e(TAG, "Unable to create .thumbnails directory "- + directory.toString());- }- }+ final private String getMiniThumbDataFilePath(int version,long miniThumbDataFileId)+ {+ return randomAccessFilePath(version)+SEPERATE_CHAR+miniThumbDataFileId;+ }+ private RandomAccessFile miniThumbDataFile(final long miniThumbDataFileId) {+ return miniThumbDataFile(miniThumbDataFileId,false);+ }+ private RandomAccessFile miniThumbDataFile(final long miniThumbDataFileId,boolean onlyRead) {+ RandomAccessFile miniThumbFile=mMiniThumbFilesMap.get(miniThumbDataFileId);+ String path = getMiniThumbDataFilePath(MINI_THUMB_DATA_FILE_VERSION,miniThumbDataFileId);File f = new File(path);+ if (miniThumbFile == null||!f.exists()) {+ if(onlyRead)+ return null;+ removeOldMiniThumbDataFile(miniThumbDataFileId);+ File directory = new File(path).getParentFile();+ if (!directory.isDirectory()) {+ if (!directory.mkdirs()) {+ Log.e(TAG, "Unable to create .thumbnails directory "+ + directory.toString());+ }+ }+try {- mMiniThumbFile = new RandomAccessFile(f, "rw");+ miniThumbFile = new RandomAccessFile(f, "rw");} catch (IOException ex) {// Open as read-only so we can at least read the existing// thumbnails.try {- mMiniThumbFile = new RandomAccessFile(f, "r");+ miniThumbFile = new RandomAccessFile(f, "r");} catch (IOException ex2) {}}- if (mMiniThumbFile != null) {- mChannel = mMiniThumbFile.getChannel();- }+ mMiniThumbFilesMap.put(miniThumbDataFileId, miniThumbFile);}- return mMiniThumbFile;+ return miniThumbFile;}public MiniThumbFile(Uri uri) {@@ -189,10 +231,19 @@ public class MiniThumbFile {}public synchronized void deactivate() {- if (mMiniThumbFile != null) {+ if (mMiniThumbFilesMap != null) {+ Set<Long> keySet=mMiniThumbFilesMap.keySet();try {- mMiniThumbFile.close();- mMiniThumbFile = null;+ for(Long key:keySet)+ {+ RandomAccessFile file=mMiniThumbFilesMap.get(key);+ if(file!=null)+ {+ mMiniThumbFilesMap.put(key, null);+ file.close();+ }+ }+ mMiniThumbFilesMap.clear();} catch (IOException ex) {// ignore exception}@@ -205,18 +256,20 @@ public class MiniThumbFile {// check the mini thumb file for the right data. Right is// defined as having the right magic number at the offset// reserved for this "id".- RandomAccessFile r = miniThumbDataFile();+ final long miniThumbDataFileId=getMiniThumbFileId(id);+ RandomAccessFile r = miniThumbDataFile(miniThumbDataFileId,true);if (r != null) {- long pos = id * BYTES_PER_MINTHUMB;+ FileChannel channel=r.getChannel();+ long pos = getPositionInMiniThumbFile(id);FileLock lock = null;try {mBuffer.clear();mBuffer.limit(1 + 8);- lock = mChannel.lock(pos, 1 + 8, true);+ lock = channel.lock(pos, 1 + 8, true);// check that we can read the following 9 bytes// (1 for the "status" and 8 for the long)- if (mChannel.read(mBuffer, pos) == 9) {+ if (channel.read(mBuffer, pos) == 9) {mBuffer.position(0);if (mBuffer.get() == 1) {return mBuffer.getLong();@@ -242,10 +295,12 @@ public class MiniThumbFile {public synchronized void saveMiniThumbToFile(byte[] data, long id, long magic)throws IOException {- RandomAccessFile r = miniThumbDataFile();+ final long miniThumbDataFileId=getMiniThumbFileId(id);+ RandomAccessFile r = miniThumbDataFile(miniThumbDataFileId);if (r == null) return;- long pos = id * BYTES_PER_MINTHUMB;+ final long pos = getPositionInMiniThumbFile(id);+ FileChannel channel=r.getChannel();FileLock lock = null;try {if (data != null) {@@ -266,14 +321,13 @@ public class MiniThumbFile {check = sChecker.getValue();}mBuffer.putLong(check);- if (LOG) Log.i(TAG, "saveMiniThumbToFile(" + id + ") flag=1, magic="- + magic + ", length=" + data.length + ", check=" + check);-mBuffer.put(data);mBuffer.flip();- lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, false);- mChannel.write(mBuffer, pos);+ lock = channel.lock(pos, BYTES_PER_MINTHUMB, false);+ channel.write(mBuffer, pos);+ if (LOG) Log.i(TAG, "saveMiniThumbToFile(" + id + ") flag=1, magic="+ + magic + ", length=" + data.length + ", check=" + check);}} catch (IOException ex) {Log.e(TAG, "couldn't save mini thumbnail data for "@@ -319,27 +373,29 @@ public class MiniThumbFile {* @return*/public synchronized byte[] getMiniThumbFromFile(long id, byte [] data, ThumbResult result) {- RandomAccessFile r = miniThumbDataFile();+ final long miniThumbDataFileId=getMiniThumbFileId(id);+ RandomAccessFile r = miniThumbDataFile(miniThumbDataFileId,true);if (r == null) return null;- long pos = id * BYTES_PER_MINTHUMB;+ long pos = getPositionInMiniThumbFile(id);+ FileChannel channel=r.getChannel();FileLock lock = null;try {mBuffer.clear();- lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, true);- int size = mChannel.read(mBuffer, pos);+ lock = channel.lock(pos, BYTES_PER_MINTHUMB, true);+ int size = channel.read(mBuffer, pos);if (size > 1 + 8 + 4 + 8) { // flag, magic, length, check codemBuffer.position(0);byte flag = mBuffer.get();long magic = mBuffer.getLong();int length = mBuffer.getInt();long check = mBuffer.getLong();- if (LOG) Log.i(TAG, "getMiniThumbFromFile(" + id + ") flag=" + flag- + ", magic=" + magic + ", length=" + length + ", check=" + check);-long newCheck = UNKNOWN_CHECK_CODE;if (size >= 1 + 8 + 4 + 8 + length && data.length >= length) {mBuffer.get(data, 0, length);+ Log.i(TAG, " success to getMiniThumbFromFile(" + id + ") flag=" + flag+ + ", magic=" + magic + ", length=" + length + ", check=" + check);synchronized (sChecker) {sChecker.reset();sChecker.update(data, 0, length);
MediaScanner.java文件的修改如下:
@@ -1224,14 +1224,14 @@ public class MediaScannerc.close();}- if (videoCount != 0) {+ /*if (videoCount != 0) {String fullPathString = MiniThumbFile.getThumbdataPath(mVideoThumbsUri);existingFiles.remove(fullPathString);}if (imageCount != 0) {String fullPathString = MiniThumbFile.getThumbdataPath(mThumbsUri);existingFiles.remove(fullPathString);- }+ }*/for (String fileToDelete : existingFiles) {if (LOG)Log.v(TAG, "fileToDelete is " + fileToDelete);@@ -1247,7 +1247,163 @@ public class MediaScannere.printStackTrace();}}+ /*[robin_20120511*/+ private void pruneDeadMiniThumbnailFiles() {+ HashSet<String> existingFiles = new HashSet<String>();+ String directory = Environment.getExternalStorageDirectory().toString() + "/DCIM/.thumbnails";+ File file=new File(directory);+ String [] files = (file).list();+ if (files == null) {+ files = new String[0];+ }+ for (int i=0; i < files.length; i++) {+ String fullPathString = directory + "/" + files[i];+ existingFiles.add(fullPathString);+ }+ try {+ Cursor c = mMediaProvider.query(+ mThumbsUri,+ new String [] { "_data" },+ null,+ null,+ null);+ if (null != c) {+ if (c.moveToFirst()) {+ do {+ String fullPathString = c.getString(0);+ existingFiles.remove(fullPathString);+ } while (c.moveToNext());+ }+ c.close();+ }+ c = mMediaProvider.query(+ mVideoThumbsUri,+ new String [] { "_data" },+ null,+ null,+ null);+ if (null != c) {+ if (c.moveToFirst()) {+ do {+ String fullPathString = c.getString(0);+ existingFiles.remove(fullPathString);+ } while (c.moveToNext());+ }+ c.close();+ }+ String strs[];+ long fileSN;+ long id0;+ long id1;+ int blockNum=MiniThumbFile.getMiniThumbDataFileBlockNum();+ final String selection_IMAGE=" "+Images.Thumbnails.IMAGE_ID+">=? AND "+Images.Thumbnails.IMAGE_ID+"<?";+ final String sortOrder_IMAGE=Images.Thumbnails.IMAGE_ID;+ final String selection_VIDEO=" "+Video.Thumbnails.VIDEO_ID+">=? AND "+Video.Thumbnails.VIDEO_ID+"<?";+ final String sortOrder_VIDEO=Video.Thumbnails.VIDEO_ID;+ HashSet<String> fileSet=new HashSet<String>(existingFiles);+ for (String fileToDelete : fileSet) {+ strs=MiniThumbFile.parseFileName(fileToDelete);+ if(strs==null||strs.length!=4)+ {+ existingFiles.remove(fileToDelete);+ continue;+ }+ if("images".equals(strs[2])||"video".equals(strs[2]))+ {+ fileSN=Long.parseLong(strs[3]);+ id0=blockNum*fileSN;+ id1=blockNum*(fileSN+1);+ final String[] selectionArgs= new String[]{id0+"",id1+""};+ if("images".equals(strs[2]))+ {++ c = mMediaProvider.query(+ mThumbsUri,+ new String [] { Images.Thumbnails.IMAGE_ID },+ selection_IMAGE,+ selectionArgs,+ sortOrder_IMAGE);+ if (null != c) {+ if (c.getCount()>0) {++ existingFiles.remove(fileToDelete);+ }+ c.close();+ }+ }+ else if ("video".equals(strs[2]))+ {+ c = mMediaProvider.query(+ mVideoThumbsUri,+ new String [] { Video.Thumbnails.VIDEO_ID },+ selection_VIDEO,+ selectionArgs,+ sortOrder_VIDEO);+ if (null != c) {+ if (c.getCount()>0) {+ existingFiles.remove(fileToDelete);+ }+ c.close();+ }+ }+ }+ else+ {+ existingFiles.remove(fileToDelete);+ }+ }+ for (String fileToDelete : existingFiles) {+ if (LOG)+ Log.v(TAG, "fileToDelete is " + fileToDelete);+ try {+ (new File(fileToDelete)).delete();+ } catch (SecurityException ex) {+ ex.printStackTrace();+ }+ }+ file=new File(directory);+ long freeDiskSpape=(file.getUsableSpace()>>20);+ /*+ * when the free Disk is very low(<100M),delete all MiniThumbFile+ */+ if(freeDiskSpape<100)+ {+ files = (file).list();+ if (files == null) {+ files = new String[0];+ }+ for (int i=0; i < files.length; i++) {+ String fullPathString = directory + "/" + files[i];+ existingFiles.add(fullPathString);+ }+ fileSet=new HashSet<String>(existingFiles);+ for (String fileToDelete : fileSet) {+ strs=MiniThumbFile.parseFileName(fileToDelete);+ if(strs==null||strs.length!=4)+ {+ existingFiles.remove(fileToDelete);+ continue;+ }+ }+ for (String fileToDelete : existingFiles) {+ if (LOG)+ Log.v(TAG, "Memeroy is very Low.delete MiniThumbFile:" + fileToDelete);+ try {+ (new File(fileToDelete)).delete();+ } catch (SecurityException ex) {+ ex.printStackTrace();+ }+ }++ }+ Log.v(TAG, "pruneDeadMiniThumbnailFiles... " + c);+ } catch (RemoteException e) {+ /* We will soon be killed...*/+ e.printStackTrace();+ }+ }+ /*robin_20120511]*/private void postscan(String[] directories) throws RemoteException {Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();@@ -1306,8 +1462,12 @@ public class MediaScannerif ((mOriginalCount == 0 || mOriginalVideoCount == 0)&& mImagesUri.equals(Images.Media.getContentUri("external"))) {pruneDeadThumbnailFiles();+ }/*[robin_20120511*/+ else if (mImagesUri.equals(Images.Media.getContentUri("external")))+ {+ pruneDeadMiniThumbnailFiles();}-+ /*robin_20120511]*//* allow GC to clean up*/mPlayLists = null;mFileCache = null;@@ -1399,7 +1559,12 @@ public class MediaScannerlong prune = System.currentTimeMillis();pruneDeadThumbnailFiles();if (LOG) Log.d(TAG, "mtkPostscan: pruneDeadThumbnailFiles takes " + (System.currentTimeMillis() - prune) + "ms.");+ }/*[robin_20120511*/+ else if (mImagesUri.equals(Images.Media.getContentUri("external")))+ {+ pruneDeadMiniThumbnailFiles();}+ /*robin_20120511]*/// allow GC to clean upmPlayLists = null;
结束!