目前Android 發(fā)展至今優(yōu)秀的圖片加載框架太多,例如: Volley ,Picasso,Imageloader,Glide等等。但是作為程序猿,懂得其中的實(shí)現(xiàn)原理還是相當(dāng)重要的,只有懂得才能更好地使用。于是乎,今天我就簡(jiǎn)單設(shè)計(jì)一個(gè)網(wǎng)絡(luò)加載圖片框架。主要就是熟悉圖片的網(wǎng)絡(luò)加載機(jī)制。
一般來說,一個(gè)優(yōu)秀的 圖片加載框架(ImageLoader) 應(yīng)該具備如下功能:
- 圖片壓縮
- 內(nèi)存緩存
- 磁盤緩存
- 圖片的同步加載
- 圖片的異步加載
- 網(wǎng)絡(luò)拉取
那我們就從以上幾個(gè)方面進(jìn)行介紹:
1.圖片壓縮(有效的降低OOM的發(fā)生概率)
圖片壓縮功能我在Bitmap 的高效加載中已經(jīng)做了介紹這里不多說直接上代碼。這里直接抽象一個(gè)類用于完成圖片壓縮功能。
public class ImageResizer {
private static final String TAG = "ImageResizer";
public ImageResizer() {
super();
// TODO Auto-generated constructor stub
}
public Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromBitmapFileDescriptor(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);
}
public int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
final int width = options.outWidth;
final int height = options.outHeight;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > halfWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
2.內(nèi)存緩存和磁盤緩存
緩存直接選擇 LruCache 和 DiskLruCache 來完成內(nèi)存緩存和磁盤緩存工作。
首先對(duì)其初始化:
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
public ImageLoader(Context context) {
mContext = context.getApplicationContext();
//分配內(nèi)存緩存為當(dāng)前進(jìn)程的1/8,磁盤緩存容量為50M
int maxMemory = (int) (Runtime.getRuntime().maxMemory() * 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
File diskCacheDir = getDiskChaheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
創(chuàng)建完畢后,接下來則需要提供方法來視線添加以及獲取的功能。首先來看內(nèi)存緩存。
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
相對(duì)來說內(nèi)存緩存比較簡(jiǎn)單,而磁盤緩存則復(fù)雜的多。磁盤緩存(LruDiskCache)并沒有直接提供方法來實(shí)現(xiàn),而是要通過Editor以及Snapshot 來實(shí)現(xiàn)對(duì)于文件系統(tǒng)的添加以及讀取的操作。
首先看一下,Editor,它提供了commit 和 abort 方法來提交和撤銷對(duì)文件系統(tǒng)的寫操作。
//將下載的圖片寫入文件系統(tǒng),實(shí)現(xiàn)磁盤緩存
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight)
throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
if (mDiskLruCache == null)
return null;
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor
.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
mDiskLruCache.flush();
return loadBitmapForDiskCache(url, reqWidth, reqHeight);
}
Snapshot, 通過它可以獲取磁盤緩存對(duì)象對(duì)應(yīng)的 FileInputStream,但是FileInputStream 無法便捷的進(jìn)行壓縮,所以通過FileDescriptor 來加載壓縮后的圖片,最后將加載后的bitmap添加到內(nèi)存緩存中。
public Bitmap loadBitmapForDiskCache(String url, int reqWidth, int reqHeight)
throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread , it's not recommended");
}
if (mDiskLruCache == null)
return null;
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
FileInputStream fileInputStream = (FileInputStream) snapshot
.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromBitmapFileDescriptor(
fileDescriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
3.同步加載
同步加載的方法需要外部在子線程中調(diào)用。
//同步加載
public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmpaFromMemCache(uri);
if (bitmap != null) {
return bitmap;
}
try {
bitmap = loadBitmapForDiskCache(uri, reqWidth, reqHeight);
if (bitmap != null) {
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && !mIsDiskLruCacheCreated) {
bitmap = downloadBitmapFromUrl(uri);
}
return bitmap;
}
從方法中可以看出工作過程遵循如下幾步:
首先嘗試從內(nèi)存緩存中讀取圖片,接著嘗試從磁盤緩存中讀取圖片,最后才會(huì)從網(wǎng)絡(luò)中拉取。此方法不能再主線程中執(zhí)行,執(zhí)行環(huán)境的檢測(cè)是在loadBitmapFromHttp中實(shí)現(xiàn)的。
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
4.異步加載
//異步加載
public void bindBitmap(final String uri, final ImageView imageView,
final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI, uri);
Bitmap bitmap = loadBitmpaFromMemCache(uri);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
if (bitmap != null) {
LoaderResult result = new LoaderResult(imageView, uri,
bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result)
.sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
從bindBitmap的實(shí)現(xiàn)來看,bindBitmap 方法會(huì)嘗試從內(nèi)存緩存中讀取圖片,如果讀取成功就直接返回結(jié)果,否則會(huì)在線程池中去調(diào)用loadBitmap方法,當(dāng)圖片加載成功后再將圖片、圖片的地址以及需要綁定的imageView封裝成一個(gè)LoaderResult對(duì)象,然后再通過mMainHandler向主線程發(fā)送一個(gè)消息,這樣就可以在主線程中給imageView設(shè)置圖片了。
下面來看一下,bindBitmap這個(gè)方法中用到的線程池和Handler,首先看一下線程池 THREAD_POOL_EXECUTOR 的實(shí)現(xiàn)。
private static final int CPU_COUNT = Runtime.getRuntime()
.availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
// TODO Auto-generated method stub
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(), sThreadFactory);
1.使用線程池和handler的原因。
首先不能用普通線程去實(shí)現(xiàn),如果采用普通線程去加載圖片,隨著列表的滑動(dòng)可能會(huì)產(chǎn)生大量的線程,這樣不利于效率的提升。 Handler 的實(shí)現(xiàn) ,直接采用了 主線程的Looper來構(gòu)造Handler 對(duì)象,這就使得 ImageLoader 可以在非主線程構(gòu)造。另外為了解決由于View復(fù)用所導(dǎo)致的列表錯(cuò)位這一問題再給ImageView 設(shè)置圖片之前會(huì)檢查他的url有沒有發(fā)生改變,如果發(fā)生改變就不再給它設(shè)置圖片,這樣就解決了列表錯(cuò)位問題。
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
imageView.setImageBitmap(result.bitmap);
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(result.uri)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "set image bitmap,but url has changed , ignored!");
}
}
};
總結(jié):
圖片加載的問題 ,尤其是大量圖片的加載,對(duì)于android 開發(fā)者來說一直是比較困擾的問題。本文只是提到了最基礎(chǔ)的一種解決方法,用于學(xué)習(xí)還是不錯(cuò)的。
最后結(jié)尾不多說,直接上demo:
自定義圖片加載框架--運(yùn)用MVP+Retrofit+Rxjava的應(yīng)用架構(gòu)