Kongregate Developers Blog

Dealing with the 64K Method Limit in Your Unity3D Game

Could you ever imagine a Unity Android game that used more than 64K Java methods? Neither could the architects of the Dalvik bytecode. Perhaps they did (I haven’t read the spec) and the blame falls on other elements in your toolchain. The long and short of it is if your game taps against the 64K method limit per DEX file you’re going to need to get down and dirty with your native plugins and/or build workflow. This post will attempt to walk you through the various ways to deal with it.

First Things First

There’s no shortage of forum posts and blog entries on this subject. The most
important takeaway is if you can manage to keep your game comfortably below this number, you will save yourself a lot of trouble.

Know Your Plugins

The most common way to hit this limit in Unity is through the use of native plugins. Android native plugins are a necessity for almost all Unity games. Unfortunately, some plugins are quite large. Google Play Game Services, for example, is close to 25K methods on its own, a significant chunk of the 64K you are allotted.

Super Brief Anatomy of Unity Android Plugins

Unity Android Plugins typically consist of some C# Unity code along with native Android code and resources. The native code and resources will be packaged either as an Android Library Project or Android Archive (AAR) under the Assets/Plugins/Android/ directory. Library Projects are the old way of sharing components in the Android ecosystem and AARs are the newer way. You will find plugins that use both.

The classes in both Library projects and AARs exist in JAR files, which are simple zips of compiled Java class files. The AAR file is also simply a zip of various Android resources, some of which will be libs/.jar (a.k.a. Java class archives). Library projects are simple directory structures, and again the JARs will be in libs/.jar.

Steps to Minimize Method Counts

The only way to reduce the number of Java methods included in your game's APK using the standard Unity build system is to remove or modify the JAR files included with your Android native plugins. The alternative is to export your Unity Project as an Android Project where you can apply more advanced techniques.

You should try each of the following techniques in order:

  • Remove any plugins your game is not using
  • Since Google has broken Play Services into a set of modules, only include the ones you actually use.
  • Use the Jar Jar Links tool with the zap rule to remove unneeded classes from the plug-ins JARs. You could also simply unzip, delete unused classes, and rezip the JAR.
  • Export your project as an Android Project so you can apply ProGuard or MultiDex. This is where things get dicey.

Most of this blog post will focus on the last bit because, at the time of writing, there aren’t a lot of resources that walk you through this approach. Exporting as an Android Project will be more disruptive to your development and build cycle. Until ProGuard or MultiDex are directly supported by Unity, you're best off going down this path as a last resort.

What to Look for When Testing

Once you have your game under the 64K limit and are able to generate the APK again, the key thing to look for while testing your game is ClassNotFoundException and VerifyError errors in logcat. This indicates your code is trying to use some class or method which is unavailable. Usually the error will be associated with a crash, so it’ll be quite obvious. Sometimes, however, the plugin may attempt to fail gracefully and, though your app does not crash, some feature you hope to be available will not function as expected.

ProGuard and MultiDex

ProGuard is a tool used to obfuscate and trim unused classes and methods. MultiDex is technology that enables multiple DEX files within your APK, thus removing the 64K method limit for your game. Unity does not have direct support for either of these techniques, but you can make use of them if you export your project to an Android Project.

When all else fails, ProGuard will hopefully bring you below the limit. If not, you can turn to MultiDex. MultiDex has the added strike of only working in API Level 14 (4.0) and up. It is natively supported in Android (5.0) and up. Support libraries must be used for 4.X. Finally, MultiDex comes with a set of Known Limitations.

Exporting to an Android Project

If you need to use ProGuard or MultiDex, the first step is to export your Unity Project as an Android Project. Depending on the complexity of your Project, this in itself can be a daunting task. It also likely means no more Unity Cloud Build. When done correctly, however, it can work similarly to exporting to XCode for iOS. The Android Studio or Gradle project will need to be set up after the export, but this should be a one-time task. You will be able to re-export your project without having to set up the Android build configuration again.

