之前已经对我们学习插件化原理需要的预备知识进行了比较详细的讲解了,从这篇文章开始,我们将具体介绍插件化原理,同时会根据原理写一个比较简单的插件化管理器。
插件化主要用到的技术知识有:
- Android ClassLoader 加载 class 文件原理,这也是插件化最重要的技术点,我们在上篇文章中讲解的也比较详细了,插件化框架都会通过自定义 ClassLoader 来加载插件中的 class 文件。
- Java 反射原理,这是制作插件化框架中最基础和最核心的知识点了。
- Android 资源加载原理,即 Android 如何加载资源文件,主要通过 Resource 类和 AssetManager 类等来完成资源的加载。
- 四大组件加载原理。了解四大组件的加载流程,以及它们是如何通过 ActivityManagerService 完成与系统的通信。
这四点是最基本要了解的点,你还需要了解像 Gradle 打包原理,Android Framework 层以及一些包的管理——PackageManager 的原理,所以如果想制作一个插件化框架,实际上是非常复杂的,要对 Android 系统非常熟悉,这里我们只讲解基本原理。
Manifest 处理
清单文件在 App 中非常重要,它记录了你的应用中有哪些组件。
这是市面上大部分的框架对清单文件的处理方式。首先,无论是宿主 app 还是 aar 还是 Bundle,都是有自己的清单文件的,那么我们平时使用的时候,当我们依赖 library 或者 aar 的时候,就会有多个清单文件,这个时候,Gradle 在构建 app 产物的时候,会将 aar 的 manifest 文件 merge 到 app module 中的清单文件中。
基于这个原理,插件化框架会修改整个打包流程,在输出 apk 的 manifest 文件的时候,会将所有插件的清单文件都合并到宿主清单文件中,这样的话,宿主 manifest 就记录了所有插件和 aar 文件中所有清单的内容,这样就可以保证我们调用各个插件组件的时候不会报错。
光是清单文件的处理,就比较复杂了。你不仅要了解 Gradle 的打包原理,甚至还需要去修改流程。这样才能打到合并清单文件的要求。
我们来总结下插件化框架对于 manifest 的处理,主要工作主要有两个:
- 文件的合并,实际上就是一个 IO 操作,将所有的清单文件合并到一个总的 manifest 文件中。
- 修改构建流程,在构建时将所有插件的清单文件合并到宿主的 manifest 文件中。大家有兴趣的话可以深入了解下如何修改编译流程从而完成清单文件的合并。
插件类加载
每个插件实际上都是一个 APK,每个 APK 有自己的 Dex 文件,所有的 class 字节码就都存储在了这些 dex 文件中的。市面上绝大多数的插件化框架都是根据上图的这样的方式来加载插件中的类的。
在加载之前,他首先会区分宿主 apk 和 插件 apk,这样区分的好处是,因为宿主 apk 已经安装到了系统中了,所以系统会给宿主 apk 创建 ClassLoader ,而无需手动去创建了。所以宿主 apk 的 ClassLoader 使用 PathClassLoader 就完全够用了,PathClassLoader 可以加载已安装 apk 中的类,这个我们在之前的文章中已经分析过了。
对于插件 apk 来说,因为没有安装到我们系统中,所以插件 apk 本身是没有 ClassLoader 的,系统也不会帮我们创建,需要我们自己手动创建 apk,插件化框架会给每个插件创建对应的 ClassLoader,在加载插件中 apk 文件的时候,就使用我们创建的 ClassLoader 来加载插件 apk 中的类。
根据这个思路,我们就会引出两个问题:
- 如何自定义 ClassLoader 加载类文件
- 如何调用插件 apk 文件中的类
下面我们就简单实现下上面两个问题的代码,通过简单的模拟来理解原理。
首先我们新建一个项目,并且在项目下再新建一个 Project 作为插件模块。
这里的app
代表宿主模块,app.bundle
代表某个插件模块,名字大家不要过多纠结,这里只是为了遵循 Small 框架而起的插件名。app.bundle
中有一个简单的类,静态方法输出一段 Log:
public class BundleUtil {
public static void printLog(){
Log.e("Bundle","I am a class in the Bundle");
}
}
现在我们的宿主模块并没有在build.gradle
中 compile 这个模块,我们要手动在宿主 apk 中加载并调用这个类。现在我们在宿主模块的 MainActivity 中的 onCreate() 方法中实现加载逻辑。
protect void onCreate(Bundle savedInstanceState){
//省略一些代码
...
String apkPath = getExternalCacheDir().getAbsolutePath()+"/bundle-debug.apk";
loadApk(apkPath);
}
private void loadApk(String apkPath) {
//应用内部目录,MODE_PRIVATE 代表只有自己应用可以访问这个路径。
File optDir = getDir("opt", MODE_PRIVATE);
//初始化 classLoader,通过 DexClassLoader 来加载指定目录下的插件中的类
DexClassLoader classLoader = new DexClassLoader(apkPath,
optDir.getAbsolutePath(), null, this.getClassLoader());
try {
//获取指定路径插件的 class 字节码文件
Class cls = classLoader.loadClass("org.sojex.stockquotes.bundle.BundleUtil");
if (cls != null) {
Object instance = cls.newInstance();
Method method = cls.getMethod("printLog");
method.invoke(instance);
}
} catch (Exception e) {
e.printStackTrace();
}
}
这里apkPath
指的是插件 apk 存放的路径,我们把app.bundle
通过./gradlew assembleDebug
指令打出 apk 包,并通过adb push
命令推送到手机上的 apkPath 对应的目录中。
loadApk()
方法是我们的核心方法,我们传入一个apkPath
参数,指定插件 apk 存放的路径,再通过Context.getDir
获取一个应用内部路径,使用这两个参数,可以新建一个DexClassLoader
对象,我们之前讲过,DexClassLoader
可以加载没有安装的 apk 文件中的类,通过它的loadClass
方法,获取到BundleUtil
的字节码文件。最后通过反射,即可调用到插件 apk 类中的 printLog()
方法。我们运行宿主 apk ,发现结果成功的打印了出来。
我们下面自定义一个 ClassLoader。
public class CustomClassLoader extends DexClassLoader {
public CustomClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
}
/**
* 定义 ClassLoder 要以何种策略加载 class 文件
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData != null) {
return defineClass(name, classData, 0, classData.length);
} else {
throw new ClassNotFoundException();
}
}
private byte[] getClassData(String name) {
try {
InputStream inputStream = new FileInputStream(name);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = -1;
while ((bytesNumRead = inputStream.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
自定义一个 ClassLoader 最核心的就是重写findClass
方法,方法里要定义我们要以何种策略加载 class 文件。现在我们没有什么特殊的策略,
这里就是通过指定类的路径加载字节码,最后通过获取到的字节码转化为 class 对象,getClassData
方法就是一个简单的文件读取。这里只是一个模拟如何定义 ClassLoader。通过自定义 ClassLoader,表明插件化框架可以为每一个插件维护一个 ClassLoader,在加载普通类的时候就会绕过 Android 系统的加载机制,即使没有安装这些插件 apk,我们依然能加载其中的类。
因为 Android 系统在加载 apk 的时候会创建一个 PathClassLoader,而插件 apk 的加载绕过了 Android 系统,所以我们就要手动的为每一个插件 apk 都要创建一个 ClassLoader。不仅如此,如果宿主和各插件 apk 中有同名类,如果不为每个插件创建 ClassLoader,那么如果该同名类已经被 ClassLoader 加载过,其他的同名类就无法再被加载了,而不同的 ClassLoader 的同名类不会被判定为同一个类,插件中的同名类在调用的时候依然会被加载。
当然,真正的商业插件化框架不会这么简单,类加载模块不仅要完成类的查找和加载,还要对插件的 ClassLoader 进行管理,确保所有类都能加载。
那么下一篇文章我们将写一个简单的插件管理器来模拟插件化框架的管理步骤。
本文部分内容参考于慕课网实战课程「Android 应用发展趋势必备武器 热修复与插件化」,有兴趣的朋友可以付费学习。
插件化实战课程