Как работает кэширование и повторное использование SKTexture в SpriteKit?

В StackOverflow часто упоминается, что SpriteKit выполняет собственное внутреннее кэширование и повторное использование.

Если я не прилагаю никаких усилий для повторного использования текстур или атласов, какое поведение кэширования и повторного использования я могу ожидать от SpriteKit?


person Karl Voskuil    schedule 09.05.2016    source источник


Ответы (1)


Кэширование текстур в SpriteKit

"Короче говоря: полагайтесь на набор Sprite, который сделает все за вас". -@LearnCocos2D

Вот длинная история, начиная с iOS 9.

Объемные данные изображения текстуры не хранятся непосредственно в объекте SKTexture. (См. SKTexture справочник по классам.)

  • SKTexture откладывает загрузку данных до тех пор, пока это не потребуется. Создание текстуры даже из большого файла изображения выполняется быстро и требует мало памяти.

  • Данные для текстуры загружаются с диска обычно при создании соответствующего узла спрайта. (Или действительно, всякий раз, когда данные необходимы, например, когда вызывается метод size).

  • Данные для текстуры подготавливаются для рендеринга во время (первого) прохода рендеринга.

SpriteKit имеет встроенное кэширование объемных данных изображения текстуры. Две особенности:

  1. Объемные данные изображения текстуры кэшируются SpriteKit до тех пор, пока SpriteKit не захочет избавиться от них.

    • Согласно ссылке на класс SKTexture: «Как только объект SKTexture готов к рендерингу, он остается готовым до тех пор, пока не будут удалены все сильные ссылки на объект текстуры».

    • В текущей iOS он имеет тенденцию задерживаться дольше, возможно, даже после того, как вся сцена исчезла. Комментарий StackOverflow цитирует службу технической поддержки Apple: «iOS освобождает память, кэшированную с помощью +textureWithImageNamed: или +imageNamed:, когда считает нужным, например, когда он обнаруживает нехватку памяти».

    • В симуляторе, выполняя тестовый проект, я смог увидеть текстурную память, восстановленную сразу после removeFromParent. Однако при работе на физическом устройстве память, казалось, задерживалась; повторный рендеринг и освобождение текстуры не приводили к дополнительным обращениям к диску.

    • Интересно: может ли память рендеринга освобождаться раньше в некоторых критических для памяти ситуациях (когда текстура сохраняется, но не отображается в данный момент)?

  2. 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
comment
Браво. Это очень полезно. - person Womble; 16.07.2020