基本流程
- 将修复好的方法加上一个自定义注解
- 将所有修复好的class打包成dex
- 从dex中取出修复好的method
- 将class中出错的method的引用指向已修复的method
Andfix热修复的优点在于性能高,并且不需要重启APP,但是要注意,一点退出APP(结束dalvik)就需要重新修复。
开始撸码
创建一个新的项目,需要引入c++。创建一个错误的方法
package com.example.chauncey.ndk_lsn15_andfix;
/**
* Created by 45216 on 2017/9/2.
*/
class Calculator {
int calculate() {
int a = 10;
int b = 0;
return a / b;
}
}
创建一个自定义注解类
package com.example.chauncey.ndk_lsn15_andfix;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by 45216 on 2017/9/2.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Replace {
String clazz();
String method();
}
创建一个修复好的method,类名方法名一致
package com.example.chauncey.ndk_lsn15_andfix.Web;
import com.example.chauncey.ndk_lsn15_andfix.Replace;
/**
* Created by 45216 on 2017/9/2.
*/
class Calculator {
//此处的参数分别是待修复的类和方法
@Replace(clazz = "com.example.chauncey.ndk_lsn15_andfix.Calculator", method = "calculate")
int calculate() {
int a = 10;
int b = 1;
return a / b;
}
}
创建一个DexManager工具类
项目结构
build一下项目,提取已经编译好的class文件
将整个com文件夹提取出来,在这里我是放到桌面上的dx文件夹中,将除了Web文件夹以外的class文件全部删除,我们只需要将修复好的class打包成dex就行了。
使用Android sdk\build-tools\版本号\下的dx进行打包(我是偷懒,要不然放到系统环境变量里会好一些),以管理员身份运行cmd
E:\Android\android-sdk\build-tools\26.0.1>dx --dex --output C:\Users\45216\Desktop\dx\out.dex C:\Users\45216\Desktop\dx\
以上命令行中
- –dex 打包成dex
- –output 输出到指定位置
把dex文件放到设备上(模拟从服务器上下载),我放到了当前程序的cache文件夹下。
接下来是DexManger
public class DexManager {
private static final DexManager ourInstance = new DexManager();
//创建文件时勾选了单例模式,此处懒得改了
public void setContext(Context context) {
mContext = context;
}
private Context mContext;
public static DexManager getInstance() {
return ourInstance;
}
private DexManager() {
}
public void loadFile(File file) {
try {
if(!file.exists()){
return;
}
//专门用来处理Dex文件的类,在dalvik.system包下,毕竟Android4.4开始就已经使用了art虚拟机,所以这个类已经过时了
DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
new File(mContext.getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE);
Enumeration<String> entries = dexFile.entries();
while (entries.hasMoreElements()) {
String className = entries.nextElement();
Class clazz = dexFile.loadClass(className, mContext.getClassLoader());
if (clazz != null) {
fixClass(clazz);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void fixClass(Class clazz) {
//获取所有类中的所有方法
Method[] methods = clazz.getDeclaredMethods();
for (Method rightMethod : methods) {
Replace replace = rightMethod.getAnnotation(Replace.class);
//只需要带有Replace注解的方法
if (replace == null) {
continue;
}
String wClass = replace.clazz();
String wMethod = replace.method();
try {
//Class.forName只能获取已存在的类,上面的dexFile.loadClass则是获取不存在于当前系统中的类
Class wrongClass = Class.forName(wClass);
Method wrongMethod = wrongClass.getDeclaredMethod(wMethod, rightMethod.getParameterTypes());
replaceMethod(Build.VERSION.SDK_INT, wrongMethod, rightMethod);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private native void replaceMethod(int sdkVersion, Method wrongMethod, Method rightMethod);
}
在c++中进行方法的替换
首先要引入dalvik.h,但是引入时发现dalvik.h又引入了很多其它的头文件,所以在这里自己创建一个dalvik.h,因为Android4.4以下版本都是dalvik虚拟机,所以具体的实现已经集成好了。
以下是dalvik.h的具体内容
#include <string.h>
#include <jni.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;
typedef void (*DalvikBridgeFunc)(const u4* args, void* pResult,
const void* method, void* self);
struct Field {
void* clazz; /* class in which the field is declared */
const char* name;
const char* signature; /* e.g. "I", "[C", "Landroid/os/Debug;" */
u4 accessFlags;
};
struct Method;
struct ClassObject;
typedef struct Object {
/* ptr to class object */
struct ClassObject* clazz;
/*
* 类的加载过程
* A word containing either a "thin" lock or a "fat" monitor. See
* the comments in Sync.c for a description of its layout.
*/
u4 lock;
} Object;
struct InitiatingLoaderList {
/* a list of initiating loader Objects; grown and initialized on demand */
void** initiatingLoaders;
/* count of loaders in the above list */
int initiatingLoaderCount;
};
enum PrimitiveType {
PRIM_NOT = 0, /* value is a reference type, not a primitive type */
PRIM_VOID = 1,
PRIM_BOOLEAN = 2,
PRIM_BYTE = 3,
PRIM_SHORT = 4,
PRIM_CHAR = 5,
PRIM_INT = 6,
PRIM_LONG = 7,
PRIM_FLOAT = 8,
PRIM_DOUBLE = 9,
}typedef PrimitiveType;
enum ClassStatus {
CLASS_ERROR = -1,
CLASS_NOTREADY = 0, CLASS_IDX = 1, /* loaded, DEX idx in super or ifaces */
CLASS_LOADED = 2, /* DEX idx values resolved */
CLASS_RESOLVED = 3, /* part of linking */
CLASS_VERIFYING = 4, /* in the process of being verified */
CLASS_VERIFIED = 5, /* logically part of linking; done pre-init */
CLASS_INITIALIZING = 6, /* class init in progress */
CLASS_INITIALIZED = 7, /* ready to go */
}typedef ClassStatus;
typedef struct ClassObject {
struct Object o; // emulate C++ inheritance, Collin
/* leave space for instance data; we could access fields directly if we
freeze the definition of java/lang/Class */
u4 instanceData[4];
/* UTF-8 descriptor for the class; from constant pool, or on heap
if generated ("[C") */
const char* descriptor;
char* descriptorAlloc;
/* access flags; low 16 bits are defined by VM spec */
u4 accessFlags;
/* VM-unique class serial number, nonzero, set very early */
u4 serialNumber;
/* DexFile from which we came; needed to resolve constant pool entries */
/* (will be NULL for VM-generated, e.g. arrays and primitive classes) */
void* pDvmDex;
/* state of class initialization */
ClassStatus status;
/* if class verify fails, we must return same error on subsequent tries */
struct ClassObject* verifyErrorClass;
/* threadId, used to check for recursive <clinit> invocation */
u4 initThreadId;
/*
* Total object size; used when allocating storage on gc heap. (For
* interfaces and abstract classes this will be zero.)
*/
size_t objectSize;
/* arrays only: class object for base element, for instanceof/checkcast
(for String[][][], this will be String) */
struct ClassObject* elementClass;
/* arrays only: number of dimensions, e.g. int[][] is 2 */
int arrayDim;
PrimitiveType primitiveType;
/* superclass, or NULL if this is java.lang.Object */
struct ClassObject* super;
/* defining class loader, or NULL for the "bootstrap" system loader */
struct Object* classLoader;
struct InitiatingLoaderList initiatingLoaderList;
/* array of interfaces this class implements directly */
int interfaceCount;
struct ClassObject** interfaces;
/* static, private, and <init> methods */
int directMethodCount;
struct Method* directMethods;
/* virtual methods defined in this class; invoked through vtable */
int virtualMethodCount;
struct Method* virtualMethods;
/*
* Virtual method table (vtable), for use by "invoke-virtual". The
* vtable from the superclass is copied in, and virtual methods from
* our class either replace those from the super or are appended.
*/
int vtableCount;
struct Method** vtable;
} ClassObject;
typedef struct Method {
struct ClassObject *clazz;
u4 accessFlags;
//u2 methodIndex 方法表里面的索引
u2 methodIndex;
u2 registersSize; /* ins + locals */
u2 outsSize;
u2 insSize;
/* method name, e.g. "<init>" or "eatLunch" */
const char* name;
/*
* Method prototype descriptor string (return and argument types).
*
* TODO: This currently must specify the DexFile as well as the proto_ids
* index, because generated Proxy classes don't have a DexFile. We can
* remove the DexFile* and reduce the size of this struct if we generate
* a DEX for proxies.
*/
DexProto prototype;
/* short-form method descriptor string */
const char* shorty;
/*
* The remaining items are not used for abstract or native methods.
* (JNI is currently hijacking "insns" as a function pointer, set
* after the first call. For internal-native this stays null.)
*/
/* the actual code */
u2* insns;
/* cached JNI argument and return-type hints */
int jniArgInfo;
/*
* Native method ptr; could be actual function or a JNI bridge. We
* don't currently discriminate between DalvikBridgeFunc and
* DalvikNativeFunc; the former takes an argument superset (i.e. two
* extra args) which will be ignored. If necessary we can use
* insns==NULL to detect JNI bridge vs. internal native.
*/
DalvikBridgeFunc nativeFunc;
#ifdef WITH_PROFILER
bool inProfile;
#endif
#ifdef WITH_DEBUGGER
short debugBreakpointCount;
#endif
bool fastJni;
/*
* JNI: true if this method has no reference arguments. This lets the JNI
* bridge avoid scanning the shorty for direct pointers that need to be
* converted to local references.
*
* TODO: replace this with a list of indexes of the reference arguments.
*/
bool noRef;
} Method;
接下来是replace方法的具体实现
#include <jni.h>
#include "dalvik.h"
typedef Object *(*FindObject)(void *thread, jobject obj);
typedef void *(*FindThread)();
FindObject findObject;
FindThread findThread;
extern "C" {
JNIEXPORT void JNICALL
Java_com_example_chauncey_ndk_1lsn15_1andfix_DexManager_replaceMethod
(JNIEnv *env, jobject instance, jint sdk, jobject wrongMethod, jobject rightMethod) {
//将传入的方法转换层JNI的Method结构体
Method *wrong = (Method *) env->FromReflectedMethod(wrongMethod);
Method *right = (Method *) env->FromReflectedMethod(rightMethod);
//下一步 把right 对应Object 第一个成员变量ClassObject status
//获取dalvik句柄
void *dvm_handle = dlopen("libdvm.so", RTLD_NOW);
//sdk 10前后需要不同的名字,具体原因不明。注意:这里编译器会报错,要求传三个参数,其实只要两个,不必理会
findObject = (FindObject) dlsym(dvm_hand, sdk > 10 ?
"_Z20dvmDecodeIndirectRefP6ThreadP8_jobject":
"dvmDecodeIndirectRef");
findThread = (FindThread) dlsym(dvm_handle , sdk > 10 ? "_Z13dvmThreadSelfv" :
"dvmThreadSelf");
//获取 java中的Method类
jclass methodClazz = env->FindClass("java/lang/reflect/Method");
jmethodID rightMethodId = env->GetMethodID(methodClazz, "getDeclaringClass",
"()Ljava/lang/Class;");
jobject ndkObject = env->CallObjectMethod(rightMethod, rightMethodId);
//上面所有的操作都是为了下面做铺垫,只有将status 赋值为CLASS_INITIALIZED时才表示类已经加载完毕,其中的方法才可以调用
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;
}
}
最后只需要
File file = new File(this.getCacheDir(), "out.dex");
if (file.exists()) {
DexManager.getInstance().loadFile(file);
}
修复完成。