關(guān)于Android UI繪制優(yōu)化你應(yīng)該了解的知識點

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

1.1、Android繪制原理

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

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

1.2、優(yōu)化工具

1.Systrace

  • 關(guān)注Frames
  • 正常:綠色圓點,丟幀:黃色或紅色
  • Alerts:Systrace中自動分析并且標(biāo)注異常性能的條目

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

②、Layout Inspector

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

  • Android Studio自帶的工具
  • 查看試圖層次結(jié)構(gòu)

③、Choreographer

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

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

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

輸出的結(jié)果可以看到基本上都是59和60之間的數(shù)值。

二、Android布局加載原理

2.1、布局加載流程

1.源碼解析

這一部分我們來看下源碼,因為內(nèi)容比較多,我就盡可能的簡單說,對于源碼閱讀的流程我們之前已經(jīng)說過幾次了,這里就不再介紹了,基本上就是找到你需要的入口方法,然后一路跟蹤下去,把整個流程串起來,不需要你把每一行的代碼都讀懂。

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

setContentView(R.layout.activity_main);

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

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

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

發(fā)現(xiàn)這是一個抽象方法,此時你需要去找它的實現(xiàn)類AppCompatDelegateImpl中的方法了,點擊左側(cè)向下的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);
    }

這個方法內(nèi)部又調(diào)用了另一個inflate()方法,所以繼續(xù)點擊:

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()方法,入?yún)⒂幸粋€parser,看了看上下的代碼,知道了它其實是XmlResourceParser的實例,那我們先不去看這個inflate()方法具體的實現(xiàn),先來看下這個parser究竟是什么?找到res.getLayout()方法,里面?zhèn)魅肓宋覀兊馁Y源id,返回的是XmlResourceParser,看名字XML資源解析器,就知道這玩意應(yīng)該很屌,來吧,繼續(xù)點擊getLayout():

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

沒啥實質(zhì)性的內(nèi)容,繼續(xù)點擊它的實現(xiàn)方法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類型的,然后繼續(xù)調(diào)用了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文件中的,這個方法就是為我們具體所寫的布局文件準(zhǔn)備一個XML的解析器,所以它實際上就是一個XML的Pull解析的過程。需要注意的是:android的布局實際上是一個XML文件,它在加載的時候會首先將它讀取到內(nèi)存中,這個過程實際上就是一個IO過程,一般在android開發(fā)中操作IO都會將其置于工作線程中,所以這里可能會成為我們優(yōu)化的一個方向。

關(guān)于這個XmlResourceParser就說到這里,下面繼續(xù)回到上面說的那個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;
        }
    }

這里同樣的省略了部分代碼,我們知道日常開發(fā)中經(jīng)常會碰到一些報錯,其實這些報錯在Android的源碼中都是有所體現(xiàn)的,比如這里定義的關(guān)于merge標(biāo)簽的一個異常信息。接著看createViewFromTag()這個方法,看名字我們應(yīng)該能大致猜測出來它是干嘛的了,它應(yīng)該就是通過一系列的Tag來創(chuàng)建相對應(yīng)的View,我們點擊該方法跟進:

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

這里面又調(diào)用了另一個createViewFromTag()方法,繼續(xù)跟進:

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

這里就到了重點的地方了,這里面就是創(chuàng)建View的過程了:

首先:View view = tryCreateView(parent, name, context, attrs); 它通過這個tryCreateView()方法構(gòu)建出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不為空則調(diào)用Factory2的onCreateView()方法創(chuàng)建View對象,否則判斷Factory是否為空,如果Factory不為空則調(diào)用Factory的onCreateView()創(chuàng)建View對象,如果都為空,則View為空。如果view為空并且PrivateFactory不為空,則調(diào)用PrivateFactory的onCreateView()方法構(gòu)建View,需要注意的是PrivateFactory它只用于Fragment標(biāo)簽的加載。當(dāng)這些條件都不滿足的時候,我們回到上面的createViewFromTag()方法中接著看,它會走到view==null的條件判斷中去,它會走onCreateView()或者createView(),點擊createView()繼續(xù)跟蹤:

@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的構(gòu)造方法,通過反射的方式將其設(shè)置為外部可調(diào)用的,然后下面final View view = constructor.newInstance(args); 這一行它通過構(gòu)造函數(shù)反射創(chuàng)建了View,在這個方法中是真正進行了View的創(chuàng)建,當(dāng)然這是在沒有使用Factory的情況下哦。這個過程實際上它是使用了反射,反射是有可能導(dǎo)致程序變慢的一個因素,所以這里也可以作為我們的一個優(yōu)化點。

