Android高手筆記-屏幕適配 & UI優(yōu)化

Android高手筆記-屏幕適配 & UI優(yōu)化

屏幕與適配

  • 由于Android碎片化嚴重摇邦,屏幕分辨率千奇百怪巩检,而想要在各種分辨率的設(shè)備上顯示基本一致的效果术幔,適配成本越來越高元咙;
  • 屏幕適配究其根本只有兩個問題:
    1. 在不同尺寸及分辨率上UI的一致(影響著用戶體驗)梯影;
    2. 從效果圖到UI界面代碼的轉(zhuǎn)化效率(影響著開發(fā)效率);

適配方式:

px

  • 在標識尺寸時庶香,Android官方并不推薦使用px(像素)甲棍,因為不同分辨率的屏幕上,同樣像素大小的控件赶掖,在分辨率越高的手機上UI顯示效果越懈忻汀;(因為分辨率越高單位尺寸內(nèi)容納的像素數(shù)越多)

dp

  • 所以官方推薦使用dp作為尺寸單位來適配UI奢赂,dp在不同分辨率和尺寸的手機上代表了不同的真實像素陪白;(px和dp的轉(zhuǎn)化關(guān)系:px=dp*(dpi/160),其中dpi是像素密度,是系統(tǒng)軟件上指定的單位尺寸的像素數(shù)量膳灶,往往是寫在系統(tǒng)出廠配置文件的一個固定值咱士,這和屏幕硬件的ppi(物理像素密度)是不同的,是參考了物理像素密度后轧钓,人為指定的一個值序厉,保證了某個區(qū)間內(nèi)的物理像素密度在軟件上都使用同一個值,有利于UI適配的簡化)這就是最原始的Android適配方案:dp+自適應(yīng)布局和weight比例布局毕箍,基本可以解決不同手機上的適配問題弛房;
  • 但是這種方案有兩個缺陷:
    1. 只能適配大部分手機(也做不到和效果圖的完全一致),某些特殊機型仍需單獨適配(比如同樣1920*1080的手機的dpi卻可能不同)而柑;
    2. 設(shè)計稿到布局代碼的實現(xiàn)效率低(設(shè)計稿的寬高和手機屏幕的寬高不同文捶,px和dp間的轉(zhuǎn)換,往往需要百分比或估算等媒咳,會極大地拉低開發(fā)效率)拄轻;

寬高限定符適配

  • 窮舉市面上所有的Android手機的寬高像素值,設(shè)定一個基準的分辨率(最好和設(shè)計稿的寬高一致)伟葫,其他分辨率根據(jù)這個基準分辨率來計算,生成對應(yīng)的dimens文件(可通過java院促、python腳本實現(xiàn)自動生成)筏养,放在不同的尺寸文件夾(values)內(nèi)部斧抱;(插圖)如480320為基準,對于800480的dimens文件:x1=(480/320)1=1.5px; x2=(480/320)2=3px;但是這種方案也有個致命缺陷渐溶,需要精準命中才能適配辉浦,如1440*750的手機如果找不到對應(yīng)的尺寸文件夾就只能用統(tǒng)一默認的dimens文件,UI就可能變形茎辐,而Android手機廠商眾多宪郊,機型更是不可枚舉,所以容錯機制很差拖陆;

鴻洋的AndroidAutoLayout適配方案等動態(tài)計算UI適配框架

  • 鴻洋的適配方案也來自于寬高限定符方案的啟發(fā)(目前已經(jīng)停止維護)弛槐;因為框架要在運行時會在onMeasure里面做變換,我們自定義的控件可能會被影響或限制依啰,可能有些特定的控件乎串,需要單獨適配,這里面可能存在的暗坑是不可預見的速警;整個適配工作是有框架完成的叹誉,而不是系統(tǒng)完成的,一旦使用這個框架闷旧,未來一旦遇到很難解決的問題长豁,替換起來是非常麻煩的,而且項目一旦停止維護忙灼,后續(xù)的升級就只能靠你自己了匠襟;

