Самый эффективный способ нарисовать часть изображения в iOS - PullRequest
36 голосов
/ 07 ноября 2011

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

Ответы [ 5 ]

78 голосов
/ 26 ноября 2011

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


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

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

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

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

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


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

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

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

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


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

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

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

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

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

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


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

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

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

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


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

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

5 голосов
/ 24 сентября 2012

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

Очень простой способ перемещения большого изображения внутри 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);
1 голос
/ 07 ноября 2011

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

0 голосов
/ 14 ноября 2014

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

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