用Kotlin改造AsyncLayoutInflater

AsyncLayoutInflater使用

当我们的UI布局因为过于复杂,影响冷启动或者用户体验的时候,AsyncLayoutInflater可以帮助我们优化,因xml-layout反射变成View占用主线程的卡顿问题。首先我们需要查看下AsyncLayoutInflater的使用并且剖析源码了解它的优缺点:

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        AsyncLayoutInflater(this).inflate(
            R.layout.activity_splash, null
        ) { view, _, _ ->
            setContentView(view)
        }
        //setContentView(R.layout.activity_splash)
    }

使用很简单构造函数接受一个Context,inflate函数最后一个参数是接口,在回调方法中可以获取到View。

java 复制代码
public interface OnInflateFinishedListener {
    void onInflateFinished(@NonNull View view, @LayoutRes int resid,@Nullable ViewGroup parent);
}

AsyncLayoutInflater源码剖析

  1. 构造函数中创建3个对象,分别是布局解析器,用于切换到主线程的Handler,解析线程单利

    java 复制代码
     LayoutInflater mInflater;
     Handler mHandler;
     InflateThread mInflateThread;
    
     public AsyncLayoutInflater(@NonNull Context context) {
         mInflater = new BasicInflater(context);
         mHandler = new Handler(mHandlerCallback);
         mInflateThread = InflateThread.getInstance();
     }
  2. 对于BasicInflater没什么重点内容,看下Handler的callback做了什么

    java 复制代码
     private Callback mHandlerCallback = new Callback() {
         @Override
         public boolean handleMessage(Message msg) {
             //获取子线程结果的封装
             InflateRequest request = (InflateRequest) msg.obj;
             //如果view在子线程解析失败没有赋值,在main线程中重新解析一次
             if (request.view == null) {
                 request.view = mInflater.inflate(
                         request.resid, request.parent, false);
             }
             //回调 onInflateFinished
             request.callback.onInflateFinished(
                     request.view, request.resid, request.parent);
             //线程回收资源
             mInflateThread.releaseRequest(request);
             return true;
         }
     };
  3. InflateThread继承于Thread,所以重点看start和run方法

    java 复制代码
    private static class InflateThread extends Thread {
        private static final InflateThread sInstance;
        static {
            //单利对象并且创建之后就启动并且进入run中的死循环
            sInstance = new InflateThread();
            sInstance.start();
        }
    
        public static InflateThread getInstance() {
            return sInstance;
        }
        //解析队列最大支持10个
        private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
        //解析请求的对象池
        private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);
    
        @Override
        public void run() {
            while (true) {
                runInner();
            }
        }
  4. 原理就是把布局的解析工作放在子线程中,解析完成变成View之后通过Handler回调到主线程再使用。

    java 复制代码
     public void runInner() {
         InflateRequest request;
         try {
             //没有任务会阻塞
             request = mQueue.take();
         } catch (InterruptedException ex) {
             // Odd, just continue
             Log.w(TAG, ex);
             return;
         }
         try {
             request.view = request.inflater.mInflater.inflate(
                     request.resid, request.parent, false);
         } catch (RuntimeException ex) {
             // Probably a Looper failure, retry on the UI thread
             Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
                     + " thread", ex);
         }
         Message.obtain(request.inflater.mHandler, 0, request).sendToTarget();
     }

优点整理

  • 在子线程中解析优化主线程占用
  • 在子线程解析失败,会再次回到主线程inflate一次
  • 可以优化启动Activity时候主线程还有其他任务同步进行的卡白屏问题。

缺陷整理

  • 所有的解析工作在一个线程中,同时有多个inflate任务只能串行。
  • 在子线程中初始化View时候不能创建Handler或者调用Looper.myLooper()
  • 队列添加任务超过10个时候会阻塞主线程。
  • 不支持LayoutInflater.Factory or LayoutInflater.Factory2,全局换字体或者替换控件功能会有影响。
  • 没有提供取消解析的api,可能出现内存泄漏。
  • 由于是callback方式在fragment中使用很困难。

用kotlin优化

Coroutine可以提供解析的子线程和切换到主线程,使用挂起函数就不需要接口回调了,并且可以自由的取消任务。这可以解决上面的部分缺陷了,看起来直接替换掉InflateThread和Handler所有工作就可以了,复制一份AsyncLayoutInflater代码改造,代码一下子少了很多。

