性能優(yōu)化:異步加載View的一些思路

已經(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原因是

image.png

具體這個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通知到主線程我衬。

image.png

換句話說叹放,操作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。

但是這樣需要對布局進行更改


image.png

原始的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種


image.png

建議root傳null踊餐,不傳null的話,attach一定要傳false臀稚。
因為realview的頂層layout 寬高屬性會丟失吝岭,補救策略就是再套一層layout,或者在外部view就指定寬高屬性吧寺。

image.png
image.png

風險提示

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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末姨拥,一起剝皮案震驚了整個濱河市绅喉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌叫乌,老刑警劉巖柴罐,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異憨奸,居然都是意外死亡革屠,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門排宰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來似芝,“玉大人,你說我怎么就攤上這事板甘〉澄停” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵虾啦,是天一觀的道長麻诀。 經(jīng)常有香客問我,道長傲醉,這世上最難降的妖魔是什么蝇闭? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮硬毕,結(jié)果婚禮上呻引,老公的妹妹穿的比我還像新娘。我一直安慰自己吐咳,他們只是感情好逻悠,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著韭脊,像睡著了一般童谒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沪羔,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天饥伊,我揣著相機與錄音,去河邊找鬼。 笑死琅豆,一個胖子當著我的面吹牛愉豺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茫因,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蚪拦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了冻押?” 一聲冷哼從身側(cè)響起驰贷,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎翼雀,沒想到半個月后饱苟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡狼渊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年箱熬,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狈邑。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡城须,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出米苹,到底是詐尸還是另有隱情糕伐,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布蘸嘶,位于F島的核電站良瞧,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏训唱。R本人自食惡果不足惜褥蚯,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望况增。 院中可真熱鬧赞庶,春花似錦、人聲如沸澳骤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽为肮。三九已至摊册,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間颊艳,已是汗流浹背茅特。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工蟆沫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人温治。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像戒悠,于是被迫代替她去往敵國和親熬荆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354