近两年,Android的热升级技术成为Android领域的一个热点。由于快速迭代的需要,热修复,插件化的技术已经深入app及framework的各个研究领域。
背景技术
简单的介绍一下热修复技术的背景: 以往的app项目管理流程如下:
代码修复有两大主要方案:
- 阿里系的底层替换方案
- 腾讯系的类加载方案
两类方案的优劣:
底层替换方案限制多,但时效性最好,加载轻快,立即见效。
类加载方案时效性差,需要app的冷启动才能见效,但是修复范广,限制少。
关于底层替换方案,比较出色的应该是阿里的Sophix了。核心原理是替换java方法对应的底层虚拟机的ArtMethod,达到即时修复的效果。这个不是本文介绍的重点,详情大家可以参看《深入探索Android热修复技术原理》一书。 而冷启动的方式则是将要修改的代码打成dex通过插包或者是合并的方式打入dexElements里。这种方式能够突破底层的诸多限制,但是同样也会碰到一些Android原有校验规则的限制,比如:CLASS_ISPREVERIFIED问题。
framework特性插件化
同样的,在Android手机的framework层也遇到类似的问题。目前,各大手机厂商基本都会对Google的原生framework进行或多或少的定制。而如果framework的特性需要升级,以往的流程是:
而framework层有很多特性,在framework层的客户端,本质上是app依赖的一些系统级lib。为了缩短发布周期,让用户更快的体验到我们的新特性,我们也希望能够使用热升级技术,将特性lib从framework层脱离出来,成为一个独立的个体存在:
- 特性更新快,热升级
- 跨平台,不依赖于系统rom
- 向后兼容
support包
Google的support包就是Google对framework向后兼容的一个实现。将framework的部分特性抽离,做成support包的形式,单独发布,让新特性得以向后兼容,不依赖于系统rom,可以横跨多个Android版本。主要的实现方式是将support包作为静态jar一起打包至app,特性跟着app走而不跟随系统:
类加载
我们都知道Java的类加载是通过ClassLoader来加载的。
预加载
BootClassLoader是所有classLoader的parent,加载的优先级最高,负责加载一些需要预加载的类。类定义在 /libcore/ojluni/src/main/java/java/lang/ClassLoader.java
class BootClassLoader extends ClassLoader {
private static BootClassLoader instance;
public static synchronized BootClassLoader getInstance() {
if (instance == null) {
instance = new BootClassLoader();
}
return instance;
}
...
}
复制代码
从以上代码可见,BootClassLoader是ClassLoader的内部类,访问权限是包内可见,所以可以知道仅仅只对部分系统开放。那么BootClassLoader是在哪创建的,前面所说的预加载的类,又是在哪加载的?这个就要从系统启动时的zygote进程的初始化说起了。
zygote进程初始化在/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java执行,在该类的main函数里会执行一些系统的zygote进程的初始化操作,并预加载操作:
public static void main(String argv[]) {
...
preload(bootTimingsTraceLog);
...
}
复制代码
继续看preload方法:
static void preload(TimingsTraceLog bootTimingsTraceLog) {
...
preloadClasses();
...
}
复制代码
preload方法里会执行preloadClass()方法进行类的预加载:
private static void preloadClasses() {
...
InputStream is;
try {
is = new FileInputStream(PRELOADED_CLASSES);
} catch (FileNotFoundException e) {
Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
return;
}
...
try {
BufferedReader br
= new BufferedReader(new InputStreamReader(is), 256);
int count = 0;
String line;
while ((line = br.readLine()) != null) {
// Skip comments and blank lines.
line = line.trim();
...
try {
...
Class.forName(line, true, null);
...
} catch (ClassNotFoundException e) {
Log.w(TAG, "Class not found for preloading: " + line);
} catch (UnsatisfiedLinkError e) {
Log.w(TAG, "Problem preloading " + line + ": " + e);
} catch (Throwable t) {
...
}
}
...
}
复制代码
可以看到,在preloadClass方法中,首先会去逐行读取PRELOADED_CLASSES文件,看下该文件指向的路径:
private static final String PRELOADED_CLASSES = "/system/etc/preloaded-classes";
复制代码
我们看下preload-classes文件:
...
android.app.INotificationManager
android.app.INotificationManager$Stub
android.app.INotificationManager$Stub$Proxy
android.app.IProcessObserver
android.app.IProcessObserver$Stub
android.app.ISearchManager
android.app.ISearchManager$Stub
android.app.IServiceConnection
android.app.IServiceConnection$Stub
android.app.ITransientNotification
android.app.ITransientNotification$Stub
android.app.IUiAutomationConnection
...
复制代码
可以看到该文件每一行基本都是framework的类,则Zygote进程通过BufferedReader逐行读取文件里的每一个类,通过Class.forName方法加载到Zygote进程的内存中。这里我们注意到,Class.forName的第三个传参为null
Class.forName(line, true, null);
复制代码
那么我们再来看/libcore/ojluni/src/main/java/java/lang/Class.java文件:
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)throws ClassNotFoundException {
if (loader == null) {
loader = BootClassLoader.getInstance();
}
...
}
复制代码
可以看到第三个参数是ClassLoader,并且当传参为null时,会构造BootClassLoader。到这里我们可以看到,预加载的类,最终是会交给BootClassLoader来加载。当app运行时,如果需要用到系统的类时,则可以通过访问他们父进程内存空间中在系统初始化时就已加载的类定义来访问framework的类了,而不需要在使用到时才重新进行加载。并且系统公共的类定义只存在与zygote进程的内存当中,而不需要每个app进程加载一份,可以同时达到空间和时间上的节省。
插件加载
从上述过程我们可以知道,framework里的特性,都是在系统启动时就通过BootClassLoader加载到zygote进程当中了,那么如果我们需要更新那些特性,则需要更新系统配置,系统jar包,并且重启系统。整个过程非常麻烦。如果我们希望将framework里的特性抽取出来,作为一种可插拔式的插件存在如何做到呢?上述ClassLoader的树形关系则给了我们启发:
public void addPluginLoader(Application app) {
if(!checkToDownloadPlugin(app)) {
return;
}
PathClassLoader parent = new PathClassLoader(mDexPath, app.getClassLoader().getParent());
try {
Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(app.getClassLoader(), parent);
} catch (NoSuchFieldException e) {
...
} catch (IllegalAccessException e) {
...
}
}
复制代码
其中的mDexPath则是插件特性的路径,此方法可放在Application的onCreate方法里执行,实现插件的加载。 一般来说,卸载插件,不能影响到app的其他特性,所以在整体架构设计上我们还需要有一个接口作为没有plugin的默认实现打包到apk当中。如下图所示:
- 检查当前是否存在plugin或plugin是否是最新,如果不是则从云端下载,并校验其安全性。
- 如果plugin已经ready,则构建plugin的ClassLoader并进行app classLoader的parent重定向。
例如plugin里有一个叫PluginFeature的类,如果classLoader已经重定向好,则根据ClassLoader的双亲代理特性,虽然说interface里也存在同样的PluginFeature的类,但是interface是由app的ClassLoader加载的,由于plugin的ClassLoader优先级更高,会去加载plugin的PluginFeature类,这样,就可以达到接口与实现的分离。当我们需要更新特性时,只需要更新plugin,而不需要更新app。以达到特性更好的模块化开发,降低特性与app的耦合度。
当然这种方式,我们得保证对应用开放的接口不变,如果接口需要改变,应用还是需要更新自己的apk。 与Google提供的的support方案相比,这种动态加载的插件化方案能够给我们带来以下好处:
- 在接口不变的情况下实现热更新,不需要重新安装apk。
- 系统可以只存在一份插件,而不需要每个apk一份,节省rom空间。
当然,Android的plugin特性里不仅仅只是代码,还有资源。我们知道已安装的apk资源是在apk之间互相访问的,但是我们下载的plugin并不希望在系统里安装,仅仅只是作为一个文件存在于手机系统中,那plugin里资源的加载如何实现呢?感兴趣的同学可以看下下一篇:《framework插件化技术-资源加载(免安装)》