前言

同事发现了一个crash是来源于公司内某个闭源jar的,而负责此Jar开发的同事已经离职,难以找人修复,于是就想找我帮忙弄下。
想修改class文件,方式很多,如Javassist、ASM等

  • Javassist的修改是基于源代码,因此更容易上手,可参考修改jar的.class文件,并重新打包
  • ASM则是直接基于java bytecode,复杂性相对高些,但几乎无所不能。若能理解ASM的设计的话,使用也并不复杂,不懂的可参考ASM库的介绍

要实现的功能

把uilib.doraemon.model.layer.CompositionLayer类的drawLayer方法中的代码

1
canvas.clipRect(this.originalClipRect, Op.REPLACE);

改为

1
2
3
4
5
if (VERSION.SDK_INT >= 26) {
canvas.clipRect(this.originalClipRect);
} else {
canvas.clipRect(this.originalClipRect, Op.REPLACE);
}

bug修复方法出处

思路

1、加载Jar包,并转换成ClassNode对象列表
2、找到uilib.doraemon.model.layer.CompositionLayer类对应的ClassNode,并遍历其所有方法
3、找到drawLayer,并定位到要替换的代码
4、对比替换前后的代码的ASM实现,并替换
5、用新的ClassNode生成新的jar包

新建Java工程,并导入ASM库

这边用Android Studio直接开发
1、新建一个Java Library

2、导入ASM库,可到官网下载,也可在这直接下载asm lib
3、新建main入口类

1
2
3
4
5
6
package com.noverguo.asm;

public class AsmMain {
public static void main(String[] args) throws IOException {
}
}

加载Jar包并解析成List

这里用到我以前写的一个JarLoader工具类

1
List<ClassNode> classNodes = JarLoader.loadJar(inJarPath);

定位对应的类及其方法,并保存更改后的类和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<ClassNode> changeNodes = new ArrayList<>();
for (ClassNode cn : classNodes) {
if (cn.name.equals("uilib/doraemon/model/layer/CompositionLayer")) {
List<MethodNode> newMethods = new ArrayList<>();
for (MethodNode mn :cn.methods) {
if (mn.name.equals("drawLayer")) {
// 这里定位到了uilib.doraemon.model.layer.CompositionLayer类的drawLayer方法
MethodNode newMethodNode = new MethodNode(mn.access, mn.name, mn.desc, mn.signature, mn.exceptions.toArray(new String[0]));
// 这里需要实现新方法的修改
newMethods.add(newMethodNode);
} else {
newMethods.add(mn);
}
}
cn.methods = newMethods;
changeNodes.add(cn);
}
}

定位要替换的代码

ASM操作的是字节码bytecode,因此先安装ASM bytecode online插件

在AndroidStudio中进入要修改的Jar包里的类,会自动反编译,如果没有反编译成功,可尝试安装jd-inteIIij插件
要修改的方法的实现如下:

1
2
3
4
5
6
7
8
9
void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
...

if (!this.originalClipRect.isEmpty()) {
canvas.clipRect(this.originalClipRect, Op.REPLACE);
}

...
}

在此类中右键-点击【Show Bytecode outline】
定位到此方法对应的ASM实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
mv = cw.visitMethod(0, "drawLayer", "(Landroid/graphics/Canvas;Landroid/graphics/Matrix;I)V", null, null);
mv.visitCode();

...

mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, "uilib/doraemon/model/layer/CompositionLayer", "originalClipRect", "Landroid/graphics/Rect;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/graphics/Rect", "isEmpty", "()Z", false);
Label l14 = new Label();
mv.visitJumpInsn(Opcodes.IFNE, l14);
Label l15 = new Label();
mv.visitLabel(l15);
mv.visitLineNumber(100, l15);
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, "uilib/doraemon/model/layer/CompositionLayer", "originalClipRect", "Landroid/graphics/Rect;");
mv.visitFieldInsn(Opcodes.GETSTATIC, "android/graphics/Region$Op", "REPLACE", "Landroid/graphics/Region$Op;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/graphics/Canvas", "clipRect", "(Landroid/graphics/Rect;Landroid/graphics/Region$Op;)Z", false);
mv.visitInsn(Opcodes.POP);
mv.visitLabel(l14);
mv.visitLineNumber(102, l14);

...

mv.visitMaxs(5, 7);
mv.visitEnd();
}

也就是说,最终要替换的代码是这个

1
2
3
4
5
6
7
8
9
Label l15 = new Label();
mv.visitLabel(l15);
mv.visitLineNumber(100, l15);
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, "uilib/doraemon/model/layer/CompositionLayer", "originalClipRect", "Landroid/graphics/Rect;");
mv.visitFieldInsn(Opcodes.GETSTATIC, "android/graphics/Region$Op", "REPLACE", "Landroid/graphics/Region$Op;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/graphics/Canvas", "clipRect", "(Landroid/graphics/Rect;Landroid/graphics/Region$Op;)Z", false);
mv.visitInsn(Opcodes.POP);

这里我们看到一个可以非常准确定位的点:

1
mv.visitLineNumber(100, l15);

这行的意思是接下来的代码在源码中的行号为100,主要用于调试定位

实现定位代码,并过滤掉旧的指令

