Activity and Service in Android host startup plug-in

Loading the four major components in the plug-in App in the host App requires the following steps:

1. Declare the four major components in the plug-in in the host's AndroidManifest file in advance
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.chinatsp.zeusstudy1">

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ZeusStudy1"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
		<activity android:name="com.chinatsp.zeusstudy1.ActivityA"/>
		<!-- 插件中的类 -->
        <service android:name="com.chinatsp.plugin1.TestService1"/>
        <activity android:name="com.chinatsp.plugin1.TestActivity1"/>
        
    </application>

</manifest>
2. The host loads the plug-in class
2.1 Merge the dex of all plug-ins to solve the loading problem of plug-in classes

Merge the plug-in dex into the host's dex, then the ClassLoader corresponding to the host App can load any class in the plug-in.

static void mergeDexs(String apkName, String dexName) {

        File dexFile = mBaseContext.getFileStreamPath(apkName);
        File optDexFile = mBaseContext.getFileStreamPath(dexName);

        try {
            BaseDexClassLoaderHookHelper.pathClassLoader(mBaseContext.getClassLoader(), dexFile, optDexFile);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
/**
 * 由于应用程序使用的ClassLoader为PathClassLoader
 * 最终继承自 BaseDexClassLoader
 * 查看源码得知,这个BaseDexClassLoader加载代码根据一个叫做
 * dexElements的数组进行, 因此我们把包含代码的dex文件插入这个数组
 * 系统的classLoader就能帮助我们找到这个类
 *
 * 这个类用来进行对于BaseDexClassLoader的Hook
 * @author weishu
 * @date 16/3/28
 */
public final class BaseDexClassLoaderHookHelper {
    public static void pathClassLoader(ClassLoader classLoader, File apkFile,File dexFile) throws IllegalAccessException,NoSuchMethodException, IOException, InvocationTargetException,
            InstantiationException,NoSuchFieldException {
        // 获取BaseDexClassLoader 中的字段 pathList
        Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(),classLoader,"pathList");
        // 获取PathList中的字段 Element[] dexElements
        Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj,"dexElements");

        // Element类型
        Class<?> elementClass = dexElements.getClass().getComponentType();
        // 创建一个数组, 用来替换原始的数组
        Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);

        // 构造插件Element
        Class[] params = {DexFile.class,File.class,};
        DexFile dexFile1 = DexFile.loadDex(apkFile.getCanonicalPath(),dexFile.getAbsolutePath(),0);
        Object[] values = {dexFile1,apkFile};
        Object dexObj = RefInvoke.createObject(elementClass,params,values);

        Object[] toAddNewElementArray = new Object[]{dexObj};
        // 把原始的elements复制进去
        System.arraycopy(dexElements,0,newElements,0,dexElements.length);
        // 插件的那个element复制进去
        System.arraycopy(toAddNewElementArray,0,newElements,dexElements.length,toAddNewElementArray.length);

        // 替换
        RefInvoke.setFieldObject(pathListObj,"dexElements",newElements);
    }
}

Just call this method in the attachBaseContext method of Applciation to merge the plug-in's dex into the host's dexElements.

Through the above two steps, we can normally open the Service and Activity classes in the plug-in App.

 public void startService1InPlugin1(View view) {
        try {
            Intent intent = new Intent();
            String serviceName = PluginManager.plugins.get(0).packageInfo.packageName + ".TestService1";
            intent.setClass(this, Class.forName(serviceName));
            startService(intent);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void startActivityInPlugin1(View view){
        try {
            Intent intent = new Intent();

            String activityName = PluginManager.plugins.get(0).packageInfo.packageName + ".TestActivity1";
            intent.setClass(this, Class.forName(activityName));

            startActivity(intent);

        }catch (Exception e){
            e.printStackTrace();
        }
    }
2.2 Modify the app’s native ClassLoader

Directly ClassLoaderreplace the system's with our own ZeusClassLoader. ZeusClassLoaderwill be passed into the host's constructor ClassLoader. In addition, there is a variable inside it mClassLoaderListthat saves ClassLoaderthe collection of all plug-ins. Therefore ZeusClassLoader, loadClassthe method will first try to use the host ClassLoaderto load the class. If it cannot be loaded, it will traverse mClassLoaderListuntil it finds one that can load the class ClassLoader.

/***
 * 这是一个空ClassLoader,主要是个容器
 * <p>
 * Created by huangjian on 2016/6/21.
 */
class ZeusClassLoader extends PathClassLoader {
    private List<DexClassLoader> mClassLoaderList = null;

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

        mClassLoaderList = new ArrayList<DexClassLoader>();
    }

    /**
     * 添加一个插件到当前的classLoader中
     */
    protected void addPluginClassLoader(DexClassLoader dexClassLoader) {
        mClassLoaderList.add(dexClassLoader);
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = null;
        try {
            //先查找parent classLoader,这里实际就是系统帮我们创建的classLoader,目标对应为宿主apk
            clazz = getParent().loadClass(className);
        } catch (ClassNotFoundException ignored) {

        }

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

        //挨个的到插件里进行查找
        if (mClassLoaderList != null) {
            for (DexClassLoader classLoader : mClassLoaderList) {
                if (classLoader == null) continue;
                try {
                    //这里只查找插件它自己的apk,不需要查parent,避免多次无用查询,提高性能
                    clazz = classLoader.loadClass(className);
                    if (clazz != null) {
                        return clazz;
                    }
                } catch (ClassNotFoundException ignored) {

                }
            }
        }
        throw new ClassNotFoundException(className + " in loader " + this);
    }
}
private static void composeClassLoader() {
        // 传入宿主的ClassLoader
        ZeusClassLoader classLoader = new ZeusClassLoader(mBaseContext.getPackageCodePath(), mBaseContext.getClassLoader());

        // 添加插件的ClassLoader
        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);
        }
		// 替换PackgeInfo和当前线程的mClassLoader
        RefInvoke.setFieldObject(mPackageInfo, "mClassLoader", classLoader);
        Thread.currentThread().setContextClassLoader(classLoader);
        mNowClassLoader = classLoader;
    }

