携手创作,一起生长!这是我参加「日新计划 8 月更文应战」的第4天,点击检查活动概况

Android 异步加载布局的几种完结

场景如下:当咱们发动一个 Activity 的时分,假如此页面的布局太过杂乱,或者是一个很长的表单,此时加载布局,履行页面转场动画,等操作都是在主线程,可能会抢Cpu资源,导致主线程block住,感知便是卡顿。

要么是点了跳转按钮,但是等待1S才会出现动画,要么是履行动画的过程中卡顿。有没有什么方式能优化此等杂乱页面的发动速度,到达秒发动?

咱们之前讲动画的时分就知道,转场动画是无法异步履行的,那么咱们能不能再异步加载布局呢?试试!

一、异步加载布局

LayoutInflater 的 inflate 办法的几种重载办法,咱们应该都会的。这儿我直接把布局加载到容器中试试。

        lifecycleScope.launch {
            val start = System.currentTimeMillis()
            async(Dispatchers.IO) {
                YYLogUtils.w("开端异步加载真实的跟视图")
                val view = layoutInflater.inflate(R.layout.include_pensonal_turn_up_rate, mBinding.rootView,false)
                val end = System.currentTimeMillis()
                YYLogUtils.w("加载真实布局耗时:" + (end - start))
            }
        }

果不其然是报错的,不能在子线程增加View

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

因为线程操作UI有 checkThread 的校验,增加布局操作改变了UI,校验线程就无法经过。

那么咱们只在子线程创立布局,然后再主线程增加到容器中行不行?试试!

        lifecycleScope.launch {
            val start = System.currentTimeMillis()
            val rootView = async(Dispatchers.IO) {
                YYLogUtils.w("开端异步加载真实的跟视图")
                val view =  mBinding.viewStubRating.viewStub?.inflate()
                val end = System.currentTimeMillis()
                YYLogUtils.w("加载真实布局耗时:" + (end - start))
                view
            }
            if (rootView.await() != null) {
                val start1 = System.currentTimeMillis()
                mBinding.llRootContainer.addView(rootView.await(), 0)
                val end1 = System.currentTimeMillis()
                YYLogUtils.w("增加布局耗时:" + (end1 - start1))
        }

这样还真行,打印日志如下:

开端异步加载真实的跟视图 加载真实布局耗时:809 增加布局耗时:22

已然可行,那咱们是不是就能够经过异步网络恳求+异步加载布局,完结这样相同作用,进页面展现Loading占位图,然后异步网络恳求+异步加载布局,当两个异步使命都完结之后展现布局,加载数据。

    private fun inflateRootAndData() {
        showStateLoading()
        lifecycleScope.launch {
            val start = System.currentTimeMillis()
            val rootView = async(Dispatchers.IO) {
                YYLogUtils.w("开端异步加载真实的跟视图")
                val view = layoutInflater.inflate(R.layout.include_pensonal_turn_up_rate, null)
                val end = System.currentTimeMillis()
                YYLogUtils.w("加载真实布局耗时:" + (end - start))
                view
            }
            val request = async {
                YYLogUtils.w("开端恳求用户概况数据")
                delay(1500)
                true
            }
            if (request.await() && rootView.await() != null) {
                mBinding.llRootContainer.addView(rootView.await(), 0)
                showStateSuccess()
                popupProfile()
            }
        }
    }

完美完结了秒进杂乱页面的功用。当然有同学说了,自己写的行不行哦,会不会太Low,好吧,其实官方自己也出了一个异步加载布局框架,一起来看看。

二、AsyncLayoutInflater

部分源码如下:

public final class AsyncLayoutInflater {
    private static final String TAG = "AsyncLayoutInflater";
    LayoutInflater mInflater;
    Handler mHandler;
    InflateThread mInflateThread;
    public AsyncLayoutInflater(@NonNull Context context) {
        mInflater = new BasicInflater(context);
        mHandler = new Handler(mHandlerCallback);
        mInflateThread = InflateThread.getInstance();
    }
    @UiThread
    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
            @NonNull OnInflateFinishedListener callback) {
        if (callback == null) {
            throw new NullPointerException("callback argument may not be null!");
        }
        InflateRequest request = mInflateThread.obtainRequest();
        request.inflater = this;
        request.resid = resid;
        request.parent = parent;
        request.callback = callback;
        mInflateThread.enqueue(request);
    }
    private Callback mHandlerCallback = new Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            InflateRequest request = (InflateRequest) msg.obj;
            if (request.view == null) {
                request.view = mInflater.inflate(
                        request.resid, request.parent, false);
            }
            request.callback.onInflateFinished(
                    request.view, request.resid, request.parent);
            mInflateThread.releaseRequest(request);
            return true;
        }
    };
}

