文章目录
  1. 1. AndFix加载补丁包过程分析
    1. 1.1. 一、解析补丁包
    2. 1.2. 二、加载补丁包中的类
    3. 1.3. 三、安全校验
    4. 1.4. 四、Dalvik虚拟机方法替换
    5. 1.5. 五、ART虚拟机方法替换

AndFix加载补丁包过程分析

andfix提供了一套可以热替换java中原有方法的工具,在实际应用中可以用来热替换修复出现bug的方法,从而使用应用不需要升级应用就能解决部分问题。

本文主要是分析运行在android应用中的一套工具环境,从java层到native层分析andfix框架热替换方法的过程。

一、解析补丁包

首先来看andfix初始化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

import com.alipay.euler.andfix.patch.PatchManager;

public class App extends Application {

public void onCreate() {
super.onCreate();

String patchPath = "";
PatchManager patchManager = new PatchManager(getApplicationContext());
patchManager.init("1.0");//current version

patchManager.addPatch(patchPath);
patchManager.loadPatch();
}
}

一般这段初始化代码是放在Application的onCreate()方法中,原因是应用启动时,尽早替换出现问题的方法,等到方法执行时再替换就无意义了。

在PatchManager对象调用init(String)方法时,传入的版本号值很关键。如果是初次初始化或者版本号发生变化,那么会清空原来已经缓存的patch包,同时会缓存新的版本号;如果不是,则会加载已经缓存过的patch包并解析成Patch对象缓存到集合中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

/**
* initialize
*
* @param appVersion
* App version
*/
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();
}
}

addPatch(String)的功能是加载patch包,并将patch包缓存到应用的/data/data/packageName/files/apatch/目录下。如果该缓存目录下已存在,则不会重复缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

/**
* add patch at runtime
*
* @param path
* patch path
* @throws IOException
*/
public void addPatch(String path) throws IOException {
File src = new File(path);
File dest = new File(mPatchDir, src.getName());
if(!src.exists()){
throw new FileNotFoundException(path);
}
if (dest.exists()) {
Log.d(TAG, "patch [" + path + "] has be loaded.");
return;
}
FileUtil.copyFile(src, dest);// copy to patch's directory
Patch patch = addPatch(dest);
if (patch != null) {
loadPatch(patch);
}
}

patch包解析构造成Patch类对象时,主要是解析patch包中的META-INF/PATCH.MF文件,将该文件中的Patch-Classes项对应的值解析出来,在前面 apkpatch工具实现分析 一文中有介绍,这个字段对应的值是将修复的Class以英文逗号分隔组成。

二、加载补丁包中的类

加载补丁包中的类并替换相应的方法是通过调用PatchManager的loadPatch()方法完成的。该方法是将缓存的patch中的Class名称,通过调用AndFixManager类的fix(File, ClassLoader, List)方法转换成对应的Class类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);

if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}

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;
}
};

在fix()方法中会进行patch包的版本支持校验和安全校验,如果不支持或者校验不通过,则不会继续执行。在这个方法中以Patch包的路径为参数创建了DexFile类对象,并构造了ClassLoader类,并重写了其findClass(String)方法,ClassLoader去查找Class的时候,实际上是调用DexFile.loadClass(String, ClassLoader)方法来完成的。

最后是调用DexFile.entries()方法该patch包中所有的类名,将这些已经在Patch-Classes中的类名通过调用DexFile.loadClass(String, ClassLoader)获取对应的Class。

方法替换是通过调用AndFixManager的fixClass(Class, ClassLoader)方法实现,该方法中先找出该Class中的有MethodReplace注解的方法,然后拿出该注解的clz(原方法对应的类名)和meth(原方法名称)字段的值,获取到原方法的Method对象,最后通过执行AndFix.addReplaceMethod(Method, Method)方法调用replaceMethod(Method, Method)方法进而转入native层进行方法替换操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

/**
* 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);
}
}
}

三、安全校验

是否支持方法热替换操作的是通过AndFixManager的mSupport属性判断。该属性是通过调用Compact.isSupport()方法返回的值,在该方法中会判断是否不是阿里云OS系统、调用native中的setup根据不同虚拟机初始化是否成功、判断android系统版本是否是在2.3 ~ 6.0之间。

安全校验分两步:第一是校验patch包的签名是否与主应用的签名一致,如果主应用是调试的签名,也给予通过,这样做是为了调试方便。该过程是在SecurityChecker的verifyApk(File)方法中实现。

第二步验证的是验证patch包中优化过的classes.dex文件,即odex或oat文件,只不过andfix将该生成的文件名修改为与patch包名称相同,放在了apatch_opt目录。该验证过程是通过比较优化后的odex文件的MD5值与上次获取优化后的odex文件缓存的MD5值,如果不相同,则会删除odex文件,重新调用SecurityChecker类的saveOptSig(File)方法缓存新生成的odex文件的MD5值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

/**
* @param file
* Dex file
* @return true if verify fingerprint success
*/
public boolean verifyOpt(File file) {
String fingerprint = getFileMD5(file);
String saved = getFingerprint(file.getName());
if (fingerprint != null && TextUtils.equals(fingerprint, saved)) {
return true;
}
return false;
}

四、Dalvik虚拟机方法替换

java层在调用AndFix类的replaceMethod()方法时,就转入了native层,在native层,该本地方法对应的函数是static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,
jobject dest),中间使用了JNI中的主动注册的方式进行关联。如下代码所示:

1
2
3
4
5
6
7
8
9
10

