Android 重學(xué)系列 View的繪制流程 (一)View的初始化

前言

View的繪制流程這一篇文章其實(shí)十分不好寫,因?yàn)樵诰W(wǎng)上已經(jīng)有千篇一律的文章,導(dǎo)致我一直不太想寫這一篇文章。不過既然是Android重學(xué)系列,還是一步一腳印來分析分析里面的細(xì)節(jié)。如果對這個(gè)流程很熟悉的人來說,本文就沒必要閱讀了。如果不是很熟悉的朋友可以閱讀本文,看看系統(tǒng)上設(shè)計(jì)的優(yōu)點(diǎn)以及可以優(yōu)化的地方。

正文

在整個(gè)View的繪制流程中,從大的方向看來,大致上分為兩部分:

  • 在Activity的onCreate生命周期,實(shí)例化所有的View。
  • Activity的onResume生命周期,測量,布局,繪制所有的View。

暫時(shí)不去看Activity如何聯(lián)通SurfaceFlinger,之后會(huì)有專門的專題再來聊聊。
那么Activity是怎么管理View的繪制的呢?接下來我們會(huì)以上面兩點(diǎn)為線索來分析一下源碼。

不過內(nèi)容很多,本文集中重點(diǎn)聊聊view的實(shí)例化是怎么回事。

總覽

  • Activity的初始化與分層
  • LayoutInflater原理,以及思考
  • AsyncLayoutInflater的原理以及缺陷

Activity onCreate的綁定

其實(shí)Activity的生命周期僅僅只是管理著Activity這個(gè)對象的活躍狀態(tài),并沒有真的去管理View,那么Activity是怎么通過管理View的繪制的呢?我們來看看在ActivityThread調(diào)用performLaunchActivity:
文件:/frameworks/base/core/java/android/app/ActivityThread.java

                Window window = null;
                if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                    window = r.mPendingRemoveWindow;
                    r.mPendingRemoveWindow = null;
                    r.mPendingRemoveWindowManager = null;
                }
                appContext.setOuterContext(activity);
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);

能看到在這個(gè)步驟中,對著實(shí)例化的Activity做了一次綁定操作。具體做什么呢?

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
...
        mUiThread = Thread.currentThread();

        mMainThread = aThread;
        mInstrumentation = instr;
        mToken = token;
        mIdent = ident;
        mApplication = application;
        mIntent = intent;
        mReferrer = referrer;
        mComponent = intent.getComponent();
        mActivityInfo = info;
....
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;

        mWindow.setColorMode(info.colorMode);

...
    }

能看到在attach中實(shí)際上最為重要的工作就是實(shí)例化一個(gè)PhoneWindow對象,并且把當(dāng)前Phone相關(guān)的監(jiān)聽,如點(diǎn)擊事件的回調(diào),窗體消失的回調(diào)等等。

并且把ActivityThread,ActivityInfo,Application等重要的信息綁定到當(dāng)前的Activity。

從上一個(gè)專欄WMS,就能知道,實(shí)際上承載視圖真正的對象實(shí)際上是Window窗口。那么這個(gè)Window對象又是什么做第一次的視圖加載呢?

其實(shí)是調(diào)用了我們及其熟悉的api:

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

在這個(gè)api中設(shè)置了PhoneWindow的內(nèi)容視圖區(qū)域。這也是每一個(gè)Android開發(fā)的接觸到的第一個(gè)api。因?yàn)槠渲陵P(guān)重要承載了Android接下來要顯示什么內(nèi)容。

接下來我們很容易想到setContentView究竟做了什么事情,來初始化所有的View對象。

PhoneWindow.setContentView

文件:/frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
...
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
....
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

我們這里只關(guān)注核心邏輯。能看到當(dāng)mContentParent為空的時(shí)候,會(huì)調(diào)用installDecor生成一個(gè)父容器,最終會(huì)通過我們另一個(gè)熟悉的函數(shù)LayoutInflater.inflate把所有的View都實(shí)例化出來。

那么同理,我們把整個(gè)步驟分為2部分:

  • 1.installDecor生成DecorView安裝在FrameLayout作為所有View的頂層View
  • 2.LayoutInflater.inflate 實(shí)例化傳進(jìn)來的內(nèi)容。

installDecor生成DecorView作為所有View的頂層View

    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
//核心事件1
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
//核心事件二
            mContentParent = generateLayout(mDecor);

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
...
            } else {
...
            }

            if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
                mDecor.setBackgroundFallback(mBackgroundFallbackResource);
            }

            // Only inflate or create a new TransitionManager if the caller hasn't
            // already set a custom one.
            if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
...
            }
        }
    }

我們抽出核心邏輯看看這個(gè)installDecor做的事情實(shí)際上很簡單:

  • 1.generateDecor生成DecorView
  • 2.generateLayout 獲取DecorView中的內(nèi)容區(qū)域
  • 3.尋找DecorView中的DecorContentParent處理PanelMenu等系統(tǒng)內(nèi)置的掛在view。
  • 4.處理專場動(dòng)畫。

我們只需要把關(guān)注點(diǎn)放在頭兩項(xiàng)。這兩個(gè)才是本文的重點(diǎn)。第四項(xiàng)的處理實(shí)際上是處理

generateDecor生成DecorView

    protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don't cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext().getResources());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
    }

能看到里面十分簡單,聲明DecorContext,注入到DecorView。如果處理著名的鍵盤內(nèi)存泄漏的時(shí)候,把打印打開,當(dāng)切換到另一個(gè)Activity的時(shí)候,就會(huì)看到這個(gè)這個(gè)Context。

在Android系統(tǒng)看來DecorView必須擁有自己的Context的原因是,DecorView是系統(tǒng)自己的服務(wù),因此需要做Context的隔離。不過雖然是系統(tǒng)服務(wù),但是還是添加到我們的View當(dāng)中。

generateLayout 獲取DecorView中的內(nèi)容區(qū)域

 protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.

        TypedArray a = getWindowStyle();
//獲取當(dāng)前窗體所有的標(biāo)志位
   ...
//根據(jù)標(biāo)志位做初步處理,如背景
  ....

 ...

        // 根據(jù)標(biāo)志位設(shè)置資源id
        int layoutResource;
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
      ...
        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
 ...
        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
   ...
        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
...
        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
...
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        }

        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

 ...

        return contentParent;
    }

這里做的事情如下:

  • 1.獲取設(shè)置在xml中的窗體style,設(shè)置相應(yīng)的標(biāo)志位如mFloat等
  • 2.獲取DecorView窗體的屬性,進(jìn)一步處理一些背景,根據(jù)當(dāng)前的Android版本,style重新設(shè)置窗體的屬性,以供后面使用。
  • 3.根據(jù)上面設(shè)置的標(biāo)志位設(shè)置合適的窗體資源
  • 4.獲取ID_ANDROID_CONTENT中的內(nèi)容區(qū)域。

假如,我們當(dāng)前使用的是最普通的狀態(tài),將會(huì)加載R.layout.screen_simple;資源文件到DecorView中,當(dāng)實(shí)例化好當(dāng)前的xml資源之后,將會(huì)從DecorView找到我們的內(nèi)容區(qū)域部分。

我們看看screen_simple是什么東西:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

能看到這是一個(gè)LinearLayout包裹著的一個(gè)FrameLayout。把當(dāng)前的View設(shè)置一個(gè)windowContentOverlay屬性。這個(gè)屬性可以在Activity生成之后,渲染速度太慢可以設(shè)置成白色透明或者圖片。一個(gè)ViewStub用來優(yōu)化顯示actionbar

和startingWindow有本質(zhì)上的區(qū)別,startingWindow的出現(xiàn)是為了處理還沒有進(jìn)入Activity,繪制在屏幕的窗體,同時(shí)還能獲取之前保留下來的屏幕像素渲染在上面。是一個(gè)優(yōu)化顯示體驗(yàn)的設(shè)計(jì)。

最后找到這個(gè)id為content的內(nèi)容區(qū)域返回回去。

這樣我們就知道網(wǎng)上那個(gè)Android的顯示區(qū)域劃分圖是怎么來的。


Android顯示View的構(gòu)成.jpeg

如果我們把對象考慮進(jìn)來大致上是如此:


對象包含圖.jpeg

LayoutInflater.inflate實(shí)例化所有內(nèi)容視圖

核心代碼如下:

mLayoutInflater.inflate(layoutResID, mContentParent);

