Hello, everyone, let’s continue our plug-in series. Here are my ideas for this series of articles,

This article is the core of this series, and I plan to explain the general techniques of plug-in. I will cover the first two articles in this series, but I recommend reading the first two basic articles about Android Plugins: Introduction, Binder Mechanisms, ClassLoader, and Android Plugins: Resources and packaging Processes

This article is expected to take more than half an hour to read. After reading this article, you will understand: 1. The development and genre of plug-ins 2

  • How do I load classes and resources in a plug-in
  • How do I parse the information in the plug-in
  • How to use aAPT and other methods to solve the problem of host and plug-in resource conflict
  • How to support the plug-in of the four components

1. Development history and genre

Let’s talk a little bit about the history of plug-ins. Plug-in technology, mainly used in news, e-commerce, reading, travel, video and other fields, can see a lot of scenes in our life. In the process of application iteration, 1. It can quickly fix the problematic parts of the application; 2. In order to seize the market, it can quickly iterate according to the market response; 3. Making infrequently used modules into plug-ins and reducing package size are all important things for the development of your application. In this context, plug-in technology arises at the historic moment.

The following are some of the more well-known plug-in frameworks, which can be divided into three generations by studying their principles, in order of their time.

era On behalf of the library The characteristics of
ancient AndroidDynamicLoader Adl realizes page switching based on dynamic replacement Fragment. Although it is limited, it provides us with a basis for imagination
The first generation of Dynamic Load-APK (Ren Yugang), DroidPlugin(Zhang Yong) Dla is distributed by creating a ProxyActivity. Plug-ins must inherit ProxyActivity, be intrusive, and handle context carefully. DroidPlugin uses hook system services to jump activities. Its disadvantages are too many hooks, complicated and unstable codes.
The second generation VirtualApk, Small, RePlugin In order to achieve both the low invasibility of plug-in development (developing plug-ins as common APP development) and the stability of the framework, the implementation principle tends to select as few hooks as possible, and realize the plug-in of the four major components by embedding some components in the manifest.
The third generation VirtualApp, Atlas In this generation, plug-in compatibility and stability are elevated to a higher level. At the same time, the concept of containerized frameworks is gaining popularity.

In 2015 and before, plug-in technology was divided into two distinct groups: dynamic replacement solutions represented by DroidPlugin and static proxy solutions represented by dynamic-load-APk. Later, dynamic replacement schemes have been gradually supported by more people because of their low invasions and flexibility and stability. Since the application of Hot fix solution and React Native, plug-in technology is no longer the only choice, but enters the stage of gradual improvement. After 2017, plug-in technology is basically mature, and its compatibility and stability have reached a higher level. If you are interested, you can take a look at several open source libraries mentioned above to experience the development of plug-in technology.

2. Plug-in technology

The techniques of plug-in technology can be summarized as follows: 1. Code and resource interaction between plug-in and host 2. Four major components of the plugin support and jump

Here we talk about code and resource interaction between plug-ins and hosts. In fact, there is also a learned. Plug-ins are divided into standalone plug-ins and coupled plug-ins depending on whether or not the resource code needs to be shared. Standalone plug-ins run separately in a process and are completely isolated from the host, so crashes do not affect the host. But coupled plug-ins run in the same process as the host, so the plug-in crashes and the host crashes. Therefore, the general business needs to consider the plug-in type comprehensively according to the coupling degree of resources and code and the reliability of the plug-in.

We’re going to talk about it.

2.1 Code and resource communication

The plug-in with dex

Because some of you may read my article and have not been exposed to plug-ins, this part is added to explain how plug-ins and DEX exist on earth. Plug-ins can be understood as a separate packaged APK. We can create modules in the project and change the apply plugin: ‘com.android. Library ‘to apply Plugin: ‘com.android. The resulting package of this module is APK.

During the packaging process of APK, a class file is inserted into the dex, and finally the DEX exists in APK. The ClassLoader used to load classes in this dex is also very specific. Binder and ClassLoader are common Android plugins, including PathClassLoader and DexClassLoader. The PathClassLoader applies to apK that is already installed and generally acts as the default loader. The apK of the plug-in is not installed, so we need to use DexClassLoader to load the classes in the plug-in dex. Here is a basic code that demonstrates how to read classes from the dex of the plug-in APK.

