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
ClassLoader
replace the system's with our ownZeusClassLoader
.ZeusClassLoader
will be passed into the host's constructorClassLoader
. In addition, there is a variable inside itmClassLoaderList
that savesClassLoader
the collection of all plug-ins. ThereforeZeusClassLoader
,loadClass
the method will first try to use the hostClassLoader
to load the class. If it cannot be loaded, it will traversemClassLoaderList
until it finds one that can load the classClassLoader
.
/***
* 这是一个空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.forName
methods 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 becauseClass.forName
the method is usedBootClassLoader
to load the class. This class has not been hooked, so naturally it cannot be loaded. To the class in the plug-in.
WhatgetClassLoader
the method obtains is the new class we hookedClassLoader
, 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
AssetManager
objectnewAssetManager
, launch the method that calls thisnewAssetManager
toaddAssetPath
load the path of the plug-in Apk, and then generate a new object based on this ,newAssetManager
andResource
then rewrite and returnnewResource
in the host Activity , so that the host Activity can find the plug-in Apk resources.getResources
getAssets
newAssetManager
newResource
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
mNowResources
to the Activity in the plug-in, and rewrite the method of the plug-in ActivitygetResources
so thatgetResources
the method returnsmNowResources
the 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