Android啟動速度優(yōu)化二測量和優(yōu)化View的加載時間

在第一篇《Android啟動速度優(yōu)化一啟動速度測量》日志里面有提到怎樣在APP啟動過程中測量整個啟動過程的時間叹卷,這一節(jié)記錄了在啟動過程中如何去測量和優(yōu)化View的創(chuàng)建時間字旭。
我們知道在Android中叉讥,Activity都是通過調(diào)用setContentView這個方法來給Activity設(shè)置視圖测蹲。一般的開發(fā)過程是:

  • 依據(jù)UI像屋、UX在layout目錄下定義好布局的XML
  • 創(chuàng)建Activity
  • 在Activity的onCreate方法中調(diào)用setContentView加載布局
  • 加載刷新視圖的數(shù)據(jù)(這部分可能是預(yù)加載资铡,也可能是延遲加載)
  • 通過findViewById獲取目標(biāo)View电禀,并設(shè)置用戶響應(yīng)事件(例如點擊,滑動等)
  • 使用加載好的數(shù)據(jù)對目標(biāo)View進(jìn)行視覺刷新

在這個過程中笤休,本文重點關(guān)注布局的定義和setContentView這兩個事件尖飞,其他的在后面的章節(jié)進(jìn)行討論。

一店雅、View加載時間的測量

在做優(yōu)化之前我們需要先知道View加載的實際耗時是多少政基,加載過程中是哪些維度占用時間多,了解了時間占用情況后才能結(jié)合實際場景和業(yè)務(wù)邏輯進(jìn)行加載優(yōu)化闹啦。

1.1沮明、測量setContentView的總耗時

我們知道setContentView里面會去解析布局文件,而Android的View體系是一個樹狀的數(shù)據(jù)結(jié)構(gòu)窍奋,這個就使得系統(tǒng)需要通過遞歸的形式來解析荐健,那如果你的布局層次越多,遞歸的時間就會越長琳袄。
在解析完了之后Android實際上是通過反射來實例化View的江场。基于java發(fā)射機(jī)制的話本身就會有一定的性能影響窖逗,如果這個時候你在自己的View的構(gòu)造函數(shù)中執(zhí)行過多的且耗時的操作址否,也是會直接影響View加載的時間。
基于以上現(xiàn)象滑负,我們需要在啟動過程中知道整個View加載的總時間和每一個View的創(chuàng)建時間

  • 侵入式打點獲得總耗時
    這個比較簡單在张,在setContentView前后打點記錄時間就好
        long start = System.currentTimeMillis();
        setContentView(R.layout.activity_main);
        Log.d("SpeedTest","setContentView cost:" + (System.currentTimeMillis() - start)+ "ms");
  • AOP實現(xiàn)非侵入式獲得總耗時
    為了能夠快速且方便的集成AOP用含,這里提供一個集成AOP的插件矮慕,可以直接使用,簡單兩步就可以集成啄骇。
    https://github.com/alexluotututu/aop_plugin
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class TestViewInflate {

    @Around("call(* android.app.Activity.setContentView(..))")
    public void setContentViewTest(ProceedingJoinPoint point) {
        long start = System.currentTimeMillis();
        Signature signature = point.getSignature();
        try {
            point.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.d("SpeedTest", point.getTarget().getClass().getName()
                + " setContentView total time:" + (System.currentTimeMillis() - start));
    }

}

我們這里不討論AOP是怎樣集成到項目中的痴鳄,這個不是本章的重點,需要集成的話可以參考其他博客的文章缸夹。
我們可以看到以上兩種方式都可以統(tǒng)計到setContentView的總耗時痪寻,輸出結(jié)果如下:

05-19 10:39:11.061 26091 26091 D SpeedTest: com.snail.speeddemo.MainActivity setContentView total time:83
05-19 10:39:11.061 26091 26091 D SpeedTest: setContentView cost:83ms

兩者的耗時統(tǒng)計是一致的。

  • Systrace統(tǒng)計總耗時
        TraceCompat.beginSection("setContentView");
        setContentView(R.layout.activity_main);
        TraceCompat.endSection();

我們抓到的trace中找到setContentView這個節(jié)點虽惭,便可以知道總的耗時是多少了


systrace獲取setContentView時間
systrace獲取setContentView時間

1.2橡类、測量每個View的加載時間

在第1.1節(jié)中我們通過打點或者Systrace來獲取到setContentView的總耗時,但是在優(yōu)化過程中我們還是需要知道每個View的加載耗時芽唇,這個時候怎么辦顾画。結(jié)合Android的View加載機(jī)制和網(wǎng)上一些大神們的做法取劫,可以通過設(shè)置LayoutInflater的factory來實現(xiàn),下面是針對兩種不同的父Activity的設(shè)置方法

  • 繼承自AppCompatActivity的實現(xiàn)方法
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
            @Nullable
            @Override
            public View onCreateView(@Nullable View view, @NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
                long start = System.currentTimeMillis();
                View createdView = getDelegate().createView(view,s,context,attributeSet);
                Log.d("SpeedTest","create "+s+" cost:"+(System.currentTimeMillis() - start));

                return createdView;
            }

            @Nullable
            @Override
            public View onCreateView(@NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
                return null;
            }
        });

