Skip to content

title: Glide图片加载框架 author: 书虫 tags:

  • 工作记录 categories:
  • 工作 date: 2021-01-08 13:58:00

结论

  • 优化内存表现最简单、有效的方案是修复内存泄露。然后才是考虑框架层的优化。因为修复一个泄露,比如阅读页的泄露,内存回收会非常明显。

“小的泄漏将击沉一艘大船。”

  • 框架层能优化的地方大部分都尝试过了,所以在制定新的优化前要慎重。

  • 多考虑为什么以前没有用这个优化方案,很可能这个优化方案会带来更多的问题,而不是性能上的收益。

  • 如果非要继续优化的话,推荐从下面这些地方入手。

    • 考虑使用硬件位图,硬件位图仅需要一半于其他位图配置的内存。硬件位图可避免绘制时上传纹理导致的内存抖动。(目前这个功能是禁用的,禁用的方式是GlideModuleImpl#applyOptions#disallowHardwareConfig;以及format(DecodeFormat.PREFER_RGB_565)https://muyangmin.github.io/glide-docs-cn/doc/hardwarebitmaps.html
    • 把所有插件中使用url加载的改成使用view加载 那些使用url加载但是无法修改成view加载的,添加clear的操作。在bitmap使用结束后clear一下及时回收资源。

针对于硬件位图以及&&ARGB_8888做了下测试。发现效果差。

  1. 实验组:DecodeFormat缺省是ARGE_8888,允许使用硬件位图,url方式加载使用的是SimpleTarget,内存的容量是默认值的1/2,

    内存情况是,打开一系列页面之后,内存维持在700M的高位,然后GC后下降到500M。

  2. 对照组:DecodeFormat缺省是RGE_565,不允许使用硬件位图,url方式加载使用的是SimpleTarget,内存的容量是默认值的1/2,

    内存情况是,打开一系列页面之后,内存维持在550M的高位,然后GC后下降到500M。

Glide定制实现方案

生命周期管理

Glide缺省支持Activity和fragment声明周期监听。

android.support.v4.app.FragmentActivity

android.support.v4.app.Fragment

使用的是SupportRequestManagerFragment来管理声明周期

android.app.Activity

android.app.Fragment

使用的是RequestManagerFragment来管理声明周期,目前RequestManagerFragment该类已经Deprecated。

但是这两种方法都不适用于iReader的Fragment框架。

针对iReader的Fragment框架定制

思路:主动调用ActivityFragmentLifecycle来通知注册的listener回调生命周期相关的方法来触发内存回收。

主要的定制流程

把Application转换成Fragment或者Activity进而正常使用Fragment生命周期的注册监听的逻辑。

为了实现这个目的,需要事先了解Glide管理生命周期的原理,梳理如下: 创建了一个SupportRequestManagerFragment,它是一个Fragment 这个Fragment构造方法中自动初始化一个ActivityFragmentLifecycle类型的成员变量 ActivityFragmentLifecycle负责管理该Fragment页面的监听器,每个Fragment页面的监听器有且仅有两个 这两个监听器是RequestManager和DefaultConnectivityMonitor。前者负责取消请求、回收资源;后者负责监听网络状态变化后重启请求 了解了Glide管理生命周期的原理,定制的思路就明确了。只要把来自于CoverFragmentManger的Fragment的生命周期方法回调桥接给ActivityFragmentLifecycle即可,这里要保证是同一个ActivityFragmentLifecycle对象。 通过ActivityBase和BaseFragment的生命周期回调来调用ActivityFragmentLifecycle的回调方法,ActivityFragmentLifecycle内部自动分发给监听器运行

针对于主TAB做特别处理

  1. 需要对主TAB(MainTabFragment)做特别处理,因为主TAB不会运行onDestroy,而如果直接使用这个Fragment为context加载图片的话,会导致书城或其他用到BookStoreFragment的TAB内存不会及时回收。内存不会及时回收的原因再次解释一下:

    • 从上面可以看出一个页面对应于一个SupportRequestManagerFragment,也就对应于一个ActivityFragmentLifecycle以及RequestManager和DefaultConnectivityMonitor。 所以想要即时回收资源就要找到对应的SupportRequestManagerFragment。

    • 由于请求图片时传的是MainTabFragment,所以onStop或onDestroy时需要传MainTabFragment而不是BookStoreFragment

  2. 所以给书城的BookStoreFragment加载图片时,不能使用MainTabFragment而是要使用对应的BookStoreFragment。这样当viewpager回收这个BookStoreFragment的时候,就会及时回收内存了

  3. 那就要找到这个BookStoreFragment。由于从MainTabFragment到特定的BookStoreFragment中间嵌套了很多层,需要一层层剥离出来。

  4. 剥离出来是比较麻烦的,具体参见下面的代码

效果验证:

测试发现,该方案和Fragment框架不兼容,不兼容原因参见wiki中的技术分享部分的PPT。

所以废弃了该方案,使用了Glide的新特性:clearOnDetach。它的作用如下:

当{@link View}从其{@link android.view.Window} 分离时,清除{@link View}的{@link Request},
并在重新连接{@link View}时重新启动{@link Request}。
<p>这是实验性API,将来的版本中可能会删除它。
<p>使用此方法可以通过允许Glide在滚动查看中切换屏幕或交换适配器时更急于清除资源来节省内存。
但是它也大大增加图像将不在内存中的几率,如果用户随后返回屏幕中先前加载图像的位置。
是否发生这种情况将取决于新屏幕中加载的图像和内存缓存的大小。
增加内存缓存的大小可以改善此行为,但在很大程度上抵消了使用此功能来降低内存带来的好处。
<p>请谨慎使用此方法,并测量您的内存使用情况,以确保它实际上是在提高您所关心的那个内存指针,比如降低内存开销,或者提高缓存命中率。

于是我就打开了这个功能,所以view方式加载的图片都缺省打开了这个功能。测试阶段没发现问题。 但是出于稳定性考虑,全量的时候我就关闭了。留给插件,尤其是书城插件这种recycleview滚动需要频繁attach、detach的场景下自行优化使用。 后续插件中可以单独打开这个功能。在BitmapCustomViewTarget7400这个类的子类的构造方法中添加一行:clearOnDetach()

项目中给url方式(非view绑定的方式)加载的时候,尝试使用了PreloadTarget,来取代以前的SimpleTarget方式。 这样做的原因是因为,原先使用SimpleTarget方式需要做clear的处理。当业务使用完bitmap之后,需要clear掉这部分资源。这个clear的操作需要业务层添加,改动量较大。 所以使用了PreloadTarget来替代它。PreloadTarget支持自动clear。它会在加载bitmap成功后自动clear掉自己的情况。 于是这个功能随着beta上线了一次。 在线发现如下异常情况:

java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@ff4a146
android.graphics.BaseCanvas.throwIfCannotDraw(BaseCanvas.java:66)
android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:277)
android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:88)
android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:548)
android.widget.ImageView.onDraw(ImageView.java:1434)
android.view.View.draw(View.java:21460)
android.view.View.updateDisplayListIfDirty(View.java:20337)
android.view.View.draw(View.java:21192)
android.view.ViewGroup.drawChild(ViewGroup.java:4388)
android.view.ViewGroup.dispatchDraw(ViewGroup.java:4149)

