スピードと安心を両立!非同期レイアウトを変換すると、クライアント レイアウトのパフォーマンスが大幅に向上します

AsyncLayoutInflater は、レイアウトのパフォーマンス最適化のための新しいアイデアを提供する非同期インフレート レイアウトを提供する、Android によって公式に作成されたツールです。この記事では、このツールの使用と変換における Xiaohongshu の経験を紹介し、より多くの最適化方法について話し合いたいと考えています。

1.背景紹介

Xiaohongshu のユーザー規模の継続的な成長に伴い、アプリのパフォーマンスがユーザー エクスペリエンスに与える影響はますます重要になっています.たとえば、ページを開く速度、アプリの起動速度など、数十ミリ秒の改善.重要なビジネス データをもたらすことができます。今日紹介したいのは、公式フレームワークの実践と最適化です. この間、私は多くの落とし穴を踏みましたが、その利点もかなりのものです.

AsyncLayoutInflater は、2015 年に非同期インフレート レイアウト用の support.v4 パッケージに初めて登場しました。一般に、インフレートはメイン スレッドで実行する必要があるため、ページ初期化プロセスの主要な部分であり、時間のかかる部分です.このツールは、非同期でインフレートする機能を提供し、それによってメイン スレッドの輻輳を軽減します. この記事では、主にツールの使用方法と改善方法、および改善の際に発生したいくつかの問題について紹介します。

2.使用する

AsyncLayoutInflater の使用は非常に簡単で、依存関係を追加するだけです。

同時に、コードでの使用は次のとおりです。

非同期インフレートが完了するとコールバックが発生し、この時点でビューを使用できます。

3. ソースコード解析

このツールの最も強力な部分は、非同期のインフレータブル ビューにスレッド セーフに関連するいくつかの問題がないことです. スレッド セーフを処理する方法を見てみましょう.

まず、スレッド セーフなブロック キューとスレッド セーフなオブジェクト プールを持つ Thread の単一のインスタンスがあります。

このシングルトンの 1 つのメソッドは enqueue メソッドです。これは、ブロッキング キューの put を呼び出し、リクエストをキューに挿入します。スレッドセーフなキュー+スレッドセーフなオブジェクトプールなので、この一連の操作でスレッドセーフが保証されます。

以下がinflateの処理で、mInflateThread.obtainRequestでオブジェクトプールからリクエストを取得し、キューに入れます。

下面是一个简化过的代码,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 工程师,曾负责业务架构设计与性能优化,目前专注于交易链路的迭代与优化。

おすすめ

転載: juejin.im/post/7220230543006236731