Glide源码略看

##碎碎念

介绍Glide的文章看了挺多,偶尔也会点击查看部分的Glide的源码,可感觉对Glide的还是不太懂。个人觉得,每个人 的水平不同,对知识点的掌握的面不同,可能他人的已掌握的知识点于自己而言是盲点,,写源码分析的人一般将自己 觉得值得写的东西记录下来,故通过看他人的二手资料来了解一个库,可能会出现这么个情况,感觉自己自己看懂了, 但却没有学到什么东西。看文章时是假设他人掌握的知识来得出结论的,可自己实际上没有学到那些前置知识。故,我 还是想尝试自己看看。

##开始

看不懂现在的库,切换早些年的版本,比较容易查看整体轮廓。

版本时间2012/12/21

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public class PhotoManager {
//1.两个缓存
//加载图片,二级缓存,优先从内存获取,其次从磁盘,最后才会请求网络。
private PhotoDiskCache diskCache;
private LruPhotoCache memoryCache;

//2.图片大小转换器
//解决源图片,与我们显示的图片大小不一致的问题
private PhotoStreamResizer resizer;
//3.任务调度
private Map<Object, Future> taskManager = new HashMap<Object, Future>();
private Handler backgroundHandler;
}

二个缓存

加载图片的流程,感觉是最常问也最重要的知识点了。加载图片,存在二个缓存,内存缓存和磁盘缓存,空间换时间的 经典例子了。其中,缓存的实现使用是LRU算法,内部实现可以使用LinkedHashMap,只要将构造方法中 accessOrder设为true既可。

图片大小转换器

1
2
3
4
5
6
7
8
9
10
11
12
    // 有图片地址,有想要图片的大小,故我们需要将原图缩放成指定的大小
// 转换时耗时的,故这里使用Future来
// Future个人理解就是可以获得返回结果的线程
public Future<Bitmap> loadApproximate(final String path, final int width, final int height, ResizeCallback callback){
Callable<Bitmap> task = new Callable<Bitmap>() {
@Override
public Bitmap call() throws Exception {
return Utils.streamIn(path, width, height);
}
};
return startTask(task, callback);
}

###关于线程切换,这里使用了Handler的例子。
比如我们执行转换图片的任务,需要到子线程处理,转换完成后,需要将结果给UI线程来处理接口。
如何将一部分实现交给调用者,这里使用了Callback。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

private static class StreamResizeFuture extends FutureTask<Bitmap> {
// 处理的结果结果需要交给UI线程处理,故需要主线程的handler
private final Handler mainHandler;
private final ResizeCallback callback;
private Bitmap result;

@Override
protected void done() {
super.done();
if (!isCancelled()){
try {
result = get();
// 将部分逻辑在UI线程执行
mainHandler.post(new Runnable() {
@Override
public void run() {
callback.onResizeComplete(result);
}
});
} catch (final Exception e) {
e.printStackTrace();
mainHandler.post(new Runnable() {
@Override
public void run() {
callback.onResizeFailed(e);
}
});
}
}
}
}

图片大小转换的具体实现

感觉缩放图片的大小具体实现挺有意思(见识短,大惊小怪)

将变换的逻辑在矩阵上操作,比如放大,设置轴点,再将该变换应用于图片流上,最后便得到我们想要的图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public Future<Bitmap> resizeCenterCrop(final String path, final int width, final int height, ResizeCallback callback){
Callable<Bitmap> task = new Callable<Bitmap>(){
@Override
public Bitmap call() throws Exception {
Bitmap result = null, streamed = null;
// 获取到图片流,
streamed = Utils.streamIn(path, width, height);

if (streamed.getWidth() == width && streamed.getHeight() == height) {
return streamed;
}

//from ImageView/Bitmap.createScaledBitmap
//https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/widget/ImageView.java
//https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/graphics/java/android/graphics/Bitmap.java
final float scale;
float dx = 0, dy = 0;
// 创建矩阵,将图片流画在矩阵中
Matrix m = new Matrix();
if (streamed.getWidth() * height > width * streamed.getHeight()) {
scale = (float) height / (float) streamed.getHeight();
dx = (width - streamed.getWidth() * scale) * 0.5f;
} else {
scale = (float) width / (float) streamed.getWidth();
dy = (height - streamed.getHeight() * scale) * 0.5f;
}

m.setScale(scale, scale);
m.postTranslate((int) dx + 0.5f, (int) dy + 0.5f);
Bitmap bitmap = Bitmap.createBitmap(width, height, streamed.getConfig());
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
//only if scaling up
paint.setFilterBitmap(false);
paint.setAntiAlias(true);
canvas.drawBitmap(streamed, m, paint);
result = bitmap;

return result;
}
};
return startTask(task, callback);
}