/ / this
File apkFile = File(apkPath, apkName);
String dexPath = apkFile.getPath();
File releaseFile = context.getDir("dex".0);
DexClassLoader loader = new DexClassLoader(dexPath, releaseFile.getAbsolutePath(), null, getClassLoader());

// Load the class, using the class's methods
Class bean = loader.loadClass("xxx.xxx.xxx")  // Fill in the package name of the class
Object obj = bean.newInstance();
Method method = bean.getMethod("xxx")  // Fill in the method name
method.setAccessible(true);
method.invoke(obj)
Copy the code

In this way, we can use reflection to retrieve the class and use the corresponding methods.

Interface oriented programming

As you can see, if reflection is used as extensively as above, the code is pretty ugly and scalability is poor. This led us to wonder if we could define interfaces in advance by referring to the idea of interface-oriented or abstract programming from the dependency inversion principle. This way, when needed, you just need to convert the object to the interface and call the interface’s methods.

For example, our APP module and plug-in module plugin rely on the interface module, in which the interface IPlugin is defined. IPlugin is defined as

interface IPlugin {
    void sayHello(String name)
}
Copy the code

The implementation class can be defined in plugin

class PluginImpl implement IPlugin {
    @override
    void sayHello(String name) {
        Log.d("log"."hello world"+ name); }}Copy the code

This way, we can use it in the host app module. Specific usage methods can include reflection and service discovery mechanisms. For simplicity, reflection is used only to invoke concrete implementation classes.

Class pluginImpl = loader.loadClass("xxx.xxx.xxx")  // Package name of the PluginIMpl class
Object obj = pluginImpl.newInstance();              // Generate PluginImpl objects
IPlugin plugin = (IPlugin)obj;
plugin.sayHello("AndroidEarlybird");
Copy the code

Now that we have the interfaces, we can certainly do other things with ease. However, it is important to note that the premise is that both the host and the plug-in need to depend on the interface module, that is, both have code and resource dependencies, so this method only applies to coupled plug-ins, independent plug-ins can only be called using reflection.

PMS

In plug-in technologies, ActivityManagerService (AMS) and PackageManagerService(PMS) are important system services. AMS since needless to say, the four components of various operations will need to deal with it, the PMS are also important, completed such as permission to school pick up (checkPermission checkUidPermission), Apk meta information acquisition (getApplicationInfo, etc.), Four components information acquisition (Query series methods) and other important functions.

The use of PMS

Android generally uses PMS for application installation. During installation, PMS needs to use PackageParser for APK parsing, mainly responsible for parsing a PackageParser.Package object, which is very useful. Here are some property values for this Package object.

It can be seen that we can get the information of apK’s four components, permissions and so on through this class. In the plug-in process, we sometimes need to use this class to get the information of broadcast to deal with static broadcast in the plug-in.

So how do you use the PackageParser class? Here are some uses of VirtualApk

    public static final PackageParser.Package parsePackage(final Context context, final File apk, final int flags) throws PackageParser.PackageParserException {
        if (Build.VERSION.SDK_INT >= 24) {
            return PackageParserV24.parsePackage(context, apk, flags);
        } else if (Build.VERSION.SDK_INT >= 21) {
            return PackageParserLollipop.parsePackage(context, apk, flags);
        } else {
            returnPackageParserLegacy.parsePackage(context, apk, flags); }}private static final class PackageParserV24 {
        static final PackageParser.Package parsePackage(Context context, File apk, int flags) throws PackageParser.PackageParserException {
            PackageParser parser = new PackageParser();
            PackageParser.Package pkg = parser.parsePackage(apk, flags);
            ReflectUtil.invokeNoException(PackageParser.class, null."collectCertificates".new Class[]{PackageParser.Package.class, int.class}, pkg, flags);
            returnpkg; }}Copy the code

Because PackageParser varies widely from system to system, VirtualApk ADAPTS this class to multiple versions, of which only one is shown here.

Hook PMS

Just as we need hook AMS to do some plug-in work, sometimes we also need to hook PMS. GetPackageManager: ContextImpl getPackageManager: ContextImpl getPackageManager: ContextImpl getPackageManager

public PackageManager getPackageManager(a) {
    if(mPackageManager ! =null) {
        return mPackageManager;
    }

    IPackageManager pm = ActivityThread.getPackageManager();
    if(pm ! =null) {
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));
    }
    return null;
}