这里使用装饰器模式的实现方式,先new一个新的MethodNode,然后通过MethodVisitor拦截visitLineNumber,并标记为found。方便后续过滤掉不要的指令
这里以visitInsn作为过滤的出口,对应的是mv.visitInsn(Opcodes.POP);
这样旧的实现代码就会被清掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
MethodNode newMethodNode = new MethodNode(mn.access, mn.name, mn.desc, mn.signature, mn.exceptions.toArray(new String[0]));
mn.accept(new MethodVisitor(Opcodes.ASM5, newMethodNode) {
boolean found = false;
@Override
public void visitLineNumber(int line, Label start) {
super.visitLineNumber(line, start);
if (line == 100) {
found = true;
}
}

@Override
public void visitInsn(int opcode) {
if (found) {
found = false;
return;
}
super.visitInsn(opcode);
}


@Override
public void visitVarInsn(int opcode, int var) {
if (found) {
return;
}
super.visitVarInsn(opcode, var);
}

@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
if (found) {
return;
}
super.visitFieldInsn(opcode, owner, name, desc);
}


@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
if (found) {
return;
}
super.visitMethodInsn(opcode, owner, name, desc);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
if (found) {
return;
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
});

新代码ASM实现

新代码的Java实现如下:

1
2
3
4
5
if (VERSION.SDK_INT >= 26) {
canvas.clipRect(this.originalClipRect);
} else {
canvas.clipRect(this.originalClipRect, Op.REPLACE);
}

这里有个问题是,如何得到其对应的ASM实现?

我们可以把反编译出来的代码,复制一份到新的类中,如uilib.doraemon.model.layer.CompositionLayer2

由于此类用了很多安卓的类。因此建议直接在安卓项目中新建此类。当然也可人为导入android.jar到java项目中。

之后在CompositionLayer2类中右键-点击【Show Bytecode outline】,即可找到新的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
mv.visitLabel(l6);
mv.visitLineNumber(99, l6);
mv.visitFrame(Opcodes.F_CHOP, 1, null, 0, null);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, "uilib/doraemon/model/layer/CompositionLayer", "originalClipRect", "Landroid/graphics/Rect;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/graphics/Rect", "isEmpty", "()Z", false);
Label l14 = new Label();
mv.visitJumpInsn(Opcodes.IFNE, l14);
Label l15 = new Label();
mv.visitLabel(l15);
mv.visitLineNumber(100, l15);


mv.visitFieldInsn(Opcodes.GETSTATIC, "android/os/Build$VERSION", "SDK_INT", "I");
mv.visitIntInsn(Opcodes.BIPUSH, 26);
Label l16 = new Label();
mv.visitJumpInsn(Opcodes.IF_ICMPLT, l16);
Label l17 = new Label();
mv.visitLabel(l17);
mv.visitLineNumber(101, l17);
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, "uilib/doraemon/model/layer/CompositionLayer", "originalClipRect", "Landroid/graphics/Rect;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/graphics/Canvas", "clipRect", "(Landroid/graphics/Rect;)Z", false);
mv.visitInsn(Opcodes.POP);
mv.visitJumpInsn(Opcodes.GOTO, l14);
mv.visitLabel(l16);
mv.visitLineNumber(103, l16);
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, "uilib/doraemon/model/layer/CompositionLayer", "originalClipRect", "Landroid/graphics/Rect;");
mv.visitFieldInsn(Opcodes.GETSTATIC, "android/graphics/Region$Op", "REPLACE", "Landroid/graphics/Region$Op;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/graphics/Canvas", "clipRect", "(Landroid/graphics/Rect;Landroid/graphics/Region$Op;)Z", false);
mv.visitInsn(Opcodes.POP);


mv.visitLabel(l14);
mv.visitLineNumber(102, l14);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitLdcInsn("CompositionLayer#draw");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "uilib/doraemon/L", "endSection", "(Ljava/lang/String;)F", false);
mv.visitInsn(Opcodes.POP);

现在,我们更改下visitLineNumber的实现,在找到后插入新的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public void visitLineNumber(int line, Label start) {
super.visitLineNumber(line, start);
if (line == 100) {
found = true;
newCode();
}
}
private void newCode() {
mv.visitFieldInsn(Opcodes.GETSTATIC, "android/os/Build$VERSION", "SDK_INT", "I");
mv.visitIntInsn(Opcodes.BIPUSH, 26);
Label l16 = new Label();
mv.visitJumpInsn(Opcodes.IF_ICMPLT, l16);
Label l17 = new Label();
mv.visitLabel(l17);
mv.visitLineNumber(101, l17);
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, "uilib/doraemon/model/layer/CompositionLayer", "originalClipRect", "Landroid/graphics/Rect;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/graphics/Canvas", "clipRect", "(Landroid/graphics/Rect;)Z", false);
mv.visitInsn(Opcodes.POP);
mv.visitJumpInsn(Opcodes.GOTO, l14);
mv.visitLabel(l16);
mv.visitLineNumber(103, l16);
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, "uilib/doraemon/model/layer/CompositionLayer", "originalClipRect", "Landroid/graphics/Rect;");
mv.visitFieldInsn(Opcodes.GETSTATIC, "android/graphics/Region$Op", "REPLACE", "Landroid/graphics/Region$Op;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/graphics/Canvas", "clipRect", "(Landroid/graphics/Rect;Landroid/graphics/Region$Op;)Z", false);
mv.visitInsn(Opcodes.POP);
}

上面会显示l14找不到,因为这里的代码没定义,而l14实际上是在mv.visitInsn(Opcodes.POP);之后的旧代码里访问的,因此我们比较难拿到。
这里可以定义个新的Label解决

1
2
3
4
5
6
7
Label newLabel = new Label()
...
mv.visitInsn(Opcodes.POP);
mv.visitJumpInsn(Opcodes.GOTO, newLabel);
...
mv.visitInsn(Opcodes.POP);
mv.visitLabel(newLabel);

保存新的ClassNode到Jar中

1
JarLoader.saveToJar(inJarPath, outJarPath, changeNodes);