After such Hook, the ClassLoaders of all plug-ins are together. But the original way of starting Activity/Service needs to be changed:

 public void startService1InPlugin1(View view) {
        try {
            Intent intent = new Intent();
            String serviceName = PluginManager.plugins.get(0).packageInfo.packageName + ".TestService1";
            //  intent.setClass(this, Class.forName(serviceName)); 
            intent.setClass(this, getClassLoader().loadClass(serviceName));
            startService(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

We used to use Class.forNamemethods to start Servcie, but it would throw an exception that the host Apk could not be found or the plug-in Service class could not be found. This is because Class.forNamethe method is used BootClassLoaderto load the class. This class has not been hooked, so naturally it cannot be loaded. To the class in the plug-in.
What getClassLoaderthe method obtains is the new class we hooked ClassLoader, and it can be loaded into the plug-in class.

3. Load resources in the plug-in

Except for Activity, the four major components have no interface and therefore do not involve resources. Activity relies heavily on resource files, so if you want to correctly display the Activity in the plug-in, you must solve the problem of loading the resources in the plug-in.

The host wants to load the resources in the plug-in, how do we do it?

Generate a new AssetManagerobject newAssetManager, launch the method that calls this newAssetManagerto addAssetPathload the path of the plug-in Apk, and then generate a new object based on this , newAssetManagerand Resourcethen rewrite and return newResourcein the host Activity , so that the host Activity can find the plug-in Apk resources.getResourcesgetAssetsnewAssetManagernewResource

This is a way to separate host and plugin resources. There is another way to merge host and plugin resources.

Create a new AssetManager object and insert the host and plug-in resources through the addAssetPath method; create a new Resources object through the new AssetManager object; replace the mResources variable and LoadedApk variable in ContextImpl with the new Resources object. mResources variable and empty mThem variable

So follow the above steps to achieve the following through code:

public static void reloadInstalledPluginResources(ArrayList<String> pluginPaths) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);

            // 塞入宿主的资源
            addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath());

            for(String pluginPath: pluginPaths) {
                // 塞入插件的资源
                addAssetPath.invoke(assetManager, pluginPath);
            }
            mAssetManager = assetManager;

            Resources newResources = new Resources(assetManager,
                    mBaseContext.getResources().getDisplayMetrics(),
                    mBaseContext.getResources().getConfiguration());
                    
			// 获取 ContextImpl 中的 Resources 类型的 mResources 变量,并替换它的值为新的 Resources 对象
            RefInvoke.setFieldObject(mBaseContext, "mResources", newResources);
            //这是最主要的需要替换的,如果不支持插件运行时更新,只留这一个就可以了
            RefInvoke.setFieldObject(mPackageInfo, "mResources", newResources);

            mNowResources = newResources;
            //需要清理mTheme对象,否则通过inflate方式加载资源会报错
            //如果是activity动态加载插件,则需要把activity的mTheme对象也设置为null
            RefInvoke.setFieldObject(mBaseContext, "mTheme", null);

        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

The timing of calling the above method is after executing the plug-in dex merge . If you want to call the Activity in the plug-in in the host at this time, you need to do one more thing, which is to pass the object in the above method mNowResourcesto the Activity in the plug-in, and rewrite the method of the plug-in Activity getResourcesso that getResourcesthe method returns mNowResourcesthe object. .

But after this is completed, you will find out, why is it that the Activity in the plug-in is clearly started in the code, but it is indeed the Activity in the host?
If you open the host's Apk and the plug-in's Apk (use Build ==》Analazy Apk in AS) and check the resources.arsc file, you will find that the resource files in the two APKs have the same ID after comparison. This is For problems caused by resource ID conflicts, you can modify the resource ID generated by the plug-in Apk by configuring the parameters of aapt2:

# build.gradle
android {
	...
	 aaptOptions {
            additionalParameters '--allow-reserved-package-id','--package-id','0x70'
    }
}

In this way, the resource ID generated in the plug-in Apk will start with 0x70, while the resource ID generated by our host Apk starts with 0x7f by default.

It is recommended to check this for the resource ID generation process.

Merge dex version source code
ZeusClassLoader version source code

Guess you like

Origin blog.csdn.net/jxq1994/article/details/130865274