Як вирішити проблему обмеження в 64К методів в Unity3D

чи Можете ви уявити гру для 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 — це технологія, що дозволяє використовувати в АПК кілька файлів 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.бібліотека'

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

Посилання
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.