// Follow up to ActivityThread's getPackageManager method
public static IPackageManager getPackageManager(a) {
    if(sPackageManager ! =null) {
        return sPackageManager;
    }
    IBinder b = ServiceManager.getService("package");
    sPackageManager = IPackageManager.Stub.asInterface(b);
    return sPackageManager;
}
Copy the code

Here we can see that the PMS needs to hook both places:

  • ActivityThread static field sPackageManager
  • Through the Context getPackageManager method to obtain ApplicationPackageManager object of a class of mPM fields.

Example code is as follows:

// Get the global ActivityThread objectClass<? > activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

// Get the original sPackageManager in ActivityThread
Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);

// Prepare the proxy object to replace the original objectClass<? > iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
        newClass<? >[] { iPackageManagerInterface },new HookHandler(sPackageManager));

// 1. Replace the sPackageManager field in ActivityThread
sPackageManagerField.set(currentActivityThread, proxy);

/ / 2. Replace ApplicationPackageManager mPM object inside
PackageManager pm = context.getPackageManager();
Field mPmField = pm.getClass().getDeclaredField("mPM");
mPmField.setAccessible(true);
mPmField.set(pm, proxy);
Copy the code

To manage this

We talked about how to use ClassLoader to load classes in dex. Now let’s talk more about this topic. First, to be clear, because our plugin’s classes are in the dex of an apK that is not installed, we cannot use the main app’s ClassLoader directly. Then there are multiple solutions.

The more straightforward idea is to create a new ClassLoader for each plug-in to load. So if we have a lot of plug-ins, what we need to do is to record the ClassLoader of each plug-in, when using a plug-in class, use its corresponding ClassLoader to load. As we showed in the example in the previous section.

Another idea is to manipulate the DEX array directly. Both the host and the plug-in’s ClassLoader correspond to an array of dex. If we can merge the plugin’s dex array into the host’s dex array, we can use the host’s ClassLoader to reflect the classes in the plugin’s dex array. The idea is that you don’t need to manage the plug-in’s ClassLoader, just use the host ClassLoader. For example, we covered the source code for DexClassLoader in the Android Plug-in series I: Introduction, Binder mechanism, ClassLoader.

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);  / / see below
        // Collect dex files and Native dynamic libraries [see Section 3.2]
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }}public class DexPathList {
    private Element[] dexElements;

    public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext);
    }

    private static List<File> splitDexPath(String path) {
       return splitPaths(path, false);
    }

    private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
        List<File> result = new ArrayList<>(); 
        if(searchPath ! =null) {
            for (String path : searchPath.split(File.pathSeparator)) {
                / / to omit}}returnresult; }}Copy the code

From the above we can see that the dexPath string is separated by multiple semicolons. Broken down into strings, each path is an external dex/ APK path. It is natural to wonder if the dex path of the plug-in can be manually added to the host’s dexElements array. The answer is ok, of course, the solution is to use Hook. We can reflect the dexPathList of the ClassLoader, then get the dexElements array of the list, and then manually build the plugin into Element and copy it into the dexElements array. Nuwa, the thermal repair framework, also uses this idea.

The third idea is a ClassLoader delegate. This approach is recommended in this article. First we define a custom ClassLoader, replace the original host ClassLoader, and make the host as a Parent, and at the same time in the custom ClassLoader with a collection of all plug-in classloaders. When the custom ClassLoader loads any class, it will first look for the host ClassLoader according to the parent delegation mechanism. If there is no class, it will look for the plug-in ClassLoader that can load this class. Of course, there are efficiency optimizations, such as traversing collections by first looking for loaded collections and then unloaded collections. Here is the sample code.

