使用OkLayoutInflater优化RecyclerView的布局加载,使得首帧渲染耗时65ms->35ms
,滑动帧率45fps->52fps
。
一、Android View开发现状
传统XML布局
Android开发长期以来采用XML布局文件的方式构建UI,这种方式存在以下特点:
- 运行时inflate:在应用运行时,通过LayoutInflater将XML解析为View对象
- 主线程阻塞:inflate过程在主线程执行,涉及IO操作和View树构建
- 复杂布局的inflate耗时显著,特别是在RecyclerView等需要快速创建大量ViewHolder的场景
自定义View开发方式及其缺点
虽然自定义View可以避免XML inflate的开销,但存在开发效率低的问题:
- 需要手动编写所有View的创建、布局、测量逻辑
- 缺乏可视化预览,难以直观看到UI效果,每次UI调整都需要重新编译和运行
- 调试困难,中大型Android项目增量运行普遍在1分钟以上
现代开发体验对比
Jetpack Compose优势:
- 实时预览:支持编译器实时预览(@Previewb标签)和实体机热重载,开发效率大幅提升
- 声明式UI:代码即UI,减少XML解析开销
- 编译时优化:编译器优化减少运行时开销
iOS开发体验:
- Xcode运行快:即使使用UIKit这样的传统UI,也可以快速运行,查看实际效果
相比之下,传统Android XML布局开发在开发效率和运行时性能方面都存在改进空间。
二、优化措施
掌阅X2C
GitHub - iReaderAndroid/X2C: Increase layout loading speed 200%
2018年开源,原理是在编译阶段,使用APT(Annotion Processor Tool),将XML解析成对应的Java文件。
xml代码
ini
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="10dp">
<include
android:id="@+id/head"
layout="@layout/head"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true" />
<ImageView
android:id="@+id/ccc"
style="@style/bb"
android:layout_below="@id/head" />
</RelativeLayout>
编译后生成的Java代码:
ini
public class X2C_2131296281_Activity_Main implements IViewCreator {
@Override
public View createView(Context ctx, int layoutId) {
Resources res = ctx.getResources();
RelativeLayout relativeLayout0 = new RelativeLayout(ctx);
relativeLayout0.setPadding((int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,res.getDisplayMetrics())),0,0,0);
View view1 =(View) new X2C_2131296283_Head().createView(ctx,0);
RelativeLayout.LayoutParams layoutParam1 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
view1.setLayoutParams(layoutParam1);
relativeLayout0.addView(view1);
view1.setId(R.id.head);
layoutParam1.addRule(RelativeLayout.CENTER_HORIZONTAL,RelativeLayout.TRUE);
ImageView imageView2 = new ImageView(ctx);
RelativeLayout.LayoutParams layoutParam2 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,(int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1,res.getDisplayMetrics())));
imageView2.setLayoutParams(layoutParam2);
relativeLayout0.addView(imageView2);
imageView2.setId(R.id.ccc);
layoutParam2.addRule(RelativeLayout.BELOW,R.id.head);
return relativeLayout0;
}
}
优点:
- 性能高,没有加载 XML 的 IO 和递归解析、反射创建过程。
缺点:
- View 适配成本高,尤其自定义 View ,需要配置属性对应的方法。
- 不支持 Merge 标签,无法查询系统 style,所以只支持应用内 style。
- 由于 APT 本身的特性,在 XML 发生变化时,对应注解处理器生成的 Java 构建文件不会同步发生变化, 对于不熟悉的同学来说容易踩坑。
ViewCompiler
Google 在Android10 加入了一个实验性功能 ViewCompiler,可以手动地将 xml 布局转化为 java 文件或者 dex 文件,但它并不支持 merge 和 include 标签。