我們需要注意的是需要在super.onCreate之前調(diào)用這段代碼研侣,如果不是的話會出現(xiàn)crash的問題,因為super里面會設(shè)置factory谱邪,再重新設(shè)置的話系統(tǒng)會拋出以下異常

Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater

我們最終的輸出結(jié)果:

05-19 11:20:26.722  6894  6894 D SpeedTest: create LinearLayout cost:3
05-19 11:20:26.722  6894  6894 D SpeedTest: create ViewStub cost:0
05-19 11:20:26.723  6894  6894 D SpeedTest: create FrameLayout cost:0
05-19 11:20:26.725  6894  6894 D SpeedTest: create androidx.appcompat.widget.ActionBarOverlayLayout cost:0
05-19 11:20:26.727  6894  6894 D SpeedTest: create androidx.appcompat.widget.ContentFrameLayout cost:0
05-19 11:20:26.728  6894  6894 D SpeedTest: create androidx.appcompat.widget.ActionBarContainer cost:0
05-19 11:20:26.731  6894  6894 D SpeedTest: create androidx.appcompat.widget.Toolbar cost:0
05-19 11:20:26.736  6894  6894 D SpeedTest: create androidx.appcompat.widget.ActionBarContextView cost:0
05-19 11:20:26.745  6894  6894 D SpeedTest: create androidx.constraintlayout.widget.ConstraintLayout cost:0
05-19 11:20:26.755  6894  6894 D SpeedTest: create TextView cost:1

這樣一來我們就可以看到每個View的創(chuàng)建時間了

  • 繼承自framework中的Activity或者FragmentActivity的設(shè)置方法
LayoutInflaterCompat.setFactory(getLayoutInflater(), new LayoutInflaterFactory() {

            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                long start = System.currentTimeMillis();
                View view = null;
                try{
                    view = getLayoutInflater().createView(name,null,attrs);
                }catch (ClassNotFoundException ex){

                }
                Log.d("SpeedTest","create "+name+" cost:"+(System.currentTimeMillis() - start));

                return view;
            }
        });

同樣也是可以得到每個View的加載時間

二、優(yōu)化View的加載時間

通過第一節(jié)中的測量View加載方法庶诡,我們可以知道加載的總耗時和每個View的加載耗時惦银,為我們做進(jìn)一步優(yōu)化加載時間提供更多的輸入。

2.1末誓、優(yōu)化布局層次

布局層次會影響系統(tǒng)遞歸遍歷布局的時間扯俱,因此我們在布局的時候應(yīng)該要勁量采用輕量化的布局層次來實現(xiàn)功能。

2.1.1喇澡、 查看布局層次

我們可以使用Android Studio提供的Layout Inspector來查看布局層級


Layout Inspector
2.1.2蘸吓、 ConstraintLayout VS RelativeLayout

我們在布局的時候為了實現(xiàn)一些特殊的布局,可能會用到LinearLayout撩幽,RelativeLayout或者ConstraintLayout库继。后兩者都是可以減小布局層級的,但是這兩位中RelativeLayout如果子View太多也是有性能問題窜醉。因此我們在布局的時候還是要做一個綜合的評估宪萄。總體上ConstraintLayout的性能優(yōu)于RelativeLayout榨惰,因此都會建議大家用ConstraintLayout來實現(xiàn)復(fù)雜布局

2.1.3拜英、 merge標(biāo)簽減少布局層次

merge標(biāo)簽可以減少一個布局層次,使用merge需要注意以下幾點

  • merge標(biāo)簽只能作為布局xml中的root節(jié)點
  • merge不是View琅催,對它設(shè)置布局參數(shù)是沒用的
  • 使用merge的時候如果要動態(tài)inflate布局居凶,需要設(shè)置parent,且attachToParent參數(shù)要為true
getLayoutInflater().inflate(R.layout.activity_main,mParent,true);

  • merge標(biāo)簽如果需要在Android Studio中預(yù)覽到視圖的話需要添加parentTag
tools:parentTag="android.widget.LinearLayout"
2.1.4藤抡、 ViewStub懶加載