class PluginManager {
    public static void init(Application application) {
        // Initialize some member variables and load the installed plug-ins
        mPackageInfo = RefInvoke.getFieldObject(application.getBaseContext(), "mPackageInfo");
        mBaseContext = application.getBaseContext();
        mNowResources = mBaseContext.getResources();

        mBaseClassLoader = mBaseContext.getClassLoader();
        mNowClassLoader = mBaseContext.getClassLoader();
        
        ZeusClassLoader classLoader = new ZeusClassLoader(mBaseContext.getPackageCodePath(), mBaseContext.getClassLoader());

        File dexOutputDir = mBaseContext.getDir("dex", Context.MODE_PRIVATE);
        final String dexOutputPath = dexOutputDir.getAbsolutePath();
        for(PluginItem plugin: plugins) {
            DexClassLoader dexClassLoader = new DexClassLoader(plugin.pluginPath,
                    dexOutputPath, null, mBaseClassLoader);
            classLoader.addPluginClassLoader(dexClassLoader);
        }
        // Replace the original host ClassLoader with the custom ClassLoader, and replace the original host ClassLoader with the custom ClassLoader
        RefInvoke.setFieldObject(mPackageInfo, "mClassLoader", classLoader); Thread.currentThread().setContextClassLoader(classLoader); mNowClassLoader = classLoader; }}class ZeusClassLoader extends PathClassLoader {
    private List<DexClassLoader> mClassLoaderList = null;

    public ZeusClassLoader(String dexPath, ClassLoader parent, PathClassLoader origin) {
        super(dexPath, parent);

        mClassLoaderList = new ArrayList<DexClassLoader>();
    }

    /** * Add a plugin to the current classLoader */
    protected void addPluginClassLoader(DexClassLoader dexClassLoader) {
        mClassLoaderList.add(dexClassLoader);
    }

    @Override
    protectedClass<? > loadClass(String className,boolean resolve) throwsClassNotFoundException { Class<? > clazz =null;
        try {
            Parent classLoader (apK); parent classLoader (apK)
            clazz = getParent().loadClass(className);
        } catch (ClassNotFoundException ignored) {
        }

        if(clazz ! =null) {
            return clazz;
        }

        // Search the plugins one by one
        if(mClassLoaderList ! =null) {
            for (DexClassLoader classLoader : mClassLoaderList) {
                if (classLoader == null) continue;
                try {
                    // The plugin can only find its own APK, not need to check the parent, avoid many useless queries, improve performance
                    clazz = classLoader.loadClass(className);
                    if(clazz ! =null) {
                        returnclazz; }}catch (ClassNotFoundException ignored) {
                }
            }
        }
        throw new ClassNotFoundException(className + " in loader " + this); }}Copy the code

resources

Resources&AssetManager

Resources in Android can be roughly divided into two types: one is the compilable resource files existing in the RES directory, such as Anim and String, and the other is the original resource files stored in the assets directory. Since these files are not compiled by Apk, they cannot be accessed by id, and certainly not by absolute path. So the Android system lets us get the AssetManager through the getAssets method of Resources and use the AssetManager to access these files.

Resources resources = context.getResources();
AssetManager manager = resources.getAssets();
InputStream is = manager.open("filename");
Copy the code

The relationship between Resources and AssetManager is similar to sales and r&d. Resources is responsible for external, external needs getString, getText, all kinds of methods are called through Resources class. These methods are actually private methods of the calling AssetManager. Therefore, in the end, the two types of resources are AssetManager’s conscientious request for resources from the Android system to serve the outside world.

One of the most important methods in AssetManager is the addAssetPath(String Path) method. When the App starts, it will pass in the path of the current APK, and then the AssetManager can access all the resources in the path of the host APK. The idea arises that if we pass the address of the plug-in into this method, we can have a “super” AssetManager that can access all the resources of the host and the plug-in at the same time. The answer is yes, and this is a plug-in solution to resources.

The following is an example code that shows the method of getting the AssetManager from the host Resources, calling addAssetPath to add the plug-in path, and finally generating a new Resources

// Create a new AssetManager and call addAssetPath
AssetManager assetManager = resources.getAssets();  // Get the sample code from Resources first
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexPath1);
mAssetManager = assetManager;

