Can you imagine an Android game made in Unity that uses more than 64K Java methods? Could not this and the architects of the
byte code Dalvik . Perhaps they succeeded (I did not read the specifications), and other elements of the toolchain should be blamed. Be that as it may, if your game exceeds the limit of 64K methods on a DEX file, you will have to poke around in your native plugins and / or build process. This post is an attempt to show different ways to solve the problem.
Start over
There are very few
posts on forums and blogs on this topic. The most important conclusion is that if you manage to stay well below this number, it will save you from a lot of problems.
Understand your plugins
The most likely way to exceed this limit in Unity is to use native plugins. Native Android plugins are required in almost all Unity games. Unfortunately, some plugins are quite large. For example,
Google Play Game Services itself contains almost 25K methods. This is a significant chunk of 64K that you are limited to.
Super brief introduction to Android plugins under Unity
Android Unity plugins usually consist of Unity C # code and native Android code and resources. Native code and resources are packaged either as an Android library project (Library Project), or as an Android Archive archive (AAR) in the
Assets/Plugins/Android/
directory. Project libraries are the old way to transfer components to the Android system, and AAR is a newer one. You will encounter plugins using both methods.
')
Classes in both project libraries and AAR exist in JAR files, which are simple zip files from compiled Java class files. The AAR file is also a simple zip of various Android resources. Some of them will become
libs/*.jar
(also known as Java class archives). Library projects are simple directory structures, and the JAR, again, will be in
libs/*.jar
.
Stages of minimizing the number of methods
The only way to reduce the number of Java methods contained in the game's APK using the standard Unity build system is to delete or modify the JAR files included with the native Android plugins. An alternative way is to export the Unity project as an Android project, in which more powerful technologies can be applied.
Try each of the following techniques in turn:
- Remove all plugins that are not used by the game.
- Google has broken Play Services into a set of modules. Use only those that you really need.
- Use the Jar Jar Links tool with the zap rule to remove unnecessary classes from JAR files of plugins.
- Export the project as an Android project to apply ProGuard or MultiDex. But this method is quite dangerous.
Most blog posts focus only on the last paragraph, because at the time of writing there were not many resources that could help with this approach. Exporting to an Android project has a more negative impact on the development cycle and builds. As long as ProGuard and MultiDex are not directly supported in Unity, it’s better to use this method as a last resort.
What to look for when testing
When your game stops violating the 64K limit and you can generate the APK file again, the most important thing to look for in a
logcat when testing a game is the errors
ClassNotFoundException
and
VerifyError
. They mean that your code is trying to use an inaccessible class or method. Usually, an error causes the application to crash, so it will be pretty obvious. However, sometimes the plugin can continue to work without failure. In this case, some functions in the availability of which you are sure will not work correctly.
ProGuard and MultiDex
ProGuard is a tool used to obfuscate and remove unused classes and methods.
MultiDex is a technology that allows you to use multiple DEX files in your APK, thus removing the limit of 64K methods in the game. Unity does not directly support these technologies, but you can use them by exporting a project as an Android project.
If nothing else helps, ProGuard can help lower the maximum limit. If this fails, use MultiDex. MultiDex has another limitation - it works only in API Level 14 (4.0) and higher. It is natively supported in Android (5.0) and higher. For versions 4.X you need to use support libraries. In addition, MultiDex has a list of
known limitations .
Export to Android project
If you need ProGuard or MultiDex, the first step is to export the Unity project as an Android project. If your project is complex enough, this in itself can be a daunting task. Most likely, it will also mean the unavailability of Unity Cloud Build. However, with the right process, it may look like exporting to Xcode for iOS. After export, you need to set up an Android Studio or Gradle project, but this will be a one-time task. Re-exporting the project does not require new configuration of the Android assembly configuration.
I found three ways to successfully work with a project exported to Android. In brief, I will describe the first two, because they are simpler, and may be preferred if the project is not too complicated. The latter approach requires a little more manual setup, but this is probably the cleanest way to organize a project. It may also be the only option if you need MultiDex.
A couple of words of caution
Even after exporting the game to Android Studio, the plugins used by your game may depend on Unity post-processing scripts that are not translated into Android Studio or Gradle builds. This may lead you to a dead end.
First way: simple export from Unity and import to Android Studio
This method is suitable for games that do not use a lot of plug-ins. I hope that Unity and Android Studio will continue to improve this way.
- In the File -> Build Settings -> Android section, select the Google Android Project checkbox and click the Export button. Create or select a directory to export. I recommend choosing the Android directory.
- Open Android Studio and select Import project (Eclipse ADT, Gradle, etc.) . Navigate to the exported Unity project, which will be located in the subdirectory of the export directory (for example,
./Android/Your Unity Project
). - Select destination directory. All options can be left as is.
After that, if everything went well, you can run the project in Android Studio.
Advantages and disadvantages
- Plus : this is an easy way.
- Plus : the imported Android Studio project is also a standard Gradle project, providing easy integration of tasks performed in Gradle.
- Minus : each time you export from Unity and import into Android Studio, a completely new project is created. Any changes you make to the Studio project — for example, setting up ProGuard — must be made during each build. This is quite a serious impact on the development cycle.
- Less : if the project is very complicated, it may simply not make money without significant changes in the project of Android Studio.
Second way: import the exported Unity project from source
With this method, the exported Unity project is imported into Android Studio directly from the sources, and then manually updated various modules and dependencies. The difference with the first method is that instead of importing
/Android/Your Unity Project
importing
/Android
, and Android Studio is trying to configure modules for the main application and projects of each exported library.
The good side of this approach is that after setting up an Android Studio project, you can re-export a Unity project to the same directory. In this case, in general, an update of the Android Studio project is not required.
The disadvantage of this method is that the Android project will be associated with the files of the Android Studio project. Configuring and configuring dependencies will be challenging.
Since I want to focus on the third method, I’ll just say that after transferring the project to Android Studio, connecting ProGuard is quite simple. However, the process of setting up an Android Studio project involves correctly configuring each module and dependencies using the Android Studio interface. If you haven't mastered the Android Studio project modules very well, this can be quite a tricky task. In addition, configuring MultiDex through the interface of Android Studio seemed to me difficult, and this led me to the third method.
Third way: configuring the Gradle project for the exported Unity project
Gradle is a build tool that was used in Android several years ago. Android Studio projects can sync to Gradle projects. While old Android Studio project modules are still supported, new projects are based on Gradle files. In the third method, we will correctly configure the Gradle files for the exported Unity project, after which we will be able to work with them and perform builds from Android Studio or from the command line. We will get access to such useful functions of Gradle, as ProGuard and MultiDex.
Setting Gradle Wrapper
Set up the Gradle Wrapper in the directory with the exported project with the following command:
gradle wrapper
Gradle comes with Android Studio, so you need to have some version of it installed. The above command creates a
gradlew
script that binds your build script to a specific version of Gradle. At the moment, it is well suited
2.14.1
.
Create root build.gradle file
In the same directory, create your top-level Gradle
build.gradle
file. You can simply copy the following code:
Creating the build.gradle application file
Place the following file in the main project subdirectory created for the Unity project in the export directory (for example,
Android/Your Unity Project
). This file should also be called
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 file settings.gradle
In the root directory of the exported Android project, create a file
settings.gradle
with the following contents. Of course, you need to replace
:Your Unity Project
with the name of the directory created by Unity for the exported project.
include ':Your Unity Project'
If you have a super-simple Unity project without plug-ins, this will be enough for you. In Android Studio, you can select the
Open an existing Android Studio project . Then find and open the
settings.gradle
file you created and work with the project in Android Studio. You can also build a project from the command line as follows:
./gradlew assembleDebug
You can view the entire Gradle build task list:
./gradlew tasks
But my project was not so simple.
There is a possibility that you are reading this, because your project was not so simple. When exporting from Unity, in addition to the main application directory (for example, the
Android/Your Unity Project
), the engine creates a directory for each library project and AAR used by native plugins. AARs are extracted into the library's project format.
Add the following file to each subdirectory of library projects created when exporting from Unity. Name these files
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') } }
Then, in the
settings.gradle
file, add rules for each subdirectory.
include ':appcompat' include ':google-play-services_lib'
Finally, in the main application’s
build.gradle
file (for example,
Android/Your Unity Project/build.gradle
), change the dependencies section to include library projects.
dependencies { compile fileTree(dir: 'libs', include: '*.jar') compile project(':google-play-services_lib') }
Work with dependencies
In some cases, you may need one library project depending on another library project. For example, this is what is displayed when the
MainLibProj
module from Google Play Game Services depends.
.../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 unambiguous and fast rule for interpreting such dependencies, but in the general case the name of the missing resource gives a sufficient hint. In our case,
google_play_services_version
quite clearly to Google Play Game Services. You can use
grep to determine which of the modules of Google Play game services 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 that the resource is defined in
play-services-basement
, and it is referenced by
MainLibProj
. Open
<_>/MainLibProj/build.gradle
and change the entry with the dependency as follows:
dependencies { compile fileTree(dir: 'libs', include: '*.jar') compile project(':play-services-basement-9.4.0') }
Now Gradle knows that the
MainLibProj
module depends on
play-services-basement-9.4.0
.
Resolving Class Duplication Conflicts
When Unity exports plugins as library projects, the following errors often appear:
Dex: Error converting bytecode to dex: Cause: com.android.dex.DexException: Multiple dex files define Lcom/unity/purchasing/googleplay/BuildConfig;
The
BuildConfig
class
BuildConfig
generated by Android build tools. They are often included when a plugin is constructed as AAR, and a duplicate is created during the build process, when AAR is converted to a library project and recompiled. This error can be fixed by removing the class from the extended library project.
zip -d GooglePlay/libs/classes.jar "com/unity/purchasing/googleplay/BuildConfig.class" deleting: com/unity/purchasing/googleplay/BuildConfig.class
Since this will need to be done with each export, you may want to write a script to clear all JARs after export.
Alternative solution: use AAR, if it exists in the plugin, instead of the extracted library project created by Unity for AAR when exporting. In our example, we will find
GooglePlay.aar
, which is included in the
UnityPurchasing
plugin, and copy it to the new
aars
directory that we created in the tree of the exported project.
cp /Assets/Plugins/UnityPurchasing/Bin/Android/GooglePlay.aar <exported_proj>/aars/
Then we add the line to the root file
build.gradle
to add the new
aars
directory to the repository search path.
allprojects { repositories { jcenter() flatDir { dirs '../aars' } } }
Finally, add a dependency to
Your Unity Project/build.gradle
. Note that we use a slightly different format to refer to aar instead of the library project.
dependencies { compile fileTree(dir: 'libs', include: '*.jar') compile ':GooglePlay@aar' }
Other problems
There are many other issues that you may or may not encounter when converting an exported Unity project to Gradle / Android Studio. In general, these will be two classes of problems:
- conflicts between
AndroidManifest.xml
included in plugins - postprocess scripts behavior on which native plugins depend may be incorrectly translated into the exported project
The first type of problem occurs regularly in regular Unity builds during the process of merging manifests. Solving such problems requires configuring manifest entries. Usually errors tell where the conflict is found and give hints on how to resolve it. Whenever possible, it is better to resolve them in the main Unity project so as not to re-execute the steps on each export.
The second type, related to post-processing scripts, is much more complex and can become an obstacle for efficient work with the exported project. For his decision it is impossible to give general recommendations.
Solving the problem of 64K limiting DEX methods in the Gradle project
So, our Unity project is already in Gradle, now you can use ProGuard to try to make the number of methods less than 64K, or enable MultiDex to support more than 64K methods.
Enable ProGuard
You can write a separate post about setting up ProGuard for exported Unity projects. I'll show you how to add ProGuard to the Gradle build script. Add the following lines to the
android
section of the
Your Unity Project/build.gradle
to enable ProGuard for release builds.
buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-unity.txt' } }
We have specified two ProGuard configuration files - standard, supplied with the Android SDK (
proguard-android.txt
) and exported with the Unity project for Unity version 5.4 (
proguard-unity.txt
). Almost always you will need to maintain another ProGuard configuration file with rules defining which classes and methods should be saved for the plugins used by the game.
To disable ProGuard, you can simply change the value of
minifyEnabled
to
false
.
Enable MultiDex
To enable MultiDex for the exported assembly, add the following lines to the
android
section of the
Your Unity Project/build.gradle
.
defaultConfig { minSdkVersion 15 targetSdkVersion 24 // multidex. multiDexEnabled true }
This enables MultiDex support for devices running Android 5.0 and higher. To support devices for Android 4.0 and above, you need to make additional changes. First, add a new dependency to the
New Unity Project\build.gradle
to support the
com.android.support:multidex
library.
dependencies { compile 'com.android.support:multidex:1+' compile fileTree(dir: 'libs', include: '*.jar') // }
Then change the label
<application>
basically
AndroidManifest.xml
, specifying the support class
MultiDexApplication
.
<application android:name="android.support.multidex.MultiDexApplication" ... >
If the Unity project does not yet contain the main
AndroidManifest.xml
file, then you can add it to
/Assets/Plugins/Android/AndroidManifest.xml and
change the
application
label there to include it in future builds.
Complete application file build.gradle
Here’s what a complete build.gradle file looks like for a simple application with a simple dependency. A complex project exceeding 64K methods will probably contain much 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 // multidex. multiDexEnabled true } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-unity.txt' signingConfig signingConfigs.myConfig } } }
This snippet also adds entries needed to sign the application with the private key. The key password is retrieved from the environment variable. If everything worked out, you can build a game processed by ProGuard / MultiDex as follows:
KEY_PASSWORD=XXXXXX ./gradlew assembleRelease
Links