美文网首页
Android 异步加载布局的几种实现

Android 异步加载布局的几种实现

作者: ModestStorm | 来源:发表于2022-08-04 16:09 被阅读0次

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

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

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

    1.异步加载布局

    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,好吧,其实官方自己也出了一个异步加载布局框架,一起来看看。

    2.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 也有很多限制,相关的改进大家可以看看这里。

    https://www.jianshu.com/p/f0c0eda06ae4

    3.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 那样,子线程加载布局,在主线程添加进去?

    这个嘛,好像还真没有。

    那我们自己写一个?好像还真能。

    4.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()
        }
    }
    

    重点讲解了几种可以实用的启动优化方案:

    1.异步启动器加快初始化速度
    官方提供了一个类,可以来进行异步的inflate,但是有两个缺点:

    1.每次都要现场new一个出来

    2.异步加载的view只能通过callback回调才能获得,使用不方便(死穴)

    3.如果在Activity中进行初始化,通过callback回调时,并没有减少加载时间,仍然需要等待

    由于以上问题,一个思考方向就是,能不能提前在子线程inflate布局,然后在Activity中通过id取出来

    核心思想如下

    1.初始化时在子线程中inflate布局,存储在缓存中

    2.Activity初始化时,先从缓存结果里面拿View,拿到了view直接返回

    3.没拿到view,但是子线程在inflate中,等待返回

    4.如果还没开始inflate,由UI线程进行inflate

    这种方案的优点:

    可以大大减少View创建的时间,使用这种方案之后,获取View的时候基本在 10ms 之内的。

    缺点

    1.由于View是提前创建的,并且会存在在一个map,需要根据自己的业务场景将View从map中移除,不然会发生内存泄露

    2.View如果缓存起来,记得在合适的时候重置view的状态,不然有时候会发生奇奇怪怪的现象。

    总得来说,优缺点都很明显,读者可根据实际情况(主要是项目中inflate的时间长不长,改用提前加载后收益明不明显?),根据实际情况决定是否使用.

    神奇的的预加载(预加载View,而不是data)

    相关文章

      网友评论

          本文标题:Android 异步加载布局的几种实现

          本文链接:https://www.haomeiwen.com/subject/yvppwrtx.html