Android 框架-ImageLoader 圖片載入框架
Android 開發中我們會經常會載入網路圖片的需求,目前成熟的圖片載入框架有 Fresco、Glide、Picasso, 比較老的還有UniversaclImageLoader(15年開發的時候還在用這個開源庫,可惜現在已停止維護了)。由於最近想分析下Glide 的原始碼,之前有沒有分析過圖片載入框架,所以就自己參考網上簡單的圖片載入框架自己寫了一個ImageLoader,分析下圖片載入的基本原理和流程,為以後分析更復雜框架打下基礎。
ImageLoader 基本框架
整個框架還是有點複雜,我們從底層往上層開始分析 。
第一部分 快取
為了提高使用者體驗,提升圖片的載入速度, 我們一般會將下載的圖片快取起來,從快取的位置來分,可以分為記憶體快取和本地快取(一般是Data或者Cache 分割槽快取)。
快取介面
public interface BitmapCache {
Bitmap get(Target key);
void put(Target key, Bitmap value);
void remove(Target key);
}
為了可擴充套件,我們定義了BitmapCache介面, 該介面中只定義了三個方法, 分別是 獲取、新增、移除等,該介面中有個名字為Target 的類,該類表示一張圖片的請求,類中包含的屬性有Image、url、resourceId等,我們主要圖圖片的url 做為key。
記憶體快取
記憶體快取實現了快取介面,記憶體快取將解碼後的Bitmap儲存到Map中,下次直接從記憶體中載入,這樣大大提升了圖片重複載入的速度, 我們採用了
程式碼如下:
public class MemoryCache implements BitmapCache {
private LruCache<String, Bitmap> mMemoryCache;
public MemoryCache() {
final int totalMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
int cacheSize = totalMemory / 4;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
@Override
public synchronized Bitmap get(Target key) {
return mMemoryCache.get(key.imgUri);
}
@Override
public synchronized void put(Target key, Bitmap bitmap) {
mMemoryCache.put(key.imgUri, bitmap);
}
@Override
public synchronized void remove(Target key) {
mMemoryCache.remove(key.imgUri);
}
}
本地快取
如果只快取到記憶體的話,下次啟動應用還需要從伺服器拉圖片,這樣即費流量使用者體驗又差,一次我們增加了本地快取,這裡我們採用了DiskLruCache類,同樣該類也實現了快取介面
程式碼如下
public class DiskCache implements BitmapCache {
private static final String DISK_IMG_PATH = "disk_cache_path";
private DiskLruCache mDiskLruCache;
private static DiskCache sDiskCache;
private DiskCache(Context context) {
initial(context);
}
public static DiskCache getInstance(Context context) {
if (sDiskCache == null) {
synchronized (DiskCache.class) {
if (sDiskCache == null) {
sDiskCache = new DiskCache(context);
}
}
}
return sDiskCache;
}
private void initial(Context context) {
try {
File cacheDir = getDiskCacheDir(context, DISK_IMG_PATH);
if (!cacheDir.exists()) {
boolean success = cacheDir.mkdir();
}
int versionCode = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
mDiskLruCache = DiskLruCache.open(cacheDir, versionCode, 1, 50 * 1024 * 1024);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
private File getDiskCacheDir(Context context, String uniqueName) {
String cachePath = null;
try {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
} catch (Exception e) {
e.printStackTrace();
}
return new File(cachePath + File.separator + uniqueName);
}
@Override
public Bitmap get(final Target request) {
BitmapDecoder decoder = new BitmapDecoder() {
@Override
public Bitmap decodeBitmapWithOption(BitmapFactory.Options options) {
final InputStream inputStream = getInputStream(request.imgUriMd5);
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
};
return decoder.decodeBitmap(0, 0);
}
@Override
public void put(Target target, Bitmap value) {
if (target.justCacheInMem) {
return;
}
DiskLruCache.Editor editor = null;
try {
editor = mDiskLruCache.edit(target.imgUriMd5);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (writeBitmap2Disk(value, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void remove(Target key) {
try {
mDiskLruCache.remove(Md5Helper.toMD5(key.imgUriMd5));
} catch (IOException e) {
e.printStackTrace();
}
}
private boolean writeBitmap2Disk(Bitmap bitmap, OutputStream outputStream) {
BufferedOutputStream bos = new BufferedOutputStream(outputStream, 8 * 1024);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
boolean result = true;
try {
bos.flush();
} catch (IOException e) {
e.printStackTrace();
result = false;
} finally {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
private InputStream getInputStream (String md5) {
DiskLruCache.Snapshot snapshot;
try {
snapshot = mDiskLruCache.get(md5);
if (snapshot != null) {
return snapshot.getInputStream(0);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
二級快取
就是記憶體和本地同時快取 程式碼如下:
public class DoubleCache implements BitmapCache {
private DiskCache mDiskCache;
private MemoryCache mMemoryCache = new MemoryCache();
public DoubleCache(Context context) {
mDiskCache = DiskCache.getInstance(context);
}
@Override
public synchronized Bitmap get(Target key) {
Bitmap bitmap = mMemoryCache.get(key);
if (bitmap == null) {
bitmap = mDiskCache.get(key);
if (bitmap != null) {
mMemoryCache.put(key, bitmap);
}
}
return bitmap;
}
@Override
public synchronized void put(Target key, Bitmap value) {
mDiskCache.put(key, value);
mMemoryCache.put(key, value);
}
@Override
public synchronized void remove(Target key) {
mDiskCache.remove(key);
mMemoryCache.remove(key);
}
}
以上是圖片快取部分接下來是圖片載入部分
圖片的載入
圖片載入分為兩大類,一類是載入網路圖片,一類是載入本地圖片(Sd卡、Data 分割槽、Asset中的等)
圖片載入Factory
根據不同的uri 產生不同不同的Loder,程式碼如下:
public class LoaderFactory {
private static LoaderFactory INSTANCE;
public static final String HTTP = "http";
public static final String FILE = "file";
private BaseLoader mNullBaseLoader = new NullLoader();
private static Map<String, BaseLoader> mLoaderMap = new HashMap<>();
static {
mLoaderMap.put(HTTP, new UrlLoader());
mLoaderMap.put(FILE, new NativeLoader());
}
private LoaderFactory() {
}
public static LoaderFactory getInstance() {
if (INSTANCE == null) {
synchronized (LoaderFactory.class) {
if (INSTANCE == null) {
INSTANCE = new LoaderFactory();
}
}
}
return INSTANCE;
}
public BaseLoader getLoader(String schema) {
if (mLoaderMap.containsKey(schema)) {
return mLoaderMap.get(schema);
}
return mNullBaseLoader;
}
}
網路圖片
下載網路圖片然後Decoder 成Bitmap, 程式碼如下
public class UrlLoader extends Loader {
@Override
protected Bitmap onLoadBitmap(Target request) {
final String imageUrl = request.imgUri;
InputStream inputStream = null;
Bitmap bitmap = null;
HttpURLConnection connection = null;
try {
URL url = new URL(imageUrl);
connection = (HttpURLConnection) url.openConnection();
inputStream = new BufferedInputStream(connection.getInputStream());
bitmap = BitmapFactory.decodeStream(inputStream, null, null);
} catch (Exception e) {
e.printStackTrace();
} finally {
IOUtil.closeQuietly(inputStream);
if (connection != null) {
connection.disconnect();
}
}
return bitmap;
}
}
本地圖片
public class NativeLoader extends AbsLoader {
@Override
protected Bitmap onLoadBitmap(Target request) {
final String imagePath = Uri.parse(request.imgUri).getPath();
File imgFile = new File(imagePath);
if (!imgFile.exists()) {
return null;
}
request.justCacheInMem = true;
BitmapDecoder decoder = new BitmapDecoder() {
@Override
public Bitmap decodeBitmapWithOption(BitmapFactory.Options options) {
return BitmapFactory.decodeFile(imagePath, options);
}
};
return decoder.decodeBitmap(request.getImageWidth(), request.getImageHeight());
}
}
最後就是啟動執行緒不斷的從佇列中取出請求然後載入圖片程式碼如下:
啟動執行緒
public final class TargetQueue {
private BlockingQueue<Target> mRequestQueue = new PriorityBlockingQueue<>();
private AtomicInteger mAtomicInteger = new AtomicInteger(0);
private static int CORE_NUM = Runtime.getRuntime().availableProcessors();
private int mDispatchNum = CORE_NUM;
private Executor[] mDispatchers = null;
public TargetQueue() {
}
private void startDispatchers() {
mDispatchers = new Executor[mDispatchNum];
for (int i = 0; i < mDispatchNum; i++) {
mDispatchers[i] = new Executor(mRequestQueue);
mDispatchers[i].start();
}
}
public void addRequest(Target request) {
if (!mRequestQueue.contains(request)) {
request.sequenceNumber = mAtomicInteger.incrementAndGet();
mRequestQueue.add(request);
}
}
/**
* 開始請求圖片
*/
public void start() {
stop();
startDispatchers();
}
/**
* 停止所有的請求
*/
public void stop() {
if (mDispatchers != null && mDispatchers.length > 0) {
for (Executor dispatcher : mDispatchers) {
dispatcher.interrupt();
}
}
}
public BlockingQueue<Target> getAllRequest() {
return mRequestQueue;
}
public void clear() {
mRequestQueue.clear();
}
}
執行緒中的迴圈
public class Executor extends Thread {
private BlockingQueue<Target> mTargetQueue;
Executor(BlockingQueue<Target> targetQueue) {
mTargetQueue = targetQueue;
}
@Override
public void run() {
while (!this.isInterrupted()) {
try {
final Target request = mTargetQueue.take();
if (request.isCancelled) {
continue;
}
String target = parseSchema(request.imgUri);
BaseLoader imgBaseLoader = LoaderFactory.getInstance().getLoader(target);
imgBaseLoader.loadImage(request);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private String parseSchema(String uri) {
if (uri.contains("://")) {
return uri.split("://")[0];
}
return "";
}
}