Большой пользовательский UIView - производительность CABackingStoreUpdate

Почему на iOS 4.3.5 пользовательский UIView выполняет "большой" (960 x 1380) тактический CABackingStoreUpdate и как я могу улучшить производительность операций рисования?

Не совсем уверен, что я имею в виду? Читайте дальше...

Примечание:

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

Контекст

У меня есть невероятно базовое приложение (код внизу), который рисует один elipses в методе drawRect: пользовательский UIView. Приложение демонстрирует разницу в производительности, когда размер элипсов, нарисованных, остается тем же, но размер пользовательского UIView становится больше:

Small UIView

Large UIView

Я запустил приложение на iPod 4th Gen под управлением iOS 4.3.5 и iPad 1-го поколения, работающего под управлением iOS 5.1.1, с использованием пользовательских UIView разных размеров.
В следующей таблице представлены результаты, полученные с помощью инструмента профайлера времени:

Time profiler results

Следующие следы инструментов отображают детали двух крайних значений для каждого устройства:

iOS 5.1.1 - (пользовательский размер UIView 320 x 460)

iOS 5.1.1 - Custom UIView size 320 x 460

iOS 5.1.1 - (пользовательский размер UIView 960 x 1380)

iOS 5.1.1 - Custom UIView size 960 x 1380

iOS 4.3.5 - (пользовательский размер UIView 320 x 460)

iOS 4.3.5 - Custom UIView size 320 x 460

iOS 4.3.5 - (пользовательский размер UIView 960 x 1380)

iOS 4.3.5 - Custom UIView size 960 x 1380

Как вы можете (надеюсь) увидеть в 3 из 4 случаев, мы получаем то, что ожидаем: большая часть времени была потрачена на выполнение пользовательских методов UIViews drawRect: и каждый из них - 10 кадров в секунду.
Но в четвертом случае демонстрируется плюммет в производительности, при этом приложение изо всех сил пытается удерживать 7 кадров в секунду, а только рисует одну фигуру. Большинство времени было потрачено на копирование памяти во время отображения UIView CALayer, в частности:

[CALayer display] >
[CALayer _display] >
CABackingStoreUpdate >
CA:: Render:: ShmemBitmap:: copy_pixels (CA:: Render:: ShmemBitmap const *, CGSRegionObject *) >
тетср $ВАРИАНТ $CortexA8

Теперь не нужно гениально видеть из цифр, что здесь что-то серьезно не так. С пользовательским UIView размером 960 x 1380 iOS 4.3.5 тратит в 4 раза больше времени на копирование памяти, чем на рисование всего содержимого представления.

Вопрос

Теперь, учитывая контекст, я снова задаю свой вопрос:

Почему на iOS 4.3.5 пользовательский UIView выполняет "большой" (960 x 1380) тактический CABackingStoreUpdate и как я могу улучшить производительность операций рисования?

Любая помощь очень ценится.

Я также разместил этот вопрос на форумах разработчиков Apple.

Реальная сделка

Теперь, очевидно, я уменьшил свою реальную проблему до простейшего воспроизводимого случая ради этого вопроса. Я на самом деле пытаюсь оживить часть пользовательского UIView 960 x 1380, который находится внутри UIScrollView.

Несмотря на то, что я ценю соблазн направить кого-либо в сторону OpenGL ES, когда они не достигают уровня производительности, которого они хотят с помощью Quartz 2D, я прошу, чтобы любой, кто берет этот маршрут, хотя бы объяснил, почему Quartz 2D борется с выполняйте даже самые основные операции рисования на iOS 4.3.5, где iOS 5.1.1 не имеет проблем. Как вы можете себе представить, я не в восторге от идеи переписывать все для этого краеугольного камня. Это также относится к людям, предлагающим использовать Core Animation. Хотя я использовал элипсы, меняющие цвет (задача, идеально подходящая для Core Animation) в демонстрации ради простоты, операции рисования, которые я действительно хотел бы выполнить, - это большое количество строк, расширяющихся со временем, задача рисования Кварц 2D идеально подходит для (когда он исполнен!). Кроме того, это потребует повторной записи и не поможет объяснить эту проблему с нечетной производительностью.

Код

TViewController.m(Реализация стандартного контроллера представления)

#import "TViewController.h"
#import "TCustomView.h"

