Android 宿主启动插件中的Activity和Service

在宿主App中加载插件App中的四大组件,需要以下几个步骤:

1. 预先在宿主的AndroidManifest文件中声明插件中的四大组件
<?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. 宿主加载插件的类
2.1 合并所有插件的dex 来解决插件的类的加载问题

把插件dex都合并到宿主的dex中,那么宿主App对应的ClassLoader就可以而加载插件中的任意类了

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);
    }
}

在Applciation的attachBaseContext 方法中调用该方法将插件的dex合并进宿主的dexElements中就可以了。

通过以上两步,我们就可以正常的打开插件App中的Service和Activity类了

 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 修改app原生的ClassLoader

直接把系统的ClassLoader替换为我们自己的ZeusClassLoaderZeusClassLoader 的构造函数中将会传递进宿主的ClassLoader,除此之外,其内部有一个mClassLoaderList变量,保存着所有插件ClassLoader的集合。于是ZeusClassLoaderloadClass方法,会先尝试使用宿主ClassLoader加载类,如果不能加载,就遍历mClassLoaderList,直到找到一个能加载类的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;
    }

经过这样Hook,所有插件的ClassLoader都在一起了。但是原先启动Activity/Service的方式需要改变:

 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();
        }
    }

我们之前是使用Class.forName方法来启动Servcie,会抛出找不到宿主Apk或找不到插件Service类的异常,这是因为Class.forName方法会使用BootClassLoader来加载类,这个类并没有被Hook,所以自然也就加载不到插件中的类了。
getClassLoader方法获取到的是我们Hook过的新ClassLoader,就可以加载到插件中的类了。

3. 加载插件中的资源

四大组件除了Activity,其他都是没有界面的,因此不涉及到资源。Activity则严重依赖资源文件,所以要想正确的显示插件中的Activity,必须解决加载插件中资源的问题。

宿主想要加载插件中的资源,我们是怎么做的呢?

生成一个新的AssetManager对象newAssetManager,发射调用这个newAssetManageraddAssetPath方法把插件Apk的路径加载进来,然后根据这个newAssetManager生成一个新的Resource对象newResource,然后在宿主Activity中重写getResourcesgetAssets 返回newAssetManagernewResource,这样宿主Activity就可以查找到插件Apk中的资源了。

这是一种分离宿主和插件资源的方式。还有另一种合并宿主和插件资源的方式。

创建一个新的 AssetManager 对象,并将宿主和插件的资源都通过addAssetPath方法塞入;通过新的AssetManager对象来创建出一个新的Resources对象;将新的Resources对象替换ContextImpl中的mResources变量、LoadedApk变量里的mResources变量 以及 置空mThem变量

所以按照上述步骤通过代码实现如下:

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();
        }
    }

上述方法的调用时机就是在执行完插件dex合并后调用。如果此时要在宿主中调用插件中的Activity,还需要做一件事情,就是将上面方法中的mNowResources对象传递到插件中的Activity中去,并重写插件Activity的getResources方法,使getResources方法返回mNowResources对象即可。

但是这样完成之后会发现,怎么明明代码中启动的是插件中的Activity,但显示的确是宿主中的Activity呢?
如果你把宿主的Apk和插件的Apk打开( 使用AS中的Build ==》Analazy Apk ),查看其中的resources.arsc文件,对比后会发现两个APK中的资源文件有相同的id,这就是资源id冲突引起的问题,可以通过 配置aapt2 的参数来修改插件Apk生成的资源id:

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

这样,插件Apk中生成的资源id就会以0x70开头,而我们的宿主Apk生成的资源id默认是0x7f开头。

资源id的生成过程建议查看这个

合并dex版本源码
ZeusClassLoader版本源码

猜你喜欢

转载自blog.csdn.net/jxq1994/article/details/130865274