рисовать или не рисовать (когда следует использовать drawRect / Core Graphics против подпредставлений / изображений и почему?)

142

Чтобы прояснить цель этого вопроса: я знаю, КАК создавать сложные представления как с подпредставлениями, так и с использованием drawRect. Я пытаюсь полностью понять, когда и почему использовать один над другим.

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

Я часто путаюсь с изучением того, как сделать прокрутку табличного представления действительно плавной и быстрой. Конечно, первоначальный источник этого метода от автора за твиттер для iPhone (ранее tweetie). В основном это говорит о том, что для того, чтобы сделать скроллинг таблицы плавным, секрет состоит в том, чтобы НЕ использовать подпредставления, а вместо этого делать все рисование в одном пользовательском интерфейсе. По сути, кажется, что использование большого количества подпредставлений замедляет рендеринг, потому что у них много накладных расходов, и они постоянно перекомпоновываются в родительские представления.

Честно говоря, это было написано, когда 3GS был довольно новеньким, и с тех пор iDevices стали намного быстрее. Тем не менее , этот метод будет регулярно предлагается на межсетях и в других местах для высоких столов производительности. Фактически, это предложенный метод в Таблице примера кода Apple , предложенный в нескольких видеороликах WWDC ( Практическое рисование для разработчиков iOS ) и во многих книгах по программированию для iOS .

Есть даже потрясающие инструменты для разработки графики и генерации кода Core Graphics для них.

Итак, сначала я считаю, что есть причина, по которой существует Core Graphics. Это БЫСТРО!

Но как только я думаю, что у меня появляется идея: «По возможности выбирайте базовую графику», я начинаю понимать, что drawRect часто отвечает за плохую отзывчивость в приложении, чрезвычайно дорогую память и действительно нагружает процессор. По сути, я должен « избегать переопределения drawRect » ( Производительность iOS-приложения WWDC 2012 : графика и анимация )

Так что, как и все, это сложно. Может быть, вы можете помочь себе и другим понять, когда и зачем использовать drawRect?

Я вижу пару очевидных ситуаций для использования Core Graphics:

  1. У вас есть динамические данные (пример графика акций Apple)
  2. У вас есть гибкий элемент пользовательского интерфейса, который не может быть выполнен с простым изменяемым размером изображения
  3. Вы создаете динамическую графику, которая после визуализации используется в нескольких местах.

Я вижу ситуации, чтобы избежать Core Graphics:

  1. Свойства вашего вида должны быть анимированы отдельно
  2. У вас сравнительно небольшая иерархия представлений, поэтому любые дополнительные усилия по использованию компьютерной графики не стоят
  3. Вы хотите обновить части представления, не перерисовывая все это
  4. Макет ваших подпредставлений должен обновляться при изменении размера родительского представления.

Так дарите свои знания. В каких ситуациях вы используете DrawRect / Core Graphics (это также может быть достигнуто с помощью подпредставлений)? Какие факторы приводят вас к этому решению? Как / почему рисование в одном пользовательском представлении рекомендуется для плавной прокрутки ячеек таблицы, но Apple рекомендует использовать drawRect для повышения производительности в целом? А как насчет простых фоновых изображений (когда вы создаете их с помощью CG против изображений с изменяемым размером PNG)?

Глубокое понимание этого предмета может и не понадобиться для создания достойных приложений, но я не люблю выбирать методы, не имея возможности объяснить почему. Мой мозг злится на меня.

Обновление вопроса

Спасибо всем за информацию. Некоторые уточняющие вопросы здесь:

  1. Если вы рисуете что-то с помощью основной графики, но можете добиться того же с помощью UIImageViews и предварительно отрендеренного png, следует ли вам всегда идти по этому пути?
  2. Подобный вопрос: особенно с такими крутыми инструментами , когда вы должны рассмотреть возможность рисования элементов интерфейса в основной графике? (Возможно, когда отображение вашего элемента является переменным. Например, кнопка с 20 различными цветовыми вариациями. Есть ли другие случаи?)
  3. Учитывая мое понимание в моем ответе ниже, можно ли получить тот же прирост производительности для ячейки таблицы, эффективно захватив растровое изображение снимка вашей ячейки после самого сложного рендеринга UIView, и отображая его при прокрутке и скрытии сложного представления? Очевидно, что некоторые части должны быть разработаны. Просто интересная мысль у меня была.
