卡頓分析與布局優(yōu)化

大多數(shù)用戶感知到的卡頓等性能問題的最主要根源都是因?yàn)殇秩拘阅堋ndroid系統(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)嗎?這 是因?yàn)槿搜叟c大腦之間的協(xié)作無法感知超過60fps的畫面更新。
12fps大概類似手動快速翻動書籍的幀率,這明顯是可以感知到不夠順滑的。24fps使得人眼感知的是連續(xù)線 性的運(yùn)動,這其實(shí)是歸功于運(yùn)動模糊的效果。24fps是電影膠圈通常使用的幀率,因?yàn)檫@個(gè)幀率已經(jīng)足夠支撐 大部分電影畫面需要表達(dá)的內(nèi)容,同時(shí)能夠最大的減少費(fèi)用支出。但是低于30fps是無法順暢表現(xiàn)絢麗的畫面 內(nèi)容的,此時(shí)就需要用到60fps來達(dá)到想要的效果,當(dāng)然超過60fps是沒有必要的。
開發(fā)app的性能目標(biāo)就是保持60fps,這意味著每一幀你只有16ms=1000/60的時(shí)間來處理所有的任務(wù)。

如果某個(gè)操作花費(fèi)時(shí)間是24ms,系統(tǒng)在得到VSYNC信號的時(shí)候就無法進(jìn)行正常渲染,這樣就發(fā)生了丟幀現(xiàn)象。那么用戶在32ms內(nèi)看到的會是同一幀畫面。

有很多原因可以導(dǎo)致丟幀, 一般主線程過多的UI繪制、大量的IO操作或是大量的計(jì)算操作占用CPU,都會導(dǎo)致App 界面卡頓。
一般主線程過多的UI繪制、大量的IO操作或是大量的計(jì)算操作占用CPU,導(dǎo)致App界面卡頓。

卡頓分析

Systrace

Systrace 是Android平臺提供的一款工具,用于記錄短期內(nèi)的設(shè)備活動。該工具會生成一份報(bào)告,其中匯總了 Android 內(nèi)核中的數(shù)據(jù),例如 CPU 調(diào)度程序、磁盤活動和應(yīng)用線程。Systrace主要用來分析繪制性能方面的問 題。在發(fā)生卡頓時(shí),通過這份報(bào)告可以知道當(dāng)前整個(gè)系統(tǒng)所處的狀態(tài),從而幫助開發(fā)者更直觀的分析系統(tǒng)瓶頸,改 進(jìn)性能。

TraceView可以看出代碼在運(yùn)行時(shí)的一些具體信息,方法調(diào)用時(shí)長,次數(shù),時(shí)間比率,了解代碼運(yùn)行過程的 效率問題,從而針對性改善代碼。所以對于可能導(dǎo)致卡頓的耗時(shí)方法也可以通過TraceView檢測。

要使用Systrace,需要先安裝 Python2.7。安裝完成后配置環(huán)境變量 path ,隨后在命令行輸入: python -- version 進(jìn)行驗(yàn)證。

參考

Trace API

由于profile配置時(shí)候可能會影響我們分析。所以我們用Trace API生成trace文件后導(dǎo)入進(jìn)來的方式會更準(zhǔn)確

在Application中 我們 可這樣設(shè)置

 Debug.startMethodTracingSampling(new File(Environment.getExternalStorageDirectory(),
                        "zcwfeng").getAbsolutePath(), 8 * 1024 * 1024, 1_000);
    

采樣的方式,有時(shí)候我們需要多次采樣,有可能會漏掉細(xì)節(jié),性能優(yōu)化是個(gè)非常細(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)用因?yàn)槭裁丛驅(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ī)制進(jìn)行UI更新,UI線程有個(gè)Looper,在其loop方法中會不斷取出message,調(diào)用其綁定的 Handler在UI線程執(zhí)行。如果在handler的dispatchMesaage方法里有耗時(shí)操作,就會發(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í)行時(shí)間,就能檢測到部分UI線程是否有耗時(shí)的操作。注意到這行 執(zhí)行代碼的前后,有兩個(gè)logging.println函數(shù),如果設(shè)置了logging,會分別打印出>>>>> Dispatching to和 <<<<< Finished to 這樣的日志,這樣我們就可以通過兩次log的時(shí)間差值,來計(jì)算dispatchMessage的執(zhí)行時(shí) 間,從而設(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) 方法,所以我們可以自己實(shí)現(xiàn)一個(gè)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í)行耗時(shí)超過閾值,輸出卡頓信息
        if (!mPrintingStarted) {
            //記錄開始時(shí)間
            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() {
                //獲得卡頓時(shí)主線程堆棧
                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);
            }
        }
    };

}

