Startalk(星语)现已在 GitHub 上全面开源, 邀君一起添砖加瓦~~~
Startalk(星语)官方网站: https://im.qunar.com/new/#/home
Startalk(星语)开源代码地址: https://github.com/qunarcorp/qtalk
***********************************************************************************
1. 背景
做为 IM 的核心部分, 会话页的展示和流畅度十分影响用户体验, 本次优化的内容正是会话里面的 Gif 图片的展示, Android 原生是没有 View 直接支持 Gif 图片播放的, Startalk 使用 Glide+FrameSequenceDrawable 实现对 Gif 的支持, 但是在使用过程中发现了一些问题, 例如在一个会话里面 Gif 图过多过大, IM 在运行一段时间后内存吃紧, 造成页面开始卡顿, 甚至 OOM 等问题, 为了解决这个问题我们通过 Android Studio 3.0 开始内置的 Android Profiler 工具来检测 Memory 的变化, 从而发现问题所在并实施优化.
2.Android Profiler 介绍
首先看一下 Android Profiler 共享时间线视图
(图片来自 developer.Android.com)
Android Profiler 现在显示一个共享时间线视图, 其中包括一个带有 CPU,MEMORY 和 NETWORK 使用情况实时图表的时间线. 该窗口还包括时间线缩放控件 3, 用于跳转到实时更新的按钮 4 以及显示活动状态, 用户输入事件和屏幕旋转事件 5 的事件时间线, 1 是连接的设备, 2 当前所选进程.
3. 问题分析
了解了 Android Profiler 后, 我们通过 MEMORY 时间线看一下在我们进入会话页后 & 当会话页有较多较大的 GIF 时我们的 IM App 内存占用对比情况, 首先看我们刚进入没有 GIF 的会话页内存占用如下
说明:
•Total: 当前所选进程占用的总内存大小
•Java: 从 Java 或 Kotlin 代码分配的对象的内存
•Native: 从 C 或 C ++ 代码分配的对象的内存
•Graphics: 用于图形缓冲区队列的内存
•Stack: 应用程序中堆栈和 Java 堆栈使用的内存, 这通常与您的应用运行的线程数有关
•Code: 应用程序使用代码和资源的内存, 例如 dex 字节码, 优化或编译的 dex 代码,.so 库和字体
•Others: 应用程序使用的内存, 系统不知道如何分类
接着我们看一下在我进入一个 Gif 比较多 (个别 Gif 图很大 20M 左右) 会话后, 滑动会话页后内存占用如下图:
从 MEMORY 时间线可以看到 Native 增加了将近 70M, 并且在显示之前已经展示过的 Gif 时 Native 内存同样还是在增长, 结束会话页后内存一直保持在一定值没有下降. 通过上面的分析得出的结论是在加载 Gif 的时候程序不断的在申请内存, 前面背景中提到我们的 Gif 时 Glide+FrameSequenceDrawable 加载的, 所以 C&C++ 申请内存的操作于应该时在 FrameSequence 中, 看一下 FrameSequenceDrawable 源码, 发现这三个 native 申请内存方法.
再看一下我们程序里面是如何使用的
- Glide.with(context)
- .load(url)
- .asGif()
- .toBytes()
- .diskCacheStrategy(DiskCacheStrategy.ALL)
- .dontAnimate()
- .into(new ViewTarget<LoadingImgView, byte[]>(mLoadingImgView) {
- @Override
- public void onResourceReady(byte[] resource, GlideAnimation<? super byte[]> glideAnimation) {
- FrameSequence fs = FrameSequence.decodeByteArray(resource);
- FrameSequenceDrawable drawable = new FrameSequenceDrawable(fs);
- view.setImageDrawable(drawable);
- }
});
这段代码是在会话列表的 adapter 中执行的, FrameSequence.decodeByteArray(resource)每次这个 view 展示的时候都会被调用到, 也就意味着每次都会申请创建 byte[] resource 长度大小的内存, 这也是重复显示同一个 Gif 时内存不断增加的原因.
接下来我们对这段代码进行优化, 使用 Cache 策略 (LruCache) 确保同一个 url 对应一个 FrameSequenceDrawable.
- Glide.with(context)
- .load(url)
- .asGif()
- .toBytes()
- .diskCacheStrategy(DiskCacheStrategy.ALL)// 缓存全尺寸
- .dontAnimate()
- .into(new ViewTarget<LoadingImgView, byte[]>(mLoadingImgView) {
- @Override
- public void onResourceReady(byte[] resource, GlideAnimation<? super byte[]> glideAnimation) {
- WeakReference<Parcelable> cached = new WeakReference<>(MemoryCache.getMemoryCache(url));
- if(cached.get() == null){
- FrameSequence fs = FrameSequence.decodeByteArray(resource);
- FrameSequenceDrawable drawable = new FrameSequenceDrawable(fs);
- drawable.setByteCount(resource.length);
- view.setImageDrawable(drawable);
- MemoryCache.addObjToMemoryCache(url,drawable);
- }else {
- if(cached.get() instanceof FrameSequenceDrawable){
- FrameSequenceDrawable fsd = (FrameSequenceDrawable)cached.get();
- view.setImageDrawable(fsd);
- }
- }
- }
});
其中 MemoryCache 为 LruCache 封装的工具类, 同时使用了 WeakReference 来保证 FrameSequenceDrawable 更容易被回收, 回收的好处是 native 申请的内存可以被销毁释放
- protected void finalize() throws Throwable {
- try {
- if (mNativeFrameSequence != 0) nativeDestroyFrameSequence(mNativeFrameSequence);
- } finally {
- super.finalize();
- }
}
我们在 Application 的 onTrimMemory(level)方法来清空 MemoryCache 里面的缓存, 触发 GC(备注: onTrimMemory(level)方法会在程序内存吃紧的时候回调到又不通的 level 级别), 我们这里设置 level>= TRIM_MEMORY_RUNNING_MODERATE, 这样在我们 Home 出程序的时候会被执行.
- public void onTrimMemory(int level) {
- super.onTrimMemory(level);
- if (level>= TRIM_MEMORY_RUNNING_MODERATE) {
- QIMSdk.getInstance().clearMemoryCache();
- }
}
然后我们重新通过 Android Profiler 查看上面同样的操作内存情况
在我退出会话页若干秒或者 Home 出去后, Native 内存瞬间降下来了, 大概回到进会话前大小.
通过 Android Profiler 对内存的分析我们优化了 Gif 的内存消耗问题, 其实通过这个工具我们还能分析出程序的不足地方, 本次针对的主要是 Native 的内存部分, 而我们内存的另一大开销 Java 堆内存也是我们优化的重点.
问题: 在分析 FrameSequenceDrawable 源码的时候我们发现 Android7.0 及以上当 view 隐藏的时候回调不到 setVisible 方法, 只做了临时处理, 有知道的小伙伴可以评论回复我.
- public boolean setVisible(boolean visible, boolean restart) {
- boolean changed = super.setVisible(visible, restart);
- //TODO 7.0 及以上特殊处理 暂时没找到其他好办法
- if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.M){
- if(visible && !isRunning() && !restart){
- restart = true;
- }
- }
- if (!visible) {
- super.setVisible(visible, restart);
- stop();
- } else if (restart || changed) {
- stop();
- start();
- }
- return changed;
}
****************************************************************************************
Startalk(星语)官方网站: https://im.qunar.com/new/#/home
Startalk(星语)开源代码地址: https://github.com/qunarcorp/qtalk
来源: https://juejin.im/post/5c6be106e51d4561776342ca