前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >模仿手写andfix的实现原理

模仿手写andfix的实现原理

作者头像
包子388321
发布2020-06-19 11:10:25
6140
发布2020-06-19 11:10:25
举报
文章被收录于专栏:包子的书架包子的书架

正经前言

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

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

andfix和tinker区别:

框架

优点

缺点

andfix

不要重启app可以直接生效

存在兼容性问题

tinker

没有兼容性问题

需要重启app

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

开始正经撸码

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

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

andfix的原理就是通过dex的类进行替换修改存在的问题;

热修复是基于类的层面:

Andfix的原理.png

dex多分包

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

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

bug类代码:

代码语言:javascript
复制
package com.jason.andfix;

public class Calculator {

  public int calculate() {

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

  }
}

修复的类代码:

代码语言:javascript
复制
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

  • 打包命令
代码语言:javascript
复制
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代码:
代码语言:javascript
复制
#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文件中

代码语言:javascript
复制
//
// 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;
 ................................ 代码太长,省略,后续会提供完整的项目下载 ....................
  • 撸码实现
  • Java实现dex文件的加载 DexFileManager.java:
代码语言:javascript
复制
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://cloud.tencent.com/developer/article/1645991

  1. MethodReplace注解 该注解是用来标识修复的方法,以及被修复的方法和类名
代码语言:javascript
复制
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替换
代码语言:javascript
复制
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替换
代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 正经前言
  • 开始正经撸码
    • 原理
      • dex多分包
        • Android的虚拟机
          • jvm的类加载
            • 基于Dalvik虚拟机的实现修复
            • 基于Art虚拟机上实现热修复
            • 总结
        • 结语
        • 参考文献
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档