Source code analysis | layout file loading process

Posted by miniu on Tue, 15 Feb 2022 02:48:15 +0100

setContentView in Activity

    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar(); 
}

The setContentView method is as follows. It calls setContentView in the window, but the setContentView in the window is only an abstract method:

public abstract void setContentView(View view, ViewGroup.LayoutParams params);

At the beginning of the window class, it was also said that the only implementation class of window is PhoneWindow. Therefore, setContentView in PhoneWindow is called here, as follows:

public void setContentView(int layoutResID) {
   	//If the parent container is null, it will be created
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        //Load the incoming resource Id onto the mContentParent
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            // return new DecorView(context, featureId, this, getAttributes());
            //Finally, a DecorView will be created and assigned to mDecor
            mDecor = generateDecor(-1);
            //.....
        } else {
            mDecor.setWindow(this);
        }
    	//mContentParent is a 	 VeiwGroup
        if (mContentParent == null) {
            //Finally, a resource file will be loaded according to the current window style and loaded into mDecor,
            //And return the ViewGroup with id: @ android:id/content in the resource file, and the type is FrameLayout
            mContentParent = generateLayout(mDecor);
        }
    }


protected ViewGroup generateLayout(DecorView decor) {
        // Get data from the current topic
        TypedArray a = getWindowStyle();
		//.......
    
        // Decorate decor, judge the properties of the current window, and add the qualified layout to decor

        int layoutResource;
        int features = getLocalFeatures();
      
        else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
           
            layoutResource = R.layout.screen_progress;
        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
                //load resources
                layoutResource = R.layout.screen_custom_title;
        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
                layoutResource = res.resourceId;
                layoutResource = R.layout.screen_title;
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
            layoutResource = R.layout.screen_simple;
        }

        mDecor.startChanging();
    	//Call mDecor's method and load the system layout file just found into DecorView
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    	// Id_ ANDROID_ The Id of content is the Id in the resource file
    	//Click findViewById and you can see that the View used inside is DecorView 
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
		//......
        mDecor.finishChanging();

        return contentParent;
    }

In the setContentView method, installDecor() is called, and this method is analyzed below.

First, a DecorView will be created, which is a View that inherits the child FrameLayout. That is, the Window class contains a DecorView.

Then it will judge whether mContentParent is null. If it is null, it will call generateLayout to create it. mContentParent is a ViewGroup. In generateLayout, system resources will be called to judge the current window mode of the system. Then load the corresponding layout. Finally, the resource file will be loaded into DecorView. And it will call findViewById to find a ViewGroup and return it. Click findViewById to see that the View used inside is DecorView. The id loaded is as follows:

Generally, the framelayout View is included in the loaded resource layout, and you can see that the id is @ android:id/content. You can copy the layout name and search globally to see the layout.

The layout is as follows:

    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>

So far, I've seen the more important parts of the installDecor() method. Sort it out:

Create a DecorView in setContentView(), and then obtain a resource layout according to the type of system window. Get the resource in the @ byviewid: Android control file and load it into the @ byviewid: Android control file.

Later errata: in the second box: DecorView is inherited from FrameLayout

In the setContentView method, after calling the installDecor() method, there is still a very important sentence.

mLayoutInflater.inflate(layoutResID, mContentParent);

This layoutResID is passed in when calling setContentView. This resource is loaded onto mContentParent here. Through the above analysis, we can know that contentParent is the Framelayout layout with id @ android:id/content in DecorView.

Later errata: in the second box: DecorView is inherited from FrameLayout

The final general logic is shown in the figure above

We can do a test to see if there are any problems in our analysis:

        super.onCreate(savedInstanceState)
//        setContentView(layout())

        val view = LayoutInflater.from(this).inflate(layout(), null)
        val decorView = window.decorView
        val frameLayout = decorView.findViewById<FrameLayout>(android.R.id.content)
        frameLayout.addView(view)

}

This is the onCreate method of an activity. I annotated the setContentView method and directly loaded a layout. Then call decorView in window. Get the frameLayout and add the layout.

The effect of the final operation display is normal.

Here is a diagram that clearly shows the process of layout loading

setContentView in AppCompatActivity

In fact, there are some differences compared with setContentView of Activity. It is mainly to be compatible with some things of lower versions.

Next, let's take a look at the source code

public void setContentView(View view, ViewGroup.LayoutParams params) {
    getDelegate().setContentView(view, params);
}
@NonNull
public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
        return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}

setContentView called here is also an abstract method, and the final call is in AppCompatDelegateImpl

