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

Учитывая UIImage и a 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 дает лучшие результаты (с меньшей гибкостью).

Ответ 1

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


Доверять Apple для файлов обычного интерфейса

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

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

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

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


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

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

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

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


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

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

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

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

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

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


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

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

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

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


О будущем >

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

Ответ 2

В конечном счете это будет быстрее, при этом гораздо меньше будет создано изображение из атласов спрайтов, если вы можете установить не только изображение для 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;
}

Ответ 3

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

Пусть у нас есть образ размера (100, 400), представляющий 4 состояния некоторого изображения один под другим. Мы хотим показать второе изображение со смещением 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);

Ответ 4

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

Ответ 5

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

// 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); 
}