「性能優(yōu)化2.1」LayoutInflater Hook控件加載耗時(shí)

「性能優(yōu)化1.0」啟動(dòng)分類及啟動(dòng)時(shí)間的測(cè)量
「性能優(yōu)化1.1」計(jì)算方法的執(zhí)行時(shí)間
「性能優(yōu)化1.2」異步優(yōu)化
「性能優(yōu)化1.3」延遲加載方案
「性能優(yōu)化2.0」布局加載原理
「性能優(yōu)化2.1」LayoutInflater Hook控件加載耗時(shí)

一书蚪、繪制原理

CPU 負(fù)責(zé)計(jì)算需要展示的數(shù)據(jù)埋合,而 GPU 負(fù)責(zé)將數(shù)據(jù)繪制到屏幕上汰扭。

屏幕繪制過(guò)程中涉及到兩個(gè)基本概念:

  • 屏幕刷新率:

屏幕刷新率代表屏幕在一秒內(nèi)刷新屏幕的次數(shù)麦向,這個(gè)值用赫茲來(lái)表示,取決于硬件的固定參數(shù)。這個(gè)值一般是60Hz,即每16.66ms系統(tǒng)發(fā)出一個(gè) VSYNC 信號(hào)來(lái)通知刷新一次屏幕残炮。

  • 幀速率:

幀速率代表了GPU一秒內(nèi)繪制操作的幀數(shù)半等,比如30fps/60fps。

如果 GPU 無(wú)法在 16.6ms 完成一幀數(shù)據(jù)的繪制拾氓,對(duì)應(yīng)的就是屏幕刷新率比幀速率快冯挎,屏幕會(huì)在兩幀中顯示同一個(gè)畫(huà)面,這樣給用戶的直接感受就是卡頓咙鞍,因?yàn)槔L制速率跟不上屏幕的刷新速率房官。

二、布局加載原理

我在上一篇博客中描述了布局的加載流程「性能優(yōu)化4」布局加載原理续滋。在布局的加載中主要是分為兩個(gè)過(guò)程翰守,第一通過(guò) IO 從磁盤(pán)中加載資源文件并封裝為 XmlPullParser 對(duì)象,第二通過(guò) XML 解析器解析 XML 并通過(guò)反射創(chuàng)建 View 對(duì)象疲酌。

如果 View 層級(jí)嵌套過(guò)深會(huì)導(dǎo)致:

  • 加長(zhǎng) IO 讀取時(shí)間潦俺。
  • 加長(zhǎng)反射時(shí)間。
  • 導(dǎo)致 GPU 繪制不能及時(shí)完成徐勃,出現(xiàn)卡頓現(xiàn)象事示。

三、LayoutInflater

3.1僻肖、LayoutInflater 大致介紹

這里拷貝了源碼的注釋肖爵,從注釋來(lái)看,它負(fù)責(zé)將 xml 的布局文件加載為一個(gè) View 這樣的一個(gè)功能臀脏。

這個(gè)過(guò)程會(huì)涉及兩個(gè)步驟:

  1. 通過(guò) IO 讀取 xml 文件劝堪。
  2. 通過(guò)反射來(lái)創(chuàng)建對(duì)應(yīng)的 View。
/**
 * Instantiates a layout XML file into its corresponding {@link android.view.View}
 */
@SystemService(Context.LAYOUT_INFLATER_SERVICE)
public abstract class LayoutInflater {...}

3.2揉稚、LayoutInflater.Factory

這個(gè)接口是干嘛用的呢秒啦?我們?cè)谏弦还?jié)「性能優(yōu)化4」布局加載原理分析提到,在創(chuàng)建 View 對(duì)象時(shí)搀玖,LayoutInflater#createViewFromTag中首先回去判斷是否設(shè)置了 ①Factory2 或者 ②Factory余境,它會(huì)將 View 的創(chuàng)建工作交給這兩個(gè)工廠類的其中一個(gè)去實(shí)現(xiàn)。

