自定義圖檔加載器
前言
圖檔加載器是一個非常常用的功能子產品,但是一般我們不會去從零開始自己寫一個,因為有Glide、Picasso、Fresco等一些優秀的開源庫,或者公司自己維護了一套。再者完整實作這一整套功能是很耗費精力的。而這篇文章僅僅是為了學習程式的設計模式和LruCache、Bitmap優化顯示等,去實作這樣的一個庫。
先說下面向對象六大原則及在此項目裡的展現:
1. 單一原則
簡單來說,一個類中,應該是一組相關性很高的函數、資料的封裝。具體根據類的職責。
2. 開閉原則
提取共同的函數為一個接口,通過設定接口對象或者實作該接口的對象,實作不同的緩存政策。
3. 裡氏替換原則
在這裡展現的就是隻要實作IImageCache接口的都可以設定到緩存去。
4. 依賴倒置原則
子產品間依賴通過抽象發生,實作類不直接發生依賴關系,其依賴是通過抽象類和接口。通過這樣減少耦合。
5. 接口隔離原則
fileOutputStream等其他流的關閉,這裡隻要是實作Closeable接口的都可以關閉close();隻要知道類實作了closeable,就可關閉,其他的一概不關心,這就是接口隔離。
6. 迪米特原則
ImageCache裡面使用了DiskLruCache,但是使用者不需要知道實作細節,隻需要和ImageCache打交道。即使裡面的DiskLruCache替換為其他的緩存實作,使用者也不會感覺到。
下面開始分析
ImageLoader:負責下載下傳圖檔和加載的類.
ImageCache:含有LruCache和DiskLruCache.
ImageResizer:擷取特定采樣的bitmap.
IImageCacahe:抽象ImageCache的接口,之前說的六大原則,很重要的一點是強調抽象.
ImageCache裡面使用了LruCache和DiskLruCache:
lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / ;
}
};
try {
diskLruCache = DiskLruCache.open(diskCacheDir, , , DISK_CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
這裡緩存大小cacheSize設定為目前程序可用記憶體的0.25倍.sizeOf完成對Bitmap對象大小的計算。
lruCache緩存和讀取:
lruCache.put(key, bitmap), Bitmap bitmap = lruCache.get(key);
DiskLruCache緩存和讀取:
public void addToDiskCache(Bitmap bitmap, String key) throws IOException {
DiskLruCache.Editor editor = diskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (addToDisk(bitmap, outputStream)) {
editor.commit();
} else {
editor.abort();
}
diskLruCache.flush();
outputStream.close();
}
}
try {
DiskLruCache.Snapshot snapShot = diskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, width, height);
if (bitmap != null)
addToCache(bitmap, url);
return bitmap;
}
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "getDiskLruCache error");
}
BitmapFactory.decodeStream(in),不能decode兩次。是以用ImageResizer.decodeSampledBitmapFromFileDescriptor根據ImageView大小擷取對應采樣率的Bitmap(因為ImageView小,而圖檔的分辨率大時,加載原圖是很浪費記憶體的)。主要是計算options.inSampleSize:
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
ImageLoader裡面先從ImageCache中讀取,若是沒有,者從網絡下載下傳,但是第一次下載下傳擷取的Bitmap不能直接設定到ImageView中去。
private IImageCacahe cache;
private Bitmap downloadBitmapFromUrl(String urlString, int width, int height) {
Bitmap bitmap = null;
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(in);
if (cache instanceof ImageCache)
((ImageCache) cache).addToDiskCache(bitmap, Utils.keyFormUrl(urlString));
bitmap.recycle();
bitmap = cache.getFromCache(urlString, width, height);// TODO: 2017/4/4 這裡因為第一次下載下傳的原圖需要重新采樣擷取需求大小的bitmap,而BitmapFactory.decodeStream(in),不能decode兩次。
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap: " + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
Utils.close(in);
}
return bitmap;
}
根據imageview的大小,通過BitmapFactory.Options options,,options.inSampleSize,計算需要的inSampleSize壓縮得到bitmap再加載到imageview上去。
若未采樣優化,加載一屏6張圖檔原圖,總20張,上下滑動時,記憶體占用情況如下:
優化後記憶體占用情況如下 :這裡隻是指定了下需要的bitmap顯示的大小(機關為像素,不同的裝置上相同的dp顯示的像素不一緻,是以大小也會不同)。根據需要的大小及其FileDescriptor,從其DiskLruCache擷取bitmap,再顯示到ImageVIew中去。
對比記憶體占用差別相當大,但是顯示的效果一緻并沒有打折扣。
還有需要解決的一個問題是ImageView的複用,例如在Listview中加載圖檔,如果需要加載的圖檔使用者已經劃過去了,那麼應該忽略這張圖檔,具體的就是在 target.setTag(TAG_KEY_URI, url),線程池的任務中 handler.obtainMessage(SUCCESS_COMPLETE, new LoaderResult(target, url, bitmap)).sendToTarget();然後在handler的handleMessage中
imageView.getTag(TAG_KEY_URI);對比url是否一緻,不一緻則忽略該圖檔。
最後順便接入了leakcanary,發現記憶體洩漏:華為mate8上Android6.0的HwPhoneWindow的mContext持有MainActivity對象,導緻洩漏;紅米note2上Android5.0:未發現有記憶體洩漏。這裡是華為的系統問題,不知有沒有大神能指點這該如何解決,感激不盡。
項目源碼位址:https://github.com/Ulez/UImageLoader
參考資料:
1.Android開發藝術探索
2.Android源碼設計模式解析與實戰