public void setContentView(View v) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) 		        			 	mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    contentParent.addView(v);
    mOriginalWindowCallback.onContentChanged();
}
private void ensureSubDecor() {
        if (!mSubDecorInstalled) {
            mSubDecor = createSubDecor();
        }
}
private ViewGroup createSubDecor() {
        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

        final LayoutInflater inflater = LayoutInflater.from(mContext);
    	//Empty ViewGroup
        ViewGroup subDecor = null;
    
    	//According to a series of judgments, finally load a layout to the ViewGroup
        if (!mWindowNoTitle) {
                subDecor = (ViewGroup) inflater.inflate(
                        R.layout.abc_dialog_title_material, null);
            } else if (mHasActionBar) {
              
                subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                        .inflate(R.layout.abc_screen_toolbar, null);
        } else {
            if (mOverlayActionMode) {
                subDecor = (ViewGroup) inflater.inflate(
                        R.layout.abc_screen_simple_overlay_action_mode, null);
            } else {
                subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
            }
        }
    
    //Gets the id in the subdicor
     final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);
		
    //The DecorView in Window is obtained here 
        final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        if (windowContentView != null) {
            //Set an empty Id
            windowContentView.setId(View.NO_ID);
            //Set a new id for contentView
            contentView.setId(android.R.id.content);
        }

		//Finally, the setContentView method in the window is called
        // Now set the Window's content view with the decor
        mWindow.setContentView(subDecor);

        return subDecor;
    }

Looking at the process, you can find that the setContentView in the window is finally called, but there is an additional layer here

He will first create a ViewGroup and then add a layout based on a series of judgments. Finally, call the setContentView of window. Then we will add the layout we passed in to the ViewGroup, compared with the Activity. He has an additional ViewGroup.

Compatibility of AppCompatActivity

A small example: create an activity and create an ImageView in the layout.

    android:id="@+id/test_iv"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:layout_marginTop="50dp"
    android:src="@drawable/image"/>

Then let the activity inherit the activity first, and finally AppCompatActivity, and then print the ImageView

android.widget.ImageView
androidx.appcompat.widget.AppCompatImageView

It is found that if it inherits from AppcompatActivity, iamgeView finally creates AppCompatImageView, which is a compatible ImageView. Why? Let's analyze the source code:

Source code analysis:

First, a very important method is invoked in the onCreate method of AppCompatActivity, which is as follows:

protected void onCreate(@Nullable Bundle savedInstanceState) {
    final AppCompatDelegate delegate = getDelegate();
    //The factory where View is installed. The delegate here is AppCompatDelegateImpl 
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
    super.onCreate(savedInstanceState);
}

Appcompartdelegateimpl implements an interface layouteinflator Factory2

public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        //Set the Factory of layoutinflator to this, that is, creating a View will lose its onCreateView method
        //If you don't understand it, take a look at the source code of LayoutInflater, LayoutInflater From (mcontext) is actually a singleton
        //If Factory is set, the onCreateView method will be executed first each time a View is created
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}

LayoutInflater.from(mContext) is actually a singleton. He will set the Factory of layoutinflator to this.

When we're using this: layoutinflator from(this). When inputting (layout(), parent) code, it will be called into the AppCompatDelegateImpl class to implement layouteinflator onCreateView method of factory2,

	@UnsupportedAppUsage(trackingBug = 122360734)
	@Nullable
	public final View tryCreateView(@Nullable View parent, @NonNull String name,
	    @NonNull Context context,
	    @NonNull AttributeSet attrs) {
 	   if (name.equals(TAG_1995)) {
    	    // Let's party like it's 1995!
   	 }

  	  View view;
  	  if (mFactory2 != null) {
        //Here, layoutinflator from(this). Inflate (layout(), null). If AppCompatActivity is used, a value will be set for mFactory2. Finally, onCreateView in AppCompatDelegateImpl will be called here.
  	      view = mFactory2.onCreateView(parent, name, context, attrs);
 	   } else if (mFactory != null) {
 	       view = mFactory.onCreateView(name, context, attrs);
    	} else {
    	    view = null;
   		 }

  	  if (view == null && mPrivateFactory != null) {
  	      view = mPrivateFactory.onCreateView(parent, name, context, attrs);
 	   }
		return view;
	}
}

As you can see from the above, you are using layoutinflator from(this). How to call onCreateView in AppCompatDelegateImpl when inflate (layout(), null).

Then take a look at onCreateView

 * From {@link LayoutInflater.Factory2}.
 */
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    return createView(parent, name, context, attrs);
}

 @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
    //.......

        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP,     true,    VectorEnabledTintResources.shouldBeUsed() 
        );
    }
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    final Context originalContext = context;
	//.......

    View view = null;
	//Replaced here
    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);
    }

 
    return view;
}

//Replaced AppComptImageView
@NonNull
protected AppCompatImageView createImageView(Context context, AttributeSet attrs) {
    return new AppCompatImageView(context, attrs);
}

Finally, the replaced View will be returned

The important thing here is that mFactory2 will be used for the creation of View first, and then mFactory. As long as it is not null, the onCreateView method of Factory will be executed. Otherwise, the createView method of the system will be used.

LayoutInflater

​ It is mainly used to instantiate our layout

​ How to use

LayoutInflater.from(this).inflate(R.layout.activity_main,null) //2
LayoutInflater.from(this).inflate(R.layout.activity_main,null,false) //3
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
    LayoutInflater factory = LayoutInflater.from(context);
    return factory.inflate(resource, root);
}
//2
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}
//3
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
	//......
}

​ As can be seen from the source code, the first one calls the second one, and the second one calls the third one. The third parameter is passed in according to whether the root layout is passed in or not. So we just need to lo ok at the third method

