關於Android UI繪製優化你應該瞭解的知識點

語言: CN / TW / HK

本文已收錄到 GitHub/Android-Notes 中,這裡有 Android 進階成長知識體系筆記,有志同道合的朋友,關注公眾號 [小塵Android專欄] 跟我一起成長。

一、Android繪製原理及工具選擇

1.1、Android繪製原理

對於Android手機來說,它的畫面渲染依賴於兩個硬體:1.CPU;2.GPU:

  • CPU負責計算顯示內容,比如:檢視建立、佈局計算、圖片解碼、文字繪製等
  • GPU負責柵格化(UI元素繪製到螢幕上),柵格化:將一些元件比如Button、Bitmap拆分成不同的畫素進行顯示然後完成繪製,這個操作相對比較耗時,所以引入GPU來加快柵格化操作
  • 16ms發出VSync訊號觸發UI渲染,意思就是Android系統要求每一幀都要在16ms內完成,具體到專案中就是不管業務程式碼或者其他邏輯程式碼有多複雜,想要保證每一幀都很平滑,渲染程式碼就應該在16ms內完成
  • 大多數的Android裝置螢幕重新整理頻率:60Hz ,60幀/秒是人眼和大腦之間協作的極限

1.2、優化工具

1.Systrace

  • 關注Frames
  • 正常:綠色圓點,丟幀:黃色或紅色
  • Alerts:Systrace中自動分析並且標註異常效能的條目

image.png

上面這張圖是我找的一個使用Systrace生成的.html檔案,圖中每一個F的出現就表明出現了一幀,可以看到這兩個F之間的時間間隔比16ms多了不少,Alert type這裡面就是Systrace自動給出的一些提示資訊,我們可以根據提示資訊來查詢修改的方向。

②、Layout Inspector

選單欄——>Tools——>Layout Inspector

  • Android Studio自帶的工具
  • 檢視試圖層次結構

③、Choreographer

獲取FPS,線上使用,具備實時性

  • Api 16之後
  • 使用方式是:Choreographer.getInstance().postFrameCallback

這裡寫了一個方法getFPS()來獲取這個APP的FPS情況,方法內部一開始是做了一個保護性操作,確保使用的Choreographer發生在API16之後,然後在doFrame回撥中首先判斷是不是統計週期的第一次,如果是就記錄第一次回撥的時間,接下來就是判斷時間間隔是否超過預設的閥值160ms,如果超過則計算FPS,計算方式是間隔時間除以間隔時間內發生的次數,如果沒有超過則直接將次數加1。

image.png

輸出的結果可以看到基本上都是59和60之間的數值。

二、Android佈局載入原理

2.1、佈局載入流程

1.原始碼解析

這一部分我們來看下原始碼,因為內容比較多,我就儘可能的簡單說,對於原始碼閱讀的流程我們之前已經說過幾次了,這裡就不再介紹了,基本上就是找到你需要的入口方法,然後一路跟蹤下去,把整個流程串起來,不需要你把每一行的程式碼都讀懂。

既然說的是佈局載入,那麼我們首先肯定是找入口方法,這個方法你回想一下每個頁面載入佈局都是呼叫的什麼方法呢?很簡單啦:

java setContentView(R.layout.activity_main);

然後點選這個方法進入原始碼中去就到了AppCompatActivity類的setContentView()方法中:

java @Override public void setContentView(@LayoutRes int layoutResID) { getDelegate().setContentView(layoutResID); }

繼續跟蹤點選setContentView()方法:

image.png

發現這是一個抽象方法,此時你需要去找它的實現類AppCompatDelegateImpl中的方法了,點選左側向下的fx向下箭頭:

java @Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent); mAppCompatWindowCallback.getWrapped().onContentChanged(); }

這個方法中由於傳遞進來的resId也就是佈局檔案的id,它只在LayoutInflater這一行用到了,所以接著跟蹤這一行,點選inflate()方法:

java public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); }

