模仿手写阿里andfix的实现原理

正经前言

当公司的项目出现问题了,早期的老套路子是解决bug,重新发新版本apk,但是随着技术不断的更新,线上项目出现严重问题,可以通过进行热修复,在不需要发布新版本的情形下进行问题处理。常见的热修复:阿里家的andfix和sophix, 腾讯家的tinker和QQ空间补丁技术...等等。

个人用过两款热修复:andfix和tinker
andfix和tinker区别:

框架 优点 缺点
andfix 不要重启app可以直接生效 存在兼容性问题
tinker 没有兼容性问题 需要重启app

今天主要分析一下Andfix,手写模仿Andfix的修复原理。

开始正经撸码.jpeg

开始正经撸码

热修复是基于dex分包方案和Android虚拟机的类加载器(ClassLoader)实现的。

  • 实现思路
  1. 发现bug 并修改bug,将修复的java文件 编译成class 然后打包成dex 放到服务器 供客户端下载
  2. 将修复的方法体 Method 从dex 文件取出,将会出现bug的方法 Method 也取出来
  3. 将取出的正确的 和 错误的method 一并传到底层做替换操作
  4. 在底层进行替换

原理

andfix的原理就是通过dex的类进行替换修改存在的问题;
热修复是基于类的层面:


Andfix的原理.png

dex多分包

实现代码,打包生产dex文件

  • 栗子:以除数是0的异常,作为栗子

bug类代码:

package com.jason.andfix;

public class Calculator {

  public int calculate() {

    int j = 10;
    int i = 0;
    int result = j / i;
    return result;

  }
}

修复的类代码:

package com.jason.andfix.web;

import com.jason.andfix.MethodReplace;

public class Calculator {

  @MethodReplace(clazz = "com.jason.andfix.Calculator", method = "calculate")
  public int calculate() {

    int j = 10;
    int i = 1;
    int result = j / i;
    return result;
    
  }
}

上面两个类,一个bug类是com.jason.andfix.Calculator,一个修复类是com.jason.andfix.web.Calculator
我们需要将修复的类打包成一个dex文件
这边采用的是SDK默认的dx.bat的工具进行打包


dx.bat在SDK所在的位置.png
  • 打包命令
dx  --dex --output  生产的dex文件名  所要打包的类

打包成功如下图,会在对应的目录下找到生成的out.dex文件,通常是会放到服务端,提供下载,这边demo上是直接将dex文件放到外置卡,省略了dex文件下载的过程


dx命令将class打包成dex包.png

Android的虚拟机

  • 基本虚拟机介绍
    Android jvm虚拟机采用的是JIT技术,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码。
    Android虚拟机分为dalvik虚拟机和art(Android Runtime)虚拟机
    Dalvik 是 Android 4.4 之前的标准虚拟机,Art是Android4.4之后的标准虚拟机,在Android4.4到Android7.0之前dalvik和art虚拟机是同时存在的,只是在Android5.0开始,Android的app都是依赖于art虚拟机上运行。
    关于dalvik和art的详细介绍https://blog.csdn.net/u011330638/article/details/82830027

  • 二者的区别:
    Dalvik虚拟机在jit编译器是在app运行时发生的,所以在Android5.0以下的机器,运行时候通常会容易卡顿
    Art虚拟机是将jit的字节码转机器码的过程,放在了apk在安装的过程中,所以在Android5.0以及以上的系统上安装过程比较长,但是大大提高了app的运行效率,采用了空间换时间的策略。

  • 示例上的实现
    上面介绍了两种虚拟机,说明Android虚拟机的类加载器(ClassLoader)至少有两套;因此,我们需要针对这两个进行适配。

    虚拟机jvm.jpg

    因此,我们需要从系统源码入手,进行分析
    系统源码:从Android1.6到android8.1的各个版本的系统源码
    链接:https://pan.baidu.com/s/1i3tiGwpeDuDL955RDhNJ0A 提取码:yzyr

jvm的类加载

Java类的加载分为三个过程 :加载(load),连接(link),初始化(init);
加载过程如下图:


类加载.png

类的对象结构图:


ClassObject.png

基于Dalvik虚拟机的实现修复

dalvik虚拟机的源码(由于没有4.4以下的机器,所以采用了4.4的系统源码,后面的dalvik修复也是基于该版本)


Android4.4.2系统源码dalvik虚拟机源码的头文件.png

