08 September 2014

如果你有 Android App 持續開發一年以上,那你多半已經遇過很有名的 Dex 64k method 數量上限:

Unable to execute dex: method ID not in [0, 0xffff]: 65536
Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536

以及 LinearAlloc exceeded 5MB capacity:

ERROR/dalvikvm(4620): LinearAlloc exceeded capacity (5242880), last=...

兩者的限制不同,但成因卻很類似,都是因為 App 的程式太大。如果你 很幸運的 遇到這個問題 (是幸運沒錯,App 要受歡迎才有機會變大) 那你八成已經靠 stackoverflow 上的各種奇怪的藥方暫時的解決這個問題。

App 太大無法安裝這問題現在無解,即使到的新的 runtime ART 也是一樣。我們 Android 開發者會跟這個問題相處一段很長的時間,所以能做的就是了解它,並且事先做好預防。

Dex 64k method size limit

.dex 檔是 Dalvik EXecutable,裡面存的是 dex byte code,可在 Davlik VM 上執行。你 unzip 解開 .apk 後就會看到一個 classes.dex 檔,就是它了。不過,dex method 64k 上限數跟 dex 檔案的格式無關。根據 stackoverlfow 回應的說法,是因為 Dalvik 指令集裡,執行 method 的 invoke-kind index 大小只給了 16bit,所以一個 Android 程式裡最多只能執行前 65536 個 method,後面多的都不能用。

因為是指令集的限制,所以新一代 ART Runtime 也受同樣的限制。.dex 檔頭裡已經有寫總 method 數,你可以用 Android SDK 內附的 dexdump 指令查看你的 app 定義了多少個 method:

cd android-sdk-macosx
./build-tools/19.1.0/dexdump -f  /path/to/your/apk | grep method_ids_size

下面是範例輸出,這個範例 apk 定義了 51306 個 method

method_ids_size     : 51306

5萬個 method 很多啊,快到64k上限了,一個 App 真能寫這麼多 method 嗎?其實不然,後面會解釋這一大票 method 大多是第三方的 .jar 造成的。

LinearAlloc 5MB capacity

有關 LinearAlloc 的問題,網路上已經有很好的解說。簡單說就是 Android 程式執行前會將 class 讀進 LinearAlloc 這塊 buffer 裡,它的大小在 Android 2.3 之前是 5MB,到了 4.0 後才改成 8MB 或 16MB。5MB 太小了,通常你還沒踩到 64k method 限制時,就會先踩到 LinearAlloc 的問題。

這個問題到了 4.0 才改善,但是 2.3 還有約十幾 % 的市場,所以我們還是得面對它。注意 5MB buffer 的限制不是 classes.dex 的檔案大小的限制,像我們自家的 App classes.dex 的大小已經 7 MB 了,還是可以在 2.3 執行。最主要還是看 class 結構的複雜性,以及總 method 數。

  • 根據我們的經驗,總 method 數要維持在 56000 以下才能塞進 5MB buffer 裡。
  • 根據 這個 issue 22586,太複雜的 interface 繼承會出問題,像是用 scala 語言開發 Android 就容易出錯。
  • 某些 Andoird 2.3 的 LinearAlloc 的可用大小比其他 2.3 的還小,我們的用戶中使用 HTC Desire 的手機特別容易遇到這個錯誤

錯誤訊息 INSTALL_FAILED_DEXOPT

安裝 apk 時,如果出現上面提到的兩種錯誤,你通常會看到錯誤訊息有 INSTALL_FAILED_DEXOPT 這行。dexopt 是 dex optimization 的意思,這一步驟會發生在安裝完 apk 之後,它會檢驗 .dex 裡面的指令集是不是合法,也會驗 method 的上限數。超過上限的話,app 還沒啟動就被這一步擋下,直接噴錯。dexopt 也會試著將所有 class/method 都讀進 VM 驗證,這自然會運用到 LinearAlloc buffer。如果 buffer 不夠也是直接噴了。所以程式太大的話,通通會死在 dexopt 這過程裡。