// VERSION 1 features the custom UIView the same size as the screen.
// VERSION 2 features the custom UIView nine times the size of the screen.
#define VERSION 2


@interface TViewController ()
@property (strong, nonatomic) TCustomView *customView;
@property (strong, nonatomic) NSTimer *animationTimer;
@end


@implementation TViewController

- (void)viewDidLoad
{
    // Custom subview.
    TCustomView *customView = [[TCustomView alloc] init];
    customView.backgroundColor = [UIColor whiteColor];
#if VERSION == 1
    customView.frame = CGRectMake(0.0f, 0.0f, 320.0f, 460.0f);
#else
    customView.frame = CGRectMake(0.0f, 0.0f, 960.0f, 1380.0f);
#endif

    [self.view addSubview:customView];

    UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
    [customView addGestureRecognizer:singleTap];

    self.customView = customView;
}

#pragma mark - Timer Loop

- (void)handleTap:(UITapGestureRecognizer *)tapGesture
{
    self.customView.value = 0.0f;

    if (!self.animationTimer  || !self.animationTimer.isValid) {
        self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(animationLoop) userInfo:nil repeats:YES];
    }
}

#pragma mark - Timer Loop

- (void)animationLoop
{
    // Update model here. For simplicity, increment a single value.
    self.customView.value += 0.01f;

    if (self.customView.value >= 1.0f)
    {
        self.customView.value = 1.0f;
        [self.animationTimer invalidate];
    }

    [self.customView setNeedsDisplayInRect:CGRectMake(0.0f, 0.0f, 320.0f, 460.0f)];
}

@end

-

TCustomView.h(пользовательский заголовок представления)

#import <UIKit/UIKit.h>

@interface TCustomView : UIView
@property (assign) CGFloat value;
@end

-

TCustomView.m(реализация пользовательского представления)

#import "TCustomView.h"

@implementation TCustomView

- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    // Draw ellipses.
    CGContextSetRGBFillColor(context, self.value, self.value, self.value, 1.0f);
    CGContextFillEllipseInRect(context, rect);

    // Draw value itself.
    [[UIColor redColor] set];
    NSString *value = [NSString stringWithFormat:@"%f", self.value];
    [value drawAtPoint:rect.origin withFont:[UIFont fontWithName:@"Arial" size:15.0f]];
}

@end

Ответ 1

Так как у iPod Touch 4-го поколения и iPad 1-го поколения есть аналогичные аппаратные средства (одинаковый объем памяти/один и тот же графический процессор), это говорит о том, что проблема, которую вы видите, связана с не оптимизированным кодом в iOS4.

Если вы посмотрите на размер просмотров, которые вызывают (отрицательный) всплеск производительности на iOS4, у них есть одна сторона длиннее 1024. Первоначально 1024x1024 был максимальным размером UIView, и, хотя это ограничение с тех пор было снято вполне вероятно, что взгляды, большие, чем это, стали эффективными только в iOS5 и более поздних версиях.

Я бы предположил, что избыточное копирование памяти, которое вы видите в iOS4, связано с использованием UIKit с использованием полноразмерного буфера памяти для большого UIView, но затем необходимо скопировать соответствующие плитки определенного размера до их компоновки; и что в iOS5 и позже они либо удалили любое ограничение на размер фрагментов, которые могут быть скомпонованы, либо изменили способ отображения UIKit для таких больших UIView.

С точки зрения работы с этим узким местом на iOS4 вы можете попробовать разбивать область, которую хотите покрыть, с меньшими UIView. Если вы структурируете его как:

  Parent View - contains drawing and event related code
    Tile View 1 - contains drawRect
    ...
    Tile View n - contains drawRect

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

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

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

Ответ 2

Как вы заметили, время в основном тратится на передачу некоторых данных. Я думаю, что в iOS 4.3.5 CoreGraphics не использует графический процессор и графическую память для реализации примитивных функций рисования, таких как CGContextFillEllipseInRect и т.д.

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

Я предполагаю, что с iOS 5. или 5.1 примитивные функции рисования вызывают некоторые графические шейдеры (программы внутри GPU), а затем все тяжелые вещи там делаются.

Затем из основной памяти оперативной памяти в графическую память переносятся только несколько данных (параметры и программный код).