首先异常的发生一定是在方法中,想修复的话:
- 重新安装apk(我是来逗逼的)
- 覆盖含有异常方法的类
- 覆盖发生异常的方法
AndFix实现的是可以实现实时修复异常函数,不需要重启APP。
对于上面2、3的选择,要了解虚拟机执行原理才能选择
先了解内存中方法区、堆、栈是干什么用的:
- 方法区:当JVM使用类加载器加载class文件并输入到内存时,会提取class文件的类型信息,然后将这些信息、方法和静态变量放入到方法区中。
- 堆区:Java程序在运行时创建的类型对象和数组都存储在堆中,JVM会根据new指令在堆中开辟块内存空间,但是堆中开辟的空间并没有人工指令可以回收,而是通过JVM的垃圾回收器负责回收。
-
栈:存放要执行的方法和局部变量等。每启动一个线程,JVM都会分配一个Java栈给该线程,当该线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧,并将该栈帧压入到栈中,方法执行完毕时,JVM会弹出该栈帧并释放。
假设ClassA类有methodA、methodB两个函数,classA是ClassA的一个实例对象,来张内存图:
image.png
类加载和执行流程:
- 调用 ClassA classA = new ClassA()的时候执行第一步,JVM会根据对象的class类型在堆中分配一块储存空间,并且指向改类的符号变量(方法区中的每个类都有一个int型的符号变量指向该类)。
- 调用classA. methodA()的时候执行第二步,JVM根据方法区中该方法的字节码组建一个栈帧,并将该栈帧压入到栈中。
选什么修复方式?在哪一步可以完成修复?
Java特性决定一个class只会加载一次,方法区中的ClassA在第一次被实例化的时候调用,之后再调用new的时候不会重新加载。所以想不需要重启APP就实现修复功能,只能选择3. 覆盖发生异常的方法,从方法表入手,要做的就是修改方法区中异常函数的ArtMethod结构体。
准备开撸(用JNI是跑不了的),模拟一个AndFix热修复过程。
页面布局:
页面
ClassA中methodA是个异常函数:
public class ClassA {
public String methorA() {
throw new RuntimeException("------异常------");
}
public String methorB() {
return "我是methorB返回值";
}
}
MainActivity:
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
TextView tv;
ClassA classA;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.sample_text);
classA = new ClassA();
}
public void callMethodA(View view) {
tv.setText(classA.methorA());
}
public void callMethodB(View view) {
tv.setText(classA.methorB());
}
public void fixMethod(View view) {
}
}
现在肯定是调用methorB()修改text文字,调methorA()闪退。下面在fixMethod中进行对ClassA中的methorA(),就行修复。
首先肯定需要一个正确的ClassA文件:
public class ClassA {
public String methorA() {
return "我是已修复后的methorA返回值";
}
// 这个不修复,写这里是为了对比。
public String methorB() {
return "我是已修复后的methorB返回值";
}
}
现在修复文件有了,如果想要修复指定函数,肯定要确定要修复的类名和函数名。这里用注解实现,新建一个注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Replace {
// 要修复的类
String clazz();
// 要修复的方法
String method();
}
然后把注解加到正确的methorA()上,此时正确的ClassA:
public class ClassA {
@Replace(clazz = "com.yu.myfix.ClassA", method = "methorA")
public String methorA() {
return "我是已修复后的methorA返回值";
}
public String methorB() {
return "我是已修复后的methorB返回值";
}
}
正确和错误的类、函数都就绪。
问题又来了,apk中的类文件是怎么加载到内存方法区的?我们怎么读区正确的ClassA类?
答:通过DexFlex类 !!!就可以加载dex文件了。
问题叕来了,dex文件是什么?怎么生存dex文件?
答:莫慌,先理解为他就是ClassA.java的另一种形式。
继续:
正确和错误的类、函数都就绪,现在可以实现修复过程了,这里的修复包就不写网络下载了,直接放到sdcard里。
修复的过程新建一个FixManager 类,在MainActivity调用:
public void fixMethod(View view) {
File file = new File(Environment.getExternalStorageDirectory(), "classAfix.dex");
FixManager dexManager = new FixManager(this);
dexManager.load(file);
}
FixManager类:
public class FixManager {
Context context;
public FixManager(Context context) {
this.context = context;
}
/**
* 读取文件,遍历dex文件中的类
* @param file
*/
public void load(File file){
try {
DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
new File(context.getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE);
// 遍历dex文件中的类
Enumeration<String> entry=dexFile.entries();
while (entry.hasMoreElements()) {
String clazzName= entry.nextElement();
Class realClazz= dexFile.loadClass(clazzName, context.getClassLoader());
if (realClazz != null) {
// 修复类
fixClazz(realClazz);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 修复类中加了特定注解的方法
* @param clazz
*/
public void fixClazz(Class clazz){
//获取类中的方法
Method[] methods= clazz.getMethods();
for (Method rightMethod : methods) {
//筛选添加了Replace注解的方法
Replace replace = rightMethod.getAnnotation(Replace.class);
if (replace == null) {
continue;
}
// 获取注解中的类名和方法名
String clazzName=replace.clazz();
String methodName=replace.method();
try {
// 从内存中获取已经加载过的错误类
Class wrongClazz = Class.forName(clazzName);
// 获取错误类的错误方法
Method wrongMethod = wrongClazz.getDeclaredMethod(methodName, rightMethod.getParameterTypes());
// 替换方法
replace(wrongMethod, rightMethod);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* native函数,替换错误方法
* @param wrongMethod
* @param rightMethod
*/
private native void replace(Method wrongMethod, Method rightMethod);
}
FixManager中其实就是读取修复包(刚才说了可以把classAfix.dex文件先理解成ClassA.java的另一种形式),遍历其中的类,遍历类中的函数,看看有没有需要修复的函数,判断的依据就是方法是否有我们刚才添加的注解,一切正常的话,应该是拿到了正确的methorA,因为methorB并没有添加注解。
然后就需要实现FixManager 中replace这个native函数了。
native-lib.cpp:
#include <jni.h>
#include <string>
#include "art_method.h"
// 这个函数没用,创建C++项目时AS自动生成的示例
extern "C" JNIEXPORT jstring JNICALL
Java_com_yu_myfix_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT void JNICALL
Java_com_yu_myfix_FixManager_replace(JNIEnv *env, jobject instance, jobject wrongMethod,
jobject rightMethod) {
art::mirror::ArtMethod *wrong= (art::mirror::ArtMethod *)env->FromReflectedMethod(wrongMethod);
art::mirror::ArtMethod *right= (art::mirror::ArtMethod *)env->FromReflectedMethod(rightMethod);
wrong->declaring_class_ = right->declaring_class_;
wrong->dex_cache_resolved_methods_ = right->dex_cache_resolved_methods_;
wrong->access_flags_ = right->access_flags_;
wrong->dex_cache_resolved_types_ = right->dex_cache_resolved_types_;
wrong->dex_code_item_offset_ = right->dex_code_item_offset_;
wrong->method_index_ = right->method_index_;
wrong->dex_method_index_ = right->dex_method_index_;
}
我的哥,这是啥?
我的弟,哥也解释不太清楚这些变量具体是干啥用的,哥只能告诉你这就是上面提到的ArtMethod结构体,它就是长这样的,就像系统的类你不一定非得知道他的成员变量都是干啥的,但是知道的话,肯定很牛X!(也许你就可以尝试实现一下Sophix这里的逻辑了)
我的哥,你咋知道这样写?
我的弟,哥是看系统源码知道的(其实哥是看见AndFix源码差不多就这样写的)。
上面这段native代码其实就是替换方法,把错误的methodA换成正确的methodA,但是不能直接写wrongMethodA = rightMethodA, 要把wrongMethodA里面的变量替换成rightMethodA里的变量。直接上面这样写编译不通过,因为找不到ArtMethod.h。我们去系统源码中找到这个ArtMethod.h文件copy过来,也就知道了要替换那些变量。
系统源码可以自己找资源下载,也可以用在线系统源码。
这个文件5.0在/art/runtime/mirror/art_method.h,9.0在/art/runtime/art_method.h
其他版本反正大概就在这里吧。
直接copy过来发现这里代码很多,而且其中引入了其他.h。如果你在继续copy其他的,最后可能会把Android系统全copy过来- -!
.h文件不会被编译到apk中,所以我们只要有art_method.h文件和它里面的一些变量声明,保证我们的项目可以编译通过就可以了,直接把art_method.h文件copy过来之后一顿删,最后的结果:
namespace art {
namespace mirror {
class ArtMethod : public Object {
public:
// Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
// The class we are a part of.
uint32_t declaring_class_;
// Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
uint32_t dex_cache_resolved_methods_;
// Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
uint32_t dex_cache_resolved_types_;
// Method dispatch from the interpreter invokes this pointer which may cause a bridge into
// compiled code.
uint64_t entry_point_from_interpreter_;
// Pointer to JNI function registered to this method, or a function to resolve the JNI function.
uint64_t entry_point_from_jni_;
// Method dispatch from quick compiled code invokes this pointer which may cause bridging into
// portable compiled code or the interpreter.
uint64_t entry_point_from_quick_compiled_code_;
// Pointer to a data structure created by the compiler and used by the garbage collector to
// determine which registers hold live references to objects within the heap. Keyed by native PC
// offsets for the quick compiler and dex PCs for the portable.
uint64_t gc_map_;
// Access flags; low 16 bits are defined by spec.
uint32_t access_flags_;
/* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
// Offset to the CodeItem.
uint32_t dex_code_item_offset_;
// Index into method_ids of the dex file associated with this method.
uint32_t dex_method_index_;
/* End of dex file fields. */
// Entry within a dispatch table for this method. For static/direct methods the index is into
// the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
// ifTable.
uint32_t method_index_;
};
} // namespace mirror
} // namespace art
然后Object还会报错,这个很显然不能删。。要保留。我们采取同样的copy删减方式对待Object.h(删的更多,有用的就两行,直接写在art_method.h里了)。
art_method.h如下:
namespace art {
namespace mirror {
class Object {
public:
uint32_t klass_;
uint32_t monitor_;
};
class ArtMethod : public Object {
public:
// Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
// The class we are a part of.
uint32_t declaring_class_;
// Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
uint32_t dex_cache_resolved_methods_;
// Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
uint32_t dex_cache_resolved_types_;
// Method dispatch from the interpreter invokes this pointer which may cause a bridge into
// compiled code.
uint64_t entry_point_from_interpreter_;
// Pointer to JNI function registered to this method, or a function to resolve the JNI function.
uint64_t entry_point_from_jni_;
// Method dispatch from quick compiled code invokes this pointer which may cause bridging into
// portable compiled code or the interpreter.
uint64_t entry_point_from_quick_compiled_code_;
// Pointer to a data structure created by the compiler and used by the garbage collector to
// determine which registers hold live references to objects within the heap. Keyed by native PC
// offsets for the quick compiler and dex PCs for the portable.
uint64_t gc_map_;
// Access flags; low 16 bits are defined by spec.
uint32_t access_flags_;
/* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
// Offset to the CodeItem.
uint32_t dex_code_item_offset_;
// Index into method_ids of the dex file associated with this method.
uint32_t dex_method_index_;
/* End of dex file fields. */
// Entry within a dispatch table for this method. For static/direct methods the index is into
// the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
// ifTable.
uint32_t method_index_;
};
} // namespace mirror
} // namespace art
这里的变量,其实就是上面替换方法时需要替换的变量。
到这里已经可以在5.0的系统中愉快的修复了。
为什么是5.0?
因为我们的art_method.h和Object都是从5.0的系统源码中copy来的,不同版本的系统,这俩文件还不一样 - -!, 看下AndFix的源码目录:
AndFix的源码目录
尴尬吧。。每个版本可能都要适配,这也是AndFix的缺点。所以到7.0就不维护了,换成了Sophix
这里也贴一下9.0适配文件
native-lib.cpp:
#include <jni.h>
#include <string>
#include "art_method_9_0_0.h"
// 这个函数没用,创建C++项目时AS自动生成的示例
extern "C" JNIEXPORT jstring JNICALL
Java_com_yu_myfix_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT void JNICALL
Java_com_yu_myfix_FixManager_replace(JNIEnv *env, jobject instance, jobject wrongMethod,
jobject rightMethod) {
art::ArtMethod *wrong= (art::ArtMethod *)env->FromReflectedMethod(wrongMethod);
art::ArtMethod *right= (art::ArtMethod *)env->FromReflectedMethod(rightMethod);
wrong->declaring_class_ = right->declaring_class_;
wrong->ptr_sized_fields_.data_ = right->ptr_sized_fields_.data_;
wrong->access_flags_ = right->access_flags_;
wrong->ptr_sized_fields_.entry_point_from_quick_compiled_code_ = right->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
wrong->dex_code_item_offset_ = right->dex_code_item_offset_;
wrong->method_index_ = right->method_index_;
wrong->dex_method_index_ = right->dex_method_index_;
}
art_method_9_0_0.h:
namespace art {
class ArtMethod {
public:
// Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
// The class we are a part of.
uint32_t declaring_class_;
// Access flags; low 16 bits are defined by spec.
uint32_t access_flags_;
/* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
// Offset to the CodeItem.
uint32_t dex_code_item_offset_;
// Index into method_ids of the dex file associated with this method.
uint32_t dex_method_index_;
/* End of dex file fields. */
// Entry within a dispatch table for this method. For static/direct methods the index is into
// the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
// ifTable.
uint16_t method_index_;
// The hotness we measure for this method. Incremented by the interpreter. Not atomic, as we allow
// missing increments: if the method is hot, we will see it eventually.
uint16_t hotness_count_;
// Fake padding field gets inserted here.
// Must be the last fields in the method.
// PACKED(4) is necessary for the correctness of
// RoundUp(OFFSETOF_MEMBER(ArtMethod, ptr_sized_fields_), pointer_size).
struct PtrSizedFields {
// Depending on the method type, the data is
// - native method: pointer to the JNI function registered to this method
// or a function to resolve the JNI function,
// - conflict method: ImtConflictTable,
// - abstract/interface method: the single-implementation if any,
// - proxy method: the original interface method or constructor,
// - other methods: the profiling data.
void* data_;
// Method dispatch from quick compiled code invokes this pointer which may cause bridging into
// the interpreter.
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
};
}
这俩文件替换了,就可以在9.0中愉快的修复了。这里主要的区别会发现是这些结构体中的变量大小不一样。如果不进行版本适配会导致内存乱掉了,Sopfix应该是对这进了内存对齐,具体咋搞的不知道(Sopfix不开源)。
还有个事没说,classAfix.dex文件咋来的?
Android的sdk中build-tools提供了用.class文件生成.dex文件的工具:
用法baidu或google,很简单。配置目录到环境变量到dx工具目录下,在终端直接输入dex可以查看帮助。
使用dx --dex --output=输出文件名 .class文件路径
比如上面项目,我的文件在:
image.png
使用:
dx --dex --output=classAfix.dex /Users/zhangyu/Desktop/tofix
上图中classAfix.dex就是生成后的
项目地址
网友评论