實(shí)際上對于Android開發(fā)來說,這個(gè)api也熟悉的不能再熟悉了。我們看看LayoutInflater常用的用法。

    public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

可以去結(jié)合我上一個(gè)專欄的WMS的getSystemService分析。實(shí)際上在這里面LayoutInflater是一個(gè)全局單例。為什么一定要設(shè)計(jì)為單例這是有原因的??吹胶竺婢椭罏槭裁戳?。

LayoutInflater.inflate原理

接下來我們把注意力轉(zhuǎn)移到inflater如何實(shí)例化view上面:

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

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
    }

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

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

我們常用的LayoutInflater其實(shí)有三種方式,最后都會(huì)調(diào)用三個(gè)參數(shù)的inflate方法。
分為2個(gè)步驟

  • 1.首先先通過Resource獲取XmlResourceParser的解析器
  • 2.inflate按照解析器實(shí)例化View。

換句話說,為了弄清楚View怎么實(shí)例化這個(gè)流程,我們必須看Android是怎么獲取資源的。

Resource解析xml

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

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

能看到此時(shí)是通過loadXmlResourceParser來告訴底層解析的是Layout資源。能看到此時(shí)會(huì)把工作交給ResourcesImpl和AssetManager去完成。這個(gè)類如果看過我的文章的朋友就很熟悉了,這兩個(gè)類就是Java層加載資源的核心類,當(dāng)我們做插件化的時(shí)候,是不可避免的接觸這個(gè)類。

經(jīng)過的方法,大致上我們把資源讀取的步驟分為3部分:

  • 1.獲取TypedValue
  • 2.讀取資源文件,生成保存著xml解析內(nèi)容的對象
  • 3.釋放掉TypedValue

這個(gè)步驟和我們平時(shí)開發(fā)自定義View設(shè)置自定義屬性的時(shí)候何其相似,都是通過obtainStyledAttributes打開TypeArray,讀取其中的數(shù)據(jù),最后關(guān)閉TypeArray。

這背后隱藏這什么玄機(jī)呢?我們之后會(huì)有文章專門探索,我們不要打斷當(dāng)前的思緒。

inflate解析Xml解析器中的數(shù)據(jù)

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }
                final String name = parser.getName();
...
                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);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            temp.setLayoutParams(params);
                        }
                    }
                    rInflateChildren(parser, temp, attrs, true);

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
...
            } catch (Exception e) {
...
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

從這我們就能看到是否添加父布局以及是否綁定父布局之間的差別了。

首先不斷的從xml的頭部開始查找(第一個(gè)“<”),直到找到第一個(gè)view的進(jìn)行解析。接下來就分為兩個(gè)路線:

  • 1.此時(shí)xml布局使用了merge優(yōu)化,調(diào)用rInflate。請注意直接使用LayoutInflate實(shí)例化merge標(biāo)簽,請?jiān)O(shè)置根布局不然會(huì)報(bào)錯(cuò)。
  • 2.xml是普通的布局,調(diào)用rInflateChildren

先來看看第二種不普通情況,在沒有merge布局的情況:

  • 1.先通過createViewFromTag實(shí)例化對應(yīng)的view。
  • 2.接著根據(jù)查看當(dāng)前有沒有root需要綁定的父布局,如果有,則獲取根布局的generLayout:
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

根據(jù)xml當(dāng)前標(biāo)簽的屬性生成適配根布局的LayoutParams。

attachToRoot如果為true,則會(huì)直接添加到根布局中。

  • 3.rInflateChildren繼續(xù)解析當(dāng)前布局下的根布局,進(jìn)入遞歸。

這也解釋了三個(gè)inflate方法之間的區(qū)別,帶著根部布局參數(shù)的inflate能夠?qū)?dāng)前根部的標(biāo)簽的參數(shù)生成一個(gè)適配根部LayoutParams,也就保留了根部布局的屬性。而最后一個(gè)bool僅僅是代表用不用系統(tǒng)自動(dòng)幫你添加到根布局中

在這里面有一個(gè)核心的函數(shù)createViewFromTag,這是就是如何創(chuàng)建View的核心。

createViewFromTag創(chuàng)建View

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

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

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

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

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

            return view;
        } catch (InflateException e) {
...
        } catch (ClassNotFoundException e) {
...
        } catch (Exception e) {
...
        }
    }

