[关闭]
@whosea 2017-12-18T06:12:34.000000Z 字数 5528 阅读 824

Andfix热修复原理和流程分析

Android热修复


原文链接:https://www.zybuluo.com/whosea/note/929832

Andfix是阿里开源的热修复框架,也是Native热修复里面比较有代表性的方案。实现简洁直接,应用patch不需要重启。但由于从实现上直接跳过了类初始化,设置为初始化完毕,所以像是静态函数、静态成员、构造函数都会出现问题,复杂点的类Class.forname很可能直接就会挂掉。但是可做为我们分析它实现的思路,为自己的技术做储备。

Andfix最核心的部分就是方法的替换,把原出Bug的方法A替换为修复后的方法B,从而达到线上修复的目的。
因此,为了更好的了解Andfix实现原理,我们干脆直接模拟Andfix做个热修复的demo,尽量把涉及到的知识点都描述清楚,好让各位看官看得愉悦。

步骤一:找出产生错误的类,新建一个包来存放修复好的类,然后针对新增的类重新生成dex,最后把dex上传到服务器,app下载到sd卡后加载(这里下载省略,方便测试,直接放到手机sd卡里面)。

1.先修复问题(情景:线上app出现问题或者有一天我突然发现线上app猫的叫声太粗狂了,想立马改成其他低调的叫声...)。

发生错误的类或者想改的类叫Cat类,它放在com.andfixjni.demo包里。

  1. //原错误的类
  2. public class Cat {
  3. public int call(){
  4. //随便弄个错误
  5. int i=0;
  6. int j = 100;
  7. Log.e("Cat","汪汪汪");
  8. int result = j/i;
  9. return result;
  10. }
  11. }

把错误的类修复,然后放在com.andfixjni.demo.ok包里。

  1. public class Cat {
  2. public int call(){
  3. int i=1;
  4. int j = 100;
  5. Log.e("Cat","妙妙妙");
  6. int result = j/i;
  7. return result;
  8. }
  9. }

2.如何生成修复dex
编译后,可以在build\intermediates\classes找到修复好的类,复制com目录到项目下新建目录的andfix_repair里,只保留com\alipay\euler\andfix\ok目录,其他文件都删除。
找到Sdk\build-tools\26.0.0\dx.bat文件(比如我的路径是E:\Android\Sdk\build-tools\26.0.0\dx.bat),打开cmd,先cd到dx.bat的目录,然后执行这语句:
dx --dex --output E:\github\AndFix-master\andfix_repair\out.dex E:\github\AndFix-master\andfix_repair

dex参数是打包为dex文件
output参数设置输出路径,这里把dex文件存放到andfix_repair目录下
最后设置要打包class文件的路径

步骤二:加载dex文件,找到修复好的类,接着找回出bug的类,替换掉其中出bug的方法。

1.新增DxManager类,实现加载dex文件和找出要修复的类

  1. public void loadDex(File dexFilePath){
  2. File optFile = new File(context.getCacheDir(),dexFilePath.getName());
  3. if(optFile.exists()){
  4. optFile.delete();
  5. }
  6. //加载Dex文件
  7. try {
  8. DexFile dexFile = DexFile.loadDex(dexFilePath.getAbsolutePath(),optFile.getAbsolutePath(),Context.MODE_PRIVATE);
  9. //遍历dex的class文件
  10. Enumeration<String> entry = dexFile.entries();
  11. Class<?> clazz = null;
  12. while (entry.hasMoreElements()){
  13. String className = entry.nextElement();
  14. //修复好的类
  15. clazz = dexFile.loadClass(className, context.getClassLoader());
  16. if (clazz != null) {
  17. fixClass(clazz);
  18. }
  19. }
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. }
  23. }

2.如何找出错误的类?从andfix源码中发现主要使用了注解的方式。

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface MethodReplace {
  4. String clazz();
  5. String method();
  6. }

其中RetentionPolicy.RUNTIME用在运行时,ElementType.METHOD用在方法上面。

然后修改前面用到的修复Cat类(ok包下的Cat类)

  1. //要修复的地方class名+要修复的方法
  2. @MethodReplace(clazz = "com.andfixjni.demo.Cat",method = "call")
  3. public int call(){...}

