常规的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库时,需要解析插件的apk,根据要配置的CPU架构,过滤插件apk中的so库,并把过滤后的so文件复制到一个单独的目录中(这里复制到与插件apk同级的lib目录中)。关键步骤如下所示:
获取插件apk的完整路径;
获取系统支持的宿主apk的CPU架构;
根据系统支持的宿主apk的CPU架构,获取插件最终要适配的CPU架构;
根据要适配的CPU架构,过滤插件apk的so库并保存到lib目录。
整体流程如下图所示:
第一步比较简单,这里详细介绍后三步。
根据宿主的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;
}
根据系统支持的宿主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架构
* */
"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};
}
}
这一步需要遍历插件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中的类、so库和资源等,就必须先加载插件apk,生成插件的ClassLoader,并添加到宿主APP的ClassLoader体系中。为便于理解,本小节先介绍类加载机制,再介绍插件apk的加载方法。
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对象;若不能加载到,则最后再由发起请求的类加载器去加载该类。这种方式能够保证类的加载按照一定的规则次序进行,越是基础的类,越是被上层的类加载器加载,从而保证程序的安全性。
由于ClassLoader的双亲委派模型,在加载类时,如果已经加载了插件中的类,那么就会直接使用插件中的类,而不会再加载宿主的类,反之对插件也是一样。这就很容易造成宿主和插件的类加载冲突,并且也可能会抛出异常-- java.lang.IllegalAccessError: Class ref in pre-verified class...。因此,在Android插件化时,需要自定义一个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;
}
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文件名
* */
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()尝试查找;如果仍然找不到,就尝试在父类加载器(宿主的类加载器)中加载;如果父类加载器依然找不到,就抛异常ClassNotFoundException。
so库加载:首先调用super.findLibrary(name),从父类中查找库文件,因为ApkClassLoader的构造方法中给父类传入了保存插件so库的lib目录的路径,所以这里会到这个路径中查找。如果找不到,且父类加载器(parentClassLoader)是 BaseDexClassLoader的实例,就会调用parentClassLoader的findLibrary(),从宿主中查找。如果parentClassLoader不是 BaseDexClassLoader的实例,就用反射机制调用 parentClassLoader 对象的 findLibrary() 来查找。
利用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,主要存在以下问题:
有时会因缓存的影响,出现一些难以处理的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的代码如下所示:
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;
}
}
前面已经创建了宿主和插件的Resources,这里需要创建一个混合的Resources(MixResources),同时持有宿主和插件的Resources对象,获取资源时,先从宿主Resources中获取,获取不到再从插件Resources中获取。
创建MixResources的关键步骤如下:
创建ResourcesWrapper,继承系统的Resources,持有宿主的Resources,并且重写获取资源的方法(比如:getText()、getString()、getDimension()等),直接从宿主Resources中获取资源。
创建MixResources,继承ResourcesWrapper,持有宿主和插件的Resources,并且重写获取资源的方法,优先调用ResourcesWrapper中对应的方法,从宿主中获取资源,获取失败时,再从插件中获取。
ResourcesWrapper的代码如下所示:
public class ResourcesWrapper extends Resources {
private final Resources hostResources; // 宿主的Resources
public ResourcesWrapper(Resources hostResources) {
super(hostResources.getAssets(), hostResources.getDisplayMetrics(), hostResources.getConfiguration());
this.hostResources = hostResources;
}
public CharSequence getText(int id) throws NotFoundException {
// 从宿主中获取指定id的text
return hostResources.getText(id);
}
(Build.VERSION_CODES.O)
public Typeface getFont(int id) throws NotFoundException {
// 从宿主中获取指定id的font
return hostResources.getFont(id);
}
public CharSequence getQuantityText(int id, int quantity) throws NotFoundException {
return hostResources.getQuantityText(id, quantity);
}
public String getString(int id) throws NotFoundException {
return hostResources.getString(id);
}
public String getString(int id, Object... formatArgs) throws NotFoundException {
return hostResources.getString(id, formatArgs);
}
public String getQuantityString(int id, int quantity, Object... formatArgs) throws NotFoundException {
return hostResources.getQuantityString(id, quantity, formatArgs);
}
public String getQuantityString(int id, int quantity) throws NotFoundException {
return hostResources.getQuantityString(id, quantity);
}
public CharSequence getText(int id, CharSequence def) {
return hostResources.getText(id, def);
}
public CharSequence[] getTextArray(int id) throws NotFoundException {
return hostResources.getTextArray(id);
}
public String[] getStringArray(int id) throws NotFoundException {
return hostResources.getStringArray(id);
}
public int[] getIntArray(int id) throws NotFoundException {
return hostResources.getIntArray(id);
}
public TypedArray obtainTypedArray(int id) throws NotFoundException {
return hostResources.obtainTypedArray(id);
}
public float getDimension(int id) throws NotFoundException {
return hostResources.getDimension(id);
}
public int getDimensionPixelOffset(int id) throws NotFoundException {
return hostResources.getDimensionPixelOffset(id);
}
public int getDimensionPixelSize(int id) throws NotFoundException {
return hostResources.getDimensionPixelSize(id);
}
public float getFraction(int id, int base, int pbase) {
return this.hostResources.getFraction(id, base, pbase);
}
public Drawable getDrawable(int id) throws NotFoundException {
return hostResources.getDrawable(id);
}
(Build.VERSION_CODES.LOLLIPOP)
public Drawable getDrawable(int id, Theme theme) throws NotFoundException {
return hostResources.getDrawable(id, theme);
}
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;
}
}
(Build.VERSION_CODES.LOLLIPOP)
public Drawable getDrawableForDensity(int id, int density, Theme theme) {
return hostResources.getDrawableForDensity(id, density, theme);
}
public Movie getMovie(int id) throws NotFoundException {
return hostResources.getMovie(id);
}
public int getColor(int id) throws NotFoundException {
return hostResources.getColor(id);
}
(Build.VERSION_CODES.M)
public int getColor(int id, Theme theme) throws NotFoundException {
return hostResources.getColor(id, theme);
}
public ColorStateList getColorStateList(int id) throws NotFoundException {
return hostResources.getColorStateList(id);
}
(Build.VERSION_CODES.M)
public ColorStateList getColorStateList(int id, Theme theme) throws NotFoundException {
return hostResources.getColorStateList(id, theme);
}
public boolean getBoolean(int id) throws NotFoundException {
return hostResources.getBoolean(id);
}
public int getInteger(int id) throws NotFoundException {
return hostResources.getInteger(id);
}
public XmlResourceParser getLayout(int id) throws NotFoundException {
return hostResources.getLayout(id);
}
public XmlResourceParser getAnimation(int id) throws NotFoundException {
return hostResources.getAnimation(id);
}
public XmlResourceParser getXml(int id) throws NotFoundException {
return hostResources.getXml(id);
}
public InputStream openRawResource(int id) throws NotFoundException {
return hostResources.openRawResource(id);
}
public InputStream openRawResource(int id, TypedValue value) throws NotFoundException {
return hostResources.openRawResource(id, value);
}
public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException {
return hostResources.openRawResourceFd(id);
}
public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
hostResources.getValue(id, outValue, resolveRefs);
}
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);
}
}
public void getValue(String name, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
hostResources.getValue(name, outValue, resolveRefs);
}
public TypedArray obtainAttributes(AttributeSet set, int[] attrs) {
return hostResources.obtainAttributes(set, attrs);
}
public DisplayMetrics getDisplayMetrics() {
return hostResources.getDisplayMetrics();
}
public Configuration getConfiguration() {
return hostResources.getConfiguration();
}
public int getIdentifier(String name, String defType, String defPackage) {
return hostResources.getIdentifier(name, defType, defPackage);
}
public String getResourceName(int resid) throws NotFoundException {
return hostResources.getResourceName(resid);
}
public String getResourcePackageName(int resid) throws NotFoundException {
return hostResources.getResourcePackageName(resid);
}
public String getResourceTypeName(int resid) throws NotFoundException {
return hostResources.getResourceTypeName(resid);
}
public String getResourceEntryName(int resid) throws NotFoundException {
return hostResources.getResourceEntryName(resid);
}
public void parseBundleExtras(XmlResourceParser parser, Bundle outBundle) throws XmlPullParserException, IOException {
hostResources.parseBundleExtras(parser, outBundle);
}
public void parseBundleExtra(String tagName, AttributeSet attrs, Bundle outBundle) throws XmlPullParserException {
hostResources.parseBundleExtra(tagName, attrs, outBundle);
}
}
MixResources的代码如下所示:
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();
}
public CharSequence getText(int id) throws Resources.NotFoundException {
try {
// 从宿主中获取text
return super.getText(id);
} catch (Resources.NotFoundException e) {
// 宿主中获取失败时,再从插件中获取
return pluginResources.getText(id);
}
}
public String getString(int id) throws Resources.NotFoundException {
try {
// 从宿主中获取String
return super.getString(id);
} catch (Resources.NotFoundException e) {
// 宿主中获取失败时,再从插件中获取
return pluginResources.getString(id);
}
}
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);
}
}
public float getDimension(int id) throws Resources.NotFoundException {
try {
return super.getDimension(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getDimension(id);
}
}
public int getDimensionPixelOffset(int id) throws Resources.NotFoundException {
try {
return super.getDimensionPixelOffset(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getDimensionPixelOffset(id);
}
}
public int getDimensionPixelSize(int id) throws Resources.NotFoundException {
try {
return super.getDimensionPixelSize(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getDimensionPixelSize(id);
}
}
public Drawable getDrawable(int id) throws Resources.NotFoundException {
try {
return super.getDrawable(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getDrawable(id);
}
}
(Build.VERSION_CODES.LOLLIPOP)
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);
}
}
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;
}
}
}
(Build.VERSION_CODES.LOLLIPOP)
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);
}
}
public int getColor(int id) throws Resources.NotFoundException {
try {
return super.getColor(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getColor(id);
}
}
(Build.VERSION_CODES.M)
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);
}
}
public ColorStateList getColorStateList(int id) throws Resources.NotFoundException {
try {
return super.getColorStateList(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getColorStateList(id);
}
}
(Build.VERSION_CODES.M)
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);
}
}
public boolean getBoolean(int id) throws Resources.NotFoundException {
try {
return super.getBoolean(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getBoolean(id);
}
}
public XmlResourceParser getLayout(int id) throws Resources.NotFoundException {
try {
return super.getLayout(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getLayout(id);
}
}
public String getResourceName(int resid) throws Resources.NotFoundException {
try {
return super.getResourceName(resid);
} catch (Resources.NotFoundException e) {
return pluginResources.getResourceName(resid);
}
}
public int getInteger(int id) throws Resources.NotFoundException {
try {
return super.getInteger(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getInteger(id);
}
}
public CharSequence getText(int id, CharSequence def) {
try {
return super.getText(id, def);
} catch (Resources.NotFoundException e) {
return pluginResources.getText(id, def);
}
}
public InputStream openRawResource(int id) throws Resources.NotFoundException {
try {
return super.openRawResource(id);
} catch (Resources.NotFoundException e) {
return pluginResources.openRawResource(id);
}
}
public XmlResourceParser getXml(int id) throws Resources.NotFoundException {
try {
return super.getXml(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getXml(id);
}
}
(Build.VERSION_CODES.O)
public Typeface getFont(int id) throws Resources.NotFoundException {
try {
return super.getFont(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getFont(id);
}
}
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);
}
}
public Movie getMovie(int id) throws Resources.NotFoundException {
try {
return super.getMovie(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getMovie(id);
}
}
public XmlResourceParser getAnimation(int id) throws Resources.NotFoundException {
try {
return super.getAnimation(id);
} catch (Resources.NotFoundException e) {
return pluginResources.getAnimation(id);
}
}
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);
}
}
public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {
try {
return super.openRawResourceFd(id);
} catch (Resources.NotFoundException e) {
return pluginResources.openRawResourceFd(id);
}
}
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);
}
}
插件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;
}
}
前面介绍了宿主和插件资源的自定义方法,在加载插件apk时,还需要实例化这些资源的对象,代码如下:
SuperHostResources superHostResources = new SuperHostResources(application, pluginPath);
Resources resources = new MixResources(superHostResources.get(), application, pluginPath);
宿主APP启动后,要先加载插件的so库、ClassLoader和资源等,并缓存在内存中,需要时直接从内存中调用。前面介绍了加载方法,这里主要介绍一下如何缓存。
缓存插件信息的主要步骤如下:
自定义保存插件信息的类Plugin;
创建单例类PluginManager,持有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;
}
}
创建一个单例类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()方法。
宿主和插件是两个独立的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对象,并设置插件信息。
扫码关注 了解更多