這個方法內部又呼叫了另一個inflate()方法,所以繼續點選:

```java public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); if (DEBUG) { Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" + Integer.toHexString(resource) + ")"); }

    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

```

這裡面又有一個inflate()方法,入參有一個parser,看了看上下的程式碼,知道了它其實是XmlResourceParser的例項,那我們先不去看這個inflate()方法具體的實現,先來看下這個parser究竟是什麼?找到res.getLayout()方法,裡面傳入了我們的資源id,返回的是XmlResourceParser,看名字XML資源解析器,就知道這玩意應該很屌,來吧,繼續點選getLayout():

java @NonNull public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException { return loadXmlResourceParser(id, "layout"); }

沒啥實質性的內容,繼續點選它的實現方法loadXmlResourceParser():

java @NonNull @UnsupportedAppUsage XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type) throws NotFoundException { final TypedValue value = obtainTempTypedValue(); try { final ResourcesImpl impl = mResourcesImpl; impl.getValue(id, value, true); if (value.type == TypedValue.TYPE_STRING) { return impl.loadXmlResourceParser(value.string.toString(), id, value.assetCookie, type); } throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id) + " type #0x" + Integer.toHexString(value.type) + " is not valid"); } finally { releaseTempTypedValue(value); } }

這個方法開始是一些物件的宣告,後面是異常的處理,所以看下來真正有用的就是if判斷裡面的,它判斷了value.type如果是String型別的,然後繼續呼叫了impl的loadXmlResourceParser()方法,我們點進去看下:

java /** * Loads an XML parser for the specified file. * * @param file the path for the XML file to parse * @param id the resource identifier for the file * @param assetCookie the asset cookie for the file * @param type the type of resource (used for logging) * @return a parser for the specified XML file * @throws NotFoundException if the file could not be loaded */ @NonNull XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie, @NonNull String type) throws NotFoundException { ... //程式碼有點多就不貼了,不然文章會很長,大家有需要的自己對照這個過程讀一下原始碼,敬請諒解 }

主要看註釋那裡的說明哈,Android中的佈局都是寫在XML檔案中的,這個方法就是為我們具體所寫的佈局檔案準備一個XML的解析器,所以它實際上就是一個XML的Pull解析的過程。需要注意的是:android的佈局實際上是一個XML檔案,它在載入的時候會首先將它讀取到記憶體中,這個過程實際上就是一個IO過程,一般在android開發中操作IO都會將其置於工作執行緒中,所以這裡可能會成為我們優化的一個方向。

關於這個XmlResourceParser就說到這裡,下面繼續回到上面說的那個inflate()方法中:

```java public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { 。。。 if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException(" can be used only with a valid " + "ViewGroup root and attachToRoot=true"); }

                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
               。。。
            }
            。。。
        return result;
    }
}

```

這裡同樣的省略了部分程式碼,我們知道日常開發中經常會碰到一些報錯,其實這些報錯在Android的原始碼中都是有所體現的,比如這裡定義的關於merge標籤的一個異常資訊。接著看createViewFromTag()這個方法,看名字我們應該能大致猜測出來它是幹嘛的了,它應該就是通過一系列的Tag來建立相對應的View,我們點選該方法跟進:

java @UnsupportedAppUsage private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) { return createViewFromTag(parent, name, context, attrs, false); }

這裡面又呼叫了另一個createViewFromTag()方法,繼續跟進:

```java @UnsupportedAppUsage View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); }

    // Apply a theme wrapper, if allowed and one is specified.
    if (!ignoreThemeAttr) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
    }

    try {
        View view = tryCreateView(parent, name, context, attrs);

        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(context, parent, name, attrs);
                } else {
                    view = createView(context, name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
    } catch (InflateException e) {
        throw e;

    } catch (ClassNotFoundException e) {
        final InflateException ie = new InflateException(
                getParserStateDescription(context, attrs)
                + ": Error inflating class " + name, e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;

    } catch (Exception e) {
        final InflateException ie = new InflateException(
                getParserStateDescription(context, attrs)
                + ": Error inflating class " + name, e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;
    }
}

```

