系列文章目录:

插件化基础(一)——加载插件的类
插件化基础(二)——加载插件资源
插件化基础(三)——启动插件组件


一、了解 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 的 ContextContextImpl 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()) {Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "SplitDependencies");try {classLoader = packageInfo.getSplitClassLoader(activityInfo.splitName);splitDirs = packageInfo.getSplitPaths(activityInfo.splitName);} catch (NameNotFoundException e) {// Nothing above us can handle a NameNotFoundException, better crash.throw new RuntimeException(e);} finally {Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);}}// 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 的所有配置上下文都会依赖于这个 Resourcescontext.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, // CopycompatInfo);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 对象,名字上能判断出并不是每次都创建一个新的 Resourcesreturn 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 ResourcesImplResourcesImpl 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 时要传入 ResourcesKeyfinal 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);

看到这里,实现插件资源加载的思路也就出现了:

  1. 需要创建一个 Resources 对象,用来加载插件的资源
  2. Resources 的创建需要 AssetManager 对象
  3. AssetManager 创建的时候,需要指定资源路径

那么我们通过反射 AssetManager 的 addAssetPath(),将插件路径添加进去就可以了。

三、加载插件资源的方式

第一篇文章在讲加载插件类的时候介绍了两种方式:单 DexClassLoader 和多 DexClassLoader,区别在于一个 DexClassLoader 对象是只负责加载一个插件,还是负责加载所有插件。

资源加载也是如此的分成两种方式:

  1. 独立式:专门创建 AssetManager、Resources 加载插件资源(一个 AssetManager 只负责加载一个插件)。
  2. 合并式:插件资源和宿主资源直接合并(一个 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;@Overridepublic void onCreate() {super.onCreate();mResources = PluginManager.getInstance().loadResource(this, "XXX");}@Overridepublic Resources getResources() {return mResources == null ? super.getResources() : mResources;}
}

然后在插件的 BaseActivity 中从 Application 获取 Resources:

 @Overridepublic Resources getResources() {if (getApplication() != null && getApplication().getResources() != null) {return getApplication().getResources();}return super.getResources();}

这样做虽然确实能在宿主中加载到插件的资源,但是有两个问题:

  1. 宿主的 Application 中的 getResources() 返回的是插件的 Resources,而不是宿主的;
  2. 插件的 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 不能是 ActivityResources resources = context.getResources();return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());} catch (Exception e) {e.printStackTrace();}return null;}
}

插件的 BaseActivity 在调用 getResources() 时,传递的 Context 应该是 Application 的,不能是 Activity 的:

 @Overridepublic 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 为 0x7fcase 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,修改方式有三种:

  1. 修改 aapt 工具源码,在编译期间就进行修改,参考文章Android中如何修改编译的资源ID值
  2. 修改 aapt 工具的产物,在编译后期重新整理插件的资源,编排 ID,参考文章插件化-解决插件资源ID与宿主资源ID冲突的问题
  3. 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 referenceat 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 referenceat 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 为 nullmDecorContentParent = (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 并且绑定加载插件资源的 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;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);Resources resources = LoadUtil.getResources(getApplicationContext(), PLUGIN_APK_PATH);mContext = new ContextThemeWrapper(getBaseContext(), 0);// 替换 ContextImpl 中的 mResourcesClass<? extends Context> clazz = mContext.getClass();try {Field mResourcesField = clazz.getDeclaredField("mResources");mResourcesField.setAccessible(true);mResourcesField.set(mContext, resources);} catch (Exception e) {e.printStackTrace();}}@Overridepublic 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 {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);View view = LayoutInflater.from(mContext).inflate(R.layout.activity_plugin, null);setContentView(view);}
}

这样就避免空指针异常正常加载了:

