Можете ли вы представить игру для Android, сделанную в Unity, которая использует больше 64K методов Java? Не удалось это и архитекторам байт-кода Dalvik. Возможно, у них получилось (я не читал спецификации), и винить следует другие элементы тулчейна. Как бы то ни было, если ваша игра превышает ограничение в 64K методов на файл DEX, вам придётся ковыряться в своих нативных плагинах и/или процессе сборки. Этот пост является попыткой показать различные способы решения проблемы.

Начнём сначала


По этой теме очень мало постов на форумах и в блогах. Самый важный вывод из них — если вам удастся удержаться значительно ниже этого числа, это убережёт вас от множества проблем.

Разберитесь в своих плагинах


Наиболее вероятный способ превышения этого предела в Unity — использование нативных плагинов. Нативные плагины Android необходимы практически во всех играх Unity. К сожалению, некоторые плагины довольно большие. Например Google Play Game Services сам по себе содержит почти 25K методов. Это значительный кусок от 64K, которыми вы ограничены.

Сверхкраткое введение в плагины Android под Unity


Плагины Android под Unity обычно состоят из C#-кода Unity и нативного кода и ресурсов Android. Нативный код и ресурсы упаковываются либо как проект библиотеки Android (Library Project), либо как архив Android Archive (AAR) в каталоге Assets/Plugins/Android/. Библиотеки проектов — это старый способ передачи компонентов в систему Android, а AAR — более новый. Вы столкнётесь с плагинами, использующими оба способа.

