AndFix的原理是在加载补丁文件后,通过Native层使用指针替换的方式将老方法Method对象的方法指针替换成补丁包中新方法的,从而达到修复bug的目的。
AndFix具体的使用方法就不多介绍了,大家可以参考这篇文章,接下来我们直接从代码角度来分析整个修复的过程。
首先,进行热修复要有补丁包,AndFix的补丁包是由apkpatch工具生成的。apkpatch是一个jar包,将bug修复后的新apk包和已发布的线上apk包放在一起,通过这个jar工具可以生成查分补丁,但阿里并没有开源出来,我们可以用JD-GUI看下它的源码,版本1.0.3。
首先找到Main.class,位于com.euler.patch包下,找到Main()方法
public static void main(final String[] args) {
.....
//根据脚本命令输入拿到参数
final ApkPatch apkPatch = new ApkPatch(from, to, name, out, keystore, password, alias, entry);
apkPatch.doPatch();
}
ApkPatch类的doPatch方法
public void doPatch() {
try {
//生成smali文件夹
final File smaliDir = new File(this.out, "smali");
if (!smaliDir.exists()) {
smaliDir.mkdir();
}
//新建diff.dex文件
final File dexFile = new File(this.out, "diff.dex");
//新建diff.apatch文件
final File outFile = new File(this.out, "diff.apatch");
//第一步,拿到两个apk文件对比,对比信息写入DiffInfo
final DiffInfo info = new DexDiffer().diff(this.from, this.to);
//第二步,将对比结果info写入.smali文件中,然后打包成dex文件
this.classes = buildCode(smaliDir, dexFile, info);
//第三步,将生成的dex文件写入jar包,并根据输入的签名信息进行签名,生成diff.apatch文件
this.build(outFile, dexFile);
//第四步,将diff.apatch文件重命名,结束
this.release(this.out, dexFile, outFile);
}
catch (Exception e2) {
e2.printStackTrace();
}
}
以上可以简单描述为两步
DexDiffer().diff()
方法
public DiffInfo diff(final File newFile, final File oldFile) throws IOException {
//提取新apk的dex文件
final DexBackedDexFile newDexFile = DexFileFactory.loadDexFile(newFile, 19, true);
//提取旧apk的dex文件
final DexBackedDexFile oldDexFile = DexFileFactory.loadDexFile(oldFile, 19, true);
final DiffInfo info = DiffInfo.getInstance();
boolean contains = false;
for (final DexBackedClassDef newClazz : newDexFile.getClasses()) {
final Set oldclasses = oldDexFile.getClasses();
for (final DexBackedClassDef oldClazz : oldclasses) {
if (newClazz.equals(oldClazz)) {
//对比class文件的变量
this.compareField(newClazz, oldClazz, info);
//对比class文件的方法,如果同一个类中没有相同的方法
//则判定为新增方法
this.compareMethod(newClazz, oldClazz, info);
contains = true;
break;
}
}
if (!contains) {
//否则是新增的类
info.addAddedClasses(newClazz);
}
}
//返回包含diff信息的DiffInfo对象
return info;
}
其原理就是遍历两个apk文件的class来做差别信息。对比方法的过程中对比两个dex文件中同时存在的方法,如果方法实现不同则存储为修改过的方法;如果方法名不同,存储为新增的方法。
之后,将得到的diff信息写入smali文件,并且生成diff.dex文件。smali文件的命名以_CF.smali结尾,并且在修改的地方用自定义的Annotation(MethodReplace)标注,用于在替换之前查找修复的变量或方法,举个例子如下。
.method private getUserProfile()V
.locals 2 #表示需要申请2个本地寄存器
.annotation runtime Lcom/alipay/euler/andfix/annotation/MethodReplace;
clazz = "com.abc.account.UserProfileActivity"
method = "getUserProfile"
.end annotation
编译成Java是这样的:
//生成的注解
@MethodReplace(clazz="com.abc.account.UserProfileActivity", method="getUserProfile")
private void getUserProfile() {
...
}
然后就是签名,打包,加密的流程,就不具体分析了。注意,apkPatch在生成.apatch补丁文件的时候会加入签名信息,并且会进行加密操作,在加载补丁的时候AndFix会验证签名信息是否正确。
补丁包下发给客户端后,AndFix就可以加载了,AndFix
加载补丁有两种情况,一种是程序启动前补丁文件已经存在本地了,就在AndFix执行初始化完毕后加载补丁。另一种,程序启动之后(AndFix初始化之后)才开始下载补丁文件,待下载成功后,才去加载。
两种情况的区别仅仅是加载补丁的时机不同,加载前都要对AndFix
进行初始化操作,加载的最终逻辑也都会走到com.alipay.euler.andfix.patch.PatchManager.loadPatch()
方法中,loadPatch
方法有三个重载实现,一个在AndFix初始化后调用,一个在下载补丁成功后调用,一个提供了自定义类加载器的实现。
见代码:
//PatchManager.java
/**
* load patch,call when plugin be loaded. used for plugin architecture.
*
* need name and classloader of the plugin
*
* @param patchName
* patch name
* @param classLoader
* classloader
*/
public void loadPatch(String patchName, ClassLoader classLoader) {
mLoaders.put(patchName, classLoader);
Set patchNames;
List classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
if (patchNames.contains(patchName)) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), classLoader, classes);
}
}
}
/**
* 应用程序启动时调用
* load patch,call when application start
*
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set patchNames;
List classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
/**
* load specific patch
* 补丁文件下载好后调用,参数是补丁文件
* @param patch
* patch
*/
private void loadPatch(Patch patch) {
Set patchNames = patch.getPatchNames();
ClassLoader cl;
List 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);
}
}
}
不管调用的机制如何,最终都是会走到AndFixManager.fix(File file, ClassLoader classLoader,
方法
List
见代码:
/**
* fix
*
* @param file
* patch file
* @param classLoader
* classloader of class that will be fixed
* @param classes
* classes will be fixed
*/
public synchronized void fix(File file, ClassLoader classLoader,
List 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;
}
}
//将下载路径的pathch文件加载到data/data/packagename/file/ 目录下,并返回一个DexFile对象
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);
if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}
// 自定义一个类加载器,重写findClass方法
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
// 没有找到类并且className以“com.alipay.euler.andfix”开头,则通过反射返回该类的实例
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
Enumeration entrys = dexFile.entries();
Class clazz = null;
//遍历patch文件中的所有Entry
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
// 加载patch中的类
clazz = dexFile.loadClass(entry, patchClassLoader);
if (clazz != null) {
// 对这个class进行修复
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}
private void fixClass(Class clazz, ClassLoader classLoader) {
// 反射找到该类中的所有方法
Method[] methods = clazz.getDeclaredMethods();
// MethodReplace是个注解
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
//遍历该类的所有方法,找到有MethodReplace注解的就是需要替换的方法
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
//找到注解中标注的clazz和method属性后调用replaceMethod
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();
// 根据key,查找缓存中的数据,该缓存记录了已经被修复过的class对象。
Class clazz = mFixedClass.get(key);
if (clazz == null) {// class not load
//找不到则表示该class没有被修复过,则通过类加载器去加载。
Class clzz = classLoader.loadClass(clz);
// initialize target class
// 通过C层,改写accessFlags,把需要替换的类的所有方法(Field)改成了public
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
// 反射得到修复前老的Method对象
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
最终,AndFix.addReplaceMethod()调用了native的replaceMethod方法。
/**
* 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);
}
}
private static native void replaceMethod(Method dest, Method src);
可以看到,最后把老方法的Method对象和新方法的Method对象作为参数传入了C层,Java层的代码到此结束,接下来看Native层
//andfix.cpp
static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,
jobject dest) {
if (isArt) {
art_replaceMethod(env, src, dest);
} else {
dalvik_replaceMethod(env, src, dest);
}
}
看到dalvik和art的替换逻辑不一样,接下来,我们分析art的
//art_method_replace.cpp
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);
}
}
看到根据系统版本又分出了几个分支,我们看5.0的
//art_method_replace_5_0.cpp
void replace_5_0(JNIEnv* env, jobject src, jobject dest) {
// 通过jni.h中的FromReflectedMethod方法反射得到源方法和替换方法的ArtMethod指针(ArtMethod的数据结构定义在头文件中,接下来会分析)
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
// 替换方法所在类的类加载器
dmeth->declaring_class_->class_loader_ =
smeth->declaring_class_->class_loader_; //for plugin classloader
// 替换用于检查递归调用的线程id
dmeth->declaring_class_->clinit_thread_id_ =
smeth->declaring_class_->clinit_thread_id_;
// 把目标方法所在类的初始化状态值设置成源方法的状态值-1
dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;
// 把原方法的各种属性都改成补丁方法的
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->access_flags_ = dmeth->access_flags_;
smeth->frame_size_in_bytes_ = dmeth->frame_size_in_bytes_;
smeth->dex_cache_initialized_static_storage_ =
dmeth->dex_cache_initialized_static_storage_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->vmap_table_ = dmeth->vmap_table_;
smeth->core_spill_mask_ = dmeth->core_spill_mask_;
smeth->fp_spill_mask_ = dmeth->fp_spill_mask_;
smeth->mapping_table_ = dmeth->mapping_table_;
smeth->code_item_offset_ = dmeth->code_item_offset_;
// 最重要的两个方法指针替换,下面两个entry_point指针代表了ART运行时执行方法的两种模式(compiled_code,interpreter),Andfix根据方法不同的调用机制通过这两个指针做方法替换
//方法执行方式为本地机器指令的指针入口
smeth->entry_point_from_compiled_code_ =
dmeth->entry_point_from_compiled_code_;
// 方法执行方式为解释执行的指针入口
smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;
smeth->native_method_ = dmeth->native_method_;
smeth->method_index_ = dmeth->method_index_;
smeth->method_dex_index_ = dmeth->method_dex_index_;
LOGD("replace_5_0: %d , %d", smeth->entry_point_from_compiled_code_,
dmeth->entry_point_from_compiled_code_);
}
在头文件art_5_0.h
中找到了ArtMethod
的定义,也看到了上面代码中替换的所有变量的定义
// art_5_0.h
class ArtMethod: public Object {
public:
// Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
// The class we are a part of
Class* declaring_class_;
// short cuts to declaring_class_->dex_cache_ member for fast compiled code access
void* dex_cache_initialized_static_storage_;
// short cuts to declaring_class_->dex_cache_ member for fast compiled code access
void* dex_cache_resolved_methods_;
// short cuts to declaring_class_->dex_cache_ member for fast compiled code access
void* dex_cache_resolved_types_;
// short cuts to declaring_class_->dex_cache_ member for fast compiled code access
void* dex_cache_strings_;
// Access flags; low 16 bits are defined by spec.
uint32_t access_flags_;
// Offset to the CodeItem.
uint32_t code_item_offset_;
// Architecture-dependent register spill mask
uint32_t core_spill_mask_;
// compiled_code调用方式,本地机器指令入口
// Compiled code associated with this method for callers from managed code.
// May be compiled managed code or a bridge for invoking a native method.
// TODO: Break apart this into portable and quick.
const void* entry_point_from_compiled_code_;
// 通过interpreter方式调用方法 解释执行入口
// Called by the interpreter to execute this method.
void* entry_point_from_interpreter_;
// Architecture-dependent register spill mask
uint32_t fp_spill_mask_;
// Total size in bytes of the frame
size_t frame_size_in_bytes_;
// Garbage collection map of native PC offsets (quick) or dex PCs (portable) to reference bitmaps.
const uint8_t* gc_map_;
// Mapping from native pc to dex pc
const uint32_t* mapping_table_;
// Index into method_ids of the dex file associated with this method
uint32_t method_dex_index_;
// For concrete virtual methods, this is the offset of the method in Class::vtable_.
//
// For abstract methods in an interface class, this is the offset of the method in
// "iftable_->Get(n)->GetMethodArray()".
//
// For static and direct methods this is the index in the direct methods table.
uint32_t method_index_;
// The target native method registered with this method
const void* native_method_;
// When a register is promoted into a register, the spill mask holds which registers hold dex
// registers. The first promoted register's corresponding dex register is vmap_table_[1], the Nth
// is vmap_table_[N]. vmap_table_[0] holds the length of the table.
const uint16_t* vmap_table_;
static void* java_lang_reflect_ArtMethod_;
};
}
代码分析到这里,思路也大致有了,补丁被andfix
框架加载后,先通过一系列的校验和解析,最终会得到两个Method
对象(源方法和目的方法)传给jni
层,jni
层根据虚拟机版本去实现相应的替换逻辑,参照art5.0版本的代码,发现andfix通过ArtMethod指针将源方法Method对象的成员变量转换成为新方法的成员变量,从而实现方法替换的功能。(我的个人理解,如有错误还请指正)
而上面说的ART执行方法的两种方式是什么呢?
ART
虚拟机引入了AOT(Ahead Of Time)
机制,也就是在app运行之前(程序安装时),系统会把apk包中的classes.dex
文件通过工具dex2oat
翻译成本地机器指令,最终得到一个ELF格式的OAT
文件。app启动时会将OAT
文件加载到内存中,当ART需要执行某个方法时,就可以直接在OAT
中查找该方法对应的本地机器指令来执行。
上面说的ART运行方法是直接将其本地机器指令交给CPU执行,是默认的执行方式。ART另一种执行方法的方式是像Dalvik虚拟机一样,将其DEX字节码交给解释器执行。
而具体的执行方案,是由dex字节码翻译成本地机器指令的后端所决定的(分为Quick类型和Portable类型,默认Quick类型)。Quick类型对应本地机器指令,Portable类型对应解释执行。
这两种方法调用的方案正好对应了刚才代码中ArtMethod
类的两个entry_point指针(entry_point_from_compiled_code_
,entry_point_from_interpreter_
)
但是,我的疑问来了,在默认情况下,app安装时已经在OAT文件中将老方法对应的本地机器指令生成了,那么ART要执行AndFix替换后的新方法必然会去找新方法对应的本地机器指令,那么新方法对应的机器指令是如何生成的呢?
带着问题,我又去研究了一下ART加载OAT文件的过程。发现一个应用程序的classes.dex文件经过ART运行时编译后,得到一个OAT主执行文件。不过,该应用程序也可以在运行时动态加载dex文件(热修复加载补丁)。这些动态加载的dex文件在加载的时候同样会被翻译成OAT再运行,但动态加载的dex文件不属于主执行文件。
如果我参考的资料没有错误的话,就意味着补丁包中的dex文件在被加载时就被ART翻译成OAT文件,新的方法也生成了对应的本地机器指令。那么我们在ART上使用AndFix进行热修复替换方法时,因为替换了Method对象中的成员变量(entry_point
指针),所以新方法所指向的本地机器指令也一起被替换了。
AndFix热补丁原理就是,通过加载差分补丁,把需要替换的方法注入到native层,然后通过替换新老方法的函数指针,从而达到bug修复目的,但需要注意的是,因为Andfix是动态的,跳过了类的初始化,所以对于静态方法、静态成员变量、构造方法或者class.forname()的处理可能会有问题,也不支持新增成员变量和修改成员变量。
参考资料
Android运行时ART加载OAT文件的过程分析
Android运行时ART加载类和方法的过程分析
Android运行时ART执行类方法的过程分析
Android热补丁之AndFix原理解析
各大热补丁方案分析和比较
AndFix项目地址