Боб Сприн
источник

Ответы:

72

Придерживайтесь UIKit и subviews, когда вы можете. Вы можете быть более продуктивными и использовать преимущества всех ОО-механизмов, которые проще поддерживать. Используйте Core Graphics, когда вы не можете получить нужную вам производительность из UIKit, или вы знаете, что попытка взломать эффекты рисования в UIKit будет более сложной.

Общий рабочий процесс должен заключаться в создании табличных представлений с подпредставлениями. Используйте инструменты для измерения частоты кадров на самом старом оборудовании, которое поддерживает ваше приложение. Если вы не можете получить 60fps, перейдите в CoreGraphics. Когда вы делаете это некоторое время, вы понимаете, когда UIKit, вероятно, пустая трата времени.

Итак, почему Core Graphics работает быстро?

CoreGraphics не очень быстро. Если он используется все время, вы, вероятно, идете медленно. Это богатый API рисования, который требует, чтобы его работа выполнялась на процессоре, в отличие от большой работы UIKit, которая выгружается в графический процессор. Если бы вам пришлось анимировать шар, движущийся по экрану, было бы ужасной идеей вызывать setNeedsDisplay для просмотра 60 раз в секунду. Таким образом, если у вас есть подкомпоненты вашего представления, которые нужно индивидуально анимировать, каждый компонент должен быть отдельным слоем.

Другая проблема заключается в том, что, когда вы не выполняете пользовательское рисование с drawRect, UIKit может оптимизировать фондовые представления, поэтому drawRect не может использоваться или он может использовать ярлыки при компоновке. Когда вы переопределяете drawRect, UIKit должен идти медленным путем, потому что он понятия не имеет, что вы делаете.

Эти две проблемы могут быть перевешены преимуществами в случае ячеек табличного представления. После вызова drawRect, когда представление впервые появляется на экране, содержимое кэшируется, и прокрутка - это простой перевод, выполняемый графическим процессором. Поскольку вы имеете дело с одним представлением, а не со сложной иерархией, оптимизация drawRect в UIKit становится менее важной. Таким образом, узким местом становится то, насколько вы можете оптимизировать чертеж Core Graphics.

Всякий раз, когда вы можете, используйте UIKit. Сделайте простейшую реализацию, которая работает. Профиль. Когда есть стимул, оптимизируйте.

Бен Сандофски
источник
53

Разница в том, что UIView и CALayer по существу имеют дело с фиксированными изображениями. Эти изображения загружаются на видеокарту (если вы знаете OpenGL, воспринимайте изображение как текстуру, а UIView / CALayer - как многоугольник, показывающий такую ​​текстуру). Как только изображение попадает в графический процессор, оно может быть нарисовано очень быстро, и даже несколько раз, и (с небольшим снижением производительности) даже с различными уровнями альфа-прозрачности поверх других изображений.

CoreGraphics / Quartz - это API для генерации изображений. Он берет пиксельный буфер (опять же, подумайте об OpenGL текстуре) и изменяет отдельные пиксели внутри него. Все это происходит в оперативной памяти и на процессоре, и только после завершения кварца изображение «сбрасывается» обратно в графический процессор. Этот способ получения изображения из графического процессора, его изменения, а затем загрузки всего изображения (или, по крайней мере, сравнительно большой его части) обратно в графический процессор довольно медленный. Кроме того, фактическое рисование, которое делает Quartz, хотя и очень быстро для того, что вы делаете, намного медленнее, чем то, что делает GPU.

Это очевидно, учитывая, что GPU в основном перемещается вокруг неизменных пикселей большими кусками. Кварц выполняет произвольный доступ к пикселям и разделяет процессор с сетью, звуком и т. Д. Кроме того, если у вас есть несколько элементов, которые вы рисуете с помощью Кварца одновременно, вы должны перерисовать все из них при изменении одного, а затем загрузить В целом, если вы измените одно изображение и затем вставите его в другие изображения с помощью UIViews или CALayers, вы сможете без труда загружать гораздо меньшие объемы данных в графический процессор.

