The author

Hello everyone, my name is Xiao Xin, you can also call me crayon xiao Xin 😊;

I graduated from Sun Yat-sen University in 2017 and joined the 37 mobile Game Android team in July 2018. I once worked in Jubang Digital as an Android development engineer.

Currently, I am the overseas head of android team of 37 mobile games, responsible for relevant business development; Also take care of some infrastructure related work.

What is plug-in

A running App serves as the host to load an uninstalled APK file and run it, which is called plug-in

Plug-in usage scenarios:

1. Online new functions (such as Taobao, Alipay, etc.)

2. Hot repair (repair the function by issuing patch plug-in)

3, when the compilation is too slow, you can use plug-in, some unchanged code into plug-ins, speed up the compilation

Two, the three common implementation of plug-in

1, placeholder to achieve plug-in

1, the characteristics of

1. Plug-ins follow the criteria defined by the host and use the host context.

2, advantages: only use a small amount of reflection, no hook, simple implementation

3. Disadvantages: The plug-in can only use the context provided by the host, such as the plug-in Activity, can not use this as the context, that is to say, there is a certain invasive, need to modify the implementation of the plug-in Activity.

2. Implementation steps

1. Define the host criteria. Take the Activity as an example

public interface ActivityInterface {

    /** * Give the host (app) environment to the plugin *@param appActivity
     */
    void insertAppContext(Activity appActivity);

    // Lifecycle methods
    void onCreate(Bundle savedInstanceState);

    void onStart(a);

    void onResume(a);

    void onDestroy(a);

    // Other declaration cycles are omitted here for demonstration purposes only

}
Copy the code

2. In the plug-in module, implement the plug-in Activity according to the standard

// BaseActivity in the plug-in module implemented according to the standard
public class BaseActivity implements ActivityInterface {
	// The context passed by the host
    public Activity appActivity; // The host's environment

    @Override
    public void insertAppContext(Activity appActivity) {
        this.appActivity = appActivity;
    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onCreate(Bundle savedInstanceState) {}@SuppressLint("MissingSuperCall")
    @Override
    public void onStart(a) {}@SuppressLint("MissingSuperCall")
    @Override
    public void onResume(a) {}@SuppressLint("MissingSuperCall")
    @Override
    public void onDestroy(a) {}// This is actually the host setContentView method
    public void setContentView(int resId) {
        appActivity.setContentView(resId);
    }

    public View findViewById(int layoutId) {
        return appActivity.findViewById(layoutId);
    }

    @Override
    public void startActivity(Intent intent) {
        Intent intentNew = new Intent();
        intentNew.putExtra("className", intent.getComponent().getClassName()); // TestActivity full class nameappActivity.startActivity(intentNew); }}Copy the code
/ / BaseActivity is the key
public class PluginActivity extends BaseActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.plugin_main);

        // This will report an error because the plug-in is not installed and there is no component environment, so the host environment must be used
        Toast.makeText(appActivity, "I'm a plug-in.", Toast.LENGTH_SHORT).show();
		
        findViewById(R.id.bt_start_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            	// This startActivity follows the startActivity of BaseActivity, which is also propped
                startActivity(newIntent(appActivity, TestActivity.class)); }}); }}Copy the code

3. Package the plug-in module apK and load the APK in the host

The loading of plug-in APK is divided into two steps: loading classes and loading resources. The custom DexClassLoader is used to load the classes and the addAssetPath method of the AssetManager is used to load the resources.

The specific code is as follows:

public class PluginManager {

    private static final String TAG = PluginManager.class.getSimpleName();

    private static PluginManager pluginManager;

    private Context context;

    public static PluginManager getInstance(Context context) {
        if (pluginManager == null) {
            synchronized (PluginManager.class) {
                if (pluginManager == null) {
                    pluginManager = newPluginManager(context); }}}return pluginManager;
    }

    public PluginManager(Context context) {
        this.context = context;
    }
    
    private DexClassLoader dexClassLoader;
    private Resources resources;