Take a look at layoutinflator from()

    LayoutInflater LayoutInflater =
            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    return LayoutInflater;
}

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

//A static Map
private static final Map<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
            new ArrayMap<String, ServiceFetcher<?>>();
public static Object getSystemService(ContextImpl ctx, String name) {
    ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    return fetcher != null ? fetcher.getService(ctx) : null;
}

You can see that what you get here is a systematic service

Then look down and you can see that this service is obtained from a static Map. So how is this Map initialized?

There is a static code block in ContextImpl, which is specially used to register various services, LAYOUT_INFLATER_SERVICE is also one of them.

From this, we can know that layoutinflator From (this) is a system service, and it is a singleton.

Next, let's see how to instantiate View

    //Get resource file
    final Resources res = getContext().getResources();
   
    //Parser for XmlResourceParser
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

    	//...........
        //Save the transmitted Viwe
        View result = root;

        try {
            advanceToRootNode(parser);
            final String name = parser.getName();
		   //If it is a merge tag, call rinflete; otherwise, execute else 
            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");
                }
			   //Here, directly load the interface, ignore the mark, and directly pass in the root to the rinfflate to load the subview
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root View found in the xml and creates a View
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;
			   // If root is not empty, set layoutParams	
                if (root != null) {
                    if (DEBUG) {
                        System.out.println("Creating params from root: " +
                                root);
                    }
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }
                //Get the template first, and then transfer the template as root to rinflatchildren to load the child view behind the template
                rInflateChildren(parser, temp, attrs, true);

              	//Add the View to the root layout and set the layout parameters
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }
			//...
            }

        } catch (XmlPullParserException e) {
       		//...
        }

        return result;
    }
}
//Create View
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
    	
		//......
        try {
            //Create View
            View view = tryCreateView(parent, name, context, attrs);
		   //If no results are created
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    //Judge whether the name is the full class name, and finally create a reflection View
                    //If not all categories, splicing is required
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
       		//....
        }
    }

 public final View tryCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs) {
      
        View view;
     	//If mFactory2 is not empty, the onCreateView of mFactory2 will be called directly
     	// mFactory2 is set in AppCompatActivity
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }
        return view;
    }

In AppCompatDelegateImp, the reason why it can use its own onCreateView method is that it can intercept the creation of View because it sets mFactory

If mFactory is equal to null, the View will be created by itself. If it is not null, the creation of View will be blocked and the methods in the corresponding mFactory will be executed

Next, let's look at the View creation without mFactory

public View onCreateView(@NonNull Context viewContext, @Nullable View parent,
        @NonNull String name, @Nullable AttributeSet attrs)
        throws ClassNotFoundException {
    return onCreateView(parent, name, attrs);
}
protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    //Add full class name
    return createView(name, "android.view.", attrs);
}

public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
      
    	//Get from cache
        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 there is no cache, the reflection is created and added to the cache	
            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);
			   //Get the constructor mConstructorSignature = new Class[] {Context.class, AttributeSet.class};
            	//Get the constructor with two parameters
                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 {
                //Reflection create View
                final View view = constructor.newInstance(args);
                if (view instanceof ViewStub) {
                    // Use the same context when inflating ViewStub later.
                    final ViewStub viewStub = (ViewStub) view;
                    viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
                }
                return view;
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }catch (Exception e) {
           //......
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

After reading the source code, you need to know some questions

1. If you get layoutinflator

​ By obtaining the service of the system, and it is a single example

2. If layoutinflator is used

​ The three ways of use are mentioned at the beginning

3. If the layout is instantiated

​ The final layout is instantiated through reflection

4. Function of mFactory

​ Intercept the creation of View and make the creation of View follow a custom process, such as in setContentView of AppCompatView.

5. What's the difference between xml and new

The VIew creation in the layout file calls the construction of two parameters, while the direct new is the construction of one parameter. And the layout defined in xml is finally created through reflection, so try not to nest multiple layers

Intercept the creation of View

​ According to the above analysis, if you want to intercept the creation of View, you need to set Factory for layoutinflator.

        intercept()
        super.onCreate(savedInstanceState)
    }

    private fun intercept() {
        val layoutInflater = LayoutInflater.from(this)
        LayoutInflaterCompat.setFactory2(layoutInflater, object : LayoutInflater.Factory2 {
            override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
                return onCreateView(null, name, context, attrs)
            }

            override fun onCreateView(
                parent: View?, name: String, context: Context, attrs: AttributeSet
            ): View? {
                Log.e("BaseSkinActivity", "Intercept View Creation of")
                //Intercept the creation of View
                return if (name == "Button") {
                    val button = Button(this@BaseSkinActivity)
                    button.id = R.id.test_btn
                    button.text = "intercept"
                    return button
                } else null
            }

        })
    }

The above set a Factory for layoutinflator to intercept the creation of VIew

In onCreateView, judge if it is a Button and modify its displayed content.

The end result is that the interception is successful.

Here, the whole article will be analyzed. If you have any questions, please point out!!!

Refer to the video from red orange Darren