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);}
03加载插件apk宿主中要使用插件apk中的类、so库和资源等,就必须先加载插件apk,生成插件的ClassLoader,并添加到宿主APP的ClassLoader体系中为便于理解,本小节先介绍类加载机制,再介绍插件apk的加载方法类加载机制ClassLoader简介ClassLoader是一个抽象类,它通过类的全限定名来获取此类的二进制字节流,核心是findClass()方法Android中的ClassLoader类加载器派生出的有DexClassLoader和PathClassLoader,这两者的区别如下:DexClassLoader: 能够加载未安装的jar/apk/dexPathClassLoader: 只能加载系统中已经安装的apkAndroid 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; } @Override protected 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文件名 / @Override public 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()尝试查找;如果仍然找不到,就尝试在父类加载器(宿主的类加载器)中加载;如果父类加载器依然找不到,就抛异常ClassNotFoundExceptionso库加载:首先调用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的加载机制如下图所示:04加载插件资源加载插件资源的要点如下所示:自定义宿主Resources,手动创建宿主的资源类;自定义插件Resources,手动创建插件的资源类;自定义包含宿主和插件资源的MixResources,加载资源时,先从宿主中拿,拿不到再从插件中拿;插件通过getLayoutId()、getStringId()等获取资源ID时,先从插件中找,找不到再从宿主中找实例化MixResources对象,供宿主或插件加载资源时使用整个流程如下图所示:自定义宿主Resources直接使用系统创建的宿主Resources,主要存在以下问题:有时会因缓存的影响,出现一些难以处理的Bug;宿主Resources中没有插件的信息,无法直接用来获取插件的资源基于上述原因,这里需要自定义宿主的Resources,代码如下所示:public class SuperHostResources { private final Context context; // 宿主的Context private final Resources resources; // 宿主的Resources private 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; // 宿主的Context private final Resources resources; // 插件apk的Resources private 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中对应的方法,从宿主中获取资源,获取失败时,再从插件中获取ResourcesWrapperResourcesWrapper的代码如下所示:public class ResourcesWrapper extends Resources { private final Resources hostResources; // 宿主的Resources public ResourcesWrapper(Resources hostResources) { super(hostResources.getAssets(), hostResources.getDisplayMetrics(), hostResources.getConfiguration()); this.hostResources = hostResources; } @Override public CharSequence getText(int id) throws NotFoundException { // 从宿主中获取指定id的text return hostResources.getText(id); } @TargetApi(Build.VERSION_CODES.O) @Override public Typeface getFont(int id) throws NotFoundException { // 从宿主中获取指定id的font return hostResources.getFont(id); } @Override public CharSequence getQuantityText(int id, int quantity) throws NotFoundException { return hostResources.getQuantityText(id, quantity); } @Override public String getString(int id) throws NotFoundException { return hostResources.getString(id); } @Override public String getString(int id, Object... formatArgs) throws NotFoundException { return hostResources.getString(id, formatArgs); } @Override public String getQuantityString(int id, int quantity, Object... formatArgs) throws NotFoundException { return hostResources.getQuantityString(id, quantity, formatArgs); } @Override public String getQuantityString(int id, int quantity) throws NotFoundException { return hostResources.getQuantityString(id, quantity); } @Override public CharSequence getText(int id, CharSequence def) { return hostResources.getText(id, def); } @Override public CharSequence[] getTextArray(int id) throws NotFoundException { return hostResources.getTextArray(id); } @Override public String[] getStringArray(int id) throws NotFoundException { return hostResources.getStringArray(id); } @Override public int[] getIntArray(int id) throws NotFoundException { return hostResources.getIntArray(id); } @Override public TypedArray obtainTypedArray(int id) throws NotFoundException { return hostResources.obtainTypedArray(id); } @Override public float getDimension(int id) throws NotFoundException { return hostResources.getDimension(id); } @Override public int getDimensionPixelOffset(int id) throws NotFoundException { return hostResources.getDimensionPixelOffset(id); } @Override public int getDimensionPixelSize(int id) throws NotFoundException { return hostResources.getDimensionPixelSize(id); } @Override public float getFraction(int id, int base, int pbase) { return this.hostResources.getFraction(id, base, pbase); } @Override public Drawable getDrawable(int id) throws NotFoundException { return hostResources.getDrawable(id); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public Drawable getDrawable(int id, Theme theme) throws NotFoundException { return hostResources.getDrawable(id, theme); } @Override public 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) @Override public Drawable getDrawableForDensity(int id, int density, Theme theme) { return hostResources.getDrawableForDensity(id, density, theme); } @Override public Movie getMovie(int id) throws NotFoundException { return hostResources.getMovie(id); } @Override public int getColor(int id) throws NotFoundException { return hostResources.getColor(id); } @TargetApi(Build.VERSION_CODES.M) @Override public int getColor(int id, Theme theme) throws NotFoundException { return hostResources.getColor(id, theme); } @Override public ColorStateList getColorStateList(int id) throws NotFoundException { return hostResources.getColorStateList(id); } @TargetApi(Build.VERSION_CODES.M) @Override public ColorStateList getColorStateList(int id, Theme theme) throws NotFoundException { return hostResources.getColorStateList(id, theme); } @Override public boolean getBoolean(int id) throws NotFoundException { return hostResources.getBoolean(id); } @Override public int getInteger(int id) throws NotFoundException { return hostResources.getInteger(id); } @Override public XmlResourceParser getLayout(int id) throws NotFoundException { return hostResources.getLayout(id); } @Override public XmlResourceParser getAnimation(int id) throws NotFoundException { return hostResources.getAnimation(id); } @Override public XmlResourceParser getXml(int id) throws NotFoundException { return hostResources.getXml(id); } @Override public InputStream openRawResource(int id) throws NotFoundException { return hostResources.openRawResource(id); } @Override public InputStream openRawResource(int id, TypedValue value) throws NotFoundException { return hostResources.openRawResource(id, value); } @Override public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException { return hostResources.openRawResourceFd(id); } @Override public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException { hostResources.getValue(id, outValue, resolveRefs); } @Override public 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); } } @Override public void getValue(String name, TypedValue outValue, boolean resolveRefs) throws NotFoundException { hostResources.getValue(name, outValue, resolveRefs); } @Override public TypedArray obtainAttributes(AttributeSet set, int[] attrs) { return hostResources.obtainAttributes(set, attrs); } @Override public DisplayMetrics getDisplayMetrics() { return hostResources.getDisplayMetrics(); } @Override public Configuration getConfiguration() { return hostResources.getConfiguration(); } @Override public int getIdentifier(String name, String defType, String defPackage) { return hostResources.getIdentifier(name, defType, defPackage); } @Override public String getResourceName(int resid) throws NotFoundException { return hostResources.getResourceName(resid); } @Override public String getResourcePackageName(int resid) throws NotFoundException { return hostResources.getResourcePackageName(resid); } @Override public String getResourceTypeName(int resid) throws NotFoundException { return hostResources.getResourceTypeName(resid); } @Override public String getResourceEntryName(int resid) throws NotFoundException { return hostResources.getResourceEntryName(resid); } @Override public void parseBundleExtras(XmlResourceParser parser, Bundle outBundle) throws XmlPullParserException, IOException { hostResources.parseBundleExtras(parser, outBundle); } @Override public void parseBundleExtra(String tagName, AttributeSet attrs, Bundle outBundle) throws XmlPullParserException { hostResources.parseBundleExtra(tagName, attrs, outBundle); }}
MixResourcesMixResources的代码如下所示:public class MixResources extends ResourcesWrapper { private final Resources pluginResources; // 插件Resources private final String pluginPackageName; // 插件apk的包名 / @param hostResources 宿主Resources @param context 宿主Context @param pluginPath 插件apk的绝对路径 / public MixResources(Resources hostResources, Context context, String pluginPath) { // 把宿主 Resources 传给父类 ResourcesWrapper super(hostResources); // 获取插件 Resources PluginResources pluginResourcesBuilder = new PluginResources(context, pluginPath); pluginResources = pluginResourcesBuilder.get(); pluginPackageName = pluginResourcesBuilder.getPackageName(); } @Override public CharSequence getText(int id) throws Resources.NotFoundException { try { // 从宿主中获取text return super.getText(id); } catch (Resources.NotFoundException e) { // 宿主中获取失败时,再从插件中获取 return pluginResources.getText(id); } } @Override public String getString(int id) throws Resources.NotFoundException { try { // 从宿主中获取String return super.getString(id); } catch (Resources.NotFoundException e) { // 宿主中获取失败时,再从插件中获取 return pluginResources.getString(id); } } @Override public String getString(int id, Object... formatArgs) throws Resources.NotFoundException { try { return super.getString(id, formatArgs); } catch (Resources.NotFoundException e) { return pluginResources.getString(id, formatArgs); } } @Override public float getDimension(int id) throws Resources.NotFoundException { try { return super.getDimension(id); } catch (Resources.NotFoundException e) { return pluginResources.getDimension(id); } } @Override public int getDimensionPixelOffset(int id) throws Resources.NotFoundException { try { return super.getDimensionPixelOffset(id); } catch (Resources.NotFoundException e) { return pluginResources.getDimensionPixelOffset(id); } } @Override public int getDimensionPixelSize(int id) throws Resources.NotFoundException { try { return super.getDimensionPixelSize(id); } catch (Resources.NotFoundException e) { return pluginResources.getDimensionPixelSize(id); } } @Override public 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) @Override public 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); } } @Override public 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) @Override public 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); } } @Override public 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) @Override public 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); } } @Override public 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) @Override public 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); } } @Override public boolean getBoolean(int id) throws Resources.NotFoundException { try { return super.getBoolean(id); } catch (Resources.NotFoundException e) { return pluginResources.getBoolean(id); } } @Override public XmlResourceParser getLayout(int id) throws Resources.NotFoundException { try { return super.getLayout(id); } catch (Resources.NotFoundException e) { return pluginResources.getLayout(id); } } @Override public String getResourceName(int resid) throws Resources.NotFoundException { try { return super.getResourceName(resid); } catch (Resources.NotFoundException e) { return pluginResources.getResourceName(resid); } } @Override public int getInteger(int id) throws Resources.NotFoundException { try { return super.getInteger(id); } catch (Resources.NotFoundException e) { return pluginResources.getInteger(id); } } @Override public CharSequence getText(int id, CharSequence def) { try { return super.getText(id, def); } catch (Resources.NotFoundException e) { return pluginResources.getText(id, def); } } @Override public InputStream openRawResource(int id) throws Resources.NotFoundException { try { return super.openRawResource(id); } catch (Resources.NotFoundException e) { return pluginResources.openRawResource(id); } } @Override public 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) @Override public Typeface getFont(int id) throws Resources.NotFoundException { try { return super.getFont(id); } catch (Resources.NotFoundException e) { return pluginResources.getFont(id); } } @Override public 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); } } @Override public Movie getMovie(int id) throws Resources.NotFoundException { try { return super.getMovie(id); } catch (Resources.NotFoundException e) { return pluginResources.getMovie(id); } } @Override public XmlResourceParser getAnimation(int id) throws Resources.NotFoundException { try { return super.getAnimation(id); } catch (Resources.NotFoundException e) { return pluginResources.getAnimation(id); } } @Override public InputStream openRawResource(int id, TypedValue value) throws Resources.NotFoundException { try { return super.openRawResource(id, value); } catch (Resources.NotFoundException e) { return pluginResources.openRawResource(id, value); } } @Override public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException { try { return super.openRawResourceFd(id); } catch (Resources.NotFoundException e) { return pluginResources.openRawResourceFd(id); } } @Override public 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 { // 先从插件中获取资源ID id = 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);
05缓存插件信息宿主APP启动后,要先加载插件的so库、ClassLoader和资源等,并缓存在内存中,需要时直接从内存中调用前面介绍了加载方法,这里主要介绍一下如何缓存缓存插件信息的主要步骤如下:自定义保存插件信息的类Plugin;创建单例类PluginManager,持有Plugin对象,并设置插件信息自定义Plugin类Plugin的代码如下所示:public class Plugin { private ApkClassLoader classLoader; // 插件的 ClassLoader private 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,创建插件 ClassLoader ApkClassLoader 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()方法06总结宿主和插件是两个独立的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对象,并设置插件信息作者:赵鹏 来源-微信公众号:三七互娱技术团队出处:https://mp.weixin.qq.com/s/9gUGukxlIbKPufiDH4C17w(图片来源网络,侵删)
0 评论