    /** * 1; /** * 2
    public void loadPlugin(a) {
        try {
            File file = AssetUtils.copyAssetPlugin(context, "p.apk"."plugin");
            if(! file.exists()) { Log.d(TAG,"Plug-in pack does not exist...");
                return;
            }
            String pluginPath = file.getAbsolutePath();
            File fileDir = context.getDir("pDir", Context.MODE_PRIVATE);
            dexClassLoader = new DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, context.getClassLoader());

         
            // Load resources
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = assetManager.getClass().getMethod("addAssetPath", String.class); 
            addAssetPathMethod.invoke(assetManager, pluginPath); // Plugin package path pluginPath
            Resources r = context.getResources(); // Host resource configuration information
            // Special Resources, Resources that load Resources inside the plugin
            resources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration()); 
        } catch(Exception e) { e.printStackTrace(); }}public ClassLoader getClassLoader(a) {
        return dexClassLoader;
    }

    public Resources getResources(a) {
        returnresources; }}Copy the code

4. Define placeholder activities in the host

The most important steps here are:

1. Override the getResources and getClassLoader methods to use the plugin’s ClassLoader and the plugin’s Resources

2. Instantiate the plug-in Activity

Inject context into the plug-in Activity

4. Call the onCreate method of the plug-in Activity

The code is as follows:

public class ProxyActivity extends Activity {

    // We are using resources from the plug-in
    @Override
    public Resources getResources(a) {
        return PluginManager.getInstance(this).getResources();
    }

    // The class loader from the plug-in is used here
    @Override
    public ClassLoader getClassLoader(a) {
        return PluginManager.getInstance(this).getClassLoader();
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Actually load the Activity inside the plugin
        String className = getIntent().getStringExtra("className");

        try {
            Class mPluginActivityClass = getClassLoader().loadClass(className);
            // Instantiate the Activity in the plugin package
            Constructor constructor = mPluginActivityClass.getConstructor(new Class[]{});
            Object mPluginActivity = constructor.newInstance(new Object[]{});

            ActivityInterface activityInterface = (ActivityInterface) mPluginActivity;

            // Inject context into the plug-in
            activityInterface.insertAppContext(this);

            Bundle bundle = new Bundle();
            bundle.putString("appName"."I'm a message from the host.");

            // Execute the onCreate method in the plugin
            activityInterface.onCreate(bundle);

        } catch(Exception e) { e.printStackTrace(); }}@Override
    public void startActivity(Intent intent) {
        String className = intent.getStringExtra("className");

        Intent proxyIntent = new Intent(this, ProxyActivity.class);
        proxyIntent.putExtra("className", className); / / package name + TestActivity
        // To push TestActivity
        super.startActivity(proxyIntent); }}Copy the code

3, summary

Now that we are ready to implement a simple placeholder plug-in, let’s summarize the steps:

1. Define a standard between the host and the plug-in, such as IActivityInterface for activities

2. Implement the plug-in module according to the standard, and type it into apK file (the most important thing here is that the context used in the plug-in is passed from the host)

3. Load the plug-in module APK in the host

4. Define a placeholder Activity. In the OnCreate method, the placeholder Activity context is injected into the plug-in Activity based on the Activity information carried by the Intent. It starts by calling the onCreate method of the plug-in Activity instance

The advantages of this implementation are: only a small amount of reflection, no hook system operation, simple adaptation.

The disadvantages are also obvious. In plug-in activities, you need to follow host rules, and if you want to make a framework, the problem of invasions is difficult to solve

2. Hook plug-in

After learning placeholder plug-in, let’s introduce a way to use this in the plug-in, using hook system API way to achieve plug-in

1, the characteristics of

1. Activities in plug-ins can use this, just like normal writing, without conforming to the standard as placeholders

2. There are many hook operations, and there are mainly two links that need hook. One is hook spoofing AMS, which starts activities that are not registered in AndroidManifest. The other is a hook implementation that merges the dex of the plug-in with the dex of the host, replacing the original dexElements

2, the principle of

1. StartActivity process

Have you declared this Activity in your Androidmanifest.xml error: have you declared this Activity in your Androidmanifest.xml This is triggered when AMS checks the Activity to be started after calling startActivity

That is, if we want to cheat AMS, the Activity in the Intent carried by startActivity must be an Activity registered in the AndroidManifest, not the Activity in our plugin.

What about here? Steal dragon to phoenix, steal beam to replace pillar, leopard cat for prince ~

The solution is to temporarily replace the Component in the Intent with a placeholder Activity (as declared in the AndroidManifest) and store the actual plug-in Activity to be launched in the Intent as a parameter.

When AMS sends the LAUNCH_ACTIVITY event to the App, the actual Activity is started

That’s the basic principle, so the key questions to solve are as follows:

1. When invoking an Activity, replace the Intent that starts the plug-in Activity with the Intent that starts the placeholder Activity, and store the Intent that starts the plug-in Activity with parameters in the Intent that starts the placeholder Activity

When AMS sends a LAUNCH_ACTIVITY event, it intercepts the Intent and replaces it with the Intent that started the Activity

3. When you start an Activity, the default ClassLoader is used to load the Activity class and reflect the instantiation, so you need to add the plug-in Activity to the default ClassLoader

So let’s do it one problem at a time

3. Implementation steps

Hook AMS, steal dragon to phoenix

private void hookAmsAction(a) throws Exception {

        Class mIActivityManagerClass = Class.forName("android.app.IActivityManager");

        // We need the IActivityManager object to invoke the dynamic proxy
        Static public IActivityManager getDefault(
        Class mActivityManagerNativeClass2 = Class.forName("android.app.ActivityManagerNative");
        final Object mIActivityManager = mActivityManagerNativeClass2.getMethod("getDefault").invoke(null);

        // Dynamic proxy IActivityManager
        Object mIActivityManagerProxy = Proxy.newProxyInstance(

                HookApplication.class.getClassLoader(),

                new Class[]{mIActivityManagerClass}, // The interface to listen on

                new InvocationHandler() { // callback method for the IActivityManager interface

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        if ("startActivity".equals(method.getName())) {
                            // Bypass the AMS check with ProxyActivity
                            Intent intent = new Intent(HookApplication.this, ProxyActivity.class);
                            // Store the Intent to start the plug-in Activity as a parameter
                            intent.putExtra("actionIntent", ((Intent) args[2])); 
                            args[2] = intent;
                        }
                        Log.d("hook"."Intercepted methods in IActivityManager" + method.getName());

                        // Let the system continue to execute normally
                        returnmethod.invoke(mIActivityManager, args); }});/** * To get gDefault * get the gDefault variable (object) via ActivityManagerNative */
        Class mActivityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
        Field gDefaultField = mActivityManagerNativeClass.getDeclaredField("gDefault");
        gDefaultField.setAccessible(true); / / authorization
        Object gDefault = gDefaultField.get(null);