2.布局加載流程總結(jié)

2.2、性能瓶頸

  • 布局文件解析:IO過程(文件過大時可能會導(dǎo)致卡頓)
  • 創(chuàng)建View對象:反射(使用過多也會導(dǎo)致變慢)

2.3、LayoutInflater.Factory

在上面解讀setContentView的源碼時,我們知道創(chuàng)建View的過程優(yōu)先是使用Factory2和Factory進行創(chuàng)建,下面對這兩個類作簡要說明:

LayoutInflater.Factory:

  • LayoutInflater創(chuàng)建View的一個Hook,Hook其實就是我們可以將自己的代碼掛在它的原始代碼之上,可以對它的流程進行更改
  • 定制創(chuàng)建View的過程:比如全局替換自定義TextView等

Factory與Factory2

  • Factory2繼承于Factory
  • 多了一個參數(shù):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);
    }

入?yún)⒅杏袀€name,來看一下它的注釋,意思就是我們要加載的Tag,比如這個Tag是TextView,那么通過這個方法返回的就是TextView,實際上如果你繼續(xù)跟蹤的話,你會發(fā)現(xiàn)這個Tag實際上就是我們平時在布局中寫的一個個的控件:比如TextView、ImageView等等,它會根據(jù)具體的Tag來進行對應(yīng)View的創(chuàng)建:

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

并且我們對比兩個接口,可以發(fā)現(xiàn)Factory2比Factory就是入?yún)⒍嗔艘粋€parent,這個parent就是你創(chuàng)建的View的parent,所以綜上可得Factory2比Factory功能上更加強大。

三、優(yōu)雅獲取界面布局耗時

隨著項目的不斷升級,項目體量逐漸變大,頁面可能也變的越來越多,然后我們希望能夠在線上進行統(tǒng)計,了解到具體哪些頁面用戶在進入時會出現(xiàn)卡頓,布局文件加載也可能會導(dǎo)致卡頓。

常規(guī)方式:覆寫方法(setContentView)、手動埋點上報服務(wù)端(不夠優(yōu)雅,代碼具有侵入性)

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

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

具體實現(xiàn):

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

結(jié)果如下:

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

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

解決方案:使用LayoutInflaterCompat.Factory2(LayoutInflaterCompat是LayoutInflater的兼容類)讓它在創(chuàng)建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;
            }
        });

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

四、異步Inflate實戰(zhàn)

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

  • 布局文件讀取慢:IO過程
  • 創(chuàng)建View慢:通過反射創(chuàng)建一個對象比直接new一個對象要慢3倍,布局嵌套層級復(fù)雜則反射更多

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

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

這里針對側(cè)面緩解的方案來介紹一種實現(xiàn)方式:AsyncLayoutInflater,谷歌提供的一個類,簡稱異步Inflate

  • WorkThread加載布局,原生是在UI Thread加載布局
  • 加載完成之后回調(diào)主線程,此時主線程拿到的是創(chuàng)建完成的View對象可以直接使用
  • 節(jié)約主線程時間,因為耗時是發(fā)生在了異步線程中,主線程的響應(yīng)能夠得到保障

使用方式:首先導(dǎo)入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的源碼,理解起來應(yīng)該不難,這個類內(nèi)部有一個Handler對象,一個InflateThread類繼承于Thread,還有一個inflate方法,該方法有三個入?yún)esid、parent、callback,同時將這三個參數(shù)封裝成了InflateRequest的數(shù)據(jù)結(jié)構(gòu),然后加到線程的隊列中,線程中同時有一個run()方法在不斷執(zhí)行,它會從隊列中取出一條InflateRequest,然后這個request.inflate開始執(zhí)行inflate()方法并返回request.view,這個方法是執(zhí)行在子線程中的,最后通過Handler將它回調(diào)到主線程中,同時有一個相關(guān)聯(lián)的Callback,在Callback中進行判斷如果沒有創(chuàng)建完成的話,會回退到主線程中進行布局的加載,最后將request.view回調(diào)到onInflateFinished()方法中,這樣主線程就可以在該方法中拿到對應(yīng)的view了。