// Generate Resources from the newly generated AssetManager
mResources = new Resources(mAssetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
Copy the code

Next we will replace the original Resources of the host and plug-in with the Resources we generated above. Note that the application passed in here should be the host and plug-in corresponding application.

Object contextImpl = RefInvoke.getFieldObject("android.app.ContextImpl", application, "getImpl")  // Get the Application context
LoadedApk loadedApk = (LoadedApk)RefInvoke.getFieldObject(contextImpl, "mPackageInfo")
RefInvoke.setFieldObject(loadedApk, "mResources", resources);
RefInvoke.setFieldObject(application.getBaseContext(), "mResources", resources);
Copy the code

In addition to replacing the Application Resources object, we also need to replace the Activity Resources object, as well as the host and plug-in Resources. This is because they are both contexts, and replacing only the Application does not affect the Activity. We can replace the callActivityOnCreate callback in the Instrumentation. More on this later in the Activity plug-in processing section.

This is just a demo, but for more information check out VirtualApk

Resolving resource Conflicts

In Android plug-in series 2: Resource and Packaging process, we mentioned that there may be Resource ID conflict when plug-in and host are packaged separately. After using a super Resource, if the ID is the same, the runtime using the ID to find the Resource will report an error.

In order to solve the problem of ID conflict, there are generally three solutions:

  1. Modify the android packaged aAPT tool to change the plugin resource prefix to a number between 0x02 and 0x7e
  2. New AssetManager and Resources are generated for each plug-in you enter

Among them, scheme 2 is more complex and is not conducive to the mutual invocation of host and plug-in resources. So we used the Super Resources solution in the last section, so here we introduce the first solution, which is to modify the AAPT tool.

Aapt is a processing tool for Android packaging resources. Most of the plug-ins and open source libraries are modified in two ways:

  • Modify aAPT source code, customize aAPT tool, modify the PP section of the resource ID during compilation, you can refer to Android how to modify the compiled resource ID value, DynamicApk is used in this way.
  • Rearrange the resources of the plugin Apk and arrange the ID. VirtualApk has created a gradle transform plugin for ProcessAndroidResources task. Gradle transforms the ProcessAndroidResources task into gradle transform. Reset the resource ID in the plug-in’s resources.arsc file and update the R.Java file. Refer to pluginization – Resolving conflicts between plug-in resource ids and host resource ids.

It can be seen that AAPT (1) is not very friendly in dealing with plug-in resources and is difficult to develop and maintain. Later, Google launched Android App Bundle, a feature similar to plug-in, and aapT2 was launched to support resource subcontracting. We pay attention to the packing parameters of these AAPT2 on the official website:

Do you find that the official has given us support to differentiate resource prefix IDS by package? How nice!

Of course there are holes here too. Before buildTools 28.0.0, AAPT2 had their own resource partitions, specified with the -package-id parameter. However, this partition only supports PP segment > 0x7F. Prior to Android 8.0, PP segment > 0x7F is not supported. However, when a PP segment resource <0x7f is specified, an error is reported when compiling the resource

error: invalid package ID 0x15. Must be in the range 0x7f-0xff..
Copy the code

So for the buildTools version used before Android P (<28.0.0), we had to modify aapT2’s source code to partition resources. After 28.0.0, AAPT2 support the function of reserving PP segment partition < 0x7F. You only need to specify allow-reserved-package-id.

--allow-reserved-package-id --package-id package-id
Copy the code
Plug-ins use host resources

When we develop plug-ins for the host, it is often inevitable that plug-ins will use resources in the host. If we copy the resources of the host in the plug-in, it will undoubtedly greatly increase the size of the package. Moreover, these are duplicate resources, which should not exist in the App. Then we have to find a way to make the plug-in use the host’s resources. Such as this

You’ve already talked about, we can through the plug-in and the host together to build a super Resources, including all the Resources the plug-in and host, theoretically can get to all the Resources through resource id, then the problem comes, plug-in is R file does not contain a host of R file, we how to use when coding?

The following is a solution for using code and XML in two ways: After completion of the plugin resources packaged task processResourcesTask will host R.t xt file (produced in the process of packaging, location in the build/intermediates/symbols/xx/xx/R.t xt) incorporated into the plug-in R.t xt file, Then regenerate to R.java so that resources can be indexed using R files normally

XML usage: we need to specify the -i parameter when AAPT2 is packaged.

Thus, we specify the host’s resource bundle with -i, and we can use the host’s resources in XML.

conclusion

In this section, we first introduce the dex loading of the plug-in code, and give the method of obtaining the code in the plug-in by using reflection and interface oriented programming. Then, we introduce the method of customizing the Delegate ClassLoader to better load the plug-in and the classes in the host. It then describes how the PMS retrieves information about plug-ins and customizes hooks, and finally discusses how plug-ins use host resources. At this stage, we can get all kinds of information about the plug-in, can realize the intercommunication between the host and the code in the plug-in, can realize the plug-in to call the host resources, basically a big step forward! But code and resources aren’t enough. What do we do with android’s four major components

2.2 Four components

The four components of Android have a lot in common. For example, they are all managed by ActivityManagerService(AMS) and require AMS services through Binder mechanisms. And they request process is basic same, in which the Activity is the most important component, the mirror the most, is also the component of daily development contact most, main Activity, for example, we will explain the plug-in support for the four components of the rest of the three components have different or it is worth noting that the place we will also pointed out. Of course, there are many solutions for the four components, and this article is limited to the dynamic replacement of the DroidPlugin.

Activity

Androidmanifest.xml placeholder

Androidmanifest.xml: androidmanifest.xml: androidmanifest.xml: androidmanifest.xml: Androidmanifest.xml: Androidmanifest.xml: Androidmanifest.xml: Androidmanifest.xml: Androidmanifest.xml: Androidmanifest.xml: Androidmanifest.xml: Androidmanifest.xml: Androidmanifest.xml Have you declared this activity in your Androidmanifest.xml? [Four components must be defined in AndroidMainfest.xml] This is a serious limitation for plugins because there is no way to declare activities in plug-ins in advance, but this limitation is not insurmountable. The DroidPlugin, for example, uses pre-loading activities into androidmanifest.xml.

The idea of DroidPlugin is very simple. The LaunchMode placeholder activities and the other three components are predefined in androidmanifest.xml. Such as

<activity
    android:name=".StubSingleTaskActivity1"
    android:exported="true"
    android:launchMode="singleTask"
    android:theme="@style/Theme.NoActionBar"
    android:screenOrientation="portrait" />

<activity
    android:name=".StubSingleTopActivity1"
    android:exported="true"
    android:launchMode="singleTop"
    android:theme="@style/Theme.NoActionBar"
    android:screenOrientation="portrait" />
Copy the code

In this case, we need to implement the method of replacing the prince with the cat, and replace the Activity that we want to start with StubActivity, Then you can bypass the system’s review of the four components that must be defined in AndroidMainfest.xml before you start the actual destination Activity. So how do we implement this idea, which requires us to be familiar with the Activity startup process.

StartActivity process

StartActivity’s process is cumbersome and could even be covered in a separate article. There are a lot of articles on the Internet in the explanation, the more detailed awesome is Lao Luo’s Android application Activity start process brief introduction and learning plan. If you are interested, you can refer to it. Here I only briefly explain part of the process.

Let’s start with a flow chart

Start with startActivity, find it calls the execStartActivity method of the Instrumentation, and then call the startActivity method of the ActivityManagerNative class in this function. The service of ActivityManagerService was requested. This is the Binder mechanism we discussed in the Introduction to The Android Plugin series I: Binder mechanism, ClassLoader, and so on. In the startActivity method of AMS, we check whether the Activity is registered or not, and determine the startup mode of the Activity. We cannot change AMS, so we come to the conclusion that we must replace the Activity in the process before verification. Moving on, you can see that the ActivityStackSupervisor finally delegates the startup responsibility to the ApplicationThread.

As mentioned in the previous series 1, Binder mechanism is actually a Client and Server. When APP applies for AMS service, AMS is the Server and AMP is the agent of AMS in APP. ApplicationThread is the Server and ApplicationThreadProxy is the proxy of ApplicationThread on AMS side when AMS needs to request the App for subsequent control after applying for AMS service.

Moving on, you can see that the ActivityThread calls class H and finally calls the handleLaunchActivity method. The Activity object is created from the Instrumentation, and the startup process ends.

“Leopard cat for Prince”

In this process, I just need to replace the target Activity with StubActivity(the first half) before calling AMS, and replace it with the target Activity(the second half) when I am about to start the Activity after AMS verification. This can achieve the goal of “leopard cat for prince” to start the target Activity. Because the process is long and there are many classes involved, we can choose quite a lot of hook points. However, the earlier we hook, the more subsequent operations, the more likely problems will arise. Therefore, we choose to hook by comparing the later process. Select here:

  • In the first half, hook ActivityManagerNative calls the startActivity method
  • In the second half, the hook H.callback object was replaced with our custom implementation,

Here is some sample code. You can see that we replace the intent object given to AMS with a TargetActivity that is temporarily replaced with the declared substitute StubActivity.

        if ("startActivity".equals(method.getName())) {// Just intercept the method // replace the arguments as you like; // Find the Intent object in the Intent raw. int index = 0;for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break; } } raw = (Intent) args[index]; Intent newIntent = new Intent(); String stubPackage = raw.getComponent().getPackagename (); ComponentName = new ComponentName(stubPackage, ComponentName) StubActivity.class.getName()); newIntent.setComponent(componentName); PutExtra (amshookHelper. EXTRA_TARGET_INTENT, raw); Args [index] = newIntent; Log.d(TAG,"hook success");
            return method.invoke(mBase, args);

        }
Copy the code

ActivityThread is a class that manages the operations of four components. H inherits from Handler, let’s look at Handler’s dispatchMessage method for handling messages.

public void dispatchMessage(Message msg) {
    if(msg.callback ! = null) { handleCallback(msg); }else {
        if(mCallback ! = null) {if (mCallback.handleMessage(msg)) {
                return; } } handleMessage(msg); }}Copy the code

H’s handleMessage method is where LAUNCH_ACTIVITY, CREATE_SERVICE, and other messages are processed. Therefore, we would think that replacing the original Activity in McAllback. handleMessage would be the latest point in time. The following is a custom Callback class that reflects the mCallback set to ActivityThread H.

class MockClass2 implements Handler.Callback { Handler mBase; public MockClass2(Handler base) { mBase = base; } @override public Boolean handleMessage(Message MSG) {switch (MSG. What) {// ActivityThread"LAUNCH_ACTIVITY"The value of this field is 100 // it would have been best to use reflection to get it, but for simplicity it is hard codedcase 100:
                handleLaunchActivity(msg);
                break;

        }

        mBase.handleMessage(msg);
        return true; } private void handleLaunchActivity(Message MSG) { Object obj = msg.obj; Intent Intent = (Intent) refinvoke.getFieldobject (Intent)"intent"); Intent targetIntent = intent.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT); intent.setComponent(targetIntent.getComponent()); }}Copy the code
