Android插件化解析 -- 插件加载详解
插件化概述
概述
常规的Android APP是由一个主模块(application)和若干个子模块(library,打包为aar)组成,最终打包成一个apk文件,运行在Android系统中。Android插件化,就是把这些子模块打包成独立的apk,主模块apk加载这些子模块apk后,即可调用子模块中的功能,如下图所示。
Android插件化可以让一个应用(宿主)动态地加载和运行其他应用(插件)的代码和资源等,就像这些代码和资源等就是宿主自身的一部分一样。这种方式在一定程度上突破了Android系统的apk包大小限制和动态更新机制限制。
插件化可以实现如下功能:
动态模块化:减少应用初次安装包的大小,并且在需要时才下载安装其他模块;
热更新:在不需要用户升级整个应用的情况下,修复Bug或更新某个模块;
模块隔离:将不同功能或模块隔离开,降低模块间的耦合。
实现内容
完成Android APP的插件化开发,通常需要实现以下内容:
插件化配置
插件加载
so库加载
apk加载
资源加载
Application插件化
四大组件插件化
Activity插件化
Service插件化
BroadcastReceiver插件化
ContentProvider插件化
……
插件加载是插件化的一大重点,也是Application和四大组件插件化的基础,本篇文章主要对插件加载进行详细介绍。
插件加载
宿主要想使用插件,首先必须加载插件apk。宿主加载插件apk的关键步骤如下:
解析过滤插件so库;
加载插件apk,生成DexClassLoader;
加载插件资源,生成含有插件和宿主资源的MixResources;
缓存插件信息。
解析过滤插件so库
宿主加载插件的so库时,需要解析插件的apk,根据要配置的CPU架构,过滤插件apk中的so库,并把过滤后的so文件复制到一个单独的目录中(这里复制到与插件apk同级的lib目录中)。关键步骤如下所示:
获取插件apk的完整路径;
获取系统支持的宿主apk的CPU架构;
根据系统支持的宿主apk的CPU架构,获取插件最终要适配的CPU架构;
根据要适配的CPU架构,过滤插件apk的so库并保存到lib目录。
整体流程如下图所示:
第一步比较简单,这里详细介绍后三步。
获取宿主CPU架构
根据宿主的Context,获取系统支持的宿主apk的CPU架构,代码如下所示:
public static String getCpuArchByAppInfo(Context context) {ApplicationInfo applicationInfo = context.getApplicationInfo();String cpuArch = "";try {// 应用在安装过程中,系统就已经确定这个应用的首选CPU架构,并将这个结果保存在应用的ApplicationInfo的primaryCpuAbi属性值中。// primaryCpuAbi是应用级别的信息,而非系统级别的信息。Field primaryCpuAbiField = ApplicationInfo.class.getDeclaredField("primaryCpuAbi");primaryCpuAbiField.setAccessible(true);Object o1 = primaryCpuAbiField.get(applicationInfo);if (o1 != null) {cpuArch = o1.toString();}} catch (Exception e) {e.printStackTrace();}return cpuArch;}
获取适配CPU架构
根据系统支持的宿主apk的CPU架构,获取插件最终要适配的CPU架构,关键步骤如下所示:
如果系统支持的宿主apk的CPU架构名称与自定义支持的CPU架构数组(supportCpuABI)中的某个名称一致,就返回宿主的CPU架构名称;否则,下一步:
如果宿主的CPU架构名称以supportCpuABI中的某个架构名称开头,就返回支持的CPU架构名称;否则,下一步:
获取系统支持的CPU架构名称数组(cpuABIs),然后遍历cpuABIs和supportCpuABI,如果有相同的名称,就返回;否则,返回默认的CPU架构名称"armeabi-v7a"。
详细代码,如下所示:
private static final String CPU_ARMEABI = "armeabi";private static final String CPU_ARMEABI_V7 = "armeabi-v7a";private static final String CPU_ARMEABI_V8 = "arm64-v8a";private static final String CPU_X86 = "x86";private static final String CPU_X86_64 = "x86_64";public static String chooseByX86andArm(Context context) {// 获取宿主的CPU架构String hostCpu = getCpuArchByAppInfo(context);// 支持的CPU架构,可自定义String[] supportCpuABIs = {CPU_ARMEABI, CPU_ARMEABI_V7, CPU_ARMEABI_V8, CPU_X86, CPU_X86_64};for (String supportCpuABI : supportCpuABIs) {// 如果宿主的CPU架构与自定义支持的CPU架构一致,则返回宿主的CPU架构if (hostCpu.equalsIgnoreCase(supportCpuABI)) {return hostCpu;}}for (String supportCpuABI : supportCpuABIs) {// 如果宿主的CPU架构名称以支持的某个CPU架构名称开头,就返回支持的CPU架构名称if (hostCpu.startsWith(supportCpuABI)) {return supportCpuABI;}}// 获取系统支持的CPU架构名称数组,然后遍历比对supportCpuABI,获取最终的架构名称return getCpuArch(supportCpuABIs);}public static String getCpuArch(String[] supportCpuABIs) {String[] cpuABIs = getCpuABIs();if (cpuABIs != null && supportCpuABIs != null) {for (String cpuABI : cpuABIs) {for (String supportCpuABI : supportCpuABIs) {if (cpuABI.equalsIgnoreCase(supportCpuABI)) {return supportCpuABI;}}}}return CPU_ARMEABI_V7;}/*** 获取系统支持的CPU架构* */@SuppressLint("NewApi")public static String[] getCpuABIs() {if (Build.VERSION.SDK_INT >= 21) {return Build.SUPPORTED_ABIS;} else {return new String[]{Build.CPU_ABI, Build.CPU_ABI2};}}
过滤插件so库
这一步需要遍历插件apk中的so文件,当so的CPU架构与要适配的CPU架构一致时,就把该so复制到插件apk的同级目录lib中(若lib目录不存在,需先创建)。
代码如下所示:
/*** 解析插件的 so 库** @param context 宿主Context* @param apkFile 插件apk的File文件* @param libDir lib目录的路径,与插件apk在同级目录中* */public static boolean releaseSoFile(Context context, File apkFile, String libDir) {/*第一步:创建lib的File文件*/File libDirFile = new File(libDir);if (!libDirFile.exists()) {libDirFile.mkdirs();} else {for (String s : libDirFile.list()) {new File(s).delete();}}/*第二步:获取插件apk要适配的CPU架构*/String cpu_architect = ApkInfoUtil.chooseByX86andArm(context);/*第三步:把插件apk中CPU架构与cpu_architect一致的so复制到lib目录中*/boolean isSuccess = true;try {// apkFile是插件apk的File文件ZipFile zipFile = new ZipFile(apkFile);Enumeration<? extends ZipEntry> entries = zipFile.entries();while (entries.hasMoreElements()) {ZipEntry zipEntry = entries.nextElement();if (zipEntry.isDirectory()) {continue;}String zipEntryName = zipEntry.getName();if (zipEntryName.endsWith(".so")) {String[] entryNames = zipEntryName.split("/");if (entryNames.length > 2) {String cpu_architect_dir = entryNames[1];if (TextUtils.equals(cpu_architect_dir, cpu_architect)) {isSuccess = releaseSoFile(libDir, zipFile, zipEntry);}}}}} catch (IOException e) {e.printStackTrace();return false;}return isSuccess;}private static boolean releaseSoFile(String libDir, ZipFile zipFile, ZipEntry zipEntry) throws IOException {InputStream inputStream = zipFile.getInputStream(zipEntry);File newSoFile = new File(libDir, parseSoFileName(zipEntry.getName()));Logger.d(TAG, String.format("newSoFile:%s", newSoFile.getAbsolutePath()));return FileUtil.copyToFile(inputStream, newSoFile);}private static String parseSoFileName(String zipEntryName) {return zipEntryName.substring(zipEntryName.lastIndexOf("/") + 1);}
加载插件apk
宿主中要使用插件apk中的类、so库和资源等,就必须先加载插件apk,生成插件的ClassLoader,并添加到宿主APP的ClassLoader体系中。为便于理解,本小节先介绍类加载机制,再介绍插件apk的加载方法。
类加载机制
ClassLoader简介
ClassLoader是一个抽象类,它通过类的全限定名来获取此类的二进制字节流,核心是findClass()方法。Android中的ClassLoader类加载器派生出的有DexClassLoader和PathClassLoader,这两者的区别如下:
DexClassLoader: 能够加载未安装的jar/apk/dex
PathClassLoader: 只能加载系统中已经安装的apk
Android 9(Pie)及其以上版本中,两者已经非常相似,主要是因为PathClassLoader扩充了功能,除了已安装的apk或jar文件之外,也可以从指定路径加载.dex或.jar文件。实际使用中,如果需要加载未安装的.dex或.jar文件,DexClassLoader会更合适。
因为插件的apk没有安装在系统中,因此这里使用DexClassLoader加载插件的apk。
双亲委派模型
Android虚拟机中类加载器的调用流程如下图所示:
双亲委派模型就是,当一个类加载器加载某个类时,首先委托自己的父类加载器去加载,一直向上查找。若顶层类加载器(优先)或父类加载器能加载到该类,则返回这个类所对应的Class对象;若不能加载到,则最后再由发起请求的类加载器去加载该类。这种方式能够保证类的加载按照一定的规则次序进行,越是基础的类,越是被上层的类加载器加载,从而保证程序的安全性。
插件apk加载机制
由于ClassLoader的双亲委派模型,在加载类时,如果已经加载了插件中的类,那么就会直接使用插件中的类,而不会再加载宿主的类,反之对插件也是一样。这就很容易造成宿主和插件的类加载冲突,并且也可能会抛出异常-- java.lang.IllegalAccessError: Class ref in pre-verified class...。因此,在Android插件化时,需要自定义一个ClassLoader,修改类加载逻辑,使得插件和宿主中的类相互隔离,各自加载。
自定义ClassLoader
这里自定义一个ClassLoader -- ApkClassLoader,用来处理插件apk的加载功能,代码如下所示:
public class ApkClassLoader extends DexClassLoader {/*** 当前ClassLoader的父类加载器,通过调用宿主的application.getClassLoader()获取* */private final ClassLoader parentClassLoader;/*** 包名白名单,若类的包名在白名单中,则直接从宿主的apk中加载该类。* */private final String[] whitePackageNames;/*** 加载so库时的 findLibrary() 方法名* */private static final String METHOD_FIND_LIBRARY = "findLibrary";private Method findLibraryMethod;/*** @param dexPath 插件apk的路径* @param optimizedDirectory 用于存储优化过的 dex 文件(即 ODEX 文件)的目录,这些ODEX文件是为了提高类加载时的效率而生成的* @param libraryPath 保存插件so库的lib目录的路径,与插件apk在同一级目录* @param classLoader 当前ClassLoader的父类加载器,通过调用宿主的application.getClassLoader()获取* @param whitePackageNames 白名单** */public ApkClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader classLoader, String[] whitePackageNames) {super(dexPath, optimizedDirectory, libraryPath, classLoader);this.parentClassLoader = classLoader;this.whitePackageNames = whitePackageNames;}@Overrideprotected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {/*获取类的包名*/String packageName;int dot = className.lastIndexOf('.');if (dot != -1) {packageName = className.substring(0, dot);} else {packageName = "";}/*判断是不是白名单中的类*/boolean isWhiteInterface = false;for (String interfacePackageName : whitePackageNames) {if (packageName.startsWith(interfacePackageName)) {isWhiteInterface = true;break;}}if (isWhiteInterface) {/*白名单中的类直接从宿主apk中找*/return super.loadClass(className, resolve);} else {/*非白名单中的类,先从插件中找,找不到再从宿主中找*/Class<?> clazz = findLoadedClass(className);if (clazz == null) {ClassNotFoundException suppressed = null;try {clazz = findClass(className);} catch (ClassNotFoundException e) {suppressed = e;}if (clazz == null) {try {clazz = parentClassLoader.loadClass(className);} catch (ClassNotFoundException e) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {e.addSuppressed(suppressed);}throw e;}}}return clazz;}}/*** 从插件apk中读取接口的具体实现类** @param clazz 接口类* @param className 实现类的类名* @param <T> 接口类型** @return 所需接口* @throws Exception*/public <T> T getInterface(Class<T> clazz, String className) throws Exception {try {Class<?> interfaceImplementClass = loadClass(className);Object interfaceImplement = interfaceImplementClass.newInstance();return clazz.cast(interfaceImplement);} catch (ClassNotFoundException| InstantiationException| ClassCastException| IllegalAccessException e) {throw new Exception(e);}}/*** 加载so库** @param name so文件名* */@Overridepublic String findLibrary(String name) {String path = super.findLibrary(name);if (path != null) {return path;}if (parentClassLoader instanceof BaseDexClassLoader) {return ((BaseDexClassLoader) parentClassLoader).findLibrary(name);}try {if (findLibraryMethod == null) {findLibraryMethod = ClassLoader.class.getDeclaredMethod(METHOD_FIND_LIBRARY, String.class);findLibraryMethod.setAccessible(true);}return (String) findLibraryMethod.invoke(parentClassLoader, name);} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {e.printStackTrace();return null;}}}
要点说明:
parentClassLoader:当前ClassLoader的父类加载器,通过调用宿主的application.getClassLoader()获取,可用于加载宿主中的类。
whitePackageNames:包名白名单,若类的包名在白名单中,则直接从宿主的apk中加载该类。因为宿主和插件中的类,遵循同一套标准(如:同一套接口)时,宿主在使用时,就需要将插件中的类,转换为宿主中这套标准的类。根据“同一个类加载器加载且全类名相同的类才算同一个类”的机制,需要用父加载器加载的接口才可以进行类型转换。比如:在Activity插件化时,需要把插件Activity实现的接口IActivityInterface加入白名单,宿主加载插件Activity时,直接用宿主的ClassLoader加载IActivityInterface接口,然后把插件中的Activity转换为IActivityInterface类型的对象,这样才能调用插件中的Activity。
白名单类加载:loadClass()中直接调用了super.loadClass(className, resolve),使用父类加载器加载白名单中的类,因为ApkClassLoader的父类是DexClassLoader,而系统在启动宿主apk时已经创建了DexClassLoader,所以DexClassLoader加载的白名单中的类就是宿主中的类,也就是说,super.loadClass(className, resolve)直接在宿主apk中加载白名单中的类。
非白名单类加载:loadClass()加载非白名单类时,先从当前已加载的类中找;如果找不到,就使用 findClass()尝试查找;如果仍然找不到,就尝试在父类加载器(宿主的类加载器)中加载;如果父类加载器依然找不到,就抛异常ClassNotFoundException。
so库加载:首先调用super.findLibrary(name),从父类中查找库文件,因为ApkClassLoader的构造方法中给父类传入了保存插件so库的lib目录的路径,所以这里会到这个路径中查找。如果找不到,且父类加载器(parentClassLoader)是 BaseDexClassLoader的实例,就会调用parentClassLoader的findLibrary(),从宿主中查找。如果parentClassLoader不是 BaseDexClassLoader的实例,就用反射机制调用 parentClassLoader 对象的 findLibrary() 来查找。
ClassLoader加载插件apk
利用ApkClassLoader加载插件apk的代码如下所示:
// pluginApkFile是插件apk的File文件String libDirPath = new File(pluginApkFile.getParent(), "lib").getAbsolutePath();// 解析过滤插件apk中的so库SoLibUtil.releaseSoFile(application, pluginApkFile, libDirPath);// 存储优化过的 dex 文件的目录File optimizeFile = application.getDir("plugin-optimize", Context.MODE_PRIVATE);// 定义白名单String[] whiteInterfaces = new String[]{"com.xxx.yyy.standard"};// pluginPath是插件apk的绝对路径ApkClassLoader pluginClassLoader = new ApkClassLoader(pluginPath, optimizeFile.getAbsolutePath(), libDirPath, application.getClassLoader(), whiteInterfaces);
上述代码执行完毕,就可在pluginClassLoader中加载插件apk中的类。
小结
插件apk的加载机制如下图所示:
加载插件资源
加载插件资源的要点如下所示:
自定义宿主Resources,手动创建宿主的资源类;
自定义插件Resources,手动创建插件的资源类;
自定义包含宿主和插件资源的MixResources,加载资源时,先从宿主中拿,拿不到再从插件中拿;
插件通过getLayoutId()、getStringId()等获取资源ID时,先从插件中找,找不到再从宿主中找。
实例化MixResources对象,供宿主或插件加载资源时使用。
整个流程如下图所示:
自定义宿主Resources
直接使用系统创建的宿主Resources,主要存在以下问题:
有时会因缓存的影响,出现一些难以处理的Bug;
宿主Resources中没有插件的信息,无法直接用来获取插件的资源。
基于上述原因,这里需要自定义宿主的Resources,代码如下所示:
public class SuperHostResources {private final Context context; // 宿主的Contextprivate final Resources resources; // 宿主的Resourcesprivate Integer pluginApkCookie; // 插件资源路径优先级/*** @param context 宿主的Context* @param pluginPath 插件apk的绝对路径* */public SuperHostResources(Context context, String pluginPath) {this.context = context;resources = buildHostResources(pluginPath);}private Resources buildHostResources(String pluginPath) {Resources hostResources;try {/*通过反射把插件路径设置给AssetManager,从而让宿主能调用插件中的资源*/AssetManager assetManager = context.getResources().getAssets();Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);addAssetPathMethod.setAccessible(true);// pluginApkCookie是资源路径的优先级,用于确定在存在多个库具有相同资源ID时,应优先从哪个库中获取资源。// pluginApkCookie的值越大,说明该路径的资源优先级越高,越先查找。// 例如,如果添加了两个路径"A"和"B",并且它们分别返回了1和2。当尝试通过资源ID访问一个资源时,系统首先会检查路径"B"看是否存在对应的资源,// 如果存在,就会使用"路径B"的资源;否则,就会查看"路径A",获取对应的资源。pluginApkCookie = (Integer) addAssetPathMethod.invoke(assetManager, pluginPath);/*手动创建宿主的Resources对象,直接使用系统创建的宿主资源的话,可能会因缓存影响而出现一些难以处理的Bug。第一个参数:资源管理器,用于访问系统或程序的原始资源文件,如:图像、音频文件等。第二个参数:屏幕显示度量信息,包含屏幕大小、密度和字体等,通常用于适应不同的屏幕分辨率和尺寸。第三个参数:描述设备当前的配置,包含设备的语言环境、屏幕方向等,可用于检测一些设备设置的改变,如:屏幕尺寸、用户语言等。*/hostResources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());} catch (Exception e) {e.printStackTrace();hostResources = context.getResources();}return hostResources;}public Resources get() {return resources;}/*** 解析插件 apk 的 AndroidManifest.xml 时会用到* */public Integer getPluginApkCookie() {return pluginApkCookie;}}
自定义插件Resources
插件Resources的代码如下所示:
public class PluginResources {private final Context context; // 宿主的Contextprivate final Resources resources; // 插件apk的Resourcesprivate final String pluginPath; // 插件apk的绝对路径private String pluginPackageName; // 插件apk的包名/*** @param context 宿主Context* @param pluginPath 插件apk的绝对路径* */public PluginResources(Context context, String pluginPath) {this.context = context;this.pluginPath = pluginPath;resources = build();}public String getPackageName() {return pluginPackageName;}public Resources get() {return resources;}private Resources build() {try {/*获取宿主的PackageInfo*/PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),PackageManager.GET_ACTIVITIES| PackageManager.GET_META_DATA| PackageManager.GET_SERVICES| PackageManager.GET_PROVIDERS| PackageManager.GET_SIGNATURES);String hostPublicSourceDir = packageInfo.applicationInfo.publicSourceDir;String hostSourceDir = packageInfo.applicationInfo.sourceDir;/*把宿主PackageInfo中的publicSourceDir和sourceDir都赋值为插件apk的绝对路径。publicSourceDir和sourceDir都是应用主apk的完整路径,在某些情况下,一个应用可能会有私有的apk文件,此时sourceDir会指向这个私有的apk文件,而publicSourceDir则仍然指向主apk文件。*/packageInfo.applicationInfo.publicSourceDir = pluginPath;packageInfo.applicationInfo.sourceDir = pluginPath;/*获取插件的PackageInfo*/PackageInfo pluginInfo = context.getPackageManager().getPackageArchiveInfo(pluginPath, PackageManager.GET_ACTIVITIES);pluginPackageName = pluginInfo.packageName; // 插件apk的包名/*获取插件的Resources*/Resources pluginResources = context.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);/*复原宿主PackageInfo中的publicSourceDir和sourceDir*/packageInfo.applicationInfo.publicSourceDir = hostPublicSourceDir;packageInfo.applicationInfo.sourceDir = hostSourceDir;return pluginResources;} catch (PackageManager.NameNotFoundException e) {e.printStackTrace();}return null;}}
自定义MixResources
前面已经创建了宿主和插件的Resources,这里需要创建一个混合的Resources(MixResources),同时持有宿主和插件的Resources对象,获取资源时,先从宿主Resources中获取,获取不到再从插件Resources中获取。
创建MixResources的关键步骤如下:
创建ResourcesWrapper,继承系统的Resources,持有宿主的Resources,并且重写获取资源的方法(比如:getText()、getString()、getDimension()等),直接从宿主Resources中获取资源。
创建MixResources,继承ResourcesWrapper,持有宿主和插件的Resources,并且重写获取资源的方法,优先调用ResourcesWrapper中对应的方法,从宿主中获取资源,获取失败时,再从插件中获取。
ResourcesWrapper
ResourcesWrapper的代码如下所示:
public class ResourcesWrapper extends Resources {private final Resources hostResources; // 宿主的Resourcespublic ResourcesWrapper(Resources hostResources) {super(hostResources.getAssets(), hostResources.getDisplayMetrics(), hostResources.getConfiguration());this.hostResources = hostResources;}@Overridepublic CharSequence getText(int id) throws NotFoundException {// 从宿主中获取指定id的textreturn hostResources.getText(id);}@TargetApi(Build.VERSION_CODES.O)@Overridepublic Typeface getFont(int id) throws NotFoundException {// 从宿主中获取指定id的fontreturn hostResources.getFont(id);}@Overridepublic CharSequence getQuantityText(int id, int quantity) throws NotFoundException {return hostResources.getQuantityText(id, quantity);}@Overridepublic String getString(int id) throws NotFoundException {return hostResources.getString(id);}@Overridepublic String getString(int id, Object... formatArgs) throws NotFoundException {return hostResources.getString(id, formatArgs);}@Overridepublic String getQuantityString(int id, int quantity, Object... formatArgs) throws NotFoundException {return hostResources.getQuantityString(id, quantity, formatArgs);}@Overridepublic String getQuantityString(int id, int quantity) throws NotFoundException {return hostResources.getQuantityString(id, quantity);}@Overridepublic CharSequence getText(int id, CharSequence def) {return hostResources.getText(id, def);}@Overridepublic CharSequence[] getTextArray(int id) throws NotFoundException {return hostResources.getTextArray(id);}@Overridepublic String[] getStringArray(int id) throws NotFoundException {return hostResources.getStringArray(id);}@Overridepublic int[] getIntArray(int id) throws NotFoundException {return hostResources.getIntArray(id);}@Overridepublic TypedArray obtainTypedArray(int id) throws NotFoundException {return hostResources.obtainTypedArray(id);}@Overridepublic float getDimension(int id) throws NotFoundException {return hostResources.getDimension(id);}@Overridepublic int getDimensionPixelOffset(int id) throws NotFoundException {return hostResources.getDimensionPixelOffset(id);}@Overridepublic int getDimensionPixelSize(int id) throws NotFoundException {return hostResources.getDimensionPixelSize(id);}@Overridepublic float getFraction(int id, int base, int pbase) {return this.hostResources.getFraction(id, base, pbase);}@Overridepublic Drawable getDrawable(int id) throws NotFoundException {return hostResources.getDrawable(id);}@TargetApi(Build.VERSION_CODES.LOLLIPOP)@Overridepublic Drawable getDrawable(int id, Theme theme) throws NotFoundException {return hostResources.getDrawable(id, theme);}@Overridepublic Drawable getDrawableForDensity(int id, int density) throws NotFoundException {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {return hostResources.getDrawableForDensity(id, density);} else {return null;}}@TargetApi(Build.VERSION_CODES.LOLLIPOP)@Overridepublic Drawable getDrawableForDensity(int id, int density, Theme theme) {return hostResources.getDrawableForDensity(id, density, theme);}@Overridepublic Movie getMovie(int id) throws NotFoundException {return hostResources.getMovie(id);}@Overridepublic int getColor(int id) throws NotFoundException {return hostResources.getColor(id);}@TargetApi(Build.VERSION_CODES.M)@Overridepublic int getColor(int id, Theme theme) throws NotFoundException {return hostResources.getColor(id, theme);}@Overridepublic ColorStateList getColorStateList(int id) throws NotFoundException {return hostResources.getColorStateList(id);}@TargetApi(Build.VERSION_CODES.M)@Overridepublic ColorStateList getColorStateList(int id, Theme theme) throws NotFoundException {return hostResources.getColorStateList(id, theme);}@Overridepublic boolean getBoolean(int id) throws NotFoundException {return hostResources.getBoolean(id);}@Overridepublic int getInteger(int id) throws NotFoundException {return hostResources.getInteger(id);}@Overridepublic XmlResourceParser getLayout(int id) throws NotFoundException {return hostResources.getLayout(id);}@Overridepublic XmlResourceParser getAnimation(int id) throws NotFoundException {return hostResources.getAnimation(id);}@Overridepublic XmlResourceParser getXml(int id) throws NotFoundException {return hostResources.getXml(id);}@Overridepublic InputStream openRawResource(int id) throws NotFoundException {return hostResources.openRawResource(id);}@Overridepublic InputStream openRawResource(int id, TypedValue value) throws NotFoundException {return hostResources.openRawResource(id, value);}@Overridepublic AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException {return hostResources.openRawResourceFd(id);}@Overridepublic void getValue(int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException {hostResources.getValue(id, outValue, resolveRefs);}@Overridepublic void getValueForDensity(int id, int density, TypedValue outValue, boolean resolveRefs) throws NotFoundException {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {hostResources.getValueForDensity(id, density, outValue, resolveRefs);}}@Overridepublic void getValue(String name, TypedValue outValue, boolean resolveRefs) throws NotFoundException {hostResources.getValue(name, outValue, resolveRefs);}@Overridepublic TypedArray obtainAttributes(AttributeSet set, int[] attrs) {return hostResources.obtainAttributes(set, attrs);}@Overridepublic DisplayMetrics getDisplayMetrics() {return hostResources.getDisplayMetrics();}@Overridepublic Configuration getConfiguration() {return hostResources.getConfiguration();}@Overridepublic int getIdentifier(String name, String defType, String defPackage) {return hostResources.getIdentifier(name, defType, defPackage);}@Overridepublic String getResourceName(int resid) throws NotFoundException {return hostResources.getResourceName(resid);}@Overridepublic String getResourcePackageName(int resid) throws NotFoundException {return hostResources.getResourcePackageName(resid);}@Overridepublic String getResourceTypeName(int resid) throws NotFoundException {return hostResources.getResourceTypeName(resid);}@Overridepublic String getResourceEntryName(int resid) throws NotFoundException {return hostResources.getResourceEntryName(resid);}@Overridepublic void parseBundleExtras(XmlResourceParser parser, Bundle outBundle) throws XmlPullParserException, IOException {hostResources.parseBundleExtras(parser, outBundle);}@Overridepublic void parseBundleExtra(String tagName, AttributeSet attrs, Bundle outBundle) throws XmlPullParserException {hostResources.parseBundleExtra(tagName, attrs, outBundle);}}
MixResources
MixResources的代码如下所示:
public class MixResources extends ResourcesWrapper {private final Resources pluginResources; // 插件Resourcesprivate final String pluginPackageName; // 插件apk的包名/*** @param hostResources 宿主Resources* @param context 宿主Context* @param pluginPath 插件apk的绝对路径* */public MixResources(Resources hostResources, Context context, String pluginPath) {// 把宿主 Resources 传给父类 ResourcesWrappersuper(hostResources);// 获取插件 ResourcesPluginResources pluginResourcesBuilder = new PluginResources(context, pluginPath);pluginResources = pluginResourcesBuilder.get();pluginPackageName = pluginResourcesBuilder.getPackageName();}@Overridepublic CharSequence getText(int id) throws Resources.NotFoundException {try {// 从宿主中获取textreturn super.getText(id);} catch (Resources.NotFoundException e) {// 宿主中获取失败时,再从插件中获取return pluginResources.getText(id);}}@Overridepublic String getString(int id) throws Resources.NotFoundException {try {// 从宿主中获取Stringreturn super.getString(id);} catch (Resources.NotFoundException e) {// 宿主中获取失败时,再从插件中获取return pluginResources.getString(id);}}@Overridepublic String getString(int id, Object... formatArgs) throws Resources.NotFoundException {try {return super.getString(id, formatArgs);} catch (Resources.NotFoundException e) {return pluginResources.getString(id, formatArgs);}}@Overridepublic float getDimension(int id) throws Resources.NotFoundException {try {return super.getDimension(id);} catch (Resources.NotFoundException e) {return pluginResources.getDimension(id);}}@Overridepublic int getDimensionPixelOffset(int id) throws Resources.NotFoundException {try {return super.getDimensionPixelOffset(id);} catch (Resources.NotFoundException e) {return pluginResources.getDimensionPixelOffset(id);}}@Overridepublic int getDimensionPixelSize(int id) throws Resources.NotFoundException {try {return super.getDimensionPixelSize(id);} catch (Resources.NotFoundException e) {return pluginResources.getDimensionPixelSize(id);}}@Overridepublic Drawable getDrawable(int id) throws Resources.NotFoundException {try {return super.getDrawable(id);} catch (Resources.NotFoundException e) {return pluginResources.getDrawable(id);}}@TargetApi(Build.VERSION_CODES.LOLLIPOP)@Overridepublic Drawable getDrawable(int id, Resources.Theme theme) throws Resources.NotFoundException {try {return super.getDrawable(id, theme);} catch (Resources.NotFoundException e) {return pluginResources.getDrawable(id, theme);}}@Overridepublic Drawable getDrawableForDensity(int id, int density) throws Resources.NotFoundException {try {return super.getDrawableForDensity(id, density);} catch (Resources.NotFoundException e) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {return pluginResources.getDrawableForDensity(id, density);} else {return null;}}}@TargetApi(Build.VERSION_CODES.LOLLIPOP)@Overridepublic Drawable getDrawableForDensity(int id, int density, Resources.Theme theme) {try {return super.getDrawableForDensity(id, density, theme);} catch (Exception e) {return pluginResources.getDrawableForDensity(id, density, theme);}}@Overridepublic int getColor(int id) throws Resources.NotFoundException {try {return super.getColor(id);} catch (Resources.NotFoundException e) {return pluginResources.getColor(id);}}@TargetApi(Build.VERSION_CODES.M)@Overridepublic int getColor(int id, Resources.Theme theme) throws Resources.NotFoundException {try {return super.getColor(id, theme);} catch (Resources.NotFoundException e) {return pluginResources.getColor(id, theme);}}@Overridepublic ColorStateList getColorStateList(int id) throws Resources.NotFoundException {try {return super.getColorStateList(id);} catch (Resources.NotFoundException e) {return pluginResources.getColorStateList(id);}}@TargetApi(Build.VERSION_CODES.M)@Overridepublic ColorStateList getColorStateList(int id, Resources.Theme theme) throws Resources.NotFoundException {try {return super.getColorStateList(id, theme);} catch (Resources.NotFoundException e) {return pluginResources.getColorStateList(id, theme);}}@Overridepublic boolean getBoolean(int id) throws Resources.NotFoundException {try {return super.getBoolean(id);} catch (Resources.NotFoundException e) {return pluginResources.getBoolean(id);}}@Overridepublic XmlResourceParser getLayout(int id) throws Resources.NotFoundException {try {return super.getLayout(id);} catch (Resources.NotFoundException e) {return pluginResources.getLayout(id);}}@Overridepublic String getResourceName(int resid) throws Resources.NotFoundException {try {return super.getResourceName(resid);} catch (Resources.NotFoundException e) {return pluginResources.getResourceName(resid);}}@Overridepublic int getInteger(int id) throws Resources.NotFoundException {try {return super.getInteger(id);} catch (Resources.NotFoundException e) {return pluginResources.getInteger(id);}}@Overridepublic CharSequence getText(int id, CharSequence def) {try {return super.getText(id, def);} catch (Resources.NotFoundException e) {return pluginResources.getText(id, def);}}@Overridepublic InputStream openRawResource(int id) throws Resources.NotFoundException {try {return super.openRawResource(id);} catch (Resources.NotFoundException e) {return pluginResources.openRawResource(id);}}@Overridepublic XmlResourceParser getXml(int id) throws Resources.NotFoundException {try {return super.getXml(id);} catch (Resources.NotFoundException e) {return pluginResources.getXml(id);}}@TargetApi(Build.VERSION_CODES.O)@Overridepublic Typeface getFont(int id) throws Resources.NotFoundException {try {return super.getFont(id);} catch (Resources.NotFoundException e) {return pluginResources.getFont(id);}}@Overridepublic void getValue(int id, TypedValue outValue, boolean resolveRefs) throws Resources.NotFoundException {try {super.getValue(id, outValue, resolveRefs);} catch (Resources.NotFoundException e) {pluginResources.getValue(id, outValue, resolveRefs);}}@Overridepublic Movie getMovie(int id) throws Resources.NotFoundException {try {return super.getMovie(id);} catch (Resources.NotFoundException e) {return pluginResources.getMovie(id);}}@Overridepublic XmlResourceParser getAnimation(int id) throws Resources.NotFoundException {try {return super.getAnimation(id);} catch (Resources.NotFoundException e) {return pluginResources.getAnimation(id);}}@Overridepublic InputStream openRawResource(int id, TypedValue value) throws Resources.NotFoundException {try {return super.openRawResource(id, value);} catch (Resources.NotFoundException e) {return pluginResources.openRawResource(id, value);}}@Overridepublic AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {try {return super.openRawResourceFd(id);} catch (Resources.NotFoundException e) {return pluginResources.openRawResourceFd(id);}}@Overridepublic int getIdentifier(String name, String defType, String defPackage) {int pluginId = super.getIdentifier(name, defType, defPackage);if (pluginId <= 0) {return pluginResources.getIdentifier(name, defType, pluginPackageName);}return pluginId;}/*** 从插件中获取资源ID,注意:插件中会通过反射调用该方法,所以千万不要删!* */public int getIdentifierFromPlugin(String name, String defType) {return pluginResources.getIdentifier(name, defType, pluginPackageName);}}
插件获取资源ID
插件apk中,有时会在代码使用getLayoutId()、getStringId()、getDrawableId()等方法动态获取插件中的资源ID。按照MixResources中加载资源的逻辑,这些资源ID会优先到宿主中找,找不到时才到插件中找。如果插件和宿主中存在同名的资源,那么插件在获取资源ID时,就会从宿主中找,最终返回的是宿主中的资源ID,而不是插件中的资源ID,这就会出现资源获取错误的问题。
为了解决这个问题,插件apk中使用上述方法动态获取插件中的资源ID时,需要先从插件中获取,获取失败时,再从宿主中获取。
这里以插件获取layoutId为例,介绍具体实现代码。
layoutId()代码如下:
private static final String TYPE_LAYOUT = "layout";private static final int ID_DEFAULT = -1;public static int getLayoutId(Context paramContext, String paramString) {return getIdentifier(paramContext, paramString, TYPE_LAYOUT);}
getIdentifier()代码如下:
private static int getIdentifier(Context context, String name, String type) {int id = ID_DEFAULT;try {// 先从插件中获取资源IDid = getIdentifierFromPlugin(context, name, type);if (id == ID_DEFAULT) {// 从插件中拿不到时,再从宿主中拿id = context.getResources().getIdentifier(name, type, context.getPackageName());}} catch (Exception e) {e.printStackTrace();}return id;}
getIdentifierFromPlugin()代码如下:
/*** 从插件中获取资源ID** @param context 上下文* @param name 资源名称* @param type 资源类型*/private static int getIdentifierFromPlugin(Context context, String name, String type) {try {/*因为插件和宿主是两个独立的apk,所以这里使用 ClassLoader 加载宿主中的 MixResources*/if (mixResource == null) {mixResource = context.getClassLoader().loadClass("MixResources的全类名");}if (mixResource == null) {return ID_DEFAULT;}/*通过反射调用 MixResources 中的 getIdentifierFromPlugin() 方法,从而从插件中获取资源 ID。*/Method method = mixResource.getMethod("getIdentifierFromPlugin", String.class, String.class);return (int) method.invoke(context.getResources(), name, type);} catch (Exception e) {e.printStackTrace();return ID_DEFAULT;}}
实例化MixResources对象
前面介绍了宿主和插件资源的自定义方法,在加载插件apk时,还需要实例化这些资源的对象,代码如下:
SuperHostResources superHostResources = new SuperHostResources(application, pluginPath);Resources resources = new MixResources(superHostResources.get(), application, pluginPath);
缓存插件信息
宿主APP启动后,要先加载插件的so库、ClassLoader和资源等,并缓存在内存中,需要时直接从内存中调用。前面介绍了加载方法,这里主要介绍一下如何缓存。
缓存插件信息的主要步骤如下:
自定义保存插件信息的类Plugin;
创建单例类PluginManager,持有Plugin对象,并设置插件信息。
自定义Plugin类
Plugin的代码如下所示:
public class Plugin {private ApkClassLoader classLoader; // 插件的 ClassLoaderprivate Resources resource; // 实际持有的是 MixResources 对象public ClassLoader getClassLoader() {return classLoader;}public void setClassLoader(ApkClassLoader classLoader) {this.classLoader = classLoader;}public Resources getResource() {return resource;}public void setResources(Resources resources) {resource = resources;}}
初始化Plugin对象并缓存
创建一个单例类PluginManager,实例化Plugin对象,并设置插件的信息,需要时直接使用即可。代码如下所示:
public class PluginManager {private static PluginManager pluginManager;private Plugin plugin;public static PluginManager getInstance() {if (pluginManager == null) {synchronized (PluginManager.class) {if (pluginManager == null) {pluginManager = new PluginManager();}}}return pluginManager;}private PluginManager() {}public Plugin getPlugin() {return plugin;}/*** 加载插件 APK,初始化 Plugin** @param application 宿主 Application* @param pluginPath 插件 APK 的绝对路径*/public void loadPlugin(Application application, String pluginPath) {/*实例化 Plugin*/plugin = new Plugin();/*配置插件 ClassLoader*/File pluginApkFile = new File(pluginPath);String libDirPath = new File(pluginApkFile.getParent(), "lib").getAbsolutePath();// 解析过滤插件apk中的so库SoLibUtil.releaseSoFile(application, pluginApkFile, libDirPath);// 存储优化过的 dex 文件的目录File optimizeFile = application.getDir("plugin-optimize", Context.MODE_PRIVATE);// 创建白名单String[] whiteInterfaces = new String[]{"com.xxx.yyy.standard"};// 根据插件 APK,创建插件 ClassLoaderApkClassLoader pluginClassLoader = new ApkClassLoader(pluginPath, optimizeFile.getAbsolutePath(), libDirPath, application.getClassLoader(), whiteInterfaces);plugin.setClassLoader(pluginClassLoader);/*配置 Resources*/SuperHostResources superHostResources = new SuperHostResources(application, pluginPath);Resources resources = new MixResources(superHostResources.get(), application, pluginPath);plugin.setResources(resources);}// ......}
注意:因为宿主Application或启动页Activity中都可能使用插件的资源,并且宿主的Context在初始化完毕后会有缓存,这可能会影响插件资源的使用,所以插件apk的加载时机要尽可能早,建议在宿主Application的attachBaseContext()中就加载插件apk,即:调用PluginManager的loadPlugin()方法。
总结
宿主和插件是两个独立的apk,要在宿主中使用插件的功能,就必须先加载插件apk,建议在宿主Application的attachBaseContext()方法中就加载插件apk。
加载插件apk的主要流程如下:
解析过滤插件so库,关键步骤如下:
获取插件apk的完整路径;
获取系统支持的宿主apk的CPU架构;
根据宿主apk的CPU架构,获取插件最终要适配的CPU架构;
根据要适配的CPU架构,过滤插件apk的so库并保存到lib目录。
详见下图:
加载插件apk,生成DexClassLoader,关键步骤如下:
自定义ApkClassLoader,继承DexClassLoader,并重写loadClass():白名单中的类直接从宿主中找,非白名单中的类,先从插件中找,找不到再从宿主中找,
传入插件apk的路径、保存so库的lib路径、宿主的ClassLoader等,创建ApkClassLoader对象。
详见下图:
加载插件资源,生成含有宿主和插件资源的MixResources,关键步骤如下:
自定义宿主Resources,手动创建宿主的资源类;
自定义插件Resources,手动创建插件的资源类;
自定义包含宿主和插件资源的MixResources,加载资源时,先从宿主中找,找不到再从插件中找;
插件通过getLayoutId()、getStringId()等获取资源ID时,先从插件中找,找不到再从宿主中找。
实例化MixResources对象,供宿主或插件加载资源时使用。
缓存插件信息,需要时直接调用,关键步骤如下:
自定义保存插件信息的Plugin类;
创建单例类PluginManager,持有Plugin对象,并设置插件信息。
三七互娱技术团队
扫码关注 了解更多