Andfix源码分析

Andfix源码分析

缩写:

ART

Andfix是阿里巴巴推出的一款基于Method Hook的热修复技术,目前Github点赞数5.7K,是一款安全性高,较为稳定,性能比较优异的方法级替换的热修复技术。代码实现上条理清晰,架构设计合理,可读性强,是一个实现上非常优雅的开源框架。下面我们重点介绍下Andfix的源码及其设计。

一个经典的开源框架首先要友好的对外暴露接口,这样才能更便于接入,实现快速启动。所以,在介绍核心源码之前,我们首先关注下Andfix的外部接口部分。

MainApplication

为了尽可能的覆盖BUG修复的范围,和其他的热修复技术一样,Andfix选择在APP启动的时候对热补丁进行加载,也即Application的OnCreate过程。整体的外部接口调用如下所示:

@Override

public void onCreate() {

super.onCreate();

// patch的初始化

mPatchManager = new PatchManager(this);

mPatchManager.init("1.0");

Log.d(TAG, "inited.");

// 加载缓存中的patch

mPatchManager.loadPatch();

Log.d(TAG, "apatch loaded.");

// 将外部存储中的patch加载到当前运行的ART中

try {

// .apatch file path

String patchFileString = Environment.getExternalStorageDirectory()

.getAbsolutePath() + APATCH_PATH;

mPatchManager.addPatch(patchFileString);

Log.d(TAG, "apatch:" + patchFileString + " added.");

} catch (IOException e) {

Log.e(TAG, "", e);

}

}

这部分接口非常简洁,大概分为三步:patch的初始化,patch的缓存加载,patch的外部存储加载。

缓存加载是为了加载之前已经从外部存储载入到缓存(data目录下)中的patch,外部存储加载是为了从外部存储中加载patch到缓存。Andfix的整体外部调用就是上面的几步,下面我们来看下Andfix的具体实现部分。

Andfix的具体实现上主要分为三部分:Patch管理部分,Fix管理部分,Native Hook部分,其整体的UML架构图如下所示:

PatchManager

整体的初始化函数的源码如下:

/**

* Patch的初始化工作

* @param appVersion App的版本号

*/

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();

}

}

其中mPatchDir表示data私有目录下存放Patch文件的文件夹。首先是关于mPatchDir的简单文件夹操作,在mPatchDir文件夹初始化完成之后,紧接着比较当前的APP版本和SharedPreferences中保存的Patch对应的APP版本,两者如果不相等的话,会直接清除掉本地缓存的Patch文件和对应Patch相关的数据。这是因为热补丁是跟APP强相关的,Patch只能精确的修复对应版本的Bug。清除的源码如下所示:

private void cleanPatch() {

File[] files = mPatchDir.listFiles();

for (File file : files) {

mAndFixManager.removeOptFile(file);

if (!FileUtil.deleteFile(file)) {

Log.e(TAG, file.getName() + " delete error.");

}

}

}

在版本号匹配之后,紧接着是Patch文件的初始化部分(initPatchs()),其源码如下:

private void initPatchs() {

File[] files = mPatchDir.listFiles();

for (File file : files) {

addPatch(file);

}

}

在上述函数中,ART会遍历Patch文件,并将Patch文件通过addPatch方法添加到内存中。

addPatch方法有两种多态实现,分别如下:

  • private Patch addPatch(File file)

  • public void addPatch(String path) throws IOException

其中第一个方法是从Patch文件中获取Patch对象,源码如下:

具体的源码如下:

/**

* add patch file

*

* @param file

* @return patch

*/

private Patch addPatch(File file) {

Patch patch = null;

if (file.getName().endsWith(SUFFIX)) {

try {

patch = new Patch(file);

mPatchs.add(patch);

} catch (IOException e) {

Log.e(TAG, "addPatch", e);

}

}

return patch;

}

此方法中把Patch文件夹映射为Patch对象,然后将Patch对象统一存放在mPatchs数据集里面。

第二个方法是从本地路径中获取Patch文件,然后从Patch文件中解析出Patch对象,之后触发Patch的加载过程,具体源码如下:

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中的内容进行Hotfix。此过程是通过Patchmanager类中的loadPatch方法实现的。loadPatch方法一共有三个多态,分别如下:

  • public void loadPatch(String patchName, ClassLoader classLoader)

  • public void loadPatch()

  • private void loadPatch(Patch patch)

三个方法入参不同,会通过不同的ClassLoader加载不同的Patch文件,已第三个方法为例,该函数中对数据进行封装之后,最终会循环调用AndfixManager中的fix方法,具体的源码如下:

private void loadPatch(Patch patch) {

Set<String> patchNames = patch.getPatchNames();

ClassLoader cl;

List<String> classes;

for (String patchName : patchNames) {

if (mLoaders.containsKey("*")) {

cl = mContext.getClassLoader();

} else {

cl = mLoaders.get(patchName);

}

if (cl != null) {

classes = patch.getClasses(patchName);

mAndFixManager.fix(patch.getFile(), cl, classes);

}

}

}

PatchManager的源码基本就如上所述,主要是对Patch的管理与加载过程,代码简洁易懂,可读性强。