實(shí)際上這里面很簡單和很巧妙,也能發(fā)現(xiàn)Android 中的新特性。

  • 1.如果標(biāo)簽直接是view,則直接獲取xml標(biāo)簽中系統(tǒng)中class的屬性,這樣就能找到view對應(yīng)的類名。

  • 2.如果name是blink,則創(chuàng)建一個(gè)深度鏈接的布局

  • 3.通過三層的Factory攔截view的創(chuàng)建,分別是Factory,F(xiàn)actory2,privateFactory。

  • 4.如果經(jīng)過上層由用戶或者系統(tǒng)定義的特殊view的生成攔截沒有生成,則會(huì)判斷當(dāng)前的標(biāo)簽名又沒有"."。有“.”說明是自定義view,沒有說明是系統(tǒng)控件。

  • 1.系統(tǒng)控件調(diào)用onCreateView創(chuàng)建View。

  • 2.自定義View調(diào)用createView創(chuàng)建View。

在繼續(xù)下一步之前,讓我們把目光放回系統(tǒng)生成LayoutInflater的方法中,看看生成LayoutInflater有什么貓膩?
文件:/frameworks/base/core/java/android/app/SystemServiceRegistry.java

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

能看到系統(tǒng)初期實(shí)例化的是一個(gè)PhoneLayoutInflater,并非是一個(gè)普通的LayoutInflater,而這個(gè)類重載了一個(gè)很重要的方法:
文件:http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/com/android/internal/policy/PhoneLayoutInflater.java

    private static final String[] sClassPrefixList = {
//常用控件
        "android.widget.",
//一般指WebView
        "android.webkit.",
//一般指Fragment
        "android.app."
    };

    @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }

        return super.onCreateView(name, attrs);
    }

能看到這樣就手動(dòng)為View添加了前綴,還是調(diào)用了createView創(chuàng)建View。舉個(gè)例子,如果是一個(gè)Linearlayout,就會(huì)為這個(gè)標(biāo)簽添加android.widget.前綴,稱為android.widget.Linearlayout。這樣就能找到相對完整的類名。

createView創(chuàng)建View的核心動(dòng)作

    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
//所有view對應(yīng)構(gòu)造函數(shù)
        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 = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);

                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, 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 = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);

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

            Object lastContext = mConstructorArgs[0];
            if (mConstructorArgs[0] == null) {
                // Fill in the context if not already within inflation.
                mConstructorArgs[0] = mContext;
            }
            Object[] args = mConstructorArgs;
            args[1] = attrs;

            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]));
            }
            mConstructorArgs[0] = lastContext;
            return view;

        } catch (NoSuchMethodException e) {
...
        } catch (ClassCastException e) {
...
        } catch (ClassNotFoundException e) {
...
        } catch (Exception e) {
...
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

實(shí)際上我們能夠看到在實(shí)例化View有一個(gè)關(guān)鍵的數(shù)據(jù)結(jié)構(gòu):sConstructorMap.

這個(gè)數(shù)據(jù)結(jié)構(gòu)保存著app應(yīng)用內(nèi)所有實(shí)例化過的View對應(yīng)的構(gòu)造函數(shù)。主要經(jīng)過存儲(chǔ)一次,就以name為key存儲(chǔ)對應(yīng)的構(gòu)造函數(shù),就不用一直反射獲取了。

實(shí)際上這段代碼就是做這個(gè)事情:

  • 1.當(dāng)從name找構(gòu)造函數(shù),找的到,就直接通過構(gòu)造函數(shù)實(shí)例化
  • 2.沒有找到構(gòu)造函數(shù),則通過prefix+name的方式查找類的構(gòu)造函數(shù),接著再實(shí)例化。

這也解釋了為什么LayoutInflater一定要做成全局單例的方式,原因很簡單就是為了加速view實(shí)例化的過程,共用反射的構(gòu)造函數(shù)的緩存。

View布局優(yōu)化細(xì)節(jié)

  • 當(dāng)然也能看到ViewStub實(shí)際上在這里面沒有做太多事情,把當(dāng)前的Layoutflater復(fù)制一份進(jìn)去,延后實(shí)例化里面的View。
  • merge優(yōu)化原理,也能明白了,實(shí)際上在正常的分支會(huì)直接實(shí)例化一個(gè)View之后再添加,接著遞歸子view繼續(xù)添加當(dāng)前的View。而merge則是跳出第一次實(shí)例化View步驟,直接進(jìn)入遞歸。

merge能做到什么事情呢?我當(dāng)然知道是壓縮層級?一般是壓縮include標(biāo)簽的層級。

這里就解釋了怎么壓縮層級。換句話說,我們使用merge的時(shí)候完全不用添加父布局,直接用merge標(biāo)簽包裹子view即可,當(dāng)我們不能預(yù)覽,在merge上添加parentTag即可。

舉個(gè)例子:

<FrameLayout>
   <include layout="@layout/layout2"/>
</FrameLayout>

layout2.xml:

<merge>
   <TextView />
</merge>

合并之后就是如下:

<FrameLayout>
   <TextView />
</FrameLayout>

如果把include作為根布局呢?很明顯會(huì)找不到android.view.include這個(gè)文件。

rInflate遞歸解析View

當(dāng)我們生成View之后,就需要繼續(xù)遞歸子View,讓我們看看其核心邏輯是什么。

能看到無論是是走merge分支還是正常解析分支也好,本質(zhì)上都會(huì)調(diào)用到rInflate這個(gè)方法。

    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

    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)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }

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

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