其實(shí)這種方式也就是 BlockCanary 原理。

做個(gè)實(shí)驗(yàn),在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)每一幀被渲染時(shí)會觸發(fā)回調(diào)
FrameCallback.doFrame (long frameTimeNanos)函數(shù)。frameTimeNanos是底層VSYNC信號到達(dá)的時(shí)間戳 。

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)時(shí)間
                    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í)時(shí)計(jì)算幀率和掉幀數(shù),實(shí)時(shí)監(jiān)測App頁面的幀率數(shù)據(jù),發(fā)現(xiàn)幀率過低,還可以自 動保存現(xiàn)場堆棧信息。

Looper比較適合在發(fā)布前進(jìn)行測試或者小范圍灰度測試然后定位問題,ChoreographerHelper適合監(jiān)控線上環(huán)境 的 app 的掉幀情況來計(jì)算 app 在某些場景的流暢度然后有針對性的做性能優(yōu)化。

布局優(yōu)化

層級優(yōu)化

measure、layout、draw這三個(gè)過程都包含自頂向下的View Tree遍歷耗時(shí),如果視圖層級太深自然需要更多的時(shí) 間來完成整個(gè)繪測過程,從而造成啟動速度慢、卡頓等問題。而onDraw在頻繁刷新時(shí)可能多次出發(fā),因此 onDraw更不能做耗時(shí)操作,同時(shí)需要注意內(nèi)存抖動。對于布局性能的檢測,依然可以使用systrace與traceview按 照繪制流程檢查繪制耗時(shí)函數(shù)。

Layout Inspector

然后選擇需要查看的進(jìn)程與Activity:

2020-12-15 18.08.43.png

在我這里,我的架構(gòu)demo工程,里面兩個(gè)自定義View,PictureTitleView 和 TitleView 都已經(jīng)集成LinearLayout,但是布局里面有嵌套了LineLayout,屬于寫的時(shí)候沒注意,對于這種就可以優(yōu)化掉xml布局,去掉多余Linelayout。可以利用merge標(biāo)簽

使用merge標(biāo)簽

  • 當(dāng)我們有一些布局元素需要被多處使用時(shí),這時(shí)候我們會考慮將其抽取成一個(gè)單獨(dú)的布局文件。在需要使用的地方 通過 include 加載。

  • 用的時(shí)候我們沒必要寫多個(gè)嵌套,這個(gè)時(shí)候可以用merge。
    修改為merge后,通過LayoutInspector能夠發(fā)現(xiàn),include的布局中TextView等直接被加入到父布局中。
    這個(gè)時(shí)候需要注意,LayoutInflat 或者 其他相關(guān)RecyclerViewAdapter中 需要注意attach 中的true,false

使用ViewStub 標(biāo)簽

當(dāng)我們布局中存在一個(gè)View/ViewGroup,在某個(gè)特定時(shí)刻才需要他的展示時(shí),可能會有同學(xué)把這個(gè)元素在xml中 定義為invisible或者gone,在需要顯示時(shí)再設(shè)置為visible可見。比如在登陸時(shí),如果密碼錯誤在密碼輸入框上顯示 提示。

invisible view設(shè)置為invisible時(shí),view在layout布局文件中會占用位置,但是view為不可見,該view還是會創(chuàng)建對
象,會被初始化,會占用資源。
gone view設(shè)置gone時(shí),view在layout布局文件中不占用位置,但是該view還是會創(chuàng)建對象,會被初始化,會占
用資源。

如果view不一定會顯示,此時(shí)可以使用 ViewStub 來包裹此View 以避免不需要顯示view但是又需要加載view消耗資 源。
viewstub是一個(gè)輕量級的view,它不可見,不用占用資源,只有設(shè)置viewstub為visible或者調(diào)用其inflater()方法 時(shí),其對應(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)在渲染單個(gè)幀的過程中多次在屏幕上繪制某一個(gè)像素。例如,如果我們有若干界面卡片堆疊在一 起,每張卡片都會遮蓋其下面一張卡片的部分內(nèi)容。但是,系統(tǒng)仍然需要繪制堆疊中的卡片被遮蓋的部分。

