Самый эффективный способ нарисовать часть изображения в iOS

Учитывая UIImage и CGRect, каков наиболее эффективный способ (по памяти и времени) нарисовать часть изображения, соответствующую CGRect (без масштабирования)?

Для справки, вот как я сейчас это делаю:

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGRect frameRect = CGRectMake(frameOrigin.x + rect.origin.x, frameOrigin.y + rect.origin.y, rect.size.width, rect.size.height);    
    CGImageRef imageRef = CGImageCreateWithImageInRect(image_.CGImage, frameRect);
    CGContextTranslateCTM(context, 0, rect.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    CGContextDrawImage(context, rect, imageRef);
    CGImageRelease(imageRef);
}

К сожалению, это кажется очень медленным с изображениями среднего размера и высокой setNeedsDisplay частотой. Игра с рамкой UIImageView и clipToBounds дает лучшие результаты (с меньшей гибкостью).


person hpique    schedule 07.11.2011    source источник
comment
Я думаю, вам следует избегать постоянного рендеринга изображения с помощью UIKit / CoreGraphics. Если вам действительно нужно это сделать, попробуйте вместо этого использовать технологию OpenGL, это намного эффективнее.   -  person mr.pppoe    schedule 23.11.2011
comment
@ mr.pppoe Можете ли вы предоставить образец кода?   -  person hpique    schedule 23.11.2011
comment
Что ж, подключение OpenGL к вашему текущему проекту может оказаться не из легких. Вы можете сначала взглянуть на это: github.com/pppoe/GLRenderLargeImage, если действительно хотите образец, дайте нам более конкретную информацию о вашей ситуации. Тогда я могу ответить. :)   -  person mr.pppoe    schedule 24.11.2011


Ответы (5)


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


Доверьтесь Apple в вопросах обычного пользовательского интерфейса

На самом деле UIImageView с clipsToBounds - это один из самых быстрых / простых способов заархивировать вашу цель, если ваша цель - просто вырезать прямоугольную область изображения (не слишком большую). Кроме того, вам не нужно отправлять setNeedsDisplay сообщение.

Или вы можете попробовать поместить UIImageView внутрь пустого UIView и установить отсечение в виде контейнера. С помощью этой техники вы можете свободно трансформировать свое изображение, установив свойство transform в 2D (масштабирование, поворот, перевод).

Если вам нужно трехмерное преобразование, вы все равно можете использовать CALayer со свойством masksToBounds, но использование CALayer даст вам очень небольшую дополнительную производительность, обычно незначительную.

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


Почему это один из самых быстрых способов?

UIView - это всего лишь тонкий слой поверх CALayer, который реализован поверх OpenGL, который является практически прямым интерфейсом для GPU. Это означает, что UIKit ускоряется графическим процессором.

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

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


Почему ваш метод медленнее, чем UIImageView?

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

IMO, CGImage методы рисования не реализованы на GPU. Думаю, я читал упоминание об этом в документации Apple, но не могу вспомнить где. Так что я не уверен в этом. В любом случае, я считаю, что CGImage реализован в ЦП, потому что

  1. Его API выглядит так, как будто он был разработан для ЦП, например, интерфейс редактирования растровых изображений и рисование текста. Они не очень хорошо подходят к интерфейсу графического процессора.
  2. Интерфейс контекста растрового изображения обеспечивает прямой доступ к памяти. Это означает, что внутреннее хранилище находится в памяти ЦП. Может быть, несколько иначе в архитектуре унифицированной памяти (а также с Metal API), но в любом случае первоначальное намерение CGImage при проектировании должно быть связано с процессором.
  3. Многие недавно выпустили другие API-интерфейсы Apple, в которых явно упоминается ускорение графического процессора. Это означает, что их старые API не были такими. Если нет особого упоминания, обычно это делается в ЦП по умолчанию.

Похоже, что это делается в ЦП. Графические операции, выполняемые в CPU, намного медленнее, чем в GPU.

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


Об ограничениях

Поскольку оптимизация - это своего рода работа над микроменеджментом, очень важны конкретные цифры и небольшие факты. Что за средний размер? OpenGL на iOS обычно ограничивает максимальный размер текстуры до 1024x1024 пикселей (возможно, больше в последних выпусках). Если ваше изображение больше этого размера, оно не будет работать или производительность будет значительно снижена (я думаю, что UIImageView оптимизирован для изображений в установленных пределах).

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

