Эта статья изначально была опубликована в Блоге разработчиков Kongregate.

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

Перво-наперво

На эту тему нет недостатка в сообщениях на форуме и в блогах. Самый важный вывод: если вам удастся комфортно удерживать свою игру ниже этого числа, вы избавите себя от многих проблем.

Знай свои плагины

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

Суперкраткая анатомия Android-плагинов Unity

Плагины Unity для Android обычно состоят из некоторого кода C# Unity, а также собственного кода и ресурсов Android. Собственный код и ресурсы будут упакованы либо в виде проекта библиотеки Android, либо в виде архива Android (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-файлов плагинов. Вы также можете просто разархивировать, удалить неиспользуемые классы и повторно заархивировать JAR.
  • Экспортируйте свой проект как проект Android, чтобы вы могли применить ProGuard или MultiDex. Здесь все становится рискованно.

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

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

Как только ваша игра окажется меньше 64 КБ и вы сможете снова сгенерировать APK, главное, на что нужно обратить внимание при тестировании вашей игры, — это ClassNotFoundException и VerifyError ошибки в logcat. Это указывает на то, что ваш код пытается использовать недоступный класс или метод. Обычно ошибка будет связана с вылетом, так что это будет довольно очевидно. Однако иногда подключаемый модуль может попытаться корректно завершить работу, и, хотя ваше приложение не аварийно завершает работу, некоторые функции, которые, как вы надеетесь, будут доступны, не будут работать должным образом.

ProGuard и MultiDex

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

Когда ничего не помогает, мы надеемся, что ProGuard поможет вам снизить лимит. Если нет, вы можете обратиться к MultiDex. MultiDex имеет дополнительную забастовку, работая только на уровне API 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. Вы все еще можете зайти в тупик.

Подход 1: простой экспорт в Unity/импорт из Android Studio

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

  1. В разделе Файл > Настройки сборки > Android установите флажок Проект Google Android и нажмите кнопку Экспорт. Создайте/выберите каталог для экспорта. Android будет хорошим выбором.
  2. Откройте Android Studio и выберите Импортировать проект (Eclipse ADT, Gradle и т. д.). Перейдите к вашему экспортированному проекту Unity, который будет подкаталогом вашего экспортного каталога (например, ./Android/Your Unity Project).
  3. Вам нужно будет выбрать каталог назначения. Вы можете оставить установленными различные флажки.

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

За и против

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

Подход 2: Импорт экспортированного проекта 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, что привело меня к третьему подходу.

Подход 3: Настройка проекта Gradle для экспортированного проекта Unity

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

Настройте оболочку Gradle

В каталоге, в который вы экспортировали свою игру, настройте оболочку Gradle с помощью следующей команды:

gradle wrapper --gradle-version 2.14.1

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

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

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

// 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()
    }
}

Создайте файл вашего приложения 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 вы можете выбрать Открыть существующий проект Android Studio. Найдите и откройте созданный вами файл 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 для каждого подкаталога.

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

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

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

Разрешение зависимостей

В некоторых случаях у вас может быть один библиотечный проект, зависящий от другого библиотечного проекта. Например, этот вывод отображается, потому что модуль MainLibProj зависит от игровых сервисов Google Play.

.../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. Мы можем использовать grep, чтобы выяснить, какие модули игровых сервисов Google Play содержат это значение.

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. Откройте <export_dir>/MainLibProj/build.gradle и обновите запись dependencies следующим образом:

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) поведение, от которого зависят собственные плагины сценариев постобработки, может неправильно транслироваться в экспортируемый проект.

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

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

Разрешение ограничения метода 64K DEX в вашем проекте Gradle

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

Включение 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 для вашей экспортированной сборки, добавьте следующие строки в раздел android файла Your Unity Project/build.gradle.

defaultConfig {
    minSdkVersion 15
    targetSdkVersion 24
    // Enabling multidex support.
    multiDexEnabled true
}

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

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

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

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

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

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

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

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

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

KEY_PASSWORD=XXXXXX ./gradlew assembleRelease

использованная литература