@whosea
2017-12-18T06:12:34.000000Z
字数 5528
阅读 824
Android热修复
原文链接:https://www.zybuluo.com/whosea/note/929832
Andfix是阿里开源的热修复框架,也是Native热修复里面比较有代表性的方案。实现简洁直接,应用patch不需要重启。但由于从实现上直接跳过了类初始化,设置为初始化完毕,所以像是静态函数、静态成员、构造函数都会出现问题,复杂点的类Class.forname很可能直接就会挂掉。但是可做为我们分析它实现的思路,为自己的技术做储备。
Andfix最核心的部分就是方法的替换,把原出Bug的方法A替换为修复后的方法B,从而达到线上修复的目的。
因此,为了更好的了解Andfix实现原理,我们干脆直接模拟Andfix做个热修复的demo,尽量把涉及到的知识点都描述清楚,好让各位看官看得愉悦。
步骤一:找出产生错误的类,新建一个包来存放修复好的类,然后针对新增的类重新生成dex,最后把dex上传到服务器,app下载到sd卡后加载(这里下载省略,方便测试,直接放到手机sd卡里面)。
1.先修复问题(情景:线上app出现问题或者有一天我突然发现线上app猫的叫声太粗狂了,想立马改成其他低调的叫声...)。
发生错误的类或者想改的类叫Cat类,它放在com.andfixjni.demo包里。
//原错误的类
public class Cat {
public int call(){
//随便弄个错误
int i=0;
int j = 100;
Log.e("Cat","汪汪汪");
int result = j/i;
return result;
}
}
把错误的类修复,然后放在com.andfixjni.demo.ok包里。
public class Cat {
public int call(){
int i=1;
int j = 100;
Log.e("Cat","妙妙妙");
int result = j/i;
return result;
}
}
2.如何生成修复dex
编译后,可以在build\intermediates\classes
找到修复好的类,复制com目录到项目下新建目录的andfix_repair里,只保留com\alipay\euler\andfix\ok
目录,其他文件都删除。
找到Sdk\build-tools\26.0.0\dx.bat
文件(比如我的路径是E:\Android\Sdk\build-tools\26.0.0\dx.bat
),打开cmd,先cd到dx.bat
的目录,然后执行这语句:
dx --dex --output E:\github\AndFix-master\andfix_repair\out.dex E:\github\AndFix-master\andfix_repair
dex参数是打包为dex文件
output参数设置输出路径,这里把dex文件存放到andfix_repair目录下
最后设置要打包class文件的路径
步骤二:加载dex文件,找到修复好的类,接着找回出bug的类,替换掉其中出bug的方法。
1.新增DxManager
类,实现加载dex文件和找出要修复的类
public void loadDex(File dexFilePath){
File optFile = new File(context.getCacheDir(),dexFilePath.getName());
if(optFile.exists()){
optFile.delete();
}
//加载Dex文件
try {
DexFile dexFile = DexFile.loadDex(dexFilePath.getAbsolutePath(),optFile.getAbsolutePath(),Context.MODE_PRIVATE);
//遍历dex的class文件
Enumeration<String> entry = dexFile.entries();
Class<?> clazz = null;
while (entry.hasMoreElements()){
String className = entry.nextElement();
//修复好的类
clazz = dexFile.loadClass(className, context.getClassLoader());
if (clazz != null) {
fixClass(clazz);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
2.如何找出错误的类?从andfix源码中发现主要使用了注解的方式。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodReplace {
String clazz();
String method();
}
其中RetentionPolicy.RUNTIME
用在运行时,ElementType.METHOD
用在方法上面。
然后修改前面用到的修复Cat类(ok包下的Cat类)
//要修复的地方class名+要修复的方法
@MethodReplace(clazz = "com.andfixjni.demo.Cat",method = "call")
public int call(){...}
最后通过注解方式来获取到要修复的类和方法
private void fixClass(Class<?> clazz) {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
//拿到要修复的注解
MethodReplace methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
String wrongClazzName = methodReplace.clazz();
String wrongMethodName = methodReplace.method();
if (!isEmpty(wrongClazzName) && !isEmpty(wrongMethodName)) {
replaceMethod(wrongClazzName, wrongMethodName, method);
}
}
}
private void replaceMethod(String wrongClazzName, String wrongMethodName, Method rightMethod) {
try {
Class wrongClass = Class.forName(wrongClazzName);
//最终拿到错误的method对象
Method wrongMethod = wrongClass.getMethod(wrongMethodName,rightMethod.getParameterTypes());
//走jni层去进行方法替换修复
replaceMethod(wrongMethod,rightMethod);
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
步骤三:调用jni层来进行方法替换
1.把前面获取到的错误的方法和正确的方法作为参数传给jni层,首先先在DxManager
类里面初始化库文件和添加jni方法。
static {
//加载库文件
System.loadLibrary("andfix");
}
public native void replaceMethod(Method wrongMethodName, Method rightMethod);
以下是C语言代码(文件是andfix.cpp)
JNIEXPORT void JNICALL Java_com_andfixjni_demo_DxManager_replaceMethod
(JNIEnv *env, jobject /* this */, jobject src, jobject dest){
//拿到错误的class
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_; //for plugin classloader
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ = reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_-1;
//for reflection invoke
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->dex_cache_strings_ = dmeth->dex_cache_strings_;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->gc_map_ = dmeth->gc_map_;
smeth->entry_point_from_jni_ = dmeth->entry_point_from_jni_;
smeth->entry_point_from_quick_compiled_code_ = dmeth->entry_point_from_quick_compiled_code_;
smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;
smeth->method_index_ = dmeth->method_index_;
}
上面代码主要在jvm虚拟内存的方法区里把原Cat类的方法表指针替换为修复Cat类的方法表指针,这样虚拟机在栈里面会加载修复Cat类的方法来压栈,从而达到修复。(为了这句话,翻了几天java内存相关的资料才总结出来,具体可移步这里)。
编译后运行效果:(突然觉得改成妙妙妙的叫声比较低调)
献上源码一枚,各位看官轻拍:
源码
Andfix存在的问题:
由于ArtMethod结构体在各个厂商下会做改动,因此这种强制性转化的方式在这些厂商下面会导致失败,当然这种也是有解决的方案,具体可以参考该文章Android热修复升级探索——追寻极致的代码热替换。
项目运行环境:
使用Genymotion模拟器的Custom Phone 5.0测试,
使用ndk build来编译,具体如何配置请移步Android NDK 介绍与使用示例
由于用的是5.0的机子,因此jni层只保留使用5.0版本的方法替换,运行本demo时,需跟我当前的环境保持一致。
环境的一些问题:
1.如何编译c或cpp文件,在android studio里进入sdk manager,勾选Cmake、llb、NDK,下载完后即可
2.sdk manager里面找不到CMake?需用64位的Android Studio才行
3.运行CMake时候出现下面问题:
CMake Error in CMakeLists.txt:
The CMAKE_CXX_COMPILER:
E:/Android/SDK/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64/bin/clang++.exe is not a full path to an existing compiler tool.
解决方法请参考该文章安卓Cmake一处错误解决