In the whole View drawing process, it can be roughly divided into two parts from the large direction:

  • Instantiate all views during the Activity’s onCreate life cycle.
  • The Activity’s onResume lifecycle measures, layouts, and draws all views.

The overview

  • Initialization and stratification of activities
  • LayoutInflater principle, and thinking
  • AsyncLayoutInflater principle and defects

Activity onCreate binding

In fact, the Activity life cycle only manages the active state of the Activity object, and does not really manage the View. So how does the Activity manage the View drawing? Let’s look at calling performLaunchActivity in ActivityThread:

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);Copy the code

You can see that in this step, a binding is done to the instantiated Activity. What exactly do you do?

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); . }Copy the code

You can see that in fact the most important work in Attach is instantiating a PhoneWindow object and attaching the current phone-related listener, such as callback for click events, callback for window disappearance, etc.

And bind important information like ActivityThread, ActivityInfo, and Application to the current Activity.

As you can see from the previous column, WMS, the real object hosting the view is actually the Window Window. So what is this Window object that does the first view load?

Instead, we call our familiar API:

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

The PhoneWindow content view area is set up in this API. This is also the first API that every Android developer touches. Because it’s crucial to what Android displays next.

And then it’s easy to think about what the setContentView actually does to initialize all the View objects.

PhoneWindow.setContentView

@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; }Copy the code

Let’s just focus on the core logic here. You can see that when the mContentParent is empty, installDecor is called to generate a parent container, which eventually instantiates all views through another familiar function, layoutInflater.inflate.

Similarly, let’s divide the whole process into two parts:

  • 1. InstallDecor generated DecorView installed in FrameLayout as the top View of all views
  • 2. Instantiate the content passed in by layoutinflater.inflate.

InstallDecor generates DecorView as the top-level View of all Views

private void installDecor() { mForceDecorInstall = false; If (mDecor == null) {// 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)) { ... }}}Copy the code

Let’s pull out the core logic and see what installDecor does is actually quite simple:

  • 1. Generate DecorView generateDecor
  • 2. GenerateLayout gets the content area in the DecorView
  • 3. Look for DecorContentParent in DecorView to handle built-in hanging views like PanelMenu.
  • 4. Handle special animations.

GenerateDecor generated 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()); }Copy the code

You can see it’s very simple. Declare the DecorContext and inject it into the DecorView. If you turn print on while dealing with the famous keyboard memory leak, you’ll see this Context when you switch to another Activity.

The reason a DecorView must have its own Context from the Android perspective is that the DecorView is a service of the system itself, so isolation of the Context is required. Although it is a system service, it is still added to our View.

GenerateLayout gets the content area in the DecorView

protected ViewGroup generateLayout(DecorView decor) { // Apply data from current theme. TypedArray a = getWindowStyle();  // Get all flag bits of the current form... // Do preliminary processing according to the flag bit, such as background.... . 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; }Copy the code

Here’s what it does:

  • 1. Get the form style set in XML and set the corresponding flag bits such as mFloat
  • 2. Get the properties of the DecorView form, further process some background, and reset the properties of the form according to the current Android version, style, for later use.
  • 3. Set the appropriate window resource according to the flag bit set above
  • 4. Obtain the content area in ID_ANDROID_CONTENT.

If we are currently using the plain state, r.layout.screen_simple will be loaded; The resource file goes to the DecorView, and when the current XML resource is instantiated, our content area portion will be found from the DecorView.

Let’s see what screen_simple is:

<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>
Copy the code

You can see that this is a FrameLayout wrapped in a LinearLayout. Set the current View to a Windows contentoverlay property. This property can be set to white transparency or an image after the Activity is generated if the rendering speed is too slow. A ViewStub is used to optimize the display of the ActionBar

StartingWindow is essentially different from startingWindow, which is used to process a form that has not yet entered the Activity, drawing on the screen, and rendering it with the screen pixels that have been saved before. Is a design that optimizes the display experience.

Finally find the content field with the ID content and go back.

This gives us an idea of where the Android display area map comes from.

If we take objects into account, it looks something like this:

Layoutinflater.inflate instantiates all content views

The core code is as follows:

mLayoutInflater.inflate(layoutResID, mContentParent);
Copy the code

In fact, this API is almost as familiar to Android development as it could be. Let’s look at the common uses of layoutinflaters.

    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;
    }
Copy the code

In this case, LayoutInflater is actually a global singleton.

LayoutInflater. Inflate principle

Let’s turn our attention to how inflaters instantiate views:

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

The layoutInflaters we commonly use actually work in three ways, each of which ends up calling the inflate method with three parameters. It is divided into two steps

  • 1. Obtain the XmlResourceParser from Resource first
  • View is instantiated according to the parser.

