Android热更新技术的研究与实现
——必备引言——
最近时间确实有点紧张啦!不过我还是连日赶工完成了这篇文章,没办法,阳阳的神秘大奖对我来说诱惑很大呀!
之前的比赛中第一篇博文介绍了kotlin语言:
使用Kotlin来开发Android【安卓巴士博文大赛】第二篇是正在重构为kotlin的项目kibo,主要侧重于使用框架的讲解:
使用Kotlin开发Android项目-Kibo【安卓巴士博文大赛】
本文因为篇幅较长,可能会有错别字出现,望见谅。
第一部分重点是对当下热门的热更新方案进行研究,第二部分则是自己动手实现一个自己的热更新框架。
Android热更新技术的研究与实现之研究篇
热更新这个词出现的时间已经很久了,感觉现在要找工作才来看是晚了不少,但是好东西什么时候学习都不晚的。
今天看到一句话,和大家分享下,人一生有三样东西是别人抢不走的:
吃进胃里的食物
藏在心中的梦想
读进大脑里的书
所以趁着我们的时光正好,多学点东西肯定是赚翻的!!(当然多吃点也没错,不配点图感觉好突兀)
言归正传,首先我们要了解与热更新相关的一些概念吧!
———概念讲解——–
热更新 相关概念
- 组件化—-就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。我之前的开发方式基本上都是这一种。具体可以参考Android组件化方案
- 插件化–将整个app拆分成很多模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk(组件化的每个模块是个lib),最终打包的时候将宿主apk和插件apk分开或者联合打包。开发中,往往会堆积很多的需求进项目,超过 65535 后,插件化就是一个解决方案。
具体组件化和插件化分析大家可以看这个系列,讲解和例子以及源码都很清楚:
APP项目如何与插件化无缝结合
放张图帮大家理解:
- 热更新 – 更新的类或者插件粒度较小的时候,我们会称之为热修复,一般用于修复bug!!比如更新一个bug方法或者紧急修改lib包,甚至一个类等。2016 Google 的 Android Studio 推出了Instant Run 功能 同时提出了3个名词;
- “热部署” – 方法内的简单修改,无需重启app和Activity。
- “暖部署” – app无需重启,但是activity需要重启,比如资源的修改。
- “冷部署” – app需要重启,比如继承关系的改变或方法的签名变化等。
- 增量更新,与热更新区别最大的一个,其实这个大家应该很好理解,安卓上的有些很大的应用,特别是游戏,大则好几个G的多如牛毛,但是每次更新的时候却不是要去下载最新版,而只是下载一个几十兆的增量包就可以完成更新了,而这所使用的技术就是增量更新了。实现的过程大概是这个样子的:我们手机上安装着某个大应用,下载增量包之后,手机上的apk和增量包合并形成新的包,然后会再次安装,这个安装过程可能是可见的,或者应用本身有足够的权限直接在后台安装完成。
今天碰到Android Studio的更新,这应该就是增量更新啦!补丁包只有51M,如果下载新版本有1G多。
而热更新究竟是什么呢?
有一些这样的情况, 当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。老是发布版本用户会疯掉的!!!(好吧 猿猿们也会疯掉。。)
这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?
这种需要替换运行时新的类和资源文件的加载,就可以认为是热操作了。而在热更新出现之前,通过反射注解、反射调用和反射注入等方式已经可以实现类的动态加载了。而热更新框架的出现就是为了解决这样一个问题的。
从某种意义上来说,热更新就是要做一件事,替换。当替换的东西属于大块内容的时候,就是模块化了,当你去替换方法的时候,叫热更新,当你替换类的时候,加热插件,而且重某种意义上讲,所有的热更新方案,都是一种热插件,因为热更新方案就是在app之外去干这个事。就这么简单的理解。无论是替换一个类,还是一个方法,都是在干替换这件事请。。这里的替换,也算是几种hook操作,无论在什么代码等级上,都是一种侵入性的操作。
所以总结一句话简单理解热更新就是
改变app运行行为的技术!(或者说就是
对已发布app进行bug修复的技术) 此时的猿猿们顿时眼前一亮,用户也笑了。。
好的,现在我们已经知道热更新为何物了,那么我们就先看看热更新都有哪些成熟的方案在使用了。
热更新方案介绍
热更新方案发展至今,有很多团队开发过不同的解决方案,包括Dexposed、AndFix,(HotFix)Sophix,Qzone超级补丁的类Nuwa方式,微信的Tinker, 大众点评的nuwa、百度金融的rocooFix, 饿了么的amigo以及美团的robust、腾讯的Bugly热更新。
苹果公司现在已经禁止了热更新,不过估计也组织不了开发者们的热情吧!
我先讲几种方案具体如何使用,说下原理,最后再讲如何实现一个自己的热更新方案!
–Dexposed & AndFix & (HotFix)SopHix –阿里热更新方案
Dexposed (阿里热更新方案一)
“Dexposed” 是大厂阿里以前的一个开源热更新项目,基于 Xposed
“Xposed”的AOP框架,方法级粒度,可以进行AOP编程、插桩、热补丁、SDK hook等功能。Xposeed 大家如果不熟悉的话可以看下:
Xposed源码剖析——概述,我以前用 Xposed 做过一些小东西(其实就是获取 root 权限后hook 修改一些手机数据,比如支付宝步数,qq 微信步数等,当然了,余额啥的是改不了滴),在这里就不献丑了,毕竟重点也不是这个。我们可以看出 Xposed 有一个缺陷就是需要 root ,而 Dexposed 就是一个不需要 root 权限的 hook 框架。以前阿里的主流 app ,例如手机淘宝,支付宝,天猫都使用了 Dexposed 支持在线热更新,现在已经不用了,用最新的 Sophix 了,后面讲。
Dexposed 中的 AOP 原理来自于 Xposed。在 Dalvik 虚拟机下,主要是通过改变一个方法对象方法在 Dalvik 虚拟机中的定义来实现,具体做法就是将该方法的类型改变为 native 并且将这个方法的实现链接到一个通用的 Native Dispatch 方法上。这个 Dispatch 方法通过 JNI 回调到 Java 端的一个统一处理方法,最后在统一处理方法中调用 before , after 函数来实现AOP。在 Art 虚拟机上目前也是是通过改变一个 ArtMethod 的入口函数来实现。
可惜 android 4.4之后的版本都用 Art 取代了 Dalvik ,所以要 hook Android4.4 以后的版本就必须去适配 Art 虚拟机的机制。目前官方表示,为了适配 Art 的 dexposed_l 只是 beta 版,所以最好不要在正式的线上产品中使用它。
现在阿里已经抛弃 Dexposed 了,原因很明显,4.4 以后不支持了,我们就不细细分析这个方案了,感兴趣的朋友可以通过
“这里”了解。简单讲下它的实现方式:
引入一个名为 patchloader 的 jar 包,这个函数库实现了一个热更新框架,宿主 apk (可能含有 bug 的上线版本)在发布时会将这个 jar 包一起打包进 apk 中;
补丁 apk (已修复线上版本 bug 的版本)只是在编译时需要这个 jar 包,但打包成 apk 时不包含这个 jar 包,以免补丁 apk 集成到宿主 apk 中时发生冲突;
补丁 apk 将会以 provided 的形式依赖 dexposedbridge.jar 和 patchloader.jar;
通过在线下载的方式从服务器下载补丁 apk ,补丁 apk 集成到宿主 apk 中,使用补丁 apk 中的函数替换原来的函数,从而实现在线修复 bug 的功能。
AndFix (阿里热更新方案二)
AndFix 是一个 Android App 的在线热补丁框架。使用此框架,我们能够在不重复发版的情况下,在线修改 App 中的 Bug 。AndFix 就是 “Android Hot-Fix”的缩写。支持 Android 2.3到6.0版本,并且支持 arm 与 X86 系统架构的设备。完美支持 Dalvik 与 ART 的 Runtime。AndFix 的补丁文件是以 .apatch 结尾的文件。它从你的服务器分发到你的客户端来修复你 App 的 bug 。
AndFix 更新实现过程(画的丑勿怪⊙﹏⊙):
首先添加依赖
`compile 'com.alipay.euler:andfix:0.3.1@aar'`
然后在 Application.onCreate() 中添加以下代码
`patchManager = new PatchManager(context);` `patchManager.init(appversion);//current version` `patchManager.loadPatch();`
可以用这句话获取 appversion,每次 appversion 变更都会导致所有补丁被删除,如果 appversion 没有改变,则会加载已经保存的所有补丁。
`String appversion= getPackageManager().getPackageInfo(getPackageName(), 0).versionName;`
然后在需要的地方调用 PatchManager 的 addPatch 方法加载新补丁,比如可以在下载补丁文件之后调用。
之后就是打补丁的过程了,首先生成一个 apk 文件,然后更改代码,在修复 bug 后生成另一个 apk。通过官方提供的工具 apkpatch 生成一个 .apatch 格式的补丁文件,需要提供原 apk,修复后的 apk,以及一个签名文件。
通过网络传输或者 adb push 的方式将 apatch 文件传到手机上,然后运行到 addPatch 的时候就会加载补丁。
AndFix 更新的原理:
首先通过虚拟机的 JarFile 加载补丁文件,然后读取 PATCH.MF 文件得到补丁类的名称
使用 DexFile 读取 patch 文件中的 dex 文件,得到后根据注解来获取补丁方法,然后根据注解中得到雷鸣和方法名,使用 classLoader 获取到 Class,然后根据反射得到 bug 方法。
jni 层使用 C++ 的指针替换 bug 方法对象的属性来修复 bug。
具体的实现主要都是我们在 Application 中初始化的
PatchManager中(具体分析在后面的注释可以看到)。
public PatchManager(Context context) {
mContext = context;
mAndFixManager = new AndFixManager(mContext);//初始化AndFixManager
mPatchDir = new File(mContext.getFilesDir(), DIR);//初始化存放patch补丁文件的文件夹
mPatchs = new ConcurrentSkipListSet<Patch>();//初始化存在Patch类的集合,此类适合大并发
mLoaders = new ConcurrentHashMap<String, ClassLoader>();//初始化存放类对应的类加载器集合
}
其中
mAndFixManager = new AndFixManager(mContext);的实现:
public AndFixManager(Context context) {
mContext = context;
mSupport = Compat.isSupport();//判断Android机型是否适支持AndFix
if (mSupport) {
mSecurityChecker = new SecurityChecker(mContext);//初始化签名判断类
mOptDir = new File(mContext.getFilesDir(), DIR);//初始化patch文件存放的文件夹
if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail
mSupport = false;
Log.e(TAG, "opt dir create error.");
} else if (!mOptDir.isDirectory()) {// not directory
mOptDir.delete();//如果不是文件目录就删除
mSupport = false;
}
}
}
。。。。。。。。。。。。
然后是对版本的初始化
mPatchManager.init(appversion),
init(String appVersion)代码如下:
public void init(String appVersion) {
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, "patch dir create error.");
return;
} else if (!mPatchDir.isDirectory()) {// not directory
mPatchDir.delete();
return;
}
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);//存储关于patch文件的信息
//根据你传入的版本号和之前的对比,做不同的处理
String ver = sp.getString(SP_VERSION, null);
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
cleanPatch();//删除本地patch文件
sp.edit().putString(SP_VERSION, appVersion).commit();//并把传入的版本号保存
} else {
initPatchs();//初始化patch列表,把本地的patch文件加载到内存
}
}
/*************省略初始化、删除、加载具体方法实现*****************/
init 初始化主要是对 patch 补丁文件信息进行保存或者删除以及加载。
那么 patch 补丁文件是如何加载的呢?其实 patch 补丁文件本质上是一个 jar 包,使用 JarFile 来读取即可:
public Patch(File file) throws IOException {
mFile = file;
init();
}
@SuppressWarnings("deprecation")
private void init() throws IOException {
JarFile jarFile = null;
InputStream inputStream = null;
try {
jarFile = new JarFile(mFile);//使用JarFile读取Patch文件
JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);//获取META-INF/PATCH.MF文件
inputStream = jarFile.getInputStream(entry);
Manifest manifest = new Manifest(inputStream);
Attributes main = manifest.getMainAttributes();
mName = main.getValue(PATCH_NAME);//获取PATCH.MF属性Patch-Name
mTime = new Date(main.getValue(CREATED_TIME));//获取PATCH.MF属性Created-Time
mClassesMap = new HashMap<String, List<String>>();
Attributes.Name attrName;
String name;
List<String> strings;
for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) {
attrName = (Attributes.Name) it.next();
name = attrName.toString();
//判断name的后缀是否是-Classes,并把name对应的值加入到集合中,对应的值就是class类名的列表
if (name.endsWith(CLASSES)) {
strings = Arrays.asList(main.getValue(attrName).split(","));
if (name.equalsIgnoreCase(PATCH_CLASSES)) {
mClassesMap.put(mName, strings);
} else {
mClassesMap.put(
name.trim().substring(0, name.length() - 8),// remove
// "-Classes"
strings);
}
}
}
} finally {
if (jarFile != null) {
jarFile.close();
}
if (inputStream != null) {
inputStream.close();
}
}
}
然后就是最重要的
patchManager.loadPatch():
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set<String> patchNames;
List<String> classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);//获取patch对应的class类的集合List
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);//修复bug方法
}
}
}
循环获取补丁对应的 class 类来修复 bug 方法,
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),classes):
public synchronized void fix(File file, ClassLoader classLoader,
List<String> classes) {
if (!mSupport) {
return;
}
//判断patch文件的签名
if (!mSecurityChecker.verifyApk(file)) {// security check fail
return;
}
/******省略部分代码********/
//加载patch文件中的dex
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);
if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}
ClassLoader patchClassLoader = new ClassLoader(classLoader) {
@Override
protected Class<?> findClass(String className)
throws ClassNotFoundException {//重写ClasLoader的findClass方法
Class<?> clazz = dexFile.loadClass(className, this);
if (clazz == null
&& className.startsWith("com.alipay.euler.andfix")) {
return Class.forName(className);// annotation’s class
// not found
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
Enumeration<String> entrys = dexFile.entries();
Class<?> clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
clazz = dexFile.loadClass(entry, patchClassLoader);//获取有bug的类文件
if (clazz != null) {
fixClass(clazz, classLoader);// next code
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
//获取此方法的注解,因为有bug的方法在生成的patch的类中的方法都是有注解的
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();//获取注解中clazz的值
meth = methodReplace.method();//获取注解中method的值
if (!isEmpty(clz) && !isEmpty(meth)) {
replaceMethod(classLoader, clz, meth, method);//next code
}
}
}
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
Class<?> clazz = mFixedClass.get(key);//判断此类是否被fix
if (clazz == null) {// class not load
Class<?> clzz = classLoader.loadClass(clz);
// initialize target class
clazz = AndFix.initTargetClass(clzz);//初始化class
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());//根据反射获取到有bug的类的方法(有bug的apk)
AndFix.addReplaceMethod(src, method);//src是有bug的方法,method是补丁方法
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
public static void addReplaceMethod(Method src, Method dest) {
try {
replaceMethod(src, dest);//调用了native方法
initFields(dest.getDeclaringClass());
} catch (Throwable e) {
Log.e(TAG, "addReplaceMethod", e);
}
}
private static native void replaceMethod(Method dest, Method src);
从上面的 bug 修复源码可以看出,就是在找补丁包中有 @MethodReplace 注解的方法,然后反射获取原 apk 中方法的位置,最后进行替换。
而最后调用的 replaceMethod(Method dest,Method src) 则是 native 方法,源码中有两个 replaceMethod:
extern void dalvik_replaceMethod(JNIEnv* env, jobject src, jobject dest);//Dalvik
extern void art_replaceMethod(JNIEnv* env, jobject src, jobject dest);//Art
从源码的注释也能看出来,因为安卓 4.4 版本之后使用的不再是 Dalvik 虚拟机,而是 Art 虚拟机,所以需要对不同的手机系统做不同的处理。
首先看 Dalvik 替换方法的实现:
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
clz->status = CLASS_INITIALIZED;
Method* meth = (Method*) env->FromReflectedMethod(src);
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
meth->jniArgInfo = 0x80000000;
meth->accessFlags |= ACC_NATIVE;//把Method的属性设置成Native方法
int argsSize = dvmComputeMethodArgsSize_fnPtr(meth);
if (!dvmIsStaticMethod(meth))
argsSize++;
meth->registersSize = meth->insSize = argsSize;
meth->insns = (void*) target;
meth->nativeFunc = dalvik_dispatcher;//把方法的实现替换成native方法
}
Art 替换方法的实现:
//不同的art系统版本不同处理也不同
extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
if (apilevel > 22) {
replace_6_0(env, src, dest);
} else if (apilevel > 21) {
replace_5_1(env, src, dest);
} else {
replace_5_0(env, src, dest);
}
}
//以5.0为例:
void replace_5_0(JNIEnv* env, jobject src, jobject dest) {
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
dmeth->declaring_class_->class_loader_ =
smeth->declaring_class_->class_loader_; //for plugin classloader
dmeth->declaring_class_->clinit_thread_id_ =
smeth->declaring_class_->clinit_thread_id_;
dmeth->declaring_class_->status_ = (void *)((int)smeth->declaring_class_->status_-1);
//把一些参数的指针给补丁方法
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->access_flags_ = dmeth->access_flags_;
smeth->frame_size_in_bytes_ = dmeth->frame_size_in_bytes_;
smeth->dex_cache_initialized_static_storage_ =
dmeth->dex_cache_initialized_static_storage_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->vmap_table_ = dmeth->vmap_table_;
smeth->core_spill_mask_ = dmeth->core_spill_mask_;
smeth->fp_spill_mask_ = dmeth->fp_spill_mask_;
smeth->mapping_table_ = dmeth->mapping_table_;
smeth->code_item_offset_ = dmeth->code_item_offset_;
smeth->entry_point_from_compiled_code_ =
dmeth->entry_point_from_compiled_code_;
smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;
smeth->native_method_ = dmeth->native_method_;//把补丁方法替换掉
smeth->method_index_ = dmeth->method_index_;
smeth->method_dex_index_ = dmeth->method_dex_index_;
LOGD("replace_5_0: %d , %d", smeth->entry_point_from_compiled_code_,
dmeth->entry_point_from_compiled_code_);
}
其实这个替换过程可以看做三步完成
打开链接库得到操作句柄,获取 native 层的内部函数,得到 ClassObject 对象
修改访问权限的属性为 public
得到新旧方法的指针,新方法指向目标方法,实现方法的替换。
如果我们想知道补丁包中到底替换了哪些方法,可以直接方便易 patch 文件,然后看到的所有含有 @ReplaceMethod 注解的方法基本上就都是需要替换的方法了。
最近我在学习 C++,顿时感觉到还是这种可以控制底层的语言是多么强大,不过安卓可以通过 JNI 调用 C++,也就没什么可吐槽的了!
好的,现在 AndFix 我们分析了一遍它的实现过程和原理,其优点是不需要重启即可应用补丁,遗憾的是它还是有不少缺陷的,这直接导致阿里再次抛弃了它,缺陷如下:
- 并不能支持所有的方法修复
不支持 YunOS
无法添加新类和新的字段
需要使用加固前的 apk 制作补丁,但是补丁文件很容易被反编译,也就是修改过的类源码容易泄露。
使用加固平台可能会使热补丁功能失效(看到有人在 360 加固提了这个问题,自己还未验证)。
Sophix—阿里终极热修复方案
不过阿里作为大厂咋可能没有个自己的热更新框架呢,所以阿里爸爸最近还是做了一个新的热更新框架
SopHix
巴巴再次证明我是最强的,谁都没我厉害!!!因为我啥都支持,而且没缺点。。简直就是无懈可击!
那么我们就来项目集成下看看具体的使用效果吧!具体就拿支持的
方法级替换来演示吧!
先去创建个应用:
获取 AppId:24582808-1,和 AppSecret:da283640306b464ff68ce3b13e036a6e 以及 RSA 密钥
**。三个参数配置在 application 节点下面:
<meta-data
android:name="com.taobao.android.hotfix.IDSECRET"
android:value="24582808-1" />
<meta-data
android:name="com.taobao.android.hotfix.APPSECRET"
android:value="da283640306b464ff68ce3b13e036a6e" />
<meta-data
android:name="com.taobao.android.hotfix.RSASECRET"
android:value="MIIEvAIBA**********" />
添加 maven 仓库地址:
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/repositories/releases"
}
}
添加 gradle 坐标版本依赖:
compile 'com.aliyun.ams:alicloud-android-hotfix:3.1.0'
项目结构也很简单:
MainActivity:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((TextView)findViewById(R.id.tv)).setText(String.valueOf(BuildConfig.VERSION_NAME));
findViewById(R.id.btn_click).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent;
intent = new Intent(MainActivity.this,SecondActivity.class);
startActivity(intent);
}
});
}
}
其实就是有一个文本框显示当前版本,还有一个按钮用来跳转到 SecondActivity
而
SecondActivity的内容:
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
String s = null;
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(SecondActivity.this, "弹出框内容弹出错啦!", Toast.LENGTH_SHORT).show();
}
});
}
}
也很简单,只有一个按钮,按钮点击之后弹出一个 Toast 显示“弹出框内容弹出错啦!”
就这样,我们的一个上线 app 完成了(粗糙是粗糙了点),下面来看下效果吧(请谅解我第一次录屏的渣渣技术,以后会做的越来越好)
然后我们的用户开始用了,发现一个bug!“弹出框弹出的内容是错误的!”,用户可不管别的,马上给我改好啊!
此时的开发er估计心头千万头草泥马在奔腾了,求神拜佛上线不要出问题,刚上线就出问题了,“where is my 测试er!!!”不说了,赶紧修吧,最暴力的方法就是 SecondActivity 的 Toast 中弹出“弹出框内容弹正常啦!”一句代码搞定!bingo!
如果没有热更新,可能就要搞个临时版本或者甚至发布一个新版本,但是现在我们有了 Sophix ,就不需要这么麻烦了。
首先我们去下载补丁打包工具(不得不说,工具确实比较粗糙(丑)。。。)
旧包:<必填> 选择基线包路径(有问题的 APK)。
新包:<必填> 选择新包路径(修复过该问题 APK)。
日志:打开日志输出窗口。
高级:展开高级选项
设置:配置其他信息。
GO!:开始生成补丁。
所以首先我们把旧包和新包添加上之后,配置好之后看看会发生什么吧!
强制冷启动是补丁打完后重启才生效。
时间看情况吧,因为项目本身内容比较少,所以生成补丁的速度比较快,等一下就好了。项目比较大的话估计需要等的时间长一点
我们来看看到底生成了什么?打开补丁生成目录
这个就是我们生成的补丁文件了,下一步补丁如何使用?
我们打开阿里的管理控制台,将补丁上传到控制台,就可以发布了.
这里有个坑,我用自己的中兴手机发现在使用补丁调试工具的时候一直获取包名错误,然后就借了别人的华为手机测试然后就可以了。最后我是在模拟器上完成录制的。
我们首先下载调试工具来看看效果吧,首先连接应用(坑就在这里,有的手机可能会提示包名错误,但是其实是没有错的,虽然官网给出了解决方案,可依旧没有解决,不得已只能用模拟器了)
然后有两种方式可以加载补丁包,一种是扫二维码,还有一种是加载本地补丁jar包,模拟器上实在不好操作啊!!!最后我屈服了,借了同学的手机扫二维码加载补丁包了。。。然后就会有 log 提示
从图中的 log 提示我们可以看出首先下载了补丁包,然后打补丁完成,要求我们重启 APP,那我们就重启呗,看到的当然就应该是补丁打好的 1.1 版本和 Toast 弹出正常啦!!
当然了,目前我们还是在调试工具上加载的补丁包,我们接下来将补丁包发布后就可以不用调试工具,直接打开 app 就可以实现打补丁了,这样就完成了 bug 的修复!
其实这么看起来确实是非常简单就实现了热修复,主要我们的生成补丁工作都是交给阿里提供的工具实现了,其实我们也能看得出来,Sophix 和前面介绍的 AndFix 很像,不同的地方是补丁文件已经给出工具可以一键生成了,而且支持的东西更多了。其他比如 so 库和 library 以及资源文件的更新大家可以查看官方文档了解。
其实 Sophix 主要是在阿里百川 HotFix 的版本上的一个更新,而 HotFix 又是什么呢?
所以阿里爸爸一直在进步着呢,知道技术存在问题就要去解决问题,这不,从Dexposed–>AndFix–>HotFix–>Sophix,技术是越来越成熟了。
下面介绍另外一个大厂的几种热更新方案
Qzone超级补丁 & 微信Tinker 腾讯热更新方案
巴巴家的热更新技术一直在发展,作为互联网巨头的腾讯怎甘落后,所以也是穷追不舍的干起来!
Qzone超级补丁(腾讯热更新方案一) & DEX加载原理
因为超级补丁技术是基于 DEX 分包方案,使用了多 DEX 加载的原理,所以我先给大家简单讲下
DEX加载 的一些东西:Android 程序要运行需要先编译打包成 dex,之后才可以被 Android 虚拟机解析运行。因此我们如果想要即时修补 bug 就要让修复的代码被 Android 虚拟机识别,如何才能让虚拟机认识我们修改过的代码呢,也就是我们需要把修改过的代码打包成单独的 dex。
然后接下来要做的就是如何让虚拟机加载我们修改过后的 dex jar包中的类呢? 我们需要了解的是
类加载器是如何加载类的。
在 Android 中有 2 种类加载器: PathClassLoader 和 DexClassLoader,源码如下:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
这两者的区别是:
DexClassLoader:可以加载 jar/apk/dex,可以从 SD 卡中加载未安装的 apk;
PathClassLoader:要传入系统中 apk 的存放 Path,所以只能加载已经安装的 apk 文件。
两个类都只是简单的对
BaseDexClassLoader 做了一下封装,具体的实现还是在父类里。不过这里也可以看出,
PathClassLoader 的
optimizedDirectory 只能是 null,进去
BaseDexClassLoader 看看这个参数是干什么的
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
这里创建了一个
DexPathList 实例:
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
……
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
}
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = new ZipFile(file);
}
……
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
//**
//* Converts a dex/jar file path and an output directory to an
//* output file path for an associated optimized dex file.
//
private static String optimizedPathFor(File path,
File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
我们不需要弄的特别明白,只要知道这里 optimizedDirectory 是用来缓存我们需要加载的 dex 文件的,并创建一个 DexFile 对象,如果它为 null,那么会直接使用 dex 文件原有的路径来创建DexFile 对象。
optimizedDirectory 必须是一个内部存储路径,无论哪种动态加载,加载的可执行文件一定要存放在内部存储。DexClassLoader 可以指定自己的 optimizedDirectory,所以它可以加载外部的 dex,因为这个 dex 会被复制到内部路径的 optimizedDirectory;而 PathClassLoader 没有 optimizedDirectory,所以它只能加载内部的 dex,这些大都是存在系统中已经安装过的 apk 里面的。
上面还只是创建了类加载器的实例,其中创建了一个 DexFile 实例,用来保存 dex 文件,我们猜想这个实例就是用来加载类的。
Android 中,ClassLoader 用 loadClass 方法来加载我们需要的类
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
loadClass 方法调用了 findClass 方法,而 BaseDexClassLoader 重载了这个方法,到 BaseDexClassLoader 看看
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
结果还是调用了 DexPathList的findClass
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
这里遍历了之前所有的 DexFile 实例,其实也就是遍历了所有加载过的 dex 文件,再调用 loadClassBinaryName 方法一个个尝试能不能加载想要的类。
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
上面的类加载中 DexPathList 的 findClass,一个 classloader 可以包含多个 dex,其中这个集合中的对象就是所有的 dex 文件,然后调用从头开始遍历所有的 dex 如果在 dex 中找到所需要的类,那么就直接返回,也就是说如果存在多个 dex 在前一个 dex 中找到了需要找到的类,也就不会继续查找其他 dex 中有没有这个类了。
而
dex.loadClassBinaryName(name, definingContext)在这个 dex 中查找相应名字的类,之后 defineClass 把字节码交给虚拟机就完成了类的加载。
也许你看到这里会比较晕,没关系,上面的你可以当做没看到,直接看下面这句话吧:如果要加载一个类,就会调用 ClassLoader 的 findClass 方法,在dex中查找这个类,找到后加载到内存
so,我们的关键人物就是在 findClass 的时候让类加载找到我们修复过后的类,而不是未修复的类。
例如,比如说要修复的类名为 BugClass,我们要做的就是将这个类修改为正确的后,打包成 dex 的 jar,然后想办法让类加载去查找我们打包的jar中的 BugClass 类 而不是先前的 BugClass 类,这样,加载类的时候使用的就是我们修复过后的代码,而忽略掉原本的有问题的代码。问题又转变到了如何让我们自己打包的 dex 文件放到原本的 dex 文件之前,也就是把我们打包的 dex 放到 dexElements 集合的靠前的位置
这样算是把超级补丁的原理讲了一遍,应该有一个大概的认识了,而超级补丁所做的就是让类加载器只找到我们修复完成的类!
通俗的说 也就是我们要改变的是 dexElements 中的内容,在其中添加一个 dex 而且放在靠前的位置,而 dexElements 是 PathClassLoader类中的一个成员变量。
因为 Qzone 超级补丁方案并没有开源,在这里只是给大家讲了类加载机制来说下实现原理,具体的实现过程应该是这样子的(图可能是最直观的):
通过反射的方式获取应用的 PathdexClassloader —> PathList —> DexElements,再获取补丁 dex 的 DexClassloader —> PathList —> DexElements,然后通过 combinArray 的方法将2个 DexElements 合并,补丁的 DexElements 放在前面,然后使用合并后的 DexElements 作为 PathdexClassloader 中的 DexElements,这样在加载的时候就可以优先加载到补丁 dex,从中可以加载到我们的补丁类,能基本保证稳定性与兼容性
优势:
没有合成整包(和微信 Tinker 比起来,下一个讲),产物比较小,比较灵活
可以实现类替换,兼容性高。(某些手机不起作用)
不足:
不支持即时生效,必须通过重启才能生效。
为了实现修复这个过程,必须在应用中加入两个 dex ! dalvikhack.dex 中只有一个类,对性能影响不大,但是对于 patch.dex 来说,修复的类到了一定数量,就需要花不少的时间加载。
在 ART 模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到 patch.dex 中,导致补丁包异常的大,进一步增加应用启动加载的时候,耗时更加严重。
微信 Tinker (腾讯热更新方案二)
对于微信来说,实现热更新使用一个“高可用”的补丁框架,至少满足以下几个条件:
稳定性与兼容性;微信需要在数亿台设备上运行,即使补丁框架带来1%的异常,也将影响到数万用户。保证补丁框架的稳定性与兼容性是我们的第一要务;
性能;微信对性能要求也非常苛刻,首先补丁框架不能影响应用的性能,这里基于大部分情况下用户不会使用到补丁。其次补丁包应该尽量少,这关系到用户流量与补丁的成功率问题;
易用性;在解决完以上两个核心问题的前提下,我们希望补丁框架简单易用,并且可以全面支持,甚至可以做到功能发布级别。
看完上面的 Qzone 超级补丁方案(主要是给大家讲了dex的加载原理,不过这个对后面的分析tinker也很重要)。那么到底有没有那么一种方案,能做到开发透明,但是却没有 QZone 方案的缺陷呢?肯定是有的,比如我们完全可以使用新的 Dex,那样既不出现 Art 地址错乱的问题,在 Dalvik 也无须插桩。当然考虑到补丁包的体积,我们不能直接将新的 Dex 放在里面。但我们可以将新旧两个 Dex 的差异放到补丁包中,最简单我们可以采用 BsDiff 算法。
Tinker 是微信官方的 Android 热补丁解决方案,它支持动态下发代码、So 库以及资源,让应用能够在不需要重新安装的情况下实现更新。
Tinker 更像是 APP 的增量更新,在服务器端通过差异性算法,计算出新旧 dex 之间的差异包,推送到客户端,进行合成。传统的差异性算法有 BsDiff,而 Tinker 的牛逼之处就在于它自己基于 Dex 的文件格式,研发出了 DexDiff 算法,这个我们后面再说。
如果我们的应用想要集成 Tinker 热更新的话,可以直接在腾讯的 Bugly 创建自己的应用,然后接入。这里我就创建了一个应用,但是集成我是直接使用官方的例子。因为官方给出的集成步骤很详细,还有对应的一整套教程,大家用起来应该都很方便。我们先来做个尝试:
首先创建一个应用,获取 AppID 和 AppKey,然后在 GitHub 上下载 BuglyHotfixEasyDemo(这里我就不新建项目了,感觉也没有必要),目录结构如下:
BugClass 就是存在错误的类:
public class BugClass {
public String bug() {
// 这段代码会报空指针异常
// String str = null;
// Log.e("BugClass", "get string length:" + str.length());
return "This is a bug class";
}
}
LoadBugClass 就是获取 BugClass中返回的字符串
public class LoadBugClass {
/**
*获取bug字符串.
*
*@return 返回bug字符串
*/
public static String getBugString() {
BugClass bugClass = new BugClass();
return bugClass.bug();
}
}
而 MainActivity 中有很多按钮,其中有一个按钮式,点击弹出 Toast,显示的内容就是上面返回的字符串;
/**********省略N行代码*************/
/**
*根据应用patch包前后来测试是否应用patch包成功.
*
*应用patch包前,提示"This is a bug class"
*应用patch包之后,提示"The bug has fixed"
*/
public void testToast() {
Toast.makeText(this, LoadBugClass.getBugString(), Toast.LENGTH_SHORT).show();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnShowToast: // 测试热更新功能 点击显示结果按钮
testToast();
break;
/***********再次省略N行代码************/
从项目结构上看也是很简单那的一个例子,多渠道打包我们就不尝试了,就来个简单的基本打包实现吧!
显示效果(点击显示效果按钮后,现在还是有 bug 的包,所以显示的是bug class):
Tinker 热更新集成实现
1、编译基准包
配置基准包的 tinkerId
在配置好如 AppId 等之后还需要在 tinker-support.gradle 文件中需要写入自己的配置:
tinkerId 最好是一个唯一标识,例如 git 版本号、versionName 等等。 如果你要测试热更新,你需要对基线版本进行联网上报。
这里强调一下,基线版本配置一个唯一的 tinkerId,而这个基线版本能够应用补丁的前提是集成过热更新 SDK,并启动上报过联网,这样我们后台会将这个 tinkerId 对应到一个目标版本,例如 tinkerId = “bugly_1.0.0” 对应了一个目标版本是 1.0.0,基于这个版本打的补丁包就能匹配到目标版本。
编译生成基准包(原包,含 bug)
执行 assembleRelease 编译生成基准包:会在 build/baseApk 目录下生成如下文件,具体路径和文件名可以自己配置
启动apk,上报联网数据
我们每次冷启动都会请求补丁策略,会上报当前版本号和 tinkerId,这样我们后台就能将这个唯一的 tinkerId 对应到一个版本,测试的时候可以打开 logcat 查看我们的日志,如下图所示:
我们能看到 tinkerId.
2、对基线版本的bug修复
其实就是讲 BugClass 中的返回字符串改为 “The bug has fixed”;
3、根据基线版本生成补丁包
修改待修复 apk 路径、mapping 文件路径、resId 文件路径
/**
*此处填写每次构建生成的基准包目录
*/
def baseApkDir = "app-0813-20-54-50" //改成刚才生成的目录 其实是按日期时间生成的目录
tinkerId = "1.0.1-patch"
执行构建补丁包的 task,其实生成的就是 bug 修复的完整 apk
如果你要生成不同编译环境的补丁包,只需要执行 TinkerSupport 插件生成的 task,比如 buildTinkerPatchRelease 就能生成 release 编译环境的补丁包。 注: TinkerSupport 插件版本低于 1.0.4 的,需要使用 tinkerPatchRelease 来生成补丁包 。
生成的补丁包在 build/outputs/patch 目录下:
主要会生成 3 个文件: unSignedApk, signedApk 以及 signedWith7ZipApk 。
unSignedApk 只要将 tinker_result 中的文件压缩到一个压缩包即可。
signedApk 将 unSignedApk 使用 jarsigner 进行签名。
signedWith7ZipApk 主要是对 signedApk 进行解压再做 sevenZip 压缩。
4、上传补丁包到平台
见证奇迹的时刻到了!!上传补丁包到平台并下发编辑规则,点击发布新补丁,上传前面生成的 patch 包,平台会自动为你匹配到目标版本,可以选择下发范围(开发设备、全量设备、自定义),填写完备注之后,点击立即下发让补丁生效,这样你就可以在客户端当中收到我们的策略,SDK会自动帮你把补丁包下到本地。
再次启动会发现停止运行,那是因为客户端收到策略需要下载补丁更新,最后的修复后效果:
好的,这下 Bugly 热更新我们就简单的看了下效果,其所应用的就是微信的 Tinker 方案,其实不难看出,Bugly 和阿里的 Sophix 都是针对补丁包的一种下发策略。
热更新技术的两大流派,一种就是阿里的 Native 流派,即 AndFix 和 Sophix,还有一种就是腾讯自己的 Qzone 超级补丁属于 java 流派,最后微信还是选择了继续走自己的 java 流派(自己的路就是要一走到黑!),但是微信并不是固守陈规,而是追求极致!这不得不提到前面说的 DexDiff 算法了:
DexDiff算法:
上面我们说了 dex 的加载过程,我们都知道 dex 文件是运行在 Dalvik 中的字节码文件,类似于运行于 JVM 中的 class 文件,在反编译的时候,apk 中会包含一个或者多个*.dex文件,该文件中存储了我们编写的代码,一般情况下我们还会通过工具转化为 jar,然后通过一些工具反编译查看(dex2jar)。
jar文件大家应该都清楚,类似于 class 文件的压缩包,一般情况下,我们直接解压就可以看到一个个 class 文件。而dex文件我们无法通过解压获取内部的 class 文件,那肯定是因为它的格式决定的,具体的格式我们不在这里分析,我们看一下 DexDiff 的基本步骤(细节分析源码时会讲到):
首先,明确有这么几个东西,bugdex,bugfixeddex,patchdex;
其次,计算出bugfixeddex中每一部分(指的是dex结构中的某一特定部分)占用的大小;
然后,比较bugdex和bugfixeddex的每一部分,对每一部分进行对比,并记录不同(删除了哪些,新增了哪些,记录和存储以什么形式我们暂时不管)。
最后,将保存的不同的记录写入补丁中
由上面可知 Tinker 中 Dex 的热更新主要分为三个部分: 一、补丁包的生成; 二、补丁包下发后生成全量 Dex; 三、生成全量Dex后的加载过程。
具体的Tinker是如何实现热更新的呢?源码出真知,我们下载tinker的源码来看看不就知道了嘛,毕竟是开源的嘛!
“tinker源码传送”
我下载的是目前最新的1.8.1版本。源码我们挑重点看,主要就找上面所说的三部分来看:
一、补丁包的生成;
我们上文在生成补丁的时候,调用了 tinker-support 中的 buildTinkerPatchRelease
当我们运行这个之后,
执行时间最长的当属 tinkerPatchRelease 的这个过程,com.tencent.tinker.build.patch.Runner 这个类就是我们在执行 buildTinkerPatchRelease 会执行的类,具体是执行类中的
tinkerPatch()方法:
protected void tinkerPatch() {
Logger.d("-----------------------Tinker patch begin-----------------------");
Logger.d(config.toString());
try {
//gen patch
ApkDecoder decoder = new ApkDecoder(config);
decoder.onAllPatchesStart();
decoder.patch(config.mOldApkFile, config.mNewApkFile);
decoder.onAllPatchesEnd();
//gen meta file and version file
PatchInfo info = new PatchInfo(config);
info.gen();
//build patch
PatchBuilder builder = new PatchBuilder(config);
builder.buildPatch();
} catch (Throwable e) {
e.printStackTrace();
goToError();
}
Logger.d("Tinker patch done, total time cost: %fs", diffTimeFromBegin());
Logger.d("Tinker patch done, you can go to file to find the output %s", config.mOutFolder);
Logger.d("-----------------------Tinker patch end-------------------------");
}
这个其实就是生成补丁的过程,其中调用 com.tencent.tinker.build.decoder.ApkDecoder 中 patch(File oldFile, File newFile) 方法:
public boolean patch(File oldFile, File newFile) throws Exception {
writeToLogFile(oldFile, newFile);
//check manifest change first
manifestDecoder.patch(oldFile, newFile);
unzipApkFiles(oldFile, newFile);
Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));
//get all duplicate resource file
for (File duplicateRes : resDuplicateFiles) {
// resPatchDecoder.patch(duplicateRes, null);
Logger.e("Warning: res file %s is also match at dex or library pattern, " + "we treat it as unchanged in the new resource_out.zip", getRelativePathStringToOldFile(duplicateRes));
}
soPatchDecoder.onAllPatchesEnd();
dexPatchDecoder.onAllPatchesEnd();
manifestDecoder.onAllPatchesEnd();
resPatchDecoder.onAllPatchesEnd();
//clean resources
dexPatchDecoder.clean();
soPatchDecoder.clean();
resPatchDecoder.clean();
return true;
}
从源码中我们可以看出首先是对 manifest 文件进行检测,看其是否有更改,如果发现 manifest 的组件有新增,则抛出异常,因为目前 Tinker 暂不支持四大组件的新增。
检测通过后解压 apk 文件,遍历新旧 apk,交给 ApkFilesVisitor 进行处理。
ApkFilesVisitor 的 visitFile 方法中,对于 dex 类型的文件,调用 dexDecoder 进行 patch 操作;我们主要是针对 dexDecoder 进行分析,所以省略 so 类型和 res 类型操作代码:
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path relativePath = newApkPath.relativize(file);
Path oldPath = oldApkPath.resolve(relativePath);
File oldFile = null;
//is a new file?!
if (oldPath.toFile().exists()) {
oldFile = oldPath.toFile();
}
String patternKey = relativePath.toString().replace("\\", "/");
if (Utils.checkFileInPattern(config.mDexFilePattern, patternKey)) {
//also treat duplicate file as unchanged
if (Utils.checkFileInPattern(config.mResFilePattern, patternKey) && oldFile != null) {
resDuplicateFiles.add(oldFile);
}
try {
dexDecoder.patch(oldFile, file.toFile());
} catch (Exception e) {
// e.printStackTrace();
throw new RuntimeException(e);
}
return FileVisitResult.CONTINUE;
}
if (Utils.checkFileInPattern(config.mSoFilePattern, patternKey)) {
//also treat duplicate file as unchanged
/*****省略so解析,对于so类型的文件,使用soDecoder进行patch操作**************/
}
if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) {
/*****省略so解析,对于Res类型文件,使用resDecoder进行操作patch操作**************/
}
return FileVisitResult.CONTINUE;
}
可以看出是调用 DexDiffDecoder.patch(final File oldFile, final File newFile) 方法,源码如下:
@SuppressWarnings("NewApi")
@Override
public boolean patch(final File oldFile, final File newFile) throws IOException, TinkerPatchException {
final String dexName = getRelativeDexName(oldFile, newFile);
/>>>>>>>>>>>>>>>>>>>>>>省略N行代码<<<<<<<<<<<<<<<<<<<<<</
try {
excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);
}
/>>>>>>>>>>>>>>>>>>>>>>省略N行代码<<<<<<<<<<<<<<<<<<<<<</
// If corresponding new dex was completely deleted, just return false.
// don't process 0 length dex
if (newFile == null || !newFile.exists() || newFile.length() == 0) {
return false;
}
File dexDiffOut = getOutputPath(newFile).toFile();
final String newMd5 = getRawOrWrappedDexMD5(newFile);
//new add file
if (oldFile == null || !oldFile.exists() || oldFile.length() == 0) {
hasDexChanged = true;
copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
return true;
}
/>>>>>>>>>>>>>>>>>>>>>>省略N行代码<<<<<<<<<<<<<<<<<<<<<</
RelatedInfo relatedInfo = new RelatedInfo();
relatedInfo.oldMd5 = oldMd5;
relatedInfo.newMd5 = newMd5;
// collect current old dex file and corresponding new dex file for further processing.
oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile, newFile));
dexNameToRelatedInfoMap.put(dexName, relatedInfo);
return true;
}
由源码可以看出是先检测输入的 dex 文件中是否有不允许修改的类被修改了,如 loader 相关的类是不允许被修改的,这种情况下会抛出异常;
如果 dex 是新增的,直接将该 dex 拷贝到结果文件;
如果 dex 是修改的,收集增加和删除的 class。oldAndNewDexFilePairList 将新旧 dex 对应关系保存起来,用于后面的分析。
单单只是将新的 dex 文件加入到 addedDexFiles。调用的是 UniqueDexDiffDecoder.patch:
@Override
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
boolean added = super.patch(oldFile, newFile);
if (added) {
String name = newFile.getName();
if (addedDexFiles.contains(name)) {
throw new TinkerPatchException("illegal dex name, dex name should be unique, dex:" + name);
} else {
addedDexFiles.add(name);
}
}
return added;
}
在 patch 完成后,会调用 generatePatchInfoFile 生成补丁文件。DexFiffDecoder.generatePatchInfoFile 中首先遍历 oldAndNewDexFilePairList,取出新旧文件对。
判断新旧文件的 MD5 是否相等,不相等,说明有变化,会根据新旧文件创建 DexPatchGenerator,DexPatchGenerator 构造函数中包含了 15 个 Dex 区域的比较算法:
private DexSectionDiffAlgorithm<StringData> stringDataSectionDiffAlg;
private DexSectionDiffAlgorithm<Integer> typeIdSectionDiffAlg;
private DexSectionDiffAlgorithm<ProtoId> protoIdSectionDiffAlg;
private DexSectionDiffAlgorithm<FieldId> fieldIdSectionDiffAlg;
private DexSectionDiffAlgorithm<MethodId> methodIdSectionDiffAlg;
private DexSectionDiffAlgorithm<ClassDef> classDefSectionDiffAlg;
private DexSectionDiffAlgorithm<TypeList> typeListSectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationSetRefList> annotationSetRefListSectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationSet> annotationSetSectionDiffAlg;
private DexSectionDiffAlgorithm<ClassData> classDataSectionDiffAlg;
private DexSectionDiffAlgorithm<Code> codeSectionDiffAlg;
private DexSectionDiffAlgorithm<DebugInfoItem> debugInfoSectionDiffAlg;
private DexSectionDiffAlgorithm<Annotation> annotationSectionDiffAlg;
private DexSectionDiffAlgorithm<EncodedValue> encodedArraySectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationsDirectory> annotationsDirectorySectionDiffAlg;
DexDiffDecoder.executeAndSaveTo(OutputStream out) 这个函数里面会根据上面的 15 个算法对 dex 的各个区域进行比较,每个算法代表每个区域,算法的目的就像我们之前描述 DexDiff 第3步的那样,要知道“删除了哪些,新增了哪些”,最后生成 dex 文件的差异。
这是整个 dex diff 算法的核心。以 StringDataSectionDiffAlgorithm 为例,算法流程如下:
每个算法都会执行 execute 和 simulatePatchOperation 方法:
/************省略N行代码*************/
this.stringDataSectionDiffAlg.execute();
this.patchedStringDataItemsOffset = patchedheaderSize + patchedIdSectionSize;
if (this.oldDex.getTableOfContents().stringDatas.isElementFourByteAligned) {
this.patchedStringDataItemsOffset
= SizeOf.roundToTimesOfFour(this.patchedStringDataItemsOffset);
}
this.stringDataSectionDiffAlg.simulatePatchOperation(this.patchedStringDataItemsOffset);
/************省略N行代码*************/
首先看 execute(代码比较长,因为是算法核心,不好省略,所以分两部分讲下,大家可以去源码中看com.tencent.tinker.build.dexpatcher.algorithms.diff.DexSectionDiffAlgorithm)
public void execute() {
this.patchOperationList.clear();
this.adjustedOldIndexedItemsWithOrigOrder = collectSectionItems(this.oldDex, true);
this.oldItemCount = this.adjustedOldIndexedItemsWithOrigOrder.length;
AbstractMap.SimpleEntry<Integer, T>[] adjustedOldIndexedItems = new AbstractMap.SimpleEntry[this.oldItemCount];
System.arraycopy(this.adjustedOldIndexedItemsWithOrigOrder, 0, adjustedOldIndexedItems, 0, this.oldItemCount);
Arrays.sort(adjustedOldIndexedItems, this.comparatorForItemDiff);
AbstractMap.SimpleEntry<Integer, T>[] adjustedNewIndexedItems = collectSectionItems(this.newDex, false);
this.newItemCount = adjustedNewIndexedItems.length;
Arrays.sort(adjustedNewIndexedItems, this.comparatorForItemDiff);
int oldCursor = 0;
int newCursor = 0;
while (oldCursor < this.oldItemCount || newCursor < this.newItemCount) {
if (oldCursor >= this.oldItemCount) {
// rest item are all newItem.
while (newCursor < this.newItemCount) {
AbstractMap.SimpleEntry<Integer, T> newIndexedItem = adjustedNewIndexedItems[newCursor++];
this.patchOperationList.add(new PatchOperation<>(PatchOperation.OP_ADD, newIndexedItem.getKey(), newIndexedItem.getValue()));
}
} else
if (newCursor >= newItemCount) {
// rest item are all oldItem.
while (oldCursor < oldItemCount) {
AbstractMap.SimpleEntry<Integer, T> oldIndexedItem = adjustedOldIndexedItems[oldCursor++];
int deletedIndex = oldIndexedItem.getKey();
int deletedOffset = getItemOffsetOrIndex(deletedIndex, oldIndexedItem.getValue());
this.patchOperationList.add(new PatchOperation<T>(PatchOperation.OP_DEL, deletedIndex));
markDeletedIndexOrOffset(this.oldToPatchedIndexMap, deletedIndex, deletedOffset);
}
} else {
AbstractMap.SimpleEntry<Integer, T> oldIndexedItem = adjustedOldIndexedItems[oldCursor];
AbstractMap.SimpleEntry<Integer, T> newIndexedItem = adjustedNewIndexedItems[newCursor];
int cmpRes = oldIndexedItem.getValue().compareTo(newIndexedItem.getValue());
if (cmpRes < 0) {
int deletedIndex = oldIndexedItem.getKey();
int deletedOffset = getItemOffsetOrIndex(deletedIndex, oldIndexedItem.getValue());
this.patchOperationList.add(new PatchOperation<T>(PatchOperation.OP_DEL, deletedIndex));
markDeletedIndexOrOffset(this.oldToPatchedIndexMap, deletedIndex, deletedOffset);
++oldCursor;
} else
if (cmpRes > 0) {
this.patchOperationList.add(new PatchOperation<>(PatchOperation.OP_ADD, newIndexedItem.getKey(), newIndexedItem.getValue()));
++newCursor;
} else {
int oldIndex = oldIndexedItem.getKey();
int newIndex = newIndexedItem.getKey();
int oldOffset = getItemOffsetOrIndex(oldIndexedItem.getKey(), oldIndexedItem.getValue());
int newOffset = getItemOffsetOrIndex(newIndexedItem.getKey(), newIndexedItem.getValue());
if (oldIndex != newIndex) {
this.oldIndexToNewIndexMap.put(oldIndex, newIndex);
}
if (oldOffset != newOffset) {
this.oldOffsetToNewOffsetMap.put(oldOffset, newOffset);
}
++oldCursor;
++newCursor;
}
}
/**********前半部分**********************/
}
分析:
首先读取 oldDex 和 newDex 对应区域的数据并排序,分别 adjustedOldIndexedItems 和 adjustedNewIndexedItems。
- 接下来就开始遍历了,分别根据当前的 cursor,获取 oldItem 和 newItem,对其 value 对对比:
如果 <0 ,则认为该 old Item 被删除了,记录为 PatchOperation.OP_DEL,并记录该 oldItem index 到 PatchOperation 对象,加入到 patchOperationList 中。
如果 >0 ,则认为该 newItem 是新增的,记录为 PatchOperation.OP_ADD,并记录该 newItem index 和 value 到 PatchOperation 对象,加入到 patchOperationList 中。
如果 =0 ,不会生成 PatchOperation。
- 经过上面的遍历操作,我们得到了一个 patchOperationList对象。
继续下半部分代码:
/*************后半部分**********************/
// So far all diff works are done. Then we perform some optimize works.
// detail: {OP_DEL idx} followed by {OP_ADD the_same_idx newItem}
// will be replaced by {OP_REPLACE idx newItem}
Collections.sort(this.patchOperationList, comparatorForPatchOperationOpt);
Iterator<PatchOperation<T>> patchOperationIt = this.patchOperationList.iterator();
PatchOperation<T> prevPatchOperation = null;
while (patchOperationIt.hasNext()) {
PatchOperation<T> patchOperation = patchOperationIt.next();
if (prevPatchOperation != null
&& prevPatchOperation.op == PatchOperation.OP_DEL
&& patchOperation.op == PatchOperation.OP_ADD
) {
if (prevPatchOperation.index == patchOperation.index) {
prevPatchOperation.op = PatchOperation.OP_REPLACE;
prevPatchOperation.newItem = patchOperation.newItem;
patchOperationIt.remove();
prevPatchOperation = null;
} else {
prevPatchOperation = patchOperation;
}
} else {
prevPatchOperation = patchOperation;
}
}
// Finally we record some information for the final calculations.
patchOperationIt = this.patchOperationList.iterator();
while (patchOperationIt.hasNext()) {
PatchOperation<T> patchOperation = patchOperationIt.next();
switch (patchOperation.op) {
case PatchOperation.OP_DEL: {
indexToDelOperationMap.put(patchOperation.index, patchOperation);
break;
}
case PatchOperation.OP_ADD: {
indexToAddOperationMap.put(patchOperation.index, patchOperation);
break;
}
case PatchOperation.OP_REPLACE: {
indexToReplaceOperationMap.put(patchOperation.index, patchOperation);
break;
}
}
}
}
分析:
首先对 patchOperationList 按照 index 排序,如果 index 一致则先 DEL(删除)、后ADD(添加)。
接下来一个对所有的 operation 的迭代,主要将 index 一致的,且连续的 DEL、ADD 转化为 REPLACE(替换)操作。
最后将 patchOperationList 转化为 3 个 Map,分别为: indexToDelOperationMap, indexToAddOperationMap, indexToReplaceOperationMap。
完成 execute 之后,我们主要的产物就是 3 个 Map,分别记录了: oldDex 中哪些 index 需要删除; newDex 中新增了哪些 item;哪些 item 需要替换为新 item。
这基本上就是DexDif算法的核心思想了( StringDataSectionDiffAlgorithm 举例,其他的一样分析);
刚才说了每个算法除了 execute() 还有个 simulatePatchOperation():
public void simulatePatchOperation(int baseOffset) {
boolean isNeedToMakeAlign = getTocSection(this.oldDex).isElementFourByteAligned;
int oldIndex = 0;
int patchedIndex = 0;
int patchedOffset = baseOffset;
while (oldIndex < this.oldItemCount || patchedIndex < this.newItemCount) {
if (this.indexToAddOperationMap.containsKey(patchedIndex)) {
PatchOperation<T> patchOperation = this.indexToAddOperationMap.get(patchedIndex);
if (isNeedToMakeAlign) {
patchedOffset = SizeOf.roundToTimesOfFour(patchedOffset);
}
T newItem = patchOperation.newItem;
int itemSize = getItemSize(newItem);
updateIndexOrOffset(this.newToPatchedIndexMap,0,getItemOffsetOrIndex(patchOperation.index, newItem),0,patchedOffset);
++patchedIndex;
patchedOffset += itemSize;
} else
if (this.indexToReplaceOperationMap.containsKey(patchedIndex)) {
PatchOperation<T> patchOperation = this.indexToReplaceOperationMap.get(patchedIndex);
/*******省略N代码***********/
++patchedIndex;
patchedOffset += itemSize;
} else
if (this.indexToDelOperationMap.containsKey(oldIndex)) {
++oldIndex;
} else
if (this.indexToReplaceOperationMap.containsKey(oldIndex)) {
++oldIndex;
} else
if (oldIndex < this.oldItemCount) {
/*******省略N代码***********/
++oldIndex;
++patchedIndex;
patchedOffset += itemSize;
}
}
this.patchedSectionSize = SizeOf.roundToTimesOfFour(patchedOffset - baseOffset);
}
首先是要遍历 oldIndex 与 newIndex,分别在 indexToAddOperationMap, indexToReplaceOperationMap, indexToDelOperationMap中查找。
这里关注一点最终的一个产物是 this.patchedSectionSize,由 patchedOffset-baseOffset 得到。
这里有几种情况会造成 patchedOffset += itemSize:
indexToAddOperationMap 中包含 patchIndex
indexToReplaceOperationMap 包含 patchIndex
不在 indexToDelOperationMap 与 indexToReplaceOperationMap 中的 oldDex.
这个 patchedSectionSize 其实对应 newDex 的这个区域的 size。所以,包含需要 ADD 的 Item,会被替代的 Item,以及 OLD ITEMS 中没有被删除和替代的 Item。
这三者相加即为 newDex 的 itemList。
到这里,StringDataSectionDiffAlgorithm 算法就执行完毕了。
经过这样的一个算法,我们得到了 PatchOperationList 和对应区域 sectionSize。那么执行完成所有的算法,应该会得到针对每个算法的 PatchOperationList,和每个区域的 sectionSize;每个区域的 sectionSize 实际上换算得到每个区域的 offset。
每个区域的算法,execute 和 simulatePatchOperation 代码都是复用的父类 com.tencent.tinker.build.dexpatcher.algorithms.diff.DexSectionDiffAlgorithm 的方法,所以其他的都差不多,可以自己查看。
接下来看执行完成所有的算法后的 writeResultToStream 方法:
private void writeResultToStream(OutputStream os) throws IOException {
DexDataBuffer buffer = new DexDataBuffer();
buffer.write(DexPatchFile.MAGIC);
buffer.writeShort(DexPatchFile.CURRENT_VERSION);
buffer.writeInt(this.patchedDexSize);
// we will return here to write firstChunkOffset later.
int posOfFirstChunkOffsetField = buffer.position();
buffer.writeInt(0);
buffer.writeInt(this.patchedStringIdsOffset);
buffer.writeInt(this.patchedTypeIdsOffset);
buffer.writeInt(this.patchedProtoIdsOffset);
/*****省略其他算法***********/
buffer.write(this.oldDex.computeSignature(false));
int firstChunkOffset = buffer.position();
buffer.position(posOfFirstChunkOffsetField);
buffer.writeInt(firstChunkOffset);
buffer.position(firstChunkOffset);
writePatchOperations(buffer, this.stringDataSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.typeIdSectionDiffAlg.getPatchOperationList());
writePatchOperations(buffer, this.typeListSectionDiffAlg.getPatchOperationList());
/*****省略其他算法***********/
byte[] bufferData = buffer.array();
os.write(bufferData);
os.flush();
}
首先写了 MAGIC,CURRENT_VERSION 主要用于检查该文件为合法的 tinker patch 文件。
然后写入 patchedDexSize,第四位写入的是数据区的 offset,可以看到先使用 0 站位,等所有的 map list 相关的 offset 书写结束,写入当前的位置。接下来写入所有的跟 maplist 各个区域相关的 offset(这里各个区域的排序不重要,读写一致即可)
然后执行每个算法写入对应区域的信息,最后生成 patch 文件其实就是对每个区域比较后将比较的结果写入 patch 文件中,文件格式写在 DexDataBuffer 中
生成的文件以 dex 结尾,但需要注意的是,它不是真正的 dex 文件,具体格式分析在 DexDataBuffer 中。
其中 writePatchOperations 方法就是写入的方法,我们还是只看 stringDataSectionDiffAlg 的:
private <T extends Comparable<T>> void writePatchOperations(
DexDataBuffer buffer, List<PatchOperation<T>> patchOperationList
) {
List<Integer> delOpIndexList = new ArrayList<>(patchOperationList.size());
List<Integer> addOpIndexList = new ArrayList<>(patchOperationList.size());
List<Integer> replaceOpIndexList = new ArrayList<>(patchOperationList.size());
List<T> newItemList = new ArrayList<>(patchOperationList.size());
for (PatchOperation<T> patchOperation : patchOperationList) {
switch (patchOperation.op) {
case PatchOperation.OP_DEL: {
delOpIndexList.add(patchOperation.index);
break;
}
case PatchOperation.OP_ADD: {
addOpIndexList.add(patchOperation.index);
newItemList.add(patchOperation.newItem);
break;
}
case PatchOperation.OP_REPLACE: {
replaceOpIndexList.add(patchOperation.index);
newItemList.add(patchOperation.newItem);
break;
}
}
}
buffer.writeUleb128(delOpIndexList.size());
int lastIndex = 0;
for (Integer index : delOpIndexList) {
buffer.writeSleb128(index - lastIndex);
lastIndex = index;
}
buffer.writeUleb128(addOpIndexList.size());
lastIndex = 0;
for (Integer index : addOpIndexList) {
buffer.writeSleb128(index - lastIndex);
lastIndex = index;
}
buffer.writeUleb128(replaceOpIndexList.size());
lastIndex = 0;
for (Integer index : replaceOpIndexList) {
buffer.writeSleb128(index - lastIndex);
lastIndex = index;
}
for (T newItem : newItemList) {
if (newItem instanceof StringData) {
buffer.writeStringData((StringData) newItem);
} else
/***********其他*******************/
}
}
从代码中我们可以看出我们的写入步骤:首先把 patchOperationList 转化为 3 个 OpIndexList,分别对应 DEL, ADD, REPLACE,以及将所有的 item 存入 newItemList。
然后依次写入:
del 操作的个数,每个 del 的 index
add 操作的个数,每个 add 的 index
replace 操作的个数,每个需要 replace 的 index
依次写入 newItemList.
最后来看看我们生成的 patch 是什么样子的:
首先包含几个字段,证明自己是 tinker patch
包含生成 newDex 各个区域的 offset,即可以将 newDex 划分了多个区域,定位到起点
包含 newDex 各个区域的 Item 的删除的索引( oldDex ),新增的索引和值,替换的索引和值
那么这么看,我们猜测 Patch 的逻辑时这样的:
首先根据各个区域的 offset,确定各个区域的起点
读取 oldDex 各个区域的 items,然后根据 patch 中去除掉 oldDex 中需要删除的和需要替换的 item,再加上新增的 item 和替换的 item 即可组成 newOld 该区域的 items。
所以,newDex 的某个区域的包含:
oldItems - del - replace + addItems + replaceItems
这样就完成了补丁包的生成过程,那么服务器在下发补丁之后如何合成全量的新 Dex 的呢?下面我们来分析第二部分:
二、补丁包下发后生成全量Dex;
如何合成全量的新Dex来运行
当 app 收到服务器下发的补丁后,会触发 DefaultPatchListener.onPatchReceived 事件,调用 TinkerPatchService.runPatchService 启动 patch 进程进行补丁 patch 工作。
UpgradePatch.tryPatch() 中会首先检查补丁的合法性,签名,以及是否安装过补丁,检查通过后会尝试 dex, so 以及 res 文件的 patch。
我们主要分析 DexDiffPatchInternal.tryRecoverDexFiles,讨论 dex 的 patch 过程。
tryRecoverDexFiles 调用 DexDiffPatchInternal.patchDexFile:
private static void patchDexFile(
ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
ShareDexDiffPatchInfo patchInfo, File patchedDexFile) throws IOException {
/**********省略N行代码 最终都会调用这个方法************/
new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);
}
最终通过 DexPatchApplier.executeAndSaveTo 进行执行及生产全量 dex。
public void executeAndSaveTo(File file) throws IOException {
OutputStream os = null;
try {
os = new BufferedOutputStream(new FileOutputStream(file));
executeAndSaveTo(os);
} finally {
if (os != null) {
try {
os.close();
} catch (Exception e) {
// ignored.
}
}
}
}
其实就是调用了 DexPatchApplier.executeAndSaveTo(os):
方法代码比较长,源码中也是分了三部分注释:
executeAndSaveTo(os) 三部分之第一部分
public void executeAndSaveTo(OutputStream out) throws IOException {
// Before executing, we should check if this patch can be applied to
// old dex we passed in.
byte[] oldDexSign = this.oldDex.computeSignature(false);
if (oldDexSign == null) {
throw new IOException("failed to compute old dex's signature.");
}
if (this.patchFile == null) {
throw new IllegalArgumentException("patch file is null.");
}
byte[] oldDexSignInPatchFile = this.patchFile.getOldDexSignature();
if (CompareUtils.uArrCompare(oldDexSign, oldDexSignInPatchFile) != 0) {
throw new IOException(
String.format(
"old dex signature mismatch! expected: %s, actual: %s",
Arrays.toString(oldDexSign),
Arrays.toString(oldDexSignInPatchFile)
)
);
}
// Firstly, set sections' offset after patched, sort according to their offset so that
// the dex lib of aosp can calculate section size.
TableOfContents patchedToc = this.patchedDex.getTableOfContents();
patchedToc.header.off = 0;
patchedToc.header.size = 1;
patchedToc.mapList.size = 1;
patchedToc.stringIds.off
= this.patchFile.getPatchedStringIdSectionOffset();
patchedToc.typeIds.off
= this.patchFile.getPatchedTypeIdSectionOffset();
patchedToc.typeLists.off
/*****省略其他算法过程************/
Arrays.sort(patchedToc.sections);
patchedToc.computeSizesFromOffsets();
// Firstly, set sections’ offset after patched, sort according to their offset so that
// the dex lib of aosp can calculate section size.
这里实际上,就是读取 patchFile 中记录的值给 patchedDex 的 TableOfContent 中各种 Section (大致对应 map list 中各个 map_list_item )赋值,即设定各个区域的偏移量。
然后就是排序,设置 byteCount 等字段信息。patchedDex 是最终合成的 dex。
executeAndSaveTo(os) 三部分之第二部分
// Secondly, run patch algorithms according to sections' dependencies.
this.stringDataSectionPatchAlg = new StringDataSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.typeIdSectionPatchAlg = new TypeIdSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
/***省略其他算法代码*****/
this.stringDataSectionPatchAlg.execute();
this.typeIdSectionPatchAlg.execute();
/***省略其他算法代码*****/
第二部分其实是将 15 种算法初始化了一遍,然后都去执行 execute()。我们依然是拿 stringDataSectionPatchAlg 来分析,其实还是调用的抽象父类 DexSectionPatchAlgorithm 中的 execute 方法:
public void execute() {
final int deletedItemCount = patchFile.getBuffer().readUleb128();
final int[] deletedIndices = readDeltaIndiciesOrOffsets(deletedItemCount);
final int addedItemCount = patchFile.getBuffer().readUleb128();
final int[] addedIndices = readDeltaIndiciesOrOffsets(addedItemCount);
final int replacedItemCount = patchFile.getBuffer().readUleb128();
final int[] replacedIndices = readDeltaIndiciesOrOffsets(replacedItemCount);
final TableOfContents.Section tocSec = getTocSection(this.oldDex);
Dex.Section oldSection = null;
int oldItemCount = 0;
if (tocSec.exists()) {
oldSection = this.oldDex.openSection(tocSec);
oldItemCount = tocSec.size;
}
// Now rest data are added and replaced items arranged in the order of
// added indices and replaced indices.
doFullPatch(
oldSection, oldItemCount, deletedIndices, addedIndices, replacedIndices
);
}
我们在写入的时候现在都被读取出来了,这里的算法和生成补丁的 DexDiff 是一个逆向的过程,每个区域的合并算法采用二路归并,在 old dex 的基础上对元素进行删除,增加,替换操作。:
del 操作的个数,每个 del 的 index,存储在一个 int[] deletedIndices 中;
add 操作的个数,每个 add 的 index,存储在一个 int[] addedIndices 中;
replace 操作的个数,每个需要replace的index,存储在一个 int[] replacedIndices 中;
接下来获取了 oldDex 中 oldItems 和 oldItemCount。然后带着这些参数执行方法 doFullPatch(oldSection, oldItemCount, deletedIndices, addedIndices, replacedIndices):
private void doFullPatch(
Dex.Section oldSection,
int oldItemCount,
int[] deletedIndices,
int[] addedIndices,
int[] replacedIndices
) {
int deletedItemCount = deletedIndices.length;
int addedItemCount = addedIndices.length;
int replacedItemCount = replacedIndices.length;
int newItemCount = oldItemCount + addedItemCount - deletedItemCount;
int deletedItemCounter = 0;
int addActionCursor = 0;
int replaceActionCursor = 0;
int oldIndex = 0;
int patchedIndex = 0;
while (oldIndex < oldItemCount || patchedIndex < newItemCount) {
if (addActionCursor < addedItemCount && addedIndices[addActionCursor] == patchedIndex) {
/****************第1部分******************/
T addedItem = nextItem(patchFile.getBuffer());
int patchedOffset = writePatchedItem(addedItem);
++addActionCursor;
++patchedIndex;
} else
if (replaceActionCursor < replacedItemCount && replacedIndices[replaceActionCursor] == patchedIndex) {
/****************第2部分 省略N行代码,和上一部分类似,后面会做具体分析******************/
int patchedOffset = writePatchedItem(addedItem);
} else
if (Arrays.binarySearch(deletedIndices, oldIndex) >= 0) {
/****************第3部分(1) 省略N行代码,和上一部分类似,后面会做具体分析******************/
int patchedOffset = writePatchedItem(addedItem);
} else
if (Arrays.binarySearch(replacedIndices, oldIndex) >= 0) {
/****************第3部分(2) 省略N行代码,和上一部分类似,后面会做具体分析******************/
int patchedOffset = writePatchedItem(addedItem);
} else
if (oldIndex < oldItemCount) {
/****************第4部分 省略N行代码,和上一部分类似,后面会做具体分析******************/
int patchedOffset = writePatchedItem(addedItem);
}
}
if (addActionCursor != addedItemCount || deletedItemCounter != deletedItemCount
|| replaceActionCursor != replacedItemCount
) {
throw new IllegalStateException(
/*************..String。。。。。。。。/
)
);
}
}
到此,生成 Dex 过程完成。
从源码中可以看出我们是向位于 patchedDex 的 stringData 区写数据,按照上面我们说的,应该要写入新增的、替换的的数据,而我们写入的过程:首先计算出 newItemCount = oldItemCount + addCount – delCount,然后开始遍历,遍历条件为 0~oldItemCount 或 0~newItemCount。
而在 patchIndex 从 0~newItemCount 之间都会写入对应的 Item。
Item 写入通过代码我们可以看到(第1、2、3(1)、3(2)、4部分),具体代码如下:
首先判断该 patchIndex 是否包含在 addIndices 中,如果包含则写入:
if (addActionCursor < addedItemCount && addedIndices[addActionCursor] == patchedIndex) {
T addedItem = nextItem(patchFile.getBuffer()); int patchedOffset = writePatchedItem(addedItem); ++addActionCursor; ++patchedIndex;
}
再者判断是否在 repalceIndices 中,如果包含则写入:
if (replaceActionCursor < replacedItemCount && replacedIndices[replaceActionCursor] == patchedIndex) {
T replacedItem = nextItem(patchFile.getBuffer()); int patchedOffset = writePatchedItem(replacedItem); ++replaceActionCursor; ++patchedIndex;
}
然后判断如果发现 oldIndex 被 delete 或者 replace,直接跳过:
if (Arrays.binarySearch(deletedIndices, oldIndex) >= 0) {
T skippedOldItem = nextItem(oldSection); // skip old item. markDeletedIndexOrOffset( oldToPatchedIndexMap, oldIndex, getItemOffsetOrIndex(oldIndex, skippedOldItem) ); ++oldIndex; ++deletedItemCounter;
} else
if (Arrays.binarySearch(replacedIndices, oldIndex) >= 0) {T skippedOldItem = nextItem(oldSection); // skip old item. markDeletedIndexOrOffset( oldToPatchedIndexMap, oldIndex, getItemOffsetOrIndex(oldIndex, skippedOldItem) ); ++oldIndex;
}
最后一个 index 指的就是,oldIndex 为非 delete 和 replace 的,也就是和 newDex 中 items 相同的部分。
if (oldIndex < oldItemCount) {
T oldItem = adjustItem(this.oldToPatchedIndexMap, nextItem(oldSection)); int patchedOffset = writePatchedItem(oldItem); updateIndexOrOffset( this.oldToPatchedIndexMap, oldIndex, getItemOffsetOrIndex(oldIndex, oldItem), patchedIndex, patchedOffset ); ++oldIndex; ++patchedIndex; }
上述 1 2 4三个部分即可组成完整的 newDex 的该区域。完成了 stringData 区域的 patch 算法。
其他的 14 种算法的 execute 代码是相同的(父抽象类),执行的操作类似,都会完成各个部分的 patch 算法。
当所有的区域都完成恢复后,那么剩下的就是 header 和 mapList 了,所以回到所有算法执行完成的地方,即 executeAndSaveTo(OutputStream out) 的第三部分:
executeAndSaveTo(os) 三部分之第三部分
public void executeAndSaveTo(OutputStream out) throws IOException {
/************省略this.stringDataSectionPatchAlg.execute()前的代码*********/
this.stringDataSectionPatchAlg.execute();
/******省略其他算法执行execute()******************/
// Thirdly, write header, mapList. Calculate and write patched dex's sign and checksum.
Dex.Section headerOut = this.patchedDex.openSection(patchedToc.header.off);
patchedToc.writeHeader(headerOut);
Dex.Section mapListOut = this.patchedDex.openSection(patchedToc.mapList.off);
patchedToc.writeMap(mapListOut);
this.patchedDex.writeHashes();
// Finally, write patched dex to file.
this.patchedDex.writeTo(out);
}
可以看到首先是定位到 header 区域,写 header 相关数据;定位到 mapList 区域,编写 mapList 相关数据。两者都完成的时候,需要编写 header 中比较特殊的两个字段:签名和 checkSum,因为这两个字段是依赖 mapList 的,所以必须在编写 mapList后。
这样就完成了完整的dex的生成,最后将内存中的所有数据写到文件中。
三、生成全量Dex后的加载过程
上述是完整 Dex 的生成过程,也是算法的核心所在,所以花了很长时间,下面就是我们生成完整 Dex 后的加载过程咯,这一部分主要是在这个包下:
TinkerApplication 通过反射的方式将实际的 app 业务隔离,这样可以在热更新的时候修改实际的 app 内容。
在 TinkerApplication 中的 onBaseContextAttached 中会通过反射调用 TinkerLoader 的 tryLoad 加载已经合成的 dex。
private static final String TINKER_LOADER_METHOD = "tryLoad";
private void loadTinker() {
//disable tinker, not need to install
if (tinkerFlags == TINKER_DISABLE) {
return;
}
tinkerResultIntent = new Intent();
try {
//reflect tinker loader, because loaderClass may be define by user!
Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());
Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);
Constructor<?> constructor = tinkerLoadClass.getConstructor();
tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);
} catch (Throwable e) {
//has exception, put exception error code
ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
}
}
下面是反射调用的 TinkerLoader 中的 tryLoad 方法:
@Override
public Intent tryLoad(TinkerApplication app) {
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
tryLoadPatchFilesInternal(app, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
其中 tryLoadPatchFilesInternal 是加载 Patch 文件的核心函数(代码比较多,大家看注释应该就可以明白每段是做什么的了):
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
final int tinkerFlag = app.getTinkerFlags();
if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
//tinkerFlag是否开启,否则不加载
Log.w(TAG, "tryLoadPatchFiles: tinker is disable, just return");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
return;
}
//tinker
File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
if (patchDirectoryFile == null) {
//tinker目录是否生成,没有则表示没有生成全量的dex,不需要重新加载
Log.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
//treat as not exist
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
return;
}
//tinker/patch.info
File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);
//check patch info file whether exist
if (!patchInfoFile.exists()) {
//tinker/patch.info是否存在,否则不加载
Log.w(TAG, "tryLoadPatchFiles:patch info not exist:" + patchInfoFile.getAbsolutePath());
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_NOT_EXIST);
return;
}
//old = 641e634c5b8f1649c75caf73794acbdf
//new = 2c150d8560334966952678930ba67fa8
File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectoryPath);
patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
if (patchInfo == null) {
//读取patch.info,读取失败则不加载
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
return;
}
String oldVersion = patchInfo.oldVersion;
String newVersion = patchInfo.newVersion;
String oatDex = patchInfo.oatDir;
if (oldVersion == null || newVersion == null || oatDex == null) {
//判断版本号是否为空,为空则不加载
//it is nice to clean patch
Log.w(TAG, "tryLoadPatchFiles:onPatchInfoCorrupted");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
return;
}
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OLD_VERSION, oldVersion);
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_NEW_VERSION, newVersion);
//tinker/patch.info/patch-641e634c
String patchVersionDirectory = patchDirectoryPath + "/" + patchName;
File patchVersionDirectoryFile = new File(patchVersionDirectory);
if (!patchVersionDirectoryFile.exists()) {
//判断patch version directory(//tinker/patch.info/patch-641e634c)是否存在
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
return;
}
//tinker/patch.info/patch-641e634c/patch-641e634c.apk
File patchVersionFile = new File(patchVersionDirectoryFile.getAbsolutePath(), SharePatchFileUtil.getPatchVersionFile(version));
if (!SharePatchFileUtil.isLegalFile(patchVersionFile)) {
//判断patchVersionDirectoryFile(//tinker/patch.info/patch-641e634c/patch-641e634c.apk)是否存在
Log.w(TAG, "tryLoadPatchFiles:onPatchVersionFileNotFound");
//we may delete patch info file
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_FILE_NOT_EXIST);
return;
}
ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);
int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
//checkTinkerPackage,(如tinkerId和oldTinkerId不能相等,否则不加载)
Log.w(TAG, "tryLoadPatchFiles:checkTinkerPackage");
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, returnCode);
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
return;
}
if (isEnabledForDex) {
//tinker/patch.info/patch-641e634c/dex
boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
if (!dexCheck) {
//检测dex的完整性,包括dex是否全部生产,是否对dex做了优化,优化后的文件是否存在(//tinker/patch.info/patch-641e634c/dex)
//file not found, do not load patch
Log.w(TAG, "tryLoadPatchFiles:dex check fail");
return;
}
}
/****省略对so res文件进行完整性检测***************/
final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
/***************************************/
//now we can load patch jar
if (isEnabledForDex) {
/********************划重点---TinkerDexLoader.loadTinkerJars********************/
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);
if (isSystemOTA) {
// update fingerprint after load success
patchInfo.fingerPrint = Build.FINGERPRINT;
patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
// reset to false
oatModeChanged = false;
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
return;
}
// update oat dir
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
}
if (!loadTinkerJars) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
return;
}
}
return;
}
其中 TinkerDexLoader.loadTinkerJars 是用来处理加载 dex 文件。
public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA) {
/*****省略部分代码****************/
PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
/***********省略N行代码,主要是生成一些合法文件列表,对dex文件进行优化**************/
// 加载Dex
SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
}
然后 SystemClassLoaderAdder.installDexes 根据安卓的版本对dex进行安装啦:
@SuppressLint("NewApi")
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
throws Throwable {
Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());
if (!files.isEmpty()) {
files = createSortedAdditionalPathEntries(files);
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
classLoader = AndroidNClassLoader.inject(loader, application);
}
//because in dalvik, if inner class is not the same classloader with it wrapper class.
//it won't fail at dex2opt
if (Build.VERSION.SDK_INT >= 23) {
V23.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 19) {
V19.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(classLoader, files, dexOptDir);
} else {
V4.install(classLoader, files, dexOptDir);
}
//install done
sPatchDexCount = files.size();
Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
if (!checkDexInstall(classLoader)) {
//reset patch dex
SystemClassLoaderAdder.uninstallPatchDex(classLoader);
throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
}
}
}
前面我们讲dex加载时说加载类一般使用的是 PathClassLoader 和 DexClassLoader,而 PathClassLoader 作为系统类和应用类的加载器。DexClassLoader 用来从.jar和.apk类型的文件内部加载classes.dex文件。
而 install 是怎么做的呢:
/**
*Installer for platform versions 23.
*/
private static final class V23 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
/* The patched class loader is expected to be a descendant of
*dalvik.system.BaseDexClassLoader. We modify its
*dalvik.system.DexPathList pathList field to append additional DEX
*file entries.
*/
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makePathElement", e);
throw e;
}
}
}
/*************省略makePathElements方法***************/
}
先获取 BaseDexClassLoader 的 dexPathList 对象,然后通过 dexPathList 的 makeDexElements 方法将我们要安装的 dex 转化成 Element[] 对象,最后将其和 dexPathList 的 dexElements 对象进行合并,就是新的 Element[] 对象,因为我们添加的 dex 都被放在 dexElements 数组的最前面,所以当通过 findClass 来查找这个类时,就是使用的我们最新的 dex 里面的类。不同版本里面的 DexPathList 等类的函数和字段都有一些变化,其他类似。
到此为止,dex的整个加载过程就结束了!
其他使用 Tinker 进行更新的,如 so 库的更新、library 的更新大家可以在源码中按照上面的 dex 加载过程看到。
热更新方案的对比
好了,上面我们也说了几种热更新的方案了,其他的热更新方案大家可以去搜索了解。
上面阿里给出了AndFix和HotFix以及Sophix的对比,现在我们就对时下的几种热更新方案进行对比,看看到底哪种好:
从对比中我们也能发现Sophix和Tinker作为两大巨头的最新热更新方案,都是比较厉害的,大家如果有需要的话可以去尝试下。
因为时间关系,实现自己的热更新方案还没有写完,暂时不放上来了,等我写完了会放上下一篇的链接的。谢谢大家的捧场支持!
本文参加第三期安卓巴士博文比赛:不做将死之蛙 安卓巴士博文大赛第三期为你加温!
题外话
马上公司的实习就要结束了,感觉时间过的飞快,自己也要好好准备找工作的事情了。巧的是 @权小阳 举办了三届博文比赛,刚好我实习也是在这三个月,每个月一篇博文也让我学到很多东西,我在这里要给Sunny点个 N 个赞,做事很用心,很认真,举办活动细节考虑的很到位,也很爱帮助别人!声音也很好听,人也很漂亮!嗯,就说这么多吧(捂脸)。
当然也认识了很多大神博主,@nanchen,@玖哥,@鸡排,@静心,@凯迪,,,等等。其他的就不一一艾特啦(貌似这么@是没用的),读大家的文章也让我学到了很多东西!
大家虽然都不认识,但全世界那么多人,我们能聚在巴士这个地方,一起学习成长(当然,还有那一起哈不完的牛!此处奸笑beginning,不得不吐槽下,为啥markdown不能添加表情(此处泪奔,,,)),也是一种莫大的缘分吧,希望大家以后的路都能越走越宽,越走越远!大家加油!