I’ve found three ways to successfully work with a project exported to Android. I’ll briefly cover the first two because they are simpler and may be preferable if your project is not too complex. The last approach requires a little more manual setup, but is probably the cleanest way to organize your project. It also may be your only option if you need MultiDex.

A Few Words of Caution

Even after exporting your game to Android Studio, it’s possible that the plugins your game uses depend on Unity Post Process scripts that will not translate to Android Studio or Gradle builds. You may still hit a dead end.

Approach 1: Simple Unity Export/Android Studio Import

This approach will work for games that do not use too many plugins. I imagine Unity and Android Studio will continue to improve this approach.

  1. Under File -> Build Settings -> Android click the Google Android Project checkbox and the Export button. Create/Choose a directory to export. Android would be a good choice.
  2. Open Android Studio and choose Import project (Eclipse ADT, Gradle, etc.). Navigate to your exported Unity Project, which will be a subdirectory of your export directory (e.g. ./Android/Your Unity Project).
  3. You will need to choose a destination directory. You may leave the various checkboxes checked.

At this point, if all goes well, you should be able to run the project within Android Studio.

Pros & Cons

  • Pro: It’s simple.
  • Pro: The imported Android Studio project is also a standard Gradle project, allowing easier integration of Gradle-based tasks.
  • Con: Every time you export from Unity and import into Android Studio, a brand new project is created. Any manipulation you need to make to the studio project – for example, configure ProGuard – will need to be done every time you build. This would pretty severely impact your development cycle.
  • Con: Depending on the complexity of your project, it simply may not work without significant modification to the Android Studio Project.

Approach 2: Import the Exported Unity Project from Source

In this approach you directly import the exported Unity Project into Android Studio from sources and then manually update the various modules and dependencies. The difference from the first approach is rather than importing /Android/Your Unity Project, you import /Android, and Android Studio will attempt to set up modules for your main application and each exported library project.

The nice thing about this approach is that once you have the Android Studio project set up, you can re-export the Unity Project to the same location and in general will not need to update Android Studio Project.

The downside to this approach is that your Android Project will be tied to Android Studio Project files. Configuring and managing the dependencies will be a challenge.

Since I’d like to focus on the third approach, I’ll simply say once you have your project in Android Studio, it isn’t too difficult to enable ProGuard. However, the process of setting up the Android Studio Project involves correctly configuring each of the modules and dependencies using Android Studio’s UI. Depending on your familiarity with Android Studio project modules, this could be a tricky task. Further, I found getting MultiDex configured through the Android Studio UI challenging, which led me to the third approach.

Approach 3: Configuring a Gradle Project for an Exported Unity Project

Gradle is the build tool Android settled into a few years back. Android Studio Projects may be synced with Gradle Projects. Though old Android Studio Project modules are still supported, new projects are based on Gradle files. In this approach we correctly set up the Gradle files for the exported Unity Project, at which point we can either work with them and build from Android Studio or from the command line. We are given access to useful Gradle tasks such as ProGuard and MultiDex.

Set Up the Gradle Wrapper

In the directory where you exported your game, set up the Gradle wrapper with the following command:

gradle wrapper --gradle-version 2.14.1

Gradle comes with Android Studio, so you should have some version of it installed. The above command will create a gradlew script that will lock your build script to a specific version of Gradle. At the moment 2.14.1 is a good choice.

Create Root build.gradle file

In the same directory, create your top level Gradle file build.gradle. You may simply copy and paste the following:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

Create Your Application build.gradle File

Place the following file in the main project sub-directory created for your Unity Project under your export directory (e.g. Android/Your Unity Project). This file also must be named build.gradle.

apply plugin: 'com.android.application'

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
}

android {
    compileSdkVersion 24
    buildToolsVersion "24"

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
            jniLibs.srcDirs = ['libs']
        }

        debug.setRoot('build-types/debug')
        release.setRoot('build-types/release')
    }
}

Create Your settings.gradle File

Back in the root directory of your exported Android project, create a settings.gradle file with the following contents. Of course, replace :Your Unity Project with whatever the directory Unity created for your exported project.