In other words, to understand how the View instantiates the process, we have to look at how Android gets resources.

The Resource to parse the 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); }}Copy the code

You can see that loadXmlResourceParser is used to tell the bottom layer that a Layout resource is being parsed. You can see that the work is now handed over to ResourcesImpl and AssetManager. This class is familiar to those of you who have read my article. These two classes are the core classes of Java layer-loaded resources. When we do plug-in, it is inevitable to contact this class.

Basically, we divide the steps of resource reading into three parts:

  • 1. Obtain TypedValue
  • 2. Read the resource file to generate an object that holds the XML parsed content
  • 3. Release TypedValue

This step is similar to what we did when we were developing custom views and setting custom attributes. We open the TypeArray via obtainStyledAttributes, read data from it, and then close the TypeArray.

Inflate parses data in the Xml parser

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; }}Copy the code

From here we can see the difference between adding a parent layout and binding a parent layout.

Start by continually searching the XML header (the first “<“) until the first view is found and parsed. Then there are two routes:

  • 1. At this point, the XML layout is merged optimization, calling rInflate. Note that the merge tag is instantiated directly with the LayoutInflate, and set the root layout otherwise an error will be reported.
  • 2. XML is a plain layout that calls rInflateChildren

Let’s take a look at the second unusual case, where there is no merge layout:

  • 1. Instantiate the view with createViewFromTag.
  • GenerLayout = root; generLayout = root; generLayout = root;
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }
Copy the code

Generate LayoutParams that fit the root layout based on the attributes of the XML’s current tag.

AttachToRoot, if true, is added directly to the root layout.

  • 3. RInflateChildren continues parsing the root layout under the current layout, entering recursion.

This explains the difference between the three inflate methods: the inflate method with the root layout parameter generates an adaptive root LayoutParams with the parameters of the current root tag, preserving the root layout property. And the last bool simply means whether or not the system automatically adds it to the root layout for you

And inside there is a core function createViewFromTag, which is the core of how to create a View.

CreateViewFromTag create 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) { ... }}Copy the code

It’s actually quite simple and clever to discover new features in Android.

  • 1. If the tag is view, then the class attribute of the XML tag is obtained directly.
  • 2. If name is blink, create a layout of deep links
  • 3. Intercept view creation with three layers of Factory: Factory, Factory2, and privateFactory.
  • 4. If a user – or system-defined special view generation interception is not generated, the current tag name is judged to have no “.”. “.” A custom view is not described as a system control.
  • 1. The system control calls onCreateView to create a View.
  • Call createView to create a View.

Before we move on, let’s look back at the way systems generate LayoutInflaters and see what’s wrong with creating LayoutInflaters.

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

You can see that the initial instantiation of the system is a PhoneLayoutInflater, not a regular LayoutInflater, and that this class overrides an important method:

Private static final String[] sClassPrefixList = {private static final String[] sClassPrefixList = {android.widget. // 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); }Copy the code

So you can see that I’m manually prefixing the View, and I’m still calling createView to create the View. For example, if it’s a Linearlayout, you’ll add an Android. widget to the tag. Prefix, called android. Widget. Linearlayout. This gives you a relatively complete class name.

CreateView Creates the core action of the View

public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException {// All views corresponding 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); }}Copy the code

In fact, we can see that there is one key data structure in the instantiated View: sConstructorMap.

This data structure holds constructors for all instantiated Views within the app. Instead of reflecting all the time, we store the corresponding constructor with name as the key.

In fact, this code does just that:

  • Select * from name; select * from name
  • 2. If the constructor is not found, use prefix+name to find the constructor and instantiate it.

This explains why LayoutInflaters must be global singletons, simply to speed up view instantiation and share the cache of reflected constructors.

View layout optimization details

  • Of course, you can also see that the ViewStub doesn’t actually do much in it, making a copy of the current Layoutflater and postponing instantiation of the View inside.
  • Merge optimization principle, in fact, the normal branch will directly instantiate a View and then add, then recursive child View to add the current View. Merge, on the other hand, jumps out of the first instantiation View step and goes straight to recursion.

What can MERGE do? Of course I know it’s compressing the hierarchy, right? Typically, you compress the hierarchy of include tags.

So here’s how to compress the hierarchy. In other words, when we merge, we don’t need to add the parent layout at all. We just wrap the child view with the merge tag. When we can’t preview, we add parentTag to the merge.

Here’s an example:

<FrameLayout>
   <include layout="@layout/layout2"/>
</FrameLayout>
Copy the code

layout2.xml:

<merge>
   <TextView />
</merge>
Copy the code

After the merger, it looks like this:

<FrameLayout>
   <TextView />
</FrameLayout>
Copy the code

What about include as the root layout? The android.view.include file will obviously not be found.

RInflate recursively parses the View

Once we’ve generated the View, we need to continue recursing the sub-view, so let’s see what the core logic is.

You can see that the method rInflate is essentially called whether you go through the merge branch or the normal parse branch.

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

In a piece of code that simply loops through the current current XML node parsing the internal tag, until the end of the tag (“/>”) splits into the following branches:

  • 1. If requestFoucs is required, set the current view flag to focus
  • 2. If the label is an include, call parseInclude and parse the layout inside the include.
  • 3. If the tag is a tag, save the tag
  • 4. An error message is displayed when the label is merge
  • 5. Otherwise, the View will be generated as normal and added to the current parent layout.

Finally, the onFinishInflate listener is called back.

Handy use of LayoutInflater Factory

Some of you might wonder what Factory is in LayoutInflater? In fact, this Factory is used everywhere, we just don’t notice it.

Let me give you an example that I’m sure some of you have noticed. When we use AppCompatActivity, if we print or break a point, we will find that the ImageView inside will be replaced with a view that starts with AppCompat, such as AppCompatImageView.

Let’s look at the onCreate method for AppCompatActivity:

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

Actually AppCompatDelegate is essentially AppCompatDelegateImpl, which calls the installViewFactory method.

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

Since we need to override the onCreateView method until Factory, let’s look directly at the method in this class:

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 */ ); }Copy the code

As you can see, it instantiates AppCompatViewInflater and gives it the instantiation View to process.

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; }Copy the code