目標 method 數

好了,我們現在知道問題的成因,目標就很明確了:

  1. 開發進行期間,維持 method 數在 65536 以下 (未 proguard)
    開發時通常我們會用 Android 4.0 以上的手機來測,所以不用管 56000 method 數的限制。但要確保尚未做 proguard 之前,總 method 數要小於 65536。相信我,如果開發時每次 build 都要做 proguard 才能將 method 數壓在 65536 下,你會想死,每 build 一次都要幾分鐘以上啊。
  2. 正式發佈時,目標 method 數 56000 以下 (proguard 後)
    正式發佈 apk 時,proguard 這步驟通常會做。所以確保包給 Android 2.3 的版本經過 proguard 過後,method 數可以壓在 56000 以下即可。

注意 56000 這數字只是我們的經驗值,實際的情形可能有出入。

大量的 method 數哪來的

你的 App 也許才幾千個 method,不過你跑一下上面的 dexdump 指令,你可能會發現你已經用掉二、三萬的 method 了。有關這個問題,今年六月的時候有高手 @rotxed 詳細解說,請大家務必去讀一遍。本文只是按照那篇的建議,做一些延伸性的探討。

為了測試,我們用 Android Studio 開啟一個 target Android 2.3 空白專案,就選 Google Play Services Activity 吧。然後跑一下由 mihaip 開發的 dex-method-count 這個程式來計數。它可以列出 apk 中所有 package 下 method 的總數,來看看這個空白程式的結果吧:

Read in 30788 method IDs.
<root>: 30788
    android: 8923
        support: 6825
            v4: 4209
            v7: 2616
    java: 723
    javax: 5
    org: 75
        apache: 24
            http: 24
        json: 39
        xmlpull: 12
            v1: 12
    dalvik: 2
        system: 2
    com: 21057
        google: 21029
            ads: 124
            android: 20905
                gms: 20905

這個空白程式已經用掉 3 萬個 method 了,最大宗的是 com.google.android.gms,也就是 google play service (5.x 版),它直接吃掉 20905 個 method,這也是上面提到 @rotxed 文中的主要內容。不過值得注意的事,內建的 android.*java.* 這兩個 package 大概只佔 2000 個 method,但是 support library v4 和 v7 則吃掉 7000 個。如果你的 App 要 target Android 2.3 版,support v4 是支援 Fragment ,v7 則是支援 ActionBar,這兩個 library 都很難避免的。

內建 class 加上 support library v4, v7,你的 method 可用數直接少一萬

知名 library method 數

好了,現在我們心裡有數了,來看看知名的 Android library 的 method 數吧

library method count function
joda-time 4602 date time
com.fasterxml.jackson 8346 JSON
google-gson 881 JSON
com.squareup.okhttp 1301 http/spdy
com.squareup.picasso 445 image/network
volley 376 image/network
guava 13587 mighty tools
commons-io 1196 I/O tools
commons lang3 2415 general tools
dagger 268 Dependency Injection
protobuf 5310 protobuf
protobuf-lite 800 protobuf
square wire + okio 384 + 381 protobuf
com.amazonaws.services.s3 11798 AmazonAWS SDK for SD (2.0)
dropbox + misc libs 412 + 2864 Dropbox SDK
bouncycastle 8875 Crypto (required by Dropbox)