dalvik虚拟机的api入口文件是Dalvik.h,我们在开发项目需要用到底层的api,就需要手动进行引入,但是系统的源码都头文件的引入:

  • 整理前的系统源码Dalvik.h代码:
#ifndef DALVIK_DALVIK_H_
#define DALVIK_DALVIK_H_

#include "Common.h"
#include "Inlines.h"
#include "Misc.h"
#include "Bits.h"
#include "BitVector.h"
#include "libdex/SysUtil.h"
#include "libdex/DexDebugInfo.h"
#include "libdex/DexFile.h"
#include "libdex/DexProto.h"
#include "libdex/DexUtf.h"
#include "libdex/ZipArchive.h"
#include "DvmDex.h"
#include "RawDexFile.h"
#include "Sync.h"
#include "oo/Object.h"
#include "Native.h"
#include "native/InternalNative.h"

#include "DalvikVersion.h"
#include "Debugger.h"
#include "Profile.h"
#include "UtfString.h"
#include "Intern.h"
#include "ReferenceTable.h"
#include "IndirectRefTable.h"
#include "AtomicCache.h"
#include "Thread.h"
#include "Ddm.h"
#include "Hash.h"
#include "interp/Stack.h"
#include "oo/Class.h"
#include "oo/Resolve.h"
#include "oo/Array.h"
#include "Exception.h"
#include "alloc/Alloc.h"
#include "alloc/CardTable.h"
#include "alloc/HeapDebug.h"
#include "alloc/WriteBarrier.h"
#include "oo/AccessCheck.h"
#include "JarFile.h"
#include "jdwp/Jdwp.h"
#include "SignalCatcher.h"
#include "StdioConverter.h"
#include "JniInternal.h"
#include "LinearAlloc.h"
#include "analysis/DexVerify.h"
#include "analysis/DexPrepare.h"
#include "analysis/RegisterMap.h"
#include "Init.h"
#include "libdex/DexOpcodes.h"
#include "libdex/InstrUtils.h"
#include "AllocTracker.h"
#include "PointerSet.h"
#if defined(WITH_JIT)
#include "compiler/Compiler.h"
#endif
#include "Globals.h"
#include "reflect/Reflect.h"
#include "oo/TypeCheck.h"
#include "Atomic.h"
#include "interp/Interp.h"
#include "InlineNative.h"
#include "oo/ObjectInlines.h"

#endif  // DALVIK_DALVIK_H_

整理后的Dalvik.h代码:就是把我们需要用到的api手动copy到我们项目新建的Dalvik.h文件中

//
// Created by PC-3046 on 2020/6/16.
//

#ifndef ANDFIX_DALVIK_H
#define ANDFIX_DALVIK_H

#include <jni.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <dlfcn.h>

#include <stdint.h>    /* C99 */


typedef uint8_t u1;
typedef uint16_t u2;
typedef uint32_t u4;
typedef uint64_t u8;
typedef int8_t s1;
typedef int16_t s2;
typedef int32_t s4;
typedef int64_t s8;

/*
 * access flags and masks; the "standard" ones are all <= 0x4000
 *
 * Note: There are related declarations in vm/oo/Object.h in the ClassFlags
 * enum.
 */
