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

大多數(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:

2020-12-15 18.08.43.png

在我這里摊崭,我的架構(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)行彩色編碼來幫我們識別過度繪制。開啟步驟如下:

  1. 進(jìn)入開發(fā)者選項 (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)用的界面時锨络,應(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上

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載戏仓,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末亡鼠,一起剝皮案震驚了整個濱河市柜去,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拆宛,老刑警劉巖嗓奢,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異浑厚,居然都是意外死亡股耽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進(jìn)店門钳幅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來物蝙,“玉大人,你說我怎么就攤上這事敢艰∥芷颍” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長震嫉。 經(jīng)常有香客問我森瘪,道長,這世上最難降的妖魔是什么票堵? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任扼睬,我火速辦了婚禮,結(jié)果婚禮上悴势,老公的妹妹穿的比我還像新娘窗宇。我一直安慰自己,他們只是感情好特纤,可當(dāng)我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布军俊。 她就那樣靜靜地躺著,像睡著了一般捧存。 火紅的嫁衣襯著肌膚如雪粪躬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天矗蕊,我揣著相機與錄音短蜕,去河邊找鬼氢架。 笑死傻咖,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的岖研。 我是一名探鬼主播卿操,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼孙援!你這毒婦竟也來了害淤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拓售,失蹤者是張志新(化名)和其女友劉穎窥摄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體础淤,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡崭放,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了鸽凶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片币砂。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖玻侥,靈堂內(nèi)的尸體忽然破棺而出决摧,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布掌桩,位于F島的核電站边锁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拘鞋。R本人自食惡果不足惜砚蓬,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盆色。 院中可真熱鬧灰蛙,春花似錦、人聲如沸隔躲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宣旱。三九已至仅父,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浑吟,已是汗流浹背笙纤。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留组力,地道東北人省容。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像燎字,于是被迫代替她去往敵國和親腥椒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,884評論 2 354

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

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