//LayoutInflater.java
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
    ...    
    try {
        View view;
        if (mFactory2 != null) {
            //①
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            //②
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
       
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                //③
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }
        return view;
    } catch (InflateException e) {
        ...
    }   
}

看完上面的源碼再來(lái)看看這個(gè)簡(jiǎn)易的操作圖解:

LayoutInflater.Factory

下面我貼出來(lái) Factory 的源碼,我們從這個(gè)接口的注釋也可以了解到它大致的作用芳来,這是一個(gè) Hook 操作含末,因此我們可以在這里做我們想做的事。

public interface Factory {
    /**
     * Hook you can supply that is called when inflating from a LayoutInflater.
     * You can use this to customize the tag names available in your XML
     * layout files.
     *
     * <p>
     * Note that it is good practice to prefix these custom names with your
     * package (i.e., com.coolcompany.apps) to avoid conflicts with system
     * names.
     *
     * @param name Tag name to be inflated.
     * @param context The context the view is being created in.
     * @param attrs Inflation attributes as specified in XML file.
     *
     * @return View Newly created view. Return null for the default
     *         behavior.
     */
    public View onCreateView(String name, Context context, AttributeSet attrs);
}

那么們可以通過(guò) Factory 可以做啥事呢即舌?

例如:可以全局修改 TextView 的顏色佣盒,字體等,這里推薦一篇張鴻洋的博文Android 探究 LayoutInflater setFactory里面介紹了 Factory 的一些使用方式顽聂。

3.3肥惭、Hook控件的加載耗時(shí)

LayoutInflaterCompatsupport-v4 兼容包下的一個(gè)類,通過(guò) setFactoty2 方法給對(duì)應(yīng)的 getLayoutInflater() 設(shè)置一個(gè) Factory工廠紊搪,其內(nèi)部就是給 LayoutInflater 的 mFactory2 賦值蜜葱。我們知道布局的加載是通過(guò) LayoutInflater 布局加載器去加載的,因此這里設(shè)置的 Factory2 可以在 LayoutInflater 加載每一個(gè)控件時(shí)進(jìn)行hook操作嗦明,具體的實(shí)現(xiàn)如下:

//MainActivity extends AppCompatActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
    //Hook 每一個(gè)控件加載耗時(shí)
    LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            //①
            long startTime = System.currentTimeMillis();
            //②
            View view = getDelegate().createView(parent, name, context, attrs);
            //③
            long cost = System.currentTimeMillis() - startTime;
            Log.d(TAG, "加載控件:" + name + "耗時(shí):" + cost);
            return view;
        }
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
    });
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}
  • 在①處打點(diǎn)開(kāi)始時(shí)間
  • ②處執(zhí)行加載布局的工作笼沥,
  • ③位置結(jié)束點(diǎn),輸出對(duì)應(yīng)的時(shí)間差即是該控件的加載時(shí)間娶牌。

關(guān)于②處使用了 getDalegate().createView(...) 這個(gè)方法是在 support-v7兼容包下的 AppCompatActivity 中定義的奔浅。而如果項(xiàng)目中 BaseActivity 沒(méi)有繼承至 AppCompatActivity 那么②處就不能getDalegate().createView(...)這樣寫(xiě)了。

有些項(xiàng)目的 BaseActivity 是直接繼承至 FragmentActivity 诗良,那么這時(shí)我們?cè)撛趺慈ゲ僮髂兀?/p>

我們?cè)俅位氐?LayoutInflater#createViewFromTag源碼:

//LayoutInflater.java
if (view == null) {
    final Object lastContext = mConstructorArgs[0];
    mConstructorArgs[0] = context;
    try {
        //①
        if (-1 == name.indexOf('.')) {
            view = onCreateView(parent, name, attrs);
        } else {
            view = createView(name, null, attrs);
        }
    } finally {
        mConstructorArgs[0] = lastContext;
    }
}