enum {
    ACC_PUBLIC = 0x00000001,       // class, field, method, ic
    ACC_PRIVATE = 0x00000002,       // field, method, ic
    ACC_PROTECTED = 0x00000004,       // field, method, ic
    ACC_STATIC = 0x00000008,       // field, method, ic
    ACC_FINAL = 0x00000010,       // class, field, method, ic
    ACC_SYNCHRONIZED = 0x00000020,       // method (only allowed on natives)
    ACC_SUPER = 0x00000020,       // class (not used in Dalvik)
    ACC_VOLATILE = 0x00000040,       // field
    ACC_BRIDGE = 0x00000040,       // method (1.5)
    ACC_TRANSIENT = 0x00000080,       // field
    ACC_VARARGS = 0x00000080,       // method (1.5)
    ACC_NATIVE = 0x00000100,       // method
    ACC_INTERFACE = 0x00000200,       // class, ic
    ACC_ABSTRACT = 0x00000400,       // class, method, ic
    ACC_STRICT = 0x00000800,       // method
    ACC_SYNTHETIC = 0x00001000,       // field, method, ic
    ACC_ANNOTATION = 0x00002000,       // class, ic (1.5)
    ACC_ENUM = 0x00004000,       // class, field, ic (1.5)
    ACC_CONSTRUCTOR = 0x00010000,       // method (Dalvik only)
    ACC_DECLARED_SYNCHRONIZED = 0x00020000,       // method (Dalvik only)
    ACC_CLASS_MASK = (ACC_PUBLIC | ACC_FINAL | ACC_INTERFACE | ACC_ABSTRACT
                      | ACC_SYNTHETIC | ACC_ANNOTATION | ACC_ENUM),
    ACC_INNER_CLASS_MASK = (ACC_CLASS_MASK | ACC_PRIVATE | ACC_PROTECTED
                            | ACC_STATIC),
    ACC_FIELD_MASK = (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC
                      | ACC_FINAL | ACC_VOLATILE | ACC_TRANSIENT | ACC_SYNTHETIC
                      | ACC_ENUM),
    ACC_METHOD_MASK = (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC
                       | ACC_FINAL | ACC_SYNCHRONIZED | ACC_BRIDGE | ACC_VARARGS
                       | ACC_NATIVE | ACC_ABSTRACT | ACC_STRICT | ACC_SYNTHETIC
                       | ACC_CONSTRUCTOR | ACC_DECLARED_SYNCHRONIZED),
};

typedef struct DexProto {
    u4* dexFile; /* file the idx refers to */
    u4 protoIdx; /* index into proto_ids table of dexFile */
} DexProto;
 ................................ 代码太长,省略,后续会提供完整的项目下载 ....................
  • 撸码实现
  1. Java实现dex文件的加载
    DexFileManager.java:
package com.jason.andfix;

import android.content.Context;
import android.os.Build;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Enumeration;

import dalvik.system.DexFile;

public class DexFileManager {

  private Context context;

  private static final DexFileManager INSTANCE = new DexFileManager();

  private DexFileManager(){}

  public static DexFileManager getInstance() {
    return INSTANCE;
  }

  public void setContext(Context context) {
    this.context = context.getApplicationContext();
  }

  /**
   * 加载dex文件
   * @param path
   */
  public void loadDexFile(String path) {
    File file = new File(path);
    loadDexFile(file);
  }