kotlin 复制代码
class CoroutineLayoutInflater(
    private val context: Context,
    private val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    private val inflater: BasicInflater = BasicInflater(context)

    suspend fun inflate(
        @LayoutRes resId: Int,
        parent: ViewGroup? = null
    ): View = withContext(dispatcher) {
        val view = try {
            inflater.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
            null
        }
        withContext(Dispatchers.Main) {
            view ?: inflater.inflate(resId, parent, false)
        }
    }
    ... BasicInflater

使用时候用lifecycleScope可以自动取消任务

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            val view = CoroutineLayoutInflater(this@MainActivity).inflate(R.layout.activity_splash)
            setContentView(view)
            //ActivitySplashBinding.bind(view)
        }
    }
}

解析线程优化

上面代码的CoroutineDispatcher我们使用了Dispatchers.Default,创建的线程名称默认为DefaultDispatcher-worker-#,如果大家需要单独定义一个线程池或者添加线程名称等,可以这样操作。

kotlin 复制代码
val threadFactory = ... //自定义名称或线程虚拟内存优化 512kb等
val nThreads = ... // 线程数,为1就是单线程
//最好是全局变量去保持这个layout专用的dispatcher
val dispatcher = ThreadPoolExecutor(
    nThreads, nThreads,
    0L, TimeUnit.MILLISECONDS,
    LinkedBlockingQueue<Runnable>()
).apply {
    //允许核心线程回收
    allowCoreThreadTimeOut(true)
    //转换为 CoroutineDispatcher
    asCoroutineDispatcher()
}
//go
CoroutineLayoutInflater(context, dispatcher)

支持LayoutInflater.Factory2

在androidx中提供了api LayoutInflaterCompat.setFactory2(inflater, factory2)来给LayoutInflater设置factory,inflater对象我们有了,还需要获取到factory2对象。查看源码 LayoutInflater.Factory2是个接口androidx的实现类在AppCompatDelegateImpl,因为类是@hide需要通过AppCompatActivity#getDelegate()来获取,那么在Activity中必须是继承AppCompatActivity的。那么再改造下我们的CoroutineLayoutInflater

kotlin 复制代码
class CoroutineLayoutInflater(...) {

    private val inflater: BasicInflater = BasicInflater(context)

    init {
        ((context as? AppCompatActivity)?.delegate as? LayoutInflater.Factory2)?.let { factory2 ->
            LayoutInflaterCompat.setFactory2(inflater, factory2)
        }
    }

Fragment的支持问题

虽然用了suspend函数,但是如果runBlocking { }会阻塞当前的线程,那么和不使用AsyncLayoutInflater就一样了。看起来只能曲线救国了,还是建议直接改造成Compose吧。

kotlin 复制代码
class HostFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return FrameLayout(requireActivity()).also { rootView ->
            viewLifecycleOwner.lifecycleScope.launch {
                CoroutineLayoutInflater(requireActivity()).inflate(
                    R.layout.fragment_host, container
                ).let {
                    rootView.addView(it)
                }
            }
        }
    }

Profiler分析trace

先用普通的View创建方式查看先main的耗时占用

使用优化后的CoroutineLayoutInflater

主线程的占用都移动到了DefaultDispatcher-worker-#

通过分析也可以看出在冷启动中使用会有比较好的效果,而且不太建议同时间内大量使用,会频繁的切换线程导致CPU碎片时间过多反而会卡顿。最后说下我的文章没有提供完整的代码只说了核心代码和细节,但是基本仔细阅读和思考拼凑这些代码就可以使用了。授人以鱼不如授人以渔。

相关推荐
萌面小侠Plus5 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机
wk灬丨8 小时前
Android Kotlin Flow 冷流 热流
android·kotlin·flow
晨曦_子画9 小时前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
大福是小强9 小时前
005-Kotlin界面开发之程序猿初试Composable
kotlin·界面开发·桌面应用·compose·jetpack·可组合
&岁月不待人&11 小时前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
小白学大数据14 小时前
正则表达式在Kotlin中的应用:提取图片链接
开发语言·python·selenium·正则表达式·kotlin
bytebeats2 天前
Kotlin 注解全面指北
android·java·kotlin
jzlhll1232 天前
kotlin android Handler removeCallbacks runnable不生效的一种可能
android·开发语言·kotlin
&岁月不待人&2 天前
Kotlin 协程使用及其详解
开发语言·kotlin
苏柘_level62 天前
【Kotlin】 基础语法笔记
开发语言·笔记·kotlin