接下来,我们重点分析下AndfixManager类,该类中主要介绍Andfix的BugFix的核心流程。通过之前的PatchManager类的源码分析可知,AndfixManager的关键入口函数为fix方法。其源码如下所示:

public synchronized void fix(File file, ClassLoader classLoader, List<String> classes) {

if (!mSupport) {

return;

}

if (!mSecurityChecker.verifyApk(file)) {// security check fail

return;

}

try {

File optfile = new File(mOptDir, file.getName());

boolean saveFingerprint = true;

if (optfile.exists()) {

// need to verify fingerprint when the optimize file exist,

// prevent someone attack on jailbreak device with

// Vulnerability-Parasyte.

// btw:exaggerated android Vulnerability-Parasyte

// http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html

if (mSecurityChecker.verifyOpt(optfile)) {

saveFingerprint = false;

} else if (!optfile.delete()) {

return;

}

}

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;

}

};

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

}

}

} catch (IOException e) {

Log.e(TAG, "pacth", e);

}

}

在此方法中,主要包括安全校验,bugFix两部分,具体如下;

  1. 安全校验

Andfix会对传进来的Patch文件进行安全校验,包括准确性校验和完整性校验。

安全校验的具体实现在SecurityChecker类中,其中准确性校验(签名校验)的具体实现如下:

/**

* @param path

* Apk file

* @return true if verify apk success

*/

public boolean verifyApk(File path) {

if (mDebuggable) {

Log.d(TAG, "mDebuggable = true");

return true;

}

JarFile jarFile = null;

try {

jarFile = new JarFile(path);

JarEntry jarEntry = jarFile.getJarEntry(CLASSES_DEX);

if (null == jarEntry) {// no code

return false;

}

loadDigestes(jarFile, jarEntry);

Certificate[] certs = jarEntry.getCertificates();

if (certs == null) {

return false;

}

return check(path, certs);

} catch (IOException e) {

Log.e(TAG, path.getAbsolutePath(), e);

return false;

} finally {

try {

if (jarFile != null) {

jarFile.close();

}

} catch (IOException e) {

Log.e(TAG, path.getAbsolutePath(), e);

}

}

}

// verify the signature of the Apk

private boolean check(File path, Certificate[] certs) {

if (certs.length > 0) {

for (int i = certs.length - 1; i >= 0; i--) {

try {

certs[i].verify(mPublicKey);

return true;

} catch (Exception e) {

Log.e(TAG, path.getAbsolutePath(), e);

}

}

}

return false;

}

上述过程对APK进行证书签名校验,符合签名的APK为合法的APK,否则为非法的APK,中断热修复过程。

Andfix的过程不仅进行签名校验,还进行完整性校验。完整性校验是为了防止出现在进行patch下载的过程中下载不完整,导致修复出现异常的情况。完整性校验是通过校验MD4来实现的,具体如下;

/**

* @param path

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

}

  1. BugFix

Andfix热修复的核心实现中,分为两个步骤:

  1. 找到需要修复的Class;

  2. 替换需要进行修复的Method。

第一步的具体实现如下:

/**

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

}

}

}

第二部的具体实现如下:

/**

* replace method

*

* @param classLoader classloader

* @param clz class

* @param meth name of target method

* @param method source method

*/

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

}

}

其中核心函数AndFix.addReplaceMethod(src, method)的具体实现如下:

/**

* replace method's body

*

* @param src

* source method

* @param dest

* target method

*

*/

public static void addReplaceMethod(Method src, Method dest) {

try {

replaceMethod(src, dest);

initFields(dest.getDeclaringClass());

} catch (Throwable e) {

Log.e(TAG, "addReplaceMethod", e);

}

}

可以观察到,Andfix中函数的替换是通过Native方法replaceMethod(Method dest, Method src)实现的。从JNI中找到这部分的源码如下:

static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,

jobject dest) {

if (isArt) {

art_replaceMethod(env, src, dest);

} else {

dalvik_replaceMethod(env, src, dest);

}

}

发现对于不同的虚拟机,调用的Native方法是不一样的,ART(Android Running Time)虚拟机调用的是art_replaceMethod(env, src, dest)方法;Dalvik虚拟机调用的是dalvik_replaceMethod(env, src, dest)方法。

对于ART虚拟机,不同Android API的系统,会适配不同的实现,其代码如下:

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

}

}

不同API的实现类如下:

Andfix源码分析

所以说Andfix可以兼容到Android 7.0,对于超过Android7.0的版本,目前使用的替换方法和7.0保持一致。

具体的替换实现如下所示,以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_);

}

评价

总结

本文对Andfix的原理进行了分析介绍,并对Andfix客户端的源码实现进行了简要分析,其中重点介绍了客户端在获取Patch后进行Class匹配与Method替换的过程。

初次此外,在开发过程中,有几个技术细节也有较大的可挖掘性,具体如下:

  1. 热修复Patch的生成过程;

  2. Patch的下载流程,更新,版本管理;

  3. MultiDex下的Andfix;

  4. ClassLoader的内核原理;

  5. Android Running Time与Dalvik。

相关推荐