Android画像の引き出しとキャッシュ


Anroidアプリでは、ネット上から画像を引き出す需要がよくあります.画像を引くのは簡単でやりやすいし、難しいと言っても骨が折れるので、ネット上の案が多く、オープンソースのフレームワークも少なくありませんが、具体的な実現は需要を見なければなりません.私がプロジェクトで使った2つの写真を引く案を共有します.
1.少量の画像
画像が少なければ、フレームワークを使うと冗長になり、直接ダウンロードするとより簡潔になります.
    public static boolean downloadImage(String url, String savePath) {
        LogUtil.v(TAG, "url=" + url + "; savepath=" + savePath);
        if (TextUtils.isEmpty(url) || TextUtils.isEmpty(savePath)) {
            return false;
        }
        //        ,              
        String tempPath = savePath + "_temp";
        checkFilePath(savePath);
        checkFilePath(tempPath);
        //     ,        
        HttpEntity entity = getHttpEntity(url);
        if (entity == null) {
            return false;
        }
        //       
        long imgLength = entity.getContentLength();
        long spaceLeft = SdCardSize.getUsableSpace(savePath);
        LogUtil.v(TAG, "space left =" + spaceLeft);
        if (imgLength <= 0 || spaceLeft < imgLength*3) {
            LogUtil.v(TAG, "imgLength*3=" + imgLength*3);
            return false;
        }

        try {
            InputStream stream = entity.getContent();
            LogUtil.i(TAG, "imgLength = " + imgLength);
            //  stream       ,      ,          
            //         
            if (imgLength == writeImageFile(stream, tempPath)
                    && FileUtil.renameFile(tempPath, savePath)) {
                return true;
            } else {
                LogUtil.w(TAG, "failed to write image file");
                FileUtil.deleteFile(tempPath);
            }
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

この方法はただ画像をダウンロードするだけで、ダウンロードに成功したかどうかを返すかどうか、この機能は多くないはずです.画像をダウンロードしただけなので、bitmapオブジェクトを返さず、直接使用しやすいからです.画像を直接使う必要がある場合はinputstreamを手に入れるときにBitmapFactoryを直接使うことができます.decodeStream(is)はbitmapオブジェクトを得る;
2.大量の画像
大量の画像を引き出す必要がある場合、上記の方法はあまり使いにくく、主にスレッドのパッケージ、優先度、キャッシュの処理です.ここでは必要に応じてvolleyを修正して実現する.
    // volley ImageRequest      ,volley request    listener,
    //                ,        
    public void fetchImage(final String url, final String savePath, int width, int height,
            final IFetchDataListener<Bitmap> listener) {
        ImageRequest request = new ImageRequest(url, new Listener<Bitmap>() {
            @Override
            public void onResponse(Bitmap response) {
                LogUtil.v(TAG, "fetchImage ByteCount=" + response.getByteCount());
                //      bitmap
                listener.onFetchFinished(response);
            }
        }, width, height, ScaleType.FIT_XY, Config.ARGB_8888, new ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                LogUtil.e(TAG, "fetchImage url=" + url);
                LogUtil.e(TAG, "fetchImage error=" + error);
                // volley     ,            
                ThreadManager.getPoolProxy().execute(new Runnable() {
                    @Override
                    public void run() {
                        final Bitmap bitmap = ImageFetcher.getImage(url, savePath);
                        LogUtil.v(TAG, "bitmap from imagefetcher:" + bitmap);
                        //          UI  
                        new InnerHandle().post(new Runnable() {
                            @Override
                            public void run() {
                                listener.onFetchFinished(bitmap);
                            }
                        });
                    }
                });
            }
        });
        mQueue.add(request);
    }

volleyを使用するのは、requestやメッセージの管理メカニズムが優れており、自分で実現するのは難しいため、volleyを直接採用しています.volley引き抜きに失敗した後にもう一度土の方法で引き直すのは実際の状況のためで、volley引き抜きの時はずっと404の間違いを返していたが、この時土の方法は毎回引き抜きに成功し、両者を結合した.
    public static Bitmap getImage(String url, String savePath) {
        LogUtil.v(TAG, "url=" + url + ";savePath=" + savePath);
        if (TextUtils.isEmpty(savePath)) {
            return null;
        }
        //          
        Bitmap bitmap = getImageByPath(savePath);
        if (bitmap != null) {
            return bitmap;
        }
        //      
        if (downloadImage(url, savePath)) {
            return getImageByPath(savePath);
        }
        return null;
    }