Если вы не реализуете -drawRect:, большинство представлений можно просто оптимизировать. Они не содержат пикселей, поэтому не могут ничего рисовать. Другие представления, такие как UIImageView, рисуют только UIImage (который, опять же, по сути, является ссылкой на текстуру, которая, вероятно, уже загружена в графический процессор). Таким образом, если вы рисуете один и тот же UIImage 5 раз, используя UIImageView, он загружается в графический процессор только один раз, а затем выводится на дисплей в 5 разных местах, что экономит наше время и ресурсы ЦП.

Когда вы реализуете -drawRect:, это приводит к созданию нового изображения. Затем вы рисуете это на процессоре, используя Quartz. Если вы рисуете UIImage в вашем drawRect, он, вероятно, загружает изображение из графического процессора, копирует его в изображение, на которое вы рисуете, и, когда вы закончите, загружает эту вторую копию изображения обратно на видеокарту. Таким образом, вы используете в два раза больше памяти GPU на устройстве.

Таким образом, самый быстрый способ рисования, как правило, состоит в том, чтобы отделить статический контент от изменяющегося контента (в отдельных подклассах UIViews / UIView / CALayers). Загрузите статический контент как UIImage и нарисуйте его, используя UIImageView, и поместите контент, сгенерированный динамически во время выполнения, в drawRect. Если у вас есть контент, который рисуется многократно, но сам по себе не меняется (т. Е. 3 значка, которые отображаются в одном и том же слоте для обозначения некоторого статуса), также используйте UIImageView.

Одно предостережение: есть такая вещь, как слишком много UIViews. Особенно прозрачные области отнимают больше графического процессора, потому что они должны смешиваться с другими пикселями позади них при отображении. Вот почему вы можете пометить UIView как «непрозрачный», чтобы указать графическому процессору, что он может просто стереть все за этим изображением.

Если у вас есть контент, который генерируется динамически во время выполнения, но остается неизменным в течение всего времени жизни приложения (например, метка, содержащая имя пользователя), может иметь смысл просто нарисовать все это один раз, используя Quartz, с текстом, границы кнопки и т. д., как часть фона. Но обычно это оптимизация, которая не нужна, если только приложение Instruments не скажет вам иначе.

uliwitness
источник
Спасибо за подробный ответ. Надеюсь, что больше людей прокрутят вниз и проголосуют так же.
Боб Сприн
Хотелось бы, чтобы я проголосовал дважды. Спасибо за отличную рецензию!
Морское побережье Тибета
Незначительная коррекция: В большинстве случаев нет вниз нагрузок от GPU, но загрузка все еще в основном медленной операция вы можете задать GPU , чтобы сделать, потому что он должен передавать большие пиксельные буфера с более медленной оперативной памяти в VRAM быстрее. OTOH, как только изображение появляется, его перемещение включает в себя отправку нового набора координат (несколько байтов) в GPU, чтобы он знал, где рисовать.
Uliwitness
@uliwitness Я просматривал ваш ответ, и вы упомянули пометку объекта как «непрозрачный, стирающий все, что скрывается за этим изображением». Не могли бы вы пролить свет на эту часть?
Рих
@Rikh Если пометить слой как непрозрачный, это означает, что a) в большинстве случаев графический процессор не будет беспокоить рисование других слоев под этим слоем, ну, по крайней мере, те их части, которые находятся под непрозрачным слоем. Даже если они были нарисованы, он фактически не будет выполнять альфа-смешивание, а просто скопирует содержимое верхнего слоя на экране (обычно полностью прозрачные пиксели в непрозрачном слое заканчиваются черным или, по крайней мере, непрозрачной версией их цвета)
Uliwitness
26

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

Основной подход

Совершенно очевидно, что общий подход, как отметил Бен Сандофски в своем ответе , должен звучать так: «Всякий раз, когда вы можете, используйте UIKit. Делайте простейшую реализацию, которая работает. Профиль. Когда есть стимул, оптимизируйте».

