速度与安全可兼得!改造异步布局大幅提升客户端布局性能
1. 背景介绍
随着小红书用户规模的不断增长,App 性能对用户体验的影响显得越来越重要,例如页面的打开速度、App 的启动速度等,几十毫秒的提升都能带来业务数据上比较显著的收益。今天要介绍的是对一个官方框架的实践以及优化,期间踩了不少坑,但收益也很可观。
AsyncLayoutInflater 最早于 2015 年出现在 support.v4 包中,用来异步 inflate 布局。通常来讲 inflate 需要在主线程执行,所以是一个页面初始化过程中的耗时主要部分,这个工具提供了可以在异步 inflate 的能力,进而减少主线程堵塞。本文主要介绍工具的使用以及如何改进,以及改进中遇到的一些问题。
2. 使用
AsyncLayoutInflater 的使用非常简单,只需要加入一个依赖即可。
同时在代码中的使用如下:
在异步 inflate 好之后会有回调,这时候就可以使用 view 了。
3. 源码分析
这个工具最厉害的地方就在于异步 inflate view 居然没有出现线程安全相关的一些问题,下面我们就来看看它是怎么处理线程安全的问题的。
首先,里面有一个 Thread 的单例,单例里有一个线程安全的阻塞队列和一个线程安全的对象池。
这个单例里有个方法是 enqueue 方法,会调用阻塞队列的 put,将 request 插入队列中。因为是一个线程安全的队列+线程安全的对象池,所以这一系列操作就保证了线程安全。
下面是inflate的流程,inflate的时候会通过 mInflateThread.obtainRequest 从对象池里拿到一个 request,然后再将这个 request 插入队列中。
下面是一个简化过的代码,run 中有一个死循环,通过阻塞队列的 take 元素进行 inflate 的操作。
以上这个简单的工具就分析完了。这部分基本就回答了线程间如何同步数据的一个问题,在一个典型的生产者消费者模型中加入线程安全的容器即可保证。
4. 问题与改进
在使用中还是遇到很多线程相关的问题,以下列举几点相对重要的问题进行阐述。
4.1 单线程与多线程
InflateThread 在这里的设计是一个单例单线程,当需要对线程有一些定制或者收拢的话,改动就有些麻烦了,这里可以通过开放一个设置线程池的方法来提供一些线程管理和定制的能力,默认可以内置一个单线程的线程池。
通过比较长时间的实验我们发现,在主线程比较空闲的时候,单线程的效果会好一些,因为都在大核上执行了,效率更高。主线程繁忙的时候,例如冷启阶段,多线程会有更好的效率。
4.2 ArrayMap 与线程安全
我们在实际使用中发现,在一些自定义 View 的构造函数中和 darkmode 的实现中使用了 SimpleArrayMap 或 ArrayMap,ArrayMap 是 SimpleArrayMap 的子类,本身 SimpleArrayMap 是用过两个 static 的数组来实现对象的缓存,从而起到复用的作用,在多线程的情况下会有线程安全问题,这里会出现复用对象不匹配导致的 crash。一个简单的方式就是当出现 crash 的时候讲对应的 cache 数组清空,即可避免。
4.3 inflate、锁与线程安全
LayoutInflater 的 inflate 方法中有一个锁,这个导致了如果你想多线程去调用 inflate 的时候,起不到多线程的效果,如果是单线程的情况下,还可能遇到和主线程在 inflate 时同样等待锁的问题。这里 mConstructorArgs 是一个成员变量,通过重写 LayoutInflater 中的 cloneInContext 方法,配合对象池就可以避开这里锁的问题。
同时 inflate 过程中用到的这些数组和容器类型,都不是线程安全的,如果想要去掉 inflate 方法开头的 synchronize 的限制,这些线程不安全的容器类也是需要特别注意的。
4.4 BasicInflater 改造
AsyncLayoutInflater 本身有一个 BasicInflater,根据以上的一些改进点,我们在实践中对其做了一些改造,扩展出了可以设置线程池的接口,使用了基础架构提供的线程池,做到了对线程的统一管理。实践下来,在CPU比较繁忙的时候,多线程的线程池效果要好于单线程,当 CPU 比较空闲的时候,单线程的效果会更好一些,因为可以更好的利用释放出来的CPU 大核的性能。
同时重写了 ArrayMap 中线程不安全的一些处理方式,使得在多线程使用 ArrayMap 或者使用依赖 ArrayMap 的功能时不会出现 crash,这里涉及到了我们的一些自定义 View 和我们的 darkmode 的实现。
在对于 inflate 的锁和一些线程不安全的容器处理上,重写了LayoutInflater 的 cloneInContext 方法去掉了 synchronized 的限制,同时在 onCreateView 的流程中加入了线程安全的容器来保障 inflate 过程的线程安全。
综合来说就是重写了 AsyncLayoutInflater,ArrayMap 和 LayoutInflater,以达到线程安全的目的,同时将这些融入到我们的业务框架中,使得使用成本更低。
4.5 ViewCache
另一个实践是在业务侧做了进一步的封装,通过一个 ViewCache 的单例,提前将一些模块化的 View 提前 inflate 好,存在 ViewCache 中,在后续需要使用的时候从 ViewCache 中在获取,这样就避免了用的时候再 inflate 导致的耗时问题了。这块整体的代码比较简单,就不单独展开讲了,需要注意的点是有些 View 没有被使用需要及时释放,避免内存泄漏。
5. 总结
AsyncLayoutInflater 的实践与优化,前后持续了半年左右,我们在 App 冷启动和笔记详情页的性能优化中获得了超过的 20% 的性能收益以及显著的业务收益。同时,我们也将这个能力沉淀了到了业务框架中,方便了后续的接入和使用成本,通过 ViewCache 和业务框架,基本做到了可以覆盖大部分业务需求的能力。未来,我们将会在框架的易用性以及一些场景的使用上做进一步的优化,结合其他的优化手段给业务方提供更多的选择,使其能在写业务的同时无需关注这部分的耗时与复杂度,从而提升开发效率。
六、作者信息
殇不患
小红书商业技术 Android 工程师,曾负责业务架构设计与性能优化,目前专注于交易链路的迭代与优化。
链接:https://juejin.cn/post/7220230543006236731
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。