volleyディスクキャッシュスキームvolleyを変更すると、キャッシュをディスクに書き込むときにデータに付属する情報も同じファイルに書き込まれます.これにより、volleyがディスクにキャッシュしたファイルを直接使用することはできません.私の最初の方法から分かるように、私は直接画像ファイルを使用する必要があります.そこでvolleyのディスクキャッシュDiskBasedCacheを変更しました.
    public synchronized void put(String key, Entry entry) {
        pruneIfNeeded(entry.data.length);
        File file = getFileForKey(key);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            CacheHeader e = new CacheHeader(key, entry);
            //           ,                   
            // boolean success = e.writeHeader(fos);
            // if (!success) {
            // fos.close();
            // VolleyLog.d("Failed to write header for %s",
            // file.getAbsolutePath());
            // throw new IOException();
            // }
            fos.write(entry.data);
            fos.close();
            putEntry(key, e);
            return;
        } catch (IOException e) {
        }
        boolean deleted = file.delete();
        if (!deleted) {
            VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
        }
    }

同様に、getメソッドを変更する必要があります.そうしないと、volley自身はキャッシュファイルを使用できません.
    public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get(key);
        // if the entry does not exist, return.
        if (entry == null) {
            return null;
        }
        VolleyLog.v("getEntry key=" + key);
        File file = getFileForKey(key);
        CountingInputStream cis = null;
        try {
            cis = new CountingInputStream(new BufferedInputStream(
                    new FileInputStream(file)));
                    //           ,  volley            
// CacheHeader.readHeader(cis); // eat header
// byte[] data = streamToBytes(cis, (int) (file.length()-cis.bytesRead));
            byte[] data = streamToBytes(cis, (int) file.length());
            return entry.toCacheEntry(data);
        } catch (IOException e) {
            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
            remove(key);
            return null;
        } catch (NegativeArraySizeException e) {
            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
            remove(key);
            return null;
        } finally {
            if (cis != null) {
                try {
                    cis.close();
                } catch (IOException ioe) {
                    return null;
                }
            }
        }
    }

以上の修正は使えますが、アプリケーションを再起動した後、キャッシュが役に立たないことに気づきました.volleyが起動したときにキャッシュ、つまりinitialize()が初期になります.volleyのやり方はキャッシュディレクトリの下のすべてのファイルのヘッダ情報を読み取ることでキャッシュを初期化しますが、私はさっきキャッシュファイルのヘッダ情報を削除しましたので、ヘッダ情報を別の方法で記録しなければなりません.volleyの初期化に便利です.
    public synchronized void initialize() {
        if (!mRootDirectory.exists()) {
            if (!mRootDirectory.mkdirs()) {
                VolleyLog.e("Unable to create cache dir %s",
                        mRootDirectory.getAbsolutePath());
            }
            return;
        }
        // mEntries   Gson   mEntryMapJsonPath   json  ,
        //           gson   Map<String, CacheHeader>  
        StringBuilder str = FileUtil.readFile(mEntryMapJsonPath);
        if (TextUtils.isEmpty(str)) {
            return;
        }
        try {
            Map<String, CacheHeader> entries = new Gson().fromJson(str.toString(),
                    new TypeToken<Map<String, CacheHeader>>(){}.getType());
            mEntries.putAll(entries);
            VolleyLog.v("entries=" + entries);
        } catch (Exception e) {
        }
// File[] files = mRootDirectory.listFiles();
// if (files == null) {
// return;
// }
// for (File file : files) {
// BufferedInputStream fis = null;
// try {
// fis = new BufferedInputStream(new FileInputStream(file));
// CacheHeader entry = CacheHeader.readHeader(fis);
// entry.size = file.length();
// putEntry(entry.key, entry);
// } catch (IOException e) {
// if (file != null) {
// file.delete();
// }
// } finally {
// try {
// if (fis != null) {
// fis.close();
// }
// } catch (IOException ignored) {
// }
// }
// }
    }

これを実現するには,キャッシュの追加と削除時にjsonファイルを更新する必要がある.
    private void putEntry(String key, CacheHeader entry) {
        if (!mEntries.containsKey(key)) {
            mTotalSize += entry.size;
        } else {
            CacheHeader oldEntry = mEntries.get(key);
            mTotalSize += (entry.size - oldEntry.size);
        }
        mEntries.put(key, entry);
        saveCachedJsonToFile();
    }

    private void removeEntry(String key) {
        CacheHeader entry = mEntries.get(key);
        if (entry != null) {
            mTotalSize -= entry.size;
            mEntries.remove(key);
        }
        saveCachedJsonToFile();
    }

    private void saveCachedJsonToFile() {
        synchronized (mEntries) {
            String json = new Gson().toJson(mEntries);
            FileUtil.writeFileWithBackUp(mEntryMapJsonPath, json, false);
        }
    }

