プラグイン アプリの 4 つの主要コンポーネントをホスト アプリにロードするには、次の手順が必要です。
1. プラグインの 4 つの主要コンポーネントをホストの 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 にマージすると、ホスト アプリに対応する 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 にマージするだけです。
上記の 2 つの手順により、通常はプラグイン アプリで 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 アプリのネイティブ ClassLoader を変更する
ClassLoader
システムを独自の に直接置き換えますZeusClassLoader
。ZeusClassLoader
はホストのコンストラクターに渡されますClassLoader
。さらに、その中にはすべてのプラグインのコレクションをmClassLoaderList
保存する変数があります。ClassLoader
したがってZeusClassLoader
、loadClass
メソッドは最初にホストを使用して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;
}
このようなフックの後、すべてのプラグインの ClassLoader が一緒になります。ただし、アクティビティ/サービスを開始する元の方法は変更する必要があります。
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
はフックされていないので当然ロードできません プラグイン内のクラスに。
メソッドが取得するのはgetClassLoader
フックした新しいクラスでありClassLoader
、プラグイン クラスにロードできます。
3. プラグインにリソースをロードする
アクティビティを除く 4 つの主要コンポーネントにはインターフェイスがないため、リソースが関与しません。アクティビティはリソース ファイルに大きく依存しているため、プラグインでアクティビティを正しく表示したい場合は、プラグインでのリソースのロードの問題を解決する必要があります。
ホストはプラグインにリソースをロードしたいと考えていますが、どうすればよいでしょうか?
新しい
AssetManager
オブジェクトを生成しnewAssetManager
、これを呼び出すメソッドを起動してプラグイン Apk のパスをnewAssetManager
ロードし、次にthis に基づいて新しいオブジェクトを生成し、ホスト Activity で書き換えて返します。これにより、ホスト Activityはプラグイン APK リソース。addAssetPath
newAssetManager
Resource
newResource
getResources
getAssets
newAssetManager
newResource
これは、ホストとプラグインのリソースを分離する方法です。ホストとプラグインのリソースをマージする別の方法もあります。
新しい AssetManager オブジェクトを作成し、addAssetPath メソッドを通じてホスト リソースとプラグイン リソースを挿入します。新しい AssetManager オブジェクトを通じて新しい Resources オブジェクトを作成します。ContextImpl の mResources 変数と LoadedApk 変数を新しい Resources オブジェクトに置き換えます。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 mergeの実行後です。このとき、ホストのプラグインのアクティビティを呼び出したい場合は、もう 1 つ行う必要があります。それは、上記のメソッドのオブジェクトをプラグインのアクティビティに渡し、メソッドを書き直すことです
mNowResources
。プラグイン アクティビティのメソッドがオブジェクトを返すgetResources
ようにします。getResources
mNowResources
しかし、これが完了すると、プラグイン内のアクティビティがコード内で明らかに開始されているのに、実際にはホスト内のアクティビティであるのはなぜでしょうか?
ホストの APK とプラグインの APK (AS で Build ==》Analazy Apk を使用) を開いて、resources.arsc ファイルを確認すると、比較した結果、2 つの 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 の生成プロセスでは、これを確認することをお勧めします。