似乎是一个常见的面试问题的源头

如何加载一个很大的图片?假设一张原图片像素有1000010000,而我们只需要1010的像素的图片,那么我们不该 将整个图片读取下来再缩放成我们想要的大小,而且假设有一个巨大大图片,读取整个图片内存直接爆了。我们是能 否将部分图片读取显示呢?

大佬也遇到了这个问题,也给(google)出了答案。
其中,思路是这样的,

  1. 读取图片文件前半部分的字节(16字节足以),获取到该图片的信息,如图片大小
  2. 根据原图大小和想要的大小,算出采样的比例,再次读取是传入采样比例,这样子,读取的图片就只有目标图片 所占的内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//from http://stackoverflow.com/questions/7051025/how-do-i-scale-a-streaming-bitmap-in-place-without-reading-the-whole-image-first
//streams in to near, but not exactly at the desired width and height.
public static Bitmap streamIn(String path, int width, int height) {

Bitmap result = null;
try {
// 创建一个Opitions来记录图片大小
final BitmapFactory.Options decodeBitmapOptions = new BitmapFactory.Options();
// For further memory savings, you may want to consider using this option
InputStream first = new BufferedInputStream(new FileInputStream(path), 16384);

//find the dimensions of the actual image
//获取到原图的实际大小
final BitmapFactory.Options decodeBoundsOptions = new BitmapFactory.Options();
// 如其所说,仅解码该图片大小
decodeBoundsOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(first, null, decodeBoundsOptions); //doesn't load, just sets the decodeBounds
first.close();

final int originalWidth = decodeBoundsOptions.outWidth;
final int originalHeight = decodeBoundsOptions.outHeight;

// inSampleSize prefers multiples of 2, but we prefer to prioritize memory savings
int sampleSize = Math.min(originalHeight / height, originalWidth / width);
InputStream second = new BufferedInputStream(new FileInputStream(path), 16384);

//算出采样比例,并传入作为参数传入下一次解码方法中
decodeBitmapOptions.inSampleSize = sampleSize;
if (Build.VERSION.SDK_INT > 11) {
decodeBitmapOptions.inMutable = true;
}
Log.d("PSR: Loading image with sample size: " + sampleSize);
result = BitmapFactory.decodeStream(second, null, decodeBitmapOptions);
if(orientation != 0) {
result = Photo.rotateImage(result, orientation);
}
second.close();
} catch (Exception e){
Log.d("PSR: error decoding image: " + e);
} catch (OutOfMemoryError e){
Log.d("PSR: not enough memory to resize image at " + path);
Log.d(e.toString());
}
return result;
}

要明白以上问题的答案,感觉应该查看这个文件。暂且了解以下两个属性吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class BitmapFactory {
public static class Options {
// 解码器仅查询原图大小,而不会分配内存来保存这些图片的像素
/**
* If set to true, the decoder will return null (no bitmap), but
* the <code>out...</code> fields will still be set, allowing the caller to
* query the bitmap without having to allocate the memory for its pixels.
*/
public boolean inJustDecodeBounds;

// 采样图片的比例,比如传入4,加载图片有16M, 则加载的图片大小只有1M
/**
* If set to a value > 1, requests the decoder to subsample the original
* image, returning a smaller image to save memory. The sample size is
* the number of pixels in either dimension that correspond to a single
* pixel in the decoded bitmap. For example, inSampleSize == 4 returns
* an image that is 1/4 the width/height of the original, and 1/16 the
* number of pixels. Any value <= 1 is treated the same as 1. Note: the
* decoder uses a final value based on powers of 2, any other value will
* be rounded down to the nearest power of 2.
*/
public int inSampleSize;
}
}

关于图片的处理,有挺多盲区的。平常对图片精细的操作很少,诸如上述问题,日常开发App也很少遇到, 但是其原理确实应当了解。

一些零碎的版本追踪

看到作者作者一步步添加了好多小东西,

添加Presenter来将Bitmap放置到ImageView

ImageLoader可以从加载Path和Assets资源文件,抽象成公共的接口