  public void loadDexFile(File file) {
    try {
      //dalvik虚拟机的dex对象
      DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
          new File(context.getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE);
      //下一步  得到class   ----取出修复好的Method
      Enumeration<String> entry= dexFile.entries();
      while (entry.hasMoreElements()) {
        //拿到全类名
        String className=entry.nextElement();
        //Class.forName(className);   拿到修复的dex的类
        Class clazz = dexFile.loadClass(className, context.getClassLoader());
        if (clazz != null) {
          fixClazz(clazz);
        }

      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  private void fixClazz(Class fixClazz) {
    //修复好的class
    Method[] methods = fixClazz.getDeclaredMethods();
    for (Method rightMethod : methods) {
      MethodReplace replace = rightMethod.getAnnotation(MethodReplace.class);
      if(replace == null) {
        continue;
      }

      String wrongClazzName = replace.clazz();
      String wrongMethodName = replace.method();

      try{
        Class clazz = Class.forName(wrongClazzName);
        Method wrongMethod = clazz.getDeclaredMethod(wrongMethodName, rightMethod.getParameterTypes());
        if (Build.VERSION.SDK_INT <= 19) {  //实际是<=18 ,由于没有4.4以下的机器,改成了19进行测试
          replaceDalvik(Build.VERSION.SDK_INT ,wrongMethod, rightMethod);
        }else {
          replaceArt(wrongMethod, rightMethod);
        }
      } catch (ClassNotFoundException e) {
        e.printStackTrace();
      } catch (NoSuchMethodException e) {
        e.printStackTrace();
      }
    }
  }


 //修复在通过jni调用底层进行method替换
  private native  void replaceArt(Method wrongMethod, Method rightMethod);
  public native void replaceDalvik(int sdk, Method wrongMethod, Method rightMethod);
}

上面实现了dex的文件加载,然后将加载到的dex解析,获取到我们修复好的类,再通过jni调用dalvik的C++底层进行底层method替换。关于JNI忘记的同学,可以参考我之前写的https://www.jianshu.com/p/3fdf924680af

  1. MethodReplace注解
    该注解是用来标识修复的方法,以及被修复的方法和类名
package com.jason.andfix;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodReplace {
  String  clazz();
  String  method();
}
  1. Dalvik虚拟机api的jni的实现method替换
extern "C"
JNIEXPORT void JNICALL
Java_com_jason_andfix_DexFileManager_replaceDalvik(JNIEnv *env, jobject thiz, jint sdk,
                                                   jobject wrong_method, jobject right_method) {

    Method *wrong = (Method *) env->FromReflectedMethod(wrong_method);
    Method *right =(Method *) env->FromReflectedMethod(right_method);

    //ClassObject
    void *dvm_hand=dlopen("libdvm.so", RTLD_NOW);
    //sdk  10    以前是这样   10会发生变化
    findObject= (FindObject) dlsym(dvm_hand, sdk > 10 ?
                                             "_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
                                             "dvmDecodeIndirectRef");
    findThread = (FindThread) dlsym(dvm_hand, sdk > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
    // method   所声明的Class

    jclass methodClaz = env->FindClass("java/lang/reflect/Method");
    jmethodID rightMethodId = env->GetMethodID(methodClaz, "getDeclaringClass",
                                               "()Ljava/lang/Class;");
    //dalvik  odex   机器码
    //  firstFiled->status=CLASS_INITIALIZED
    //  art不需要    dalvik适配
    jobject ndkObject = env->CallObjectMethod(right_method, rightMethodId);
    ClassObject *firstFiled = (ClassObject *) findObject(findThread(), ndkObject);
    firstFiled->status=CLASS_INITIALIZED;
    wrong->accessFlags |= ACC_PUBLIC;

    wrong->methodIndex=right->methodIndex;
    wrong->jniArgInfo=right->jniArgInfo;
    wrong->registersSize=right->registersSize;
    wrong->outsSize=right->outsSize;
//    方法参数 原型
    wrong->prototype=right->prototype;
//
    wrong->insns=right->insns;
    wrong->nativeFunc=right->nativeFunc;
}
  1. 测试运行(在Android4.4的机器运行),将我们最开始生产的out.dex放到手机的外置存储卡;


    Android4.4手机上的修复结果.png

基于Art虚拟机上实现热修复

art虚拟机的源码(由于没有5.1以下的机器,所以采用了5.1的系统源码,后面的art修复也是基于该版本)


Android5.1系统的源码art虚拟机的代码头文件.png

整理后的art_method.h在后续源码中

  • Art虚拟机api的jni的实现method替换
extern "C"
JNIEXPORT void JNICALL
Java_com_jason_andfix_DexFileManager_replaceArt(JNIEnv *env, jobject thiz, jobject wrong_method,
                                                jobject right_method) {
//    art虚拟机替换  art  ArtMethod  ---》Java方法
    art::mirror::ArtMethod *wrong = (art::mirror::ArtMethod *) env->FromReflectedMethod(wrong_method);
    art::mirror::ArtMethod *right = (art::mirror::ArtMethod *) env->FromReflectedMethod(right_method);

    wrong->declaring_class_=right->declaring_class_;

    wrong->dex_code_item_offset_=right->dex_code_item_offset_;
    wrong->method_index_=right->method_index_;
    wrong->dex_method_index_=right->dex_method_index_;


    //入口
    wrong->ptr_sized_fields_.entry_point_from_jni_=right->ptr_sized_fields_.entry_point_from_jni_;
    //    机器码模式
    wrong->ptr_sized_fields_.entry_point_from_quick_compiled_code_=right->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
}
  • 测试运行的效果(这边在6.0的机器上进行测试,需要动态申请存储权限),同样将out.dex 放到手机外置卡


    Android6.0机器上的允许结果.png

总结

不管是art虚拟机还是dalvik虚拟机,实现热修复的关键是,在底层进行method的指针的替换,将错误的method的指针替换到修复后的新的method的指针。

结语

关于目前文章描述的是dalvik和art的适配,但是在Android7.0的系统,Google又进行了新的调整,因此art的热修复需要再7.0的系统在做一次兼容处理。
以上就是说模拟手写阿里Andfix的内容,如有错误,欢迎指正。

参考文献

https://www.jianshu.com/p/cc66138d72b1
https://blog.csdn.net/u011330638/article/details/82830027

我的博客将同步至腾讯云+社区

https://cloud.tencent.com/developer/column/87704

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260