在一段代碼就是不斷循環(huán)當(dāng)前的當(dāng)前xml的節(jié)點(diǎn)解析內(nèi)部的標(biāo)簽,直到到了標(biāo)簽的末尾("/>")
分為如下幾個(gè)分支:

  • 1.如果發(fā)現(xiàn)需要requestFoucs,則設(shè)置當(dāng)前view標(biāo)志為聚焦
  • 2.如果發(fā)現(xiàn)標(biāo)簽是include,則調(diào)用parseInclude,解開include內(nèi)部的layout進(jìn)行解析。
  • 3.標(biāo)簽是tag,則保存tag中的內(nèi)容
  • 4.發(fā)現(xiàn)標(biāo)簽是merge則報(bào)錯(cuò)
  • 5.其他情況,如正常一般,會(huì)正常生成View,并且添加當(dāng)前的父布局中。

最后會(huì)回調(diào)onFinishInflate監(jiān)聽。

LayoutInflater Factory的妙用

或許有人會(huì)覺得LayoutInflater中設(shè)置Factory干什么的?實(shí)際上這個(gè)Factory到處有在用,只是我們沒有注意到過而已。

舉一個(gè)例子,肯定有人注意過吧。當(dāng)我們使用AppCompatActivity的時(shí)候,如果打印或者打斷點(diǎn),會(huì)發(fā)現(xiàn)內(nèi)部的ImageView這些view,會(huì)被替換掉,變成AppCompat開頭的view,如AppCompatImageView。

我們來看看AppCompatActivity的onCreate方法:

  @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
}

實(shí)際上AppCompatDelegate本質(zhì)上是AppCompatDelegateImpl,在這個(gè)類中調(diào)用了installViewFactory的方法。

    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
...
        }
    }

既然我們直到Factory需要重寫onCreateView的方法,我們直接看看這個(gè)類中的方法:

    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        if (mAppCompatViewInflater == null) {
            TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
            String viewInflaterClassName =
                    a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
            if ((viewInflaterClassName == null)
                    || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
...
                mAppCompatViewInflater = new AppCompatViewInflater();
            } else {
                try {
                    Class viewInflaterClass = Class.forName(viewInflaterClassName);
                    mAppCompatViewInflater =
                            (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                                    .newInstance();
                } catch (Throwable t) {
..
                    mAppCompatViewInflater = new AppCompatViewInflater();
                }
            }
        }

...
        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
                true, /* Read read app:theme as a fallback at all times for legacy reasons */
                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
        );
    }

能看到此時(shí),會(huì)實(shí)例化AppCompatViewInflater,把實(shí)例化View交給它來處理。

AppCompatViewInflater.createView

    final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        final Context originalContext = context;

        // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
        // by using the parent's context
        if (inheritContext && parent != null) {
            context = parent.getContext();
        }
        if (readAndroidTheme || readAppTheme) {
            // We then apply the theme on the context, if specified
            context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
        }
        if (wrapContext) {
            context = TintContextWrapper.wrap(context);
        }

        View view = null;

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        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;
            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);
        }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check its android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

