ProGuard是一个压缩、优化和混淆Java字节码的工具,非常好用。本篇文章总结一下许多人在使用ProGuard时经常遇到的问题。
我把在使用ProGuard时经常遇到的问题分为两类,分别是导致构建失败的编译时问题,以及构建通过但运行时崩溃或结果不正确的运行时问题。大多数人所遇到的大多数问题,都可以在下面的内容中找到对应的解决套路。
在开始讲这两类问题前,先明确一点:我们所说的添加混淆规则,不是指加入了才会混淆相关的类,相反,事实上,当你启用混淆之后,添加的一些诸如-keep xxxx
的规则才是起着不混淆的作用。
下面开始讲这两类问题。
编译时问题
问题
首先讲编译时的问题。导致编译不通过,最常见的情况是这样的:
在漫长的编译之后,我们等到的控制台上的这样一个输出结果:
1 | ... |
原因及解决方法
有些小伙伴会自动忽略英文日志,即便它给出了明确的答案,这是个严重的不良习惯。如上,其实在这段日志中,已经表明了原因及解决方案了。注意Warning
开头的警告内容,最后一个警告是让你先解决第一个警告的内容,所以先忽略。我们看它前面的警告:
1 | there were 11 unresolved references to classes or interfaces. |
这是什么意思呢?
第一句话是告诉你原因:有11个未解析的类或引用。
后面两句是解决方案。方案一:你可能需要添加丢失的库或更新它们的版本。方案二:如果你现在代码运行得好好的,也就是没有它们也没关系,那你可以使用-dontwarn
来禁止这样的警告。
那么如何知道有哪些未解析的类或引用呢?
遇到这样的问题,很简单,我们往上翻,最终肯定会找到Warning开头的日志内容:
1 | Warning: okhttp3.internal.platform.ConscryptPlatform: can't find referenced class org.conscrypt.OpenSSLProvider |
它的句型是这样的:Warning: xxxx.xxx.ABC: can't find referenced class xxx.xxx.XYZ
意思就是第三方库里的ABC引用了XYZ这个类,但是我在类路径中找不到XYZ这个类。
这种情况很常见,比如一个Java库使用了另一个库的一些类来实现某个特性。如果你在使用这个库的时候需要这个特性,那么你就要把另一个库也加进来,也就是前面给出的方案一。而如果你不需要用到,如上面的例子,项目中实际上用不到org.conscrypt
包里的内容,那么我们在混淆规则的文件中添加上-dontwarn
规则就可以了。
如何添加?
规则很简单。
1 | -dontwarn 类名 |
在上面的日志中,有引用的类,也有被引用的类,这两者都可以。也就是对于前面的句型,你既可以使用-dontwarn xxxx.xxx.ABC
,也可以使用-dontwarn xxx.xxx.XYZ
。
比如上面的例子,我们用
1 | -dontwarn okhttp3.internal.platform.ConscryptPlatform |
和用
1 | -dontwarn org.conscrypt.OpenSSLProvider |
都可以解决问题。但从这里也可以看出疑惑来了。如果有多个类呢?是不是每一个类都要写上去呢?
当然不是。这里的类名是支持通配符的,比如上面,我们可以写为:
1 | -dontwarn org.conscrypt.* |
表示禁止org.conscrypt
包下的类的警告。但是这里的*
是不包含包分隔符的,也就是说它的子包里面的类是不会被禁止的。如果需要连它下面的包的类也一并禁止,可以使用包含包分隔符的**
,也就是如下:
1 | -dontwarn org.conscrypt.** |
关于过滤器的更多用法,可以参阅文档:https://www.guardsquare.com/en/products/proguard/manual/usage#filters
总结
这是最常见的一类问题,我们来总结一下这个套路:
- 编译报错,提示内容是
Warning: there were xxx unresolved references to classes or interfaces.
。 - 往上找带有
Warning:
的日志,句型结构为:Warning: xxxx.xxx.ABC: can't find referenced class xxx.xxx.XYZ
- 往混淆规则里添加
-dontwarn 类名
的规则。
运行时问题
我们再来看另一类问题。
当有人在讨论群里求助混淆相关的问题时,往往会有人说加-keep
。那么-keep
是作什么用呢?在什么情况下需要它呢?
在文章开头一句话介绍过,Proguard 是一个压缩、优化和混淆Java字节码文件的工具。也就是,它所做的不仅仅包括我们所知道的混淆,它会分析所有的类,找出没有用的类、字段、方法等把它们删除掉,从而达到对字节码的压缩及优化。而如果我们使用了反射去调用一些类或方法的话,它是不知道的,这样就会导致“误删”的情况。
所以当开启混淆之后,出现类找不到,方法找不到,属性找不到时,我们就要使用keep相关规则来把它们给留住啦。比如使用-keep class xxxx {*;}
保留指定的类名及其成员。
keep规则有三种(Android新增加的@keep
注解不谈,这里只讲规则文件的内容):
- -keep 保留指定的类名及其成员
- -keepclassmembers 只保留住成员,不能保留住类名
- -keepclasseswithmembers 根据成员找到满足条件的所有类,保留它们的类名和成员名
比如我们使用了事件总线,为使方法能保留,我们可以添加如下的规则:
1 | -keepclassmembers class ** { |
表示保留所有类中的有 @org.greenrobot.eventbus.Subscribe
注解的方法。<methods>
是指方法,如果要保留字段,则使用<fields>
。
反正就是找不到类或成员,你就keep住。这里的keep规则也是很灵活的,比如除了上面指定的类,你也可以指定为接口:
1 | -keepclassmembernames,allowobfuscation interface * { |
可以指定继承自某个类的所有类,如:
1 | -keep public class * extends android.app.Service |
可以指定实现某个接口的类,如:
1 | -keep class * implements com.google.gson.TypeAdapterFactory |
可以指定某个包下的所有类(包括子包)及所有成员,如:
1 | -keep class com.tencent.stat.** {* ;} |
除此之外,我们还可能会遇到其他问题,这里把其他常用的规则也快速过一下:
注解解析不到?
1 | -keepattributes *Annotation* |
泛型转换失败?
1 | -keepattributes Signature |
枚举?
1 | -keepclassmembers enum * { *; } |
掌握以上几条规则,通常可以解决绝大多数混淆以后运行出错的问题了。
结语
本篇讲到这里,希望对被以上常见问题所困扰的同学有所帮助。这里需要注意一下,对于第一类问题,在编译的时候就能暴露出来,而第二类问题,只有在运行到相关代码时才会出现。所以如果一个项目以前是不使用混淆的话,在启用混淆之后一定要做好回归测试。