您的位置:首页 > 其它

AndFix使用感想

2016-06-16 20:21 411 查看
AndFix已经使用了一段时间了,但是到AndFix上看了一下,最近2个月都没有更新代码了,有141个issues和3个pull
request没人处理,其实AndFix的Contributors就俩个人,一个是rogerAce还有个是supern
lee,虽然快要沦为了阿里的KPI项目,但是并不妨碍AndFix在业界的地位-一个低成本快速接入的Bugxiuf第一方案。

第二方案Nuwa,Nuwa的原理是修改了gradle的编译task流程,替换dex的方式来实现。但是可惜的是gradle
plugin在1.5以后取消了predexdebug这个task,而Nuwa恰恰是依赖这个task的,所以导致Nuwa在gradle plugin1.5版本后无法使用,而且Nuwa的作者是贾吉鑫也在一次技术分享的时候也表示,不再维护Nuwa,因为他感觉AndFix已经做的足够好,他不想把AndFix做的事情再做一次。

闲话扯完,网上AndFix的教程和解析已经很多,这里就仅分享一下我在AndFix中学到的东西。


SortedSet

1
2

private final SortedSet<Patch> mPatchs;
this.mPatchs = new ConcurrentSkipListSet();


SortedSet是个接口,它里面的(只有TreeSet这一个实现可用)中的元素一定是有序的。

保证迭代器按照元素递增顺序遍历的集合,可以按照元素的自然顺序(参见 Comparable)进行排序, 或者按照创建有序集合时提供的 Comparator进行排序。要采用此排序,

ConcurrentSkipListSet(在JavaSE 6新增的)提供的功能类似于TreeSet,能够并发的访问有序的set。因为ConcurrentSkipListSet是基于“跳跃列表(skip list)”实现的,只要多个线程没有同时修改集合的同一个部分,那么在正常读、写集合的操作中不会出现竞争现象。



mPatchs是一个有序并发Set,Set中的元素都必须实现 Comparable 接口,所以Patch实现了Comparable的compareTo方法,可见Patch是按照时间从小到大顺序

1
23

public int compareTo(Patch another) {
return this.mTime.compareTo(another.getTime());
}


Patch生成

首先看一下Patch是什么?解压之后(盗图)

meta-inf文件夹为:

打开patch.mf文件可以发现两个apk的差异信息:

1
23
4
5
6
7

Manifest-Version: 1.0
Patch-Name: app-release-fix
To-File: app-release-online.apk
Created-Time: 30 Mar 2016 06:26:27 GMT
Created-By: 1.0 (ApkPatch)
Patch-Classes: com.qianmi.shine.activity.me.AboutActivity_CF
From-File: app-release-fix.apk

AndFix是如何把文件读取出来,并且初始化Patch的尼?Android大部分的文件都是压缩包,所以这里的处理也有一定的代表性。

1
23
4
5
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39

private void init() throws IOException {
JarFile jarFile = null;
InputStream inputStream = null;

try {
jarFile = new JarFile(this.mFile);
JarEntry entry = jarFile.getJarEntry("META-INF/PATCH.MF");
inputStream = jarFile.getInputStream(entry);
Manifest manifest = new Manifest(inputStream);
Attributes main = manifest.getMainAttributes();
this.mName = main.getValue("Patch-Name");
this.mTime = new Date(main.getValue("Created-Time"));
this.mClassesMap = new HashMap();
Iterator it = main.keySet().iterator();

while(it.hasNext()) {
Name attrName = (Name)it.next();
String name = attrName.toString();
if(name.endsWith("-Classes")) {
List strings = Arrays.asList(main.getValue(attrName).split(","));
if(name.equalsIgnoreCase("Patch-Classes")) {
this.mClassesMap.put(this.mName, strings);
} else {
this.mClassesMap.put(name.trim().substring(0, name.length() - 8), strings);
}
}
}
} finally {
if(jarFile != null) {
jarFile.close();
}

if(inputStream != null) {
inputStream.close();
}

}

}


JarFile 类用于从任何可以使用 java.io.RandomAccessFile 打开的文件中读取 jar 文件的内容。它扩展了 java.util.zip.ZipFile 类,使之支持读取可选的 Manifest 条目。Manifest 可用于指定关于 jar 文件及其条目的元信息。



