平时我们在开发的过程中基本都是用的第三方图片的加载框架:Glide、Fresco等等,如果进一步的学习,想去对图片加载框架一探究竟,我们回去了解它们的源码,了解它们的框架和实现方式。但是呢,一个框架的成熟时经过好多版本的迭代。它们是怎样从零到一的呢?我试着从零开始写一个简易的图片加载框架。
一个图片加载框架需要具备哪些功能呢?
- 网络加载功能
- 缓存功能
根据面向对象思想,首先图片加载的话需要一个类:ImageLoader,和一个对外的使用接口:displayImage()方法。于是撸起袖子干起来:
/**
* Created by xucong on 2017/7/17.
* 图片加载类
*/
public class ImageLoader {
private static ImageLoader mImageLoader = new ImageLoader();
private ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
Handler handler = new Handler();
private LruCache<String, Bitmap> lruCache;
public ImageLoader() {
init();
}
/**
* 获取图片加载的单列
* @return
*/
public static ImageLoader getInstance() {
return mImageLoader;
}
/**
* 初始化
*/
private void init() {
int maxMemory = (int) Runtime.getRuntime().maxMemory()/1024;
lruCache = new LruCache<String, Bitmap>(maxMemory/4){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight()/1024;
}
};
}
public void displayImage(final String url, final ImageView imageView) {
if (imageView == null)return;
imageView.setTag(url);
//先从缓存取,如果没有采去下载
Bitmap bitmap = this.lruCache.get(url);
if(bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
//下载图片
mExecutorService.execute(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = downloadImage(url);
if(bitmap == null) {
return;
}
if(imageView.getTag().equals(url)) {
handler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
lruCache.put(url,bitmap);
}
}
});
}
private Bitmap downloadImage(String imageUrl) {
Bitmap bitmap = null;
try {
URL url = new URL(imageUrl);
URLConnection urlConnection = url.openConnection();
bitmap = BitmapFactory.decodeStream(urlConnection.getInputStream());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
乍一看基本功能实现了,但是呢,一个优秀的框架设计需要具有高扩展性,模块之间低耦合,上面功能所有代码写在一个类里面怎么行呢,显然不符合单一职责原则,如果需要扩展功能,需要修改源码,而不是通过扩展,这又违背了开闭原则。所以我们需要进行改进。
根据单一职责原则和面向对象思想,图片缓存功能应该是一个独立的模块,需要把缓存抽离出来和ImageLoader类进行解耦:
public class ImageCache {
private LruCache<String, Bitmap> mLruCache;
public void ImageCache() {
init();
}
/**
* 初始化
*/
private void init() {
int maxMemory = (int) Runtime.getRuntime().maxMemory()/1024;
mLruCache = new LruCache<String, Bitmap>(maxMemory/4){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight()/1024;
}
};
}
public Bitmap get(String url) {
return mLruCache.get(url);
}
public void put(String url, Bitmap bitmap) {
mLruCache.put(url, bitmap);
}
}
public class ImageLoader {
//图片缓存
private ImageCache mImageCache = new ImageCache();
public void displayImage(final String url, final ImageView imageView) {
if (imageView == null)return;
imageView.setTag(url);
//先从缓存取,如果没有采去下载
Bitmap bitmap = this.mImageCache.get(url);
if(bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
//下载图片
mExecutorService.execute(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = downloadImage(url);
if(bitmap == null) {
return;
}
if(imageView.getTag().equals(url)) {
handler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
mImageCache.put(url,bitmap);
}
}
});
}
将代码一分为二,Imageloader加载图片,ImageCache缓存图片,这样职责就清晰了,需要修改图片缓存功能不会影响图片加载类,修改图片加载功能不会影响图片缓存类。这就是单一职责很好的体现。
但是问题又来了,如果我们需要自定义缓存策略呢?或者我们还需要内存缓存和SD卡缓存同时使用呢?这样的话我们又只能修改源码,对扩展是不支持的,这样违反了开闭原则。
于是运用策略模式和里氏替换原则来对缓存策略模块的修改:
所有的缓存策略用抽象ICache接口开约束:
public interface ICache {
Bitmap get(String url);
void put(String url,Bitmap bitmap);
}
所有的缓存实现类必须要实现get、put,方法来存取Bitmap。
/**
* 内存缓存
* Created by xucong on 2017/7/19.
*/
public class MemoryCache implements ICache {
private LruCache<String, Bitmap> mLruCache;
public MemoryCache(){
init();
}
/**
* 初始化
*/
private void init() {
int maxMemory = (int) Runtime.getRuntime().maxMemory()/1024;
mLruCache = new LruCache<String, Bitmap>(maxMemory/4){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight()/1024;
}
};
}
@Override
public Bitmap get(String url) {
return mLruCache.get(url);
}
@Override
public void put(String url,Bitmap bitmap) {
mLruCache.put(url, bitmap);
}
}
/**
* SD卡缓存
* Created by xucong on 2017/7/19.
*/
public class DiskCache implements ICache{
private static String cacheDir = Environment.getExternalStorageDirectory().getPath() + "/cache/image/";
@Override
public Bitmap get(String url) {
return BitmapFactory.decodeFile(cacheDir + url);
}
@Override
public void put(String url, Bitmap bitmap) {
url = cacheDir + url;
FileOutputStream fos = null;
try {
fos = new FileOutputStream(url);
bitmap.compress(Bitmap.CompressFormat.PNG,100,fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 双缓存
* Created by xucong on 2017/7/19.
*/
public class DoubleCache implements ICache {
private DiskCache mDiskCache = new DiskCache();
private MemoryCache memoryCache = new MemoryCache();
@Override
public Bitmap get(String url) {
Bitmap bitmap = memoryCache.get(url);
if(bitmap != null) {
bitmap = mDiskCache.get(url);
}
return bitmap;
}
@Override
public void put(String url, Bitmap bitmap) {
mDiskCache.put(url,bitmap);
memoryCache.put(url,bitmap);
}
}
ImageLoader:
public class ImageLoader {
//图片缓存
private ICache mImageCache = new MemoryCache();
public void setImageCache(ICache cache) {
this.mImageCache = cache;
}
public void displayImage(final String url, final ImageView imageView) {
if (imageView == null)return;
imageView.setTag(url);
//先从缓存取,如果没有采去下载
Bitmap bitmap = this.mImageCache.get(url);
if(bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
//下载图片
mExecutorService.execute(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = downloadImage(url);
if(bitmap == null) {
return;
}
...
mImageCache.put(url,bitmap);
}
}
});
}
可以看到通过setImageCache()方法来替换不同的缓存策略,所有的缓存实现都实现ICache接口。这样就体现了高扩展性,也符合了开闭原则和里氏替换原则。其中重要的思想其实就是面向接口编程,ImageLoader以来缓存模块是以来的抽象,而不是以来细节(具体实现)这样父类的引用指向子类的对象,这样就保证了缓存功能的高扩展性。