加载图片的key,缓存使用到,key的生成从加载的大小,加入显示类型,比如center_crop,fit_center

使用Weak reference避免内存泄漏

引入android项目后的第一个版本 2013-1-10

项目使用Flickr网站的图片作为Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//Activity中的一个ListView中的Adapter实现
@Override
public View getView(int position, View view, ViewGroup container) {
final ImagePresenter<Photo> presenter;
if (view == null) {
Log.d("MyActivity: inflate");
ImageView imageView = (ImageView) inflater.inflate(R.layout.photo_grid_square, container, false);
final Animation fadeIn = AnimationUtils.loadAnimation(MyActivity.this, R.anim.fade_in);
// Presenter作为连接ImageView和ImageLoader的桥梁
// 传入泛型参数包含图片源的基本信息,可以是图片的URL地址或是磁盘地址,这里的Photo包含有了
presenter = new ImagePresenter.Builder<Photo>()
.setImageView(imageView)
//这里传入一个缓存地址,会优先从磁盘获取的
.setPathLoader(new FlickPathLoader(flickerApi, cacheDir))
// 设置我们的加载ImageLoader哈
.setImageLoader(new CenterCrop<Photo>(imageManager))
// 获取到图片加载到ImageView
.setImageSetCallback(new ImageSetCallback() {
@Override
public void onImageSet(ImageView view, boolean fromCache) {
view.clearAnimation();
if (!fromCache)
view.startAnimation(fadeIn);
}
})
.build();
imageView.setTag(presenter);
view = imageView;
} else {
presenter = (ImagePresenter<Photo>) view.getTag();
}

presenter.setModel(photos.get(position));
return view;
}
}

看看如何优先从磁盘加载的呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void downloadPhoto(Photo photo, File cacheDir, final PhotoCallback cb) {
//根据photo的信息和缓存地址,定位到对应的文件是否存在哈
File out = new File(cacheDir.getPath() + File.separator + photo.id + photo.secret);
if (out.exists()) {
cb.onDownloadComplete(out.getPath());
} else {
Log.d("API: missing photo, downloading");
// 开始从网络获取了
downloader.download(getPhotoUrl(photo), out, new Downloader.DiskCallback() {
@Override
public void onDownloadReady(String path) {
cb.onDownloadComplete(path);
}
});
}
}