最后通过注解方式来获取到要修复的类和方法

  1. private void fixClass(Class<?> clazz) {
  2. Method[] methods = clazz.getDeclaredMethods();
  3. for (Method method : methods) {
  4. //拿到要修复的注解
  5. MethodReplace methodReplace = method.getAnnotation(MethodReplace.class);
  6. if (methodReplace == null)
  7. continue;
  8. String wrongClazzName = methodReplace.clazz();
  9. String wrongMethodName = methodReplace.method();
  10. if (!isEmpty(wrongClazzName) && !isEmpty(wrongMethodName)) {
  11. replaceMethod(wrongClazzName, wrongMethodName, method);
  12. }
  13. }
  14. }
  15. private void replaceMethod(String wrongClazzName, String    wrongMethodName, Method rightMethod) {
  16. try {
  17. Class wrongClass = Class.forName(wrongClazzName);
  18. //最终拿到错误的method对象
  19. Method wrongMethod = wrongClass.getMethod(wrongMethodName,rightMethod.getParameterTypes());
  20. //走jni层去进行方法替换修复
  21. replaceMethod(wrongMethod,rightMethod);
  22. } catch (Exception e) {
  23. Log.e(TAG, "replaceMethod", e);
  24. }
  25. }

步骤三:调用jni层来进行方法替换
1.把前面获取到的错误的方法和正确的方法作为参数传给jni层,首先先在DxManager类里面初始化库文件和添加jni方法。

  1. static {
  2. //加载库文件
  3. System.loadLibrary("andfix");
  4. }
  5. public native void replaceMethod(Method wrongMethodName, Method rightMethod);

以下是C语言代码(文件是andfix.cpp)

  1. JNIEXPORT void JNICALL Java_com_andfixjni_demo_DxManager_replaceMethod
  2. (JNIEnv *env, jobject /* this */, jobject src, jobject dest){
  3. //拿到错误的class
  4. art::mirror::ArtMethod* smeth =
  5. (art::mirror::ArtMethod*) env->FromReflectedMethod(src);
  6. art::mirror::ArtMethod* dmeth =
  7. (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
  8. reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_ =
  9. reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_; //for plugin classloader
  10. reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
  11. reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
  12. reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ = 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. smeth->declaring_class_ = dmeth->declaring_class_;
  16. smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
  17. smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
  18. smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
  19. smeth->dex_cache_strings_ = dmeth->dex_cache_strings_;
  20. smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
  21. smeth->dex_method_index_ = dmeth->dex_method_index_;
  22. smeth->gc_map_ = dmeth->gc_map_;
  23. smeth->entry_point_from_jni_ = dmeth->entry_point_from_jni_;
  24. smeth->entry_point_from_quick_compiled_code_ = dmeth->entry_point_from_quick_compiled_code_;
  25. smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;
  26. smeth->method_index_ = dmeth->method_index_;
  27. }

上面代码主要在jvm虚拟内存的方法区里把原Cat类的方法表指针替换为修复Cat类的方法表指针,这样虚拟机在栈里面会加载修复Cat类的方法来压栈,从而达到修复。(为了这句话,翻了几天java内存相关的资料才总结出来,具体可移步这里)。

编译后运行效果:(突然觉得改成妙妙妙的叫声比较低调)

献上源码一枚,各位看官轻拍:
源码

Andfix存在的问题:
由于ArtMethod结构体在各个厂商下会做改动,因此这种强制性转化的方式在这些厂商下面会导致失败,当然这种也是有解决的方案,具体可以参考该文章Android热修复升级探索——追寻极致的代码热替换

项目运行环境:
使用Genymotion模拟器的Custom Phone 5.0测试,
使用ndk build来编译,具体如何配置请移步Android NDK 介绍与使用示例
由于用的是5.0的机子,因此jni层只保留使用5.0版本的方法替换,运行本demo时,需跟我当前的环境保持一致。

环境的一些问题:
1.如何编译c或cpp文件,在android studio里进入sdk manager,勾选Cmake、llb、NDK,下载完后即可

2.sdk manager里面找不到CMake?需用64位的Android Studio才行

3.运行CMake时候出现下面问题:

CMake Error in CMakeLists.txt:
The CMAKE_CXX_COMPILER:
E:/Android/SDK/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64/bin/clang++.exe is not a full path to an existing compiler tool.

解决方法请参考该文章安卓Cmake一处错误解决

参考文章

android热修复框架andfix原理解析及使用
Android热修复方案AndFix的实践

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注