smallestWidth適配 或者叫sw限定符適配

  • 指的是Android會識別屏幕可用高度和寬度的較小者的dp值(其實就是手機的寬度值),然后根據(jù)識別到的結(jié)果去資源文件中尋找對應(yīng)限定符的文件夾下的資源文件缀棍。這種機制和上文提到的寬高限定符適配原理上是一樣的宅此,都是系統(tǒng)通過特定的規(guī)則來選擇對應(yīng)的文件舉個例子,小米5的dpi是480,橫向像素是1080px爬范,根據(jù)px=dp(dpi/160)父腕,橫向的dp值是1080/(480/160),也就是360dp,系統(tǒng)就會去尋找是否存在value-sw360dp的文件夾以及對應(yīng)的資源文件;(插圖)smallestWidth限定符適配和寬高限定符適配最大的區(qū)別在于青瀑,有很好的容錯機制璧亮,如果沒有value-sw360dp文件夾,系統(tǒng)會向下尋找斥难,比如離360dp最近的只有value-sw350dp枝嘶,
    那么Android就會選擇value-sw350dp文件夾下面的資源文件,這個特性就完美的解決了
    上文提到的寬高限定符的容錯問題哑诊。(通過java群扶、python腳本實現(xiàn)自動生成dimens文件)(插圖)
    (這種方案的優(yōu)勢是穩(wěn)定性,不會有暗坑)
  • smallestWidth適配方案有一個小問題,那就是它是在Android 3.2 以后引入的竞阐,Google的本意是用它來適配平板的布局文件(但是實際上顯然用于diemns適配的效果更好)所以這種方案支持的最小版本就是Android3.2了缴饭;
  • 還有一個缺陷就是多個dimens文件可能導致apk變大,根據(jù)生成的dimens文件的覆蓋范圍和尺寸范圍骆莹,apk可能會增大300kb-800kb左右颗搂;
  • 糗百的拉丁吳大佬生成好的文件https://github.com/ladingwu/dimens_sw
  • 所有的適配方案都不是用來取代match_parent,wrap_content的,而是用來完善他們的幕垦;

今日頭條適配方案

  • 通過修改density(density = dpi / 160)值丢氢,強行把所有不同尺寸分辨率的手機的寬度dp值改成一個統(tǒng)一的值,這樣就解決了所有的適配問題這個方案侵入性很低先改,而且也沒有涉及私有API疚察,只是對老項目是不太友好;

  • 如果我們想在所有設(shè)備上顯示完全一致盏道,其實是不現(xiàn)實的稍浆,因為屏幕高寬比不是固定的撰茎,16:9称勋、4:3甚至其他寬高比層出不窮流礁,寬高比不同轻绞,顯示完全一致就不可能了潭枣。但是通常下悼嫉,我們只需要以寬(支持上下滑動的頁面)或高(不支持上下滑動的頁面)一個維度去適配

  • 通過閱讀源碼文虏,我們可以得知节视,density 是 DisplayMetrics 中的成員變量论皆,而 DisplayMetrics 實例通過 Resources#getDisplayMetrics 可以獲得益楼,而Resouces通過Activity或者Application的Context獲得;DisplayMetrics 中和適配相關(guān)的幾個變量:

    1. DisplayMetrics.density 就是上述的density;
    2. DisplayMetrics.densityDpi 就是上述的dpi;
    3. DisplayMetrics#scaledDensity 字體的縮放因子点晴,正常情況下和density相等感凤,但是調(diào)節(jié)系統(tǒng)字體大小后會改變這個值;
  • 布局文件中dp的轉(zhuǎn)換,最終都是調(diào)用TypedValue#applyDimension(int unit, float value,DisplayMetrics metrics) (插圖)來進行轉(zhuǎn)換,方法中用到的DisplayMetrics正是從Resources中獲得的粒督;再看看圖片的decode陪竿,BitmapFactory#decodeResourceStream方法(插圖),也是通過 DisplayMetrics 中的值來計算的屠橄;因此族跛,想要滿足上述需求,我們只需要修改 DisplayMetrics 中和 dp 轉(zhuǎn)換相關(guān)的變量即可锐墙;所以得到了下面適配方案:假設(shè)設(shè)計圖寬度是360dp礁哄,以寬維度來適配,那么適配后的 density = 設(shè)備真實寬(單位px) / 360溪北,接下來只需要把我們計算好的 density 在系統(tǒng)中修改下即可桐绒,同時在 Activity#onCreate 方法中調(diào)用下但是會有字體過小的現(xiàn)象夺脾,原因是在上面的適配中,我們忽略了DisplayMetrics#scaledDensity的特殊性掏膏,將DisplayMetrics#scaledDensity和DisplayMetrics#density設(shè)置為同樣的值劳翰,從而某些用戶在系統(tǒng)中修改了字體大小失效了,但是我們還不能直接用原始的scaledDensity馒疹,直接用的話可能導致某些文字超過顯示區(qū)域,因此我們可以通過計算之前scaledDensity和density的比獲得現(xiàn)在的scaledDensity乙墙;但是測試后發(fā)現(xiàn)另外一個問題颖变,就是如果在系統(tǒng)設(shè)置中切換字體,再返回應(yīng)用听想,字體并沒有變化腥刹。于是還得監(jiān)聽下字體切換,調(diào)用 Application#registerComponentCallbacks 注冊下onConfigurationChanged 監(jiān)聽即可汉买;

  • 可以參考https://github.com/Blankj/AndroidUtilCode衔峰,https://github.com/JessYanCoding/AndroidAutoSize這兩個開源庫;

UI優(yōu)化