所以PreloadTarget的方案和现阶段的项目结合后虽然会及时清除资源,但是会导致异常。Canvas: trying to use a recycled bitmap。

理想情况下可以这么做:

  1. 把url加载方式改成view加载方式,这样Glide会自动管理clear掉view的bitmap资源。这个改动量挺大的,而且有些业务是无法修改的。
  2. 如果1无法实现。退而求其次,可以继续使用SimpleTarget,但是这样就需要在业务层使用完毕后,及时clear掉bitmap资源,这其实做的是Glide应该做的事。这个改动量也挺大的,而且难点是要一个个的梳理出来,全部改一边,而且个别情况下,找不到使用完毕的时机无法清理。
  3. 如果1和2都无法实现。只能是使用SimpleTarget,然后不做clear的处理。这样会导致申请的bitmap逐渐积累起来,等待activity的onDestroy的时候清理一次,或者trimOnSize的时候清理。无法做到及时清理。而且还会导致加载图片的request请求所占用的资源无法及时清理掉。

通用的优化方法

  1. context尽量传Fragment,获取不到Fragment则传Activity,最后才是application
  2. 能绑定控件的就传控件,Glide会根据view的Width、Height按需加载bitmap
  3. 解码格式尽量不使用高清图格式,比如使用Bitmap.Config.RGB_565
  4. 解决内存泄露问题,避免导致内存无法及时回收的情况

针对项目的优化方法: 上文中根据支持Fragment生命周期回调也是一项重要的性能优化,有利于Glide及时回收内存。

性能优化很重要的一部分是内存优化。

内存优化的时候,先完成了下文中的优化点1、2、3。

即便这三个优化点都支持了,但是发现native层的内存还是不能及时回收。native层的内存一般都在100MB以上,加上Graphic层的内存也在100MB以上。整体内存也就不可能低于300MB。

native层和Graphic层的内存占用如果不能大幅减少的话,内存的整体开销就会较大。(native层是android 8.0+以上保存bitmap的地方)

要想减少native层和Graphic层内存,不能直接回收,只能间接回收,要考虑到bitmap在java层的缓存,也就是LruResourceCache(MemoryCache),要降低就是这部分内存。