这里使用了JarFile来解压.patch文件,然后找到META-INF/PATCH.MF 然后找到Patch-Classes,这样就可以取出里面被修复的类,当然这些类都是在原来的包名+CF,当真正修复的时候执行this.mAndFixManager.fix(patch.getFile(), cl, classes)方法,里面patch.getFile()里面是dex,classes里面是指定修复的类。


fix

下面就是AndFix中最重要的一个方法了,核心的fix逻辑都在这里

1
23
4
5
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27

public synchronized void fix(File file, ClassLoader classLoader, List<String> classes) {
if(this.mSupport) {
if(this.mSecurityChecker.verifyApk(file)) {
try {
File e = new File(this.mOptDir, file.getName());
boolean saveFingerprint = true;
if(e.exists()) {
if(this.mSecurityChecker.verifyOpt(e)) {
saveFingerprint = false;
} else if(!e.delete()) {
return;
}
}

DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(), e.getAbsolutePath(), 0);
。。。
clazz = dexFile.loadClass(entry, classLoader);
。。。
this.fixClass(clazz, classLoader);
return;
}
} catch (IOException var12) {
Log.e("AndFixManager", "pacth", var12);
}
}
}
}

mSupport- 是否支持热更新修复,简单看了一下,就是不支持yunOS,sdk在8~23都可以。

verifyApk- 对比一下签名信息,看dex是否合法,这里面的方法也很典型,自己写库的时候也可以用。

verifyOpt- 获取file的md5值看是否改变过

DexFile.loadDex


打开一个DEX文件,并提供一个文件来保存优化过的DEX数据。如果优化过的格式已存在并且是最新的,就直接使用它。如果不是,虚拟机将试图重新创建一个。该方法主要用于应用希望在通常的应用安装机制之外下载和执行DEX文件。不能在应用里直接调用该方法,而应该通过一个类装载器例如dalvik.system.DexClassLoader.

关于动态加载可以看看扩展



接下来就是找到带MethodReplace注解的方法,这个注解是在代码对比过程自动生成的,

1
23
4
5
6
7
8

MethodReplace methodReplace = (MethodReplace)method.getAnnotation(MethodReplace.class);
if(methodReplace != null) {
String clz = methodReplace.clazz();
String meth = methodReplace.method();
if(!isEmpty(clz) && !isEmpty(meth)) {
this.replaceMethod(classLoader, clz, meth, method);
}
}

replaceMethod传入的有class名字,方法的名字,方法本身,这样根据meth就有找到原来app中对应的方法

1

Method src1 = clazz.getDeclaredMethod(meth, method.getParameterTypes());

一个有bug的方法,一个修复后的方法,一招乾坤大罗移,

1

AndFix.addReplaceMethod(src1, method);

为啥说是乾坤大罗移是因为AndFix在C的层面将源方法(meth)的各个属性被替换成了新的方法(target)的各个属性,就完成了方法的替换,当虚拟机误以为方法还是之前的“方法”。

1
23
4
5
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23

extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
clz->status = CLASS_INITIALIZED;

Method* meth = (Method*) env->FromReflectedMethod(src);
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);

meth->clazz = target->clazz;
meth->accessFlags |= ACC_PUBLIC;
meth->methodIndex = target->methodIndex;
meth->jniArgInfo = target->jniArgInfo;
meth->registersSize = target->registersSize;
meth->outsSize = target->outsSize;
meth->insSize = target->insSize;

meth->prototype = target->prototype;
meth->insns = target->insns;
meth->nativeFunc = target->nativeFunc;
}

至此,AndFix的整个过程就结束了,可见要完成这样的库,需要会写插件,NDK,还有对类的加载过程非常了解,最关键的是,从问题入手找解决方法的过程,试着想一下,如果你有了这些技术栈,现在需要解决动态修复的问题,你会怎么做?我可能会把整个类都替换掉,因为在我感觉中,一块代码的替换好像比一个类的替换要难,但是阿里的同学做到了。实在佩服佩服,也为阿里在开源届做的贡献点赞!!!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: