preface

Plug-in technology is mainly applied in the field of dynamic and skin, the former also needs to solve the problem of plug-in Activity jump, jump compatibility problems. Darren, ChangeSkin, netease Cloud Music and other skin changing frameworks have the same idea. However, there is no need to inherit BaseActivity now. LifeCycle can be used to avoid invasion. Even though the technology is outdated, and everyone knows it, let’s output it a little bit more profoundly.

API: AndroidX 1.2.0, 29

directory

View loading

I’m going to start with the setContentView method directly. As mobile development, Android class startup, class loading process is already a basic knowledge, see more draw flow chart on the clear, not too much introduction here.

1. A brief introduction to the Activity startup process

The starting process of an Activity is described from the startActivity Context, which is ContextImpl’s startActivity implementation, and then the internal Instrumentation will try to start the Activity. This is a cross-process process. It calls AMS’s startActivity method, and when AMS validates the Activity, it calls back to our process via ApplicationThread, which is a Binder. The callback logic is done in the Binder thread pool, so it needs to be cut back to the UI thread via Handler H. The first message is LAUNCH_ACTIVITY, which corresponds to the handleLaunchActivity. This method creates and starts the Activity. Then, in the Activity’s onResume, the content of the Activity will start rendering onto the Window and start drawing until we can see it.

2, the setContentView

The DecorView is created during this process, followed by the onResume stage. The ViewRootImpl is created and associated with the PhoneWindow, and then displayed.

API environment AdnroidX 1.2.0

The setContentView method, which is given to AppCompatDelegateImpl on AndroidX, then focuses on the view creation part of the code

    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }
Copy the code

See ensureSubDecor first

private void ensureSubDecor() { if (! mSubDecorInstalled) { mSubDecor = createSubDecor(); . }Copy the code

The createSubDecor method has a key piece of code mWindow.getDecorView(), where mWindow is a PhoneWindow

    @Override
    public final @NonNull View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }
Copy the code

This will make sure to create a DecorView, setting up the theme, setting the ID for the layout, and creating a contentParent (FrameLayout).

So we go back to the original setContentView method, and we basically take the contentParent, and then we load in the view that we wrote in the setContentView, which is basically the inflate method

LayoutInflater.from(mContext).inflate(resId, contentParent);
Copy the code

3, inflate

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); if (DEBUG) { Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" + Integer.toHexString(resource) + ")"); } View view = tryInflatePrecompiled(resource, res, root, attachToRoot); if (view ! = null) { return view; } XmlResourceParser parser = res.getLayout(resource); {return inflate(parser, root, attachToRoot); // XML parse try {return inflate(parser, root, attachToRoot); } finally { parser.close(); }}Copy the code

The tryInflatePrecompiled method above must return NULL because the mUseCompiledView field is always false in the API, so focus on the inflate method below, Layout is parsed using XmlResourceParser.

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; . // 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) { if (DEBUG) { System.out.println("Creating params from root: " + root); } // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); if (! AttachToRoot) {// false, no binding, set Params. Sometimes attachToRoot false crashes because here, set the parent LayoutParams, // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); }} // addView, if attachToRoot is.... return result; }}Copy the code

So let’s look at the createViewFromTag method above,

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { ........ try { View view = tryCreateView(parent, name, context, attrs); if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; Try {// here because if there is no. For example, TextView, we need to add android.widget to reflect and build the class. If (-1 == name.indexof ('.')) {view = onCreateView(context, parent, name, attrs); } else { view = createView(context, name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; . }Copy the code

Focus on the tryCreateView method above, which creates the View. If it returns the View correctly, the if will not be executed. The main code in this method is as follows

public final View tryCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, TAG_1995 = blink if (name.equals(TAG_1995)) {// Let's party like it's 1995! return new BlinkLayout(context, attrs); } 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); } return view; }Copy the code

You can see that if mFactory2 is empty, then the view is built from mFactory. If it is still empty, then the view is built from mPrivateFactory. If both are empty, then null is returned and the outer layer handles them.

In fact, this method does return null, so the trick is to get mFactory2 or mFactory to be replaced with our own class, where we can collect all the views and dynamically change them each time. To create a view, we can create a view from the createViewFromTag method. The two internal details are not lost: the cache of constructors and the reflection of a class’s fully qualified name that needs to be prefixed.

One more question? We already know that we can replace it with the class we wrote, so how do we get other resources in APK?

4. Resource loading

Resources were previously created at the beginning of the inflate method

Resources res = getContext().getResources();
Copy the code

The getResources method corresponding to ContextImpl

    @Override
    public Resources getResources() {
        return mResources;
    }
Copy the code

So how is this mResources created?

When Android starts, it calls the ActivityThread’s handleBindApplication method, which contains a line of code,

app = data.info.makeApplication(data.restrictedBackupMode, null);
Copy the code

We’re building the Application, and we’re focusing on the makeApplication method, which intercepts some of the internal code

java.lang.ClassLoader cl = getClassLoader(); if (! mPackageName.equals("android")) { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "initializeJavaContextClassLoader"); initializeJavaContextClassLoader(); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this); app = mActivityThread.mInstrumentation.newApplication( cl, appClass, appContext); appContext.setOuterContext(app);Copy the code

Continue to focus on ContextImpl createAppContext this line, found in the process of creating context, will set up the Resources, mainly through packageInfo get Resources, So we can also get the plugin resources through the plugin APK package PackageInfo.

    static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
            String opPackageName) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
                null, opPackageName);
        context.setResources(packageInfo.getResources());
        return context;
    }
