热修复主要有三个步骤:
1 生成差异补丁
2 加载差异补丁
3 替换方法
1.1 生成差异补丁
阿里提供的差量补丁生成工具https://github.com/alibaba/AndFix/raw/master/tools/apkpatch-1.0.3.zip
2.1 加载差异包
判断版本 如果是当前版本,加载所有的差量包,否则删除所有的差量包
public void init(String appVersion) {
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, "patch dir create error.");
return;
} else if (!mPatchDir.isDirectory()) {// not directory
mPatchDir.delete();
return;
}
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);
String ver = sp.getString(SP_VERSION, null);
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
cleanPatch();
sp.edit().putString(SP_VERSION, appVersion).commit();
} else {
initPatchs();
}
}
这里的initPatchs 会把本地的补丁全部add到PatchManager 的变量mPatchs中然后在loadPatch方法中加载所有的补丁
/**
* load patch,call when application start
*
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set<String> patchNames;
List<String> classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
接下来我们来看下mAndFixManager.fix() 的方法是怎么实现的
关键代码如下
ClassLoader patchClassLoader = new ClassLoader(classLoader) {
@Override
protected Class<?> findClass(String className)
throws ClassNotFoundException {
Class<?> clazz = dexFile.loadClass(className, this);
if (clazz == null&& className.startsWith("com.alipay.euler.andfix")) {
return Class.forName(className);// annotation’s class
// not found
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
Enumeration<String> entrys = dexFile.entries();
Class<?> clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
clazz = dexFile.loadClass(entry, patchClassLoader);
if (clazz != null) {
fixClass(clazz, classLoader);
}
}
通过classLoader找到补丁中需要替换的class,然后调fixClass方法,来完成方法的替换
下面我们在看下fixClass是怎么实现的:
/**
* fix class
*
* @param clazz
* class
*/
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
if (!isEmpty(clz) && !isEmpty(meth)) {
replaceMethod(classLoader, clz, meth, method);
}
}
}
通过apkpatch生成的补丁,在需要替换的方法上加上@MethodReplace的注解,找到注解的方法,然后再调用replaceMethod就可以实现方法的替换了
3.1 方法的替换
继续贴源码
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
Class<?> clazz = mFixedClass.get(key);
if (clazz == null) {// class not load
Class<?> clzz = classLoader.loadClass(clz);
// initialize target class
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
上面的mFixClass 的put 和get方法是为了减少多次重复的loadClass,关键方法是addReplaceMethod这个方法的具体实现是用native方法做的。
下面重点讲下这个方法的实现原理。这里需要引入一个概念ArtMethod,说到ArtMethod 不得不说Art,在Android4.4 之前的运行时环境是Dalvik虚拟机,之后的是Android Runtime ,Android 中的每个方法在art中都对应着一个ArtMethod结构体,用面向对象的说法就是,每个方法其实都是一个ArtMethod对象,在该对象中保存着方法的类,访问权限,执行地址等,所以如果要替换方法,其实就要把该方法的所有ArtMethod对象的属性全部替换成另外一个就可以了。
AndFix 中实现ArtMethod方法的替换是这样实现的
extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
if (apilevel > 23) {
replace_7_0(env, src, dest);
} else if (apilevel > 22) {
replace_6_0(env, src, dest);
} else if (apilevel > 21) {
replace_5_1(env, src, dest);
} else if (apilevel > 19) {
replace_5_0(env, src, dest);
}else{
replace_4_4(env, src, dest);
}
}
我们可以看到,在不同Android版本的Art虚拟机里,Java对象对应底层的数据结构是不同的,因此需要根据不同版本分别处理,分别替换不同的函数。
以replace_7_0 为例
void replace_7_0(JNIEnv* env, jobject src, jobject dest) {
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*>(smeth->declaring_class_)->class_loader_ =
// reinterpret_cast<art::mirror::Class*>(dmeth->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_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->method_index_ = dmeth->method_index_;
smeth->hotness_count_ = dmeth->hotness_count_;
smeth->ptr_sized_fields_.dex_cache_resolved_methods_ =
dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;
smeth->ptr_sized_fields_.dex_cache_resolved_types_ =
dmeth->ptr_sized_fields_.dex_cache_resolved_types_;
smeth->ptr_sized_fields_.entry_point_from_jni_ =
dmeth->ptr_sized_fields_.entry_point_from_jni_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
LOGD("replace_7_0: %d , %d",
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
}
通过env->FromReflectedMethod,可以由Java层的Method对象得到这个方法对应Native层的ArtMethod的真正起始地址。然后替换这个ArtMethod中的所有成员变量就完成了热修复逻辑。以后调用这个方法时就会直接走到新方法的实现中了。这种方法虽然可以实现,但是在特殊情况下回出现替换失败的情况,如厂商对ArtMethod就行了修改。
网上有大神提供了优化方案,就是对ArtMethod进行整体替换
也就是把原先这样的逐一替换:

更改为整体替换:

因此Andfix这一系列繁琐的替换:
target->declaring_class_ = meth->declaring_class_;
target->access_flags_ = meth->access_flags_;
target->dex_code_item_offset_ = meth->dex_code_item_offset_;
target->dex_method_index_ = meth->dex_method_index_;
target->method_index_ = meth->method_index_;
target->hotness_count_ = meth->hotness_count_;
...
更改为:
memcpy(target,meth, sizeof(ArtMethod));
这其实也就是Sophix实现的热修复方案。这样即使ArtMethod被改动,只要我们知道了ArtMethod的size,也就依然可以实现方法的替换。在Art虚拟机中,每个类的ArtMethod在内存中是紧密排列在一起的,所以要拿到ArtMethod的size其实也是很简单的,只要我们构建一个类,其中包含两个静态方法,我们来比较这两个方法的起始地址差值,就可以拿到ArtMethod的大小了。
public class MeasureArtMethodSize {
public static final void M1() {
}
public static final void M2() {
}
}
具体c代码的实现
JNIEXPORT jlong JNICALL
Java_com_example_edz_myapplication_hotfix_HotFixManager_measureMethodSize(JNIEnv *env,
jobject instance,
jclass c) {
size_t firMid = reinterpret_cast<size_t>(env->GetStaticMethodID(c, "M1", "()V"));
size_t nexMid = reinterpret_cast<size_t>(env->GetStaticMethodID(c, "M2", "()V"));
return nexMid - firMid;
}
通过比较两个方法的内存地址差异就可以拿到artMethod的大小了,接下来替换方法也就简单了
JNIEXPORT void JNICALL
Java_com_example_edz_myapplication_hotfix_HotFixManager_replaceMethod(JNIEnv *env, jobject instance,
jobject oldMethod,
jobject newMethod,
jlong size) {
jmethodID smeth = env->FromReflectedMethod(oldMethod);
jmethodID dmeth = env->FromReflectedMethod(newMethod);
memcpy(smeth, dmeth, size);
}
通过memcpy 就可以实现方法的替换了。以上方法其实可以使用纯java来实现
Java中有个隐藏的类sun.misc.Unsafe,通过这个类可以实现获取内存地址,和memcpy的方法,具体实现方式大家可以自行去查阅,网上实现方法很好找。
网友评论