已經(jīng)在郭霖老師公眾號發(fā)布蚂斤,不再投遞其他公眾號辛块,轉(zhuǎn)載請務必標明來源扣癣。
https://mp.weixin.qq.com/s/haV2Bp5MyEw5pXaheBJRhA
目錄
- 背景及目標
- 理論基礎
- 流程分析
- 具體操作
- 風險提示及踩坑記錄
防杠
能解決view本身的問題就去解決view本身的問題,這是治根憨降,我這個方案無法解決view本身的問題。
如果你們也有很多上古view是因為業(yè)務要求该酗,時間要求授药,最終導致你們沒法治根,
那這個就是幫你們快速解決block主線程卡頓的方案呜魄。
當然悔叽,這個方案也無法降低cpu開銷,你們走投無路的時候可以拿這個方案試試爵嗅,僅此提供給你們一個思路娇澎。
背景及目標
最近在做性能優(yōu)化工作,代碼實際上已經(jīng)經(jīng)過幾代人的優(yōu)化睹晒,已經(jīng)做了大量的 按需加載(懶加載)趟庄,布局ViewStub優(yōu)化括细,層級優(yōu)化,代碼質(zhì)量也很高戚啥,檢查了沒有多余的耗時操作(業(yè)務上的無法避免的數(shù)據(jù)請求帶來的耗時操作已盡可能減少)奋单。
但是因為涉及的行業(yè)特殊,整個業(yè)務很復雜猫十,界面里充滿大量的自定義view览濒。
現(xiàn)在就是通過工具查到了其中某個方法,該方法會解析某個布局(布局內(nèi)有n個自定義view)拖云,
測試會卡住主線程800+ms贷笛,現(xiàn)在就需要對這個方法進行優(yōu)化。
理論基礎
需要至少對以下三點基礎理論有了解宙项。
1.切換線程的基礎技能乏苦;
2.官方提供的AsyncLayoutInflater
源碼鏈接
http://androidxref.com/9.0.0_r3/xref/frameworks/support/asynclayoutinflater/src/main/java/androidx/asynclayoutinflater/view/AsyncLayoutInflater.java
3.ViewStub,merge操作及View初始化操作
http://www.reibang.com/p/68717519c4a5
流程分析
大家都知道在Android中杉允,只能在主線程操作UI原因是
具體這個mThread是什么時候傳入的邑贴,這個checkTread方法什么時候調(diào)用的,
https://mp.weixin.qq.com/s/tg96p50alrqAtRih8a3AhA
那博主今天在這里吹什么牛比呢叔磷?
『只能在主線程操作UI』 這句話你細品拢驾,假如我不操作UI,只inflate View行不行改基?答案當然是可以的繁疤,官方提供的AsyncLayoutInflater 就是這樣的操作。
(再多比比一句秕狰,準確的說稠腊,只能在主線程操作UI不太準確,準確的說法見這篇文章分析http://www.reibang.com/p/3f03a26d247a)
內(nèi)部實現(xiàn)很簡單鸣哀,把需要加載的 Layout.xml的包裝成一個任務架忌,內(nèi)部線程inflate解析,解析完畢再通過handler通知到主線程我衬。
換句話說叹放,操作UI的行為,是指要內(nèi)部調(diào)用到checkThread的行為挠羔。
由于AsyncLayoutInflater只在高于API 24的版本有用井仰,那么我們就借助這個思路,仿照它來完成性能優(yōu)化工作破加。
具體操作
我們以一個簡單的demo來模擬這個需求俱恶,順便看看源碼。
就一個MainActivty, 主界面布局一個button,按一下把功能view加載到主界面合是。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/btn"
android:layout_width="100dp"
android:layout_height="50dp"
android:gravity="center"
android:text="Hello World!" />
<ViewStub
android:id="@+id/viewStub"
android:layout="@layout/realview"
android:layout_width="match_parent"
android:layout_height="200dp" />
</LinearLayout>
我們假設這個realview是一個巨復雜的view(或者里面的自定義view初始化里有耗時方法)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#fcc" />
</RelativeLayout>
就這么一個例子了罪,點擊按鈕加載viewStub
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addViewInNormalWay();
}
});
}
public void addViewInNormalWay(){
ViewStub viewStub = findViewById(R.id.viewStub);
viewStub.inflate();
}
}
假如我們直接開子線程把這個viewStub,100%拋出異常
Schedulers.io().scheduleDirect(new Runnable() {
@Override
public void run() {
addViewInNormalWay();
}
});
原因是ViewStub的inflate()方法內(nèi)部,replaceSelfWithView()調(diào)用了 requestLayout端仰,這部分checkThread捶惜。
我們借助AsyncLayoutInflater的思想,
把ViewStub.inflate()內(nèi)部拆開荔烧,inflateViewNoAdd()放到子線程解析吱七,解析完畢再回到主線程替換目標view。
但是這樣需要對布局進行更改
原始的ViewStub占位需要替換成View
在子線程中把目標view解析,解析完畢再替換目標view
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addView();
}
});
}
private void addView() {
Disposable d = Single
.create(new SingleOnSubscribe<View>() {
@Override
public void subscribe(SingleEmitter<View> emitter) throws Exception {
View view = getLayoutInflater().inflate(R.layout.realview, null);
emitter.onSuccess(view);//只做inflated 解析xml的操作
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<View>() {
@Override
public void accept(View view) throws Exception {
View stub = findViewById(R.id.viewStub);
if (stub == null) return;
ViewGroup parent = (ViewGroup) stub.getParent();
int index = parent.indexOfChild(stub);//找到原始占位view
ViewGroup.LayoutParams vlp = stub.getLayoutParams();//拿到lp
view.setLayoutParams(vlp);//把lp給到新view
parent.removeViewAt(index);//從樹里刪除
parent.addView(view, index);//替換上去
}
});
}
}
這里有個細節(jié)鹤竭,LayoutInflater.inflate()一共有4種
建議root傳null踊餐,不傳null的話,attach一定要傳false臀稚。
因為realview的頂層layout 寬高屬性會丟失吝岭,補救策略就是再套一層layout,或者在外部view就指定寬高屬性吧寺。
風險提示
1.原來的ViewStub要替換成一個占位View窜管,這樣就會破壞原有的布局優(yōu)化策略;
2.被inflate的View稚机,根標簽不能用merge幕帆,原因去看inflate源碼;
3.不保證你的功能view里面有奇奇怪怪的操作赖条,這些都會導致子線程解析失敗失乾。
這些奇奇怪怪的操作有(包括但不限于)
I.在異步inflated的布局,其 parent 的 generateLayoutParams 函數(shù)必須要是線程安全的纬乍;
II.所有構建的 View 中必須不能創(chuàng)建 Handler 或者是調(diào)用 Looper.loop碱茁, 因為子線程默認沒有 Looper.prepare(),
補救措施仿贬,找到出錯的自定義view纽竣,初始化handler請加上 Looper.getMainLooper() 參數(shù);
III.實現(xiàn)了諸如GestureDetector組件,這些組件內(nèi)部會初始化handler茧泪,報錯原因見第二條蜓氨,
補救措施,在自定義view里调炬,對這些組件的初始化請切換到主線程。
...
最后
谷歌不推薦在子線程操作UI的原因有很多舱馅,比如多線程多次inflate 缰泡,風險還是有的。
現(xiàn)在找到卡頓的原因就是 inflate自定義view耗時,采用這套方案后該方法耗時120ms棘钞。
風險跟收益缠借,各位自己評估。
以上代碼很簡單宜猜,就不傳github泼返。
順便打個廣告,一行代碼幫你做安卓防護
https://github.com/lamster2018/EasyProtector