在我的Google Pixel 3A手机上,使用Glide缺省的缓存大小(2个屏幕且每个像素4个字节)大约是17MB。也就是LruResourceCache(MemoryCache)是17MB。

很显然这里存放的并不是bitmap占用的真实内存,这里放的是bitmap的属性信息。但是如果能及时回收掉这里的bitmap的,那也就会间接释放掉native层的内存了。

基于此做了优化点4.

做了优化点4之后发现native层的内存确实会降低,比较明显,但是会导致内存抖动明显。所以trimToSize(maxSize/8)不适合,应该调大一些,比如trimToSize(maxSize/4)。

另外,优化点5也是为了尽可能减少native层的内存,但是优化点5做不到框架层统一封装处理,只能在业务层主动处理。

疑问:

  • 既然置于后台的时候Glide可以明显释放一部分内存,那参考它使用的方式,主动在Fragment#onDestroy的时候调用trimOnSize是不是也可以释放内存?
  • 置于后台的时候,为什么即便是Glide也不能明显回收掉native层的内存?
  • Fragment在onDestroy的时候会主动释放内存,那为什么内存下降不明显呢?

优化点1:根据控件的尺寸决定加载图片的尺寸

  1. 首先获取的是调用传入的overrideWidth、overrideHeight的值
  2. 如果这两个值有效,则进入加载流程
  3. 如果这两个值无效,比如是-1,则尝试获取尺寸,具体步骤是(以获取width为例)
    • 通过view获取到LayoutParams.width,padding值和view.getWidth
    • 先判断LayoutParams.width - padding是否有效,有效则返回
    • 判断view.getWidth - padding是否有效,有效则返回
    • 如果均无效,这是普遍情况,刚加载的时候LayoutParams.width和view.getWidth这些值都是0,则进入等待下次获取过程
    • 测量是通过ViewTreeObserver.addOnPreDrawListener的方式,在onPreDraw方法中触发的再次获取,获取过程从步骤a开始
  4. 测试发现,项目中能获取控件真实尺寸的情况大都是在onPreDraw方法中触发的再次获取的
  5. 以前封装框架的时候没有把view的LayoutParams参数带上,LayoutParams为空,导致加载的bitmap是原始尺寸
  6. 这次给这些方法加上了Deprecated标签,不推荐使用
  7. 看是否根据控件大小加载bitmap,需要看到如下日志才可以证明是按照控件大小加载的
    • 用TAG CustomViewTarget过滤日志,需要打印:
V/CustomViewTarget: OnGlobalLayoutListener called attachStateListener=com.bumptech.glide.request.target.CustomViewTarget$SizeDeterminer$SizeDeterminerLayoutListener@667a3d9
- 用TAG GlideRequest过滤日志,需要打印:
V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5328.244957999999 this: 103137153
- 注意步骤b中的宽高的值
- 用TAG Downsampler过滤日志,需要打印:
V/Downsampler: Calculated target [330x413] for source [728x910], sampleSize: 2, targetDensity: 1769323234, density: 1949254399, density multiplier: 0.9076923

支持按照控件尺寸加载bitmap后,用TAG GlideRequest过滤日志,打印的日志如下

2020-09-23 11:33:52.559 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 69.489642 this: 239600185
2020-09-23 11:33:57.871 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5374.561889 this: 241593214
2020-09-23 11:33:57.874 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5352.17246 this: 157280429
2020-09-23 11:33:57.876 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5352.557355999999 this: 39924192
2020-09-23 11:33:57.877 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5328.010947 this: 129224862
2020-09-23 11:33:57.878 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5328.244957999999 this: 103137153

支持按照控件尺寸加载bitmap后,用TAG DownSampler过滤日志,打印的日志如下

V/Downsampler: Calculated target [330x413] for source [728x910], sampleSize: 2, targetDensity: 1769323234, density: 1949254399, density multiplier: 0.9076923
V/Downsampler: Calculated target [904x1130] for source [728x910], sampleSize: 1, targetDensity: 2147483647, density: 1729389506, density multiplier: 1.2417582

如果不支持按照控件尺寸加载bitmap,用TAG GlideRequest过滤日志,打印的日志如下

2020-09-23 12:04:37.598 20840-20840/com.chaozh.iReaderFree V/GlideRequest: onSizeReady width=-2147483648, height=-2147483648 , Got onSizeReady in 0.053542 this: 175187022
2020-09-23 12:04:37.598 20840-20840/com.chaozh.iReaderFree V/GlideRequest: onSizeReady width=-2147483648, height=-2147483648 , Got onSizeReady in 0.019688 this: 49803077
2020-09-23 12:04:37.600 20840-20840/com.chaozh.iReaderFree V/GlideRequest: onSizeReady width=-2147483648, height=-2147483648 , Got onSizeReady in 0.010104 this: 170736321