ViewStub的使用方法網(wǎng)上已經(jīng)有很多了侠碧,因此我們這里不討論怎樣用。主要看一下ViewStub為什么會可以后話加載時間缠黍。

  • ViewStub是一個輕量化的View弄兜,它默認(rèn)的顯示狀態(tài)是View.GONE,并且不會繪制任何東西瓷式。我們從它的源碼里面可以看出來
    構(gòu)造函數(shù)中把自己設(shè)置為不可見的
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context);

  //其他代碼

        setVisibility(GONE);
        setWillNotDraw(true);
    }

  • ViewStub本身不繪制任何東西替饿,所有加載時間很快

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
    }


可以從源碼上看到,它的draw方法和dispatchDraw直接是空的贸典,onMeasure方法也是直接設(shè)置寬度和高度為0

  • ViewStub在調(diào)用inflate加載真正的View之后自己會被替換掉
    我們可以從源碼中看到有這段邏輯,因此在使用它的時候要注意這種情況
    inflate方法
    public View inflate() {
        //ViewStub和真正加載的View的parent為同一個
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                //這里會先通過LayoutInfater把ViewStub中指定的layout加載進(jìn)來
                final View view = inflateViewNoAdd(parent);
                //這里就會把真正的View添加到parent里面视卢,并且把ViewStub移除掉
                replaceSelfWithView(view, parent);

                mInflatedViewRef = new WeakReference<>(view);
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }

                return view;
            } else {
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }

replaceSelfWithView把自己干掉

    private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        //先把自己從parent中移除
        parent.removeViewInLayout(this);

        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        //下面的這段代碼就是把真正的View添加到原來ViewStub的位置上
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }

三、其他的一些關(guān)鍵優(yōu)化項

3.1廊驼、APP主界面需要平衡包大小和啟動速度的關(guān)系据过,勁量少使用webp

webp和png之間的差異主要在于壓縮這個維度颊埃,但是在內(nèi)存占用上兩者是差不多的。因為壓縮率的問題蝶俱,導(dǎo)致webp在解碼的時候會比png慢4~5倍的樣子

3.2班利、不是很必要的話不要在布局中使用大量的TextView

TextView因為要處理文本排班和測量繪制的原因,導(dǎo)致TextView的性能比較低榨呆,主界面不是非必要的話文本內(nèi)容可以做懶加載

3.3罗标、 Lottie動畫文件的加載

現(xiàn)在很多APP可能都會使用Lottie來實現(xiàn)一些動畫,為了方便動畫的加載积蜻,會直接在布局文件中制定動畫json文件闯割。這個也是會消耗掉一部分的加載時間,因此如果可以做懶加載的話會比較好

四竿拆、總結(jié)

啟動速度和性能有話是一個看上去簡單宙拉,但是做起來難,特別是持續(xù)做比較難的一件事情丙笋。需要細(xì)心持續(xù)的去做谢澈,本節(jié)羅列和記錄了一些普遍常用的工具和方法。后續(xù)會有一些進(jìn)階的部分逐步呈現(xiàn)出來御板。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锥忿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子怠肋,更是在濱河造成了極大的恐慌敬鬓,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笙各,死亡現(xiàn)場離奇詭異钉答,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)杈抢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門数尿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人春感,你說我怎么就攤上這事砌创÷哺祝” “怎么了鲫懒?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長刽辙。 經(jīng)常有香客問我窥岩,道長,這世上最難降的妖魔是什么宰缤? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任颂翼,我火速辦了婚禮晃洒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘朦乏。我一直安慰自己球及,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布呻疹。 她就那樣靜靜地躺著吃引,像睡著了一般。 火紅的嫁衣襯著肌膚如雪刽锤。 梳的紋絲不亂的頭發(fā)上镊尺,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天,我揣著相機(jī)與錄音并思,去河邊找鬼庐氮。 笑死,一個胖子當(dāng)著我的面吹牛宋彼,可吹牛的內(nèi)容都是我干的弄砍。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼输涕,長吁一口氣:“原來是場噩夢啊……” “哼输枯!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起占贫,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤桃熄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后型奥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞳收,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年厢汹,在試婚紗的時候發(fā)現(xiàn)自己被綠了螟深。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡烫葬,死狀恐怖界弧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情搭综,我是刑警寧澤垢箕,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站兑巾,受9級特大地震影響条获,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蒋歌,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一帅掘、第九天 我趴在偏房一處隱蔽的房頂上張望委煤。 院中可真熱鬧,春花似錦修档、人聲如沸碧绞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽头遭。三九已至,卻和暖如春癣诱,著一層夾襖步出監(jiān)牢的瞬間计维,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工撕予, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留鲫惶,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓实抡,卻偏偏與公主長得像欠母,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子吆寨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,947評論 2 355