原生Android缩略图填满SD卡的问题

本人博客原文

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 . java
      modified : 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;


 结束!

你可能感兴趣的:(android,String,File,null,Class,Path)