include ':Your Unity Project'

At this point, if you had a super-simple Unity Project with no plugins, you should be good to go. Within Android Studio you may choose Open an existing Android Studio project. Navigate to and open the settings.gradle file you created and work with your project within Android Studio. You may also build your project from the command line like so:

./gradlew assembleDebug

You can see the complete list of Gradle build tasks with:

./gradlew tasks

My Project Wasn’t That Simple :(

Chances are if you are reading this, it’s because your project wasn’t that simple. When you export from Unity, in addition to a main app directory (e.g. Android/Your Unity Project), it creates a directory for each library project and AAR used by your native plugins. For the AARs, they were extracted into Library project format.

Add the following file for each library project subdirectory created by the Unity export. Again, name this file(s) build.gradle

apply plugin: 'com.android.library'

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
}

android {
    compileSdkVersion 24
    buildToolsVersion "24"
    publishNonDefault true

    defaultConfig {
        minSdkVersion 9
        targetSdkVersion 24
    }

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
        }

        debug.setRoot('build-types/debug')
        release.setRoot('build-types/release')
    }
}

Next, back in your settings.gradle file, add include rules for each subdirectory.

include ':appcompat'
include ':google-play-services_lib'

Finally, back in the build.gradle file for your main application (e.g. Android/Your Unity Project/build.gradle), update the dependencies section to include the library projects.

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
    compile project(':google-play-services_lib')
}

Resolving Dependencies

In some cases, you may have one library project that depends on another library project. For example, this output is shown because the MainLibProj module depends on Google Play Game Services.

.../MainLibProj/build/intermediates/manifests/aapt/release/AndroidManifest.xml:31:28-65: AAPT: No resource found that matches the given name (at 'value' with value '@integer/google_play_services_version').

There is no hard and fast rule to interpret these dependencies, but in general, the name of the missing resource gives you a pretty good clue. For this case, googleplayservices_version pretty clearly points to the Google Play Game Services. We can use grep to figure out which of the Google Play game services modules contain this value.

grep -r  google_play_services_version .
./MainLibProj/AndroidManifest.xml:            android:value="@integer/google_play_services_version" />
...
./play-services-basement-9.4.0/res/values/values.xml:    <integer name="google_play_services_version">9452000</integer>

We see the resource is defined in play-services-basement and referenced by MainLibProj. Open up <export_dir>/MainLibProj/build.gradle and update the dependencies entry like so:

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
    compile project(':play-services-basement-9.4.0')
}

Now Gradle knows the MainLibProj module depends on play-services-basement-9.4.0.

Resolving Duplicate Class Conflicts

When Unity exports the plugins as Library projects, it’s not unusual to see errors along these lines:

Dex: Error converting bytecode to dex:
Cause: com.android.dex.DexException: Multiple dex files define Lcom/unity/purchasing/googleplay/BuildConfig;

The BuildConfig class is generated by Android build tools. They are often included when a plugin is constructed as an AAR and then a second one is created by your build process when the AAR is converted to a library project and re-compiled. You can fix this by deleting the class from the expanded library project.

zip -d GooglePlay/libs/classes.jar "com/unity/purchasing/googleplay/BuildConfig.class"
deleting: com/unity/purchasing/googleplay/BuildConfig.class

Since you’ll need to do this every time you export, you probably want to write script to clean up all the JARs after export.

An alternative solution is to use the AAR, if one exists for the plugin, rather than the extracted Library Project Unity creates for the AAR when exporting. For this example, we find GooglePlay.aar, which is included with the UnityPurchasing plugin, and copy it over to a new aars directory we create in our exported project tree.

cp /Assets/Plugins/UnityPurchasing/Bin/Android/GooglePlay.aar <exported_proj>/aars/

Next we add a line to our root build.gradle file to add the new aars directory to the repository search path.

allprojects {
    repositories {
        jcenter()
        flatDir { dirs '../aars' }
    }
}

Finally, add the dependency to Your Unity Project/build.gradle. Note that we use a slightly different format to reference the aar instead of the library project.

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
    compile ':GooglePlay@aar'
}

Other Issues

There is a host of other issues you may or may not encounter converting your exported Unity Project to Gradle/Android Studio. In general, the two classes of issues include (1) conflicts between the AndroidManifest.xml included by plugins and (2) the behavior the post-process scripts native plugins depend on may not translate properly to the exported project.

The former will typically occur in regular Unity builds as well, during the manifest merging task. Resolving them requires tweaking the manifest entries. Usually, the errors tell you what the conflict is and provide some clue about how to resolve them. If possible, it’s better to resolve these in the main Unity Project, so you don’t need to re-perform the steps every time you export.

The second issue regarding post process scripts is a lot more tricky, and may end up being a blocker for effectively working with the exported project. There are no general guidelines here.

Resolving 64K DEX Method Limit in Your Gradle Project

Now that we have our Unity Project in Gradle, we can use ProGuard to attempt to bring our method count below 64K, or we can enable MultiDex to support greater than 64K.

Enabling ProGuard

A whole separate blog post could be written on how to configure ProGuard for exported Unity Projects. Here we’ll show how to add ProGuard to your Gradle build script. Add the following to the android section of Your Unity Project/build.gradle to enable ProGuard for release builds.

buildTypes {
  release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-unity.txt'
  }
}

We specified two ProGuard configuration files – the standard one that’s included with the Android SDK (proguard-android.txt) and one which is exported with the Unity Project as of Unity 5.4, (proguard-unity.txt). You almost certainly need to maintain another ProGuard configuration file with rules specifying which classes and methods need to be kept for the plugins your game uses.

To disable ProGuard, simply change the value of minifyEnabled to false.

Enabling MultiDex

To enable MultiDex for your exported build, add the following lines to the android section of Your Unity Project/build.gradle.

defaultConfig {
    minSdkVersion 15
    targetSdkVersion 24

    // Enabling multidex support.
    multiDexEnabled true
}

This will enable MultiDex support on Android 5.0 and up devices. To support Android 4.0 and up devices, you must make a few additional modifications. First, add a new dependency for the support library com.android.support:multidex to New Unity Project\build.gradle.

dependencies {
    compile 'com.android.support:multidex:1+'
    compile fileTree(dir: 'libs', include: '*.jar')
    // other dependencies
}

Next, update the <application> tag in your main AndroidManifest.xml to specify the MultiDexApplication support class.

<application android:name="android.support.multidex.MultiDexApplication"
... >

If your Unity Project does not already have a main AndroidManifest.xml file, you probably want to add one to /Assets/Plugins/Android/AndroidManifest.xml and update the application tag there, so it’s included with future exports.

Complete Application build.gradle File

Here’s what the complete build.gradle file looks like for our simple application with a single dependency. A complex project bumping against the 64K method limit will likely have quite a bit more dependencies.

apply plugin: 'com.android.application'

dependencies {
    compile 'com.android.support:multidex:1+'
    compile fileTree(dir: 'libs', include: '*.jar')
    compile ':GooglePlay@aar'
}

android {
    compileSdkVersion 24
    buildToolsVersion "24"

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
            jniLibs.srcDirs = ['libs']
        }
        debug.setRoot('build-types/debug')
        release.setRoot('build-types/release')

      signingConfigs {
        myConfig {
          storeFile file("<path-to-key>/private_keystore.keystore")
          storePassword System.getenv("KEY_PASSWORD")
          keyAlias "<your_key_alias>"
          keyPassword storePassword
        }
      }
    }

    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 24

         // Enabling multidex support.
         multiDexEnabled true
    }

    buildTypes {
      release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-unity.txt'
        signingConfig signingConfigs.myConfig
      }
    }

}

This snippet also adds entries required to sign your app with a private key. The key password is drawn from an environment variable. If all is good, you can build your minified/multidexed game like so:

KEY_PASSWORD=XXXXXX ./gradlew assembleRelease

References