zoukankan      html  css  js  c++  java
  • 【热修复】Andfix源码分析

    转载请标注来源:http://www.cnblogs.com/charles04/p/8471301.html


     Andfix源码分析

    0、目录

    1. 背景介绍
    2. 源码分析
    3. 方案评价
    4. 总结与思考
    5. 参考文献

    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热修复的核心实现中,分为两个步骤:

    1. 找到需要修复的Class;
    2. 替换需要进行修复的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.优点

    (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等。

    5.参考文献

  • 相关阅读:
    Asp.Net WebService 使用后来管理系统对接口方法进行公开控制
    ASP.NET使用NPOI加载Excel模板并导出下载
    VS2010 根据模型生成数据库 打开edmx.sql文件时 vs出现无响应的解决方案
    ASP.NET中Session简单原理图
    三层架构学习总结图
    备忘录
    帶編號漏洞列表
    pwn with glibc heap(堆利用手册)
    基于qemu和unicorn的Fuzz技术分析
    winafl 源码分析
  • 原文地址:https://www.cnblogs.com/charles04/p/8471301.html
Copyright © 2011-2022 走看看