Android 異步加載布局的幾種實(shí)現(xiàn)

場(chǎng)景如下:當(dāng)我們啟動(dòng)一個(gè) Activity 的時(shí)候,如果此頁面的布局太過復(fù)雜,或者是一個(gè)很長的表單,此時(shí)加載布局,執(zhí)行頁面轉(zhuǎn)場(chǎng)動(dòng)畫,等操作都是在主線程,可能會(huì)搶Cpu資源,導(dǎo)致主線程block住,感知就是卡頓。

要么是點(diǎn)了跳轉(zhuǎn)按鈕,但是等待1S才會(huì)出現(xiàn)動(dòng)畫,要么是執(zhí)行動(dòng)畫的過程中卡頓。有沒有什么方式能優(yōu)化此等復(fù)雜頁面的啟動(dòng)速度,達(dá)到秒啟動(dòng)?

我們之前講動(dòng)畫的時(shí)候就知道,轉(zhuǎn)場(chǎng)動(dòng)畫是無法異步執(zhí)行的,那么我們能不能再異步加載布局呢?試試!

1.異步加載布局

LayoutInflater 的 inflate 方法的幾種重載方法,大家應(yīng)該都會(huì)的。這里我直接把布局加載到容器中試試。

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("加載真正布局耗時(shí):" + (end - start))

    }

}

果不其然是報(bào)錯(cuò)的,不能在子線程添加View。

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

因?yàn)榫€程操作UI有 checkThread的校驗(yàn),添加布局操作改變了UI,校驗(yàn)線程就無法通過。

那么我們只在子線程創(chuàng)建布局,然后再主線程添加到容器中行不行?試試!

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("加載真正布局耗時(shí):" + (end - start))

        view
    }


    if (rootView.await() != null) {
        val start1 = System.currentTimeMillis()
        mBinding.llRootContainer.addView(rootView.await(), 0)
        val end1 = System.currentTimeMillis()
        YYLogUtils.w("添加布局耗時(shí):" + (end1 - start1))

}

這樣還真行,打印日志如下:

開始異步加載真正的跟視圖 加載真正布局耗時(shí):809 添加布局耗時(shí):22

既然可行,那我們是不是就可以通過異步網(wǎng)絡(luò)請(qǐng)求+異步加載布局,實(shí)現(xiàn)這樣一樣效果,進(jìn)頁面展示Loading占位圖,然后異步網(wǎng)絡(luò)請(qǐng)求+異步加載布局,當(dāng)兩個(gè)異步任務(wù)都完成之后展示布局,加載數(shù)據(jù)。

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("加載真正布局耗時(shí):" + (end - start))

            view
        }

        val request = async {
            YYLogUtils.w("開始請(qǐng)求用戶詳情數(shù)據(jù)")
            delay(1500)
            true
        }

        if (request.await() && rootView.await() != null) {
            mBinding.llRootContainer.addView(rootView.await(), 0)
            showStateSuccess()

            popupProfile()
        }

    }
}

完美實(shí)現(xiàn)了秒進(jìn)復(fù)雜頁面的功能。當(dāng)然有同學(xué)說了,自己寫的行不行哦,會(huì)不會(huì)太Low,好吧,其實(shí)官方自己也出了一個(gè)異步加載布局框架,一起來看看。

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;
        }
    };

}

其實(shí)也沒有什么魔法,就是啟動(dòng)了一個(gè)線程去加載布局,然后通過handler發(fā)出回調(diào),只是線程內(nèi)部多了一些任務(wù)隊(duì)列和任務(wù)池。和我們直接用協(xié)程異步加載布局主線程添加布局是一樣樣的。

既然說到這里了,我們就用 AsyncLayoutInflater 實(shí)現(xiàn)一個(gè)一樣的效果。

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("開始請(qǐng)求用戶詳情數(shù)據(jù)")
    CommUtils.getHandler().postDelayed({
        mUserProfile = "xxx"
        showStateSuccess()
        popupData2View()
    }, 1200)
}

private fun popupData2View() {
    if (mUserProfile != null && mRootBinding != null) {
        //加載數(shù)據(jù)
    }
}

同樣的是并發(fā)異步任務(wù),異步加載布局和異步請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù),然后都完成之后展示成功的布局,并顯示數(shù)據(jù)。

他的效果和性能與上面協(xié)程自己寫的是一樣的。這里就不多說了。

當(dāng)然 AsyncLayoutInflater 也有很多限制,相關(guān)的改進(jìn)大家可以看看這里。

http://www.itdecent.cn/p/f0c0eda06ae4

3.ViewStub 的占位

看到這里大家心里應(yīng)該有疑問,你說的這種復(fù)雜的布局,我們都是使用 ViewStub 來占位,讓頁面能快速進(jìn)入,完成之后再進(jìn)行 ViewStub 的 inflate ,你整那么多花活有啥用!

