「Android10源碼分析」爲何複雜佈局會產生卡頓?-- LayoutInflater詳解

LayoutInflater源碼詳解

前言html

這篇文章會從源碼的角度分析,LayoutInflater將xml文件實例化爲一個view對象的流程java

咱們會發現,其中有兩個部分是耗時的主要來源android

  1. XmlResourseParser對xml的遍歷
  2. 反射建立View對象致使的耗時

這兩點,又跟Xml的複雜程度成正相關,Xml越複雜,則遞歸調用所消耗的時間就越長,就產生了咱們所說的,卡頓問題git

總體流程概覽

彩蛋:BlinkLayout

BlinkLayout是LayoutInflater中的一個內部類,它自己是是FrameLayout的子類,若是當前標籤爲TAG_1995,則建立一個隔0.5秒閃爍一次的BlinkLayout來承載它的佈局內容github

源碼註釋也頗有意思,寫了Let's party like it's 1995!, 聽說是爲了慶祝1995年的復活節bash

LayoutInflaterapp

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);
        }
				...
        return view;
    }
複製代碼

具體使用也很簡單框架

<blink
        android:layout_below="@id/iv_qr_code"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Android研習社"
            android:textColor="#157686"
            android:textSize="55sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </blink>
複製代碼

效果以下,這種效果也適合來作EditText中光標的閃爍效果ide

掃描上方二維碼關注「Android研習社」公衆號,獲取更多學習資料!函數

ps: 想深刻學習的都關注了,還不趕快關注一波?

LayoutInflater的建立

概覽

LayoutInflater是一個抽象類,它的建立,並非交由App層處理的,而是調用了from()的靜態函數,經由系統服務LAYOUT_INFLATER_SERVICE,最終建立了一個LayoutInflater的子類對象--PhoneLayoutInflater

重要函數解析

LayoutInflater.from(cxt)

這個函數比較簡單,就是根據傳遞過來的Context對象,調用getSystemService()來獲取對應的系統服務,並賦值給LayoutInflater

public static LayoutInflater from(Context context) { 
        LayoutInflater LayoutInflater =  //LayoutInflate是一個系統服務,最終返回的是`PhoneLahyoutInflater`
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }
複製代碼

Context自己是一個抽象類,它真正的實例化對象,是ContextImpl, 在這個類的getSystemService()函數中,真正執行獲取系統服務的類,是SystemServiceRegistry,其中又封裝了一個ServiceFetcher來獲取真正的系統服務,全部的系統服務,都是存儲在一個map集合--SYSTEM_SERVICE_FETCHERS當中,這裏實際上是一個get方法,是從這個map集合中取出對應的系統服務

LayoutInflater

@Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

複製代碼

SystemServiceRegistry

/** * Gets a system service from a given context. */
    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }
複製代碼

關於對應的服務的添加,也就是調用了SYSTEM_SERVICE_FETCHERS的put函數,這個動做是交由registerService()來完成的

/** * Statically registers a system service with the context. * This method must be called during static initialization only. */
    private static <T> void registerService(String serviceName, Class<T> serviceClass, ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }
複製代碼

SystemServiceRegistry這個類中有一個靜態代碼塊,是用來完成全部服務的註冊的,這裏咱們只關心LAYOUT_INFLATER_SERVICE對應的服務是如何註冊的

static{
				...
				registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});
        ...
}
複製代碼

正如咱們以前所說,這裏最終是建立了一個PhoneLayoutInflater並返回的,到這裏LayoutInflater的建立流程就分析完了

思考

爲何要交由系統服務來作,而不是直接建立一個PhoneLayoutInflater的實例對象?

LayoutInflater佈局的實例化

總體流程

實例化的調用流程咱們都很熟悉了,調用layoutInflaterinflater()函數,傳入一個xml的resId參數就能夠了

重要函數解析

inflate

這個函數就是咱們把Xml佈局文件實例化爲一個View對象的入口了

LayoutInflater

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); //這段代碼實際上是必然返回null的,由於當前版本寫死了預編譯的Enable爲false
        if (view != null) {
            return view;
        }
        XmlResourceParser parser = res.getLayout(resource); //獲取XmlBlock.Parser對象
        try {
            return inflate(parser, root, attachToRoot); 
        } finally {
            parser.close(); 
        }
    }
複製代碼

此處又調用了inflate(parser, root, attachToRoot)這個函數,來對Xml佈局進行解析