上面列的有點雜,包含網路類、圖形類、工具類、加密類、常見的 S3 和 Dropbox 等等,不過你可以發現有些工具硬是比其他同質性的大的多。

  • Json 的工具:我自己偏好使用 jackson,但看到它的 method 數要 8346,整個都傻了,我想 Android 還是比較適合用小很多的 google-gson。

  • 一般工具類:guava 這工具是很好用的,但是 method 數達到破表的 13587,而且由於它內部 class 交互依賴,所以 proguard 對它無效。因此 guava 算是直接出局了,不管有什麼理由你都不該在 Android 使用 guava。你必須用 commons-io, commons-lang3 之類的工具取代。

  • 取代 java Date 的工具:joda-time,4602 個 method,我覺得太多了,除非有大量的日期處理,比方說你寫的是日曆類的 App,才可以考慮它。不然還是建議用 commons langs3 與 android.text.format.DateUtils 就好了。

  • picasso 和 volley 都是不錯的圖片下載與顯示的工具,小而且好用。我們是選 volley,因為它兼具 image loader 與 http client 的功能。

  • protobuf 一般 app 應該比較少用,不過我們的 App Cubie 用很兇,如果你有需要 binary protocol ,一般也是推薦用 protobuf。原生的 protobuf library 吃掉 5000 個 method,很可怕,記得在 Android 要換成 lite 版的版本。但是原生 protobuf 產生的程式碼隨便就會超過 1000 個 method。所以建議在 Android 上整套換掉,改用 square 的 wire,它的 method 數少很多。

  • amazonaws S3 Android SDK method 數達 11798,嚇呆了,我只是要上傳個檔案到 S3 就要吃掉一萬個 method ? 對付這個 SDK,proguard 是有用的。但是開發的階段你可不想每次都跑 proguard 啊!怎麼辦?只能 手動 proguard -- 把它的 source code 拿出來,手動刪掉不要的 method,再包成新的 jar

  • Dropbox SDK 應該不少 App 都有機會用到,但是它依賴的 jar 很多,而且還加進 bouncycastle 這個 8875 method 的加密 library,總 method 數直接衝到 12151 個,我們有多少個一萬可以揮霍?

預防 method 數爆增

綜觀上面的統計,有個趨勢是,如果 library 本來是設計給 server-side Java 用的,它的 method 數動輒上千,破萬的也有。除非有必要,你不該在 Android 使用這些工具,你必須找專門設計給 Android 的 library。現在 Android 已經進入成熟期,有很多專門的工具了,花點心思就能找到。這裡特別推薦 square 這家公司出品的工具,很多都是小而質精,建議大家找工具時可以先逛逛他們的 github。

使用專門設計給 Android 的 library,不要找 server-side 的 Java library

我們前面提到一個空白的專案,撇開 google play service 不提,就要一萬個 method。而 google play service 則要價兩萬,不過靠一些工具可以減少到一萬。所以算起來還有四萬的額度,視情況你最少必須保留一萬個 method 給自己的 App。這樣就剩三萬,看起來很多,但看看上面的表,不小心加個 SDK 就花掉五千、一萬個 method,一下就沒了。

所以 library 要慎選啊。你要把額度留給你無法挑選的 library,像是 google play service 這種無法取代的。最後,給各位個參考,我們的 App Cubie,歷經三年的開發,不含任何 library 的 method 數現在達兩萬了。你可以想像我們現在到處在查程式中可以減 method 數的地方,快瘋了!

總結

由於 Android dex 的種種限制,造成 method 數量有上限的問題,再加上第三方 library 揮霍無度,使得即使你的 App 還很小,卻不小心會撞上這個 64k method 數限制。建議:

  1. 慎選第三方 library,最好採用前先用 dex-method-count 度量一下
  2. scala, groovy 這類非原生的開發語言想都不要想,它們只會帶來更多的問題。用非 Java 語言開發可以,但是僅限於 NDK
  3. 要替 Google play service 瘦身
  4. square 出品的 library 都很適合 Android 使用
  5. Android 2.3 的可用 method 數更少,約 56000,不過這只要 proguard 後達成即可。

如果什麼都做了,但還是超過 64k 怎麼辦?網路上有很多將 dex 拆成多個的方法,並且動態讀進 .dex 檔。那些做法一個比一個髒,而且未來 ART 上也不能再用了。雖然如此,拆多個 dex 是唯一解決方法,真的碰到了不用也得用。又或者,山不轉路轉,向老闆提出這個 App 該拆成多個來賣了。