插件化基础(二)——加载插件资源
系列文章目录:
插件化基础(一)——加载插件的类
插件化基础(二)——加载插件资源
插件化基础(三)——启动插件组件
一、了解 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);
看到这里,实现插件资源加载的思路也就出现了:
- 需要创建一个 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;@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();}
这样做虽然确实能在宿主中加载到插件的资源,但是有两个问题:
- 宿主的 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 不能是 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,修改方式有三种:
- 修改 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 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);}
}
这样就避免空指针异常正常加载了:
插件化基础(二)——加载插件资源相关推荐
- 携程Android App插件化和动态加载实践
转载自:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading?email=947091870@qq.com 编者按:本文为携程无 ...
- wi8ndows无法加载,Win8.1系统更新Flash插件后无法自动加载插件怎么办
当前,为了实现各种编码格式的音频.视频节目的播放,不少第三方媒体提供商均开发了自己的播放器插件,提供音频.视频节目的网站一般会提示您下载.安装相应的插件或程序.Flash是Win8.1系统内置的播放器 ...
- (4.6.29.3)插件化之代码加载:启动Activity等四大组件之hook方式
文章目录 一.代理模式和Hook原理 1.1 Hook 原理 1.2 代理模式 二.Binder Hook 2.1 分析:系统服务的获取过程 2.2 寻找Hook点 2.3 hook Binder示例 ...
- 【Android 插件化】基于插件化的恶意软件的加载策略分析 ( 自定义路径加载插件 | 系统路径加载插件 | 用户同意后加载插件 | 隐藏恶意插件 )
文章目录 一.自定义路径加载插件 二.系统路径加载插件 三.用户同意后加载插件 四.隐藏恶意插件 一.自定义路径加载插件 插件化应用中 , 宿主应用 加载 插件 APK , 需要获取该插件 APK 文 ...
- Android插件化原理—ClassLoader加载机制
前面<Android 插件化原理学习 -- Hook 机制之动态代理>一文中我们探索了一下动态代理 hook 实现了 启动没有在 AndroidManifest.xml 中显式声明的 Ac ...
- iOS之深入解析CocoaPods的插件机制和如何加载插件整合开发工具
一.CocoaPods 插件机制 ① Ruby 在 Ruby 中,类永远是开放的,总是可以将新的方法加入到已有的类中,除了自己的代码中,还可以用在标准库和内置类中,这个特性被称为 Ruby Open ...
- idea安装插件plugins时无法加载插件三种解决方法(亲测有效且下载速度飞起)
注意:统一链接测试是否可以链接插件官网方法(建议每种方法都测试链接): 1.打开 2.点击check connection 3.在弹出款中输入plugins.jetbrains.com 4.点击测试 ...
- java类加载器 架构 设计_类加载器(DexClassLoader)与插件化(动态加载)
类加载器与插件化解析 2.1 类装载器 DexClassLoader 首先,我们需要了解关于java代码本地import的一些知识: import中所引用的类有两个特点: 1.必须存在于本地,当程序运 ...
- vs插件html,VSCode插件推荐-html实时加载插件-live server
很多刚接触前端的小伙伴们在开发 html 页面的时候觉得调试很不方便.因为每一次进行html 代码的更改的时候,要先保存 html 代码,然后用浏览器打开这个代码.而一个 html 页面在开发的时候总 ...
- 【Android 插件化】Hook 插件化框架 ( 加载插件包资源 )
Android 插件化系列文章目录 [Android 插件化]插件化简介 ( 组件化与插件化 ) [Android 插件化]插件化原理 ( JVM 内存数据 | 类加载流程 ) [Android 插件 ...
最新文章
- CTA核心技术及应用峰会开幕!(附第二日参会攻略)
- 笔记本打字不知道按了什么键,打字老出现数字?
- 计算机网络按功能自底而上划分,大连理工大学2011计算机期末模拟题3
- 【android-tips】adb 常用命令汇总(持续更新中)
- 聊聊成为大神路上的过程
- 京瓷6525_京瓷6525扫描怎么设置?
- 【完美解决】Could not process result for mapping: ResultMapping{property=‘null‘, column=‘xxx‘, javaType=
- SAP UI5 oSelectedItem.getBindingContext(json)
- websocket底层处理粘包_Socket解决粘包问题1
- 小米冲击高端,这次能否成功?
- 澜起科技加速中国本土数据中心解决方案进程
- 【滤波器】基于matlab低通滤波器(LPF)设计【含Matlab源码 323期】
- 没有鼠标怎么打开笔记本的触摸板
- 微信开发者工具使用bug
- 李庄 220kV变电站电气部分初步设计
- 前端数据可视化之使用 canvas、svg、zrender画图
- Data URL实现用户头像上传
- 项目实训(十三)安装pun,pun的基础使用和概念
- 年面向大学生的 9 个最佳 Chrome 扩展程序
- 【面霸系列 - 3】初级java如何挖掘自身的优势