通过日志看到了如下奇葩情况

  1. 要加载的bitmap尺寸比原图大
Calculated target [904x1602] for source [578x1024], sampleSize: 1, targetDensity: 2147483647, density: 1373059236, density multiplier: 1.5640138
  1. 传进来的尺寸是默认值,
Calculated target [-1x-1] for source [-1x-1], sampleSize: 1, targetDensity: 0, density: 0, density multiplier: 1.0

优化点2:滑动、抛动列表时暂停加载、停止之后恢复加载

RecyclerView在上下滑动的时候,会复用holder,进而复用holder中的view。

由于Glide会把view和加载请求绑定,所以在滑动recyclerView的时候,就会出现复用的view需要先解绑原来的请求,然后绑定新的请求的情况。

在解绑原来的请求的时候,Glide会把这部分资源释放掉,把bitmap放到LruCache中。这是Glide本身就支持的功能。

不仅限于RecyclerView,其实是view复用的场景都适用。

滑动列表时暂停加载的机制和上面描述的view复用的场景不同,但是会有相互的影响。滑动列表时暂停加载的原理是这样的。

滑动列表时,会不停的bindHolder进而产生一系列的加载请求。Glide内部使用数组把这些请求管理起来了。

如果列表处于滑动、抛动状态,则标记一个暂停加载的flag。此时,一些列的家在请求都不会被真正的处理,而是放在队列里处于等待状态。

当列表处于停止状态时,则标记一个恢复加载的flag。此时会遍历列表,把等待状态的请求恢复。

注意,滑动列表暂停加载,停止滑动恢复加载的功能只对加载中的请求有效,已经加载完毕的请求不受影响。

这个功能激活之后有个问题。

滑动列表会暂停加载,所以滑动过程中看到的被复用的holder的图片其实是缺省图。因为被复用的holder在onViewRecycled方法中清理掉了bitmap。

而滑动时又不会加载bitmap,导致绘制的是缺省图。

只有停止滑动列表后才会加载真正的图片,如果加载的图片来自内存缓存,则不会做动画而是直接出现。这样从缺省图直接显示成真实图,会导致视觉上的闪现。

这2情况的视觉效果都是无法接受的。

基于此对Glide做了定制。

定制思路:

激活“滑动列表暂停加载”功能后,滑动列表允许继续加载(定制前不会继续加载)。

不过这种情况下仅允许从缓存(内存缓存 + 磁盘缓存)中加载。

等列表滑动结束后,加载图片允许从缓存+网络加载。

优化点3:CustomTarget导致的内存无缓存

不推荐使用这种方法:

public void get(String requestUrl, final ZyImageLoaderListener listener, int maxWidth, int maxHeight, android.graphics.Bitmap.Config config)

这种调用方式性能很差,

  1. 因为没有绑定view,所以无法获取控件大小,导致只能加载原图
  2. context没有传,也就无法监控生命周期,也就没有销毁的触发时机,导致customTarget加载bitmap不会放回到LruCache。
  3. 不放到LruCache中,就无法被复用
  4. 每次都会新建,但是不会被复用

定制之后出现新的问题:

问题是Glide.with()传入Fragment时,内部会用到handler,而我们的Fragment框架定制的原因,导致获取Fragment.mHost(FragmentHostCallback)的handler是null。

为了支持这个功能就需要修改mHost,影响较大。

另外在性能上这里使用了BitmapCustomViewTarget7400#clearOnDetach方法代替本功能。殊途同归,效果一样。

优化点4:在Fragment的onDestroy时,及时给MemoryCache调用trimToSize(maxSize/2),及时回收内存

该方案依赖于Fragment生命周期绑定,目前不支持Fragment生命周期绑定。支持activity生命周期绑定,有一定效果。

优化点5:有些图片,只展示一次的那种,使用glide可以设置跳过缓存,skipMemoryCache,用完就给它回收(未激活)

比如弹窗图片。

图片被缓存起来之后如果不能复用,就会占用cache的空间,cache里是强引用。正常情况下占用的内存是不会被释放的。

优化点6:Glide自带功能(功能介绍)

Glide内部注册了ComponentCallbacks2接口,这个功能是从Glide版本4.0+开始支持的。Glide初始化的时候会主动注册该接口。

当APP置于后台时,系统会调用onTrimMemory(TRIM_MEMORY_UI_HIDDEN)方法,Glide会响应这个方法把释放内存。

trimToSize(getMaxSize() / 2);

下面是效果图:

内存从打开时的380MB下降到了270MB。

优化点7:项目中使用VolleyLoader加载的老代码,缓存会放到Volley的缓存中

目前这部分的缓存大小被进一步减小了

final int maxMemory = Math.min(activityManager.getMemoryClass() * MB, Integer.MAX_VALUE);
if(maxMemory < 64 * MB) {
    return MB >> 1;
} else if(maxMemory < 128 * MB) {
    return MB;
} else if(maxMemory < 256 * MB) {
    return MB << 1;
} else {
    return 3 * MB;
}