CPU 與 GPU

  • Android的繪制實現(xiàn)主要是借助CPU與GPU結(jié)合刷新機制共同完成的蛙粘。
  • 除了屏幕垫卤,UI 渲染還依賴兩個核心的硬件:CPU 與 GPU。
  • UI 組件在繪制到屏幕之前出牧,都需要經(jīng)過 Rasterization(柵格化)操作穴肘,而柵格化操作又是一個非常耗時的操作。GPU(Graphic Processing Unit )也就是圖形處理器舔痕,它主要用于處理圖形運算评抚,可以幫助我們加快柵格化操作。
  • CPU軟件繪制使用的是 Skia 庫伯复,它是一款能在低端設(shè)備如手機上呈現(xiàn)高質(zhì)量的 2D 跨平臺圖形框架慨代,類似 Chrome、Flutter 內(nèi)部使用的都是 Skia 庫;

OpenGL 與 Vulkan

  • 對于硬件繪制啸如,我們通過調(diào)用 OpenGL ES 接口利用 GPU 完成繪制侍匙。OpenGL是一個跨平臺的圖形 API,它為 2D/3D 圖形處理硬件指定了標準軟件接口组底。而 OpenGL ES 是 OpenGL 的子集丈积,專為嵌入式設(shè)備設(shè)計。
  • Android 7.0 把 OpenGL ES 升級到最新的 3.2 版本同時债鸡,還添加了對Vulkan的支持江滨。Vulkan 是用于高性能 3D 圖形的低開銷、跨平臺 API厌均。相比 OpenGL ES唬滑,Vulkan 在改善功耗、多核優(yōu)化提升繪圖調(diào)用上有著非常明顯的優(yōu)勢。
  • 把應(yīng)用程序圖形渲染過程當作一次繪畫過程晶密,那么繪畫過程中 Android 的各個圖形組件的作用是:
1. 畫筆:Skia 或者 OpenGL擒悬。我們可以用 Skia 畫筆繪制 2D 圖形,也可以用 OpenGL 來繪制 2D/3D 圖形稻艰。正如前面所說懂牧,前者使用 CPU 繪制,后者使用 GPU 繪制尊勿。
2. 畫紙:Surface僧凤。所有的元素都在 Surface 這張畫紙上進行繪制和渲染。在 Android 中元扔,Window 是 View 的容器躯保,每個窗口都會關(guān)聯(lián)一個 Surface。
而 WindowManager 則負責管理這些窗口澎语,并且把它們的數(shù)據(jù)傳遞給 SurfaceFlinger途事。
3. 畫板:Graphic Buffer。Graphic Buffer 緩沖用于應(yīng)用程序圖形的繪制擅羞,在 Android 4.1 之前使用的是雙緩沖機制尸变;在 Android 4.1 之后,使用的是三緩沖機制祟滴。
4. 顯示:SurfaceFlinger振惰。它將 WindowManager 提供的所有 Surface,通過硬件合成器 Hardware Composer 合成并輸出到顯示屏垄懂。

Android 渲染的演進

  1. 在 Android 3.0 之前骑晶,或者沒有啟用硬件加速時,系統(tǒng)都會使用軟件方式來渲染 UI;
  2. Androd 3.0 開始草慧,Android 開始支持硬件加速;
  3. Android 4.0 時桶蛔,默認開啟硬件加速;
  4. Android 4.1:
    1. 開啟了Project Butter: 主要包含兩個組成部分,一個是 VSYNC漫谷,一個是 Triple Buffering仔雷。VSYNC信號:對于 Android 4.0,CPU 可能會因為在忙別的事情舔示,導致沒來得及處理 UI 繪制碟婆。 為解決這個問題,Project Buffer 引入了VSYNC惕稻,它類似于時鐘中斷。每收到 VSYNC 中斷俺祠,CPU 會立即準備 Buffer 數(shù)據(jù)公给,由于大部分顯示設(shè)備刷新頻率都是 60Hz(一秒刷新 60 次)借帘,也就是說一幀數(shù)據(jù)的準備工作都要在 16ms 內(nèi)完成。三緩沖機制 Triple Buffering:Android 4.1 之前淌铐,Android 使用雙緩沖機制肺然,CPU、GPU 和顯示設(shè)備都能使用各自的緩沖區(qū)工作腿准,互不影響
    2. Android 4.1還新增了 Systrace 性能數(shù)據(jù)采樣和分析工具际起。
    3. Tracer for OpenGL ES 也是 Android 4.1 新增加的工具,它可逐幀释涛、逐函數(shù)的記錄 App 用 OpenGL ES 的繪制過程加叁。它提供了每個 OpenGL 函數(shù)調(diào)用的消耗時間,所以很多時候用來做性能分析唇撬。但因為其強大的記錄功能,在分析渲染問題時展融,當 Traceview窖认、Systrace 都顯得棘手時,還找不到渲染問題所在時告希,此時這個工具就會派上用場了扑浸。
  5. Android 4.2,系統(tǒng)增加了檢測繪制過度工具燕偶;
  6. Android 5.0:RenderThread:
    • 經(jīng)過 Project Butter 黃油計劃之后喝噪,Android 的渲染性能有了很大的改善。但是不知道你有沒有注意到一個問題指么,雖然我們利用了 GPU 的圖形高性能運算酝惧,但是從計算 DisplayList,到通過 GPU 繪制到 Frame Buffer伯诬,整個計算和繪制都在 UI 主線程中完成晚唇。
    • Android 5.0 引入了兩個比較大的改變。一個是引入了 RenderNode 的概念盗似,它對 DisplayList 及一些 View 顯示屬性做了進一步封裝哩陕。另一個是引入了 RenderThread,所有的 GL 命令執(zhí)行都放到這個線程上赫舒,渲染線程在 RenderNode 中存有渲染幀的所有信息悍及,可以做一些屬性動畫,這樣即便主線程有耗時操作的時候也可以保證動畫流暢接癌。
    • 還可以開啟 Profile GPU Rendering 檢查心赶。
  7. Android 6.0 ,在 gxinfo 添加了更詳細的信息扔涧;
  8. 在 Android 7.0 又對 HWUI 進行了一些重構(gòu)园担,而且支持了 Vulkan届谈;
  9. 在 Android P 支持了 Vulkun 1.1。

