http://tech.dianping.com/posts/368
在iOS上目前已经有不少比较成熟的给APP打补丁的方案如NU框架,但如果去搜索Android上的打补丁方案却找不到很好的结果。抱着试一试的态度,准备探索一下在Android上实现打补丁的方法。
第一个想法是既然Android可以做到动态加载类,那能否通过DexClassLoader来加载补丁中与要替换的类名一致的Class呢?经过试验发现这条路行不通。原因是即便两个类的包名类名方法名等等完全一样,但却是两个不同的ClassLoader加载的,虚拟机也会把他们当做两个不同的类,运行时会报java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation的错误。具体可以参看一下这篇讲Android插件原理的文章。http://www.alloyteam.com/2014/04/android-cha-jian-yuan-li-pou-xi/
第二个想法是看到一篇hack Dalvik虚拟机来实现修改系统API方法的博客而联想到的。首先在Android 5.0之前Dalvik虚拟机是通过libdvm.so库实现的,也就是说把Java的Class加载到虚拟机的过程也是由libdvm.so来完成的。我们可以在C层用dlopen()和dlsym()得到libdvm.so中的函数指针,再去调用其中的方法。
那么libdvm.so是否有什么方法可以让我们利用的呢?有的。 Dalvik虚拟机的实现位于ANDROID_SRC/dalvik/vm目录下,在ANDROID_SRC/dalvik/vm/oo/Class.h中有这样一个方法。
/*
* Find a loaded class by descriptor. Returns the first one found.
* Because there can be more than one if class loaders are involved,
* this is not an especially good API. (Currently only used by the
* debugger and "checking" JNI.)
*
* "descriptor" should have the form "Ljava/lang/Class;" or
* "[Ljava/lang/Class;", i.e. a descriptor and not an internal-form
* class name.
*/
ClassObject* dvmFindLoadedClass(const char* descriptor);
那么我们就可以通过dvmFindLoadedClass方法找到已经加载到虚拟机中的Class,得到一个ClassObject结构体的指针。ClassObject的定义位于ANDROID_SRC/dalvik/vm/oo/Object.h,可以发现其中包含了Class的方法表。
到这里让人不由的激动,有希望了!那么用这种方式实现动态打补丁的思路是: 假设我们希望用Class A中的M1替换Class B中的M2方法。首先用dlopen打开libdvm.so得到方法dvmFindLoadedClass的函数指针p_dvmFindLoadedClass,再用DexClassLoader将补丁里的Class A加载进来,以Class A和需要做替换的Class B的类名作为参数调用p_dvmFindLoadedClass,得到ClassObject* A’ 和 B’, 之后根据M1和M2的方法名分别找到在A’ 和 B’中Method* M1′ 和 M2’,再将M2’指向M1’,那么之后再调用Class B的M2方法实际上就会调用Class A的M1了。 结果这个方法成功了!这里贴一下关键的代码。
int swap_virtual_methods(const char *origclass, const char *origmeth,
const char *newclass, const char *newmeth) {
if (!g_dvmfindloadedclass) {
return 0;
}
LOGE(MYLOG_TAG, "%s REPLACE %s", newmeth, origmeth);
int i = 0;
ClassObject *newclazz = g_dvmfindloadedclass(newclass);
if (!newclazz) {
LOGE(MYLOG_TAG, "failed to find new class");
return 0;
}
ClassObject *oldclazz = g_dvmfindloadedclass(origclass);
if (!oldclazz) {
LOGE(MYLOG_TAG, "failed to find old class");
return 0;
}
struct Method *oldm = NULL, *newm = NULL;
if (newclazz) {
for (i = 0; i < newclazz->vtableCount; i++) {
if (!strcmp(newclazz->vtable[i]->name, newmeth))
// this is the new method
newm = newclazz->vtable[i];
}
}
if (oldclazz) {
for (i = 0; i < oldclazz->vtableCount; i++) {
if (!strcmp(oldclazz->vtable[i]->name, origmeth)) {
// save old method
oldm = oldclazz->vtable[i];
// put new method in place of old
oldclazz->vtable[i] = newm;
}
}
}
if (!newm || !oldm) {
LOGE(MYLOG_TAG, "failed to find methods/objects");
return 0;
}
// now new method gets old method name
newm->name = oldm->name;
// swap method indexes
newm->methodIndex = oldm->methodIndex;
// now old method gets proper index
LOGE(MYLOG_TAG, "swap successful!");
return 1;
}
当然在这个过程中也发现了一些坑。在Android 2.3上Dalvik是用C写的,而2.3以上是用C++写的。C++的name mangle使得在编译之后会方法名会被编译器加上一些特殊字符,比如dvmFindLoadedClass就成了_Z18dvmFindLoadedClassPKc,而且Struct ClassObject也有略微的不同,因此需要针对2.3及2.3以下的版本和2.3以上的版本分别编一个so库。另外我之前说的都是在Android 5.0以下的版本这一前提下,因为从5.0开始Android用ART取代了Dalvik,所以在5.0上这套方案行不通。但这并不意味着在5.0上就完全不可能实现打补丁,ART与Dalvik一样,也是Java虚拟机的一套实现,只是将之前的字节码改成了机器码,同时做了一些优化,目的就是为了提高效率,在ART的源码中还是可以看到Dalvik源码中的一些影子,比如ANDROID_SRC/art/runtime/mirror/Class.h和ANDROID_SRC/art/runtime/mirror/Object.h中就包含了对一个Java Class和Object的定义,只是其中获取类方法指针的方式更复杂了,通过计算相对于类指针的偏移量来得到方法指针,具体我也还没有研究明白,但我觉得也是可以实现的,后续会继续对ART做一下研究。
最后小结一下,第一种方案是想实现Class级别的补丁,用一个新的Class来替换旧的Class,但因为加载Class的ClassLoader不同,两个Class也会被认为不同的Class,其实从后面也能看出这一点,ClassObject中还包含一个ClassLoader的指针,因此此方案不可行;第二种方案是方法级别的补丁,用一个类的方法去替代另一个类的方法,甚至可以删除或者添加方法,经过在很多机型上的试验,在一定范围内是可行的。
示例工程在gitlab上的地址 http://code.dianpingoa.com/renkai.shan/android-gecko