Copy the code

Now, how does PackageInfo get its resources

    public Resources getResources() {
        if (mResources == null) {
            final String[] splitPaths;
            try {
                splitPaths = getSplitPaths(null);
            } catch (NameNotFoundException e) {
                // This should never fail.
                throw new AssertionError("null split not found");
            }

            mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                    splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                    Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                    getClassLoader());
        }
        return mResources;
    }
Copy the code

I’m not going to look at the code, it’s going to create Resources, it’s going to create ResourcesImpl associated with Resources, and it’s going to call createAssetManager to createAssetManager associated with ResourcesImpl, And then cache it so you don’t have to create it next time.

protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) { final AssetManager.Builder builder = new AssetManager.Builder(); // resDir can be null if the 'android' package is creating a new Resources object. // This is fine, since each AssetManager automatically loads the 'android' package // already. if (key.mResDir ! = null) { try { builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/, false /*overlay*/)); } catch (IOException e) { Log.e(TAG, "failed to add asset path " + key.mResDir); return null; }}... }Copy the code

So for the resource processing of the plug-in package, you can follow the source code

Resources appResource = mContext.getResources(); / / reflection to create AssetManager and Resource AssetManager AssetManager = AssetManager. Class. NewInstance (); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", string.class); addAssetPath.invoke(assetManager, skinPath); SkinResource = new Resources(assetManager, appResource.getDisplayMetrics (), appResource.getConfiguration()); / / get the external Apk package (skin) package name = mContext PackageManager mPm. GetPackageManager (); PackageInfo info = mPm.getPackageArchiveInfo(skinPath, PackageManager .GET_ACTIVITIES); String packageName = info.packageName;Copy the code

5. Whether createViewFromTag will be called by other layouts

We can use mFactory2 or mFactory in the createViewFromTag method chain to create a View instead of creating a View. Will subviews or custom views be called? No, that still doesn’t handle dynamic peels

It certainly will.

There is also a method in the previous inflate method

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

The main code in this method is

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

So it’s safe to use

2. Implementation of the scheme

1. Scheme context

There are three main steps

  • Collect XML data using the Factory2 interface, onCreateView method, in the process of producing objects

  • Record the attribute name and the corresponding resource ID. When Factory2 produces a View, it corresponds to SkinAttribute, SkinView, and SkinPair

  • Read the content of the skin package, load it in through the AssetManager, and finally replace the principle is to get the Resources of the plug-in package, and then go to the plug-in package to find the resource name and resource type corresponding to the previously recorded ID, and then go to the plug-in package to find the resource ID of the plug-in package, and set it to the control.

2. Precautions

  • With a view to different writing do in XML processing, such as XML controls EditText and androidx constraintlayout. Widget. Constraintlayout this distinction method, as it had been written of the fully qualified name of a class, we need to add a package name, the full path, Then you can reflect the constructor class. Also note that control attribute values are written differently in XML, such as textColor=” #FFFFFF “and textColor=”@color/white” and textColor=”? ColorAccent “, for the first one, we can’t handle it because the value is written like this.

  • If by LayoutInflaterCompat mFactory2 setFactory2 mode setting, need to pay attention to mFactorySet reflection setting failure, specific writing can look at the code

    SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory(activity); If (build.version.sdk_int <= build.version_codes.p) {// reflection try {Field Field = LayoutInflater.class.getDeclaredField("mFactorySet"); field.setAccessible(true); field.setBoolean(layoutInflater, false); } catch (Exception e) { e.printStackTrace(); } LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory); mLayoutInflaterFactories.put(activity, skinLayoutInflaterFactory); }else{ try { Field field = LayoutInflater.class.getDeclaredField("mFactory2"); field.setAccessible(true); field.set(layoutInflater,skinLayoutInflaterFactory); } catch (Exception e) { e.printStackTrace(); }}Copy the code

    With the first approach, set mFactorySet to false if the outcome is indeed possible, but in API 29

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) private boolean mFactorySet; @unsupportedappusage private Factory mFactory; @UnsupportedAppUsage private Factory2 mFactory2;Copy the code

    In the Android Q update, there is a limited change in the non-SDK interface, which is reflected in the above one. The reflection mFactorySet is false and will never succeed unless targetSdk is changed below 29, otherwise it will flash back.

3. Code address

SkinDemo