目錄
從零實(shí)現(xiàn)ImageLoader(一)—— 架構(gòu)
從零實(shí)現(xiàn)ImageLoader(二)—— 基本實(shí)現(xiàn)
從零實(shí)現(xiàn)ImageLoader(三)—— 線程池詳解
從零實(shí)現(xiàn)ImageLoader(四)—— Handler的內(nèi)心獨(dú)白
從零實(shí)現(xiàn)ImageLoader(五)—— 內(nèi)存緩存LruCache
從零實(shí)現(xiàn)ImageLoader(六)—— 磁盤緩存DiskLruCache
ImageLoader類
我們今天先從ImageLoader類入手,由于是鏈?zhǔn)降恼{(diào)用方式,ImageLoader以單例的方式實(shí)現(xiàn),下面是代碼:
public class ImageLoader {
private static volatile ImageLoader mSingleton;
private final Context mContext;
private ImageLoader(Context context) {
mContext = context;
}
public static ImageLoader with(Context context) {
if(mSingleton == null) {
synchronized (ImageLoader.class) {
if(mSingleton == null) {
mSingleton = new ImageLoader(context);
}
}
}
return mSingleton;
}
}
至于單例為什么要這么實(shí)現(xiàn),不清楚的同學(xué)可以看一下這篇文章:如何正確地寫出單例模式 | Jark's Blog。單例如何實(shí)現(xiàn),為什么要這么實(shí)現(xiàn),在這篇文章中都有詳細(xì)的介紹,這里就不再贅述了。
內(nèi)存泄露!
不過,上面的代碼看似沒有問題,但細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn)了,ImageLoader類持有了Context對(duì)象,而ImageLoader作為一個(gè)單例持有Context對(duì)象是很有可能造成內(nèi)存泄露的。
我們可以用LeakCanary檢測(cè)一下,使用起來也很簡單, 在build.gradle中加入以下代碼:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.2'
}
接著創(chuàng)建自己的Application類:
public class App extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}
然后隨便在MainActivity的什么地方調(diào)用ImageLoader:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageLoader.with(this);
}
}
現(xiàn)在只需要打開應(yīng)用、退出、再打開就會(huì)看到下面的畫面:

這是LeakCanary在導(dǎo)出內(nèi)存信息,過一會(huì)等待LeakCanary分析完成就會(huì)出現(xiàn)一條通知:

可以看到LeakCanary提示MainActivity產(chǎn)生內(nèi)存泄露了,點(diǎn)進(jìn)去有更詳細(xì)的情況:

可以很明顯看到MainActivity的引用被ImageLoader的單例持有,由于單例的生命周期是伴隨整個(gè)應(yīng)用的,當(dāng)Activity調(diào)用onDestory()方法時(shí),依然被ImageLoader引用而無法釋放,這就造成了內(nèi)存泄露。
這給了我們一個(gè)很重要的警示:不要在單例中持有Activity對(duì)象的Context。
可這就產(chǎn)生了一個(gè)問題,我們必須要用Context,這可怎么辦呢?其實(shí)解決方法也很簡單,既然ImageLoader的生命周期是整個(gè)應(yīng)用,那我們使用生命周期同樣是整個(gè)應(yīng)用的ApplicationContext不就可以了嗎?于是代碼變成了這樣:
public class ImageLoader {
private static volatile ImageLoader mSingleton;
private final Context mContext;
private ImageLoader(Context context) {
//防止單例持有Activity的Context導(dǎo)致內(nèi)存泄露
mContext = context.getApplicationContext();
}
public static ImageLoader with(Context context) {
if(mSingleton == null) {
synchronized (ImageLoader.class) {
if(mSingleton == null) {
mSingleton = new ImageLoader(context);
}
}
}
return mSingleton;
}
}
再打開應(yīng)用,已經(jīng)沒有內(nèi)存泄露了。
同步實(shí)現(xiàn)
解決了內(nèi)存泄露問題,接著實(shí)現(xiàn)其他功能。我們使用load(String url)傳入需要加載的圖片路徑:
public class ImageLoader {
...
public Dispatcher load(String url) {
return new Dispatcher(url);
}
}
由于是鏈?zhǔn)秸{(diào)用,所以返回了Dispatcher類:
public class Dispatcher {
private final String mUrl;
public Bitmap get() throws IOException {
URL realUrl = new URL(mUrl);
HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
try(InputStream in = connection.getInputStream()) {
return BitmapFactory.decodeStream(in);
} finally {
connection.disconnect();
}
}
}
這里只做一個(gè)簡單的同步加載實(shí)現(xiàn),也就是get()方法,其他功能等之后再慢慢添加。
到這里,ImageLoader就已經(jīng)有了加載圖片的功能了,先測(cè)試一下:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView imageView = findViewById(R.id.image);
new Thread(() -> {
Bitmap bitmap;
try {
bitmap = ImageLoader.with(this)
.load("https://i.redd.it/20mplvimm8ez.jpg")
.get();
MainActivity.this.runOnUiThread(() -> {
imageView.setImageBitmap(bitmap);
});
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}

代碼優(yōu)化
上面的get()方法實(shí)現(xiàn)現(xiàn)在看起來好像沒什么問題,不過Dispatcher類是用來進(jìn)行線程切換以及緩存加載的,如果再將網(wǎng)絡(luò)下載放在這里,Dispatcher類就會(huì)顯得過于臃腫,為了保持Dispatcher類功能的單一性,這里選擇將網(wǎng)絡(luò)下載功能抽出來單獨(dú)做一個(gè)類:
public class NetworkUtil {
private NetworkUtil() {}
public static Bitmap getBitmap(String url) throws IOException {
URL realUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
try(InputStream in = connection.getInputStream()) {
return BitmapFactory.decodeStream(in);
} finally {
connection.disconnect();
}
}
}
這樣做還有一個(gè)好處,那就是以后想要使用其他的網(wǎng)絡(luò)下載框架比如OkHttp或者Volley,只需要在NetworkUtil中修改而不影響其他類了。
現(xiàn)在,Dispatcher的get()方法也變得非常簡潔:
public class Dispatcher {
private final String mUrl;
public Bitmap get() throws IOException {
return NetworkUtil.getBitmap(mUrl);
}
}
總結(jié)
在這篇文章里我們實(shí)現(xiàn)了基本的同步加載,同時(shí)解決了Context的內(nèi)存泄露問題,下一篇,我們將要實(shí)現(xiàn)的是異步圖片加載。