用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碎片时间过多反而会卡顿。最后说下我的文章没有提供完整的代码只说了核心代码和细节,但是基本仔细阅读和思考拼凑这些代码就可以使用了。授人以鱼不如授人以渔。

相关推荐
zhangphil6 小时前
Android Coil3缩略图、默认占位图placeholder、error加载错误显示,Kotlin(1)
android·kotlin
xvch12 小时前
Kotlin 2.1.0 入门教程(二十三)泛型、泛型约束、协变、逆变、不变
android·kotlin
xvch2 天前
Kotlin 2.1.0 入门教程(二十四)泛型、泛型约束、绝对非空类型、下划线运算符
android·kotlin
zhangphil2 天前
Android Coil ImageLoader MemoryCache设置Key与复用内存缓存,Kotlin
android·kotlin
mmsx2 天前
kotlin Java 使用ArrayList.add() ,set()前面所有值被 覆盖 的问题
android·开发语言·kotlin
lavins3 天前
android studio kotlin项目build时候提示错误 Unknown Kotlin JVM target: 21
jvm·kotlin·android studio
面向未来_3 天前
JAVA Kotlin Androd 使用String.format()格式化日期
java·开发语言·kotlin
alexhilton3 天前
选择Retrofit还是Ktor:给Android开发者的指南
android·kotlin·android jetpack
GordonH19913 天前
Kotlin 优雅的接口实现
android·java·kotlin
wangz764 天前
Android 下用kotlin写一个sqlite
android·sqlite·kotlin·jetpack compose