系列文章目录:
插件化基础(一)——加载插件的类
插件化基础(二)——加载插件资源
插件化基础(三)——启动插件组件
一、了解 Asset 和 Resources
我们加载的资源通常来自 res 和 assets 两个目录,加载它们的方式有所不同:
// 使用 Resources 获取 res 目录下的资源
String appName = getResources().getString(R.string.app_name);
// 使用 AssetManager 获取 assets 目录下的资源
InputStream inputStream = getAssets().open("xxx.png");
表面上看起来似乎是不同的加载方式,但查看源码,会发现二者实际上都是通过 AssetManager 去执行资源加载的。就比如说通过 Resources 获取字符串的过程:
public String getString(@StringRes int id) throws NotFoundException {
return getText(id).toString();
}
public CharSequence getText(@StringRes int id) throws NotFoundException {
// ResourcesImpl.getAssets() 获取到 AssetManager,再去获取字符串
CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
if (res != null) {
return res;
}
throw new NotFoundException("String resource ID #0x"
+ Integer.toHexString(id));
}
内部实际上会先拿到 ResourcesImpl 中的 AssetManager 再去获取相应资源。
既然都是用 AssetManager 去加载资源,那两种加载资源的方式有何区别呢?
- Resources 主要用来访问被编译过的应用资源文件,在访问这些文件之前,会先根据资源 ID 查找得到对应的资源文件名
- AssetManager 既可以通过文件名访问那些被编译过的,也可以访问没有被编译过的资源文件
两种加载方式的不同其实也映射出 res 和 assets 两个资源目录的差别:
- res:系统会为 res 目录下的所有资源文件生成一个 ID,这意味着很容易就可以访问到这个资源,甚至在 xml 中都是可以访问的,使用 ID 访问的速度是最快的
- assets:不会生成 ID,只能通过 AssetManager 访问,xml 中不能访问,访问速度会慢些,不过操作更加方便
二、系统是如何加载资源的
了解宿主加载资源的方式,会给我们加载插件资源提供灵感。
在上一节中,我们提到 Resources 内部都会有一个 AssetManager 来真正执行资源的加载,而 Resources 又会与 Context 绑定,所以了解 Context 是如何创建 Resources 对象的就成为了关键。由于 Application 与四大组件都有类似的过程,限于篇幅,我们以 Activity 为例看看源码是怎么做的。
先上时序图,结合源码更易理解:
系统在启动一个 Activity 时会调用 handleLaunchActivity(),由 performLaunchActivity() 返回一个 Activity 对象,接着在 createBaseContextForActivity() 中创建 Activity 的 Context:
private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
final int displayId;
try {
displayId = ActivityManager.getService().getActivityDisplayId(r.token);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
// 创建 Activity 的 Context
ContextImpl appContext = ContextImpl.createActivityContext(
this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
...
return appContext;
}
ContextImpl 的 createActivityContext() 来负责 Context 的创建工作:
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
Configuration overrideConfiguration) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
String[] splitDirs = packageInfo.getSplitResDirs();
ClassLoader classLoader = packageInfo.getClassLoader();
if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
try {
classLoader = packageInfo.getSplitClassLoader(activityInfo.splitName);
splitDirs = packageInfo.getSplitPaths(activityInfo.splitName);
} catch (NameNotFoundException e) {
...
} finally {
...
}
}
// 1.先创建一个 ContextImpl 对象
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
activityToken, null, 0, classLoader);
// Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY.
displayId = (displayId != Display.INVALID_DISPLAY) ? displayId : Display.DEFAULT_DISPLAY;
final CompatibilityInfo compatInfo = (displayId == Display.DEFAULT_DISPLAY)
? packageInfo.getCompatibilityInfo()
: CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
final ResourcesManager resourcesManager = ResourcesManager.getInstance();
// 2.创建一个基础 Resources 对象,该 Activity 的所有配置上下文都会依赖于这个 Resources
context.setResources(resourcesManager.createBaseActivityResources(activityToken,
packageInfo.getResDir(),
splitDirs,
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
classLoader));
context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
context.getResources());
return context;
}
通过 ResourcesManager 的 createBaseActivityResources() 创建一个 Resources 对象:
public @Nullable Resources createBaseActivityResources(@NonNull IBinder activityToken,
@Nullable String resDir,
@Nullable String[] splitResDirs,
@Nullable String[] overlayDirs,
@Nullable String[] libDirs,
int displayId,
@Nullable Configuration overrideConfig,
@NonNull CompatibilityInfo compatInfo,
@Nullable ClassLoader classLoader) {
try {
...
// resDir 是资源路径,可以为 null(则只加载 framework 中的资源)
final ResourcesKey key = new ResourcesKey(
resDir,
splitResDirs,
overlayDirs,
libDirs,
displayId,
overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
synchronized (this) {
// Force the creation of an ActivityResourcesStruct.
getOrCreateActivityResourcesStructLocked(activityToken);
}
// Update any existing Activity Resources references.
updateResourcesForActivity(activityToken, overrideConfig, displayId,
false /* movedToDifferentDisplay */);
// 根据 key 请求一个真实的 Resources 对象,名字上能判断出并不是每次都创建一个新的 Resources
return getOrCreateResources(activityToken, key, classLoader);
} ...
}
getOrCreateResources() 会根据给定的 ResourcesKey 是否存在,而决定创建一个新的 Resources 还是直接返回一个已经缓存过的 Resources 对象:
// ResourcesImpl 的缓存
private final ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls =
new ArrayMap<>();
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
// 1.同步块中会根据 key 去 mResourceImpls 缓存中查找是否有对应的 Resources,有就直接返回
synchronized (this) {
if (activityToken != null) {
final ActivityResources activityResources =
getOrCreateActivityResourcesStructLocked(activityToken);
...
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
}
} else {
...
// Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
}
}
// 2.代码运行到这里,说明没能根据 key 找到 Resources,所以就创建一个
ResourcesImpl resourcesImpl = createResourcesImpl(key);
if (resourcesImpl == null) {
return null;
}
synchronized (this) {
ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
if (existingResourcesImpl != null) {
resourcesImpl.getAssets().close();
resourcesImpl = existingResourcesImpl;
} else {
// 将这个 resourcesImpl 放入缓存
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
}
final Resources resources;
if (activityToken != null) {
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
} else {
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
return resources;
}
}
在第 2 步调用 createResourcesImpl() 创建一个 ResourcesImpl 时,会先通过 createAssetManager() 创建一个 AssetManager 对象,并将其与新创建的 ResourcesImpl 绑定:
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
// 注意创建 AssetManager 时要传入 ResourcesKey
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
// 将 assets 与 impl 绑定
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
return impl;
}
在用 createAssetManager() 创建 AssetManager 时,会检查传入的 ResourcesKey 中的各种资源路径,如果非空,AssetManager 就会通过 addAssetPath() 向 AssetManager 添加这些额外的资源:
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
AssetManager assets = new AssetManager();
if (key.mResDir != null) {
if (assets.addAssetPath(key.mResDir) == 0) {
return null;
}
}
if (key.mSplitResDirs != null) {
for (final String splitResDir : key.mSplitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
return null;
}
}
}
if (key.mOverlayDirs != null) {
for (final String idmapPath : key.mOverlayDirs) {
assets.addOverlayPath(idmapPath);
}
}
if (key.mLibDirs != null) {
for (final String libDir : key.mLibDirs) {
if (libDir.endsWith(".apk")) {
// Avoid opening files we know do not have resources,
// like code-only .jar files.
if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
}
return assets;
}
addAssetPath() 内最终会调用到 native 方法 addAssetPathNative() 去执行添加操作:
public final int addAssetPath(String path) {
return addAssetPathInternal(path, false);
}
private final int addAssetPathInternal(String path, boolean appAsLib) {
synchronized (this) {
int res = addAssetPathNative(path, appAsLib);
makeStringBlocks(mStringBlocks);
return res;
}
}
private native final int addAssetPathNative(String path, boolean appAsLib);
看到这里,实现插件资源加载的思路也就出现了:
- 需要创建一个 Resources 对象,用来加载插件的资源
- Resources 的创建需要 AssetManager 对象
- AssetManager 创建的时候,需要指定资源路径
那么我们通过反射 AssetManager 的 addAssetPath(),将插件路径添加进去就可以了。
三、加载插件资源的方式
第一篇文章在讲加载插件类的时候介绍了两种方式:单 DexClassLoader 和多 DexClassLoader,区别在于一个 DexClassLoader 对象是只负责加载一个插件,还是负责加载所有插件。
资源加载也是如此的分成两种方式:
- 独立式:专门创建 AssetManager、Resources 加载插件资源(一个 AssetManager 只负责加载一个插件)
- 合并式:插件资源和宿主资源直接合并(一个 AssetManager 加载宿主与所有插件)
下面我们分别来介绍这两种方式的实现方法。
3.1 独立式
正如我们前面说的那样,反射得到 AssetManager 对象,并在调用 addAssetPath() 时将插件路径传递进去即可:
public Resources loadResource(Context context, String pluginPath) {
if (TextUtils.isEmpty(pluginPath) || !new File(pluginPath).exists()) {
Log.e(TAG, "插件包路径有误!");
return null;
}
try {
// 获取 AssetManager 并执行 addAssetPath() 将插件路径传递进去
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, pluginPath);
// 创建一个绑定 assetManager 的 Resources 对象并返回
Resources resources = context.getResources();
return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
仅有以上代码并不意味着大功告成,还需要考虑一个问题,由于宿主与各个插件间的 Resources 是隔离的,当前不能相互访问资源,这该如何解决?
如果将 loadResource() 放在宿主的 Application 中:
public class MyApplication extends Application {
private Resources mResources;
@Override
public void onCreate() {
super.onCreate();
mResources = PluginManager.getInstance().loadResource(this, "XXX");
}
@Override
public Resources getResources() {
return mResources == null ? super.getResources() : mResources;
}
}
然后在插件的 BaseActivity 中从 Application 获取 Resources:
@Override
public Resources getResources() {
if (getApplication() != null && getApplication().getResources() != null) {
return getApplication().getResources();
}
return super.getResources();
}
这样做虽然确实能在宿主中加载到插件的资源,但是有两个问题:
- 宿主的 Application 中的 getResources() 返回的是插件的 Resources,而不是宿主的,这种做法影响到了宿主资源的加载
- 插件的 Application 不会被执行(由于双亲委派,只能加载宿主的 Application)
鉴于以上两点,loadResource() 放在宿主中不可行,那就尝试放在插件中:
public class LoadUtil {
private static final String TAG = LoadUtil.class.getSimpleName();
private static volatile Resources sResources;
public static Resources getResources(Context context, String pluginPath) {
if (sResources == null) {
synchronized (LoadUtil.class) {
if (sResources == null) {
sResources = loadResource(context, pluginPath);
}
}
}
return sResources;
}
public static Resources loadResource(Context context, String pluginPath) {
if (TextUtils.isEmpty(pluginPath) || !new File(pluginPath).exists()) {
Log.e(TAG, "插件包路径有误!");
return null;
}
try {
// 获取 AssetManager 并执行 addAssetPath() 将插件路径传递进去
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, pluginPath);
// 创建一个绑定 assetManager 的 Resources 对象并返回,注意这个 context 不能是 Activity
Resources resources = context.getResources();
return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
插件的 BaseActivity 在调用 getResources() 时,传递的 Context 应该是 Application 的,不能是 Activity 的:
@Override
public Resources getResources() {
Resources resources = LoadUtil.getResources(getApplication(), PLUGIN_APK_PATH);
// 插件作为单独 app 时需要返回 super.getResources()
return resources == null ? super.getResources() : resources;
}
因为假如 loadResource() 的 context 是 Activity,那么就会执行到 BaseActivity 的 getResources(),进而执行 LoadUtil.getResources(),形成循环调用,导致程序崩溃。
综上来看,独立式虽然实现了资源隔离,不会在单个 Resources 对象中发生资源冲突(注意有强调单个哦,因为多个 Resources 间可能会发生相同资源 ID 表示不同资源文件的情况,下一节会介绍),但是资源共享的过程比较麻烦。
3.2 合并式
合并式需要在执行 addAssetPath() 时将宿主与所有插件的资源路径全部加进去,实现方式与独立式的 loadResource() 很像,只不过需要一个 Resources 对象加载所有插件的资源路径:
public Resources loadResources(Context context, List<String> pluginPaths) {
if (pluginPaths == null || pluginPaths.size() == 0) {
throw new IllegalArgumentException("插件集合不能拿为空!");
}
try {
// 获取 AssetManager 并执行 addAssetPath() 将插件路径传递进去
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
for (String pluginPath : pluginPaths) {
File pluginFile = new File(pluginPath);
if (!pluginFile.exists()) {
Log.e(TAG, "插件文件不存在:" + pluginPath);
continue;
}
addAssetPathMethod.invoke(assetManager, pluginPath);
}
// 创建一个绑定 assetManager 的 Resources 对象并返回
Resources resources = context.getResources();
return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
这样做的好处是宿主与插件能直接相互访问资源,但也有一个明显的缺点是,在 Resources 内,宿主与插件可能会因为生成相同的资源 ID 而发生资源冲突。
四、资源冲突
不论使用哪种方式,都可能会产生资源冲突,原因是 apk 在编译打包的过程中会为资源生成资源 ID,而宿主与各个插件都是单独打包编译的,就难免会产生不同 apk 间的资源 ID 相同,但实际引用的资源文件不同的情况。
比如说宿主中一个 layout 资源 ID 为 0x7f0b0070:
同样的资源 ID,在插件中表示的就是另一个 layout 文件:
在介绍资源冲突的解决方法之前,先来了解资源 ID 的生成过程。
4.1 资源 ID 的生成
在编译打包 apk 时,资源会被 aapt 工具处理,生成 R.java 文件和 .ap_ 文件。
aapt 工具生成资源 ID 的时序图:
该工具源码在 /frameworks/base/tools/aapt/ 目录下,入口在 main.cpp 的 main():
int main(int argc, char* const argv[])
{
Bundle bundle;
...
if (argv[1][0] == 'v')
bundle.setCommand(kCommandVersion);
else if (argv[1][0] == 'p')
// 注意编译资源时会使用这个 kCommandPackage 的命令,后面会用到
bundle.setCommand(kCommandPackage);
// 省略其它的命令判断...
bundle.setFileSpec(argv, argc);
// 重点是这里,命令处理
result = handleCommand(&bundle);
return result;
}
handleCommand() 会根据不同的命令调用相应方法:
int handleCommand(Bundle* bundle)
{
switch (bundle->getCommand()) {
case kCommandVersion: return doVersion(bundle);
case kCommandList: return doList(bundle);
case kCommandDump: return doDump(bundle);
case kCommandAdd: return doAdd(bundle);
case kCommandRemove: return doRemove(bundle);
// 针对包进行处理
case kCommandPackage: return doPackage(bundle);
case kCommandCrunch: return doCrunch(bundle);
case kCommandSingleCrunch: return doSingleCrunch(bundle);
case kCommandDaemon: return runInDaemonMode(bundle);
default:
fprintf(stderr, "%s: requested command not yet supported\n", gProgName);
return 1;
}
}
编译资源时会调用 Command.cpp 中的 doPackage():
int doPackage(Bundle* bundle)
{
if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) {
// 去 Resource.cpp,编译资源
err = buildResources(bundle, assets, builder);
if (err != 0) {
goto bail;
}
}
}
然后创建资源表 ResourceTable:
status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuilder>& builder)
{
ResourceTable table(bundle, String16(assets->getPackage()), packageType);
}
ResourceTable 的构造函数会根据包的类型决定 packageId:
ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage, ResourceTable::PackageType type)
: mAssetsPackage(assetsPackage)
, mPackageType(type)
, mTypeIdOffset(0)
, mNumLocal(0)
, mBundle(bundle)
{
ssize_t packageId = -1;
switch (mPackageType) {
// app 的 packageId 为 0x7f
case App:
case AppFeature:
packageId = 0x7f;
break;
// 系统资源 ID 就是以 0x01 开头
case System:
packageId = 0x01;
break;
case SharedLibrary:
packageId = 0x00;
break;
default:
assert(0);
break;
}
sp<Package> package = new Package(mAssetsPackage, packageId);
mPackages.add(assetsPackage, package);
mOrderedPackages.add(package);
// Every resource table always has one first entry, the bag attributes.
const SourcePos unknown(String8("????"), 0);
getType(mAssetsPackage, String16("attr"), unknown);
}
这个 packageId 其实就是 apk 包中 resources.arsc 文件中资源 ID 的前两位:
资源 ID 由十六进制数字表示,由三部分组成:
- PackageId:apk 包的 id,默认为 0x7f
- TypeId:资源类型 id,如 layout、id、string 等都有自己的类型 id 值,从 0x01 开始按顺序递增,如 attr = 0x01,drawable = 0x02
- EntryId:TypeId 下各个资源的 id 值,从 0x0000 开始递增
正是因为我们编译的 app 的资源 ID 固定由 0x7f 开头,再加上 TypeId、EntryId 也有固定的取值范围,所以不同 apk 间发生资源冲突的概率是很大的。
4.2 解决
知道了资源冲突的原因,解决方法也就出现了,无非就是将插件资源的 PackageId 修改成其它未被系统占用的值,如 0x70~0x7e,修改方式有三种:
-
修改 aapt 工具源码,在编译期间就进行修改,参考文章Android中如何修改编译的资源ID值
-
修改 aapt 工具的产物,在编译后期重新整理插件的资源,编排 ID,参考文章插件化-解决插件资源ID与宿主资源ID冲突的问题
-
build.gradle 中配置 aaptOptions(只在 compileSdkVersion ≥ 28 时才生效):
android { aaptOptions { additionalParameters "--package-id", "0x66","--allow-reserved-package-id" } ... }
此外,独立式还可能会发生因为双亲委派机制而产生的资源冲突,当宿主与插件都使用 AppCompatActivity(demo 使用的 appcompat 版本是 1.3.0)时可能会发生如下异常:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.demo.hook.plugin/com.demo.hook.plugin.PluginActivity}: java.lang.NullPointerException: Attempt to invoke interface method 'void androidx.appcompat.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2817)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892)
...
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'void androidx.appcompat.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
at androidx.appcompat.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:903)
at androidx.appcompat.app.AppCompatDelegateImpl.ensureSubDecor(AppCompatDelegateImpl.java:809)
at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:696)
at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:195)
at com.demo.hook.plugin.PluginActivity.onCreate(PluginActivity.java:11)
...
源码是 AppCompatDelegateImpl 的 createSubDecor() 出现空指针:
private ViewGroup createSubDecor() {
...
// decor_content_parent 获取失败导致 mDecorContentParent 为 null
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(R.id.decor_content_parent);
mDecorContentParent.setWindowCallback(getWindowCallback());
...
}
获取 decor_content_parent 失败的根本原因是,这个 id 在宿主和插件中的资源 ID 值不同:
decor_content_parent 在宿主中的资源 ID 为 0x7f08007c,在插件中为 0x7f08007d,又因为类加载机制不会重复加载已经加载过的类(包名类名相同的类),所以即使是插件中的 AppCompatDelegateImpl 运行的也是宿主的代码,所以 findViewById(R.id.decor_content_parent) 带入的是宿主的 0x7f08007c,但是在插件的资源中找到的就不是 decor_content_parent,而是 decelerateAndComplete(在上图中),获取失败导致空指针。
如果测试时宿主和插件的 decor_content_parent 资源 ID 一样导致无法复现出该问题,可以在宿主或者插件的 layout 中通过 @+id/xxx 的方式生成一个资源 ID,由于资源 ID 是根据资源名称字母顺序排列的,所以 xxx 取一个字母顺序在 decor 之前的就可以了。
这类问题的根本原因就是在加载插件资源时,使用了宿主的 Context 加载了宿主的资源。解决方法是,在插件中自定义一个属于插件的 Context 并且绑定加载插件资源的 Resources:
public class BaseActivity extends AppCompatActivity {
private static final String PLUGIN_APK_PATH = "/data/data/com.demo.hook.host/files/hook_plugin-debug.apk";
protected Context mContext;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Resources resources = LoadUtil.getResources(getApplicationContext(), PLUGIN_APK_PATH);
mContext = new ContextThemeWrapper(getBaseContext(), 0);
// 替换 ContextImpl 中的 mResources
Class<? extends Context> clazz = mContext.getClass();
try {
Field mResourcesField = clazz.getDeclaredField("mResources");
mResourcesField.setAccessible(true);
mResourcesField.set(mContext, resources);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Resources getResources() {
/* 这种方式也不行,有冲突,改在 onCreate() 中实现
// 插件没有 Application,所以 getApplicationContext()/getApplication() 拿到的都是宿主的
Resources resources = LoadUtil.getResources(getApplicationContext(), PLUGIN_APK_PATH);
// 插件作为单独 app 时需要返回 super.getResources()
return resources == null ? super.getResources() : resources;*/
return super.getResources();
}
}
然后需要显示的 Activity 通过 mContext 获取 View:
public class PluginActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View view = LayoutInflater.from(mContext).inflate(R.layout.activity_plugin, null);
setContentView(view);
}
}
这样就避免空指针异常正常加载了: