Androidでメソッド数が65536を超えた時の対処方法

Androidアプリでメソッド数が64k(65536)を超えるとビルド時 or インストール時にエラーになることがあります。ライブラリも含めたコード全体でカウントされるので、色々使ってると結構すぐに上限超えてしまいます。


結構めんどくさかったので対処法をまとめます。


#1. ビルド時にエラーになる場合
エラーが出てビルドすらできないことがあります。

##1. gradleでjumboModeオプションを有効にする
こんなエラーが出た時は、jumboModeを有効にすると解消できるかもしれません。

```
com.android.dex.DexException: Cannot merge new index 65576 into a non-jumbo instruction!
```

build.gradleに下記を追加して試してみましょう。

```build.gradle
android {
    dexOptions {
       jumboMode true
    }
}
```



##2. proguardを使う
こんなエラーが出た時は、頑張ってメソッドを減らすかProguardを使ってビルドするかしかないみたいです。メソッド数を減らすのは辛いので、私はProguardを使うようにしました。

```
java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536
```

####1. app/build.gradleに記述を追加
runProguardオプションをtrueにして、proguardFilesを指定します。

```build.gradle
android {
    buildTypes {
        release {
            runProguard true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
```


####2. proguard-rules.pro を作成
proguardファイルはアプリによると思うので割愛しますが、例えばgsonを使う場合はこんな感じで書かないと動作しなくなったりします。

```proguard-rules.pro
-keepattributes Signature
# Gson specific classes
-keep class sun.misc.Unsafe { *; }
-keep class com.google.gson.stream.** { *; }
```
proguardは調べて動かしながらがんばって作っていくしかなさそうです。



#2. インストール時にエラーになる場合
ビルドが成功してapkを作成できても、Android2でインストールエラーになることがあります。

