「性能優(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 這樣的一個功能。
這個過程會涉及兩個步驟:
- 通過 IO 讀取 xml 文件。
- 通過反射來創(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) {
...
}
}
看完上面的源碼再來看看這個簡易的操作圖解:

下面我貼出來 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控件的加載耗時
LayoutInflaterCompat 是 support-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日