能看到吧,此時(shí)把根據(jù)name去生成不同的View,這樣就替換原來的View加入到布局中。

從官方的App包我們能夠得到什么啟發(fā)?我們可以通過在這里做一個(gè)Factory,做一次View的生成攔截。實(shí)際上這種思路,已經(jīng)被用于換膚框架中,其中一種,且個(gè)人認(rèn)為最好的流派。就是這樣設(shè)計(jì)的

當(dāng)然除了App包有這種操作,實(shí)際上在Activity里面也有一樣的設(shè)計(jì),不過是專門針對Fragment的。

Fragment標(biāo)簽的實(shí)例化

還記得開篇的attach方法嗎?其中一行設(shè)置了當(dāng)前LayoutInflater的PrivateFactory

mWindow.getLayoutInflater().setPrivateFactory(this);

這個(gè)Factory也是有一個(gè)onCreateView的接口,我們直接看看做了什么:

    public View onCreateView(@Nullable View parent, @NonNull String name,
            @NonNull Context context, @NonNull AttributeSet attrs) {
        if (!"fragment".equals(name)) {
            return onCreateView(name, context, attrs);
        }

        return mFragments.onCreateView(parent, name, context, attrs);
    }

能看到在PrivateFactory攔截的正是fragment標(biāo)簽。接著就通過mFragments.onCreateView創(chuàng)建Fragment。關(guān)于Fragment,我之后專門用一篇文章聊聊。

正是因?yàn)镕ragment不是一個(gè)View,因此才需要做這種特殊處理。

AsyncLayoutInflater的性能優(yōu)化

在Android的渲染中,其實(shí)大部分的事情都在ui線程中完成。我們稍微思考其中的工作,我們暫時(shí)只考慮Java可以輕易看見的地方。做了反射,做了測量布局,渲染,都在一個(gè)線程中。除此之外還有很多業(yè)務(wù)邏輯,這樣會(huì)導(dǎo)致ui線程十分重量級。

為了解決這個(gè)問題,官方也好,各大廠商也好,都做十分巨大的努力去優(yōu)化這個(gè)ui渲染速度。

接下來AsyncLayoutInflater就是官方提供優(yōu)化工具,實(shí)際上這是一個(gè)封裝好的異步LayoutInflater,這樣就能降低ui線程的壓力。想法是好的,實(shí)際上不過這個(gè)api設(shè)計(jì)上有點(diǎn)缺陷,導(dǎo)致有點(diǎn)雞肋。

我們看看怎么使用

        new AsyncLayoutInflater(Activity.this)
                .inflate(R.layout.async_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
                    @Override
                    public void onInflateFinished(View view, int resid, ViewGroup parent) {
                        setContentView(view);
                    }
                });

能看到當(dāng)異步實(shí)例化好View之后,再去setContentView。

我們直接看看構(gòu)造函數(shù),以及實(shí)例化的方法:

 public AsyncLayoutInflater(@NonNull Context context) {
        mInflater = new BasicInflater(context);
        mHandler = new Handler(mHandlerCallback);
        mInflateThread = InflateThread.getInstance();
    }

    @UiThread
    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
            @NonNull OnInflateFinishedListener callback) {
        if (callback == null) {
            throw new NullPointerException("callback argument may not be null!");
        }
        InflateRequest request = mInflateThread.obtainRequest();
        request.inflater = this;
        request.resid = resid;
        request.parent = parent;
        request.callback = callback;
        mInflateThread.enqueue(request);
    }

能看到實(shí)際上每一次調(diào)用,都會(huì)把一個(gè)所有需要實(shí)例化的request封裝起來,丟進(jìn)mInflateThread實(shí)例化隊(duì)列中。

private static class InflateThread extends Thread {
        private static final InflateThread sInstance;
        static {
            sInstance = new InflateThread();
            sInstance.start();
        }

        public static InflateThread getInstance() {
            return sInstance;
        }

