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"); 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()) { Log.e(TAG, "patch dir create error."); return; } else if (!mPatchDir.isDirectory()) { 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); 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[] = { { "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 –