maxMemory和一屏幕所占内存谁小用谁。

优化点8:广告插件加载图片优化(有专门的PPT分享,去本站其他目录寻找)

  1. 代码上,目前的加载没有传控件,没有传尺寸,导致加载原图,甚至有的是加载屏幕宽、高的图
  2. 下图是有问题的代码截屏
    • 明明有ImageView但是加载图片时却不传给Glide使用,导致Glide无法做优化
    • 宽、高传的太大
    • 图片解码格式传的是高清的,一像素会占用4个字节,有必要这么高清吗,是否有优化空间
    • 使用的时候没有封装统一的方法

广告插件存在问题的原因分析:

  1. 广告在使用bitmap时有特殊需求:加载bitmap + 结果回调 + 调用先后顺序(图片加载成功之后再显示控件,正常情况下是先显示控件再加载图片)
  2. 使用过程中没来记得考虑加载图片优化问题,而选择了性能差的使用方式

优化:

  1. 添加BitmapUtil类,统一封装加载图片的方法
  2. 添加加载方法,满足这种特殊需求:加载bitmap + 结果回调
  3. 按照固定尺寸加载:把view传给Glide
  4. 支持限制加载图片的最大宽、高,避免某张广告图片过大导致的OOM

优化点9:各个广告位加载图片的情况统计分析

限制可加载的广告图片的最大宽高720*1440 底通之类的广告,限制图片的最大高度是48dp,由于存在不同的宽高比的问题,所以只能指定高度的最大值,而无法指定宽度的最大值

底通广告,控件尺寸已知,尺寸小,但是加载的广告图片是1280 × 720

插页广告,控件尺寸已知,尺寸中等,但是加载的广告图片是1280 × 720

其他广告,控制尺寸未知,尺寸中等,但是加载的广告图片是1280 × 720

以上尺寸的图片优化后尺寸是720 x 405

更多示例:

底通广告优化后:

BitmapTransformation: transform from (width, height)=(1080, 1920) to (74, 133)

BitmapTransformation: transform from (width, height)=(654, 933) to (93, 133)

BitmapTransformation: transform from (width, height)=(640, 960) to (88, 133)

优化前,解码阶段的日志情况:

Decoded [1280x720] RGB_565 (3686400) from [1280x720] image/jpeg with inBitmap [1280x720] RGB_565 (3686400) for [-2147483648x-2147483648], sample size: 1, density: 0, target density: 0, thread: glide-source-thread-3, duration: 13.797189


Decoded [1280x720] RGB_565 (3686400) from [1280x720] image/jpeg with inBitmap [1280x720] RGB_565 (3686400) for [-2147483648x-2147483648], sample size: 1, density: 0, target density: 0, thread: glide-disk-cache-thread-0, duration: 16.571147

Decoded [1280x720] RGB_565 (3686400) from [1280x720] image/jpeg with inBitmap [1280x720] RGB_565 (3686400) for [-2147483648x-2147483648], sample size: 1, density: 0, target density: 0, thread: glide-disk-cache-thread-0, duration: 20.53021

Decoded [1280x720] RGB_565 (3686400) from [1280x720] image/jpeg with inBitmap [1280x720] RGB_565 (3686400) for [-2147483648x-2147483648], sample size: 1, density: 0, target density: 0, thread: glide-source-thread-0, duration: 24.960732

目前不支持的优化点

UnionPagesFetcher不支持根据控件大小按需加载。因为里面有两个控件,动态显示,无法提前预知。

UnionPagesFetcher是阅读页插页、段间广告的模版,该模版加载完图片之后供段间、插页广告显示

UnionPagesFetcher里加载图片时,由于控件的尺寸无法提前确定,需要加载完图片之后,根据图片是大图还是小图才能确定控件的尺寸,所以这里无法提前知道控件尺寸,也就无法做到按需加载UnionPagesFetcher使用很频繁,但是目前只能做到加载宽最大是720,高最大是1440的图。

优化点10:把CustomTarget改成PreloadTarget

PreloadTarget优于CustomTarget的地方是,前者加载完成之后就会自动清理掉

该方案被验证不可行。

高低版本兼容处理

问题1:低版本上书城频道是按照图片的原始尺寸加载的

加载请求的日志:

2020-09-25 19:57:35.093 11157-11157/com.chaozh.iReaderFree V/GenericRequest: Got onSizeReady width=-2147483648, height=-2147483648, in 0.051771 this: 117751467

宽、高给的是一个特殊的初始值,该值会导致直接加载原始尺寸的bitmap,直接略过获取控件尺寸的步骤。

解码bitmap的日志:

2020-09-25 19:57:35.953 11157-11657/com.chaozh.iReaderFree V/Downsampler: Decoded [300x400] RGB_565 (240000) from [300x400] image/webp with inBitmap null for [-2147483648x-2147483648], sample size: 1, density: 0, target density: 0, thread: fifo-pool-thread-7, duration: 245.66122299999998

从日志可以看出,加载和解码的都是原始尺寸的图片。

适配上已经支持低版本根据控件的尺寸来加载bitmap,所有插件都已经修改,而且插件都可以配到可支持的最低版本。

适配方法:

要了解根据控件尺寸加载bitmap的原理才能进行适配。原理参见第四部分的优化点1。 适配方法就是直接把真实的view传过去,而不是把封装的ZyImageTargetView传过去。 传过去真实的view之后,如果view在业务层调用了setTag方法,由于Glide内部也使用了setTag方法,导致两者冲突,我认为这是Glide V3.x版本设计上的缺陷 4.x版本的Glide解决了setTag和应用层冲突的问题,所以我适配的时候参考了Glide V4.x.0版本的方案。 方案很简单,就是使用setTag(int id,Object obj)方法,给Glide定义一个专属的id,这样应用层不论是否传了id都不会冲突了 其实3.x版本的Glide也提供了这种方法ViewTarget#setTagId(int tagId),但是这个方法需要App启动的时候调用(调用时机晚了就失效了),而且只能调一次(否则崩溃),所以从兼容低版本角度考虑只能反射了

高版本和适配后的低版本都已经支持根据控件的尺寸按需加载bitmap。

问题2:插件中打iReader_plugin的jar

为了方便打jar包,保留了主工程中的GlideV3.5.2版本的库,这个库从iReaderV7.38.0开始已经废弃不用。 GlideV3.5.2版本的库本应该删除掉,但是删除之后打的jar包会有编译错误,无奈只能保留这个库。 代码中已经添加了废弃的注释。不推荐使用。推荐使用com.bumptech.glide.Glide。

问题3:插件中添加一个glide-4.9.x的jar包(provided-libs文件夹下)

该jar仅供编译,不应该打包到插件中。插件运行的时候使用的是主工程的库。

问题4:把定制和扩展的便利性留给插件(当然主工程本身就支持这些便利性)

不论是iReader的高低版本,插件中都能拿到Glide的类,都支持定制和扩展 只不过插件中在使用时要注意区分iReader的版本,注意以下关键版本 V7.9.0 引入Glide V3.5.3 V7.38.0升级Glide到V4.9.0 现在插件中都有一个provided方式的glide的jar包,该jar仅参与插件的编译,该jar包是从iReader V7.38.0加入的,支持插件中使用Glide扩展 插件中自定义view可以直接交给Glide去加载,只需要简单处理一下即可,处理如下 new ViewTarget<CustomView, Bitmap>(customView的实例) 或者new CustomViewTarget<CustomView, Bitmap>(customView的实例) 然后复写Target的onResourceReady方法即可拿到bitmap,支持判断是否来自缓存 把bitmap传递给自定义view显示即可

遇到的一些反常现象

现象1:使用Glide V3.7.5 + 根据控件尺寸大小加载bitmap时的反常现象

支持按照控件的尺寸加载bitmap之后,加载出来的bitmap还是比控件尺寸大,日志:

2020-09-25 20:35:16.269 13135-13771/? V/Downsampler: Decoded [300x400] RGB_565 (240000) from [300x400] image/webp with inBitmap [300x400] RGB_565 (240000) for [262x341], sample size: 1, density: 0, target density: 0, thread: fifo-pool-thread-4, duration: 21.315106

可以看出来是给尺寸为 [262x341]的控件加载的,结果解码后的尺寸是[300x400],原因是什么呢?

解码之前会先测量出bitmap原始的尺寸,此时原始尺寸是[300x400] 根据原始尺寸和目标尺寸(控件尺寸)计算采样的比例 此处采样的比例为1,所以最后解码出来的bitmap尺寸就是原始尺寸[300x400] 类似的有(左边是解码后的bitmap尺寸,右边是控件的尺寸)

[300x400]  → [132x176](这个原始尺寸是[300x400]压缩比是1,垃圾,没有优化)
[300x400] → [165x2088](注意这个控件的高度是2088,而不是280)
[150x200] →  [132x176] (这个原始尺寸是[300x400]压缩比是2,是一个不错的优化效果)

优化点:使用Glide V4.9.0 + 根据控件尺寸大小加载bitmap时的性能提升

对比上文低版本的日志,发现高版本确实有显著的提升,基本上都非常接近控件的尺寸。

高版本上的表现如下:(左边是解码后的bitmap尺寸,右边是控件的尺寸)

