美文网首页
Andfix源码分析

Andfix源码分析

作者: 码农小龙 | 来源:发表于2019-05-30 10:08 被阅读0次

热修复主要有三个步骤:

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的方法,具体实现方式大家可以自行去查阅,网上实现方法很好找。

相关文章

网友评论

      本文标题:Andfix源码分析

      本文链接:https://www.haomeiwen.com/subject/pnfmtctx.html