插件化基础(二)——加载插件资源相关推荐

  1. 携程Android App插件化和动态加载实践

    转载自:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading?email=947091870@qq.com 编者按:本文为携程无 ...

  2. wi8ndows无法加载,Win8.1系统更新Flash插件后无法自动加载插件怎么办

    当前,为了实现各种编码格式的音频.视频节目的播放,不少第三方媒体提供商均开发了自己的播放器插件,提供音频.视频节目的网站一般会提示您下载.安装相应的插件或程序.Flash是Win8.1系统内置的播放器 ...

  3. (4.6.29.3)插件化之代码加载:启动Activity等四大组件之hook方式

    文章目录 一.代理模式和Hook原理 1.1 Hook 原理 1.2 代理模式 二.Binder Hook 2.1 分析:系统服务的获取过程 2.2 寻找Hook点 2.3 hook Binder示例 ...

  4. 【Android 插件化】基于插件化的恶意软件的加载策略分析 ( 自定义路径加载插件 | 系统路径加载插件 | 用户同意后加载插件 | 隐藏恶意插件 )

    文章目录 一.自定义路径加载插件 二.系统路径加载插件 三.用户同意后加载插件 四.隐藏恶意插件 一.自定义路径加载插件 插件化应用中 , 宿主应用 加载 插件 APK , 需要获取该插件 APK 文 ...

  5. Android插件化原理—ClassLoader加载机制

    前面<Android 插件化原理学习 -- Hook 机制之动态代理>一文中我们探索了一下动态代理 hook 实现了 启动没有在 AndroidManifest.xml 中显式声明的 Ac ...

  6. iOS之深入解析CocoaPods的插件机制和如何加载插件整合开发工具

    一.CocoaPods 插件机制 ① Ruby 在 Ruby 中,类永远是开放的,总是可以将新的方法加入到已有的类中,除了自己的代码中,还可以用在标准库和内置类中,这个特性被称为 Ruby Open ...

  7. idea安装插件plugins时无法加载插件三种解决方法(亲测有效且下载速度飞起)

    注意:统一链接测试是否可以链接插件官网方法(建议每种方法都测试链接): 1.打开 2.点击check connection 3.在弹出款中输入plugins.jetbrains.com 4.点击测试 ...

  8. java类加载器 架构 设计_类加载器(DexClassLoader)与插件化(动态加载)

    类加载器与插件化解析 2.1 类装载器 DexClassLoader 首先,我们需要了解关于java代码本地import的一些知识: import中所引用的类有两个特点: 1.必须存在于本地,当程序运 ...

  9. vs插件html,VSCode插件推荐-html实时加载插件-live server

    很多刚接触前端的小伙伴们在开发 html 页面的时候觉得调试很不方便.因为每一次进行html 代码的更改的时候,要先保存 html 代码,然后用浏览器打开这个代码.而一个 html 页面在开发的时候总 ...

  10. 【Android 插件化】Hook 插件化框架 ( 加载插件包资源 )

    Android 插件化系列文章目录 [Android 插件化]插件化简介 ( 组件化与插件化 ) [Android 插件化]插件化原理 ( JVM 内存数据 | 类加载流程 ) [Android 插件 ...

最新文章

  1. CTA核心技术及应用峰会开幕!(附第二日参会攻略)
  2. 笔记本打字不知道按了什么键,打字老出现数字?
  3. 计算机网络按功能自底而上划分,大连理工大学2011计算机期末模拟题3
  4. 【android-tips】adb 常用命令汇总(持续更新中)
  5. 聊聊成为大神路上的过程
  6. 京瓷6525_京瓷6525扫描怎么设置?
  7. 【完美解决】Could not process result for mapping: ResultMapping{property=‘null‘, column=‘xxx‘, javaType=
  8. SAP UI5 oSelectedItem.getBindingContext(json)
  9. websocket底层处理粘包_Socket解决粘包问题1
  10. 小米冲击高端,这次能否成功?
  11. 澜起科技加速中国本土数据中心解决方案进程
  12. 【滤波器】基于matlab低通滤波器(LPF)设计【含Matlab源码 323期】
  13. 没有鼠标怎么打开笔记本的触摸板
  14. 微信开发者工具使用bug
  15. 李庄 220kV变电站电气部分初步设计
  16. 前端数据可视化之使用 canvas、svg、zrender画图
  17. Data URL实现用户头像上传
  18. 项目实训(十三)安装pun,pun的基础使用和概念
  19. 年面向大学生的 9 个最佳 Chrome 扩展程序
  20. 【面霸系列 - 3】初级java如何挖掘自身的优势

热门文章

  1. 杨幂穿搭有三宝:露腿,收腰,配饰亮点,赶快马起来
  2. 推荐10个免费实用的资源网站,值得收藏
  3. 抖音最常见的4种违规方式,不注意很可能会被封号!
  4. 抖音账号如何做好私域流量,私域流量是什么
  5. 猴子吃桃问题:(非常简单易懂的方法)
  6. 苹果手机人脸识别不了是什么原因_iPhone和安卓手机的人脸识别有什么区别?
  7. Java包装类及自动装箱、拆箱
  8. deepin深度操作系统
  9. HTML||从一个页面跳转至另一个html页面的子页面(超链接)
  10. 【ESD专题】3.ESD防护器件(TVS管的原理和选型)