震驚,西方的程序員跑得竟然這麼快

昨天剛剛發表了一篇文章(ProGuard又搞了個大新聞),主要吐槽的是項目裏面使用ProGuard工具致使的一個詭異的坑。其中根本的緣由就是,ProGuard混淆Java註解類的時候,把兩個方法混淆成一樣的名字,致使dx工具在打包.dex文件的時候報錯。java

原本覺得這件事情算是告一段落了,沒想到本身仍是太Naive了。今天早上忽然收到了ProGuard開發者發來的一份郵件,Exciting!郵件裏談到了此次的坑出現的真正緣由 —— Java源碼和字節碼(bytecode)裏方法的重載(OverLoading)。android

被雪藏的問題真正緣由

在上一篇文章裏,我分析到此次問題的緣由是程序員

ProGuard工具在混淆註解類類Route.java的時候,把它的兩個字段都混淆成a了,按道理應該是一個a和一個b,不知道是否是ProGuard的BUG,仍是Route與其餘庫衝突了。編程

原本我覺得是ProGuard的BUG,把註解類的兩個字段都混淆成同樣的名字,或者是ProGuard受到別的庫的影響纔出現了這個BUG。顯然,在Java代碼裏面,是不容許有兩個名字相同且形參同樣的方法的,哪怕是它們的返回值不一樣。bash

public static class Hello {
    public String[] foo() {
        return new String[]{"wor", "ld"};
    }
    public String foo() {
        return "world";
    }
}

這兩個方法是沒法重載的,IDE會提示錯誤而且沒法編譯。雖然如今很多的新編程語言支持這樣返回值類型不一樣的方法重載,可是在Java裏行不通,緣由也很簡單,相似下面的方法馬上就會產生歧義。oracle

public void call() {
    // 沒法肯定調用的是哪一個方法。
    foo();
}

問題的緣由雖然只是這麼簡單,可是其實在.class文件的字節碼(bytecode)裏,這樣的重載方法是被容許的。爲何呢?簡單點說,在字節碼裏面,對類的文件結構的描述十分嚴謹,方法調用必須有指定的返回類型,因此像上面那樣的調用是不存在的,天然也就不存在產生歧義的問題。編程語言

假設如今有這樣一個正常的類(上面的示例代碼的正常版)。工具

public class Hello {
    public String[] foo1() {
        return new String[]{"wor", "ld"};
    }
    public String foo2() {
        return "world";
    }

    public void main() {
        foo1();
        String s = foo2();
    }
}

這個類編譯成.class字節碼文件後,它的文件結構大概是這樣的。post

+ Program class: com/bilibili/routertest/Hello
 ...
Interfaces (count = 0):
Constant Pool (count = 30):
 ...
Fields (count = 0):

Methods (count = 4):
  - Method:       <init>()V
    Access flags: 0x1
      = public Hello()
      ...
      
  + Method:       foo1()[Ljava/lang/String;
    Access flags: 0x1
      = public java.lang.String[] foo1()
      ...
      
  + Method:       foo2()Ljava/lang/String;
    Access flags: 0x1
      = public java.lang.String foo2()
      ...
      
  + Method:       main()V
    Access flags: 0x1
      = public void main()
    Class member attributes (count = 1):
    + Code attribute instructions (code length = 11, locals = 2, stack = 1):
      [0] aload_0 v0
      [1] invokevirtual #7
        + Methodref [com/bilibili/routertest/Hello.foo1 ()[Ljava/lang/String;]
      [4] pop
      [5] aload_0 v0
      [6] invokevirtual #8
        + Methodref [com/bilibili/routertest/Hello.foo2 ()Ljava/lang/String;]
      [9] astore_1 v1
      [10] return
      Code attribute exceptions (count = 0):
      Code attribute attributes (attribute count = 1):
      + Line number table attribute (count = 3)
        [0] -> line 12
        [5] -> line 13
        [10] -> line 14

Class file attributes (count = 1):
  ...

咱們重點關心其中的main()V方法,能夠清楚的看到,上面的Java源碼中,main方法調用了foo1方法,雖然沒有處理返回值,可是在字節碼文件結構對應的方法裏明確地指明瞭改該方法的的返回值類型是[Ljava/lang/String,區別於foo2方法的Ljava/lang/String。也就是說,字節碼裏面並不會存在咱們上面提到的方法調用的歧義問題,所以能夠支持相同形參不一樣返回值的方法的重載。this

對於這個課題感興趣的同窗能夠參考這篇出自Oracle的調研文章:Return-Type-Based Method Overloading in Java Blog

總結一些人蔘經驗

關於形成該問題緣由的一些闡述。

  1. 上一篇文章提到的ProGuard構建問題其實不是ProGuard的BUG,而是Android SDK的dx工具的BUG。

  2. 不是隻有在開啓MultiDex的時候纔會出現這個問題,不開啓問題也會存在,這個問題與MultiDex徹底沒有關係。

  3. ProGuard混淆的是字節碼而不是Java源碼,字節碼支持相同形參不一樣返回值的方法的重載,ProGuard爲了最大限度壓縮代碼量,對後者的重載提供了支持。

  4. 不只註解類,普通的類也會出現相似的問題。

解決該問題的一些方法。

  1. 若是不開啓ProGuard的-overloadaggressively功能,ProGuard不會對字節碼中相同形參不一樣返回值的方法進行重載(這個功能默認不開啓)。

  2. 嘗試將註解類的RetentionPolicy級別降級爲SOURCE級別。

  3. 不要讓註解類出現相同形參不一樣返回值不一樣名字的方法,否則可能被混淆成重載的方法。

  4. Keep住相應的註解類。

如下是ProGuard開發者給出的建議。

Unfortunately, dx has a bug: it crashes on this overloading. Workarounds:

- Do not use the option '-overloadaggressively' in your ProGuard configuration.

- Alternatively, keep the original annotation method names:

    -keepclassmembernames @interface * { <methods>; }

The dx tool should then accept the code.

If it works, you can post this solution in your blog.

最後,感嘆做者的反饋這麼迅速。引用做者的一句原話,It's a fast world!,西方程序員跑的比誰都快。

相關文章
相關標籤/搜索