「性能優(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è)步驟:
- 通過(guò) IO 讀取 xml 文件劝堪。
- 通過(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)易的操作圖解:
下面我貼出來(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í)
LayoutInflaterCompat
是 support-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日