[TOC]

Original address: Day and night and peels (ii) – Principle analysis

The official principle

In the official recommended method, there are almost as many implementations () as there are modes or themes that need to be set.

		/**
     * Cause this Activity to be recreated with a new instance.  This results
     * in essentially the same flow as when the Activity is created due to
     * a configuration change -- the current instance will go through its
     * lifecycle to {@link #onDestroy} and a new instance then created after it.
     */
Copy the code

Recreate this activity with a new instance. This results in much the same process as when an Activity is created due to a configuration change ———— The current instance travels through its lifecycle to onDestroy, and then a new instance is created after it

The current Activity goes to onDestroy, creating a new Activity, so the new Activity’s lifecycle is executed

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

Go to the delegator.oncreate () method

		@Override
    public void onCreate(Bundle savedInstanceState) {
        // attachBaseContext will only be called from an Activity, so make sure we switch this for
        // Dialogs, etc
        mBaseContextAttached = true;

        // Our implicit call to applyDayNight() should not recreate until after the Activity is
        // created
        applyDayNight(false); . }Copy the code

Click to enter the applyDayNight method

 private boolean applyDayNight(final boolean allowRecreation) {
        if (mIsDestroyed) {
            if (DEBUG) {
                Log.d(TAG, "applyDayNight. Skipping because host is destroyed");
            }
            // If we're destroyed, ignore the call
            return false;
        }

        @NightMode final intnightMode = calculateNightMode(); . }Copy the code

Enter the calculateNightMode ()

 @NightMode
    private int calculateNightMode(a) {
        returnmLocalNightMode ! = MODE_NIGHT_UNSPECIFIED ? mLocalNightMode : getDefaultNightMode(); }public static int getDefaultNightMode(a) {
        return sDefaultNightMode;
    }

		public static void setDefaultNightMode(@NightMode int mode) {...switch (mode) {
            case MODE_NIGHT_NO:
            case MODE_NIGHT_YES:
            case MODE_NIGHT_FOLLOW_SYSTEM:
            case MODE_NIGHT_AUTO_TIME:
            case MODE_NIGHT_AUTO_BATTERY:
                if(sDefaultNightMode ! = mode) { sDefaultNightMode = mode; applyDayNightToActiveDelegates(); }break;
            default:
                Log.d(TAG, "setDefaultNightMode() called with an unknown mode");
                break; }}Copy the code

The onCreate method must be executed again to achieve dynamic switching in this mode

The ANDROIDX SDK already puts the concepts in the setDefaultNightMode() method, so you don’t need to call them manually

Whether through AppCompatDelegate. SetDefaultNightMode or delegate. SetLocalNightMode, will perform to the updateForNightMode method

private boolean updateForNightMode(@ApplyableNightMode final int mode,
            final boolean allowRecreation) {... ActivityCompat.recreate((Activity) mHost); handled =true; .return handled;
    }
Copy the code

Execution comprehension reconstructs an Activity

Third party library implementation principle

Android-skin-support

Principle analysis (SdkVersion = 29)

How did you do that?

Let’s review the loading process of setContentView() :

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}//androidx.appcompat.app.AppcompatActivity
		@Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }
Copy the code

Go to the agent to see the implementation:

    //androidx.appcompat.app.AppcompatDelegateImpl
		@Override
    public void setContentView(int resId) {
      	// Initialize DecroView
        ensureSubDecor();
      	// Get the Content root layout
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
      	// Remove all layouts
        contentParent.removeAllViews();
     		// Load the layout
        LayoutInflater.from(mContext).inflate(resId, contentParent);
      	// Layout status interface notification
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }
Copy the code

Enter the Inflate method:

		//android.view.LayoutInflater
		public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        returninflate(resource, root, root ! =null);
    }

		public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        finalResources res = getContext().getResources(); .// Generate compiled_view.dex from the XML precompilation, which is then reflected to generate the corresponding View, thus reducing the time that XmlPullParser takes to parse the XML
    		// It is important to note that it is not supported in the current release
        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
        if(view ! =null) {
            return view;
        }
      	// Parsing with XML returns an XmlResourceParser, which is eventually parsed by native methods
        XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally{ parser.close(); }}Copy the code

Enter the inflate:

//android.view.LayoutInflater
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
						...

            try {
              	// If START_TAG is not found, an exception is reported saying there is no entry
                advanceToRootNode(parser);
                finalString name = parser.getName(); .// If merge is a root node, raise an exception if merge is not a root node
                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
                  	// Get the root View
                    finalView temp = createViewFromTag(root, name, inflaterContext, attrs); .// Render the children of the root node
                    rInflateChildren(parser, temp, attrs, true); .returnresult; }}Copy the code