一直到Android12
,此功能都没有开启。同时在Android 15
上,LayoutInflater.java
中,相关代码已经完全去除。
所以ViewCompiler
对于开发者而言,无法直接使用。不过有些团队参考ViewCompiler+X2C
,做了自己的xml to java
的编译插件,比如《小红书-首页性能优化》,他们解决了不支持merge\include
标签等问题,并且在线上大规模使用。但是他们没有开源,所以我们也无法使用。
类似的方案:鸿洋:Android "退一步"的布局加载优化
优点:
官方支持
缺点:
代码已经去除,无法使用
AsyncLayoutInflater
Android Developers Jetpack AsyncLayoutInflater
AsyncLayoutInflater
是Google官方提供的异步inflate布局的方式,他与X2C
编译器转换不同,他是在运行期
的优化。
主要就是将inflate操作放在异步线程操作,这样可以把IO耗时转移到异步线程,主线程可以执行其他操作,通过异步加载完成后,通过回调设置到页面上。
优点:
运行时异步加载,不会影响编译速度
对于页面的debug无影响,如Layout Inspector可以正常查看、定位
缺点:
- 异步线程只有一个,单线程 (
FixedThreadPool(1)
) - 任务不支持取消,在退出页面后可能会有资源浪费
- 不支持设置Factory,无法使用AppcompatView的样式
- 阻塞队列容量固定,默认10个,超过10个,会导致主线程异常
- 有可能会丢失Activity主题
网上有很多对AsyncLayoutInflater的二开方案Android AsyncLayoutInflater 限制及改进,但是在Kotlin下,有了新的更好用的OkLayoutInflater
方案,所以这里不展开解释。
三、OkLayoutInflater介绍
OkLayoutInflater结合了Kotlin协程的优势,解决了AsyncLayoutInflater的大部分问题。
OkLayoutInflater的源码非常简单,可以直接查看:OkLayoutInflater.kt
其中,注释中直接说明了他解决的AsyncLayoutInflater的一些缺陷:

重要核心逻辑:
1.使用协程的SupervisorJob+Dispatcher.Default
ini
private val coroutineContext = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + coroutineContext)
优点 | SupervisorJob可以保证协程作用域下某个子协程的异常,不会导致整个协程的取消 |
缺点 | 1.调度器使用的是Default,这个默认是8线程的,并且是用来处理CPU密集型任务的 2.Default是全局调度器,可能会有其他业务的耗时任务排在前面。而View的初始化是高优任务,这样反而会导致页面加载速度变慢。 3.Default调度器,如过某些业务逻辑有问题,比如Synchronized锁异常,会导致线程阻塞,可用线程数变少。 |
改进 | 使用自定义线程池(后面会介绍) |
2.生命周期感知
当Activity/Fragment退出,View Detachde 时,会自动清理资源,主要是取消协程任务。
kotlin
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
cancel()
}
}
// 取消协程
fun cancel() {
coroutineContext.cancel()
coroutineContext.cancelChildren()
}
3.智能降级策略
异步线程加载失败,会自动回滚到主线程加载
kotlin
private suspend fun inflateView(
@LayoutRes resId: Int,
parent: ViewGroup?,
): View = try {
mInflater.inflate(resId, parent, false)
} catch (ex: RuntimeException) {
Log.e(TAG, "The background thread failed to inflate. Inflation falls back to the main thread. Error message=${ex.message}")
// Some views need to be inflation-only in the main thread,
// fall back to inflation in the main thread if there is an exception
withContext(Dispatchers.Main) { mInflater.inflate(resId, parent, false) }
}
4.支持Appcompat Factory2,主题传递
kotlin
private class BasicInflater constructor(context: Context) : LayoutInflater(context) {
override fun cloneInContext(newContext: Context): LayoutInflater {
return BasicInflater(newContext)
}
override fun onCreateView(name: String, attrs: AttributeSet): View {
for (prefix in sClassPrefixList) {
try {
val view = createView(name, prefix, attrs)
if (view != null) return view
} catch (_: ClassNotFoundException) {
}
}
return super.onCreateView(name, attrs)
}
companion object {
private val sClassPrefixList = arrayOf(
"android.widget.", "android.webkit.", "android.app."
)
}
init {
if (context is AppCompatActivity) {
val appCompatDelegate = context.delegate
if (appCompatDelegate is Factory2) {
LayoutInflaterCompat.setFactory2(this, appCompatDelegate)
}
}
}
}
自定义的类加载器继承的是android下的LayoutInflater,使用的是反射的方式创建View的,性能较差。而我们继承的AppCompatActivity,是直接用new的方式创建的,性能更好。
在OkLayoutInflater的BaseInflater中,初始化时,直接将context下的appCompatActivity注入。
主题传递链条:
Activity.theme
→ AppCompatDelegate
→ Factory2
→ 创建的 View
优化方向
OkLayoutInflater源码中使用的是Default调度器,前面分析,这是个全局调度器,可能会有任务累积+线程阻塞,导致View的加载变得更慢。
我们可以使用自定义线程池的方式进行优化,主要有两个方面:
1.自定义ThreadPool
kotlin
// 自定义线程池
private val executor by lazy {
ThreadPoolExecutor(
4,
4,
30L,
TimeUnit.SECONDS,
LinkedBlockingQueue(),
object : ThreadFactory {
val count = AtomicInteger(0)
override fun newThread(r: Runnable?): Thread {
return Thread(r, "OkLayoutInflater-${count.incrementAndGet()}")
}
}
).apply {
// 允许核心线程被超时回收
allowCoreThreadTimeOut(true)
}
}
2.退出时释放资源
- 退出页面后,关闭线程池,避免资源泄露。
- 使用
scope.cancel()
,协程作用域进入cancel状态,不再接受新任务,可以避免退出后仍然有任务提交过来。
运行中的线程本身就是GC Root,即使Activity退出了,也会一直空转(BlockingQueue阻塞等待),导致Thread无法正常退出。而每个Thread都会占用1MB的堆内存,重复多次进入该页面,会存在Thread泄露的情况。
当然,也可以把这个线程池做成全局单例的,就不需要shutdown了,这个看具体业务逻辑。
kotlin
fun cancel() {
// 这里清理的是协程任务
scope.cancel()
// BlockingQueue不再接受新任务。Thread执行完成之后退出
executor.shutdown()
}
四、RecyclerView的布局加载优化
优化方案
测试下来,OkLayoutInflater最重要的应用场景是预加载View,而不是异步加载。
**** | 对比 | |
---|---|---|
预加载 | 在使用前就初始化View完成,可以直接使用 | |
异步加载 | 在使用的那一刻,转到异步线程加载,加载完成后,返回给主线程 | 1.线程的切换需要耗时 2.主线程的线程priority是0,自定义线程优先级较低,会更低优的抢占CPU时间片 3.实测下来,优化效果不稳定,甚至有可能会劣化 |
在实际应用场景中,我们采用的是预加载+异步加载的方式,提升RecyclerView的绘制效率。对于RecyclerView需要在短时间内创建大量View的场景,OkLayoutInflater的提升非常有效。
实测数据
**** | 首帧渲染完成 | 滚动帧率 |
---|---|---|
传统方式 | 65ms | 45fps |
OkLayoutInflater | 35ms | 52fps |

