Glide源码看点-图片pre scale

Glide是如何根据View大小对图片进行缩放优化呢 (pre scale)

我们都知道图片实际大小和实际显示的区域一般不能是一样的,所以如果图片尺寸本身很大但是展示区域很小的话就会,如果已完整尺寸对图片进行加载就会效率很低浪费内存,加载速度慢。所以一般我们会根据图片实际需要显示的实际大小对图片进行缩放。
Android内置了inSampleSize方法来对图片进行缩放, Glide抽象出了一个类叫Downsampler.class来根据期望的图片大小和缩放策略进行向下采样。所以关键点变为了Glide如何知道我们的View实际需要的显示区域。下面将通过一个性能问题的debug故事分析glide源码

Wrap Content引发的性能问题“血案”

一个类似聊天信息的界面,如果列表里有4,5张图片后,滑动就会变得异常卡顿。screen_shoot

诊断

打开Android Monitor抓取性能Trace,发现咦咋性能消耗最大的是Glide decode Bitmap。
performance
感觉不应该啊,我图片虽然可能很大但是实际显示区域不大,Glide在显示前应该已经会根据ImageView的实际显示效果进行了缩放采样(DownSample)的优化不应该产生卡顿啊。
**等等****
难道缩放采样出现了问题?

我们根据Glide的调用链找分析。

GlideApp.with(context).load(imgUrl).into(target)

RequestBuilder的into方法其实是调用管理图片加载请求的RequestTrack里的track方法最后调用到了具体Request的begin方法。
Step1:
SingleRequest.run()方法 -> ViewTarget.getSize()

如果开发者有制定大小OverrideSize就会直接采用该尺寸,如果没有就去ViewTarget里去拿size
ViewTarget.getSize -> sizeDeterminer.getSize(cb);

ViewTarget里会根据SizeDeterminer去获取TargetView的具体尺寸。如果能直接从View里读取到具体的尺寸就直接用。

如果读取不到就用PreDrawListener在layout完成后去监听view的实际尺寸:
observer.addOnPreDrawListener(layoutListener);
关键点来了,去看添加的preDrawListener,里面有个特殊的if语句,如果view定义了WRAP_CONTENT, Glide就无法判断到底使用什么大小去preScale图片,所以它取了屏幕的最大宽度作为图片的加载大小。(不取图片的Target.SIZE_ORIGINAL的原因是可能图片会很大,远远大于屏幕尺寸)
SizeDeterminerLayoutListener:
`

  if (!view.isLayoutRequested() && paramSize == LayoutParams.WRAP_CONTENT) {
    if (Log.isLoggable(TAG, Log.INFO)) {
      Log.i(TAG, "Glide treats LayoutParams.WRAP_CONTENT as a request for an image the size of"
          + " this device's screen dimensions. If you want to load the original image and are"
          + " ok with the corresponding memory cost and OOMs (depending on the input size), use"
          + " .override(Target.SIZE_ORIGINAL). Otherwise, use LayoutParams.MATCH_PARENT, set"
          + " layout_width and layout_height to fixed dimension, or use .override() with fixed"
          + " dimensions.");
    }
return getMaxDisplayLength(view.getContext());
  }

`

元凶

看到这里刚才的性能问题迎刃而解,虽然我们界面里的ImageView实际显示区域很小(maxWitdh和maxHeight限制的),但是Glide不知道。一看我们list item的布局果然如此


所以在列表里的每一张图片都是以手机的最大宽度去加载的,所以decode bitmap才花了巨大的时间。

所以Fix这个问题只需要,在加载图片前设置好需要的加载宽度就可以解决啦。

总结

图片加载的一个重要手段是,“按需显示”更具实际需要显示的图片大小进行预缩放(Pre Scale), PreScale要根据图片的显示大小和图片本身实际大小来计算inSampleSize来进行。Glide在处理Wrap Content的ImageView的时候,会不知道如何进行PreScale需要指定好具体显示的尺寸,不然可能会引发性能问题。

Kotlin coroutine(一)异步回调的故事

Part 1, Motivation

在前端的世界里,为了保证UI的流畅性,耗时的操作都应该丢到异步任务里去。但是传统的异步回调却在实际工程里带来巨大的麻烦。

* 当多个异步任务需要组合执行的时候,嵌套起来可读性,维护性很差 ( *Callback Hell 回调地狱* )
* 界面销毁时,异步任务结束后的回调要进行判断,不然会造成异常

下面一个例子展示一个常见的开发场景下的问题:
同步用户书架,并获取最新章节标志,流程如下:

* 从数据库查询用户的本地数据
* 从服务器拉取用户云端备份数据
* 两份数据去重合并
* 更具合并后的数据请求服务端拉取最新章节信息

传统异步回调式写法:

这样的代码异常难读且非常容易出bug
但是如果代码可以改为类似如下的代码呢? 瞬间可读性,强了数倍。

val localData = fetchUserBookFromDB()
val remoteData = fetchUserBookFromRemote()
val mergeData = mergeData(localData)
val result = fetchBookUpdate(mergeData)

Part 2, 异步问题的改进史

Future/Promise/RxJava

如果每个异步函数都要传递一个callback,来处理问题很容易造成嵌套的结构,那有没有办法回调扁平化处理呢?更具这样的思路,于是就产生了promise和Future。
其实他们都是相当于封装了一个统一的类作为回调把回调从参数列表里移除并做为返回值包装了起来,就可以用来进行链式调用。
Before:

fun requestSomethingAsync(callback: Callabck<Data>)

requestSomethingAsync(object: Callback {
    override onSuccess() {
        doAnotherThingAsync(...)
    }
    override onFailed() {
    }
})

After:

 fun requestSomethingAsync():Promise<Data> {}

 requestSomethingAsync()
    .then(res -> {})
    .catch(err -> {})

async/await

链式调用虽然解决了层级嵌套的问题,但是仍然不是很优雅(想想RxJava那么多种操作符)。与是有了async和await关键字的概念,它可以把异步任务改为同步的方法进行编写。

suspend fun fetchBookUpdateInfo() {
    val localData = fetchUserBookFromDB().await()
    val remoteData = fetchUserBookFromRemote().await()
    val mergeData = mergeData(localData).await()
    val result = fetchBookUpdate(mergeData).await()
}

Part 3, Kotlin coroutine基本概念

coroutine Builder

如字面上的意思,这写类提供创建coroutine的方式
launch, async, runningBlock 等等

  • launch: 按coroutine作者Roman Elizarov说的那样,就像调用new Thread开启一个新的线程做任务,属于Fire and Forget(用完就走,即不用操心任务结果成功失败的那种) 它会立即返回一个Job,Job里拥有如下这样类似Thread的方法。
  • async: 返回的是Deferred,Deferred继承与Job,比一般Job多的是

下面是一个栗子:

coroutine context

coroutine里运行的上下文。规定运行使用的环境是什么。这里和RxJava里决定使用什么类型的线程池一样的概念。
分为Main,IO,Default,Unconfine。
Main就是android里的UI线程,IO是一个线程池使用与做IO操作。
给coroutine制定不同的context可以大幅度的简化UI和取数据的逻辑。
下面我改写上面的栗子:

我们在UI事件上进行对View的操作,在load数据的时候使用的IO的context来进行。

suspend关键字

suspend可以标志这个方法会时当前运行时挂起。suspend函数只能在coroutine里运行。

预告

改用async/await的编程形式大大简化了异步复杂的交互逻辑。并且对错误处理很友好(可以直接对catch)。下一篇将会简单介绍Coroutine背后实现的一些原理。