        / / replace points
        Class mSingletonClass = Class.forName("android.util.Singleton");
        // Get this field mInstance
        Field mInstanceField = mSingletonClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true); 
        / / replace
        mInstanceField.set(gDefault, mIActivityManagerProxy); 
    }
Copy the code

2, hook the LAUNCH_ACTIVITY event to replace the Activity that will be started

/** * Hook LuanchActivity, to instantiate the Activity, to change the ProxyActivity back -- "TestActivity */"
    private void hookLuanchActivity(a) throws Exception {

        Field mCallbackFiled = Handler.class.getDeclaredField("mCallback");
        mCallbackFiled.setAccessible(true); / / authorization

        /** ** ** /** ** * Public static ActivityThread CurrenvityThread () * * Find H * */ using ActivityThread
        Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
        // Get the ActivityThrea object
        Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);

        Field mHField = mActivityThreadClass.getDeclaredField("mH");
        mHField.setAccessible(true);
        // Get the real object
        Handler mH = (Handler) mHField.get(mActivityThread);

        mCallbackFiled.set(mH, new MyCallback(mH)); // Replace to add our own implementation code
    }

    public static final int LAUNCH_ACTIVITY         = 100;

    class MyCallback implements Handler.Callback {

        private Handler mH;

        public MyCallback(Handler mH) {
            this.mH = mH;
        }

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {

                case LAUNCH_ACTIVITY:
                    // Do our own business logic (replace ProxyActivity with TestActivity)
                    Object obj = msg.obj;

                    try {
                        // We need to get the TestActivity from the previous Hook
                        Field intentField = obj.getClass().getDeclaredField("intent");
                        intentField.setAccessible(true);
                        // Retrieve the intent object to retrieve the actionIntent
                        Intent intent = (Intent) intentField.get(obj);
                        // actionIntent == The Intent of the plug-in Activity
                        Intent actionIntent = intent.getParcelableExtra("actionIntent");
                        if(actionIntent ! =null) {
							// Replace ProxyActivity with a real plug-in ActivityintentField.set(obj, actionIntent); }}catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
            }

			// The event executes normally
            mH.handleMessage(msg);
            return true; // The system does not execute down}}Copy the code

3. Merge the plug-in dex and host dex

private void pluginToAppAction(a) throws Exception {
        // Step 1: find the host dexElements and get the object PathClassLoader representing the host
        PathClassLoader pathClassLoader = (PathClassLoader) this.getClassLoader(); // The essence is PathClassLoader
        Class mBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
        // private final DexPathList pathList;
        Field pathListField = mBaseDexClassLoaderClass.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object mDexPathList = pathListField.get(pathClassLoader);

        Field dexElementsField = mDexPathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        // The essence is Element[]
        Object dexElements = dexElementsField.get(mDexPathList);

        / * * * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * * * /


        // Step 2: find the plug-in dexElements to get this object, representing the plug-in DexClassLoader-- representing the plug-in
        File pluginDirFile = getDir("plugin", Context.MODE_PRIVATE);
        File file = new File(pluginDirFile.getAbsoluteFile() + File.separator + "p.apk");
        if(! file.exists()) {throw new FileNotFoundException("No plugins found!! :" + file.getAbsolutePath());
        } else {
            Log.i("ZXX"."Find plugins:" + file.getAbsolutePath());
        }
        String pluginPath = file.getAbsolutePath();
        File fileDir = this.getDir("pluginDir", Context.MODE_PRIVATE); Data /data/ package name /pluginDir/
        DexClassLoader dexClassLoader = new
                DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, getClassLoader());

        Class mBaseDexClassLoaderClassPlugin = Class.forName("dalvik.system.BaseDexClassLoader");
        // private final DexPathList pathList;
        Field pathListFieldPlugin = mBaseDexClassLoaderClassPlugin.getDeclaredField("pathList");
        pathListFieldPlugin.setAccessible(true);
        Object mDexPathListPlugin = pathListFieldPlugin.get(dexClassLoader);

        Field dexElementsFieldPlugin = mDexPathListPlugin.getClass().getDeclaredField("dexElements");
        dexElementsFieldPlugin.setAccessible(true);
        // The essence is Element[]
        Object dexElementsPlugin = dexElementsFieldPlugin.get(mDexPathListPlugin);


        Step 3: Create a new dexElements []
        int mainDexLeng =  Array.getLength(dexElements);
        int pluginDexLeng =  Array.getLength(dexElementsPlugin);
        int sumDexLeng = mainDexLeng + pluginDexLeng;

        Int [] String[]... We need Element[]
        // Parameter two: the length of the array object
        // Element[] newDexElements
        Object newDexElements = Array.newInstance(dexElements.getClass().getComponentType(),sumDexLeng); // Create an array object


        // Step 4: host dexElements + plugin dexElements =----> merge new newDexElements
        for (int i = 0; i < sumDexLeng; i++) {
            // Fuse the host first
            if (i < mainDexLeng) {
                // Parameter one: the new container to be fused -- newDexElements
                Array.set(newDexElements, i, Array.get(dexElements, i));
            } else { // Remerge pluginsArray.set(newDexElements, i, Array.get(dexElementsPlugin, i - mainDexLeng)); }}// Step 5: Set new newDexElements to the host
        / / host
        dexElementsField.set(mDexPathList, newDexElements);

        // Handle the layout in the loaded plug-in, which is consistent with the placeholder
        doPluginLayoutLoad();
    }
Copy the code

4, summary

Hook plug-in consists of three key steps:

1. Cheat AMS to bypass AMS ‘detection of plug-in Activity, mainly by stealing dragon to phoenix

2, Hook AMS LAUNCH_ACTIVITY event to start the Activity

3. Merge the plug-in Dex and host Dex

In this way, the plug-in Activity can be used in this, low invasion. However, due to the use of a lot of hook operation, system adaptation needs to do more work. The key three operations in particular need to do a certain adaptation according to the system source code

3, LoadedApk plug-in implementation

In the plug-in implementation of hook, because all plug-ins are added to dexElements, the host and plug-in use the same ClassLoader. LoadedApk is a plugin that uses multiple ClassLoaders

1, the characteristics of

The host and the plug-in use different Classloaders

2, the principle of

The implementation of cheating AMS and stealthily changing the phoenix is the same as that of hook plug-in. The difference is that hook plug-in is to add the dex of the plug-in into the dexElements in BaseDexClassLoader to successfully load the plug-in class. LoadedApk is not. The following analysis of LoadedApk plug-in implementation principle

Look at the code that starts the Activity in ActivityThread

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

        ActivityInfo aInfo = r.activityInfo;
        if (r.packageInfo == null) {
        	// get LoadedApkr.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, Context.CONTEXT_INCLUDE_CODE); }... Omit Activity Activity =null;
        try {
        	// get the ClassLoader from LoadedApk to load the Activity class
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            // Instantiate the Activityactivity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); . omitCopy the code

The code for obtaining PackageInfo is as follows:

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
            ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
            boolean registerPackage) {
        final booleandifferentUser = (UserHandle.myUserId() ! = UserHandle.getUserId(aInfo.uid));synchronized (mResourcesManager) {
            WeakReference<LoadedApk> ref;
            if (differentUser) {
                // Caching not supported across users
                ref = null;
            } else if (includeCode) {
            	// The LoadedApk object for mPackeges can be implemented as long as the LoadedApk object is constructed and put into mPackeges
                ref = mPackages.get(aInfo.packageName);
            } else {
                ref = mResourcePackages.get(aInfo.packageName);
            }

Copy the code

3. Implementation steps

The operation of cheating AMS is the same as that of hook.

How to build LoadedApk objects and add them to mPackages in ActivityThreads

/** * create a LoadedApk.ClassLoader to add to mPackages
    private void customLoadedApkAction(a) throws Exception {
        File pluginDirFile = getDir("plugin", Context.MODE_PRIVATE);
        File file = new File( pluginDirFile.getAbsoluteFile() + File.separator + "p.apk");
        if(! file.exists()) {throw new FileNotFoundException("Plug-in pack does not exist..." + file.getAbsolutePath());
        }
        String pulginPath = file.getAbsolutePath();

        // mPackages add custom LoadedApk
        // Final ArrayMap
      
       > mPackages Add custom LoadedApk
      ,>
        Class mActivityThreadClass = Class.forName("android.app.ActivityThread");

        Public static ActivityThread currenvityThread () Gets the ActivityThread object
        Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);

        Field mPackagesField = mActivityThreadClass.getDeclaredField("mPackages");
        mPackagesField.setAccessible(true);
        // Get the mPackages object
        Object mPackagesObj = mPackagesField.get(mActivityThread);

        Map mPackages = (Map) mPackagesObj;

        // How to customize a LoadedApk, how the system created LoadedApk, how to create LoadedApk
        Public Final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo)
        Class mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
        Field defaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
        defaultField.setAccessible(true);
        Object defaultObj = defaultField.get(null);

        /** * ApplicationInfo */
        ApplicationInfo applicationInfo = getApplicationInfoAction();

        Method mLoadedApkMethod = mActivityThreadClass.getMethod("getPackageInfoNoCheck", ApplicationInfo.class, mCompatibilityInfoClass); / / class type
        // Execute to get LoedApk object
        Object mLoadedApk = mLoadedApkMethod.invoke(mActivityThread, applicationInfo, defaultObj);

        // Custom loader loads plug-ins
        // String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent

        File fileDir = getDir("pulginPathDir", Context.MODE_PRIVATE);

        // Customize the ClassLoader for loading plug-ins
        ClassLoader classLoader = new PluginClassLoader(pulginPath,fileDir.getAbsolutePath(), null, getClassLoader());

        Field mClassLoaderField = mLoadedApk.getClass().getDeclaredField("mClassLoader");
        mClassLoaderField.setAccessible(true);
        mClassLoaderField.set(mLoadedApk, classLoader); // Replace the ClassLoader in LoadedApk

        // Add a custom LoadedApk plugin specifically loaded inside the class

        // The final target is mpackages.put (plugin package name, plugin LoadedApk);
        WeakReference weakReference = new WeakReference(mLoadedApk); // Add the custom LoadedApk -- plugin
        mPackages.put(applicationInfo.packageName, weakReference); // Added our own LoadedApk
    }

    /** * Get ApplicationInfo * for the plug-in@return
     * @throws* /
    private ApplicationInfo getApplicationInfoAction(a) throws Exception {
        // Execute the public static ApplicationInfo generateApplicationInfo method to get ApplicationInfo
        Class mPackageParserClass = Class.forName("android.content.pm.PackageParser");

        Object mPackageParser = mPackageParserClass.newInstance();

        // generateApplicationInfo Method class type
        Class $PackageClass = Class.forName("android.content.pm.PackageParser$Package");
        Class mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");

        Method mApplicationInfoMethod = mPackageParserClass.getMethod("generateApplicationInfo",$PackageClass,
                int.class, mPackageUserStateClass);

        File dirFile = getDir("plugin", Context.MODE_PRIVATE);
        File file = new File(dirFile.getAbsoluteFile() + File.separator + "p.apk");
        String pulginPath = file.getAbsolutePath();

        Public Package parsePackage(File packageFile, int flags
        // Get the object that executes the method
        Method mPackageMethod = mPackageParserClass.getMethod("parsePackage", File.class, int.class);
        Object mPackage = mPackageMethod.invoke(mPackageParser, file, PackageManager.GET_ACTIVITIES);

        // Parameters Package p, int flags, PackageUserState state
        ApplicationInfo applicationInfo = (ApplicationInfo)
                mApplicationInfoMethod.invoke(mPackageParser, mPackage, 0, mPackageUserStateClass.newInstance());

        // The obtained ApplicationInfo is the plugin's ApplicationInfo
        // ApplicationInfo we get here
        / / applicationInfo. PublicSourceDir = plugin path;
        / / applicationInfo. SourceDir = plugin path;
        applicationInfo.publicSourceDir = pulginPath;
        applicationInfo.sourceDir = pulginPath;
        return applicationInfo;
    }
Copy the code

2, hook AMS starts the Activity callback

class MyCallback implements Handler.Callback {

        private Handler mH;

        public MyCallback(Handler mH) {
            this.mH = mH;
        }

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {

                case LAUNCH_ACTIVITY:
                    // Do our own business logic (replace ProxyActivity with TestActivity)
                    Object obj = msg.obj; / / ActivityClientRecord nature

                    try {
                        // We need to get the TestActivity from the previous Hook
                        Field intentField = obj.getClass().getDeclaredField("intent");
                        intentField.setAccessible(true);

                        // Retrieve the intent object to retrieve the actionIntent
                        Intent intent = (Intent) intentField.get(obj);

                        Intent actionIntent = intent.getParcelableExtra("actionIntent");

                        if(actionIntent ! =null) {

                            intentField.set(obj, actionIntent); // Replace ProxyActivity with plug-in Activity

                            /*** * We distinguish between plug-ins and hosts in the following code */
                            Field activityInfoField = obj.getClass().getDeclaredField("activityInfo");
                            activityInfoField.setAccessible(true); / / authorization
                            ActivityInfo activityInfo = (ActivityInfo) activityInfoField.get(obj);

                            // The host Intent's getPackage gets the package name, and the plug-in's getPackage is empty to determine if it is a plug-in Intent
                            if (actionIntent.getPackage() == null) { 
                                // Change the applicationInfo package name to the plugin package name so that the LoadedApk we get is our own
                                activityInfo.applicationInfo.packageName = actionIntent.getComponent().getPackageName();

                                // This is the next step, hook PMS, bypass PMS detection
                                hookGetPackageInfo();

                            } else { / / hostactivityInfo.applicationInfo.packageName = actionIntent.getPackage(); }}}catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
            }


            mH.handleMessage(msg);
            // Let the system continue to execute normally
            // return false; // The system will execute down
            return true; // The system does not execute down
        }
Copy the code

3. Hook PMS to bypass detection

Light operation also not line, so the Activity starts, the PMS will detect whether the corresponding Apk package name is installed (LoadedApk initializeJavaContextClassLoader method), is not installed an error.

Calling process: performLaunchActivity – > makeApplication – > initializeJavaContextClassLoader.

InitializeJavaContextClassLoader () method code is as follows:

IPackageManager pm = ActivityThread.getPackageManager();
        android.content.pm.PackageInfo pi;
        try {
            pi = pm.getPackageInfo(mPackageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
                    UserHandle.myUserId());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
        if (pi == null) {
            throw new IllegalStateException("Unable to get package info for "
                    + mPackageName + "; is package not installed?");
        }
Copy the code

Therefore, hook PMS is also needed to bypass detection, and the implementation code is as follows:

// Hook this getPackageInfo to do its own logic
    private void hookGetPackageInfo(a) {
        try {
            // sPackageManager replaces our own dynamic proxy
            Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
            Field sCurrentActivityThreadField = mActivityThreadClass.getDeclaredField("sCurrentActivityThread");
            sCurrentActivityThreadField.setAccessible(true);

            Field sPackageManagerField = mActivityThreadClass.getDeclaredField("sPackageManager");
            sPackageManagerField.setAccessible(true);
            final Object packageManager = sPackageManagerField.get(null);

            /** * dynamic proxy */
            Class mIPackageManagerClass = Class.forName("android.content.pm.IPackageManager");

            Object mIPackageManagerProxy = Proxy.newProxyInstance(getClassLoader(),

                    new Class[]{mIPackageManagerClass}, // The interface to listen on

                    new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            if ("getPackageInfo".equals(method.getName())) {
                                // How to bypass the PMS and cheat the system
                                // pi ! = null
                                return new PackageInfo(); // PMS detection was successfully bypassed
                            }
                            // Let the system continue as normal
                            returnmethod.invoke(packageManager, args); }});// Replace tanuki with prince with our own dynamic proxy
            sPackageManagerField.set(null, mIPackageManagerProxy);

        } catch(Exception e) { e.printStackTrace(); }}Copy the code

4, summary

LoadedApk is a plug-in, which is different from hook when the Activity is started.

The main steps are:

Imitate the system source code, plug-in APK LoadedApk instance. And placed in the mPackages object in the ActivityThread

In the mH callback of the ActivityThread, stealing the plugin Activity replaces the package name in applicationInfo in activityInfo with the plugin’s package name, allowing subsequent logic to use the plugin’s LoadedApk

Finally, hook PMS to bypass the detection of plug-in installation by PMS

4. Summary of three ways to achieve plug-in

1, placeholder plug-in is relatively stable, good compatibility, because there is no HOOK system API. But writing plug-ins can be uncomfortable because you have to keep an eye on the host context

2. Hook method to implement plug-in, without considering the host environment, but hook the system API, poor compatibility.

3, LoadedApk method to achieve plug-in, and hook method is similar, do not consider the host environment, but the system API hook, poor compatibility

3. Plug-in frameworks on the market

1. Meituan Robust

The implementation of meituan Robust plug-in is different from the three methods mentioned above, but refers to the implementation of Instant Run, which inserts a control logic code for each function in the compilation and packaging stage

Here’s a quick look at the repair process:

In the following method, a bit of control logic is added at compile time to determine whether the patch operation should be implemented or the original operation

public long getIndex(a) {
      return 100;
 }
Copy the code

The compiled getIndex

public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex(a) {
     if(changeQuickRedirect ! =null) {
         //PatchProxy encapsulates the logic to get the current className and methodName, and finally calls the corresponding function of changeQuickRedirect within it
         if(PatchProxy.isSupport(new Object[0].this, changeQuickRedirect, false)) {
             return ((Long)PatchProxy.accessDispatch(new Object[0].this, changeQuickRedirect, false)).longValue(); }}return 100L;
 }
Copy the code

When a patch is loaded, it is reflected to the changeQuickRedirect setting instance. If the instance is not empty, the plugin logic is followed

Can perfect realization, of course, is not so simple, specific reference: tech.meituan.com/2016/09/14/…

2. Tencent QZone and Tinker

Tencent’s QZone and Tinker are essentially plugins implemented in hook mode and operate dexElements

Verify; verify; verify;

When apK is installed, the virtual machine optimizes classes.dex into an odex file and then executes it. During this process, the verify operation of the class is performed. If all the classes calling the relationship are on the same dex, the CLASS_ISPREVERIFIED flag is marked and then the odex file is written.

At run time, an error is reported if the marked class references a class in another DEX.

Therefore, it is necessary to solve the problem of labeling.

QZone does this by referring to one of the other Dex classes in the constructor of each class to avoid being marked

Tinker merges the host dex with the plug-in dex, deletes the original dex from dexElements, and adds the merged dex. All codes are in the same dex, so there is no CLASS_ISPREVERIFIED problem

Iv. SDK plug-in

1. It is recommended to choose placeholder implementation

The essence of SDK plug-in is not different from that of APP plug-in, but if there are not many four components of SDK, it is very recommended to use placeholder plug-in, because there are fewer compatibility problems.

2. Customize plug-in Context

However, since the SDK is usually attached to the host Activity call, it is best not to process the host Activity’s getClassLoader and getResources to avoid affecting the host’s logic. You can then implement a Context with the plugin ClassLoader and Resouces for the Sdk to use

public class SQwanCore implements ISQwanCore {

    @Override
    public void init(Context context) {
        // Construct a Context with the plugin classLoader and Resources
        SdkContextProxy sdkContext = new SdkContextProxy(context);
        try {
            ISQwanCore sdkObj = (ISQwanCore) sdkContext.getClassLoader().loadClass("com.sq.plugin.PluginSQwanCore").newInstance();
            sdkObj.init(sdkContext);
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

The SdkContextProxy code is as follows:

public class SdkContextProxy extends ContextWrapper {

    private Context baseContext;

    public SdkContextProxy(Context base) {
        super(base);
        baseContext = base;
    }

    @Override
    public ClassLoader getClassLoader(a) {
        return PluginManager.getInstance(baseContext).getClassLoader();
    }

    @Override
    public Resources getResources(a) {
        return PluginManager.getInstance(baseContext).getResources();
    }

	// When you start an Activity, do a special operation that leads to ProxyActivity, referring to placeholder ProxyActivity
    @Override
    public void startActivity(Intent intent) {
        String className = intent.getComponent().getClassName();
        Intent proxyIntent = new Intent(this, ProxyActivity.class);
        proxyIntent.putExtra("className", className); // Package name + plug-in Activity
        // To push the plug-in Activity into the stack
        super.startActivity(proxyIntent); }}Copy the code

Plugin implementation of Sdk:

public class PluginSQwanCore implements ISQwanCore {

    @Override
    public void init(Context context) {
    	//PluginActivity is implemented according to the standard IActivityInterface
        context.startActivity(newIntent(context, PluginActivity.class)); }}Copy the code

Other logical reference placeholder plug-in can be realized

As for the add-in CLASS_ISPREVERIFIED of SDK, the simplest method is to delete the classes common to the add-in and the host. For example, the ISQwanCore class is deleted in the add-in in this case.

3. Resource processing

FAQ:

Adaptation of AssetManager (different from 19 above and below)

What can and cannot be proxied (resource references in XML files)

Isolate host and plug-in Resources (handled with ContextWrapper)

Use Gradle to modify resource IDS

How to handle a getIdentifier conflict (plug-ins are preferred and ResourceWrapper is used)

Combine host and plug-in Resources into one large resource. Why?

1. Host resources contain system resources, which is needed

2. Some SDK resources are stored in the host for easy package cutting (such as flash screen)

public class SuperHostResources {

    private Context mContext;

    private Resources mResources;

    public SuperHostResources(Context context, String pluginPath) {
        mContext = context;
        mResources = buildHostResources(pluginPath);
    }


    private Resources buildHostResources(String pluginPath) {
        Resources hostResources = mContext.getResources();
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
            try {
                AssetManager assetManager = mContext.getResources().getAssets();
                Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
                addAssetPathMethod.setAccessible(true);
                addAssetPathMethod.invoke(assetManager, pluginPath);
                hostResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
            } catch(Exception e) { e.printStackTrace(); hostResources = mContext.getResources(); }}else {
            try {
                AssetManager assetManager = AssetManager.class.newInstance();
                Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
                addAssetPathMethod.setAccessible(true);
                addAssetPathMethod.invoke(assetManager, pluginPath);
                // If you want to change the id, you need to change the id
                String baseApkPath = mContext.getApplicationInfo().sourceDir;
                addAssetPathMethod.invoke(assetManager, baseApkPath);
                hostResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());

            } catch(Exception e) { e.printStackTrace(); hostResources = mContext.getResources(); }}return hostResources;
    }


    public Resources get(a) {
        returnmResources; }}Copy the code

Use ResourceWrapper. Why?

1. For example, getIdentifier can be used to determine whether plug-ins or hosts are loaded first

public class MixResources extends ResourcesWrapper {

    private Resources mPluginResources;

    private String mPluginPkgName;

    public MixResources(Resources hostResources, Context context, String pluginPath) {
        super(hostResources);
        PluginResources pluginResourcesBuilder = new PluginResources(context, pluginPath);
        mPluginResources = pluginResourcesBuilder.get();
        mPluginPkgName = pluginResourcesBuilder.getPkgName();
    }

    public MixResources(Resources hostResources, Resources pluginResources, String pluginPkgName) {
        super(hostResources);
        mPluginResources = pluginResources;
        mPluginPkgName = pluginPkgName;
    }

    public String getPluginPkgName(a) {
        return mPluginPkgName;
    }

    @Override
    public CharSequence getText(int id) throws Resources.NotFoundException {
        try {
            return super.getText(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getText(id); }}@Override
    public String getString(int id) throws Resources.NotFoundException {
        try {
            return super.getString(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getString(id); }}@Override
    public String getString(int id, Object... formatArgs) throws Resources.NotFoundException {
        try {
            return super.getString(id,formatArgs);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getString(id,formatArgs); }}@Override
    public float getDimension(int id) throws Resources.NotFoundException {
        try {
            return super.getDimension(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getDimension(id); }}@Override
    public int getDimensionPixelOffset(int id) throws Resources.NotFoundException {
        try {
            return super.getDimensionPixelOffset(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getDimensionPixelOffset(id); }}@Override
    public int getDimensionPixelSize(int id) throws Resources.NotFoundException {
        try {
            return super.getDimensionPixelSize(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getDimensionPixelSize(id); }}@Override
    public Drawable getDrawable(int id) throws Resources.NotFoundException {
        try {
            return super.getDrawable(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getDrawable(id); }}@TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public Drawable getDrawable(int id, Resources.Theme theme) throws Resources.NotFoundException {
        try {
            return super.getDrawable(id, theme);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getDrawable(id,theme); }}@Override
    public Drawable getDrawableForDensity(int id, int density) throws Resources.NotFoundException {
        try {
            return super.getDrawableForDensity(id, density);
        } catch (Resources.NotFoundException e) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
                return mPluginResources.getDrawableForDensity(id, density);
            } else {
                return null; }}}@TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public Drawable getDrawableForDensity(int id, int density, Resources.Theme theme) {
        try {
            return super.getDrawableForDensity(id, density, theme);
        } catch (Exception e) {
            returnmPluginResources.getDrawableForDensity(id,density,theme); }}@Override
    public int getColor(int id) throws Resources.NotFoundException {
        try {
            return super.getColor(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getColor(id); }}@TargetApi(Build.VERSION_CODES.M)
    @Override
    public int getColor(int id, Resources.Theme theme) throws Resources.NotFoundException {
        try {
            return super.getColor(id,theme);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getColor(id,theme); }}@Override
    public ColorStateList getColorStateList(int id) throws Resources.NotFoundException {
        try {
            return super.getColorStateList(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getColorStateList(id); }}@TargetApi(Build.VERSION_CODES.M)
    @Override
    public ColorStateList getColorStateList(int id, Resources.Theme theme) throws Resources.NotFoundException {
        try {
            return super.getColorStateList(id,theme);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getColorStateList(id,theme); }}@Override
    public boolean getBoolean(int id) throws Resources.NotFoundException {
        try {
            return super.getBoolean(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getBoolean(id); }}@Override
    public XmlResourceParser getLayout(int id) throws Resources.NotFoundException {
        try {
            return super.getLayout(id);
        } catch (Resources.NotFoundException e) {
           returnmPluginResources.getLayout(id); }}@Override
    public String getResourceName(int resid) throws Resources.NotFoundException {
        try {
            return super.getResourceName(resid);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getResourceName(resid); }}@Override
    public int getInteger(int id) throws Resources.NotFoundException {
        try {
            return super.getInteger(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getInteger(id); }}@Override
    public CharSequence getText(int id, CharSequence def) {
        try {
            return super.getText(id,def);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getText(id,def); }}@Override
    public InputStream openRawResource(int id) throws Resources.NotFoundException {
        try {
            return super.openRawResource(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.openRawResource(id); }}@Override
    public XmlResourceParser getXml(int id) throws Resources.NotFoundException {
        try {
            return super.getXml(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getXml(id); }}@Override
    public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws Resources.NotFoundException {
        try {
            super.getValue(id, outValue, resolveRefs);
        } catch(Resources.NotFoundException e) { mPluginResources.getValue(id, outValue, resolveRefs); }}@Override
    public Movie getMovie(int id) throws Resources.NotFoundException {
        try {
            return super.getMovie(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getMovie(id); }}@Override
    public XmlResourceParser getAnimation(int id) throws Resources.NotFoundException {
        try {
            return super.getAnimation(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.getAnimation(id); }}@Override
    public InputStream openRawResource(int id, TypedValue value) throws Resources.NotFoundException {
        try {
            return super.openRawResource(id,value);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.openRawResource(id,value); }}@Override
    public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {
        try {
            return super.openRawResourceFd(id);
        } catch (Resources.NotFoundException e) {
            returnmPluginResources.openRawResourceFd(id); }}@Override
    public int getIdentifier(String name, String defType, String defPackage) {
        int pluginId = super.getIdentifier(name, defType, defPackage);
        if (pluginId <= 0) {
            return mPluginResources.getIdentifier(name, defType, mPluginPkgName);
        }
        return pluginId;
    }

    public int getIdentifierFromPlugin(String name, String defType) {
        returnmPluginResources.getIdentifier(name, defType, mPluginPkgName); }}Copy the code

Five, the summary

1. This paper introduces three common plug-in implementation schemes, including placeholder, hook, LoadedApk and their respective characteristics

2. This paper introduces the scheme of plug-in framework commonly seen in the market, and briefly describes how to avoid CLASS_ISPREVERIFIED problem

3. Introduce the plug-in of SDK and the implementation scheme of SDK Context