當(dāng)沒(méi)有設(shè)置 Factory2 汹桦,F(xiàn)actory 或者 Factory#onCreateView,F(xiàn)actory2#onCreateView 返回 null 的情況鉴裹,那么創(chuàng)建 View 的工作就交給 ①onCreateView方法舞骆。也就是說(shuō)如果我們的 Activity 不是直接繼承至 AppCompatActivity的話,那么就可以使用 LayoutInflater#createView(name, null, attrs)加載指定的控件径荔。

//MainActivity extends FragmentActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
    LayoutInflaterCompat.setFactory(getLayoutInflater(), new LayoutInflaterFactory() {
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            long startTime = System.currentTimeMillis();
            View view = null;
            try {
                view = getLayoutInflater().createView(name, null, attrs);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            long cost = System.currentTimeMillis() - startTime;
            map.put(parent,new Hodler(parent,view,name,cost));
            Log.d("=========", "加載布局:" + name + "耗時(shí):" + cost);
            return view;
        }
    });
    super.onCreate(savedInstanceState);
}

總結(jié):對(duì)于 LayoutInflater.Factory 的 hook 機(jī)制督禽,我們以低侵入式的方式獲取到每一個(gè)控件的加載耗時(shí)。

四总处、總結(jié)

好了狈惫,本小節(jié)主要簡(jiǎn)單地介紹了Android繪制原理,了解 GPU 繪制頻率和屏幕刷新頻率之間的關(guān)系鹦马。緊接著分享了布局加載加載原理胧谈,并且通過(guò)分析布局的加載過(guò)程我們知道可以通過(guò) LayoutInflater.Factory 來(lái) hook 控件的創(chuàng)建過(guò)程。并且最后通過(guò)LayoutInflater.Factory 實(shí)戰(zhàn)來(lái)獲取每一個(gè)控件的加載時(shí)間荸频。通過(guò)分析這個(gè)時(shí)間菱肖,我們就可以初步判斷哪些控件是比較耗時(shí)的,然后再做進(jìn)一步的優(yōu)化旭从。

記錄于 2019年3月20日

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末稳强,一起剝皮案震驚了整個(gè)濱河市场仲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌键袱,老刑警劉巖燎窘,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摹闽,死亡現(xiàn)場(chǎng)離奇詭異蹄咖,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)付鹿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)澜汤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人舵匾,你說(shuō)我怎么就攤上這事俊抵。” “怎么了坐梯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵徽诲,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我吵血,道長(zhǎng)谎替,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任蹋辅,我火速辦了婚禮钱贯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘侦另。我一直安慰自己秩命,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布褒傅。 她就那樣靜靜地躺著弃锐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪殿托。 梳的紋絲不亂的頭發(fā)上霹菊,一...
    開(kāi)封第一講書(shū)人閱讀 52,475評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音碌尔,去河邊找鬼浇辜。 笑死,一個(gè)胖子當(dāng)著我的面吹牛唾戚,可吹牛的內(nèi)容都是我干的柳洋。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼叹坦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼熊镣!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤绪囱,失蹤者是張志新(化名)和其女友劉穎测蹲,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鬼吵,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡扣甲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了齿椅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片琉挖。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖涣脚,靈堂內(nèi)的尸體忽然破棺而出示辈,到底是詐尸還是另有隱情,我是刑警寧澤遣蚀,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布矾麻,位于F島的核電站,受9級(jí)特大地震影響芭梯,放射性物質(zhì)發(fā)生泄漏险耀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一粥帚、第九天 我趴在偏房一處隱蔽的房頂上張望胰耗。 院中可真熱鬧,春花似錦芒涡、人聲如沸柴灯。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)赠群。三九已至,卻和暖如春旱幼,著一層夾襖步出監(jiān)牢的瞬間查描,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工柏卤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留冬三,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓缘缚,卻偏偏與公主長(zhǎng)得像勾笆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子桥滨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361