具体实现
在OkLayoutInflater的基础上,封装了一个OkLayoutPool,用于预加载对应的View。这样在业务方使用的时候,可以直接获取,而不是创建。
kotlin
class OkLayoutPool {
companion object {
// 默认加载数量
const val PRELOAD_COUNT_DEFAULT = 6
}
private val ioInflater: OkLayoutInflater
private val mainInflater: OkLayoutInflater
@LayoutRes
private val resId: Int
private val concurrentViewPool = ConcurrentLinkedQueue<View>()
private var preloadCount = PRELOAD_COUNT_DEFAULT
private var threshold = PRELOAD_COUNT_DEFAULT / 3
private var parentViewGroup: ViewGroup? = null
// 是否预加载完成,防止重复预加载
@Volatile
private var finished = true
constructor(context: Context, @LayoutRes resId: Int) {
// 异步加载队列
ioInflater = OkLayoutInflater(context)
// 主线程同步加载队列
mainInflater = OkLayoutInflater(context)
this.resId = resId
bindLifecycle((context as LifecycleOwner).lifecycle)
}
// 绑定生命周期,退出时释放资源
private fun bindLifecycle(lifecycle: Lifecycle) {
lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
// 释放,防止泄露
parentViewGroup = null
}
}
})
}
/**
* 初始化
* @param parent 必须指定父类
* @param threshold 再次预加载阈值
*/
@MainThread
fun init(parent: ViewGroup, count: Int = PRELOAD_COUNT_DEFAULT, threshold: Int = count / 3) {
this.threshold = threshold
this.preloadCount = count
this.parentViewGroup = parent
}
/**
* 预加载
*/
@MainThread
fun preloadViews() {
// 上一次还未完成
if (!finished) {
return
}
// 如果没有父ViewGroup,则直接返回,无法预加载
val parent = this.parentViewGroup ?: return
this.finished = false
// 真正执行预加载
ioInflater.scope().launch {
for (index: Int in 0..preloadCount) {
val view = ioInflater.inflate(resId, parent, fallback = false)
// 创建完成,添加到缓存池中
view?.let { concurrentViewPool.add(it) }
}
finished = true
}
}
/**
* 用来替换原来的 inflate 方法
*/
@MainThread
fun inflate(fallback: (inflater: LayoutInflater, resId: Int) -> View): View {
val view = concurrentViewPool.poll()
val remain = concurrentViewPool.size
// 没有父布局,则无法预加载,走正常加载逻辑
if (parentViewGroup == null) {
return fallback(mainInflater.inflater(), resId)
}
// 剩余可用View缓存到达阈值,触发预加载
if (remain <= threshold && finished) {
preloadViews()
}
val v = if (view != null) {
view
} else {
// 如果没有命中缓存,使用原有的方式加载
val fall = fallback(mainInflater.inflater(), resId)
fall
}
return v
}
}
使用方式


流程图