這裏看到對一些熟悉的標籤,好比include,merge,的處理,具體細節請看下面的源碼及註釋

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {//XmlPullParser是一個接口
		//此函數是真正執行將xml解析爲視圖view的過程,此處的parser爲根據xml佈局獲取到的parser對象
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final inflateAttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root; //須要返回的view對象

            try {
                advanceToRootNode(parser); //對START_TAG和END_TAG進行判斷和處理
                final String name = parser.getName();  //獲取當前標籤

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) { //若是使用merge標籤
                    if (root == null || !attachToRoot) { //使用merge標籤必須有父佈局,且依賴於父佈局加載,不然就會拋出異常
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);//遞歸(Recursive)生成佈局視圖
                } else { //若是不使用merge標籤,建立tmp 做爲臨時的根節點,並最終賦值給result返回
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs); ////根據標籤名建立一個view

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {  //若是rootView不爲空
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);  //根據rootView生成layoutparams
                        if (!attachToRoot) { //若是attachToRoot爲false
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);  //設置一個臨時的params
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) { //若是root不爲空,且attachToRoot爲true
                        root.addView(temp, params); //把temp添加到到rootview中
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) { // 若是root爲空且attachToRoot爲false
                        result = temp; //將temp,也就是根結點的View賦值給result
                    }
                }

            } 
						...
            return result;  //返回結果
		}
  }
複製代碼

rInflate

從上面的代碼中咱們也能夠看到,無論是merge標籤,仍是非merge標籤,最終都會調用到rInflate()這個函數,這個是用於遞歸向下遍歷xml佈局,最終調用createViewFromTag()函數來反射建立View對象

具體細節請看下面的源碼及註釋

void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();

            if (TAG_REQUEST_FOCUS.equals(name)) { //若是需REQUEST_FOCUS標籤
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) { //若是是「tag」標籤
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) { //若是是 Include標籤
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs); //對 include標籤進行解析
            } else if (TAG_MERGE.equals(name)) { //若是是merge標籤
                throw new InflateException("<merge /> must be the root element"); //直接拋出異常
            } else { //其餘標籤
                final View view = createViewFromTag(parent, name, context, attrs); //根據Tag建立view,反射建立View
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true); //遞歸調用rInflate函數
                viewGroup.addView(view, params);
            }
        }

        if (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }
複製代碼

createViewFromTag()

終於到了咱們的重頭戲,也是真正根據解析到的Tag標籤去反射建立View的函數

LayoutInflater

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); //嘗試使用Factory來闖將View對象

            if (view == null) { //若是tryCreateView返回null
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
					//sample:com.aiwinn.base.widget.CameraSurfaceView
                    if (-1 == name.indexOf('.')) {  //若是當前Tag含有「.」
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } 
  			...
    }
複製代碼

在這個函數中會首先調用tryCreateView()來獲取View對象,若是爲null,則進一步調用createView()函數來建立View對象

public final View createView(@NonNull Context viewContext, @NonNull String name, @Nullable String prefix, @Nullable AttributeSet attrs) throws ClassNotFoundException, InflateException {
				//根據Tag反射建立view
        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);//把prefix和name進行拼接,獲取到對應的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 {
                ...
            }

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

            try {
                final View view = constructor.newInstance(args); //根據獲取到的構造器建立一個View的實例化對象
                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;
            }catch{
              	...
            }
        } 
    }
複製代碼

這裏的代碼其實在耗時上算是比較重量級了,由於是使用反射來建立的,通常的說法是,反射比直接建立對象要慢3倍,iReaderx2c框架就是基於這一點去作的優化

加餐

Resources的建立和獲取

這裏先獲取到Resources對象--mResources,這個對象的建立是由createResources()來完成的,這裏最終是交由ResourcesManager這個類來獲取對應的resources

ContextImpl

private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName, int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
        final String[] splitResDirs;
        final ClassLoader classLoader;
        try {
            splitResDirs = pi.getSplitPaths(splitName);
            classLoader = pi.getSplitClassLoader(splitName);
        } catch (NameNotFoundException e) {
            throw new RuntimeException(e);
        }
        return ResourcesManager.getInstance().getResources(activityToken,
                pi.getResDir(),
                splitResDirs,
                pi.getOverlayDirs(),
                pi.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfig,
                compatInfo,
                classLoader);
    }
複製代碼

ResourcesManager

public @Nullable Resources getResources(@Nullable IBinder activityToken, @Nullable String resDir, @Nullable String[] splitResDirs, @Nullable String[] overlayDirs, @Nullable String[] libDirs, int displayId, @Nullable Configuration overrideConfig, @NonNull CompatibilityInfo compatInfo, @Nullable ClassLoader classLoader) {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
            final ResourcesKey key = new ResourcesKey(
                    resDir,
                    splitResDirs,
                    overlayDirs,
                    libDirs,
                    displayId,
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);
            classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
            return getOrCreateResources(activityToken, key, classLoader);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }
