Android佈局優化你想知道的都在這裏了

目錄

寫在前面

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

1.1、Android繪製原理

1.2、優化工具

二、Android佈局加載原理

2.1、佈局加載流程

2.2、性能瓶頸

2.3、LayoutInflater.Factory

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

四、異步Inflate實戰

五、X2C框架使用

六、視圖繪製優化


寫在前面

人到中年不得已,莫愁前路有知己!

本篇是Android性能優化系列專欄第三篇,上一篇中通過圖文加實戰的方式介紹了Android的內存優化,有需要的可以翻看一下:

《你的應用內存優化了嗎?》,今天我們就繼續來說說Android中的佈局優化相關的知識。

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

1.1、Android繪製原理

對於Android手機來說,它的畫面渲染依賴於兩個硬件:①、CPU;②、GPU:

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

1.2、優化工具

①、Systrace

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

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

②、Layout Inspector

菜單欄——>Tools——>Layout Inspector

  • Android Studio自帶的工具
  • 查看試圖層次結構

如果進程中存在多個Activity,還會提示讓我們選擇具體哪個Activity,當然了,我這裏只有一個Activity,直接點擊該進程即可:

上面這張圖就是Layout Inspector爲我們生成的具體檢測界面的信息,這個界面總共是分成了三個部分:

左側:View Tree,即:視圖在佈局中的層次結構

中間:Load Overlay,即:相當於屏幕截圖,並且這個截圖自帶各個視圖控件的邊距

右側:Properties Table,即:選中視圖的佈局屬性,比如我這裏選中的是一個TextView,裏面就會顯示出這個TextView所顯示出的具體信息

對於這個工具我們主要關注的是最左側的這一欄層次結構,有了它就可以很方便的看到當前佈局的層級,比如我這裏的LinearLayout這是當前界面的根佈局,它裏面是一個RecyclerView,對於RecyclerView的每一個Item都有三個控件,這三個控件都處在同一層級,整個條目的層級相對還是比較簡單的。

③、Choreographer

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

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

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

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

二、Android佈局加載原理

2.1、佈局加載流程

①、源碼解析

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

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

setContentView(R.layout.activity_main);

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

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

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

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

    @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()方法:

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

這個方法內部又調用了另一個inflate()方法,所以繼續點擊:

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():

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

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

    @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()方法,我們點進去看下:

    /**
     * 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()方法中:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            。。。
            if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> 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,我們點擊該方法跟進:

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

這裏面又調用了另一個createViewFromTag()方法,繼續跟進:

    @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對象,進到這個方法中:

    @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()繼續跟蹤:

@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.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:

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

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

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的創建:

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(..))")

具體實現:

    @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));
    }

結果如下:

對AOP忘記了的可以去看本專欄的第一篇啓動優化部分有介紹:《Android啓動優化你真的瞭解嗎?》

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

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

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

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中的每個控件的耗時情況

四、異步Inflate實戰

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

  • 佈局文件讀取慢:IO過程
  • 創建View慢:通過反射創建一個對象比直接new一個對象要慢3倍,佈局嵌套層級複雜則反射更多

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

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

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

  • WorkThread加載佈局,原生是在UI Thread加載佈局
  • 加載完成之後回調主線程,此時主線程拿到的是創建完成的View對象可以直接使用
  • 節約主線程時間,因爲耗時是發生在了異步線程中,主線程的響應能夠得到保障

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

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

    @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框架的使用方式:

①、添加依賴:app/build.gradle中添加

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

②、添加註解:在使用佈局的任意java類或方法上面添加:

@Xml(layouts = "activity_main")

③、代碼實戰

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

首先是佈局文件:

<?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:orientation="vertical">

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

</LinearLayout>

然後是編譯之後的代碼:

public class X2C0_activity_main implements IViewCreator {
  @Override
  public View createView(Context context) {
    return new com.zhangyue.we.x2c.layouts.X2C0_Activity_Main().createView(context);
  }
}
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源碼來定製化實現相關功能)

六、視圖繪製優化

①、視圖繪製流程

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

可能存在的性能問題:

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

②、佈局層級及複雜度

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

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

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

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

③、過度繪製

  • 一個像素最好只被繪製一次
  • 調試GPU過度繪製
  • 藍色可接受

避免過度繪製方法:

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

④、佈局繪製的其它優化技巧

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

關於佈局優化相關的知識點就總結這麼多了,有些地方還是通過實際代碼實踐之後纔能有所體會,好了,天也不早了,人也都走了,今天就先到這裏吧,下期再會!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章