UI 渲染測量

  • 測試工具:Profile GPU Rendering 和 Show GPU Overdraw弯汰。
  • 問題定位工具:Systrace 和 Tracer for OpenGL ES
  • Layout Inspector: AndroidStudio自帶的工具艰山,它的主要作用就是用來查看視圖層級結(jié)構(gòu)的,開啟路徑如下: 點擊Tools工具欄 ->第三欄的Layout Inspector -> 選中當前的進程咏闪;
  • Choreographer:用來獲取FPS的曙搬,并且可以用于線上使用,具備實時性鸽嫂,但是僅能在Api 16之后使用纵装,具體的調(diào)用代碼如下:
    • Choreographer.getInstance().postFrameCallback();
    • 使用Choreographer獲取FPS的完整代碼如下
    private long mStartFrameTime = 0;
    private int mFrameCount = 0;
    
    /**
     * 單次計算FPS使用160毫秒
     */
    private static final long MONITOR_INTERVAL = 160L;
    private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
    
    /**
     * 設(shè)置計算fps的單位時間間隔1000ms,即fps/s
     */
    private static final long MAX_INTERVAL = 1000L;
    
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void getFPS() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
            return;
        }
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                if (mStartFrameTime == 0) {
                    mStartFrameTime = frameTimeNanos;
                }
                long interval = frameTimeNanos - mStartFrameTime;
                if (interval > MONITOR_INTERVAL_NANOS) {
                    double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;
                    // log輸出fps
                    LogUtils.i("當前實時fps值為: " + fps);
                    mFrameCount = 0;
                    mStartFrameTime = 0;
                } else {
                    ++mFrameCount;
                }
    
                Choreographer.getInstance().postFrameCallback(this);
            }
        });
    }
    
    • 我們需要排除掉頁面沒有操作的情況,即只在界面存在繪制的時候才做統(tǒng)計据某。我們可以通過 addOnDrawListener 去監(jiān)聽界面是否存在繪制行為: getWindow().getDecorView().getViewTreeObserver().addOnDrawListener
  • 在 Android Studio 3.1 之后橡娄,Android 推薦使用Graphics API Debugger(GAPID)來替代 Tracer for OpenGL ES 工具。GAPID 可以說是升級版癣籽,它不僅可以跨平臺挽唉,而且功能更加強大称勋,支持 Vulkan 與回放零院。
  • 通過上面的幾個工具,我們可以初步判斷應(yīng)用 UI 渲染的性能是否達標崖媚,例如是否經(jīng)常出現(xiàn)掉幀埂材、掉幀主要發(fā)生在渲染的哪一個階段塑顺、是否存在 Overdraw 等。
  • 雖然這些圖形化界面工具非常好用俏险,但是它們難以用在自動化測試場景中严拒,那有哪些測量方法可以用于自動化測量 UI 渲染性能呢?

1. gfxinfo

  • gfxinfo可以輸出包含各階段發(fā)生的動畫以及幀相關(guān)的性能信息寡喝,具體命令如下:
    • adb shell dumpsys gfxinfo 包名
  • 除了渲染的性能之外糙俗,gfxinfo 還可以拿到渲染相關(guān)的內(nèi)存和 View hierarchy 信息。在 Android 6.0 之后预鬓,gxfinfo 命令新增了 framestats 參數(shù)巧骚,可以拿到最近 120 幀每個繪制階段的耗時信息: adb shell dumpsys gfxinfo 包名 framestats

2. SurfaceFlinger

  • 除了耗時,我們還比較關(guān)心渲染使用的內(nèi)存格二∨耄可以通過下面的命令拿到系統(tǒng) SurfaceFlinger 相關(guān)的信息:adb shell dumpsys SurfaceFlinger

