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两部分,具体如下;
- 安全校验
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;
}
- BugFix
Andfix热修复的核心实现中,分为两个步骤:
- 找到需要修复的Class;
- 替换需要进行修复的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可以兼容到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替换的过程。
初次此外,在开发过程中,有几个技术细节也有较大的可挖掘性,具体如下:
- 热修复Patch的生成过程;
- Patch的下载流程,更新,版本管理;
- MultiDex下的Andfix;
- ClassLoader的内核原理;
- Android Running Time与Dalvik。