/*
* JNI registration.
*/
static JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
{ "setup", "(ZI)Z", (void*) setup }, { "replaceMethod",
"(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V",
(void*) replaceMethod }, { "setFieldFlag",
"(Ljava/lang/reflect/Field;)V", (void*) setFieldFlag }, };

在replaceMethod函数中根据当前是DVM还是ART虚拟机,分别调用dalvik_replaceMethod()和art_replaceMethod()函数。

dalvik_replaceMethod()函数的定义是在jni/dalvik/dalvik_method_replace.cpp文件中。其中通过调用JNIEnv的FromReflectedMethod()函数获取java层Method类对象对应native层的Method结构体,然后修改该结构体中的accessFlags和jniArgInfo属性值,使这个java层的普通方法变成一个native方法。同时还修改了insns和nativeFunc属性,insns缓存的是已经修复bug的函数的Method结构体指针;nativeFunc属性值是dalvik_dispatcher函数地址,虚拟机在执行方法时,如果该方法是本地方法就会执行nativeFunc所指向的函数地址,也就是会执行dalvik_dispatcher函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Method* meth = (Method*) env->FromReflectedMethod(src);
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);

meth->jniArgInfo = 0x80000000;
meth->accessFlags |= ACC_NATIVE;

int argsSize = dvmComputeMethodArgsSize_fnPtr(meth);
if (!dvmIsStaticMethod(meth))
argsSize++;
meth->registersSize = meth->insSize = argsSize;
meth->insns = (void*) target;

meth->nativeFunc = dalvik_dispatcher;

在dalvik_dispatcher()函数中,会根据当前替换方法,即修复bug的方法是否是静态方法而分别做处理。这个与java语法相同,如果调用的是非静态方法,则会需要当前方法所在类的对象来调用,如果是静态方法,则只需要类即可。不管是处理哪种类型的方法,都会调用到dvmCallMethod_fnPtr()函数。这个函数是通过使用NDK中的头文件中的相关函数打开android系统中的/system/lib/libdvm.so文件,使用dvmCallMethod名称获取到的函数地址,也就是说调用dvmCallMethod_fnPtr()函数,实际上是调用dvm虚拟机中的dvmCallMethod()函数,这个函数是在android系统源码中的dalvik/vm/interp/Stack.cpp文件中定义。上面获取函数地址的方法可以参见dalvik_setup()函数定义。通过dvmCallMethod()函数名称也能明白,该函数是虚拟机用来执行java层方法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

if (!dvmIsStaticMethod(meth)) {
// 中间代码省略 ...
dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
dvmCreateReflectMethodObject_fnPtr(meth), &result, thisObj,
argArray);

thisObj->clazz = tmp;
} else {
// 中间代码省略 ...
dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
dvmCreateReflectMethodObject_fnPtr(meth), &result, NULL,
argArray);
}

注意,dvmCallMethod()函数传入的第二个参数是一个Method结构体指针,也就是当需要被执行的方法,但这个方法jInvokeMethod是一个java层的java.lang.reflect.Method的invoke()方法,是通过使用JNI的GetMethodID()函数获取到其对应ID作为一个参数,具体是在dalvik_setup()函数中获取;第三个参数才是替换方法,这个参数在dvmCallMethod()函数中作为被执行方法的调用对象。到这里应该能明白,实际上对应java层的调用是执行java.lang.reflect.Method对象的invoke()方法。

在dalvik_dispatcher()函数的最后是处理异常和返回结果。

五、ART虚拟机方法替换

art_replaceMethod()函数是在jni/art/art_method_replace.cpp文件中定义,该函数主要内容是根据ART虚拟机不同的版本,调用不同的函数。下面是具体代码:

1
2
3
4
5
6
7
8
9
10
11

extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
if (apilevel > 22) {
replace_6_0(env, src, dest);
} else if (apilevel > 21) {
replace_5_1(env, src, dest);
} else {
replace_5_0(env, src, dest);
}
}

这几个函数分别是在对应的jni/art/art_method_replace_x_x.cpp文件中定义。因为ART虚拟机还不太成熟,google还在改进中,所以不同版本有些代码会不一样,才会针对不同的版本进行处理。

这里从java层过来的Method对象,被转换成ART虚拟机中能识别的ArtMethod类指针。这些函数中处理都是相同的,将被替换方法和替换方法两个方法的属性值进行互相替换。这里的处理方式与DVM有很大不同,ART中只需要替换相应的属性,不需要进行方法调用,这与ART虚拟机原理有关。其中下面两个属性替换很关键:

1
2
3
4
5

smeth->entry_point_from_compiled_code_ =
dmeth->entry_point_from_compiled_code_;

smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;

entry_point_frominterpreter可以认为是ART虚拟机执行方法的起点,这个指针实际上是一个函数指针,这个函数指针是由汇编实现,这个指针是在art/runtime/class_linker.cc文件中赋值,再往下找,就会找到汇编代码。
entry_point_from_compiledcode这个指针同理,当ART虚拟机执行代码,如果需要Interpreter,就会执行entry_point_frominterpreter指向的函数;如果不需要,则会执行entry_point_from_compiledcode指向的函数。两个函数之间又可以相互调用,这部分都是用汇编代码执行的。具体原理参见CSDN老罗的博客。

– EOF –

文章目录
  1. 1. AndFix加载补丁包过程分析
    1. 1.1. 一、解析补丁包
    2. 1.2. 二、加载补丁包中的类
    3. 1.3. 三、安全校验
    4. 1.4. 四、Dalvik虚拟机方法替换
    5. 1.5. 五、ART虚拟机方法替换