Декомпрессия изображения в iOS 7

Проблема декомпрессии изображений много обсуждалась в Stack Overflow, но до этого вопроса было 0 упоминаний kCGImageSourceShouldCacheImmediately, вариант, введенный в iOS 7, который теоретически позаботится об этой проблеме. Из заголовков:

Указывает, должно ли декодирование и кэширование изображений происходить во время создания изображения.

В Objc.io # 7 Питер Стейнбергер предложил такой подход:

+ (UIImage *)decompressedImageWithData:(NSData *)data 
{
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)@{(id)kCGImageSourceShouldCacheImmediately: @YES});

    UIImage *image = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    CFRelease(source);
    return image;
}

Библиотеки, такие как AFNetworking и SDWebImage, продолжают декомпрессировать изображения с помощью метода CGContextDrawImage. Из SDWebImage:

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    if (image.images) {
        // Do not decode animated images
        return image;
    }

    CGImageRef imageRef = image.CGImage;
    CGSize imageSize = CGSizeMake(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
    CGRect imageRect = (CGRect){.origin = CGPointZero, .size = imageSize};

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);

    int infoMask = (bitmapInfo & kCGBitmapAlphaInfoMask);
    BOOL anyNonAlpha = (infoMask == kCGImageAlphaNone ||
            infoMask == kCGImageAlphaNoneSkipFirst ||
            infoMask == kCGImageAlphaNoneSkipLast);

    // CGBitmapContextCreate doesn't support kCGImageAlphaNone with RGB.
    // https://developer.apple.com/library/mac/#qa/qa1037/_index.html
    if (infoMask == kCGImageAlphaNone && CGColorSpaceGetNumberOfComponents(colorSpace) > 1) {
        // Unset the old alpha info.
        bitmapInfo &= ~kCGBitmapAlphaInfoMask;

        // Set noneSkipFirst.
        bitmapInfo |= kCGImageAlphaNoneSkipFirst;
    }
            // Some PNGs tell us they have alpha but only 3 components. Odd.
    else if (!anyNonAlpha && CGColorSpaceGetNumberOfComponents(colorSpace) == 3) {
        // Unset the old alpha info.
        bitmapInfo &= ~kCGBitmapAlphaInfoMask;
        bitmapInfo |= kCGImageAlphaPremultipliedFirst;
    }

    // It calculates the bytes-per-row based on the bitsPerComponent and width arguments.
    CGContextRef context = CGBitmapContextCreate(NULL,
            imageSize.width,
            imageSize.height,
            CGImageGetBitsPerComponent(imageRef),
            0,
            colorSpace,
            bitmapInfo);
    CGColorSpaceRelease(colorSpace);

    // If failed, return undecompressed image
    if (!context) return image;

    CGContextDrawImage(context, imageRect, imageRef);
    CGImageRef decompressedImageRef = CGBitmapContextCreateImage(context);

    CGContextRelease(context);

    UIImage *decompressedImage = [UIImage imageWithCGImage:decompressedImageRef scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(decompressedImageRef);
    return decompressedImage;
}

Мой вопрос в том, следует ли перейти к подходу kCGImageSourceShouldCacheImmediately в iOS 7?

Ответ 1

Есть несколько проблем с реализацией, насколько я могу судить.

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

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

    • заполнить свой собственный буфер сжатыми данными JPEG из некоторого источника
    • создайте поставщика данных и прикрепите к нему данные JPEG
    • создать источник изображения с поставщиком данных с помощью CACHE IMMEDIATELY set
    • используйте источник изображения для создания изображения CG.
    • отбросьте все промежуточные объекты и передайте ваш красиво декомпрессированный объект CGImage до объекта UIImage, готового для прокрутки.

Это не так, потому что он будет ждать основного потока, где будет выполняться декомпрессия. Он думает, что все в порядке, потому что оно содержит ссылки на все эти промежуточные объекты, которые вы выпустили. Вы выпустили все эти объекты, думая, что он распаковал НЕМЕДЛЕННО, поскольку флаги сказали, что это будет. Если вы выбросили этот буфер памяти, и этот буфер памяти был передан в смысле отсутствия копии, вы в конечном итоге получите мусор. Или, если буфер памяти был повторно использован, как в моем случае, чтобы загрузить другое изображение, вы также получите мусор.

На самом деле у вас нет возможности узнать, когда это изображение будет распаковано и готово к использованию.

TL; DR = "Рассмотрим kCGImageSourceShouldCacheImime, чтобы это означало, когда это удобно для ОС"

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

Так что в принципе нет бесплатного обеда. Глядя на трассировку стека, где эта вещь потерпела крах, когда я начал повторно использовать буфер памяти после того, как я подумал, что все было подписано, я вижу, что он вызывает CA:: Transaction, CA:: Layer, CA:: Render и из там в ImageProviderCopy... вплоть до JPEGParseJPEGInfo (где он разбился о доступе к моему буферу).

Это означает, что kCGImageSourceShouldCacheImmediately делает ничего, кроме того, чтобы установить флаг, чтобы сообщить об этом изображение в декомпрессию в основном потоке как можно скорее после его создания, а не на самом деле НЕМЕДЛЕННО, поскольку вы считаете, что НЕМЕДЛЕННО означает (on чтение). Это было бы то же самое, если бы вы передали изображение на вид прокрутки для отображения, и изображение пошло на рисование. Если вам повезло, там были некоторые запасные циклы между прокруткой, и это улучшало бы ситуацию, но в основном я думаю, что это просто звучит гораздо более надежно, что он будет делать больше, чем на самом деле.

Ответ 2

Может быть так:

+ (UIImage *)imageFromURL:(NSURL *)url {
    UIImage *result = nil;
    if ([url isFileURL]) {
        CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
        if (source) {
            NSDictionary * attributes = @{ (id)kCGImageSourceShouldCache : @YES };
            CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)attributes);
            if (cgImage) {
                result = [UIImage imageWithCGImage:cgImage];
                CGImageRelease(cgImage);
            }
            CFRelease(source);
        }
    }
    return result;
}

Ответ 3

Вы можете попробовать следующий код:

+(NSData *)imageData:(UIImage *)image
{
    //1.0 == 100%
    return UIImageJPEGRepresentation(image, 0.7);
}

Ура!