之后,我们在看看Downloader是如何加载网络的资源,即download方法,我们找到了Downloader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class Downloader {
// 下载器作为单例存在,
private static Downloader DOWNLOADER;
private final Handler mainHandler;
private final ExecutorService executor;

//其中维护了一个固定6个线程池
protected Downloader() {
HandlerThread workerThread = new HandlerThread("downloader_thread");
workerThread.start();
executor = Executors.newFixedThreadPool(6);
mainHandler = new Handler();
}
//下载方法
public void download(String url, File out, DiskCallback cb) {
post(new DiskDownloadWorker(url, out, cb));
}
//将任务放入线程池中启动
private void post(Runnable runnable) {
executor.execute(runnable);
}
//这个下载任务如何执行的呢
private class DiskDownloadWorker implements Runnable {
@Override
public void run() {
Log.d("Downloader: run");
HttpURLConnection urlConnection = null;
try {
//朴素的URLConnection打开流
final URL targetUrl = new URL(url);
urlConnection = (HttpURLConnection) targetUrl.openConnection();
InputStream in = new BufferedInputStream(urlConnection.getInputStream());
OutputStream out = new FileOutputStream(output);
//将输入流写入文件
writeToOutput(in, out);
out.close();
in.close();
//这里,使用mainHandler来切换当前线程到UI线程中执行
mainHandler.post(new Runnable() {
@Override
public void run() {
cb.onDownloadReady(output.getPath());
}
});
} catch (IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}

private void writeToOutput(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[1024];
int bytesRead;
while (((bytesRead = in.read(buffer)) != -1)) {
out.write(buffer, 0, bytesRead);
}
}
}

}

Downloader实现了如何从一个URL转变成一个文件,其中,
本身作为单例存在,因为我们对于不同的下载,我们是可以复用同一个线程池的。
而使用线程池应付多个图片的请求。
另外使用了handler在下载完成后切换到UI线程中执行会回调,回调就是执行展示的操作

回到Activiy,我们不能忘了我们的主角ImageLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

//在onCreate方法中创建ImageLoader
ImageManager.Options options = new ImageManager.Options();
options.maxMemorySize = 2 * 1024 * 1024;
options.maxPerSize = 40;
imageManager = new ImageManager(this, options);

//并且监听了Activity的生命周期方法
@Override
protected void onResume() {
super.onResume();
imageManager.resume();
}

@Override
protected void onPause() {
super.onPause();
imageManager.pause();
}

其中ImageLoader主要逻辑在第一版本已经说得差不多了,代码逻辑与最差版本相差不是很大。

1
2
3
4
5
6
7
8
private final Handler mainHandler;
private final LruPhotoCache memoryCache; // 内存最近使用
private final ImageResizer resizer; //图片大小转换
private final Executor executor; //线程池调度
private final Map<Integer, Integer> bitmapReferenceCounter = new HashMap<Integer, Integer>();
private final SizedBitmapCache bitmapCache;
private final PhotoDiskCache diskCache;
private final boolean isBitmapRecyclingEnabled;

Glide名字出生了 2013-7-17

伴随着项目包名换成glide,不久之后,引入了Glide这个类,普通用户只需要懂得这个类就ok了,想了解原理的人 也可以从这个类开始探索~ 看看作者的介绍吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Static helper methods/classes to present a simple unified interface for using glide. Allows 90%
* of the functionality of the library. The trade off is some extra unused object allocation, and a few unavailable
* methods. For many users this should be enough to make effective use of the library. For others it can serve as a
* starting point and example. This class is not thread safe.
*/
public class Glide {
// 直接给创建一个单例
private static final Glide GLIDE = new Glide();
//定义了两个model,可以加载成图片流的源,File or URL
private static final Map<Class, ModelStreamLoader> classToModelStream = new HashMap<Class, ModelStreamLoader>() {{
put(File.class, new FileStreamLoader());
put(URL.class, new UrlStreamLoader());
}};
//之前的大名隐匿于此了
private ImageManager imageManager;
/**
* Manages building, tagging, retrieving and/or replacing an ImagePresenter for the given ImageView and model
* @param <T> The type of model that will be loaded into the view
*/
//如何将图片源,加载到的目标ImageView的统一
public static class Request<T> {
private final T model;
private final ImageView imageView;
private final Context context;

private ImagePresenter<T> presenter;
private ImagePresenter.Builder<T> builder;

public Request(T model, ImageView imageView) {
this.model = model;
this.imageView = imageView;
this.context = imageView.getContext();

presenter = (ImagePresenter<T>) imageView.getTag(R.id.image_presenter_id);
builder = new ImagePresenter.Builder<T>()
.setImageView(imageView)
.setImageLoader(new Approximate(getImageManager()));

ModelStreamLoader<T> loader = classToModelStream.get(model.getClass());
if (loader != null) {
builder.setModelStreamLoader(loader);
}
}
}
}

期间作者改了好多次类名哦,并加了很多注释
对于线程安全方面,一些关键方法加了syschronized关键字,或是改用了concurrent包中的集合
在加载图片的公开方法上,出现了大致的轮廓,如下

1
2
3
4
5
6
Glide.load(current)
.into(viewHolder.imageView)
.with(new DirectFlickrStreamLoader(api))
.centerCrop()
.animate(R.anim.fade_in)
.begin();

使用Vollery in UrlLoader
添加一些测试 为了方便,图片源添加一个StringLoader,其内部是转换成UrlLoader

对于UrlLoader,因为最终都是要打开流的,故存在了StreamLoader接口
其功能就是打开流,并定义了Callback,用于打开成功或失败的处理

1
2
3
4
5
6
7
8
9
10
11
12
public interface StreamLoader {
public interface StreamReadyCallback {

public void onStreamReady(InputStream is);

public void onException(Exception e);

}
public void loadStream(StreamReadyCallback cb);

public void cancel();
}

那么图片流的来源可分为两类,本地的和远程的
本地的指Android定义流,如Assert读取的文件,提供的协议头 content:file: 远程的包括http和https

允许加载任意的Transformation和加载image通任意的目标

这个两个功能确实为该框架添加了很多色彩啊

设置转场动画,比如加载图片时,需要设置fadeIn加载的动画,看起来确实很舒服,以及暴露了可以加载自定义动画

新添加了Target的概念,这个概念我第一次遇到确实蒙的

Target是定义了可以防止图片的目标,需要处理

//图片准备好了时该怎么展示啊
public void onImageReady(Bitmap bitmap);
//占位图片啊,要注意这里的占位图是包括加载前和加载失败的,在不同状态时,Glide会设置不同的图片
public void setPlaceholder(Drawable placeholder);
//获取到该目标能显示多大的图片,如ImageView的wrap_content时,是需要异步的等待结果的
public void getSize(SizeReadyCallback cb);
//可选性的提供转场动画
public void startAnimation(Animation animation);
//获取和设置Presenter
public void setImagePresenter(ImagePresenter imagePresenter);
public ImagePresenter getImagePresenter();

总的看,就是一个能放置图片的玩意,应该具备有什么能力(方法)

看完这个Target,再看Presenter

Presenter跟Android中的MVP中P概念是一致的,
持有View,负责捉取图片,和加载正确大的图片,即使View回收时 类型参数中的T就是Model,定义了图片的来源,这里看看设置Model的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

/**
* Sets a model to load an image from. Each subsequent call will override all previous calls and will prevent any
* bitmaps that are loaded from previous calls from being displayed even if the load completes successfully. Any
* image being displayed at the time of this call will be replaced either by the placeholder or by the new image
* if the load completes synchronously (ie it was in an in memory cache)
*
* <p>
* Note - A load will not begin before the ImagePresenter has determined the width and height of the wrapped
* view, which can't happen until that view has been made visible and undergone layout out for the first time. Until
* then the current load is stored. Subsequent calls will replace the stored load
* </p>
*
* @param model The model containing the information required to load a path and/or bitmap
*/
public void setModel(final T model) {
if (model == null) {
clear();
} else if (!model.equals(currentModel)) {
loadedFromCache = true;
final int loadCount = ++currentCount;
currentModel = model;
isImageSet = false;
//需要target定好自己的大小后,才会真正加载图片
target.getSize(new Target.SizeReadyCallback() {
@Override
public void onSizeReady(int width, int height) {

fetchImage(model, width, height, loadCount);
}
});

loadedFromCache = false;

if (!isImageSet) {
resetPlaceHolder();
}
}
}

private void fetchImage(final T model, int width, int height, final int loadCount) {
imageLoader.clear();
final String id = modelLoader.getId(model);
final StreamLoader sl = modelLoader.getStreamLoader(model, width, height);
final Transformation t = transformationLoader.getTransformation(model);
//使用ImageLoader来加载图片
imageToken = imageLoader.fetchImage(id, sl, t, width, height, new ImageLoader.ImageReadyCallback() {
@Override
public boolean onImageReady(Bitmap image) {
if (loadCount != currentCount || !canSetImage() || image == null) return false;

if (imageReadyCallback != null)
imageReadyCallback.onImageReady(target, loadedFromCache);
target.onImageReady(image);
isImageSet = true;
return true;
}

@Override
public void onException(Exception e) {
final boolean relevant = loadCount == currentCount;
if (exceptionHandler != null) {
exceptionHandler.onException(e, model, relevant);
}
//若是设置错误的占位图,则会显示啊
if (relevant && canSetPlaceholder()) {
target.setPlaceholder(errorDrawable);
}
}
});
}

暂且看到版本2013-8-31

感想

好累哦,感觉继续看下去自己脑子要糊了~~

2013年,有些遥远了啊,想想当时自己还没上大学呢。

考古的感觉是如何的呢?

看着大佬的一步步的从简单的版本一步步到2013-8这个版本,也快一年的工作量,消化不过来了。最初的版本的有个操作,读取文件流仅却不加载 到内存的操作,为了获取到图片源的大小,故,可以根据需要展示的图片大小,通过设置采样,不必加载整个文件,就可以加载出我们所需的图片。

这个操作在android官方文档似乎就有看到呢,且在国内的知名Android博主的文章也提及。而当自己在源码中看到这个真实的代码时,自己确实有些许 激动的。

另个一个感触是,看到开始的这些版本,感觉作者的代码,自己也是大致能完全理解的哈,Glide一步步成为知名框架,是有迹可循的。开始的代码,作者写的也 很朴素,之后代码逐渐重构清晰。结合自己看的EventBus源码,感觉大佬的炼成之路真的需要坚持啊。

另外,教训,感觉之后的源码需要切入某个方面来看,否则好难坚持。

比如作者添加错误的占位图这个功能时,根据作者写的commit,看的时候,思路就会清晰点。(小提示,以后自己提的commit也要用心点)