As you can see, create a different View by name and add the original View to the layout.

What can we learn from the official App pack? We can do a View build interception by doing a Factory here. This idea, in fact, has been used in one of the skin changing frames, which I think is the best school. That’s how it’s designed

Of course, in addition to the App package, there is actually a similar design in the Activity, but specifically for fragments.

Instantiation of the Fragment tag

Remember the attach method from the beginning? One line sets the PrivateFactory for the current LayoutInflater

mWindow.getLayoutInflater().setPrivateFactory(this);
Copy the code

The Factory also has an onCreateView interface, so let’s see what it does:

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); }Copy the code

You can see that the fragment tag is intercepted at the PrivateFactory. Then create the Fragment with mfragments.onCreateView.

It is precisely because the Fragment is not a View that this special handling is required.

Performance optimization of AsyncLayoutInflater

In Android rendering, most of the work is actually done in the UI thread. Let’s think about the work for a moment, and let’s just think about where Java can easily see it. I did reflection, I did measurement layout, rendering, all in one thread. There is a lot of business logic in addition, which makes the UI thread very heavyweight.

In order to solve this problem, both authorities and major manufacturers have made great efforts to optimize the UI rendering speed.

The next AsyncLayoutInflater is the official optimization tool, which is actually an asynchronous LayoutInflater wrapped to reduce the stress on the UI thread. The idea is good, but in fact the API is a bit flawed in design, which makes it a bit weak.

Let’s see how it works. Okay

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); }});Copy the code

And you can see that when you’ve asynchronously instantiated the View, then go setContentView.

Let’s look directly at constructors and instantiation methods:

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); }Copy the code

You can see that each call actually wraps up all the requests that need to be instantiated into the mInflateThread instantiation queue.

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); }}}Copy the code

You can see that the thread’s run method has an infinite loop in which it reads requests from the mQueue and instantiates them again and again. Once the instantiation is complete, the callback is notified of completion via Handler.

We can compare asynClayOutInflaters with regular LayoutInflaters to see what the differences are.

Let’s take a quick look at real LayoutInflaters for instantiation operations

    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);
        }
    }
Copy the code
  • First AsyncLayoutInflater can’t set a Factory, so there’s no way to create AppCompat views or fragments.

  • In addition, AsyncLayoutInflater contains only 10 blocking queues. If you encounter multiple RecyclerView sub-layouts, more than 10 views need to be instantiated, but the main thread blocking is required. You can use thread pool processing

  • If it’s a setContentView, it’s the first step in the UI thread, so it doesn’t have to be asynchronous.

  • Even if the thread is full, you can dump some tasks to the main thread.

  • Also, it does not make sense to write an infinite loop in run for reading instantiation tasks. There are better asynchronous waiting practices, such as the producer-consumer model.

    conclusion

This article summarizes the layerization of an Activity. Essentially, Android is a DecorView that wraps all views in View, and the content area we draw is typically in R.I.D.C tent.

LayoutInflater essentially uses a constructor’s cached map to speed up the reflection of views. The principle of merge compression hierarchy is to bypass the creation of the View and add the inner View directly to the parent layout. Therefore, if the parent layout and outer layer are required by include, There is no need to add an identical layout inside as well.

AsyncLayoutInflater essentially offloads reflection to a dedicated thread. However, the design of its chicken ribs leads to a wide range of scenarios.