В этой статье мы поделимся нашим опытом работы с системой Addressables и расскажем о самых существенных проблемах, с которыми столкнулись при разработке мобильного приложения. Поэтому здесь будут только сложные и неочевидные проблемы, которые отняли у нас значительное время на поиск и решение. Мы будем использовать термины из Addressables – подразумевается, что вы знакомы с этой системой хотя бы поверхностно.
Заметка от партнера IT-центра МАИ и организатора магистерской программы “VR/AR & AI” — компании PHYGITALISM.
Предисловие
Разрабатывая мобильное приложение с дополненной реальностью для проекта, нам нужна была возможность обновлять контент без пересборки всего приложения. Было решено использовать систему Addressables, которая по сути является оберткой для простого использования бандлов. Это был наш первый опыт работы с этой системой, никто из команды ранее не использовал Addressables в проектах.
Забегая вперед хочется сказать, что все упомянутые проблемы оказались описаны в официальной документации. Хотя мы и старались учесть все нюансы, внимательно читать документацию и использовать примеры, некоторые вещи становятся понятны только при их практическом использовании. В связи с этим, мы хотим не показать правильный способ работы с Addressables, а поделиться опытом решения относительно сложных или неочевидных проблем.
Проблема 1. Дублирование обновленных бандлов или как занять всё место на CDN
Одной из первых проблем, с которой мы столкнулись, был механизм обновления бандлов в Unity. Точнее тот факт, что Unity не удаляет старые бандлы при создании новых версий.
При нажатии “Update a Previous build” Unity создает новые бандлы и кладет их в указанную папку, при этом старые бандлы не удаляются. То есть, обновив 2 раза бандл, у вас в папке будет 3 файла, каждый из которых является определенной версией.
Учтите это – если не удалять старые бандлы, место на вашем диске может очень быстро закончиться.
Несмотря на то, что Unity по умолчанию не удаляет старые бандлы, вы можете указать максимальный размер кэша. Когда вес всех бандлов превысит максимальный размер кэша, Unity может удалить предыдущие бандлы.
Проблема 2. Дублирование обновленных локальных бандлов или билд, который всегда растет
Из предыдущей проблемы происходит еще одна: как мы поняли, обновление бандлов создает много версий одного и того же бандла, занимая всё больше и больше места. Но если речь идет не про удаленные, а про локальные бандлы? Ведь локальные бандлы сохраняются в папку StreamingAssets.
Напомним: всё, что находится в StreamingAssets, вшивается в билд. Это значит, что, сделав несколько обновлений, мы получим в папке StreamingAssets много версий одного и того же бандла. Собрав билд, мы запакуем в него все эти версии. И несмотря на то, что использоваться будет только последняя, в билд будут вшиты и все предыдущие. Таким образом, с каждым обновлением и пересборкой вес билда будет постоянно увеличиваться, пока вы не удалите старые версии бандлов из StreamingAssets.
Проблема 3. Ошибка: NullReferenceException при попытке загрузить сцену или актив из локального бандла после обновления
До этого проблемы были скорее производственными и не столь критичными. Сейчас речь пойдет о допущении Unity, из-за которого (при его незнании) будут ломаться бандлы в билдах до обновления.
Изначально у нас в проекте были удаленные бандлы с главной сценой. Однако, из-за слабого мобильного Интернета эта сцена могла загружаться очень долго, и пользователь был вынужден ждать несколько минут на стартовом экране загрузки. Поэтому было решено изменить тип бандла на локальный – теперь бандл вшивался в билд, что исправило проблему с долгой загрузкой.
Разработка шла, мы меняли другие бандлы с контентом, меняли код, из-за чего почти всегда перестраивали все Addressables.
Приближаясь к релизу, нужно было протестировать всю систему: мы сделали сборку приложения, собрали Addressables – всё работало хорошо. Изменили картинки в удаленных бандлах, включили “обновление бандлов”, запустили билд (без пересборки), и тут:
NullReferenceException: Object reference not set to an instance of an object., stackTrace = UnityEngine.ResourceManagement.ResourceProviders.SceneProvider+SceneOp.InternalLoadScene...
Экран загрузки зависал на этой ошибке на этапе загрузки основной сцены (основная сцена находится в локальном бандле). При этом если создать новый билд, этой ошибки нет, и загрузка проходит без каких-либо проблем.
После разных тестов и поиска информации на форумах мы поняли, что эта проблема также встречается у некоторых разработчиков при загрузке сцен из локальных активов. Итогом “поисков” стал вывод:
Если изменить что-то в локальной группе, обновить Addressables (без пересборки), то собранный ранее билд “потеряет” обновленные локальные бандлы, выдавая при попытке обращения к ним ошибку NullReferenceException.
Но почему это происходит?
К счастью, мы используем свой сервер для CDN, поэтому мы могли проводить любые тесты, чтобы найти проблему. Мы попробовали удалить из CDN все файлы-каталоги. И на удивление это решило проблему: само собой, удаленные бандлы теперь тоже не обновлялись, но старый билд больше не показывал NullReferenceException, а успешно загружал локальную сцену.
Как оказалось, причина этой проблемы в том, что Unity никак не отделяет логику загрузки локальных и удаленных бандлов (в контексте этой проблемы). Все бандлы имеют свой адрес в catalog-файле. Не только удаленные, но и локальные. Что происходит, когда пользователь запускает приложение? Unity скачивает catalog с CDN сервера. Если каталога нет, Unity использует локальный каталог, который вшит в билд.
В локальном каталоге все бандлы имеют ссылки, которые были актуальными на момент создания билда. В удаленном каталоге все ссылки меняются при обновлении.
Загрузив удаленный каталог, Unity заменяет им локальный. И теперь все пути до бандлов берутся из нового каталога. Но так как мы обновили локальный бандл, теперь его путь отличается от первоначального.
Чтобы упростить понимание, давайте посмотрим на это на диаграмме:
Мы построили Addressables и создали билд №1. В этом билде есть наш локальный бандл с основной сценой (MS_), а локальный каталог имеет ссылку на него.
В CDN лежат удаленные бандлы с контентом и каталог идентичный тому, который вшит в билд.
Вот что случится, если Unity не сможет скачать новый каталог файл с сервера: Unity обратится к локальному каталогу за адресом бандла и просто загрузит его с диска, так как это локальный бандл.
Теперь запустим обновление (Update Addressables) и для наглядности перестроим билд (билд №2)
Здесь можно увидеть сразу несколько проблем. У нас есть билд №2, в котором лежат 2 локальных бандла, которые на самом деле являются двумя версиями бандла с основной сценой. То есть сейчас в локальном билде есть 2 версии основной сцены. И обе версии занимают место. При этом в локальном каталоге есть ссылка только на новую версию (222), тогда как старая версия (111) нигде не используется и висит мертвым грузом. Это наглядная иллюстрация первой описанной в этой статье проблемы.
Теперь посмотрим на CDN: здесь мы видим то же самое – у нас случилось дублирование удаленных бандлов. Теперь у нас есть сразу несколько версий нашего контента, который просто так занимает место на CDN, так как каталог-файл (идентичен локальному из билда №2) ссылается только на новые версии бандлов. Это значит, что мы могли бы удалить все первые версии (...1) без последствий и освободить место в хранилище.
Также важно понимать, что на этом этапе билд №1 ссылается на удаленный каталог, который теперь ссылается на вторую версию бандлов.
Теперь если мы попробуем запустить первый билд, он успешно обновит свой каталог и скачает новые версии удаленных бандлов (Content_, Icons_). Но что ему делать с локальным бандлом? И так, мы подошли к причине этой ошибки.
Вот что происходит с двумя билдами:
Билд №2: нужно загрузить сцену => нужно скачать новую версию каталога с сервера => прочитать путь к бандлу с нужной сценой => загрузить бандл по полученному пути. Так как путь локальный, загрузка идет напрямую с диска. Сцена загружена успешно.
Билд №1: нужно загрузить сцену => нужно скачать новую версию каталога с сервера => прочитать путь к бандлу с нужной сценой => попытка загрузить бандл по полученному пути. Так как путь локальный, загрузка идет напрямую с диска. Но в этом билде нет бандла MS_222, так как на момент сборки этого билда №1 такого бандла еще не существовало. На этом этапе и возникает ошибка NullReferenceException.
И так, мы нашли проблему: при обновлении бандлов ссылка на любой (в т. ч. локальный) бандл меняется для нового и старого билда, но сам бандл существует только в новом билде. Как теперь это исправить?
К сожалению, Unity предлагает довольно неудобный способ, однако он включает в себя "защиту от случайных ошибок".
Подробнее вы можете прочитать здесь, но если коротко: вы должны пометить ВСЕ свои ЛОКАЛЬНЫЕ группы как "Cannot Change Post Release".
Что именно делает эта настройка? Она сообщает Unity, что этот бандл не должен меняться после пересборки. На практике это означает, что, если вы измените что-то в этом бандле и выполните UpdateAddressables, для этого бандла обновление НЕ БУДЕТ ВЫПОЛНЕНО. То есть даже собрав новую сборку, локальный бандл не обновится. Чтобы обновить его, вам придется пересобрать Addressables полностью, не используя UpdateAddressables. Также есть другой способ, который, однако кажется очень некрасивым, так как создает бесполезные копии бандлов, занимая всё больше места в новых сборках (пока вы не выполните полную перестройку Addressables). Подробнее о нем можно прочитать здесь и здесь.
Послесловие
На удивление все эти проблемы описаны в официальной документации Addressables. Однако некоторые особенности системы "могут показаться нелогичными" (с).
Сама система Addressables кажется относительно стабильной. Главная проблема в том, что некоторые вещи сделаны очень неочевидно, нелогично и неудобно.
Надеемся, эта статья поможет вам сэкономить время и избежать нескольких неприятных проблем при работе с Addressables.
Ichimitsu
Именно последний вывод и заставляет отметать любые мысли о переходе на Addressables. Вроде бы удобно, вроде бы экономит трафик и место и память главное. Но... пока что переход с классических AssetBundle нецелесообразен, если не начинается проект с нуля и в нем огромное количество внешнего контента.