Replace the Resources

Remember we left a problem in the first section that resource replacement for an Activity should be performed when the Instrumentation callback to callActivityOnCreate is called. This time point is close to onCreate, Instrumentation is also more convenient to hook. This technique, shown below, requires passing in super Resources.

public void hookInstrumentation(){
    Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread"."currentActivityThread"); // Get the original mInstrumentation field Instrumentation mInstrumentation = (Instrumentation) RefInvoke.getFieldObject(currentActivityThread,"mInstrumentation"); Instrumentation evilInstrumentation = new evilInstrumentation (mInstrumentation, resources); Refinvoke. setFieldObject(currentActivityThread,"mInstrumentation", evilInstrumentation); }} public class EvilInstrumentation extends Instrumentation {Instrumentation mBase; Resources mRes; Public EvilInstrumentation(Instrumentation base, Resources res) {mBase = base; mRes = res; } @override public void callActivityOnCreate(Activity Activity, Bundle Bundle) {// Replace Resourcesif(mRes ! = null) { RefInvoke.setFieldObject(activity.getBaseContext().getClass(), activity.getBaseContext(),"mResources", mRes); } super.callActivityOnCreate(activity, bundle); }}Copy the code

Service

Service processing is basically the same as Activity, except that calling startService multiple times does not start multiple Service instances, but only one instance, so we need to define more placeholder Service.

BroadcastReceiver

The pluginization of BroadcastReceiver is different from that of an Activity. Android broadcast is divided into two types: static broadcast and dynamic broadcast, dynamic broadcast does not need to interact with AMS, is a common class, as long as it can be loaded according to the previous ClassLoader scheme. Static broadcasting, however, can be tricky. In addition to requiring registration in androidmanifest.xml, it also adds IntentFilter information, unlike an Activity. IntentFilter messages are random and cannot be preloaded. At this point, you can only take out the static broadcast in the plug-in to dynamic broadcast. There will be some minor problems, but the impact is not significant

The PackageParser gets the BroadcastReceiver from the BroadcastReceiver via the BroadcastReceiver (PMS), and then changes the BroadcastReceiver from the BroadcastReceiver to the BroadcastReceiver.

public static void preLoadReceiver(Context context, Object packageParser = Refinvoke.createObject ()"android.content.pm.PackageParser");
        Class[] p1 = {File.class, int.class};
        Object[] v1 = {apkFile, PackageManager.GET_RECEIVERS};
        Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage", p1, v1); // The following is done according to the List<Activity> in the Package object, receivers, receivers, receivers, receivers, receivers, receivers, receivers, receivers, receivers, receivers, receivers. List = (List) Refinvoke.getFieldobject (packageObj, Receiver, Receivers)"receivers");

        for(Object receiver : receivers) { registerDynamicReceiver(context, receiver); Public static void registerDynamicReceiver(Context Context); Object receiver) {// Fetch receiver intents field List<? extends IntentFilter> filters = (List<? extends IntentFilter>) RefInvoke.getFieldObject("android.content.pm.PackageParser$Component", receiver, "intents"); Try {// Each resolved static Receiver is registered as dynamicfor (IntentFilter intentFilter : filters) {
                ActivityInfo receiverInfo = (ActivityInfo) RefInvoke.getFieldObject(receiver, "info"); BroadcastReceiver broadcastReceiver = (BroadcastReceiver) RefInvoke.createObject(receiverInfo.name); context.registerReceiver(broadcastReceiver, intentFilter); } } catch (Exception e) { e.printStackTrace(); }}Copy the code