[132x176] →  [132x176] (这个原始尺寸是[300x400]压缩比是2,是一个不错的优化效果,而且比以前尺寸更小) duration: 10.454532
[121x161] → [121x161] (这个原始尺寸是[300x400]压缩比是2,是一个不错的优化效果,而且比以前尺寸更小) duration:10.167657
[358x477] → [358x470](这个原始尺寸是[300x400]压缩比是1,图片被拉伸了,这是控件比图片尺寸大的情况) duration: 21.670575
[321x428] → [318x428](这个原始尺寸是[300x400]压缩比是1,图片被拉伸了,这是控件比图片尺寸大的情况) duration: 29.638752999999998

高版本上的这种“反常"的高性能是怎么做到的呢?值得研究一下。

具体原理参见:

https://blog.csdn.net/weixin_34416649/article/details/91372458

现象2:书城Tab切换频道 + ViewPager#offsetPageLimie(1) + Fragment#onDestroy + Glide回收内存

这种场景下,如果频繁切换频道会导致明显的卡顿。

卡顿原因是Fragment会被重建。

而且Glide回收内存效果不明显。

分类Ttab也有同样的问题。

这个现象和预期的不一样,预期的是:内存会有明显下降。

遗留的问题

针对漫画的定制,目前漫画使用的还是Volley的memoryCache,这部分有很大优化空间 项目中一些老代码仍然在使用Volley的memoryCache,这部分代码需要修改成使用Glide管理

iReader使用Glide更新说明

Glide是从TAG V4.10.0开始迁移到了Android X的。

  1. iReader是从7.9.0版本开始引入的Glide,引入的版本是V3.5.2

  2. iReader从7.10.0版本开始在AndroidManifest.xml中加入了全局配置类

    a. 设置了缓存路径和大小

    b. 缺省图片格式565

    c. 网络请求客户端改成了OkHttp

  3. iReader从7.38.0版本开始升级Glide版本到V4.9.0,后续iReader迁移到Android X之后,再升级到更新的版本上去

iReader本次升级添加特性有

根据控件尺寸按需加载图片,避免直接加载原始尺寸 加载时context绑定的是当前页面的Fragment,方便生命周期管理 页面关闭后及时回收页面占用的内存 释放该页面占用的网络请求对象,回收bitmap资源并放到缓存池 移除缓存池中不再使用的资源,减少内存 修复了一批内存泄露问题 系统可分配的内存不足时,及时回收内存,避免出现OOM App置于后台后大幅度回收内存,待定 修改缺省的缓存池的大小,减少一半,待定 滑动列表时停止图片加载,停止滑动后恢复图片加载,待定

Glide从3.5.2升级到4.9.0有以下更新

v4.1.0缺省情况下从O +的低内存设备中删除了BitmapPool,并减小了O +的缺省BitmapPool大小(bb5c391)在Android O +中添加了对Bitmap.Config.HARDWARE的支持。 v4.0.0-RCO各种性能改进,包括大幅减少对图像进行下采样时的垃圾处理,更智能的缺省磁盘缓存策略以及加载GIF时的性能提高。改进了对查看大小和布局的处理,尤其是在RecyclerView中 v3.8.0更好地捕获OOM异常导致的崩溃 bug修复 Glide4.11.0版本值得期待的新特性(目前iReader使用的版本是4.9.0)

在应用程序中不经常使用Glide时,为Glide的线程添加线程超时以减少内存 在资源ID缓存键(1b391c4)中包括昼/夜模式。

添加可以在Glide上手动调用的API以清除内存,尤其是在后台运行应用程序的情况下

在M +(525e7ba)上在后台释放更多内存

Glide原理介绍

debug模式下查看日志,打开如下命令即可。

adb shell setprop log.tag.Glide VERBOSE

adb shell setprop log.tag.Engine VERBOSE

adb shell setprop log.tag.DecodeJob VERBOSE

adb shell setprop log.tag.MemorySizeCalculator VERBOSE

adb shell setprop log.tag.GlideProfile VERBOSE

adb shell setprop log.tag.GlideRequest VERBOSE

adb shell setprop log.tag.CustomViewTarget VERBOSE

adb shell setprop log.tag.LruBitmapPool VERBOSE

adb shell setprop log.tag.Downsampler VERBOSE

adb shell setprop log.tag.RequestTracker VERBOSE

adb shell setprop log.tag.TransformationUtils VERBOSE

一、加载网络图片、解码bitmap

以bitmap格式加载网络图片成功后的回调顺序从上到下依次是:

DecodeJob#run#runWrapped

DecodeJob#runGenerators...INITIALIZE...

...

DecodeJob#runGenerators(SWITCH_TO_SOURCE_SERVICE)

DataCacheGenerator#startNext

OkHttpGlideUrlFetcher#loadData

OkHttpGlideUrlFetcher#onResponse

ByteBufferFileLoader#ByteBufferFetcher#loadData