Классы и в библиотеках проектов, и в AAR существуют в файлах JAR, которые являются простыми файлами zip из скомпилированных файлов классов Java. Файл AAR — это тоже простой zip различных ресурсов Android. Некоторые из них станут libs/*.jar (также известными как архивы классов Java). Проекты библиотек — это простые структуры каталогов, а JAR, повторюсь, будут в libs/*.jar.

Этапы минимизации количества методов


Единственный способ уменьшения количества методов Java, содержащихся в APK игры, с помощью стандартной системы сборки Unity — удаление или модификация файлов JAR, включенных вместе с нативными плагинами Android. Альтернативный способ — экспорт проекта Unity как проекта Android, в котором можно применить более мощные технологии.

Попробуйте каждую из следующих техник по очереди:

  • Удалите все плагины, которые не используются игрой.
  • Google разбила Play Services на набор модулей. Используйте только те, которые вам действительно нужны.
  • Используйте инструмент Jar Jar Links с правилом zap для удаления ненужных классов из файлов JAR плагинов.
  • Экспортируйте проект как проект Android, чтобы применить ProGuard или MultiDex. Но этот способ довольно опасен.

Большинство постов в блогах фокусируется только на последнем пункте, потому что на момент написания было не так много ресурсов, способных помочь в этом подходе. Экспорт в проект Android более негативно влияет на цикл разработки и сбоки. Пока ProGuard и MultiDex не поддерживаются в Unity напрямую, лучше использовать этот способ как последнюю надежду.

На что обращать внимание при тестировании


Когда ваша игра перестанет нарушать ограничение в 64K и вы сможете снова сгенерировать файл APK, самым важным будет искать в logcat при тестировании игры ошибки ClassNotFoundException и VerifyError. Они означают, что ваш код пытается использовать недоступный класс или метод. Обычно ошибка приводит к «вылету» приложения, так что она будет довольно очевидной. Однако иногда плагину удаётся продолжить работу без сбоя. В таком случае какие-то функции, в доступности которых вы уверены, будут работать неправильно.

ProGuard и MultiDex


ProGuard — это инструмент, используемый для обфускации и удаления неиспользуемых классов и методов. MultiDex — это технология, позволяющая использовать в APK несколько файлов DEX, таким образом снимая ограничение в 64K методов в игре. Unity не поддерживает напрямую эти технологии, но их можно использовать, экспортировав проект как проект Android.

Если ничто другое не помогает, ProGuard может помочь опуститься ниже максимального предела. Если это не удастся, используйте MultiDex. MultiDex имеет ещё одно ограничение — она работает только в API Level 14 (4.0) и выше. Она нативно поддерживается в Android (5.0) и выше. Для версий 4.X нужно использовать библиотеки поддержки. К тому же, у MultiDex есть список известных ограничений.

Экспорт в проект Android


Если вам необходимы ProGuard или MultiDex, первым шагом будет экспорт проекта Unity как проекта Android. Если ваш проект достаточно сложен, это само по себе может стать устрашающей задачей. Вероятнее всего, это также будет означать недоступность Unity Cloud Build. Однако при правильном процессе это может быть похожим на экспорт в XCode для iOS. После экспорта потребуется настройка проекта Android Studio или Gradle, но это будет разовой задачей. Повторный экспорт проекта не требует новой настройки конфигурации сборки Android.

Я нашёл три способа успешной работы с проектом, экспортированным в Android. Вкратце я опишу первые два, потому что они проще, и могут быть предпочтительными, если проект не слишком сложный. Последний подход требует немного больше ручной настройки, но это, возможно, самый «чистый» способ организации проекта. Также он может быть единственным вариантом, если вам нужна MultiDex.

Пара слов предостережения


Даже после экспорта игры в Android Studio плагины, используемые вашей игрой, могут зависеть от скриптов постпроцессинга Unity, которые не транслируются в сборки Android Studio или Gradle. Это может привести вас в тупик.

Первый способ: простой экспорт из Unity и импорт в Android Studio


Этот способ подходит для игр, использующих не очень много плагинов. Надеюсь, что Unity и Android Studio продолжат усовершенствовать этот способ.

  1. В разделе File -> Build Settings -> Android установите флажок Google Android Project и нажмите кнопку Export. Создайте или выберите каталог для экспорта. Рекомендую выбрать каталог Android.
  2. Откройте Android Studio и выберите Import project (Eclipse ADT, Gradle, etc.). Перейдите к экспортированному проекту Unity, который будет находиться в подкаталоге каталога экспорта (например, ./Android/Your Unity Project).
  3. Выберите каталог назначения. Все опции можно оставить как есть.

После этого, если всё прошло хорошо, вы сможете запускать проект в Android Studio.

Плюсы и минусы


  • Плюс: это простой способ.
  • Плюс: импортированный проект Android Studio также является стандартным проектом Gradle, обеспечивающим упрощённую интеграцию выполняемых в Gradle задач.
  • Минус: при каждом экспорте из Unity и импорте в Android Studio создаётся совершенно новый проект. Любые изменения, вносимые в проект Studio – например, настройка ProGuard – должны выполняться во время каждой сборки. Это довольно серьёзно влияет на цикл разработки.
  • Минус: если проект очень сложен, он может просто не заработать без значительных изменений в проекте Android Studio.

Второй способ: импорт экспортированного проекта Unity из исходников


При этом способе экспортированный проект Unity импортируется в Android Studio непосредственно из исходников с последующим ручным обновлением различных модулей и зависимостей. Разница с первым способом заключается в том, что вместо импорта /Android/Your Unity Project импортируется /Android, а Android Studio пытается настроить модули для основного приложения и проектов каждой экспортированной библиотеки.

Хорошая сторона такого подхода в том, что после настройки проекта Android Studio можно повторно экспортировать проект Unity в тот же каталог. При этом в общем случае обновление проекта Android Studio не требуется.

Недостаток этого способа в том, что проект Android будет связан с файлами проекта Android Studio. Конфигурирование и настройка зависимостей станут сложной задачей.

Поскольку я хочу сосредоточиться на третьем способе, я просто скажу, что после переноса проекта в Android Studio подключить ProGuard довольно просто. Однако процесс настройки проекта Android Studio включает в себя правильную настройку каждого модуля и зависимости с помощью интерфейса Android Studio. Если вы не очень хорошо освоили модули проекта Android Studio, это может стать довольно хитрой задачей. Кроме того, конфигурирование MultiDex через интерфейс Android Studio показалось мне сложным, и это привело меня к третьему способу.

Третий способ: конфигурирование проекта Gradle для экспортированного проекта Unity


Gradle — это инструмент сборки, который стал использоваться в Android несколько лет назад. Проекты Android Studio могут синхронизироваться с проектами Gradle. Хотя старые модули проектов Android Studio всё ещё поддерживаются, новые проекты базируются на файлах Gradle. При третьем способе мы корректно настроим файлы Gradle для экспортированного проекта Unity, после чего сможем работать с ними и выполнять сборки из Android Studio или из командной строки. Мы получим доступ к таким полезным функциям Gradle, как ProGuard и MultiDex.

Настройка Gradle Wrapper


Настроим Gradle Wrapper в каталоге с экспортированным проектом следующей командой:

gradle wrapper --gradle-version 2.14.1

Gradle поставляется с Android Studio, поэтому у вас должна быть установлена какая-то его версия. Приведённая выше команда создаёт скрипт gradlew, привязывающий скрипт вашей сборки к определённой версии Gradle. На данный момент хорошо подойдёт 2.14.1.

Создание корневого файла build.gradle


В том же каталоге создайте свой файл Gradle build.gradle верхнего уровня. Можно просто скопировать следующий код:

// Файл сборки верхнего уровня, в который можно добавлять опции конфигурации, общие для всех подпроектов и модулей.
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

Создание файла приложения build.gradle


Поместите следующий файл в подкаталог основного проекта, созданный для проекта Unity в каталоге экспорта (например, Android/Your Unity Project). Этот файл тоже должен называться 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')
    }
}

Создание файла settings.gradle


В корневом каталоге экспортированного проекта Android создайте файл settings.gradle со следующим содержимым. Разумеется, нужно заменить :Your Unity Project на имя каталога, созданного Unity для экспортированного проекта.

include ':Your Unity Project'

Если у вас сверхпростой проект Unity без плагинов, этого вам будет достаточно. В Android Studio можно выбрать Open an existing Android Studio project. Затем найти и открыть созданный вами файл settings.gradle и работать с проектом в Android Studio. Также можно собрать проект из командной строки следующим образом:

./gradlew assembleDebug

Можно просмотреть весь список задач сборки Gradle:

./gradlew tasks

Но мой проект оказался не таким простым


Есть вероятность, что вы читаете это, потому что ваш проект оказался не настолько простым. При экспорте из Unity в дополнение к каталогу основного приложения (например, Android/Your Unity Project) движок создаёт каталог для каждого проекта библиотеки и AAR, использованных нативными плагинами. AAR извлекаются в формат проекта библиотеки.

Добавьте следующий файл в каждый подкаталог проектов библиотек, созданный при экспорте из Unity. Снова назовите эти файлы 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')
    }
}

Затем в файл settings.gradle добавьте правила для каждого подкаталога.

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

И наконец, в файле build.gradle основного приложения (например, Android/Your Unity Project/build.gradle) измените раздел зависимостей, включив проекты библиотек.

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

Работа с зависимостями


В некоторых случаях вам может потребоваться один проект библиотеки, зависящий от другого проекта библиотеки. Например, вот что выводится при зависимости модуля MainLibProj от 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').

Не существует однозначного и быстрого правила для интерпретации таких зависимостей, но в общем случае имя отсутствующего ресурса даёт достаточную подсказку. В нашем случае google_play_services_version довольно чётко указывает на Google Play Game Services. Можно использовать grep, чтобы определить, какие из модулей Google Play game services содержат это значение.

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>

Мы видим, что ресурс определён в play-services-basement, и на него ссылается MainLibProj. Откройте <каталог_экспорта>/MainLibProj/build.gradle и измените запись с зависимостью следующим образом:

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

Теперь Gradle знает, что модуль MainLibProj зависит от play-services-basement-9.4.0.

Разрешение конфликтов дублирования классов


Когда Unity экспортирует плагины как проекты библиотек, часто появляются следующие ошибки:

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

Класс BuildConfig генерируется инструментами сборки Android. Они часто включаются, когда плагин конструируется как AAR, а дубликат создаётся в процессе сборки, когда AAR конвертируется в проект библиотеки и перекомпилируется. Эту ошибку можно исправить, удалив класс из расширенного проекта библиотеки.

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

Так как это нужно будет делать при каждом экспорте, возможно, вам захочется написать скрипт для очистки всех JAR после экспорта.

Альтернативное решение: использовать AAR, если он существует в плагине, вместо извлечённого проекта библиотеки, создаваемого Unity для AAR при экспорте. В нашем примере мы найдём GooglePlay.aar, который включён в плагин UnityPurchasing, и скопируем его в новый каталог aars, созданный нами в дереве экспортированного проекта.

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

Затем мы добавим строку в корневой файл build.gradle, чтобы добавить новый каталог aars в путь поиска репозитория.

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

И наконец, добавим зависимость в Your Unity Project/build.gradle. Учтите, что мы используем немного другой формат для ссылки на aar вместо проекта библиотеки.

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

Другие проблемы


Есть много других проблем, с которыми вы встретитесь или не встретитесь при конвертировании экспортированного проекта Unity в Gradle/Android Studio. В общем случае, это будут два класса проблем:

  1. конфликты между AndroidManifest.xml, включёнными в плагины
  2. behavior скриптов постпроцессинга, от которых зависят нативные плагины, может неправильно транслироваться в экспортированный проект

Первый тип проблем регулярно возникает и в обычных сборках Unity в процессе слияния манифестов. Решение таких проблем требует настройки записей манифестов. Обычно ошибки сообщают, где обнаружен конфликт, и дают подсказки, как разрешить его. По возможности лучше разрешать их в основном проекте Unity, чтобы не выполнять повторно этапы при каждом экспорте.

Второй тип, относящийся к скриптам постпроцессинга, гораздо более сложный, и может стать препятствием для эффективной работы с экспортированным проектом. Для его решения невозможно дать общих рекомендаций.

Решение проблемы с ограничением в 64K методов DEX в проекте Gradle


Итак, наш проект Unity уже в Gradle, теперь можно использовать ProGuard, чтобы попытаться сделать количество методов меньше 64K, или включить MultiDex для поддержки более 64K методов.

Включение ProGuard


О настройке ProGuard для экспортированных проектов Unity можно написать отдельный пост. Я покажу, как добавить ProGuard в скрипт сборки Gradle. Добавьте следующие строки в раздел android файла Your Unity Project/build.gradle, чтобы включить ProGuard для релизных сборок.

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

Мы указали два файла конфигурации ProGuard — стандартный, поставляемый с Android SDK (proguard-android.txt) и экспортированный с проектом Unity для версии Unity 5.4 (proguard-unity.txt). Почти всегда нужно будет поддерживать ещё один файл конфигурации ProGuard с правилами, определяющими, какие классы и методы должны сохраняться для плагинов, используемых игрой.

Для отключения ProGuard можно просто изменить значение minifyEnabled на false.

Включение MultiDex


Чтобы включить MultiDex для экспортированной сборки, добавьте следующие строки в раздел android файла Your Unity Project/build.gradle.

defaultConfig {
    minSdkVersion 15
    targetSdkVersion 24

    // Включаем поддержку multidex.
    multiDexEnabled true
}

Так включается поддержка MultiDex для устройств под Android 5.0 и выше. Для поддержки устройств под Android 4.0 и выше нужно внести дополнительные изменения. Во-первых, добавим новую зависимость в New Unity Project\build.gradle для поддержки библиотеки com.android.support:multidex.

dependencies {
    compile 'com.android.support:multidex:1+'
    compile fileTree(dir: 'libs', include: '*.jar')
    // другие зависимости
}

Затем изменим метку
<application>
в основном AndroidManifest.xml, указав класс поддержки MultiDexApplication.

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

Если проект Unity ещё не содержит основного файла AndroidManifest.xml, то можно добавить его в /Assets/Plugins/Android/AndroidManifest.xml and и изменить метку application там, чтобы он был включён в будущие сборки.

Полный файл приложения build.gradle


Вот как выглядит полный файл build.gradle для простого приложения с простой зависимостью. Сложный проект с превышением 64K методов наверняка будет содержать гораздо больше зависимостей.

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
      }
    }

}

Этот сниппет также добавляет записи, необходимые для подписи приложения закрытым ключом. Пароль ключа извлекается из переменной среды. Если всё получилось, можно собрать игру, обработанную ProGuard/MultiDex следующим образом:

KEY_PASSWORD=XXXXXX ./gradlew assembleRelease

Ссылки


Поделиться с друзьями
-->

Комментарии (2)


  1. burjui
    06.11.2016 04:27

    Можете ли вы представить игру для Android, сделанную в Unity, которая использует больше 64K методов Java? Не удалось это и архитекторам байт-кода Dalvik. Возможно, у них получилось (я не читал спецификации), и винить следует другие элементы тулчейна.

    Если вы пройдёте по указанной вами ссылке и найдёте описание инструкций **invoke**, то увидите, что индекс вызываемого метода — 16-битный:
    invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB
    6e: invoke-virtual
    6f: invoke-super
    70: invoke-direct
    71: invoke-static
    72: invoke-interface

    A: argument word count (4 bits)
    B: method reference index (16 bits)
    C..G: argument registers (4 bits each)

    Разработчики виртуальной машины решили сэкономить на длине инструкции — очевидно, в угоду меньшего размера и большей скорости выполнения кода. Впрочем, если у вас на мобильном устройстве работает виртуальная машина, вы уже достаточно «позаботились» о скорости выполнения. Благослови Тюринг технологию AOT!


  1. stepanp
    06.11.2016 23:45
    +1

    В этой проблеме прекрасно все: и сам факт ограничения в 64К в vm, и то что play services, написанные разработчиками хухле, которые должны быть вкурсе этой проблемы содержат аж 25К методов, и то что вообще получается так что 64К методов не хватает для unity, в которой java когда почти нет