正经前言
当公司的项目出现问题了,早期的老套路子是解决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的类进行替换修改存在的问题;
热修复是基于类的层面:
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 --dex --output 生产的dex文件名 所要打包的类
打包成功如下图,会在对应的目录下找到生成的out.dex文件,通常是会放到服务端,提供下载,这边demo上是直接将dex文件放到外置卡,省略了dex文件下载的过程
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)至少有两套;因此,我们需要针对这两个进行适配。
因此,我们需要从系统源码入手,进行分析
系统源码:从Android1.6到android8.1的各个版本的系统源码
链接:https://pan.baidu.com/s/1i3tiGwpeDuDL955RDhNJ0A 提取码:yzyr
jvm的类加载
Java类的加载分为三个过程 :加载(load),连接(link),初始化(init);
加载过程如下图:
类的对象结构图:
基于Dalvik虚拟机的实现修复
dalvik虚拟机的源码(由于没有4.4以下的机器,所以采用了4.4的系统源码,后面的dalvik修复也是基于该版本)
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;
................................ 代码太长,省略,后续会提供完整的项目下载 ....................
- 撸码实现
- 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
- 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();
}
- 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;
}
-
测试运行(在Android4.4的机器运行),将我们最开始生产的out.dex放到手机的外置存储卡;
基于Art虚拟机上实现热修复
art虚拟机的源码(由于没有5.1以下的机器,所以采用了5.1的系统源码,后面的art修复也是基于该版本)
整理后的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 放到手机外置卡
总结
不管是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