        private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
        private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);

        // Extracted to its own method to ensure locals have a constrained liveness
        // scope by the GC. This is needed to avoid keeping previous request references
        // alive for an indeterminate amount of time, see b/33158143 for details
        public void runInner() {
            InflateRequest request;
            try {
                request = mQueue.take();
            } catch (InterruptedException ex) {
                // Odd, just continue
                Log.w(TAG, ex);
                return;
            }

            try {
                request.view = request.inflater.mInflater.inflate(
                        request.resid, request.parent, false);
            } catch (RuntimeException ex) {
                // Probably a Looper failure, retry on the UI thread
                Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
                        + " thread", ex);
            }
            Message.obtain(request.inflater.mHandler, 0, request)
                    .sendToTarget();
        }

        @Override
        public void run() {
            while (true) {
                runInner();
            }
        }

        public InflateRequest obtainRequest() {
            InflateRequest obj = mRequestPool.acquire();
            if (obj == null) {
                obj = new InflateRequest();
            }
            return obj;
        }

        public void releaseRequest(InflateRequest obj) {
            obj.callback = null;
            obj.inflater = null;
            obj.parent = null;
            obj.resid = 0;
            obj.view = null;
            mRequestPool.release(obj);
        }

        public void enqueue(InflateRequest request) {
            try {
                mQueue.put(request);
            } catch (InterruptedException e) {
                throw new RuntimeException(
                        "Failed to enqueue async inflate request", e);
            }
        }
    }

能看到這個(gè)線程的run方法中是一個(gè)死循環(huán),在這個(gè)死循環(huán)里面會(huì)不斷讀取mQueue的的請求,進(jìn)行一次次的實(shí)例化。一旦實(shí)例化完成之后,將會(huì)通過Handler通知回調(diào)完成。

通過AsyncLayoutInflater和正常的LayoutInflater比較就能清楚,雙方的差異是什么?

我們稍微瀏覽一下用于實(shí)例化操作真正的LayoutInflater

    private static class BasicInflater extends LayoutInflater {
        private static final String[] sClassPrefixList = {
            "android.widget.",
            "android.webkit.",
            "android.app."
        };

        BasicInflater(Context context) {
            super(context);
        }

        @Override
        public LayoutInflater cloneInContext(Context newContext) {
            return new BasicInflater(newContext);
        }

        @Override
        protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
            for (String prefix : sClassPrefixList) {
                try {
                    View view = createView(name, prefix, attrs);
                    if (view != null) {
                        return view;
                    }
                } catch (ClassNotFoundException e) {
                    // In this case we want to let the base class take a crack
                    // at it.
                }
            }

            return super.onCreateView(name, attrs);
        }
    }
  • 首先AsyncLayoutInflater不能設(shè)置Factory,那么也就沒辦法創(chuàng)建AppCompat系列的View,也沒有辦法創(chuàng)建Fragment。
  • 其次AsyncLayoutInflater包含的阻塞隊(duì)列居然只有10個(gè),如果遇到RecyclerView這種多個(gè)子布局,超過了10個(gè)需要實(shí)例化的View,反而需要主線程的阻塞??梢允褂镁€程池子處理
  • 如果是setContentView的話,本身就是處于ui線程的第一步,也就沒有必要異步。
  • 甚至如果線程工作滿了,可以把部分任務(wù)丟給主線程處理。
  • 而且在run中寫一個(gè)死循環(huán)進(jìn)行讀取實(shí)例化任務(wù),不合理。完全有更好的異步等到做法,如生產(chǎn)者消費(fèi)者模式。

處理這幾個(gè)問題確實(shí)不難,閱讀過源碼當(dāng)然知道怎么處理,這里就不繼續(xù)贅述了。

總結(jié)

本文總結(jié)了Activity的分層,本質(zhì)上android從視圖上來看,是一個(gè)DecorView包裹所有的View,我們繪制的內(nèi)容區(qū)域一般在R.id.content中。

LayoutInflater本質(zhì)上是通過一個(gè)構(gòu)造函數(shù)的緩存map來加速反射View的速度,同時(shí)merge壓縮層級原理就是越過本次View的生成,將內(nèi)部的view生成出來直接添加到父布局,因此如果include需要的父布局和外層一直,就沒有必要在內(nèi)部也添加一個(gè)一模一樣的布局。

AsyncLayoutInflater本質(zhì)上就是把反射的工作丟給一個(gè)專門的線程去處理。但是其雞肋的設(shè)計(jì)導(dǎo)致使用場景不廣泛。

下一篇將和大家聊聊資源是怎么加載到我們的App的,把本篇遺留的問題解決了。

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

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

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