確實(shí),相信大家在這樣的場(chǎng)景下確實(shí)用的比較多的都是使用 ViewStub 來占位,但是當(dāng) ViewStub 的布局比較大的時(shí)候 還是一樣卡主線程,只是從進(jìn)入頁面前卡頓,轉(zhuǎn)到進(jìn)入頁面后卡頓而已。

那我們?cè)佼惒郊虞d 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("加載真正布局耗時(shí):" + (end - start))

                view
            }

            val request = async {
                YYLogUtils.w("開始請(qǐng)求用戶詳情數(shù)據(jù)")
                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("添加布局耗時(shí):" + (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() 方法內(nèi)部, replaceSelfWithView() 調(diào)用了 requestLayout,這部分checkThread。

那我們像 LayoutInflater 那樣,子線程加載布局,在主線程添加進(jìn)去?

這個(gè)嘛,好像還真沒有。

那我們自己寫一個(gè)?好像還真能。

4.AsyncViewStub 的定義與使用

其實(shí)很簡單的實(shí)現(xiàn),我們就是仿造 LayoutInflater 那樣子線程加載布局,在主線程添加布局嘛。

自定義View如下,繼承實(shí)現(xiàn)一個(gè)協(xié)程作用域,內(nèi)部實(shí)現(xiàn)子線程加載布局,主線程替換占位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中設(shè)置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,現(xiàn)在就怎么使用 AsyncViewStub ,只是從之前的主線程加載布局改變?yōu)樽泳€程加載布局。

//請(qǐng)求工作詳情數(shù)據(jù)-并加載真正的布局
private fun initDataAndRootView() {
    if (!mBinding.viewStubRoot.isInflate()) {
        val start1 = System.currentTimeMillis()
        mBinding.viewStubRoot.inflateAsync { view ->
            val end1 = System.currentTimeMillis()
            YYLogUtils.w("添加布局耗時(shí):" + (end1 - start1))
            mRootBinding = DataBindingUtil.bind<IncludePartTimeJobDetailActivityBinding>(view)?.apply {
                click = mClickProxy
            }

            initRV()
            checkView2Showed()
        }
    }

    //并發(fā)網(wǎng)絡(luò)請(qǐng)求
    requestDetailData()
}

//這里請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)完成,只展示頂部圖片和標(biāo)題和TabView和ViewPager
private fun requestDetailData() {
    mViewModel.requestJobDetail().observe(this) {
        checkView2Showed()
    }
}

//查詢異步加載的布局和異步的遠(yuǎn)端數(shù)據(jù)是否已經(jīng)準(zhǔn)備就緒
private fun checkView2Showed() {
    if (mViewModel.mPartTimeJobDetail != null && mRootBinding != null) {

        mRootBinding?.setVariable(BR.viewModel, mViewModel)

        showStateSuccess()

        initPager()
        popupData2Top()
    }
}

重點(diǎn)講解了幾種可以實(shí)用的啟動(dòng)優(yōu)化方案:

1.異步啟動(dòng)器加快初始化速度
官方提供了一個(gè)類,可以來進(jìn)行異步的inflate,但是有兩個(gè)缺點(diǎn):

1.每次都要現(xiàn)場(chǎng)new一個(gè)出來

2.異步加載的view只能通過callback回調(diào)才能獲得,使用不方便(死穴)

3.如果在Activity中進(jìn)行初始化,通過callback回調(diào)時(shí),并沒有減少加載時(shí)間,仍然需要等待

由于以上問題,一個(gè)思考方向就是,能不能提前在子線程inflate布局,然后在Activity中通過id取出來

核心思想如下

1.初始化時(shí)在子線程中inflate布局,存儲(chǔ)在緩存中

2.Activity初始化時(shí),先從緩存結(jié)果里面拿View,拿到了view直接返回

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

4.如果還沒開始inflate,由UI線程進(jìn)行inflate

這種方案的優(yōu)點(diǎn):

可以大大減少View創(chuàng)建的時(shí)間,使用這種方案之后,獲取View的時(shí)候基本在 10ms 之內(nèi)的。

缺點(diǎn)

1.由于View是提前創(chuàng)建的,并且會(huì)存在在一個(gè)map,需要根據(jù)自己的業(yè)務(wù)場(chǎng)景將View從map中移除,不然會(huì)發(fā)生內(nèi)存泄露

2.View如果緩存起來,記得在合適的時(shí)候重置view的狀態(tài),不然有時(shí)候會(huì)發(fā)生奇奇怪怪的現(xiàn)象。

總得來說,優(yōu)缺點(diǎn)都很明顯,讀者可根據(jù)實(shí)際情況(主要是項(xiàng)目中inflate的時(shí)間長不長,改用提前加載后收益明不明顯?),根據(jù)實(shí)際情況決定是否使用.

神奇的的預(yù)加載(預(yù)加載View,而不是data)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容