2012年8月28日火曜日

Android Bitmap 読み込みのコードを github に公開しました。

大きい画像を効果的に読み込む
バックグラウンドで Bitmap を処理する
Bitmap をキャッシュする

をまとめて、DiskLruCache として Displaying Bitmaps Efficiently のサンプル BitmapFun.zip の DiskLruCache.java をベースにしたコードを github に公開しました。Google I/O 2012 のコードも参考にしています。

github - yanzm/ImageLoadLib -

特徴は API Level 4 から使えるようにしてあることです(BitmapFun や Google I/O 2012 のコードはそうなっていません)。

こんな感じで使います。

  1. private ImageFetcher mImageFetcher;  
  2.   
  3. @Override  
  4. public void onCreate(Bundle savedInstanceState) {  
  5.     super.onCreate(savedInstanceState);  
  6.     mImageFetcher = Utils.getImageFetcher(getActivity());  
  7. }  
  8.   
  9. private void loadImage(ImageView iv, String imageUrl) {  
  10.     mImageFetcher.loadImage(imageUrl, iv, R.drawable.loading);  
  11. }  



BitmapFun の DiskLruCache は Google I/O 2012 や Androdi 4.0 のソースコードに含まれている DiskLruCache よりかなり単純化されています。 なので読めばわかると思いますが、少しだけ解説します。

内部で行っている処理は

1. キャッシュディレクトリの決定(外部ストレージが使えるなら、外部ストレージを、そうでないなら内部ストレージを使う)
  1. public static File getDiskCacheDir(Context context, String uniqueName) {  
  2.   
  3.     final String cachePath =   
  4.         Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED  
  5.         || !isExternalStorageRemovable() ?   
  6.             getExternalCacheDir(context).getPath() :   
  7.             context.getCacheDir().getPath();  
  8.   
  9.     return new File(cachePath + File.separator + uniqueName);  
  10. }  
  11.   
  12. @TargetApi(Build.VERSION_CODES.GINGERBREAD)  
  13. private static boolean isExternalStorageRemovable() {  
  14.     if (Utils.hasGingerbread()) {  
  15.         return Environment.isExternalStorageRemovable();  
  16.     }  
  17.     return true;  
  18. }  
  19.   
  20. @TargetApi(Build.VERSION_CODES.FROYO)  
  21. private static File getExternalCacheDir(Context context) {  
  22.     if (Utils.hasFroyo()) {  
  23.         File cacheDir = context.getExternalCacheDir();  
  24.         if (cacheDir != null) {  
  25.             return cacheDir;  
  26.         }  
  27.     }  
  28.   
  29.     // Froyo 以前は自前でディレクトリを作成する  
  30.     final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";  
  31.     return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);  
  32. }  


2. キャッシュディレクトリに作成(なければ)とサイズチェック
  1. public static DiskLruCache openCache(Context context, File cacheDir, long maxByteSize) {  
  2.     if (!cacheDir.exists()) {  
  3.         cacheDir.mkdir();  
  4.     }  
  5.   
  6.     if (cacheDir.isDirectory() && cacheDir.canWrite() && getUsableSpace(cacheDir) > maxByteSize) {  
  7.         return new DiskLruCache(cacheDir, maxByteSize);  
  8.     }  
  9.   
  10.     return null;  
  11. }  
  12.   
  13. @TargetApi(Build.VERSION_CODES.GINGERBREAD)  
  14. private static long getUsableSpace(File path) {  
  15.     if (Utils.hasGingerbread()) {  
  16.         return path.getUsableSpace();  
  17.     }  
  18.     final StatFs stats = new StatFs(path.getPath());  
  19.     return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();  
  20. }  
  21.   
  22. private DiskLruCache(File cacheDir, long maxByteSize) {  
  23.     mCacheDir = cacheDir;  
  24.     maxCacheByteSize = maxByteSize;  
  25. }  