ContentProvider

ContentProvider is a plugin similar to BroadcastReceiver, but unlike BroadcastReceiver, BroadcastReceiver broadcasts are registered, whereas ContentProvider is “installed.” The solution is as follows: First, call the parsePackage method of PackageParser and convert the resulting Package object to ProviderInfo via generateProviderInfo.

    public static List<ProviderInfo> parseProviders(File apkFile) throws Exception {

        // Get the PackageParser object instanceClass<? > packageParserClass = Class.forName("android.content.pm.PackageParser");
        Object packageParser = packageParserClass.newInstance();

        // First call parsePackage to get the Package object corresponding to the APK object
        Class[] p1 = {File.class, int.class};
        Object[] v1 = {apkFile, PackageManager.GET_PROVIDERS};
        Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage",p1, v1);

        // Read the services field inside the Package object
        // The next step is to get the Provider's corresponding ProviderInfo according to the List
      
        List providers = (List) RefInvoke.getFieldObject(packageObj, "providers");

        // Call the generateProviderInfo method to convert packageParser. Provider to ProviderInfo

        // Prepare the parameters required by the generateProviderInfo methodClass<? > packageParser$ProviderClass = Class.forName("android.content.pm.PackageParser$Provider"); Class<? > packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
        Object defaultUserState = packageUserStateClass.newInstance();
        int userId = (Integer) RefInvoke.invokeStaticMethod("android.os.UserHandle"."getCallingUserId");
        Class[] p2 = {packageParser$ProviderClass, int.class, packageUserStateClass, int.class};

        List<ProviderInfo> ret = new ArrayList<>();
        // Resolve the Provider corresponding to the intent
        for (Object provider : providers) {
            Object[] v2 = {provider, 0, defaultUserState, userId};
            ProviderInfo info = (ProviderInfo) RefInvoke.invokeInstanceMethod(packageParser, "generateProviderInfo",p2, v2);
            ret.add(info);
        }

        return ret;
    }
