昨天刚刚发表了一篇文章(ProGuard 又搞了个大新闻),主要吐槽的是项目里面使用 ProGuard 工具导致的一个诡异的坑。其中根本的原因就是,ProGuard 混淆 Java 注解类的时候,把两个方法混淆成同样的名字,导致 dx 工具在打包 .dex 文件的时候报错。
本来以为这件事情算是告一段落了,没想到自己还是太 Naive 了。今天早上突然收到了 ProGuard 开发者发来的一份邮件,Exciting!邮件里谈到了这次的坑出现的真正原因 —— Java 源码和字节码(bytecode)里方法的重载(OverLoading)。
被雪藏的问题真正原因
在上一篇文章里,我分析到这次问题的原因是
ProGuard 工具在混淆注解类类 Route.java 的时候,把它的两个字段都混淆成 a 了,按道理应该是一个 a 和一个 b,不知道是不是 ProGuard 的 BUG,还是 Route 与其他库冲突了。
本来我以为是 ProGuard 的 BUG,把注解类的两个字段都混淆成一样的名字,或者是 ProGuard 受到别的库的影响才出现了这个 BUG。显然,在 Java 代码里面,是不允许有两个名字相同且形参一样的方法的,哪怕是它们的返回值不同。
1 | public static class Hello { |
这两个方法是无法重载的,IDE 会提示错误并且无法编译。虽然现在不少的新编程语言支持这样返回值类型不同的方法重载,但是在 Java 里行不通,原因也很简单,类似下面的方法立刻就会产生歧义。
1 | public void call() { |
问题的原因虽然只是这么简单,但是其实在 .class 文件的字节码(bytecode)里,这样的重载方法是被允许的。为什么呢?简单点说,在字节码里面,对类的文件结构的描述十分严谨,方法调用必须有指定的返回类型,所以像上面那样的调用是不存在的,自然也就不存在产生歧义的问题。
假设现在有这样一个正常的类(上面的示例代码的正常版)。
1 | public class Hello { |
这个类编译成 .class 字节码文件后,它的文件结构大概是这样的。
1 | + Program class: com/bilibili/routertest/Hello |
我们重点关心其中的 main () V 方法,可以清楚的看到,上面的 Java 源码中,main 方法调用了 foo1 方法,虽然没有处理返回值,但是在字节码文件结构对应的方法里明确地指明了改该方法的的返回值类型是 [Ljava/lang/String,区别于 foo2 方法的 Ljava/lang/String。也就是说,字节码里面并不会存在我们上面提到的方法调用的歧义问题,因此可以支持相同形参不同返回值的方法的重载。
对于这个课题感兴趣的同学可以参考这篇出自 Oracle 的调研文章:Return-Type-Based Method Overloading in Java Blog。
总结一些人参经验
关于造成该问题原因的一些阐述。
- 上一篇文章提到的 ProGuard 构建问题其实不是 ProGuard 的 BUG,而是 Android SDK 的 dx 工具的 BUG。
- 不是只有在开启 MultiDex 的时候才会出现这个问题,不开启问题也会存在,这个问题与 MultiDex 完全没有关系。
- ProGuard 混淆的是字节码而不是 Java 源码,字节码支持相同形参不同返回值的方法的重载,ProGuard 为了最大限度压缩代码量,对后者的重载提供了支持。
- 不仅注解类,普通的类也会出现类似的问题。
解决该问题的一些方法。
- 如果不开启 ProGuard 的
overloadaggressively功能,ProGuard 不会对字节码中相同形参不同返回值的方法进行重载(这个功能默认不开启)。 - 尝试将注解类的 RetentionPolicy 级别降级为 SOURCE 级别。
- 不要让注解类出现相同形参不同返回值不同名字的方法,不然可能被混淆成重载的方法。
- Keep 住相应的注解类。
以下是 ProGuard 开发者给出的建议。
1 | Unfortunately, dx has a bug: it crashes on this overloading. Workarounds: |
最后,感叹作者的反馈这么迅速。引用作者的一句原话,It's a fast world!,西方程序员跑的比谁都快。