獲取界面布局耗時

  1. AOP
@Around("execution(* android.app.Activity.setContentView(..))")
public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
    Signature signature = joinPoint.getSignature();
    String name = signature.toShortString();
    long time = System.currentTimeMillis();
    try {
        joinPoint.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
}
  1. LayoutInflaterCompat.setFactory2
  • 上面我們使用了AOP的方式監(jiān)控了Activity的布局加載耗時,那么顶猜,如果我們需要監(jiān)控每一個控件的加載耗時沧奴,該怎么實現(xiàn)呢?
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {

    // 使用LayoutInflaterCompat.Factory2全局監(jiān)控Activity界面每一個控件的加載耗時长窄,
    // 也可以做全局的自定義控件替換處理滔吠,比如:將TextView全局替換為自定義的TextView纲菌。
    LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

            if (TextUtils.equals(name, "TextView")) {
                // 生成自定義TextView
            }
            long time = System.currentTimeMillis();
            // 1
            View view = getDelegate().createView(parent, name, context, attrs);
            LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
            return view;
        }

        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
    });

    // 2、setFactory2方法需在super.onCreate方法前調(diào)用疮绷,否則無效
    super.onCreate(savedInstanceState);
    setContentView(getLayoutId());
    unBinder = ButterKnife.bind(this);
    mActivity = this;
    ActivityCollector.getInstance().addActivity(this);
    onViewCreated();
    initToolbar();
    initEventAndData();
}

UI優(yōu)化的常用手段

1. 盡量使用硬件加速

  • 之所以不能使用硬件加速翰舌,是因為硬件加速不能支持所有的 Canvas API;
  • 如果使用了不支持的 API冬骚,系統(tǒng)就需要通過 CPU 軟件模擬繪制椅贱,這也是漸變、磨砂只冻、圓角等效果渲染性能比較低的原因庇麦。
  • SVG 也是一個非常典型的例子,SVG 有很多指令硬件加速都不支持喜德。
  • 但我們可以用一個取巧的方法山橄,提前將這些 SVG 轉(zhuǎn)換成 Bitmap 緩存起來,這樣系統(tǒng)就可以更好地使用硬件加速繪制舍悯。
  • 同理驾胆,對于其他圓角、漸變等場景贱呐,我們也可以改為 Bitmap 實現(xiàn)。