這裡就到了重點的地方了,這裡面就是建立View的過程了:

首先:View view = tryCreateView(parent, name, context, attrs); 它通過這個tryCreateView()方法構建出View物件,進到這個方法中:

```java @UnsupportedAppUsage(trackingBug = 122360734) @Nullable public final View tryCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { if (name.equals(TAG_1995)) { // Let's party like it's 1995! return new BlinkLayout(context, attrs); }

    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 && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }

    return view;
}

```

這個方法裡面就是判斷了幾個factory是否為空,首先是Factory2,如果Factory2不為空則呼叫Factory2的onCreateView()方法建立View物件,否則判斷Factory是否為空,如果Factory不為空則呼叫Factory的onCreateView()建立View物件,如果都為空,則View為空。如果view為空並且PrivateFactory不為空,則呼叫PrivateFactory的onCreateView()方法構建View,需要注意的是PrivateFactory它只用於Fragment標籤的載入。當這些條件都不滿足的時候,我們回到上面的createViewFromTag()方法中接著看,它會走到view==null的條件判斷中去,它會走onCreateView()或者createView(),點選createView()繼續跟蹤:

```java @Nullable public final View createView(@NonNull Context viewContext, @NonNull String name, @Nullable String prefix, @Nullable AttributeSet attrs) throws ClassNotFoundException, InflateException { Objects.requireNonNull(viewContext); Objects.requireNonNull(name); Constructor<? extends View> constructor = sConstructorMap.get(name); if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class<? extends View> clazz = null;

    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

        if (constructor == null) {
            // Class not found in the cache, see if it's real, and try to add it
            clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                    mContext.getClassLoader()).asSubclass(View.class);

            if (mFilter != null && clazz != null) {
                boolean allowed = mFilter.onLoadClass(clazz);
                if (!allowed) {
                    failNotAllowed(name, prefix, viewContext, attrs);
                }
            }
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            sConstructorMap.put(name, constructor);
        } else {
            // If we have a filter, apply it to cached constructor
            if (mFilter != null) {
                // Have we seen this name before?
                Boolean allowedState = mFilterMap.get(name);
                if (allowedState == null) {
                    // New class -- remember whether it is allowed
                    clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                            mContext.getClassLoader()).asSubclass(View.class);

                    boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                    mFilterMap.put(name, allowed);
                    if (!allowed) {
                        failNotAllowed(name, prefix, viewContext, attrs);
                    }
                } else if (allowedState.equals(Boolean.FALSE)) {
                    failNotAllowed(name, prefix, viewContext, attrs);
                }
            }
        }

        Object lastContext = mConstructorArgs[0];
        mConstructorArgs[0] = viewContext;
        Object[] args = mConstructorArgs;
        args[1] = attrs;

        try {
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // Use the same context when inflating ViewStub later.
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            return view;
        } finally {
            mConstructorArgs[0] = lastContext;
        }
    } 
    。。。
}

```

這個方法裡面constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); 這兩行首先找到clazz的構造方法,通過反射的方式將其設定為外部可呼叫的,然後下面final View view = constructor.newInstance(args); 這一行它通過建構函式反射建立了View,在這個方法中是真正進行了View的建立,當然這是在沒有使用Factory的情況下哦。這個過程實際上它是使用了反射,反射是有可能導致程式變慢的一個因素,所以這裡也可以作為我們的一個優化點。

2.佈局載入流程總結

image.png

2.2、效能瓶頸

  • 佈局檔案解析:IO過程(檔案過大時可能會導致卡頓)
  • 建立View物件:反射(使用過多也會導致變慢)

2.3、LayoutInflater.Factory

在上面解讀setContentView的原始碼時,我們知道建立View的過程優先是使用Factory2和Factory進行建立,下面對這兩個類作簡要說明:

LayoutInflater.Factory:

  • LayoutInflater建立View的一個Hook,Hook其實就是我們可以將自己的程式碼掛在它的原始程式碼之上,可以對它的流程進行更改
  • 定製建立View的過程:比如全域性替換自定義TextView等

Factory與Factory2

  • Factory2繼承於Factory
  • 多了一個引數:parent

我們來看一下它們的原始碼,首先來看Factory2:

java public interface Factory2 extends Factory { @Nullable View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs); }

可以看到Factory2是一個介面,並且它是繼承自Factory的,來看一下Factory:

java 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. */ @Nullable View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs); }

入參中有個name,來看一下它的註釋,意思就是我們要載入的Tag,比如這個Tag是TextView,那麼通過這個方法返回的就是TextView,實際上如果你繼續跟蹤的話,你會發現這個Tag實際上就是我們平時在佈局中寫的一個個的控制元件:比如TextView、ImageView等等,它會根據具體的Tag來進行對應View的建立:

java switch (name) { case "TextView": view = createTextView(context, attrs); verifyNotNull(view, name); break; case "ImageView": view = createImageView(context, attrs); verifyNotNull(view, name); break; case "Button": view = createButton(context, attrs); verifyNotNull(view, name); break; case "EditText": view = createEditText(context, attrs); verifyNotNull(view, name); break; case "Spinner": view = createSpinner(context, attrs); verifyNotNull(view, name); break; case "ImageButton": view = createImageButton(context, attrs); verifyNotNull(view, name); break; case "CheckBox": view = createCheckBox(context, attrs); verifyNotNull(view, name); break; case "RadioButton": view = createRadioButton(context, attrs); verifyNotNull(view, name); break; case "CheckedTextView": view = createCheckedTextView(context, attrs); verifyNotNull(view, name); break; case "AutoCompleteTextView": view = createAutoCompleteTextView(context, attrs); verifyNotNull(view, name); break; case "MultiAutoCompleteTextView": view = createMultiAutoCompleteTextView(context, attrs); verifyNotNull(view, name); break; case "RatingBar": view = createRatingBar(context, attrs); verifyNotNull(view, name); break; case "SeekBar": view = createSeekBar(context, attrs); verifyNotNull(view, name); break; case "ToggleButton": view = createToggleButton(context, attrs); verifyNotNull(view, name); break; default: // The fallback that allows extending class to take over view inflation // for other tags. Note that we don't check that the result is not-null. // That allows the custom inflater path to fall back on the default one // later in this method. view = createView(context, name, attrs); }

並且我們對比兩個介面,可以發現Factory2比Factory就是入參多了一個parent,這個parent就是你建立的View的parent,所以綜上可得Factory2比Factory功能上更加強大。

三、優雅獲取介面佈局耗時

隨著專案的不斷升級,專案體量逐漸變大,頁面可能也變的越來越多,然後我們希望能夠在線上進行統計,瞭解到具體哪些頁面使用者在進入時會出現卡頓,佈局檔案載入也可能會導致卡頓。

常規方式:覆寫方法(setContentView)、手動埋點上報服務端(不夠優雅,程式碼具有侵入性)

AOP方式:切Activity的setContentView(切面點)

@ Around("execution(*android.app.Activity.setContentView(..))")

具體實現:

java @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(); } Log.i(name, " cost " + (System.currentTimeMillis() - time)); }

結果如下:

image.png

思考:如何獲取每一個控制元件載入耗時?

我們在上面使用setContentView獲取到的是頁面中所有控制元件的耗時情況,那現在我想要知道這個頁面中各個控制元件的耗時分佈情況,以便於整體的把控分析並且可以對耗時較多的控制元件做針對性的優化,這樣一個場景該如何實現呢?由於每個頁面佈局中的控制元件都是不可控的,有可能多也有可能少,所以我們應該儘量做到低侵入性,這個問題大家可以好好想想,看看有什麼解決方案。