И не переходите на OpenGL, если вы не хотите знать все детали OpenGL. Требуется полное понимание низкоуровневой графики и как минимум в 100 раз больше кода.


О каком-то будущем

Хотя это маловероятно, но CGImage вещи (или что-то еще) не нужно застревать только в ЦП. Не забудьте проверить базовую технологию API, который вы используете. Тем не менее, графические процессоры сильно отличаются от процессоров, и парни, занимающиеся API, обычно явно и четко упоминают о них.

person eonil    schedule 26.11.2011
comment
Очень тщательно. Отличный ответ. - person dsmDev; 20.06.2014

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

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

Мое приложение создает много UIImages во время инициализации, и это обязательно требует времени. Но некоторые изображения не нужны, пока не будет выбрана определенная вкладка. Чтобы создать впечатление более быстрой загрузки, я мог бы создать их в отдельном потоке, порожденном при запуске, а затем просто дождаться его завершения, если эта вкладка выбрана.

+ (UIImage*)imageByCropping:(UIImage *)imageToCrop toRect:(CGRect)aperture {
    return [ChordCalcController imageByCropping:imageToCrop toRect:aperture withOrientation:UIImageOrientationUp];
}

// Draw a full image into a crop-sized area and offset to produce a cropped, rotated image
+ (UIImage*)imageByCropping:(UIImage *)imageToCrop toRect:(CGRect)aperture withOrientation:(UIImageOrientation)orientation {

            // convert y coordinate to origin bottom-left
    CGFloat orgY = aperture.origin.y + aperture.size.height - imageToCrop.size.height,
            orgX = -aperture.origin.x,
            scaleX = 1.0,
            scaleY = 1.0,
            rot = 0.0;
    CGSize size;

    switch (orientation) {
        case UIImageOrientationRight:
        case UIImageOrientationRightMirrored:
        case UIImageOrientationLeft:
        case UIImageOrientationLeftMirrored:
            size = CGSizeMake(aperture.size.height, aperture.size.width);
            break;
        case UIImageOrientationDown:
        case UIImageOrientationDownMirrored:
        case UIImageOrientationUp:
        case UIImageOrientationUpMirrored:
            size = aperture.size;
            break;
        default:
            assert(NO);
            return nil;
    }


    switch (orientation) {
        case UIImageOrientationRight:
            rot = 1.0 * M_PI / 2.0;
            orgY -= aperture.size.height;
            break;
        case UIImageOrientationRightMirrored:
            rot = 1.0 * M_PI / 2.0;
            scaleY = -1.0;
            break;
        case UIImageOrientationDown:
            scaleX = scaleY = -1.0;
            orgX -= aperture.size.width;
            orgY -= aperture.size.height;
            break;
        case UIImageOrientationDownMirrored:
            orgY -= aperture.size.height;
            scaleY = -1.0;
            break;
        case UIImageOrientationLeft:
            rot = 3.0 * M_PI / 2.0;
            orgX -= aperture.size.height;
            break;
        case UIImageOrientationLeftMirrored:
            rot = 3.0 * M_PI / 2.0;
            orgY -= aperture.size.height;
            orgX -= aperture.size.width;
            scaleY = -1.0;
            break;
        case UIImageOrientationUp:
            break;
        case UIImageOrientationUpMirrored:
            orgX -= aperture.size.width;
            scaleX = -1.0;
            break;
    }

    // set the draw rect to pan the image to the right spot
    CGRect drawRect = CGRectMake(orgX, orgY, imageToCrop.size.width, imageToCrop.size.height);

    // create a context for the new image
    UIGraphicsBeginImageContextWithOptions(size, NO, imageToCrop.scale);
    CGContextRef gc = UIGraphicsGetCurrentContext();

    // apply rotation and scaling
    CGContextRotateCTM(gc, rot);
    CGContextScaleCTM(gc, scaleX, scaleY);

    // draw the image to our clipped context using the offset rect
    CGContextDrawImage(gc, drawRect, imageToCrop.CGImage);

    // pull the image from our cropped context
    UIImage *cropped = UIGraphicsGetImageFromCurrentImageContext();

    // pop the context to get back to the default
    UIGraphicsEndImageContext();

    // Note: this is autoreleased
    return cropped;
}
person Scott Lahteine    schedule 24.09.2012