複製代碼

ContextImpl

@Override
    public Resources getResources() {
        return mResources;
    }
複製代碼

由這裏咱們也能夠推斷,LayoutInflater交由服務來建立來建立,是由於其須要獲取系統服務才能獲取的某些資源

XmlBlock

inflate()函數裏還涉及到一個重要的類, XmlResourceParser,這個類是負責對xml的標籤進行遍歷解析的,它的真正的實現類是XmlBlock的內部類XmlBlock.Parser,而真正完成xml的遍歷操做的函數都是由XmlBlock來實現的,爲了提高效率,該函數都是經過JNI調用native的函數來作的,對應的native層是android_util_XmlBlock.cpp

XmlBlock.java

@FastNative
    /*package*/ static final native int nativeNext(long state);
    @FastNative
    private static final native int nativeGetNamespace(long state);
    @FastNative
    /*package*/ static final native int nativeGetName(long state);
    @FastNative
    private static final native int nativeGetText(long state);
    @FastNative
    private static final native int nativeGetLineNumber(long state);
   	...
複製代碼

``android_util_XmlBlock.cpp`

static jint android_content_XmlBlock_nativeNext(JNIEnv* env, jobject clazz, jlong token) {
    ResXMLParser* st = reinterpret_cast<ResXMLParser*>(token);
    if (st == NULL) {
        return ResXMLParser::END_DOCUMENT;
    }

    do {
        ResXMLParser::event_code_t code = st->next();
        switch (code) {
            case ResXMLParser::START_TAG:
                return 2;
            case ResXMLParser::END_TAG:
                return 3;
            case ResXMLParser::TEXT:
                return 4;
            case ResXMLParser::START_DOCUMENT:
                return 0;
            case ResXMLParser::END_DOCUMENT:
                return 1;
            case ResXMLParser::BAD_DOCUMENT:
                goto bad;
            default:
                break;
        }
    } while (true);

bad:
    jniThrowException(env, "org/xmlpull/v1/XmlPullParserException",
            "Corrupt XML binary file");
    return ResXMLParser::BAD_DOCUMENT;
}
複製代碼

tryInflatePrecompiled

這個函數是Android10的源碼裏面新增的一個函數,是用來根據xml預編譯生成的dex,經過反射來生成對應的View,從而減小XmlPullParser解析Xml的時間 -- 放到編譯期來進行-- 的一個優化 ,而反射獲取對應的View時能夠直接獲取到預編譯的View對象,而不須要遞歸調用rInflate

這裏基本上就是真正完全解決了複雜佈局致使的卡頓問題

View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root, boolean attachToRoot) {
        if (!mUseCompiledView) {
            return null;
        }

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");

        // Try to inflate using a precompiled layout.
        String pkg = res.getResourcePackageName(resource);
        String layout = res.getResourceEntryName(resource);
		
		//依然是經過反射的方式,根據已經建立的mPrecompiledClassLoader來反射生成view對象
        try {
            Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader); //獲取到預編譯生成的view對象的Class類
            Method inflater = clazz.getMethod(layout, Context.class, int.class);
            View view = (View) inflater.invoke(null, mContext, resource);

            if (view != null && root != null) {
                // We were able to use the precompiled inflater, but now we need to do some work to
                // attach the view to the root correctly.
                XmlResourceParser parser = res.getLayout(resource);
                try {
                    AttributeSet attrs = Xml.asAttributeSet(parser);
                    advanceToRootNode(parser);
                    ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);

                    if (attachToRoot) {
                        root.addView(view, params);
                    } else {
                        view.setLayoutParams(params);
                    }
                } finally {
                    parser.close();
                }
            }
            return view;
        } catch (Throwable e) {
            if (DEBUG) {
                Log.e(TAG, "Failed to use precompiled view", e);
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        return null;
    }
複製代碼

寫在最後

下一篇文章,咱們會提出一些優化方案,來解決(或者說)減緩複雜佈局產生的卡頓問題,敬請期待!

參考文章:

1. https://www.reddit.com/r/androiddev/comments/3sekn8/lets_party_like_its_1995_from_the_layoutinflater/
2. https://www.cnblogs.com/liyilin-jack/p/10282385.html
3. https://blog.csdn.net/axi295309066/article/details/60128009
4. https://github.com/RTFSC-Android/RTFSC/blob/master/BlinkLayout.md
5. https://juejin.im/post/5c789b0ce51d454fbd5a8baa
複製代碼

因爲研究過程當中並未記錄全部參考文章,若有疏漏,還請私聊,謝謝

鄭重聲明

本文版權歸Android研習社全部,未經容許禁止轉載,侵權必究!

相關文章
相關標籤/搜索