解決方案:使用LayoutInflaterCompat.Factory2(LayoutInflaterCompat是LayoutInflater的相容類)讓它在建立View時進行Hook:

```java LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() { @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { long time = System.currentTimeMillis(); View view = getDelegate().createView(parent, name, context, attrs); Log.i(name,"控制元件耗時:" + (System.currentTimeMillis() - time)); return view; }

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

```

結果如下:可以看到我們確實獲取到了列表Item中的每個控制元件的耗時情況

image.png

四、非同步Inflate實戰

在上面我們已經說過了佈局檔案載入慢主要的原因是有以下兩點:

  • 佈局檔案讀取慢:IO過程
  • 建立View慢:通過反射建立一個物件比直接new一個物件要慢3倍,佈局巢狀層級複雜則反射更多

針對上面說的這兩種情況,相對應的解決套路也就是兩種:

  • 根本性解決:去掉IO過程、不使用反射
  • 側面緩解:讓主執行緒不耗時,不影響主執行緒

這裡針對側面緩解的方案來介紹一種實現方式:AsyncLayoutInflater,谷歌提供的一個類,簡稱非同步Inflate

  • WorkThread載入佈局,原生是在UI Thread載入佈局
  • 載入完成之後回撥主執行緒,此時主執行緒拿到的是建立完成的View物件可以直接使用
  • 節約主執行緒時間,因為耗時是發生在了非同步執行緒中,主執行緒的響應能夠得到保障

使用方式:首先匯入asynclayoutinflater的依賴庫,這裡我們參考谷歌官方文件中androidx的使用:

image.png 然後來修改我們的MainActivity中的onCreate()方法:

java @Override protected void onCreate(Bundle savedInstanceState) { 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); mRecycler = findViewById(R.id.mRecycler); mRecycler.setLayoutManager(new LinearLayoutManager(MainActivity.this)); mRecycler.addItemDecoration(new DividerItemDecoration(MainActivity.this, DividerItemDecoration.VERTICAL)); mRecycler.setAdapter(mAdapter); mAdapter.setOnFeedShowCallBack(MainActivity.this); } }); super.onCreate(savedInstanceState); // setContentView(R.layout.activity_main); mAdapter = new FeedAdapter(this, mList); initData(); // getFPS(); }

有興趣的可以去看一下AsyncLayoutInflater的原始碼,理解起來應該不難,這個類內部有一個Handler物件,一個InflateThread類繼承於Thread,還有一個inflate方法,該方法有三個入參resid、parent、callback,同時將這三個引數封裝成了InflateRequest的資料結構,然後加到執行緒的佇列中,執行緒中同時有一個run()方法在不斷執行,它會從佇列中取出一條InflateRequest,然後這個request.inflate開始執行inflate()方法並返回request.view,這個方法是執行在子執行緒中的,最後通過Handler將它回撥到主執行緒中,同時有一個相關聯的Callback,在Callback中進行判斷如果沒有建立完成的話,會回退到主執行緒中進行佈局的載入,最後將request.view回撥到onInflateFinished()方法中,這樣主執行緒就可以在該方法中拿到對應的view了。

總結:

  • 不能設定LayoutInflater.Factory(),需要自定義AsyncLayoutInflater解決;
  • 注意View中不能有依賴主執行緒的操作

五、X2C框架使用

上面這一部分是介紹了一種側面緩解的方式,那這一部分我們來思考一下從根本上解決該如何實現?

首先來說一下思路哈,其實也沒啥思路,就是利用Java程式碼寫佈局,這種方案的特點如下:

  • 本質上解決了效能問題(沒有xml檔案也就沒有了IO的過程,直接new物件沒有了反射的過程)
  • 引入新問題:不便於開發、可維護性差

思路有了但是看著實現起來卻不太現實哈,那咋辦呢?咋辦呢?咋辦呢?嗯,這樣拌,大神還是很多的,我們使用開源方案X2C:

X2C框架介紹:保留XML優點,解決其效能問題

  • 開發人員寫XML,載入Java程式碼
  • 原理:APT編譯期翻譯XML為Java程式碼

X2C框架的使用方式:

  1. 新增依賴:app/build.gradle中新增

Kotlin annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2' implementation 'com.zhangyue.we:x2c-lib:1.0.6'

  1. 添加註解:在使用佈局的任意java類或方法上面新增:

html @Xml(layouts = "activity_main")

  1. 程式碼實戰

image.png 將原有的setContentView註釋掉,然後使用X2C.setContentView()來設定佈局,執行之後發現是可以正常載入的,圖中左側圈出來的是使用X2C編譯之後的產物,這個其實就是它的底層實現原理了,我們來看一下:

首先是佈局檔案:

```html

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/mRecycler"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

```

然後是編譯之後的程式碼:

```java public class X2C0_Activity_Main implements IViewCreator { @Override public View createView(Context ctx) { Resources res = ctx.getResources();

    LinearLayout linearLayout0 = new LinearLayout(ctx);
    linearLayout0.setOrientation(LinearLayout.VERTICAL);

    RecyclerView recyclerView1 = new RecyclerView(ctx);
    LinearLayout.LayoutParams layoutParam1 = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
    recyclerView1.setId(R.id.mRecycler);
    recyclerView1.setLayoutParams(layoutParam1);
    linearLayout0.addView(recyclerView1);

    return linearLayout0;

} } ```

可以看到它內部就是將我們佈局檔案中的控制元件全都以Java物件的形式給new出來了。

X2C存在的問題:

  • XML中有的部分屬性Java不支援(雖然不多但是也有)
  • 失去了系統的相容(AppCompat,如果你需要使用AppCompatXXX下面的控制元件可以通過修改X2C原始碼來定製化實現相關功能)

六、檢視繪製優化

1.檢視繪製流程

  • 測量:確定大小(自頂向下進行檢視樹的遍歷,確定ViewGroup和View應該有多大)
  • 佈局:確定位置(執行另一個自頂向下的遍歷操作,ViewGroup會根據測量階段測定的大小確定自己應該擺放的位置)
  • 繪製:繪製檢視(對於檢視樹中的每個物件系統都會為它建立一個Canvas物件,然後向GPU傳送一條繪製命令進行繪製)

可能存在的效能問題:

  • 每個階段耗時
  • 自頂而下的遍歷(如果Layout層級比較深則遍歷也是很耗時的)
  • 觸發多次(比如巢狀使用RelativeLayout有可能會導致繪製環節觸發多次)

2.佈局層級及複雜度

編寫佈局的準則:減少View樹層級

  • 不巢狀使用RelativeLayout
  • 不在巢狀的LinearLayout中使用weight
  • merge標籤:減少一個層級,只能用於根View

這裡推薦使用:ConstraintLayout,網上關於它有很多的文章,後面我也準備專門寫一篇它的使用總結

  • 實現幾乎完全扁平化佈局
  • 構建複雜佈局效能更高
  • 具有RelativeLayout和LinearLayout特性

3.過度繪製

  • 一個畫素最好只被繪製一次
  • 除錯GPU過度繪製
  • 藍色可接受

避免過度繪製方法:

  • 去掉多餘背景色,減少複雜shape使用
  • 避免層級疊加
  • 自定義View使用clipRect遮蔽被遮蓋View繪製(當覆寫onDraw()之後,系統就無法知道View中各個元素的位置和層級關係,就無法做自動優化,即無法自動忽略繪製那些不可見的元素)

4.佈局繪製的其它優化技巧

  • ViewStub:高效佔位符、延遲初始化(這個標籤沒有大小,也沒有繪製功能不參與measure和layout過程,資源消耗非常低,一般用於延遲初始化)
  • onDraw中避免:建立大物件、耗時操作
  • TextView相關優化(setText顯示靜態文字)

如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙: 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。微信搜尋公眾號 [小塵Android專欄] ,第一時間閱讀更多幹貨知識!