Очень простой способ переместить большое изображение внутри UIImageView следующим образом.

Пусть у нас есть изображение размером (100, 400), представляющее 4 состояния некоторого изображения одно под другим. Мы хотим показать 2-е изображение со смещением Y = 100 в квадрате UIImageView размером (100, 100). Решение такое:

UIImageView *iView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
CGRect contentFrame = CGRectMake(0, 0.25, 1, 0.25);
iView.layer.contentsRect = contentFrame;
iView.image = [UIImage imageNamed:@"NAME"];

Здесь contentFrame - это нормализованный кадр относительно реального размера UIImage. Итак, «0» означает, что мы начинаем видимую часть изображения с левой границы, «0,25» означает, что у нас есть вертикальное смещение 100, «1» означает, что мы хотим показать всю ширину изображения, и, наконец, «0,25» означает что мы хотим показать только 1/4 часть изображения по высоте.

Таким образом, в локальных координатах изображения мы показываем следующий кадр

CGRect visibleAbsoluteFrame = CGRectMake(0*100, 0.25*400, 1*100, 0.25*400)
or CGRectMake(0, 100, 100, 100);
person malex    schedule 16.10.2013
comment
Я получил увеличение скорости в 100 раз благодаря этой методике, разделив один UIImage на 1000 UIImageViews. Я не понимал, насколько мощным был contentFrame. - person Brainware; 28.01.2017

Как насчет использования CGContextClipToRect вместо создания нового образа (что требует больших затрат, поскольку выделяет память)?

person David Dunham    schedule 07.11.2011
comment
У меня нет большого опыта работы с Quartz. Можете ли вы предоставить фрагмент кода? - person hpique; 08.11.2011
comment
Я попробовал, и производительность существенно не изменилась. Это было примерно так: CGContextRef context = UIGraphicsGetCurrentContext (); CGRect frameRect = CGRectMake (0, 0, rect.size.width, rect.size.height); CGContextClipToRect (контекст, frameRect); CGRect imageRect = CGRectMake (frameOrigin.x + rect.origin.x, frameOrigin.y + rect.origin.y, rect.size.width, rect.size.height); CGContextDrawImage (контекст, imageRect, image_.CGImage); - person hpique; 23.11.2011
comment
Я полагаю, что время рисования забирает управление памятью (как вы определяете изображение среднего размера?) Я бы все равно избегал создания (и удаления) нового CGImageRef, хотя он вам не нужен, и вы говорите, что скорость имеет решающее значение. И я действительно не понимаю, почему отсечение становится менее гибким. (О, вырезка на уровне UIView. Конечно, это высокий уровень.) - person David Dunham; 23.11.2011

Самый быстрый способ - использовать маску изображения: изображение того же размера, что и изображение, которое нужно замаскировать, но с определенным шаблоном пикселей, указывающим, какую часть изображения нужно замаскировать при рендеринге ->

// maskImage is used to block off the portion that you do not want rendered
// note that rect is not actually used because the image mask defines the rect that is rendered
-(void) drawRect:(CGRect)rect maskImage:(UIImage*)maskImage {

    UIGraphicsBeginImageContext(image_.size);
    [maskImage drawInRect:image_.bounds];
    maskImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    CGImageRef maskRef = maskImage.CGImage;
    CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(maskRef),
                                    CGImageGetHeight(maskRef),
                                    CGImageGetBitsPerComponent(maskRef),
                                    CGImageGetBitsPerPixel(maskRef),
                                    CGImageGetBytesPerRow(maskRef),
                                    CGImageGetDataProvider(maskRef), NULL, false);

    CGImageRef maskedImageRef = CGImageCreateWithMask([image_ CGImage], mask);
    image_ = [UIImage imageWithCGImage:maskedImageRef scale:1.0f orientation:image_.imageOrientation];

    CGImageRelease(mask);
    CGImageRelease(maskedImageRef); 
}
person Adam Freeman    schedule 13.11.2014