GPU 過度繪制檢查 手機(jī)開發(fā)者選項(xiàng)中能夠顯示過度渲染檢查功能,通過對界面進(jìn)行彩色編碼來幫我們識別過度繪制。開啟步驟如下:

  1. 進(jìn)入開發(fā)者選項(xiàng) (Developer Options)。
  2. 找到調(diào)試 GPU 過度繪制(Debug GPU overdraw)。
  3. 在彈出的對話框中,選擇顯示過度繪制區(qū)域(Show overdraw areas)。

Android 將按如下方式為界面元素著色,以確定過度繪制的次數(shù):

真彩色:沒有過度繪制

藍(lán)色:過度繪制 1 次

綠色:過度繪制 2 次

粉色:過度繪制 3 次

紅色:過度繪制 4 次或更多次

請注意,這些顏色是半透明的,因此您在屏幕上看到的確切顏色取決于界面內(nèi)容。
有些過度繪制是不可避免的。在優(yōu)化應(yīng)用的界面時(shí),應(yīng)嘗試達(dá)到大部分顯示真彩色或僅有 1 次過度繪制(藍(lán) 色)的視覺效果。

解決過度繪制問題

可以采取以下幾種策略來減少甚至消除過度繪制:

  • 移除布局中不需要的背景。
    默認(rèn)情況下,布局沒有背景,這表示布局本身不會直接渲染任何內(nèi)容。但是,當(dāng)布局具有背景時(shí),其有 可能會導(dǎo)致過度繪制。
    移除不必要的背景可以快速提高渲染性能。不必要的背景可能永遠(yuǎn)不可見,因?yàn)樗鼤粦?yīng)用在該視圖上 繪制的任何其他內(nèi)容完全覆蓋。例如,當(dāng)系統(tǒng)在父視圖上繪制子視圖時(shí),可能會完全覆蓋父視圖的背 景。
    -使視圖層次結(jié)構(gòu)扁平化。
    可以通過優(yōu)化視圖層次結(jié)構(gòu)來減少重疊界面對象的數(shù)量,從而提高性能。
  • 降低透明度。

對于不透明的 view ,只需要渲染一次即可把它顯示出來。但是如果這個(gè) view 設(shè)置了 alpha 值,則至 少需要渲染兩次。這是因?yàn)槭褂昧?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對象。這個(gè)過程隨著布局的復(fù)雜度上升,耗時(shí)自然也會隨之增大。Android為我們 提供了 Asynclayoutinflater 把耗時(shí)的加載操作在異步線程中完成,最后把加載結(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,那么需要這個(gè) layout 的 parent 的 generateLayoutParams 函數(shù)是線程安全的;
2、所有構(gòu)建的 View 中必須不能創(chuàng)建 Handler 或者是調(diào)用 Looper.myLooper;(因?yàn)槭窃诋惒骄€程中加載的,異
步線程默認(rèn)沒有調(diào)用 Looper.prepare );
3、AsyncLayoutInflater 不支持設(shè)置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;
4、不支持加載包含 Fragment 的 layout
5、如果 AsyncLayoutInflater 失敗,那么會自動回退到UI線程來加載布局

拓展--- 掌閱X2C思路 ---- github上

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,093評論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內(nèi)容

  • UI渲染基礎(chǔ) 1、屏幕與適配 通過dp和自適應(yīng)布局可以基本解決屏幕碎片化的問題,這也是Android推薦使用的屏幕...
    修塔尋千里閱讀 1,932評論 0 0
  • 在Android應(yīng)用優(yōu)化方面,主要從以下4個(gè)方面進(jìn)行優(yōu)化: 穩(wěn)定(內(nèi)存溢出、崩潰) 流暢(卡頓) 耗損(耗電、流量...
    幻影_2481閱讀 259評論 0 0
  • 1.了解渲染刷新機(jī)制 VSYNC(垂直刷新/繪制) 60HZ是屏幕刷新理想的頻率。60fps---一秒內(nèi)繪制的幀數(shù)...
    賈里閱讀 520評論 0 0
  • 漸變的面目拼圖要我怎么拼? 我是疲乏了還是投降了? 不是不允許自己墜落, 我沒有滴水不進(jìn)的保護(hù)膜。 就是害怕變得面...
    悶熱當(dāng)乘涼閱讀 4,296評論 0 13
  • 夜鶯2517閱讀 127,748評論 1 9