3. ディスクキャッシュに Bitmap を追加(ファイルとして保存)
  1. private final Map<String, String> mLinkedHashMap = Collections.synchronizedMap(new LinkedHashMap<String, String>(  
  2.         INITIAL_CAPACITY, LOAD_FACTOR, true));  
  3.   
  4. public void put(String key, Bitmap data) {  
  5.     synchronized (mLinkedHashMap) {  
  6.         if (mLinkedHashMap.get(key) == null) {  
  7.             try {  
  8.                 final String file = createFilePath(mCacheDir, key);  
  9.                 if (writeBitmapToFile(data, file)) {  
  10.                     put(key, file);  
  11.                     flushCache();  
  12.                 }  
  13.             } catch (final FileNotFoundException e) {  
  14.                 Log.e(TAG, "Error in put: " + e.getMessage());  
  15.             } catch (final IOException e) {  
  16.                 Log.e(TAG, "Error in put: " + e.getMessage());  
  17.             }  
  18.         }  
  19.     }  
  20. }  
  21.   
  22. public static String createFilePath(File cacheDir, String key) {  
  23.     return cacheDir.getAbsolutePath() + File.separator + CACHE_FILENAME_PREFIX + key;  
  24. }  
  25.   
  26. private boolean writeBitmapToFile(Bitmap bitmap, String file) throws IOException, FileNotFoundException {  
  27.   
  28.     OutputStream out = null;  
  29.     try {  
  30.         out = new BufferedOutputStream(new FileOutputStream(file), IO_BUFFER_SIZE);  
  31.         return bitmap.compress(mCompressFormat, mCompressQuality, out);  
  32.     } finally {  
  33.         if (out != null) {  
  34.             out.close();  
  35.         }  
  36.     }  
  37. }  


4. ハッシュマップに追加し、最大キャッシュサイズを超えていたら、超えなくなるまで最後に参照したファイルから順番に削除
  1. private void put(String key, String file) {  
  2.     mLinkedHashMap.put(key, file);  
  3.     cacheSize = mLinkedHashMap.size();  
  4.     cacheByteSize += new File(file).length();  
  5. }  
  6.   
  7. private void flushCache() {  
  8.     Entry>String, String> eldestEntry;  
  9.     File eldestFile;  
  10.     long eldestFileSize;  
  11.     int count = 0;  
  12.   
  13.     while (count < MAX_REMOVALS && (cacheSize > maxCacheItemSize || cacheByteSize > maxCacheByteSize)) {  
  14.         eldestEntry = mLinkedHashMap.entrySet().iterator().next();  
  15.         eldestFile = new File(eldestEntry.getValue());  
  16.         eldestFileSize = eldestFile.length();  
  17.         mLinkedHashMap.remove(eldestEntry.getKey());  
  18.         eldestFile.delete();  
  19.         cacheSize = mLinkedHashMap.size();  
  20.         cacheByteSize -= eldestFileSize;  
  21.         count++;  
  22.         if (BuildConfig.DEBUG) {  
  23.             Log.d(TAG, "flushCache - Removed cache file, " + eldestFile + ", " + eldestFileSize);  
  24.         }  
  25.     }  
  26. }  


5. ディスクキャッシュから Bitmap を取得
  1. public Bitmap get(String key) {  
  2.     synchronized (mLinkedHashMap) {  
  3.         final String file = mLinkedHashMap.get(key);  
  4.         if (file != null) {  
  5.             if (BuildConfig.DEBUG) {  
  6.                 Log.d(TAG, "Disk cache hit");  
  7.             }  
  8.             return BitmapFactory.decodeFile(file);  
  9.         } else {  
  10.             final String existingFile = createFilePath(mCacheDir, key);  
  11.             if (new File(existingFile).exists()) {  
  12.                 put(key, existingFile);  
  13.                 if (BuildConfig.DEBUG) {  
  14.                     Log.d(TAG, "Disk cache hit (existing file)");  
  15.                 }  
  16.                 return BitmapFactory.decodeFile(existingFile);  
  17.             }  
  18.         }  
  19.         return null;  
  20.     }  
  21. }  



Android 4.0 のソースコードの DiskLruCache の解説はまぁ、そのうち、、、、



1 件のコメント:

  1. お世話になっております。宮崎です。

    DiskLruCacheを利用させていただいております。
    その際にソースコードを見ておりました際に気になる点を確認させてください。

    DiskLruCache.openCacheでインスタンスを生成後にたまった
    mLinkedHashMapの内容はサイズオーバー・ファイル数オーバー時にデータを削除してくれるようですが、

    新たにopenCacheすると別にインスタンスになり、mLinkedHashMap自体には何もないので、そのインスタンス内で保存した画像のみ削除が可能で、別インスタンスで生成した画像は削除してくれないという認識でよいでしょうか?

    あくまで起動したインスタンスを参照できる範囲でのDiskLruであり、アプリケーション全体で使いまわす場合は例えばApplicationクラス等でstatic宣言(もしくシングルトンな設計)にしなければいけないという認識でよいでしょうか?

    お忙しい中申し訳ありませんが、もしお時間がありましたらご確認頂ければ幸いです。

    返信削除