其实也没有什么魔法,便是发动了一个线程去加载布局,然后经过handler宣布回调,仅仅线程内部多了一些使命行列和使命池。和咱们直接用协程异步加载布局主线程增加布局是相同样的。

已然提到这儿了,咱们就用 AsyncLayoutInflater 完结一个相同的作用

  var mUserProfile: String? = null
  var mRootBinding: IncludePensonalTurnUpRateBinding? = null
    private fun initData() {
        showStateLoading()
        YYLogUtils.w("开端异步加载真实的跟视图")
        if (mBinding.llRootContainer.childCount <= 1) {
            AsyncLayoutInflater(mActivity).inflate(R.layout.include_pensonal_turn_up_rate, null) { view, _, _ ->
                mRootBinding = DataBindingUtil.bind<IncludePensonalTurnUpRateBinding>(view)?.apply {
                    click = clickProxy
                }
                mBinding.llRootContainer.addView(view, 0)
                popupData2View()
            }
        }
        YYLogUtils.w("开端恳求用户概况数据")
        CommUtils.getHandler().postDelayed({
            mUserProfile = "xxx"
            showStateSuccess()
            popupData2View()
        }, 1200)
    }
    private fun popupData2View() {
        if (mUserProfile != null && mRootBinding != null) {
            //加载数据
        }
    }

同样的是并发异步使命,异步加载布局和异步恳求网络数据,然后都完结之后展现成功的布局,并显示数据。

他的作用和性能与上面协程自己写的是相同的。这儿就不多说了。

当然 AsyncLayoutInflater 也有很多限制,相关的改进咱们能够看看这儿。

三、ViewStub 的占位

看到这儿咱们心里应该有疑问,你说的这种杂乱的布局,咱们都是运用 ViewStub 来占位,让页面能快速进入,完结之后再进行 ViewStub 的 inflate ,你整那么多花活有啥用!

的确,相信咱们在这样的场景下的确用的比较多的都是运用 ViewStub 来占位,但是当 ViewStub 的布局比较大的时分 还是相同卡主线程,仅仅从进入页面前卡顿,转到进入页面后卡顿而已。

那咱们再异步加载 ViewStub 不就行了嘛

 private fun inflateRootAndData() {
        showStateLoading()
        lifecycleScope.launch {
            val start = System.currentTimeMillis()
            val rootView = async(Dispatchers.IO) {
                YYLogUtils.w("开端异步加载真实的跟视图")
                val view =  mBinding.viewStubRating.viewStub?.inflate()
                val end = System.currentTimeMillis()
                YYLogUtils.w("加载真实布局耗时:" + (end - start))
                view
            }
            val request = async {
                YYLogUtils.w("开端恳求用户概况数据")
                delay(1500)
                true
            }
            if (request.await() && rootView.await() != null) {
                val start1 = System.currentTimeMillis()
                mBinding.llRootContainer.addView(rootView.await(), 0)
                val end1 = System.currentTimeMillis()
                YYLogUtils.w("增加布局耗时:" + (end1 - start1))
                showStateSuccess()
                popupPartTimeProfile()
            }
        }
    }

是的,和 LayoutInflater 的 inflate 相同,无法在子线程增加布局。

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:10750) at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:2209)

ViewStub 的 inflate() 办法内部, replaceSelfWithView() 调用了 requestLayout,这部分checkThread。

那咱们像 LayoutInflater 那样,子线程加载布局,在主线程增加进去?

这个嘛,如同还真没有。

那咱们自己写一个?如同还真能。

四、AsyncViewStub 的界说与运用

其实很简单的完结,咱们便是仿造 LayoutInflater 那样子线程加载布局,在主线程增加布局嘛。

自界说View如下,继承方式完结一个协程作用域,内部完结子线程加载布局,主线程替换占位View。关于自界说协程作用域相关的问题假如不了解的,能够看看我之前的协程系列文章。

/**
 *  异步加载布局的 ViewStub
 */