總結(jié):

  • 不能設(shè)置LayoutInflater.Factory(),需要自定義AsyncLayoutInflater解決;
  • 注意View中不能有依賴主線程的操作

五、X2C框架使用

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

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

  • 本質(zhì)上解決了性能問題(沒有xml文件也就沒有了IO的過程,直接new對象沒有了反射的過程)
  • 引入新問題:不便于開發(fā)、可維護性差

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

X2C框架介紹:保留XML優(yōu)點,解決其性能問題

  • 開發(fā)人員寫XML,加載Java代碼
  • 原理:APT編譯期翻譯XML為Java代碼

X2C框架的使用方式:

  1. 添加依賴:app/build.gradle中添加
annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'
implementation 'com.zhangyue.we:x2c-lib:1.0.6'
  1. 添加注解:在使用布局的任意java類或方法上面添加:
@Xml(layouts = "activity_main")
  1. 代碼實戰(zhàn)

將原有的setContentView注釋掉,然后使用X2C.setContentView()來設(shè)置布局,運行之后發(fā)現(xiàn)是可以正常加載的,圖中左側(cè)圈出來的是使用X2C編譯之后的產(chǎn)物,這個其實就是它的底層實現(xiàn)原理了,我們來看一下:

首先是布局文件:

<?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 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;
  }
}

可以看到它內(nèi)部就是將我們布局文件中的控件全都以Java對象的形式給new出來了。

X2C存在的問題:

  • XML中有的部分屬性Java不支持(雖然不多但是也有)
  • 失去了系統(tǒng)的兼容(AppCompat,如果你需要使用AppCompatXXX下面的控件可以通過修改X2C源碼來定制化實現(xiàn)相關(guān)功能)

六、視圖繪制優(yōu)化

1.視圖繪制流程

  • 測量:確定大小(自頂向下進行視圖樹的遍歷,確定ViewGroup和View應(yīng)該有多大)
  • 布局:確定位置(執(zhí)行另一個自頂向下的遍歷操作,ViewGroup會根據(jù)測量階段測定的大小確定自己應(yīng)該擺放的位置)
  • 繪制:繪制視圖(對于視圖樹中的每個對象系統(tǒng)都會為它創(chuàng)建一個Canvas對象,然后向GPU發(fā)送一條繪制命令進行繪制)

可能存在的性能問題:

  • 每個階段耗時
  • 自頂而下的遍歷(如果Layout層級比較深則遍歷也是很耗時的)
  • 觸發(fā)多次(比如嵌套使用RelativeLayout有可能會導(dǎo)致繪制環(huán)節(jié)觸發(fā)多次)

2.布局層級及復(fù)雜度

編寫布局的準(zhǔn)則:減少View樹層級

  • 不嵌套使用RelativeLayout
  • 不在嵌套的LinearLayout中使用weight
  • merge標(biāo)簽:減少一個層級,只能用于根View

這里推薦使用:ConstraintLayout,網(wǎng)上關(guān)于它有很多的文章,后面我也準(zhǔn)備專門寫一篇它的使用總結(jié)

  • 實現(xiàn)幾乎完全扁平化布局
  • 構(gòu)建復(fù)雜布局性能更高
  • 具有RelativeLayout和LinearLayout特性

3.過度繪制

  • 一個像素最好只被繪制一次
  • 調(diào)試GPU過度繪制
  • 藍色可接受

避免過度繪制方法:

  • 去掉多余背景色,減少復(fù)雜shape使用
  • 避免層級疊加
  • 自定義View使用clipRect屏蔽被遮蓋View繪制(當(dāng)覆寫onDraw()之后,系統(tǒng)就無法知道View中各個元素的位置和層級關(guān)系,就無法做自動優(yōu)化,即無法自動忽略繪制那些不可見的元素)

4.布局繪制的其它優(yōu)化技巧

  • ViewStub:高效占位符、延遲初始化(這個標(biāo)簽沒有大小,也沒有繪制功能不參與measure和layout過程,資源消耗非常低,一般用于延遲初始化)
  • onDraw中避免:創(chuàng)建大對象、耗時操作
  • TextView相關(guān)優(yōu)化(setText顯示靜態(tài)文本)

性能系列專欄其他文章

關(guān)于 Android內(nèi)存優(yōu)化你應(yīng)該了解的知識點
關(guān)于 Android啟動優(yōu)化你應(yīng)該了解的知識點
Android卡頓優(yōu)化分析及解決方案,全面掌握!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容