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 code mBuffer.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 MediaScanner c.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 MediaScanner e.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 MediaScanner if ((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 MediaScanner long 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 up mPlayLists = null;
结束!