跳到主要内容

解决 ANT 打包 65536 问题

如果通过 Unity 的 ANT 打包时,由于每个 dex 包的大小有限制 (每个 dex 最多支持的方法数为 65536),所以导致打包时出现方法数超过 65536 的报错。进一步了解这个问题,请参见 Google 网站

图片:Appendix_DexMerger_65536

解决这个问题有两种方法

  • 通过 Gradle 打包,并开启 MultiDex 选项,具体请参见 针对 MultiDex 配置应用
  • 手动合并 JAR 包和资源文件,并导入 multidex.jar

由于低版本的 Unity 不支持 Gradle 打包,只能通过 ANT 打包,因此只能手动合并 JAR 包和资源。但目前 Google 官网未提供相应的工具,为了解决这个问题,Player Network SDK 提供了 INTLUtils 工具 来进行 JAR 包和资源文件的打包。

手动合并原理

在 Android 编译过程中,会将所有的 Java 方法写入 dex 文件中,如果不开启 MultiDex,那么 APK 中就只有一个 classes.dex

在开启 MultiDex 后,Gradle 打包会将 Java 方法进行分组,将重要 (启动时需要) 的 Java 方法分组到 dex1 (在启动过程中需要的方法) 中 并将其他 Java 方法放置在 dex2 (classes2.dex),dex3 (classes3.dex),等。

手动合并的原理就是模拟 Gradle 分组打包的流程,对方法进行分组打包。

简单梳理后,可知处理流程是:

  1. Ant 工程分组
    分为分组1,分组2,...

  2. 手动合并分组 2,分组 3,和其他组为 dex2,dex3 等。

  3. 分组1放入 Unity 中

  4. Unity 打包输出 APK

  5. 将 dex2,dex3,等合到 APK 中。

  6. 将 APK 重签名。

合并流程

注意

ANT 工程不只包括了 Java 方法,还包括了 AndroidManifest.xml 文件和 资源 res 文件,需要让 Unity 打包到 APK 中。

INTLUtils 提供的功能包括:

  1. 将每个 Ant 工程的 JAR 包合并为 dex 文件

  2. 将每个 Ant 工程的 AndroidManifest 合并

  3. 将每个 Ant 工程的资源 res 文件合并

为了将 AndroidManifest.xml 文件和资源 res 文件合并,整理后的主要流程是:

  1. Ant 工程分组分为分组1,分组2,...

  2. 将除分组1 以外的分组放在一起,合并 AndroidManifest.xml 和 res 文件,并将合并的文件复制到分组 1。

  3. 分组2、分组3、… 手动合并为 dex2,dex3,...

  4. 分组1放入 Unity 中

  5. Unity 打包输出 APK

  6. 将 dex2,dex3,等合到 APK 中。

  7. 将 APK 重签名

操作指引

拷贝需要合并的包到 INTLUtils 目录

图片:Appendix_DexMerger_copy_Android

需要注意:

  1. 需要配置主 AndroidManifest.xml 文件。

    主 AndroidManifest.xml 文件中的 manifest 属性需要包括属性 xmlns:tools="http://schemas.android.com/tools",且 package 改为测试 demo 的包名。

    图片:Appendix_DexMerger_AndroidManifest

    图片:Appendix_DexMerger_AndroidManifest_File

  2. 需要将外层 JAR 包拷贝到 libs 目录下。

    libs 目录作为 Unity 的主要 JAR 目录,将随分组1 进行打包。

    需要将 multidex-1.0.3.jar 文件拷贝到 libs 目录下,multidex 务必要放在 dex1 中。

    图片:Appendix_DexMerger_libs

Ant 分组

将外层 AndroidManifest.xml 文件和 libs 目录需要放置分组1中,其他工程基本可以认为任意分组,分组的原则可以简单按 4 MB (不包括 .so 文档的大小,因此有些库一个 .so 就几兆,.so 是不会影响 Java 方法数的) 一个分组进行划分。保证一个 dex 中不会超过 65536 个方法数。

分组数量根据方法数量进行划分。

图片:Appendix_DexMerger_dex2

合并分支1以外的 AndroidManifest 和资源

  1. 将除分组1以外的分组(分组2、分组3、...)放在一起。

  2. 将分组1中的 AndroidManifest 文件拷贝到分支 1 以外的分组中 (主要用于将其他分组的 AndroidMainfest 写入到主 AndroidManifest),并创建 libs 文件夹。

    图片:Appendix_DexMerger_2-5

  3. 通过 INTLUtils 进行 AndroidManifest.xml 和资源合并。

    java -jar INTLUtils.jar -unityRoot ./2-5 -buildTools /Users/lucasfan/Library/Android/sdk/build-tools/28.0.3 -output ./output2-5

    图片:Appendix_DexMerger_output2-5

  4. 将生成的 AndroidManifest 和 res 拷贝到分组 1中。

    图片:Appendix_DexMerger_new1

分别生成 dex2,dex3,...

  1. 在每个分组增加一个空的 AndroidManifest.xml 和 libs 文件夹。

    AndroidManifest.xml 文件中的 manifest 属性需要包括属性xmlns:tools="http://schemas.android.com/tools",且package改为测试demo的包名。

  2. 通过 INTLUtils 进行 Java 方法合并。

    java -jar INTLUtils.jar -unityRoot ./2 -buildTools /Users/lucasfan/Library/Android/sdk/build-tools/28.0.3 -output ./output2

图片:Appendix_DexMerger_unity

将分组1放回 Unity 中

图片: Appendix DexMerger Unity

Unity 打包为 Android APK

图片:Appendix_DexMerger_Split

将 dex2,dex3,... 合到 APK 中

工具生成的 dex 文件名称均为 classes2.dex,需要根据第几个 dex,具体修改为 classesX.dex

使用 aapt 将 dex 文件合并到 APK 中

aapt a test.apk classes2.dex

必须要将 APK 和 dex 文件放到同级目录,这样才能保证将 dex 文件添加到 APK 的根目录下。

将 APK 重签名

apksigner sign --ks debug.keystore --ks-key-alias XXXX --ks-pass pass:XXXX --key-pass pass:XXXX -out test-signed.apk test.apk

注意,如果密码中有(!)等特殊字符,需加上\,例如:--ks-pass pass:\!1234

INTLUtils 参数说明

  • -mainProject, 主工程目录,如果设置了 unityRoot,可不设置。

  • -subProject,子工程 (目录),多个工程用空格隔开。如果设置了 unityRoot,无需设置。

  • -buildTools,Android build tools 目录 (必须)。

  • -output,输出目录 (可选)。

  • -package,目标 package 名称 (可选)。默认为主工程 package name。

  • -unityRoot,标准 Android Unity 工程目录 (可选)。设置该选项相当于同时设置了 -mainProject 和 -subProject。

  • -useD8,是否使用 D8 编译器 (可选)。默认使用 DX 编译器。开启时需要设置为 1

  • -androidJar,Android SDK 的android.jar 的路径,使用 D8 编译器时必须。路径为:android_sdk/platforms/api-level/android.jar

常见问题

依赖 JDK1.8 的 JAR 生成 dex 失败

使用 dx 工具编译 JAR 生成 dex 时将提示类似如下错误:

: invalid opcode ba - invokedynamic requires --min-sdk-version >= 26 (currently 13)

原因是 dx 不支持 JDK1.8 的 JAR 转为 dex,需要使用 D8 编译器。

useD8 设置为 1,并设置 androidJar。 可开启 D8 编译器。