2. Create View 優(yōu)化

  • View 的創(chuàng)建也是在 UI 線程里入桂,對于一些非常復雜的界面奄薇,這部分的耗時不容忽視。包括各種 XML 的隨機讀的 I/O 時間抗愁、解析 XML 的時間馁蒂、生成對象的時間(Framework 會大量使用到反射)。
  • 優(yōu)化方式:
    • 使用代碼創(chuàng)建;
    Button button=new Button(this);
    button.setBackgroundColor(Color.RED);
    button.setText("Hello World");
    ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(this).inflate(R.layout.activity_main, null);
    viewGroup.addView(button);
    
    • 異步創(chuàng)建:那我們能不能在線程提前創(chuàng)建 View蜘腌,實現(xiàn) UI 的預加載嗎沫屡?可以通過又一個非常取巧的方式來實現(xiàn)。在使用線程創(chuàng)建 UI 的時候撮珠,先把線程的 Looper 的 MessageQueue 替換成 UI 線程 Looper 的 Queue沮脖。
    public static boolean prepareLooperWithMainThreadQueue(boolean reset) {
        if (isMainThread()) {
            return true;
        } else {
            ThreadLocal<Looper> threadLocal = (ThreadLocal) ReflectionHelper.getStaticFieldValue(Looper.class, "sThreadLocal");
            if (threadLocal == null) {
                return false;
            } else {
                Looper looper = null;
                if (!reset) {
                    Looper.prepare();
                    looper = Looper.myLooper();
                    Object queue = ReflectionHelper.invokeMethod(Looper.getMainLooper(), "getQUeue", new Class[0], new Object[0]);
                    if (!(queue instanceof MessageQueue)) {
                        return false;
                    }
                }
                ReflectionHelper.invokeMethod(threadLocal, "set", new Class[]{Object.class}, new Object[]{looper});
                return true;
            }
        }
    }
    // 要注意的是,在創(chuàng)建完 View 后我們需要把線程的 Looper 恢復成原來的芯急。
    
    private static boolean isMainThread() {
        return Looper.myLooper() == Looper.getMainLooper();
    }
    
    • View 重用:ListView勺届、RecycleView 通過 View 的緩存與重用大大地提升渲染性能。因此我們可以參考它們的思想娶耍,實現(xiàn)一套可以在不同 Activity 或者 Fragment 使用的 View 緩存機制免姿。
    • AsynclayoutInflater異步創(chuàng)建View
    implementation 'com.android.support:asynclayoutinflater:28.0.0'
    // 內(nèi)部分別使用了IO和反射的方式去加載布局解析器和創(chuàng)建對應(yīng)的View
    // setContentView(R.layout.activity_main);
    // 使用AsyncLayoutInflater進行布局的加載
    new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
            @Override
            public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
                setContentView(view);
                // findViewById、視圖操作等
        }
    });
    super.onCreate(savedInstanceState);
    
    • AsyncLayoutInflater是通過側(cè)面緩解的方式去緩解布局加載過程中的卡頓榕酒,但是它依然存在一些問題:
      1. 不能設(shè)置LayoutInflater.Factory胚膊,需要通過自定義AsyncLayoutInflater的方式解決故俐,由于它是一個final,所以需要將代碼直接拷處進行修改紊婉。
      2. 因為是異步加載药版,所以需要注意在布局加載過程中不能有依賴于主線程的操作。
    • Android AsyncLayoutInflater 限制及改進:
      /**
       * 實現(xiàn)異步加載布局的功能肩榕,修改點:
       *
       * 1. super.onCreate之前調(diào)用沒有了默認的Factory刚陡;
       * 2. 排隊過多的優(yōu)化;
       */
      public class AsyncLayoutInflaterPlus {
      
          private static final String TAG = "AsyncLayoutInflaterPlus";
          private Handler mHandler;
          private LayoutInflater mInflater;
          private InflateRunnable mInflateRunnable;
          // 真正執(zhí)行加載任務(wù)的線程池
          private static ExecutorService sExecutor = Executors.newFixedThreadPool(Math.max(2,
                  Runtime.getRuntime().availableProcessors() - 2));
          // InflateRequest pool
          private static Pools.SynchronizedPool<AsyncLayoutInflaterPlus.InflateRequest> sRequestPool = new Pools.SynchronizedPool<>(10);
          private Future<?> future;
      
          public AsyncLayoutInflaterPlus(@NonNull Context context) {
              mInflater = new AsyncLayoutInflaterPlus.BasicInflater(context);
              mHandler = new Handler(mHandlerCallback);
          }
      
          @UiThread
          public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent, @NonNull CountDownLatch countDownLatch,
                              @NonNull AsyncLayoutInflaterPlus.OnInflateFinishedListener callback) {
              if (callback == null) {
                  throw new NullPointerException("callback argument may not be null!");
              }
              AsyncLayoutInflaterPlus.InflateRequest request = obtainRequest();
              request.inflater = this;
              request.resid = resid;
              request.parent = parent;
              request.callback = callback;
              request.countDownLatch = countDownLatch;
              mInflateRunnable = new InflateRunnable(request);
              future = sExecutor.submit(mInflateRunnable);
          }
      
          public void cancel() {
              future.cancel(true);
          }
      
          /**
           * 判斷這個任務(wù)是否已經(jīng)開始執(zhí)行
           *
           * @return
           */
          public boolean isRunning() {
              return mInflateRunnable.isRunning();
          }
      
          private Handler.Callback mHandlerCallback = new Handler.Callback() {
              @Override
              public boolean handleMessage(Message msg) {
                  AsyncLayoutInflaterPlus.InflateRequest request = (AsyncLayoutInflaterPlus.InflateRequest) msg.obj;
                  if (request.view == null) {
                      request.view = mInflater.inflate(
                              request.resid, request.parent, false);
                  }
                  request.callback.onInflateFinished(
                          request.view, request.resid, request.parent);
                  request.countDownLatch.countDown();
                  releaseRequest(request);
                  return true;
              }
          };
      
          public interface OnInflateFinishedListener {
              void onInflateFinished(View view, int resid, ViewGroup parent);
          }
      
          private class InflateRunnable implements Runnable {
              private InflateRequest request;
              private boolean isRunning;
      
              public InflateRunnable(InflateRequest request) {
                  this.request = request;
              }
      
              @Override
              public void run() {
                  isRunning = true;
                  try {
                      request.view = request.inflater.mInflater.inflate(
                              request.resid, request.parent, false);
                  } catch (RuntimeException ex) {
                      // Probably a Looper failure, retry on the UI thread
                      Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
                              + " thread", ex);
                  }
                  Message.obtain(request.inflater.mHandler, 0, request)
                          .sendToTarget();
              }
      
              public boolean isRunning() {
                  return isRunning;
              }
          }
      
          private static class InflateRequest {
              AsyncLayoutInflaterPlus inflater;
              ViewGroup parent;
              int resid;
              View view;
              AsyncLayoutInflaterPlus.OnInflateFinishedListener callback;
              CountDownLatch countDownLatch;
      
              InflateRequest() {
              }
          }
      
          private static class BasicInflater extends LayoutInflater {
              private static final String[] sClassPrefixList = {
                      "android.widget.",
                      "android.webkit.",
                      "android.app."
              };
      
              BasicInflater(Context context) {
                  super(context);
                  if (context instanceof AppCompatActivity) {
                      // 加上這些可以保證AppCompatActivity的情況下株汉,super.onCreate之前
                      // 使用AsyncLayoutInflater加載的布局也擁有默認的效果
                      AppCompatDelegate appCompatDelegate = ((AppCompatActivity) context).getDelegate();
                      if (appCompatDelegate instanceof LayoutInflater.Factory2) {
                          LayoutInflaterCompat.setFactory2(this, (LayoutInflater.Factory2) appCompatDelegate);
                      }
                  }
              }
      
              @Override
              public LayoutInflater cloneInContext(Context newContext) {
                  return new AsyncLayoutInflaterPlus.BasicInflater(newContext);
              }
      
              @Override
              protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
                  for (String prefix : sClassPrefixList) {
                      try {
                          View view = createView(name, prefix, attrs);
                          if (view != null) {
                              return view;
                          }
                      } catch (ClassNotFoundException e) {
                          // In this case we want to let the base class take a crack
                          // at it.
                      }
                  }
      
                  return super.onCreateView(name, attrs);
              }
          }
      
          public AsyncLayoutInflaterPlus.InflateRequest obtainRequest() {
              AsyncLayoutInflaterPlus.InflateRequest obj = sRequestPool.acquire();
              if (obj == null) {
                  obj = new AsyncLayoutInflaterPlus.InflateRequest();
              }
              return obj;
          }
      
          public void releaseRequest(AsyncLayoutInflaterPlus.InflateRequest obj) {
              obj.callback = null;
              obj.inflater = null;
              obj.parent = null;
              obj.resid = 0;
              obj.view = null;
              sRequestPool.release(obj);
          }
      
      }
      
  • X2C: 框架保留了XML的優(yōu)點筐乳,并解決了其IO操作和反射的性能問題。開發(fā)人員只需要正常寫XML代碼即可乔妈,在編譯期蝙云,X2C會利用APT工具將XML代碼翻譯為Java代碼。

