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

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

一、繪制原理

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

屏幕繪制過程中涉及到兩個基本概念:

  • 屏幕刷新率:

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

  • 幀速率:

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

如果 GPU 無法在 16.6ms 完成一幀數(shù)據(jù)的繪制,對應(yīng)的就是屏幕刷新率比幀速率快,屏幕會在兩幀中顯示同一個畫面,這樣給用戶的直接感受就是卡頓,因為繪制速率跟不上屏幕的刷新速率。

二、布局加載原理

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

如果 View 層級嵌套過深會導(dǎo)致:

  • 加長 IO 讀取時間。
  • 加長反射時間。
  • 導(dǎo)致 GPU 繪制不能及時完成,出現(xiàn)卡頓現(xiàn)象。

三、LayoutInflater

3.1、LayoutInflater 大致介紹

這里拷貝了源碼的注釋,從注釋來看,它負責(zé)將 xml 的布局文件加載為一個 View 這樣的一個功能。

這個過程會涉及兩個步驟:

  1. 通過 IO 讀取 xml 文件。
  2. 通過反射來創(chuàng)建對應(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

這個接口是干嘛用的呢?我們在上一節(jié)「性能優(yōu)化4」布局加載原理分析提到,在創(chuàng)建 View 對象時,LayoutInflater#createViewFromTag中首先回去判斷是否設(shè)置了 ①Factory2 或者 ②Factory,它會將 View 的創(chuàng)建工作交給這兩個工廠類的其中一個去實現(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) {
        ...
    }   
}

看完上面的源碼再來看看這個簡易的操作圖解:

LayoutInflater.Factory

下面我貼出來 Factory 的源碼,我們從這個接口的注釋也可以了解到它大致的作用,這是一個 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);
}

那么們可以通過 Factory 可以做啥事呢?

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

3.3、Hook控件的加載耗時

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

//MainActivity extends AppCompatActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
    //Hook 每一個控件加載耗時
    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 + "耗時:" + cost);
            return view;
        }
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
    });
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}
  • 在①處打點開始時間
  • ②處執(zhí)行加載布局的工作,
  • ③位置結(jié)束點,輸出對應(yīng)的時間差即是該控件的加載時間。

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

有些項目的 BaseActivity 是直接繼承至 FragmentActivity ,那么這時我們該怎么去操作呢?

我們再次回到 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)沒有設(shè)置 Factory2 ,F(xiàn)actory 或者 Factory#onCreateView,F(xiàn)actory2#onCreateView 返回 null 的情況,那么創(chuàng)建 View 的工作就交給 ①onCreateView方法。也就是說如果我們的 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 + "耗時:" + cost);
            return view;
        }
    });
    super.onCreate(savedInstanceState);
}

總結(jié):對于 LayoutInflater.Factory 的 hook 機制,我們以低侵入式的方式獲取到每一個控件的加載耗時。

四、總結(jié)

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

記錄于 2019年3月20日

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容