場(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í)際情況決定是否使用.