3. measure/layout 優(yōu)化

  • 渲染流程中 measure 和 layout 也是需要 CPU 在主線程執(zhí)行的;
  • 優(yōu)化方法:減少 UI 布局層次路召,優(yōu)化 layout 的開銷勃刨,盡量不要重復去設(shè)置背景
    • 布局優(yōu)化:
      • 單層布局:盡量選擇LinearLayout或FrameLayout,而少用 RelativeLayout股淡,應(yīng)為RelativeLayout功能較復雜身隐,更耗性能;
        但從程序擴展性的角度看唯灵,更傾向于RelativeLayout
      • 多層布局:布局較復雜時贾铝,RelativeLayout能夠有效的減少布局層級
      • <include/>標簽:實現(xiàn)布局文件的復用,如app自定義的TitleBar
        只支持 layout_xx 和id屬性埠帕,當include和被包含布局的根標簽都指定了id時垢揩,以include為準;指定layout_xx屬性時敛瓷,
        必須也要指定layout_width和layout_height叁巨,否則無法生效
      • <merge/>標簽:在UI的結(jié)構(gòu)優(yōu)化中起著非常重要的作用,它可以刪減多余的層級呐籽,優(yōu)化UI锋勺。
        <merge/>多用于替換FrameLayout或者當一個布局包含另一個時,<merge/>標簽消除視圖層次結(jié)構(gòu)中多余的視圖組绝淡。
        例如你的主布局文件是垂直布局宙刘,引入了一個垂直布局的include,這是如果include布局使用的LinearLayout就沒意義了牢酵,
        使用的話反而減慢你的UI表現(xiàn)悬包。這時可以使用<merge/>標簽優(yōu)化。
      • <ViewStub/>標簽:懶加載馍乙,不會影響UI初始化時的性能布近;
        各種不常用的布局垫释,如進度條、顯示錯誤消息等可以使用ViewStub標簽撑瞧,以減少內(nèi)存使用量棵譬,加快渲染速度
      • 使用 style 來定義通用的屬性,從而重復利用代碼预伺,減少代碼量
      • 封裝組合view實現(xiàn)view復用
      • 使用 LinearLayoutCompat 組件來實現(xiàn)線性布局元素之間的分割線订咸,從而減少了使用View來實現(xiàn)分割線效果
  • Litho:異步布局
    • Litho是 Facebook 開源的聲明式 Android UI 渲染框架,它是基于另外一個 Facebook 開源的布局引擎Yoga開發(fā)的酬诀。
    1. 配置Litho的相關(guān)依賴
    // 項目下
    repositories {
        jcenter()
    }
    
    // module下
    dependencies {
        // ...
        // Litho
        implementation 'com.facebook.litho:litho-core:0.33.0'
        implementation 'com.facebook.litho:litho-widget:0.33.0'
    
        annotationProcessor 'com.facebook.litho:litho-processor:0.33.0'
    
        // SoLoader
        implementation 'com.facebook.soloader:soloader:0.5.1'
    
        // For integration with Fresco
        implementation 'com.facebook.litho:litho-fresco:0.33.0'
    
        // For testing
        testImplementation 'com.facebook.litho:litho-testing:0.33.0'
    
        // Sections (options脏嚷,用來聲明去構(gòu)建一個list)
        implementation 'com.facebook.litho:litho-sections-core:0.33.0'
        implementation 'com.facebook.litho:litho-sections-widget:0.33.0'
        compileOnly 'com.facebook.litho:litho-sections-annotations:0.33.0'
    
        annotationProcessor 'com.facebook.litho:litho-sections-processor:0.33.0'
    }
    2. Application下的onCreate方法中初始化SoLoader:
    @Override
    public void onCreate() {
        super.onCreate();
        SoLoader.init(this, false);
        //Litho使用了Yoga進行布局,而Yoga包含有native依賴瞒御,在Soloader.init方法中對這些native依賴進行了加載父叙。
    }
    3. 在Activity的onCreate方法中添加如下代碼即可顯示單個的文本視圖:
     // 1、將Activity的Context對象保存到ComponentContext中肴裙,并同時初始化
    // 一個資源解析者實例ResourceResolver供其余組件使用趾唱。
    ComponentContext componentContext = new ComponentContext(this);
    // 2、Text內(nèi)部使用建造者模式以實現(xiàn)組件屬性的鏈式調(diào)用蜻懦,下面設(shè)置的text甜癞、
    // TextColor等屬性在Litho中被稱為Prop,此概念引申字React宛乃。
    Text lithoText = Text.create(componentContext)
            .text("Litho text")
            .textSizeDip(64)
            .textColor(ContextCompat.getColor(this, R.color.light_deep_red))
                .build();
    // 3带欢、設(shè)置一個LithoView去展示Text組件:LithoView.create內(nèi)部新建了一個
    // LithoView實例,并用給定的Component(lithoText)進行初始化
    setContentView(LithoView.create(componentContext, lithoText));
    4. 使用自定義Component
    Litho中的視圖單元叫做Component烤惊,即組件,它的設(shè)計理念來源于React組件化的思想吁朦。
    每個組件持有描述一個視圖單元所必須的屬性與狀態(tài)柒室,用于視圖布局的計算工作。
    視圖最終的繪制工作是由組件指定的繪制單元(View或Drawable)來完成的逗宜。
    @LayoutSpec
    public class ListItemSpec {
    
        @OnCreateLayout
        static Component onCreateLayout(ComponentContext context) {
            // Column的作用類似于HTML中的<div>標簽
            return Column.create(context)
                    .paddingDip(YogaEdge.ALL, 16)
                    .backgroundColor(Color.WHITE)
                    .child(Text.create(context)
                                .text("Litho Study")
                                .textSizeSp(36)
                             .textColor(Color.BLUE)
                                .build())
                    .child(Text.create(context)
                                .text("JsonChao")
                                .textSizeSp(24)
                             .textColor(Color.MAGENTA)
                                .build())
                    .build();
        }
    }
    // 2雄右、構(gòu)建ListItem組件
    ListItem listItem = ListItem.create(componentContext).build();
    
  • Flutter:自己的布局 + 渲染引擎
  • RenderThread 與 RenderScript
    • Android 5.0,系統(tǒng)增加了 RenderThread纺讲,對于 ViewPropertyAnimator 和 CircularReveal 動畫擂仍,我們可以使用RenderThead 實現(xiàn)動畫的異步渲染。