class AsyncViewStub @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    View(context, attrs, defStyleAttr), CoroutineScope by MainScope() {
    var layoutId: Int = 0
    var mView: View? = null
    init {
        initAttrs(attrs, context)//初始化特点
    }
    private fun initAttrs(attrs: AttributeSet?, context: Context?) {
        val typedArray = context!!.obtainStyledAttributes(
            attrs,
            R.styleable.AsyncViewStub
        )
        layoutId = typedArray.getResourceId(
            R.styleable.AsyncViewStub_layout,
            0
        )
        typedArray.recycle()
    }
    fun inflateAsync(block: (View) -> Unit) {
        if (layoutId == 0) throw RuntimeException("没有找到加载的布局,你必须在xml中设置layout特点")
        launch {
            val view = withContext(Dispatchers.IO) {
                LayoutInflater.from(context).inflate(layoutId, null)
            }
            mView = view
            //增加到父布局
            val parent = parent as ViewGroup
            val index = parent.indexOfChild(this@AsyncViewStub)
            val vlp: ViewGroup.LayoutParams = layoutParams
            view.layoutParams = vlp //把 LayoutParams 给到新view
            parent.removeViewAt(index) //删除原来的占位View
            parent.addView(view, index) //把新有的View替换上去
            block(view)
        }
    }
    fun isInflate(): Boolean {
        return mView != null
    }
    fun getInflatedView(): View? {
        return mView
    }
    override fun onDetachedFromWindow() {
        cancel()
        super.onDetachedFromWindow()
    }
}

自界说特点

    <!--  异步加载布局  -->
    <declare-styleable name="AsyncViewStub">
        <attr name="layout" format="reference" />
    </declare-styleable>

运用

      <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <com.guadou.cs_cptservices.widget.AsyncViewStub
                android:id="@+id/view_stub_root"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layout="@layout/include_part_time_job_detail_activity" />
            <ImageView .../>
            <TextView .../>   
            ...
       </FrameLayout>         

那么咱们之前怎样运用 ViewStub 的 inflate,现在就怎样运用 AsyncViewStub ,仅仅从之前的主线程加载布局改变为子线程加载布局。

 //恳求作业概况数据-并加载真实的布局
    private fun initDataAndRootView() {
        if (!mBinding.viewStubRoot.isInflate()) {
            val start1 = System.currentTimeMillis()
            mBinding.viewStubRoot.inflateAsync { view ->
                val end1 = System.currentTimeMillis()
                YYLogUtils.w("增加布局耗时:" + (end1 - start1))
                mRootBinding = DataBindingUtil.bind<IncludePartTimeJobDetailActivityBinding>(view)?.apply {
                    click = mClickProxy
                }
                initRV()
                checkView2Showed()
            }
        }
        //并发网络恳求
        requestDetailData()
    }
    //这儿恳求网络数据完结,只展现顶部图片和标题和TabView和ViewPager
    private fun requestDetailData() {
        mViewModel.requestJobDetail().observe(this) {
            checkView2Showed()
        }
    }
    //查询异步加载的布局和异步的远端数据是否已经准备就绪
    private fun checkView2Showed() {
        if (mViewModel.mPartTimeJobDetail != null && mRootBinding != null) {
            mRootBinding?.setVariable(BR.viewModel, mViewModel)
            showStateSuccess()
            initPager()
            popupData2Top()
        }
    }

完结的作用如下(静态页面+模拟恳求):

开箱即用-使用异步加载布局来优化页面启动速度的几种方案

不好意思,没改之前的时分没有保存到图,之前的作用是点击会卡顿到900毫秒到1秒的时刻进入页面,比较卡顿。

当然,咱们这个页面不算是很杂乱的页面,在低端的手机上,也仅仅卡顿900毫秒。

咱们还有更杂乱的页面,比方炒鸡杂乱的表单页面,目测是卡顿2秒左右才进入页面,后边我会针对性的对页面进行类似的优化。比方我会把杂乱的页面分为多个子布局,异步加载布局的时分能够运用多个异步使命来继续优化加载速度。

总结

Ok, 这儿仅仅是提供了另一种计划,切勿生搬硬套,就必定要把一切的页面,都改造一番,毕竟这么用增加了运用成本和风险。

总的来说,假如你的页面并不是很杂乱,也没必要运用此办法,当然了,假如你的页面的确很杂乱,并且在查找一些优化的方式,那你不妨一试,的确能起到必定的优化作用。

常规,如有错漏还请指出,如有更好的办法,能够一起沟通,假如觉得本文对你有一丁点点帮助,还请点赞支撑一下哦!你的支撑是我最大的动力。

到此完结

开箱即用-使用异步加载布局来优化页面启动速度的几种方案