Copy the code

We then call ActivityThread’s installContentProviders method to “install” these ContentProviders into the host.

    public static void installProviders(Context context, File apkFile) throws Exception {
        List<ProviderInfo> providerInfos = parseProviders(apkFile);

        for (ProviderInfo providerInfo : providerInfos) {
            providerInfo.applicationInfo.packageName = context.getPackageName();
        }

        Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread"."sCurrentActivityThread");

        Class[] p1 = {Context.class, List.class};
        Object[] v1 = {context, providerInfos};

        RefInvoke.invokeInstanceMethod(currentActivityThread, "installContentProviders", p1, v1);
    }
Copy the code

The plugin of ContentProvider also needs to be noted:

  1. The App installs its ContentProvider when the process starts, before the Application’s onCreate, so this is done manually in the attachBaseContext method of the Application.
  2. It is not a very good thing for external App to call the plug-in App directly. It is best to use the App ContentProvider as a relay. Because strings are the unique identity of the ContentProvider, the forwarding mechanism is particularly useful.

conclusion

This article first introduces the plug-in in the host and the plug-in code and access resources, and then introduces the methods of four major components of the plugin, because too multifarious, pluggable technology did not cover all the details of the, this solution is just the practical, withstood the test of a set of, didn’t introduce too many methods. The goal is for readers to understand with me the mechanics of plug-ins as a whole, and then it will be easy to distinguish between the principles and ideas of the various open source libraries.

reference

Thanks to the following teachers of books or articles, let me benefit a lot. Bao Jianqiang “Android Plug-in Development Guide” 2. Tian Weizhu’s blog 3. Plug-in loading dex and resource principle 4. In-depth understanding of Android plug-in technology 5. Plugins – Resolve conflicts between plugin resource ids and host resource ids. Discuss aapT2 resource partition again

I am Android stupid bird journey, stupid birds also want to have the heart to fly up, I am here to accompany you slowly become stronger.