參考文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末逢渔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子乡括,更是在濱河造成了極大的恐慌肃廓,老刑警劉巖智厌,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異盲赊,居然都是意外死亡铣鹏,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門哀蘑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诚卸,“玉大人,你說我怎么就攤上這事绘迁『夏纾” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵脊髓,是天一觀的道長辫愉。 經(jīng)常有香客問我,道長将硝,這世上最難降的妖魔是什么恭朗? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮依疼,結(jié)果婚禮上痰腮,老公的妹妹穿的比我還像新娘。我一直安慰自己律罢,他們只是感情好膀值,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著误辑,像睡著了一般沧踏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上巾钉,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天翘狱,我揣著相機與錄音,去河邊找鬼砰苍。 笑死潦匈,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的赚导。 我是一名探鬼主播茬缩,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吼旧!你這毒婦竟也來了凰锡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寡夹,沒想到半個月后处面,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡菩掏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年魂角,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片智绸。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡野揪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瞧栗,到底是詐尸還是另有隱情斯稳,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布迹恐,位于F島的核電站硫痰,受9級特大地震影響难捌,放射性物質(zhì)發(fā)生泄漏社搅。R本人自食惡果不足惜兼犯,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望锤岸。 院中可真熱鬧竖幔,春花似錦、人聲如沸是偷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛋铆。三九已至馋评,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間刺啦,已是汗流浹背栗恩。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留洪燥,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓乳乌,卻偏偏與公主長得像捧韵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子汉操,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

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