Go to rInflateChildren() :

    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;

      	// Loop the convenient layout until the layout is complete
        while(((type = parser.next()) ! = XmlPullParser.END_TAG || parser.getDepth() > depth) && type ! = XmlPullParser.END_DOCUMENT) { ...// Get the View of the current layout
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
          			// Render the children of the current layout View
                rInflateChildren(parser, view, attrs, true);
          			// Group as ViewGroupviewGroup.addView(view, params); }}if (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }

        if(finishInflate) { parent.onFinishInflate(); }}Copy the code

Enter the createViewFromTag () :

//android.view.LayoutInflater
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null."class");
        }

        // Apply a theme wrapper, if allowed and one is specified.
        if(! ignoreThemeAttr) {final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0.0);
            if(themeResId ! =0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }

        try {
            View view = tryCreateView(parent, name, context, attrs);

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

If the view is null, then onCreateView or createView,

Both onCreateView and createView will eventually enter the following function:

@Nullable
    public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Objects.requireNonNull(viewContext);
        Objects.requireNonNull(name);
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if(constructor ! =null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add itclazz = Class.forName(prefix ! =null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);

                if(mFilter ! =null&& clazz ! =null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if(! allowed) { failNotAllowed(name, prefix, viewContext, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
                // If we have a filter, apply it to cached constructor
                if(mFilter ! =null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowedclazz = Class.forName(prefix ! =null ? (prefix + name) : name, false,
                                mContext.getClassLoader()).asSubclass(View.class);

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

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

            try {
                final View view = constructor.newInstance(args);
                if (view instanceof ViewStub) {
                    // Use the same context when inflating ViewStub later.
                    final ViewStub viewStub = (ViewStub) view;
                    viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
                }
                return view;
            } finally {
                mConstructorArgs[0] = lastContext; }}catch(NoSuchMethodException e) { ... }}Copy the code

A quick look confirms that this is done by reflection

Then we go to the tryCreateView() method:

//android.view.LayoutInflater
public final View tryCreateView(@Nullable View parent, @NonNull String name,
    @NonNull Context context,
    @NonNull AttributeSet attrs) {
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }

    View view;
    if(mFactory2 ! =null) {
      	// The system creates Factory2 objects
        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;
}
Copy the code

Then the mFactory2 / mFactory onCreateView () where to go? Where is the Factory instance?

The following

What do we need to analyze before setContenView()

 		@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
Copy the code

Click on the super.onCreate(saveInstanceState) method:

		//androidx.appcompat.app.AppCompatActivity
		@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        super.onCreate(savedInstanceState);
    }
Copy the code

Enter the installViewFactory() method:

		//androidx.appcompat.app.AppCompatDelegateImpl
		@Override
    public void installViewFactory(a) {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            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"); }}}Copy the code

First by getting layoutInflater. GetFactory () to obtain, if not will setFactory2 set.

We initialize our setFactory class, and notice that the second argument, passed in, is this. So, let’s look at this class:

class AppCompatDelegateImpl extends AppCompatDelegate
        implements MenuBuilder.Callback.LayoutInflater.Factory2
Copy the code

Well, it’s a class that implements the Factory2 interface, so let’s go straight to the implementation of onCreateView:

		@Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {... mAppCompatViewInflater =newAppCompatViewInflater(); .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

Actually call is AppcompatViewInflater createView () method:

		final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {...switch (name) {
            case "TextView":
                view = createTextView(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;
    }

		@NonNull
    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
        return new AppCompatTextView(context, attrs);
    }
Copy the code

If the switch is not processed, return the view to null

What are we going to do?

What’s the difference between Factory and Factory2?

Unfortunately, we can’t get anything out of the documentation, so why don’t we just look at the code

		public interface Factory {
        @Nullable
        View onCreateView(@NonNull String name, @NonNull Context context,
                @NonNull AttributeSet attrs);
    }

		public interface Factory2 extends Factory {
        
        @Nullable
        View onCreateView(@Nullable View parent, @NonNull String name,
                @NonNull Context context, @NonNull AttributeSet attrs);
    }
Copy the code

Factory2 is an override of Factory because it has a View superclass in the field

In the course of the analysis, we found that

	//androidx.appcompat.app.AppCompatDelegateImpl
	@Override
public void installViewFactory(a) {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        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"); }}}Copy the code

If the system doesn’t have a Factory, it will create a new one, AppCompatDelegateImpl. If we set the Factory before the super.onCreate() method we can let the system run our Factory implementation

The specific implementation

Finally we execute our Settings before the Activity’s onCreate method

		@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        LayoutInflaterCompat.setFactory(getLayoutInflater(), getSkinDelegate());
        super.onCreate(savedInstanceState);
    }
Copy the code

At the same time, generate my own view in the createView method, set the refresh interface in the View, through the observer mode when the mode changes, through all the view, change the background color, font color, do not need to rebuild the Activity, avoid splash screen