专栏首页包子的书架模仿手写andfix的实现原理

模仿手写andfix的实现原理

正经前言

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

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

框架

优点

缺点

andfix

不要重启app可以直接生效

存在兼容性问题

tinker

没有兼容性问题

需要重启app

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

开始正经撸码

热修复是基于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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • AspectJ在android上的开发

    Android上运用AOP的思想开发,可以快速的帮助我们简化在横向开发中的重复性工作,简单的说就是把涉及到众多模块的某一类问题进行统一管理比如:性能检测、日志打...

    包子388321
  • Unity中Android 触发home,回收覆盖在UnityPlayerActivity上层的activity的问题

    最近在开发一个Android SDK的项目,提供给游戏方接入,发现游戏是unity引擎;正常游戏都是只有一个activity(继承UnityPlayerActi...

    包子388321
  • 两个TextView,第二个不超过最右侧边距

    包子388321
  • VMware虚拟机 - 解决主机每次重启后 VMWare 都无法上网的问题

    小菠萝测试笔记
  • 【必读】腾讯企点销售智推上手指南

    ? 企点营销-销售智推 社交场景的销售客户管理工具 筛选消费者需要的精准信息及内容为销售赋能 01 使用前授权准备 ①开通小程序 企业必须申请一个已认证的小...

    腾讯企点
  • 如何白嫖微软Azure12个月及避坑指南

    Azure是微软提供的一个云服务平台。是全球除了AWS外最大的云服务提供商。Azure是微软除了windows之外另外一个王牌,微软错过了移动端,还好抓住了云服...

    kklldog
  • Derek解读Bytom源码-protobuf生成比原核心代码

    Gitee地址:https://gitee.com/BytomBlockchain/bytom

    比原链Bytom
  • Derek解读Bytom源码-protobuf生成比原核心代码

    Gitee地址:https://gitee.com/BytomBlockchain/bytom

    比原链Bytom
  • Dubbo集群容错模式之Failfast实现 原

                                                      图1 Dubbo的FailfastClusterInvoke...

    克虏伯
  • KDD2018 | 摩拜&amp;京东联合利用智能单车数据挖掘违章停车

    作者:Tianfu He、Jie Bao、Ruiyuan Li、Sijie Ruan、Yanhua Li、Chao Tian、Yu Zheng

    机器之心

扫码关注云+社区

领取腾讯云代金券