ここまででvolleyの修正が完了...
画像3レベルキャッシュ
    public Bitmap getBitmapFromCache(String key) {
        checkCacheParams();
        Bitmap bitmap = null;
        //  lrucache,        
        synchronized (mLruCache) {
            bitmap = mLruCache.remove(key);
            if (bitmap != null) {
                // found,move the file to the last of LinkedHashMap
                mLruCache.put(key, bitmap);
                LogUtil.v(TAG, "getBitmapFrom mLruCache");
                return bitmap;
            } else {
                LogUtil.w(TAG, "getBitmapFrom mLruCache failed");
            }
        }

        //         
        synchronized (mSoftCache) {
            SoftReference<Bitmap> bitmapReference = mSoftCache.remove(key);
            if (bitmapReference != null) {
                bitmap = bitmapReference.get();
                if (bitmap != null) {
                    // if hit, move bm to lrucache
                    addBitmapToCache(key, bitmap);
                    LogUtil.v(TAG, "getBitmapFrom mSoftCache");
                    return bitmap;
                } else {
                    LogUtil.w(TAG, "getBitmapFrom mSoftCache failed");
                }
            } else {
                LogUtil.w(TAG, "getBitmapFrom mSoftCache failed");
            }
        }
        //          
        synchronized (mFileCache) {
            bitmap = mFileCache.getImageByURL(key);
            if (bitmap != null) {
                addBitmapToCache(key, bitmap);
                LogUtil.v(TAG, "getBitmapFrom mFileCache");
                return bitmap;
            } else {
                LogUtil.w(TAG, "getBitmapFrom mFileCache failed");
            }
        }
        return null;
    }

これが伝説の3級キャッシュで、ほほほ!
初期化はソース紫です.
    public void initCacheParams(String cacheDir, int maxCacheByteSize) {
        mFileCache = new ImageFileCache(cacheDir);
        mLruCache = new LruCache<String, Bitmap>(maxCacheByteSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                if (value != null) {
                    return value.getRowBytes() * value.getHeight();
                } else {
                    return 0;
                }
            }
        //                  lru   
            @Override
            protected void entryRemoved(boolean evicted, String key,
                    Bitmap oldValue, Bitmap newValue) {
                if (oldValue != null && mSoftCache != null) {
                    //             
                    mSoftCache.put(key, new SoftReference<Bitmap>(oldValue));
                }
            }
        };

        mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(
                SOFT_CACHE_SIZE, 0.75f, true) {
            private static final long serialVersionUID = 6040103833179403725L;
        //               ,        ,             
            @Override
            protected boolean removeEldestEntry(
                    Entry<String, SoftReference<Bitmap>> eldest) {
                if (size() > SOFT_CACHE_SIZE) {
                    return true;
                }
                return false;
            }
        };

    }

キャッシュを追加するのは簡単です.
    public void addBitmapToCache(String key, Bitmap bitmap) {
        checkCacheParams();
        if (bitmap != null) {
            synchronized (mLruCache) {
                mLruCache.put(key, bitmap);
            }
        }
    }

キャッシュをlruに加えるだけで,キャッシュが増加すると以前のキャッシュは2次キャッシュ(ソフトリンク)に除去されるが,ソフトリンクから除去された後は3次キャッシュはない.ここでは3段目のキャッシュはありません.ネット上の多くの実装はここで3段目を追加し、bitmapをファイルに書き込むことです.構想的には問題なく、bitmapをキャッシュファイルに書き込む際に問題が発生しやすく、bitmapをjpgまたはpng形式で圧縮してファイルを書く実装が多い.次にキャッシュを読み込むときにbitmapにキャッシュファイルをロードすると、何度もループすると、キャッシュファイルはますます大きくなり、画像がますますはっきりしなくなります.なぜなら、bitmapがjpgやpngに移行すると圧縮が損なわれるからです.そう言えば、キャッシュファイルは毎回書く必要はありません.最初に元のファイルに書き込むだけで、後でキャッシュを取るたびに元のファイルを直接読むだけでOKです.しかし、キャッシュファイルをどのように管理するかという新しい問題も導入されます.キャッシュファイルはずっと増加できないので、これはまだ簡単な方法を見つけていません.後で補充しましょう.