DataCacheGenerator#onDataReady

SourceGenerator#onDataFetcherReady


bitmap图片decode的顺序从上到下依次是:

DecodeJob#decodeFromRetrievedData

DecodeJob#decodeFromData

DecodeJob#decodeFromFetcher

DecodeJob#runLoadPath

LoadPath#load

LoadPath#loadWithExceptionList

DecodePath#decode

DecodePath#decodeResources

DecodePath#decodeResourcesWithList

ByteBufferBitmapDecoder#decode

Downsampler#decode

详细的decode过程参见Downsampler#decode方法。

二、解码生成bitmap文件,参与bitmap解码的主要类和方法有:

DecodeJob#decodeFromFetcher
DecodePath#decodeResourceWithList
ByteBufferBitmapDecoder#decode

Glide使用model类下载图片成功后会拿到一个InputStream,在对InputStream解码之前会做一些优化。

从IO中解码一个bitmap文件,通过BitmapFactory.decodeStream(is, null, options); 具体实现参见Downsampler#decodeFromWrappedStreams

在解码之前Glide额外做了如下优化:

1. 尝试向下采样,options.inSampleSize = powerOfTwoSampleSize;
2. 调整bitmap缩放比例
3. 调整bitmap的像素密度options.inDensity
4. 给options.inBitmap设置了一个可复用的bitmap。Glide做的额外优化就是给options.inBitmap设置了一个可复用的bitmap。 
因为一旦在Options中设置inBitmap,解码方法将尝试在加载内容时重用此bitmap。具体使用方式是:
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
而这个bitmap是通过一个bitmap pool来管理并获取的。bitmap pool的具体实现参见LruBitmapPool#getDirty。
5. 自动校正显示角度

三、Glide V3.5.2和V4.9.0解码图片时裁剪功能对比

V3.5.2版本解码bitmap的原理
  1. 解码出bitmap的原始宽、高

  2. 计算bitmap的采样率

    • 用请求的宽比bitmap原始的宽sourceWidth,都是int类型

    • 用请求的高比bitmap原始的高sourceHeight,都是int类型

    • 取两者中的最小值,即为inSampleSize

    • inSampleSize参数取值应该为2的幂,即1、2、4、8等,若给的值不为2的幂,则会取一个比给的值小的最大的2的幂来代替。

    • 例如inSampleSize参数取值为10,则会用8来代替。但这个结论并非在所有系统上成立,因此此处应该严格控制,否则会得到意想不到的结果。

  3. 选取合适的颜色信道,如果图片有alpha信道,即便明确使用RGB_565也无效,Glide内部自动转成ARGB_8888

  4. inBitmap复用+BitmapFactory.decodeStream即可获取到目标bitmap

V4.9.0版本解码bitmap的原理
  1. 解码出bitmap的原始宽、高
  2. 计算bitmap的采样率
    • 用请求的宽requestWidth比bitmap原始的宽sourceWidth,都是float类型
    • 用请求的高requestHeight比bitmap原始的高sourceHeight,都是float类型
    • 取两者中的最大值,得到一个缩放因子exactScaleFactor,float类型
    • 使用scaleFactor和原始宽sourceWidth、高sourceHeight计算出目标宽outWidth、高outHeight,结算结果四舍五入,使得计算出的目标宽outWidth、高outHeight是int类型
    • 使用原始宽/目标宽sourceWidth/outWidth;原始高/目标高sourceHeight/outHeight,都是int类型。得到宽、高的缩放比widthScaleFactor、heightScaleFactor
    • 取widthScaleFactor、heightScaleFactor的最小值,得到一个int类型的缩放比scaleFactor
    • 根据scaleFactor获取inSampleSize的值。inSampleSize参数取值应该为2的幂,即1、2、4、8等,若给的值不为2的幂,则会取一个比给的值小的最大的2的幂来代替。算法同V3.5.2。
    • 根据不同的图片类型处理略有不同,以PNG为例。用bitmap原始的宽sourceWidth/(float)inSampleSize结果向下取整,得到int类型的宽powerOfTwoWidth。同样的处理得到powerOfTwoHeight。
    • 再计算一次缩放比,步骤同a、b、c。只不过这次使用powerOfTwoWidth代替requestWidth,powerOfTwoHeight代替requestHeight。得到一个缩放因子adJustedScaleFactor,float类型。
    • 使用adJustedScaleFactor的值来计算options#inTargetDensity和options#inDensity。如果这两个值可以使用则会进一步压缩略图片的尺寸。注意:这一步就是性能上高版本优于低版本的地方。
  3. 选取合适的颜色信道,如果图片有alpha信道,即便明确使用RGB_565也无效,Glide内部自动转成ARGB_8888
  4. inBitmap复用+BitmapFactory.decodeStream即可获取到目标bitmap