Кэширование текстур в SpriteKit
"Короче говоря: полагайтесь на набор Sprite, который сделает все за вас". -@LearnCocos2D
Вот длинная история, начиная с iOS 9.
Объемные данные изображения текстуры не хранятся непосредственно в объекте SKTexture
. (См. SKTexture
справочник по классам.)
SKTexture
откладывает загрузку данных до тех пор, пока это не потребуется. Создание текстуры даже из большого файла изображения выполняется быстро и требует мало памяти.
Данные для текстуры загружаются с диска обычно при создании соответствующего узла спрайта. (Или действительно, всякий раз, когда данные необходимы, например, когда вызывается метод size
).
Данные для текстуры подготавливаются для рендеринга во время (первого) прохода рендеринга.
SpriteKit имеет встроенное кэширование объемных данных изображения текстуры. Две особенности:
Объемные данные изображения текстуры кэшируются SpriteKit до тех пор, пока SpriteKit не захочет избавиться от них.
Согласно ссылке на класс SKTexture
: «Как только объект SKTexture готов к рендерингу, он остается готовым до тех пор, пока не будут удалены все сильные ссылки на объект текстуры».
В текущей iOS он имеет тенденцию задерживаться дольше, возможно, даже после того, как вся сцена исчезла. Комментарий StackOverflow цитирует службу технической поддержки Apple: «iOS освобождает память, кэшированную с помощью +textureWithImageNamed: или +imageNamed:, когда считает нужным, например, когда он обнаруживает нехватку памяти».
В симуляторе, выполняя тестовый проект, я смог увидеть текстурную память, восстановленную сразу после removeFromParent
. Однако при работе на физическом устройстве память, казалось, задерживалась; повторный рендеринг и освобождение текстуры не приводили к дополнительным обращениям к диску.
Интересно: может ли память рендеринга освобождаться раньше в некоторых критических для памяти ситуациях (когда текстура сохраняется, но не отображается в данный момент)?
SpriteKit разумно повторно использует кэшированные объемные данные изображения.
В моих экспериментах было сложно не использовать его повторно.
Допустим, у вас есть текстура, отображаемая в узле спрайта, но вместо повторного использования объекта SKTexture
вы вызываете [SKTexture
textureWithImageNamed:]
для того же имени изображения. Текстура не будет идентична по указателю исходной текстуре, но она будет совместно использовать объемные данные изображения.
Вышеизложенное верно независимо от того, является ли файл изображения частью атласа или нет.
Вышеупомянутое верно независимо от того, была ли исходная текстура загружена с использованием [SKTexture textureWithImageNamed:]
или с использованием [SKTextureAtlas
textureNamed:]
.
Другой пример: допустим, вы создаете объект атласа текстур, используя [SKTextureAtlas atlasNamed:]
. Вы берете одну из его текстур с помощью textureNamed:
и не сохраняете атлас. Вы отображаете текстуру в узле спрайта (и поэтому текстура надежно сохраняется в вашем приложении), но вы не беспокоитесь об отслеживании этого конкретного SKTexture
в кеше. Затем вы делаете все это снова: новый атлас текстур, новая текстура, новый узел. Все эти объекты будут недавно выделены, но они относительно легковесны. Между тем, и это важно: первоначально загруженные объемные данные изображения будут прозрачно распределяться между экземплярами.
Попробуйте это: вы загружаете атлас монстров по имени, а затем берете одну из его текстур орков и визуализируете ее в узле спрайтов орков. Затем плеер возвращается на главный экран. Вы кодируете узел orc во время сохранения состояния приложения, а затем декодируете его во время восстановления состояния приложения. (При кодировании он не кодирует свои двоичные данные, а вместо этого кодирует свое имя.) В восстановленном приложении вы создаете другого орка (с новым атласом, текстурой и узлом). Будет ли этот новый орк делиться своими громоздкими данными орка с расшифрованным орком? да. Да это оркинг будет.
Практически единственный способ заставить текстуру не повторно использовать данные изображения текстуры — это инициализировать ее с помощью [SKTexture
textureWithImage:]
. Конечно, возможно, UIImage
будет выполнять собственное внутреннее кэширование файла изображения, но в любом случае SKTexture
берет на себя управление данными и не будет повторно использовать данные рендеринга где-либо еще.
Если вкратце: если в вашей игре одновременно отображаются два одинаковых спрайта, можно с уверенностью сказать, что они эффективно используют память.
Соедините эти два пункта вместе: SpriteKit имеет встроенный кэш, который сохраняет важные объемные данные изображения и повторно использует их с умом.
Другими словами, это просто работает.
Не обещаю. В симуляторе, работающем с тестовым приложением, я могу легко доказать, что SpriteKit удаляет данные моей текстуры из кеша, прежде чем я действительно закончу с этим.
Однако во время прототипирования вы можете быть удивлены, обнаружив достаточно хорошее поведение вашего приложения, даже если вы никогда не используете повторно ни один атлас или текстуру.
SpriteKit тоже имеет кэширование Atlas
SpriteKit имеет механизм кэширования специально для текстурных атласов. Это работает следующим образом:
Вы вызываете [SKTextureAtlas atlasNamed:]
для загрузки атласа текстур. (Как упоминалось ранее, это еще не загружает объемные данные изображения.)
Вы надежно сохраняете атлас где-то в своем приложении.
Позже, если вы вызовете [SKTextureAtlas atlasNamed:]
с тем же именем атласа, возвращенный объект будет идентичен по указателю сохраненному атласу. Текстуры, извлеченные из атласа с помощью textureNamed:
, также будут идентичны указателю. (Обновление: текстуры не обязательно будут идентичны указателю в iOS10.)
Текстурные объекты, следует отметить, не сохраняют своих атласов.
Возможно, вы все еще хотите создать свой собственный кэш
Итак, я вижу, что вы все равно создаете свои собственные механизмы кэширования и повторного использования. Почему ты это делаешь?
В конечном счете, у вас будет больше информации о том, когда сохранять или удалять определенные текстуры.
Вам может понадобиться полный контроль над временем загрузки. Например, если вы хотите, чтобы ваши текстуры отображались мгновенно при первом рендеринге, вы будете использовать методы preload
из SKTexture
и SKTextureAtlas
. В этом случае вы должны сохранить ссылки на предварительно загруженные текстуры или атласы, верно? Или SpriteKit все равно будет кэшировать их для вас? Неясно. Пользовательский атлас или кеш текстур — хороший способ сохранить полный контроль.
В определенный момент оптимизации (не дай Бог преждевременно!!) имеет смысл перестать создавать новые объекты SKTexture
и/или SKTextureAtlas
снова и снова, какими бы легкими они ни были. Вероятно, вы бы сначала создали механизм повторного использования атласа, поскольку атласы менее легкие (в конце концов, у них есть словарь текстур). Позже вы можете создать отдельный механизм кэширования текстур для повторного использования объектов, не входящих в атлас SKTexture
. Или, может быть, вы никогда не доберетесь до второго. Ведь ты занят, а кухня сама себя не убирает, черт возьми.
Все это говорит о том, что ваше поведение при кэшировании и повторном использовании, вероятно, в конечном итоге будет ужасно похоже на поведение SpriteKit.
Как кэширование текстур SpriteKit влияет на дизайн вашего собственного кэша текстур? Вот что (сверху) нужно иметь в виду:
Вы не можете напрямую контролировать время выпуска объемных данных изображения при использовании именованных текстур. Вы освобождаете свои ссылки, а SpriteKit освобождает память, когда захочет.
Вы можете управлять временем загрузки объемных данных изображения, используя методы preload
.
Если вы полагаетесь на внутреннее кэширование SpriteKit, то вашему кешу атласа нужно только сохранять ссылки на объекты SKTextureAtlas
, а не возвращать их. Объект атласа будет автоматически повторно использоваться в вашем приложении.
Точно так же ваш кэш текстур должен только сохранять ссылки на объекты SKTexture
, а не возвращать их. Объемные данные изображения будут автоматически повторно использоваться в вашем приложении. (Однако это немного сбивает меня с толку; трудно проверить хорошее поведение.)
Учитывая последние два пункта, рассмотрите альтернативные варианты дизайна объекта одноэлементного кэша. Вместо этого вы можете сохранить используемые атласы на ваших объектах спрайтов или их контроллерах. Таким образом, на протяжении всего срока службы контроллера любые вызовы в вашем приложении atlasNamed:
будут повторно использовать идентичный указателю атлас.
Да, два идентичных указателя объекта SKTexture
совместно используют одну и ту же память, но из-за кэширования SpriteKit обратное не обязательно верно. Если вы отлаживаете проблемы с памятью и обнаруживаете два объекта SKTexture
, которые, как вы ожидали, должны быть идентичными указателю, но это не так, они все еще могут совместно использовать свои громоздкие данные изображения.
Тестирование
Я новичок в инструментах, поэтому я только что измерил общее использование памяти приложением в сборке релиза с помощью инструмента Allocations.
Я обнаружил, что «Вся куча и анонимная виртуальная машина» будут чередоваться между двумя стабильными значениями при последовательных запусках. Я запускал каждый тест несколько раз и в результате использовал наименьшее значение памяти.
Для тестирования у меня есть два разных атласа с двумя изображениями в каждом; назовите атласы A и B и изображения 1 и 2. Исходные изображения большие (одно 760 КиБ, одно 950 КиБ).
Атласы загружаются с использованием [SKTextureAtlas atlasNamed:]
. Текстуры загружаются с помощью [SKTexture textureWithImageNamed:]
. В приведенной ниже таблице загрузить на самом деле означает «вставить узел спрайта и выполнить рендеринг».
All Heap
& Anon VM
(MiB) Test
--------- ------------------------------------------------------
106.67 baseline
106.67 preload atlases but no nodes
110.81 load A1
110.81 load A1 and reuse in different two sprite nodes
110.81 load A1 with retained atlas
110.81 load A1,A1
110.81 load A1,A1 with retained atlas
110.81 load A1,A2
110.81 load A1,A2 with retained atlas
110.81 load A1 two different ways*
110.81 load A1 two different ways* with retained atlas
110.81 load A1 or A2 randomly on each tap
110.81 load A1 or A2 randomly on each tap with retained atlas
114.87 load A1,B1
114.87 load A1,A2,B1,B2
114.87 load A1,A2,B1,B2 with preload atlases
* Load A1 two different ways: Once using [SKTexture
textureWithImageNamed:] and once using [SKTextureAtlas
textureNamed:].
Внутренняя структура
Во время расследования я обнаружил некоторые правдивые факты о внутренней структуре текстур и объектов атласа в SpriteKit.
Интересно? Это зависит от того, какие вещи вас интересуют!
Структура текстуры из SKTextureAtlas
Когда атлас загружается [SKTextureAtlas atlasNamed:]
, проверка его текстур во время выполнения показывает повторное использование.
Во время сборки Xcode сценарий компилирует атлас из отдельных файлов изображений в несколько больших изображений листов спрайтов (ограниченных по размеру и сгруппированных по разрешению @1x, @2x, @3x). Каждая текстура в атласе ссылается на свое изображение листа спрайтов по пути пакета, хранящемуся в _imgName
(с _isPath
установленным true).
Каждая текстура в атласе индивидуально идентифицируется своим _subTextureName
и имеет вставку textureRect
в свой лист спрайтов.
Все текстуры в атласе, использующие одно и то же изображение листа спрайтов, будут иметь одинаковые ненулевые переменные _originalTexture
и _textureCache
.
Общий _originalTexture
, сам по себе являющийся объектом SKTexture
, предположительно представляет все изображение листа спрайтов. У него нет собственного _subTextureName
, а его textureRect
есть (0, 0, 1,
1)
.
Если атлас освободить из памяти, а затем перезагрузить, новая копия будет иметь другие SKTexture
объекты, другие _originalTexture
объекты и другие _textureCache
объекты. Из того, что я мог видеть, только _imgName
(то есть фактический файл изображения) соединяет новый атлас со старым атласом.
Структура текстуры не из SKTextureAtlas
Когда текстура загружается с использованием [SKTexture textureWithImageNamed:]
, она может исходить из атласа, но не из SKTextureAtlas
.
Текстура, загруженная таким образом, имеет отличия от приведенной выше:
У него короткий _imgName
, как у «giraffe.png», а _isPath
установлен в false.
Он имеет неустановленную _originalTexture
.
У него есть (видимо) свой собственный _textureCache
.
Два объекта SKTexture
, загруженные textureWithImageNamed:
(с одинаковым именем изображения), не имеют ничего общего, кроме _imgName.
Тем не менее, как подробно описано выше, этот тип конфигурации текстуры совместно использует громоздкие данные изображения с другим типом конфигурации текстуры. Это означает, что кэширование выполняется близко к реальному файлу изображения.
person
Karl Voskuil
schedule
09.05.2016