Почему

  1. В iDevice существует два основных узких места: процессор и графический процессор
  2. CPU отвечает за начальное рисование / рендеринг вида
  3. GPU отвечает за большинство анимации (Core Animation), эффектов слоев, композитинга и т. Д.
  4. UIView имеет много оптимизаций, кэширования и т. Д., Встроенных для обработки сложных иерархий представлений.
  5. При переопределении drawRect вы упускаете множество преимуществ, предоставляемых UIView, и это обычно медленнее, чем позволить UIView обрабатывать рендеринг.

Рисование содержимого ячеек в одном плоском UIView может значительно улучшить ваш FPS на прокручиваемых таблицах.

Как я уже говорил выше, CPU и GPU - два возможных узких места. Поскольку они обычно работают с разными вещами, вы должны обратить внимание на то, с каким узким местом вы сталкиваетесь. В случае с прокруткой таблиц, Core Graphics не рисует быстрее, и поэтому может значительно улучшить ваш FPS.

Фактически, Core Graphics вполне может быть медленнее, чем вложенная иерархия UIView для начального рендеринга. Тем не менее, похоже, что типичная причина прерывистой прокрутки в том, что у вас узкое место в GPU, поэтому вам нужно устранить это.

Почему переопределение drawRect (с использованием основной графики) может помочь при прокрутке таблицы:

Из того, что я понимаю, графический процессор не отвечает за начальный рендеринг представлений, а вместо этого передает текстуры или растровые изображения, иногда с некоторыми свойствами слоя, после их рендеринга. Затем он отвечает за наложение растровых изображений, рендеринг всех этих эффектов слоя и большую часть анимации (Core Animation).

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

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

Боб Сприн
источник
1
Осторожнее, хотя. Графические процессоры эффективно воссоздают все дерево слоев для каждого кадра. Таким образом, перемещение слоя практически не требует затрат. Если вы создаете один большой слой, перемещение этого слоя происходит быстрее, чем перемещение нескольких. Перемещение чего-либо внутри этого слоя внезапно приводит к загрузке процессора и требует затрат. Поскольку содержимое ячеек обычно не сильно изменяется (только в ответ на сравнительно редкие действия пользователя), использование меньшего количества просмотров ускоряет процесс. Но создание одного большого обзора со всеми ячейками было бы плохой идеей ™.
Uliwitness
6

Я разработчик игр, и я задавал те же вопросы, когда мой друг сказал мне, что моя иерархия представлений на основе UIImageView замедлит мою игру и сделает ее ужасной. Затем я приступил к исследованию всего, что я мог найти о том, использовать ли UIViews, CoreGraphics, OpenGL или что-то стороннее, например, Cocos2D. Последовательный ответ, который я получил от друзей, учителей и инженеров Apple из WWDC, заключался в том, что в конце не будет большой разницы, потому что на каком-то уровне они все делают одно и то же . Опции более высокого уровня, такие как UIViews, полагаются на опции более низкого уровня, такие как CoreGraphics и OpenGL, просто они заключены в код, чтобы вам было легче его использовать.

Не используйте CoreGraphics, если вы просто собираетесь переписать UIView. Тем не менее, вы можете получить некоторую скорость с помощью CoreGraphics, до тех пор , как вы делаете весь рисунок в одной точке зрения, но это действительно стоит это? Ответ, который я нашел, обычно нет. Когда я впервые начал свою игру, я работал с iPhone 3G. По мере того, как моя игра усложнялась, я начал видеть некоторое отставание, но с новыми устройствами это было совершенно незаметно. Сейчас у меня много действий, и единственное отставание - падение на 1-3 кадра в секунду при игре на самом сложном уровне на iPhone 4.

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

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

WolfLink
источник
8
Я думаю, вы можете спутать Core Animation с Core Graphics. Базовая графика действительно медленная, программная отрисовка и не используется для отрисовки обычных UIViews, если вы не переопределите метод drawRect. Core Animation имеет аппаратное ускорение, быстро и отвечает за большую часть того, что вы видите на экране.
Ник Локвуд