简世博客

一个简单的世界——博客空间,写了一些Android相关的技术文章,和一些点滴的想法

0%

AddTryCatch gradle plugin 编译期给代码加try catch的插件

AddTryCatch插件的作用——编译期加tryCatch,支持向第三方库中加tryCatch

在平时开发过程中,无可避免的会遇到crash,如果是自己写的代码里抛出异常还好,把相关代码逻辑改好即可。但是遇到集成到项目中的第三方库抛出异常,又无法通过自己可修改的代码部分规避问题,就只能依赖第三方库开发者修复问题,再发布新版本了。
而现实中遇到的第三方库,能在提issue后快速修复问题并发布新版本的实在是少之又少。这种情况下,我们就十分被动了。

每当这时候,我就在想:如果能修改第三方库中的代码就好了! 不求能精准修改逻辑,仅仅是能加上try catch就不错了,如果能在catch后捕获到异常,能再调用自定义的代码处理该异常,就更好了!

而这个AddTryCatch,就是为此而生的。

这个插件,可以通过简单的配置,做到在编译期修改字节码的效果。
因为是在编译期在字节码的层面上修改,所以不管是自己写的代码,还是引用的第三方库中的代码,都可以修改

项目地址:https://github.com/xingchenxuanfeng/AddTryCatchPlugin

AddTryCatch插件的使用

该插件的使用方式很简单,只需要两部:
  1. AddTryCatch插件发布在了jitpack上,使用时需要先把jitpack加入到buildScript 的repositories中,并在dependencies 中加入classpath 'com.github.xingchenxuanfeng:AddTryCatchPlugin:1.0.1' 。代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    buildscript {
    repositories {
    ...
    maven { url 'https://jitpack.io' }
    }
    dependencies {
    ...
    classpath 'com.github.xingchenxuanfeng:AddTryCatchPlugin:1.0.1'
    }
    }
  2. 在app moudle级别的build.gradle中apply plugin: 'add-trycatch',然后按如下格式进行配置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    apply plugin: 'add-trycatch'

    addTryCatch {
    hookPoint = [
    "com.addtrycatchplugin.TestCrash1" : [
    "crashMethod1",
    "crashMethod2"
    ],
    "com.addtrycatchplugin.TestCrash2": [
    "crashMethod1",
    "crashMethod2"
    ]
    ]
    exceptionHandler = ["com.addtrycatchplugin.ExceptionUtils": "uploadCatchedException"]
    }
    按照上面的写法配置好后,插件就可以正常工作了。

参数解释:

下面我来解释一下各个参数的具体意义。

hookPoint

hookPoint表示需要注入tryCatch的代码,hookPoint声明的数据结构是Map<String, List<String>>

上面的配置表示,在com.addtrycatchplugin.TestCrash1这个类的crashMethod1和crashMethod2两个方法,与com.addtrycatchplugin.TestCrash2这个类的crashMethod1和crashMethod2两个方法,共计四个方法中加入tryCatch。在方法的第一行加入try关键字,在方法最后一行加入catch关键字和catch代码块。

exceptionHandler

exceptionHandler表示在catch到异常后,会执行的异常处理方法(常见的处理策略是上报到统计平台以供分析)。com.addtrycatchplugin.ExceptionUtils是处理方法的类名,uploadCatchedException是方法名。(该参数可以为空,表示catch到异常后,不做任何处理,忽略该异常)

下面是插件效果:

原始代码: 三个类 异常处理类 ExceptionUtils。java,和要被修改的类,TestCrash1.java,TestCrash2.java

ExceptionUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.addtrycatchplugin;

import android.util.Log;

public class ExceptionUtils {
public static void uploadCatchedException(Exception exception) {
if (exception == null) {
return;
}
//demo里没有接入异常上报平台,仅仅打了log来测试是否捕获成功
Log.e("ExceptionUtilsTAG", "uploadCatchedException", exception);
}
}

TestCrash1.java

1
2
3
4
5
6
7
8
9
10
11
package com.addtrycatchplugin;

public class TestCrash1 {
public static void crashMethod1() {
int a = 1 / 0;
}

public static void crashMethod2() {
int a = 1 / 0;
}
}

TestCrash2.java

1
2
3
4
5
6
7
8
9
10
11
package com.addtrycatchplugin;

class TestCrash2 {
public static void crashMethod1() {
int a = 1 / 0;
}

public static void crashMethod2() {
int a = 1 / 0;
}
}

生成后的代码:TestCrash1.class 和TestCrash2.class

TestCrash1.class

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.addtrycatchplugin;

public class TestCrash1 {
public TestCrash1() {
}

public static void crashMethod1() {
try {
int var0 = 1 / 0;
} catch (Exception var2) {
ExceptionUtils.uploadCatchedException(var2);
}
}

public static void crashMethod2() {
try {
int var0 = 1 / 0;
} catch (Exception var2) {
ExceptionUtils.uploadCatchedException(var2);
}
}
}

TestCrash2.class

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.addtrycatchplugin;

public class TestCrash2 {
public TestCrash2() {
}

public static void crashMethod1() {
try {
int var0 = 1 / 0;
} catch (Exception var2) {
ExceptionUtils.uploadCatchedException(var2);
}
}

public static void crashMethod2() {
try {
int var0 = 1 / 0;
} catch (Exception var2) {
ExceptionUtils.uploadCatchedException(var2);
}
}
}

插件原理:

利用gralde transform api在编译流程中加入自定义的transform任务,然后在自定义的transform任务中,使用asm修改字节码来达到注入try catch代码的目的。同时使用Hunter框架来优化transform任务运行效率,简化代码逻辑。

asm

ASM是一个通用的Java字节码操作和分析框架。它可以用于修改现有类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从中构建自定义复杂转换和代码分析工具。ASM提供与其他Java字节码框架类似的功能,但专注于 性能。因为它的设计和实现尽可能小而且快,所以它非常适合在动态系统中使用(当然也可以以静态方式使用,例如在编译器中)。
目前已广泛应用于众多著名项目,如OpenJDK,Groovy编译器,Kotlin编译器Gradle等。

Gradle transform api

Gradle Transform是Android官方提供给开发者在项目构建阶段中由class到dex转换之前修改class文件的一套api。目前比较经典的应用是字节码插桩、代码注入技术。

Hunter

Hunter: 一个插件框架,在它的基础上可以快速开发一个并发、增量的字节码编译插件,帮助开发人员隐藏了Transform和ASM的绝大部分逻辑,开发者只需写少量的ASM code,就可以开发一款编译插件,修改Android项目的字节码。

插件实现:

具体实现见下期:
一步步实现AddTryCatch插件 —— gradle transform和ASM实践

参考:

  • ASM官方站点
  • AOP 的利器:ASM 3.0 介绍
  • Gradle Transform Api
  • Gradle Transform Javadoc
  • Hunter