转载请标注来源:http://www.cnblogs.com/charles04/p/8471301.html
Andfix源码分析
0、目录
- 背景介绍
- 源码分析
- 方案评价
- 总结与思考
- 参考文献
1、背景介绍
热修复技术是移动端领域近年非常活跃的一项新技术,通过热修复技术可以在不发布应用市场版本,在用户无感知的情况下对线上Bug进行紧急修复。正所谓修复于千里之外,剿灭与无形之中,实乃移动端开发运营中一项必备之尖端技术。其主要的运行原理如下:
简而言之,热修复就是通过一定的技术手段,让用户在程序的实际运行操作中,走到修复的Patch逻辑序列,而绕开存在问题的逻辑片段,实现问题的紧急规避。目前实现的技术手段主要有腾讯系的基于ClassLoader的热修复方案(例如微信的Tinker,qq空间的超级补丁)以及阿里系的基于Method Hook的热修复方案(例如Andfix,Sophix等)。今天主要介绍的就是阿里巴巴的Andfix。
2、源码分析
如前所述,Andfix是阿里巴巴推出的一款基于Method Hook的热修复技术,目前Github点赞数5.7K,是一款安全性高,较为稳定,性能比较优异的方法级替换的热修复技术。代码实现上条理清晰,架构设计合理,可读性强,是一个实现上非常优雅的开源框架。下面我们重点介绍下Andfix的源码及其设计。
一个经典的开源框架首先要友好的对外暴露接口,这样才能更便于接入,实现快速启动。所以,在介绍核心源码之前,我们首先关注下Andfix的外部接口部分。
2.1. 初始化部分
为了尽可能的覆盖BUG修复的范围,和其他的热修复技术一样,Andfix选择在APP启动的时候对热补丁进行加载,也即Application的OnCreate过程。整体的外部接口调用如下所示:
1 @Override
2 public void onCreate() {
3 super.onCreate();
4 // patch的初始化
5 mPatchManager = new PatchManager(this);
6 mPatchManager.init("1.0");
7 Log.d(TAG, "inited.");
8
9 // 加载缓存中的patch
10 mPatchManager.loadPatch();
11 Log.d(TAG, "apatch loaded.");
12
13 // 将外部存储中的patch加载到当前运行的ART中
14 try {
15 // .apatch file path
16 String patchFileString = Environment.getExternalStorageDirectory()
17 .getAbsolutePath() + APATCH_PATH;
18 mPatchManager.addPatch(patchFileString);
19 Log.d(TAG, "apatch:" + patchFileString + " added.");
20 } catch (IOException e) {
21 Log.e(TAG, "", e);
22 }
23 }
这部分接口非常简洁,大概分为三步:patch的初始化,patch的缓存加载,patch的外部存储加载。
缓存加载是为了加载之前已经从外部存储载入到缓存(data目录下)中的patch,外部存储加载是为了从外部存储中加载patch到缓存。Andfix的整体外部调用就是上面的几步,下面我们来看下Andfix的具体实现部分。
2.2. 核心实现
Andfix的具体实现上主要分为三部分:Patch管理部分,Fix管理部分,Native Hook部分,其整体的UML架构图如下所示:
//todo 增加整体UML
2.2.1. PatchManager
整体的初始化函数的源码如下:
1 /**
2 * Patch的初始化工作
3 * @param appVersion App的版本号
4 */
5 public void init(String appVersion) {
6 if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
7 Log.e(TAG, "patch dir create error.");
8 return;
9 } else if (!mPatchDir.isDirectory()) {// not directory
10 mPatchDir.delete();
11 return;
12 }
13 SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
14 Context.MODE_PRIVATE);
15 String ver = sp.getString(SP_VERSION, null);
16 if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
17 cleanPatch();
18 sp.edit().putString(SP_VERSION, appVersion).commit();
19 } else {
20 initPatchs();
21 }
22 }
其中mPatchDir表示data私有目录下存放Patch文件的文件夹。首先是关于mPatchDir的简单文件夹操作,在mPatchDir文件夹初始化完成之后,紧接着比较当前的APP版本和SharedPreferences中保存的Patch对应的APP版本,两者如果不相等的话,会直接清除掉本地缓存的Patch文件和对应Patch相关的数据。这是因为热补丁是跟APP强相关的,Patch只能精确的修复对应版本的Bug。清除的源码如下所示:
1 private void cleanPatch() {
2 File[] files = mPatchDir.listFiles();
3 for (File file : files) {
4 mAndFixManager.removeOptFile(file);
5 if (!FileUtil.deleteFile(file)) {
6 Log.e(TAG, file.getName() + " delete error.");
7 }
8 }
9 }
在版本号匹配之后,紧接着是Patch文件的初始化部分(initPatchs()),其源码如下:
1 private void initPatchs() {
2 File[] files = mPatchDir.listFiles();
3 for (File file : files) {
4 addPatch(file);
5 }
6 }
在上述函数中,ART会遍历Patch文件,并将Patch文件通过addPatch方法添加到内存中。
addPatch方法有两种多态实现,分别如下:
- private Patch addPatch(File file)
- public void addPatch(String path) throws IOException
其中第一个方法是从Patch文件中获取Patch对象,具体的源码如下:
1 /**
2 * add patch file
3 *
4 * @param file
5 * @return patch
6 */
7 private Patch addPatch(File file) {
8 Patch patch = null;
9 if (file.getName().endsWith(SUFFIX)) {
10 try {
11 patch = new Patch(file);
12 mPatchs.add(patch);
13 } catch (IOException e) {
14 Log.e(TAG, "addPatch", e);
15 }
16 }
17 return patch;
18 }
此方法中把Patch文件夹映射为Patch对象,然后将Patch对象统一存放在mPatchs数据集里面。
第二个方法是从本地路径中获取Patch文件,然后从Patch文件中解析出Patch对象,之后触发Patch的加载过程,具体源码如下:
1 public void addPatch(String path) throws IOException {
2 File src = new File(path);
3 File dest = new File(mPatchDir, src.getName());
4 if(!src.exists()){
5 throw new FileNotFoundException(path);
6 }
7 if (dest.exists()) {
8 Log.d(TAG, "patch [" + path + "] has be loaded.");
9 return;
10 }
11 FileUtil.copyFile(src, dest);// copy to patch's directory
12 Patch patch = addPatch(dest);
13 if (patch != null) {
14 loadPatch(patch);
15 }
16 }
获取完Patch的对象列表之后,接下来的内容就是加载Patch中的内容,并根据Patch中的内容进行Hotfix。此过程是通过Patchmanager类中的loadPatch方法实现的。loadPatch方法一共有三个多态,分别如下:
- public void loadPatch(String patchName, ClassLoader classLoader)
- public void loadPatch()
- private void loadPatch(Patch patch)
三个方法入参不同,会通过不同的ClassLoader加载不同的Patch文件,已第三个方法为例,该函数中对数据进行封装之后,最终会循环调用AndfixManager中的fix方法,具体的源码如下:
1 private void loadPatch(Patch patch) {
2 Set<String> patchNames = patch.getPatchNames();
3 ClassLoader cl;
4 List<String> classes;
5 for (String patchName : patchNames) {
6 if (mLoaders.containsKey("*")) {
7 cl = mContext.getClassLoader();
8 } else {
9 cl = mLoaders.get(patchName);
10 }
11 if (cl != null) {
12 classes = patch.getClasses(patchName);
13 mAndFixManager.fix(patch.getFile(), cl, classes);
14 }
15 }
16 }
PatchManager的源码基本就如上所述,主要是对Patch的管理与加载过程,代码简洁易懂,可读性强。
2.2.2. AndFixManager
接下来,我们重点分析下AndfixManager类,该类中主要介绍Andfix的BugFix的核心流程。通过之前的PatchManager类的源码分析可知,AndfixManager的关键入口函数为fix方法。其源码如下所示:
1 public synchronized void fix(File file, ClassLoader classLoader, List<String> classes) { 2 if (!mSupport) { 3 return; 4 } 5 6 if (!mSecurityChecker.verifyApk(file)) {// security check fail 7 return; 8 } 9 10 try { 11 File optfile = new File(mOptDir, file.getName()); 12 boolean saveFingerprint = true; 13 if (optfile.exists()) { 14 // need to verify fingerprint when the optimize file exist, 15 // prevent someone attack on jailbreak device with 16 // Vulnerability-Parasyte. 17 // btw:exaggerated android Vulnerability-Parasyte 18 // http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html 19 if (mSecurityChecker.verifyOpt(optfile)) { 20 saveFingerprint = false; 21 } else if (!optfile.delete()) { 22 return; 23 } 24 } 25 26 final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(), 27 optfile.getAbsolutePath(), Context.MODE_PRIVATE); 28 29 if (saveFingerprint) { 30 mSecurityChecker.saveOptSig(optfile); 31 } 32 33 ClassLoader patchClassLoader = new ClassLoader(classLoader) { 34 @Override 35 protected Class<?> findClass(String className) 36 throws ClassNotFoundException { 37 Class<?> clazz = dexFile.loadClass(className, this); 38 if (clazz == null 39 && className.startsWith("com.alipay.euler.andfix")) { 40 return Class.forName(className);// annotation’s class 41 // not found 42 } 43 if (clazz == null) { 44 throw new ClassNotFoundException(className); 45 } 46 return clazz; 47 } 48 }; 49 Enumeration<String> entrys = dexFile.entries(); 50 Class<?> clazz = null; 51 while (entrys.hasMoreElements()) { 52 String entry = entrys.nextElement(); 53 if (classes != null && !classes.contains(entry)) { 54 continue;// skip, not need fix 55 } 56 clazz = dexFile.loadClass(entry, patchClassLoader); 57 if (clazz != null) { 58 fixClass(clazz, classLoader); 59 } 60 } 61 } catch (IOException e) { 62 Log.e(TAG, "pacth", e); 63 } 64 }
在此方法中,主要包括安全校验,bugFix两部分,具体如下;
(1)安全校验
Andfix会对传进来的Patch文件进行安全校验,包括准确性校验和完整性校验。
安全校验的具体实现在SecurityChecker类中,其结构体如下:
//todo 补充SecurityChecker UML
其中准确性校验(签名校验)的具体实现如下:
1 /** 2 * @param path 3 * Apk file 4 * @return true if verify apk success 5 */ 6 public boolean verifyApk(File path) { 7 if (mDebuggable) { 8 Log.d(TAG, "mDebuggable = true"); 9 return true; 10 } 11 12 JarFile jarFile = null; 13 try { 14 jarFile = new JarFile(path); 15 16 JarEntry jarEntry = jarFile.getJarEntry(CLASSES_DEX); 17 if (null == jarEntry) {// no code 18 return false; 19 } 20 loadDigestes(jarFile, jarEntry); 21 Certificate[] certs = jarEntry.getCertificates(); 22 if (certs == null) { 23 return false; 24 } 25 return check(path, certs); 26 } catch (IOException e) { 27 Log.e(TAG, path.getAbsolutePath(), e); 28 return false; 29 } finally { 30 try { 31 if (jarFile != null) { 32 jarFile.close(); 33 } 34 } catch (IOException e) { 35 Log.e(TAG, path.getAbsolutePath(), e); 36 } 37 } 38 } 39 40 // verify the signature of the Apk 41 private boolean check(File path, Certificate[] certs) { 42 if (certs.length > 0) { 43 for (int i = certs.length - 1; i >= 0; i--) { 44 try { 45 certs[i].verify(mPublicKey); 46 return true; 47 } catch (Exception e) { 48 Log.e(TAG, path.getAbsolutePath(), e); 49 } 50 } 51 } 52 return false; 53 }
上述过程对APK进行证书签名校验,符合签名的APK为合法的APK,否则为非法的APK,中断热修复过程。
Andfix的过程不仅进行签名校验,还进行完整性校验。完整性校验是为了防止出现在进行patch下载的过程中下载不完整,导致修复出现异常的情况。完整性校验是通过校验MD4来实现的,具体如下;
1 /** 2 * @param path 3 * Dex file 4 * @return true if verify fingerprint success 5 */ 6 public boolean verifyOpt(File file) { 7 String fingerprint = getFileMD5(file); 8 String saved = getFingerprint(file.getName()); 9 if (fingerprint != null && TextUtils.equals(fingerprint, saved)) { 10 return true; 11 } 12 return false; 13 }
(2)Bug Fix
Andfix热修复的核心实现中,分为两个步骤:
- 找到需要修复的Class;
- 替换需要进行修复的Method。
第一步的具体实现如下:
1 /** 2 * fix class 3 * 4 * @param clazz 5 * class 6 */ 7 private void fixClass(Class<?> clazz, ClassLoader classLoader) { 8 Method[] methods = clazz.getDeclaredMethods(); 9 MethodReplace methodReplace; 10 String clz; 11 String meth; 12 for (Method method : methods) { 13 methodReplace = method.getAnnotation(MethodReplace.class); 14 if (methodReplace == null) 15 continue; 16 clz = methodReplace.clazz(); 17 meth = methodReplace.method(); 18 if (!isEmpty(clz) && !isEmpty(meth)) { 19 replaceMethod(classLoader, clz, meth, method); 20 } 21 } 22 }
第二部的具体实现如下:
1 /** 2 * replace method 3 * 4 * @param classLoader classloader 5 * @param clz class 6 * @param meth name of target method 7 * @param method source method 8 */ 9 private void replaceMethod(ClassLoader classLoader, String clz, 10 String meth, Method method) { 11 try { 12 String key = clz + "@" + classLoader.toString(); 13 Class<?> clazz = mFixedClass.get(key); 14 if (clazz == null) {// class not load 15 Class<?> clzz = classLoader.loadClass(clz); 16 // initialize target class 17 clazz = AndFix.initTargetClass(clzz); 18 } 19 if (clazz != null) {// initialize class OK 20 mFixedClass.put(key, clazz); 21 Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes()); 22 AndFix.addReplaceMethod(src, method); 23 } 24 } catch (Exception e) { 25 Log.e(TAG, "replaceMethod", e); 26 } 27 }
其中核心函数AndFix.addReplaceMethod(src, method)的具体实现如下:
1 /** 2 * replace method's body 3 * 4 * @param src 5 * source method 6 * @param dest 7 * target method 8 * 9 */ 10 public static void addReplaceMethod(Method src, Method dest) { 11 try { 12 replaceMethod(src, dest); 13 initFields(dest.getDeclaringClass()); 14 } catch (Throwable e) { 15 Log.e(TAG, "addReplaceMethod", e); 16 } 17 }
可以观察到,Andfix中函数的替换是通过Native方法replaceMethod(Method dest, Method src)实现的。从JNI中找到这部分的源码如下:
1 static void replaceMethod(JNIEnv* env, jclass clazz, jobject src, jobject dest) { 2 if (isArt) { 3 art_replaceMethod(env, src, dest); 4 } else { 5 dalvik_replaceMethod(env, src, dest); 6 } 7 }
Native层面进行Method Hook的原理是将源方法中的各个属性替换为目标方法的属性。由于不同虚拟机,甚至同样虚拟机下不同API对应的方法结构体的不同,在进行Method Hook的过程中,对不同情况,要适配不同的方法。
不同的Android版本,对于的虚拟机不同:Android 4.4以下用的是Dalvik虚拟机,而Android 4.4以上用的是ART(Android Running Time)虚拟机。如上面代码实现,在进行热修复的过程中,ART虚拟机下调用的是art_replaceMethod(env, src, dest)方法;Dalvik虚拟机调用的是dalvik_replaceMethod(env, src, dest)方法。
而对于ART虚拟机,不同Android API的系统,可能会对应不同的方法结构体(ArtMethod),所以会有对应的不同的适配实现,其代码如下:
1 extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod( 2 JNIEnv* env, jobject src, jobject dest) { 3 if (apilevel > 23) { 4 replace_7_0(env, src, dest); 5 } else if (apilevel > 22) { 6 replace_6_0(env, src, dest); 7 } else if (apilevel > 21) { 8 replace_5_1(env, src, dest); 9 } else if (apilevel > 19) { 10 replace_5_0(env, src, dest); 11 }else{ 12 replace_4_4(env, src, dest); 13 } 14 }
不同API的实现类如下:
所以说Andfix可以兼容Android2.3到7.0版本,对于超过Android7.0的版本,如果ArtMethod相比较7.0有较大的改变,就可能存在兼容性问题,这是后话。
以7.0版本为例,Andfix中Method hook的属性替换的具体实现如下:
1 void replace_7_0(JNIEnv* env, jobject src, jobject dest) { 2 art::mirror::ArtMethod* smeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(src); 3 4 art::mirror::ArtMethod* dmeth = 5 (art::mirror::ArtMethod*) env->FromReflectedMethod(dest); 6 7 // reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_ = 8 // reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_; //for plugin classloader 9 reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ = 10 reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_; 11 reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ = 12 reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_ -1; 13 //for reflection invoke 14 reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0; 15 16 smeth->declaring_class_ = dmeth->declaring_class_; 17 smeth->access_flags_ = dmeth->access_flags_ | 0x0001; 18 smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_; 19 smeth->dex_method_index_ = dmeth->dex_method_index_; 20 smeth->method_index_ = dmeth->method_index_; 21 smeth->hotness_count_ = dmeth->hotness_count_; 22 23 smeth->ptr_sized_fields_.dex_cache_resolved_methods_ = 24 dmeth->ptr_sized_fields_.dex_cache_resolved_methods_; 25 smeth->ptr_sized_fields_.dex_cache_resolved_types_ = 26 dmeth->ptr_sized_fields_.dex_cache_resolved_types_; 27 28 smeth->ptr_sized_fields_.entry_point_from_jni_ = 29 dmeth->ptr_sized_fields_.entry_point_from_jni_; 30 smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ = 31 dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_; 32 33 LOGD("replace_7_0: %d , %d", 34 smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_, 35 dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_); 36 37 }
首先调用ART的方法获取Andfix中源方法(smeth)和目标方法(dmeth)的句柄,然后将源方法的各个属性(例如declaring_class_:所属类,access_flags:访问权限,method_index_:代码执行地址等)替换为目标方法的各个属性,从而实现方法层面的Hook,实现Hotfix。
3.方案评价
3.1.优点
(2)基于Method Hook的实现,对原始APK侵入较小,性能影响几乎忽略不计
3.2.缺点
(1)只能用于方法级的修复
Andfix最为明显的缺点是只能实现方法级别的修复。而无法实现xml,资源文件级别的修复,也无法增加或者删除class类,这一点从原理分析上能够很明显的看到。但是,热修复的精髓就是在不重新发布版本,不影响性能和体验的前提下,实现对线上紧急Bug的灵活修复。在大多数情况下,通过方法级别的修复就能够达到热修复的目的,Andfix做到了小而精,改动小,影响小但性能优异,效果稳定,个人认为在一定程度上已经满足了热修复的需求。与Andfix形成鲜明对比的是微信推出的Tinker,Tinker追求的是广而博,能够实现类,xml,资源文件,so库等的修复,甚至可以新增export属性为false的Activity类,从某种意义上讲,甚至可以小型功能的发布,有点插件化的味道。
这里不过多评价两种插件化框架的优劣,和谈恋爱一样,没有最好的,只有最合适的,选择适合自己项目的热修复框架,然后用好,就可以了。
(2)兼容性问题
由于Java方法对应的底层数据结构体的差异,在进行native层面的Method Hook过程中,不同虚拟机之间要使用不同的方法,甚至在ART架构中,不同的API的Android版本间也可能要使用不同的适配方法。
目前Andfix在实现的时候,根据AOSP开源代码中不同API版本对ArtMethod的定义,将运行的Java Method强行地转换为art::mirror::ArtMethod,但是由于Android源码是公开的,在实际的设备上,不同的手机厂商可能会对ArtMethod做个性化修改,这样就有可能会导致基于开源标准代码实现的Method Hook无法兼容有些设备的情况。
为了解决Andfix的兼容性问题,阿里巴巴随后推出了Andfix的改进版热修复方案——Sophix。Sophix与Andfix的区别在于,在进行Method Hook的时候,不再进行ArtMethod属性的替换,而是直接将ArtMethod作为一个整体进行替换, 其Method Hook的核心实现如下:
- memcpy(dmeth, smeth, sizeof(ArtMethod));
Sophix通过进行整体方法体的替换,完美的解决了Andfix中的兼容性问题,这样,不仅在不能的厂商的设备上可以达到兼容,而且对于后续发布的Android版本也能够做到向后兼容,保障了热修复方案的健壮性。
4.总结与思考
本文对Andfix的原理进行了分析介绍,并对Andfix客户端的源码实现进行了简要分析,其中重点介绍了客户端在获取Patch后进行Class匹配与Method替换的过程。
初次此外,在开发过程中,有几个技术细节也有较大的可挖掘性,具体如下:
(1)Andfix中热修复Patch的生成原理;
(2)Patch的下载流程(推荐自己搭建服务器框架,通过okhttp实现下载流程),更新,版本管理;
(3)MultiDex下的Andfix;
(4)ClassLoader的内核原理;
(5)Android Running Time与Dalvik;
(6)其他同类型的热修复框架,例如腾讯微信的Tinker,美团的Robust,饿了么的MiGo,大众点评的Nuwa等。