```
pkg: /data/local/tmp/com.konifar
Failure [INSTALL_FAILED_DEXOPT]
```
[Facebookエンジニア](https://www.facebook.com/notes/facebook-engineering/under-the-hood-dalvik-patch-for-facebook-for-android/10151345597798920 "Facebookエンジニア")のノートに詳しい原因が書いてあります。

> During standard installation, a program called “dexopt” runs to prepare your app for the specific phone it’s being installed on. Dexopt uses a fixed-size buffer (called the “LinearAlloc” buffer) to store information about all of the methods in your app. Recent versions of Android use an 8 or 16 MB buffer, but Froyo and Gingerbread (versions 2.2 and 2.3) only have 5 MB. Because older versions of Android have a relatively small buffer, our large number of methods was exceeding the buffer size and causing dexopt to crash. 

どうやら通常のインストール時には『dexopt』というプログラムが実行されるらしく、その処理はすべてのメソッドの情報を一定サイズの領域に貯めこむらしいです。Android4以上の最近のバージョンだと8MB、16MBの領域が確保されているのですが、Android2.3以下のOSでは5MBしかなく、それに収まりきらないほどたくさんのメソッド数だとdexoptがクラッシュしてインストールエラーになるとのことです。


##1. メソッド数の確認
実際どのくらいのメソッド数なのかを確認します。[dex-method-count](https://github.com/mihaip/dex-method-counts "dex-method-count")というツールを使うと、簡単にapkのメソッド数を確認できます。

```shell
$ git clone https://github.com/mihaip/dex-method-counts.git
$ ./gradlew assemble # ビルド
$ ./dex-method-counts path/to/app.apk # or .zip or .dex or directory
```

こんな感じで出力されます。まずは出力してみると、節約できるところが見つかるかもしれません。

```shell
Read in 60366 method IDs.
<root>: 60366
    : 8
    android: 10815
        accessibilityservice: 6
        accounts: 8
        animation: 2
        app: 351
        bluetooth: 2
        content: 303

...(略)...

    com: 39448

...(略)...

        google: 18513
            ads: 165
                mediation: 134
                    admob: 24
                    customevent: 40
                    jsadapter: 38

```

##2. 不要なライブラリをexcludeする
build.gradleの依存関係に無駄があり、本来不要なライブラリをincludeしてしまっていることがあります。
自分の経験なのですが、`support-v4-13.0.0` と `support-v4-13.1.0` が両方インポートされていたことがあって、build.gradleのcompile部分にexcludeオプションをつけてsupportライブラリを入れないようにして対処しました。

```build.gradle
compile ('com.aviary.android.feather.sdk:aviary-sdk:3.4.3.351') {
    exclude module: 'support-v4'
}
```

##3. google play servicesの不要なパッケージを消す

2014/12/15（月）追記
最新の[google-play-services6.5](http://android-developers.blogspot.jp/2014/11/google-play-services-65.html)は、モジュール単位で利用可能なようですので問題ないようです。

GoogleAnalyticsやGoogleMapなどを利用できるライブラリ `com.google.android.gms:play-services` ですが、実は使っている機能は1つか2つということが多いのではないでしょうか。
`com.google`は18513個もメソッドがあるので、これを節約するだけでかなりメソッドの数が減ります。

####1. strip_play_services.gradle を作成する
[strip_play_services.gradle](https://gist.github.com/dmarcato/d7c91b94214acd936e42 "strip_play_services.gradle")というgradle taskが公開されているので、それを使います。`app/strip_play_services.gradle` を配置します。

```strip_play_services.gradle
// taken from https://gist.github.com/dmarcato/d7c91b94214acd936e42

def toCamelCase(String string) {
    String result = ""
    string.findAll("[^\\W]+") { String word ->
        result += word.capitalize()
    }
    return result
}

afterEvaluate { project ->
    Configuration runtimeConfiguration = project.configurations.getByName('compile')
    ResolutionResult resolution = runtimeConfiguration.incoming.resolutionResult
    // Forces resolve of configuration
    ModuleVersionIdentifier module = resolution.getAllComponents().find {
        it.moduleVersion.name.equals("play-services")
    }.moduleVersion
    String prepareTaskName = "prepare${toCamelCase("${module.group} ${module.name} ${module.version}")}Library"
    Task prepareTask = project.tasks.findByName(prepareTaskName)
    File playServiceRootFolder = prepareTask.explodedDir
    // Add the stripping to the existing task that extracts the AAR containing the original classes.jar
    prepareTask.doLast {
        // First create a copy of the GMS classes.jar
        copy {
            from(file(new File(playServiceRootFolder, "classes.jar")))
            into(file(playServiceRootFolder))
            rename { fileName ->
                fileName = "classes_orig.jar"
            }
        }
        // Then create a new .jar file containing everything from the first one except the stripped packages
        tasks.create(name: "stripPlayServices" + module.version, type: Jar) {
            destinationDir = playServiceRootFolder
            archiveName = "classes.jar"
            from(zipTree(new File(playServiceRootFolder, "classes_orig.jar"))) {
                exclude "com/google/android/gms/actions/**"
                exclude "com/google/android/gms/appindexing/**"
                exclude "com/google/android/gms/appstate/**"
                exclude "com/google/android/gms/analytics/**"
                exclude "com/google/android/gms/auth/**"
                exclude "com/google/android/gms/cast/**"
                exclude "com/google/android/gms/drive/**"
                exclude "com/google/android/gms/fitness/**"
                exclude "com/google/android/gms/games/**"
                exclude "com/google/android/gms/identity/**"
                exclude "com/google/android/gms/panorama/**"
                exclude "com/google/android/gms/plus/**"
                exclude "com/google/android/gms/security/**"
                exclude "com/google/android/gms/tagmanager/**"
                exclude "com/google/android/gms/wallet/**"
                exclude "com/google/android/gms/wearable/**"
//                exclude "com/google/ads/**"
//                exclude "com/google/android/gms/ads/**"
//                exclude "com/google/android/gms/gcm/**"
//                exclude "com/google/android/gms/location/**"
//                exclude "com/google/android/gms/maps/**"
            }
        }.execute()
        delete file(new File(playServiceRootFolder, "classes_orig.jar"))
    }
}
```

####2. app/build.gradleに1行追加する
build.gradleに `apply from` を追加します。

```build.gradle
apply from: 'strip_play_services.gradle'
```

####3. proguard-rules.proに1行追加する
Proguardを使っている場合には、proguard-rules.proに1行追加します。

```proguard-rules.pro
-dontwarn com.google.android.gms.**
```

####4. rebuildする
rebuildしてapkを作り直します。buildする前にcleanしておいた方がいいと思います。
[dex-method-count](https://github.com/mihaip/dex-method-counts "dex-method-count")を使ってメソッドの数を見ると、どのくらいメソッド数を節約できたかを確認できます。


以上。間違いや追記があれば指摘をお願いします！