大多數(shù)用戶感知到的卡頓等性能問題的最主要根源都是因為渲染性能藤树。Android系統(tǒng)每隔大概16.6ms發(fā)出VSYNC信 號屡限,觸發(fā)對UI進(jìn)行渲染翰撑,如果每次渲染都成功逝撬,這樣就能夠達(dá)到流暢的畫面所需要的60fps狡相,為了能夠?qū)崿F(xiàn)60fps萄金, 這意味著程序的大多數(shù)操作都必須在16ms內(nèi)完成浙炼。
我們通常都會提到60fps與16ms资厉,可是知道為何會是以程序是否達(dá)到60fps來作為App性能的衡量標(biāo)準(zhǔn)嗎?這 是因為人眼與大腦之間的協(xié)作無法感知超過60fps的畫面更新窄刘。
12fps大概類似手動快速翻動書籍的幀率,這明顯是可以感知到不夠順滑的展氓。24fps使得人眼感知的是連續(xù)線 性的運動穆趴,這其實是歸功于運動模糊的效果脸爱。24fps是電影膠圈通常使用的幀率遇汞,因為這個幀率已經(jīng)足夠支撐 大部分電影畫面需要表達(dá)的內(nèi)容,同時能夠最大的減少費用支出。但是低于30fps是無法順暢表現(xiàn)絢麗的畫面 內(nèi)容的空入,此時就需要用到60fps來達(dá)到想要的效果络它,當(dāng)然超過60fps是沒有必要的。
開發(fā)app的性能目標(biāo)就是保持60fps歪赢,這意味著每一幀你只有16ms=1000/60的時間來處理所有的任務(wù)化戳。
如果某個操作花費時間是24ms,系統(tǒng)在得到VSYNC信號的時候就無法進(jìn)行正常渲染埋凯,這樣就發(fā)生了丟幀現(xiàn)象点楼。那么用戶在32ms內(nèi)看到的會是同一幀畫面。
有很多原因可以導(dǎo)致丟幀白对, 一般主線程過多的UI繪制掠廓、大量的IO操作或是大量的計算操作占用CPU,都會導(dǎo)致App 界面卡頓甩恼。
一般主線程過多的UI繪制蟀瞧、大量的IO操作或是大量的計算操作占用CPU,導(dǎo)致App界面卡頓条摸。
卡頓分析
Systrace
Systrace 是Android平臺提供的一款工具悦污,用于記錄短期內(nèi)的設(shè)備活動。該工具會生成一份報告钉蒲,其中匯總了 Android 內(nèi)核中的數(shù)據(jù)切端,例如 CPU 調(diào)度程序、磁盤活動和應(yīng)用線程子巾。Systrace主要用來分析繪制性能方面的問 題帆赢。在發(fā)生卡頓時,通過這份報告可以知道當(dāng)前整個系統(tǒng)所處的狀態(tài)线梗,從而幫助開發(fā)者更直觀的分析系統(tǒng)瓶頸椰于,改 進(jìn)性能。
TraceView可以看出代碼在運行時的一些具體信息仪搔,方法調(diào)用時長瘾婿,次數(shù),時間比率烤咧,了解代碼運行過程的 效率問題偏陪,從而針對性改善代碼。所以對于可能導(dǎo)致卡頓的耗時方法也可以通過TraceView檢測煮嫌。
要使用Systrace笛谦,需要先安裝 Python2.7。安裝完成后配置環(huán)境變量 path 昌阿,隨后在命令行輸入: python -- version 進(jìn)行驗證饥脑。
Trace API
由于profile配置時候可能會影響我們分析恳邀。所以我們用Trace API生成trace文件后導(dǎo)入進(jìn)來的方式會更準(zhǔn)確
在Application中 我們 可這樣設(shè)置
Debug.startMethodTracingSampling(new File(Environment.getExternalStorageDirectory(),
"zcwfeng").getAbsolutePath(), 8 * 1024 * 1024, 1_000);
采樣的方式,有時候我們需要多次采樣灶轰,有可能會漏掉細(xì)節(jié)谣沸,性能優(yōu)化是個非常細(xì)致的活。
Debug.startMethodTracing(new File(Environment.getExternalStorageDirectory(),
"zcwfeng").getAbsolutePath());
跟蹤模式會比較慢笋颤,而且可能會影像啟動速度乳附,但是分析相對全面
在MainActivity 中的方法
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
Debug.stopMethodTracing();
}
App層面監(jiān)控卡頓
systrace可以讓我們了解應(yīng)用所處的狀態(tài),了解應(yīng)用因為什么原因?qū)е碌陌槌巍H粜枰獪?zhǔn)確分析卡頓發(fā)生在什么函數(shù)赋除,
資源占用情況如何,目前業(yè)界兩種主流有效的app監(jiān)控方式如下: 1非凌、 利用UI線程的Looper打印的日志匹配;
2贤重、 使用Choreographer.FrameCallback
Looper日志檢測卡頓
Android主線程更新UI。如果界面1秒鐘刷新少于60次清焕,即FPS小于60并蝗,用戶就會產(chǎn)生卡頓感覺。簡單來說秸妥, Android使用消息機制進(jìn)行UI更新滚停,UI線程有個Looper,在其loop方法中會不斷取出message粥惧,調(diào)用其綁定的 Handler在UI線程執(zhí)行键畴。如果在handler的dispatchMesaage方法里有耗時操作,就會發(fā)生卡頓突雪。
public static void loop() { //......
for (; ; ) { //......
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
//......
}
}
只要檢測 msg.target.dispatchMessage(msg) 的執(zhí)行時間起惕,就能檢測到部分UI線程是否有耗時的操作。注意到這行 執(zhí)行代碼的前后咏删,有兩個logging.println函數(shù)惹想,如果設(shè)置了logging,會分別打印出>>>>> Dispatching to和 <<<<< Finished to 這樣的日志督函,這樣我們就可以通過兩次log的時間差值嘀粱,來計算dispatchMessage的執(zhí)行時 間,從而設(shè)置閾值判斷是否發(fā)生了卡頓辰狡。
public final class Looper {
private Printer mLogging;
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
}
public interface Printer {
void println(String x);
}
Looper 提供了 setMessageLogging(@Nullable Printer printer) 方法锋叨,所以我們可以自己實現(xiàn)一個Printer,在 通過setMessageLogging()方法傳入即可:
public class BlockCanary {
public static void install() {
LogMonitor logMonitor = new LogMonitor();
Looper.getMainLooper().setMessageLogging(logMonitor);
}
}
public class LogMonitor implements Printer {
private StackSampler mStackSampler;
private boolean mPrintingStarted = false;
private long mStartTimestamp;
// 卡頓閾值
private long mBlockThresholdMillis = 3000;
//采樣頻率
private long mSampleInterval = 1000;
private Handler mLogHandler;
public LogMonitor() {
mStackSampler = new StackSampler(mSampleInterval);
HandlerThread handlerThread = new HandlerThread("block-canary-io");
handlerThread.start();
mLogHandler = new Handler(handlerThread.getLooper());
}
@Override
public void println(String x) {
//從if到else會執(zhí)行 dispatchMessage宛篇,如果執(zhí)行耗時超過閾值娃磺,輸出卡頓信息
if (!mPrintingStarted) {
//記錄開始時間
mStartTimestamp = System.currentTimeMillis();
mPrintingStarted = true;
mStackSampler.startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
//出現(xiàn)卡頓
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
mStackSampler.stopDump();
}
}
private void notifyBlockEvent(final long endTime) {
mLogHandler.post(new Runnable() {
@Override
public void run() {
//獲得卡頓時主線程堆棧
List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
for (String stack : stacks) {
Log.e("block-canary", stack);
}
}
});
}
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
}
public class StackSampler {
public static final String SEPARATOR = "\r\n";
public static final SimpleDateFormat TIME_FORMATTER =
new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
private Handler mHandler;
private Map<Long, String> mStackMap = new LinkedHashMap<>();
private int mMaxCount = 100;
private long mSampleInterval;
//是否需要采樣
protected AtomicBoolean mShouldSample = new AtomicBoolean(false);
public StackSampler(long sampleInterval) {
mSampleInterval = sampleInterval;
HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
}
/**
* 開始采樣 執(zhí)行堆棧
*/
public void startDump() {
//避免重復(fù)開始
if (mShouldSample.get()) {
return;
}
mShouldSample.set(true);
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, mSampleInterval);
}
public void stopDump() {
if (!mShouldSample.get()) {
return;
}
mShouldSample.set(false);
mHandler.removeCallbacks(mRunnable);
}
public List<String> getStacks(long startTime, long endTime) {
ArrayList<String> result = new ArrayList<>();
synchronized (mStackMap) {
for (Long entryTime : mStackMap.keySet()) {
if (startTime < entryTime && entryTime < endTime) {
result.add(TIME_FORMATTER.format(entryTime)
+ SEPARATOR
+ SEPARATOR
+ mStackMap.get(entryTime));
}
}
}
return result;
}
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s.toString()).append("\n");
}
synchronized (mStackMap) {
//最多保存100條堆棧信息
if (mStackMap.size() == mMaxCount) {
mStackMap.remove(mStackMap.keySet().iterator().next());
}
mStackMap.put(System.currentTimeMillis(), sb.toString());
}
if (mShouldSample.get()) {
mHandler.postDelayed(mRunnable, mSampleInterval);
}
}
};
}
其實這種方式也就是 BlockCanary 原理。
做個實驗叫倍,在MainActivity入口偷卧,setContent 之前sleep一下
// TODO Test BlockCanery 卡頓分析測試
try {
Thread.sleep(3_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
日志消息:
12-16 12:52:37.955 2056-2198/? I/art: Starting a blocking GC Explicit
12-16 12:52:42.836 29962-29982/top.zcwfeng.arch_demo E/block-canary: 12-16 12:52:40.313
java.lang.Thread.sleep(Native Method)
java.lang.Thread.sleep(Thread.java:1031)
java.lang.Thread.sleep(Thread.java:985)
top.zcwfeng.arch_demo.MainActivity.onCreate(MainActivity.java:54)
android.app.Activity.performCreate(Activity.java:6357)
android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1108)
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2436)
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2543)
android.app.ActivityThread.access$1000(ActivityThread.java:156)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1407)
android.os.Handler.dispatchMessage(Handler.java:102)
android.os.Looper.loop(Looper.java:157)
android.app.ActivityThread.main(ActivityThread.java:5653)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:746)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:636)
12-16 12:52:42.836 29962-29982/top.zcwfeng.arch_demo E/block-canary: 12-16 12:52:41.316
java.lang.Thread.sleep(Native Method)
java.lang.Thread.sleep(Thread.java:1031)
java.lang.Thread.sleep(Thread.java:985)
top.zcwfeng.arch_demo.MainActivity.onCreate(MainActivity.java:54)
android.app.Activity.performCreate(Activity.java:6357)
android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1108)
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2436)
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2543)
android.app.ActivityThread.access$1000(ActivityThread.java:156)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1407)
android.os.Handler.dispatchMessage(Handler.java:102)
android.os.Looper.loop(Looper.java:157)
android.app.ActivityThread.main(ActivityThread.java:5653)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:746)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:636)
12-16 12:52:42.836 29962-29982/top.zcwfeng.arch_demo E/block-canary: 12-16 12:52:42.318
java.lang.Thread.sleep(Native Method)
java.lang.Thread.sleep(Thread.java:1031)
java.lang.Thread.sleep(Thread.java:985)
top.zcwfeng.arch_demo.MainActivity.onCreate(MainActivity.java:54)
android.app.Activity.performCreate(Activity.java:6357)
android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1108)
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2436)
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2543)
android.app.ActivityThread.access$1000(ActivityThread.java:156)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1407)
android.os.Handler.dispatchMessage(Handler.java:102)
android.os.Looper.loop(Looper.java:157)
android.app.ActivityThread.main(ActivityThread.java:5653)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:746)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:636)
Choreographer.FrameCallback
Android系統(tǒng)每隔16ms發(fā)出VSYNC信號嘿般,來通知界面進(jìn)行重繪、渲染涯冠,每一次同步的周期約為16.6ms,代表一幀 的刷新頻率逼庞。通過Choreographer類
設(shè)置它的FrameCallback
函數(shù)蛇更,當(dāng)每一幀被渲染時會觸發(fā)回調(diào)
FrameCallback.doFrame (long frameTimeNanos)
函數(shù)。frameTimeNanos
是底層VSYNC信號
到達(dá)的時間戳 赛糟。
public class ChoreographerHelper {
static long lastFrameTimeNanos = 0;
public static void start() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
//上次回調(diào)時間
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
return;
}
long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
if (diff > 16.6f) {
//掉幀數(shù)
int droppedCount = (int) (diff / 16.6);
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
}
}
}
通過ChoreographerHelper
可以實時計算幀率和掉幀數(shù)派任,實時監(jiān)測App頁面的幀率數(shù)據(jù),發(fā)現(xiàn)幀率過低璧南,還可以自 動保存現(xiàn)場堆棧信息掌逛。
Looper比較適合在發(fā)布前進(jìn)行測試或者小范圍灰度測試然后定位問題,ChoreographerHelper適合監(jiān)控線上環(huán)境 的 app 的掉幀情況來計算 app 在某些場景的流暢度然后有針對性的做性能優(yōu)化司倚。
布局優(yōu)化
層級優(yōu)化
measure豆混、layout、draw這三個過程都包含自頂向下的View Tree遍歷耗時动知,如果視圖層級太深自然需要更多的時 間來完成整個繪測過程皿伺,從而造成啟動速度慢、卡頓等問題盒粮。而onDraw在頻繁刷新時可能多次出發(fā)鸵鸥,因此 onDraw更不能做耗時操作,同時需要注意內(nèi)存抖動丹皱。對于布局性能的檢測妒穴,依然可以使用systrace與traceview按 照繪制流程檢查繪制耗時函數(shù)。
Layout Inspector
然后選擇需要查看的進(jìn)程與Activity:
在我這里摊崭,我的架構(gòu)demo工程讼油,里面兩個自定義View,PictureTitleView 和 TitleView 都已經(jīng)集成LinearLayout呢簸,但是布局里面有嵌套了LineLayout汁讼,屬于寫的時候沒注意,對于這種就可以優(yōu)化掉xml布局阔墩,去掉多余Linelayout嘿架。可以利用merge標(biāo)簽
使用merge標(biāo)簽
當(dāng)我們有一些布局元素需要被多處使用時啸箫,這時候我們會考慮將其抽取成一個單獨的布局文件耸彪。在需要使用的地方 通過 include 加載。
用的時候我們沒必要寫多個嵌套忘苛,這個時候可以用merge蝉娜。
修改為merge后唱较,通過LayoutInspector能夠發(fā)現(xiàn),include的布局中TextView等直接被加入到父布局中召川。
這個時候需要注意南缓,LayoutInflat 或者 其他相關(guān)RecyclerViewAdapter中 需要注意attach 中的true,false
使用ViewStub 標(biāo)簽
當(dāng)我們布局中存在一個View/ViewGroup荧呐,在某個特定時刻才需要他的展示時汉形,可能會有同學(xué)把這個元素在xml中 定義為invisible或者gone,在需要顯示時再設(shè)置為visible可見倍阐。比如在登陸時概疆,如果密碼錯誤在密碼輸入框上顯示 提示。
invisible view設(shè)置為invisible時峰搪,view在layout布局文件中會占用位置枫攀,但是view為不可見赖捌,該view還是會創(chuàng)建對
象属提,會被初始化西采,會占用資源。
gone view設(shè)置gone時鞠柄,view在layout布局文件中不占用位置童漩,但是該view還是會創(chuàng)建對象,會被初始化春锋,會占
用資源矫膨。
如果view不一定會顯示,此時可以使用 ViewStub 來包裹此View 以避免不需要顯示view但是又需要加載view消耗資 源期奔。
viewstub是一個輕量級的view侧馅,它不可見,不用占用資源呐萌,只有設(shè)置viewstub為visible或者調(diào)用其inflater()方法 時馁痴,其對應(yīng)的布局文件才會被初始化。
<? xml version = "1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
android:orientation="vertical">
<ViewStub
android:id="@+id/viewStub"
android:layout_width="600dp"
android:layout_height="500dp"
android:inflatedId="@+id/textView"
android:layout="@layout/layout_viewstub" />
</LinearLayout>
<!--layout_viewstub-->
<?xml version="1.0"encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ffffff"
android:text="測試viewStub" />
加載viewStub后肺孤,可以通過 inflatedId 找到layout_viewstub 中的根View罗晕。
過度渲染
過度繪制是指系統(tǒng)在渲染單個幀的過程中多次在屏幕上繪制某一個像素。例如赠堵,如果我們有若干界面卡片堆疊在一 起小渊,每張卡片都會遮蓋其下面一張卡片的部分內(nèi)容。但是茫叭,系統(tǒng)仍然需要繪制堆疊中的卡片被遮蓋的部分酬屉。
GPU 過度繪制檢查 手機開發(fā)者選項中能夠顯示過度渲染檢查功能,通過對界面進(jìn)行彩色編碼來幫我們識別過度繪制。開啟步驟如下:
- 進(jìn)入開發(fā)者選項 (Developer Options)呐萨。
- 找到調(diào)試 GPU 過度繪制(Debug GPU overdraw)杀饵。
- 在彈出的對話框中,選擇顯示過度繪制區(qū)域(Show overdraw areas)谬擦。
Android 將按如下方式為界面元素著色切距,以確定過度繪制的次數(shù):
真彩色:沒有過度繪制
藍(lán)色:過度繪制 1 次
綠色:過度繪制 2 次
粉色:過度繪制 3 次
紅色:過度繪制 4 次或更多次
請注意,這些顏色是半透明的惨远,因此您在屏幕上看到的確切顏色取決于界面內(nèi)容谜悟。
有些過度繪制是不可避免的。在優(yōu)化應(yīng)用的界面時锨络,應(yīng)嘗試達(dá)到大部分顯示真彩色或僅有 1 次過度繪制(藍(lán) 色)的視覺效果。
解決過度繪制問題
可以采取以下幾種策略來減少甚至消除過度繪制:
- 移除布局中不需要的背景狼牺。
默認(rèn)情況下羡儿,布局沒有背景,這表示布局本身不會直接渲染任何內(nèi)容是钥。但是掠归,當(dāng)布局具有背景時,其有 可能會導(dǎo)致過度繪制悄泥。
移除不必要的背景可以快速提高渲染性能虏冻。不必要的背景可能永遠(yuǎn)不可見,因為它會被應(yīng)用在該視圖上 繪制的任何其他內(nèi)容完全覆蓋弹囚。例如厨相,當(dāng)系統(tǒng)在父視圖上繪制子視圖時,可能會完全覆蓋父視圖的背 景鸥鹉。
-使視圖層次結(jié)構(gòu)扁平化蛮穿。
可以通過優(yōu)化視圖層次結(jié)構(gòu)來減少重疊界面對象的數(shù)量,從而提高性能毁渗。
- 降低透明度践磅。
對于不透明的 view ,只需要渲染一次即可把它顯示出來灸异。但是如果這個 view 設(shè)置了 alpha 值府适,則至 少需要渲染兩次。這是因為使用了 alpha 的 view 需要先知道混合 view 的下一層元素是什么肺樟,然后再 結(jié)合上層的 view 進(jìn)行Blend混色處理檐春。透明動畫、淡入淡出和陰影等效果都涉及到某種透明度么伯,這就會 造成了過度繪制喇聊。可以通過減少要渲染的透明對象的數(shù)量蹦狂,來改善這些情況下的過度繪制誓篱。例如朋贬,如需 獲得灰色文本,可以在 TextView 中繪制黑色文本窜骄,再為其設(shè)置半透明的透明度值锦募。但是,簡單地通過 用灰色繪制文本也能獲得同樣的效果邻遏,而且能夠大幅提升性能糠亩。
布局加載優(yōu)化
異步加載
LayoutInflater加載xml布局的過程會在主線程使用IO讀取XML布局文件進(jìn)行XML解析,再根據(jù)解析結(jié)果利用反射 創(chuàng)建布局中的View/ViewGroup對象准验。這個過程隨著布局的復(fù)雜度上升赎线,耗時自然也會隨之增大。Android為我們 提供了 Asynclayoutinflater 把耗時的加載操作在異步線程中完成糊饱,最后把加載結(jié)果再回調(diào)給主線程垂寥。
dependencies {
implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0"
}
new AsyncLayoutInflater(this)
.inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() { @Override
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
setContentView(view);
//......
} });
1、使用異步 inflate另锋,那么需要這個 layout 的 parent 的 generateLayoutParams 函數(shù)是線程安全的;
2滞项、所有構(gòu)建的 View 中必須不能創(chuàng)建 Handler 或者是調(diào)用 Looper.myLooper;(因為是在異步線程中加載的,異
步線程默認(rèn)沒有調(diào)用 Looper.prepare );
3夭坪、AsyncLayoutInflater 不支持設(shè)置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;
4文判、不支持加載包含 Fragment 的 layout
5、如果 AsyncLayoutInflater 失敗室梅,那么會自動回退到UI線程來加載布局
拓展--- 掌閱X2C思路 ---- github上