Загрузка ленивого изображения в iOS
Я пытаюсь загрузить UIImages в фоновый поток, а затем отображать их на iPad. Тем не менее, там заикается, когда я устанавливаю свойство viewViews для изображения.
Вскоре я понял, что загрузка изображений ленива на iOS и нашел частичное решение в этом вопросе:
CGImage/UIImage ленивая загрузка на поток пользовательского интерфейса вызывает заикание
Это фактически заставляет изображение загружаться в поток, но при показе изображения все еще заикается.
Здесь вы можете найти мой пример проекта: http://www.jasamer.com/files/SwapTest.zip (изменить: исправлена версия), проверьте SwapTestViewController. Попробуйте перетащить изображение, чтобы увидеть заикание.
Тестовый код, который я создал, это заикание - это (метод forceLoad - это тот, который был сделан из вопроса, который я опубликовал выше):
NSArray* imagePaths = [NSArray arrayWithObjects:
[[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil],
[[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil], nil];
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock: ^(void) {
int imageIndex = 0;
while (true) {
UIImage* image = [[UIImage alloc] initWithContentsOfFile: [imagePaths objectAtIndex: imageIndex]];
imageIndex = (imageIndex+1)%2;
[image forceLoad];
//What missing here?
[self performSelectorOnMainThread: @selector(setImage:) withObject: image waitUntilDone: YES];
[image release];
}
}];
Есть две причины, почему я знаю, что заикания можно избежать:
(1) Apple может загружать изображения без заикания в приложении "Фотографии"
(2) Этот код не вызывает заикания после placeholder1, а placeholder2 отображается один раз в этой модифицированной версии вышеуказанного кода:
UIImage* placeholder1 = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil]];
[placeholder1 forceLoad];
UIImage* placeholder2 = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil]];
[placeholder2 forceLoad];
NSArray* imagePaths = [NSArray arrayWithObjects:
[[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil],
[[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil], nil];
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock: ^(void) {
int imageIndex = 0;
while (true) {
//The image is not actually used here - just to prove that the background thread isn't causing the stutter
UIImage* image = [[UIImage alloc] initWithContentsOfFile: [imagePaths objectAtIndex: imageIndex]];
imageIndex = (imageIndex+1)%2;
[image forceLoad];
if (self.imageView.image==placeholder1) {
[self performSelectorOnMainThread: @selector(setImage:) withObject: placeholder2 waitUntilDone: YES];
} else {
[self performSelectorOnMainThread: @selector(setImage:) withObject: placeholder1 waitUntilDone: YES];
}
[image release];
}
}];
Однако я не могу сохранить все мои изображения в памяти.
Это означает, что forceLoad не выполняет полную работу - там что-то еще происходит, прежде чем изображения будут фактически отображаться.
Кто-нибудь знает, что это такое, и как я могу поместить это в фоновый поток?
Спасибо, Джулиан
Обновление
Использовал несколько советов Tommys. Я понял, что это CGSConvertBGRA8888toRGBA8888, что занимает так много времени, поэтому кажется, что это преобразование цвета, которое вызывает отставание.
Здесь (перевернутый) стек вызовов этого метода.
Running Symbol Name
6609.0ms CGSConvertBGRA8888toRGBA8888
6609.0ms ripl_Mark
6609.0ms ripl_BltImage
6609.0ms RIPLayerBltImage
6609.0ms ripc_RenderImage
6609.0ms ripc_DrawImage
6609.0ms CGContextDelegateDrawImage
6609.0ms CGContextDrawImage
6609.0ms CA::Render::create_image_by_rendering(CGImage*, CGColorSpace*, bool)
6609.0ms CA::Render::create_image(CGImage*, CGColorSpace*, bool)
6609.0ms CA::Render::copy_image(CGImage*, CGColorSpace*, bool)
6609.0ms CA::Render::prepare_image(CGImage*, CGColorSpace*, bool)
6609.0ms CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms CALayerPrepareCommit
6609.0ms CA::Context::commit_transaction(CA::Transaction*)
6609.0ms CA::Transaction::commit()
6609.0ms CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*)
6609.0ms __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
6609.0ms __CFRunLoopDoObservers
6609.0ms __CFRunLoopRun
6609.0ms CFRunLoopRunSpecific
6609.0ms CFRunLoopRunInMode
6609.0ms GSEventRunModal
6609.0ms GSEventRun
6609.0ms -[UIApplication _run]
6609.0ms UIApplicationMain
6609.0ms main
Последние изменения в бит-маске, которые он предложил, ничего не изменили, к сожалению.
Ответы
Ответ 1
UIKit может использоваться только в основном потоке. Следовательно, ваш код технически недействителен, так как вы используете UIImage из потока, отличного от основного потока. Вы должны использовать только CoreGraphics для загрузки (и не-ленивого декодирования) графики в фоновом потоке, отправьте CGImageRef в основной поток и превратите его в UIImage. Возможно, он работает (хотя и с заиканием, которого вы не хотите) в вашей текущей реализации, но это не гарантируется. Кажется, что в этой области много суеверий и плохих практик, поэтому неудивительно, что вам удалось найти плохие советы...
Рекомендуется запускать фоновый поток:
// get a data provider referencing the relevant file
CGDataProviderRef dataProvider = CGDataProviderCreateWithFilename(filename);
// use the data provider to get a CGImage; release the data provider
CGImageRef image = CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO,
kCGRenderingIntentDefault);
CGDataProviderRelease(dataProvider);
// make a bitmap context of a suitable size to draw to, forcing decode
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
unsigned char *imageBuffer = (unsigned char *)malloc(width*height*4);
CGColorSpaceRef colourSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef imageContext =
CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace,
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
CGColorSpaceRelease(colourSpace);
// draw the image to the context, release it
CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), image);
CGImageRelease(image);
// now get an image ref from the context
CGImageRef outputImage = CGBitmapContextCreateImage(imageContext);
// post that off to the main thread, where you might do something like
// [UIImage imageWithCGImage:outputImage]
[self performSelectorOnMainThread:@selector(haveThisImage:)
withObject:[NSValue valueWithPointer:outputImage] waitUntilDone:YES];
// clean up
CGImageRelease(outputImage);
CGContextRelease(imageContext);
free(imageBuffer);
Нет необходимости делать malloc/free, если вы на iOS 4 или новее, вы можете просто передать NULL в качестве соответствующего параметра CGBitmapContextCreate и позволить CoreGraphics сортировать собственное хранилище.
Это отличается от решения, которое вы публикуете, потому что оно:
- создает CGImage из источника данных PNG - применяется ленивая загрузка, поэтому это необязательно полностью загруженное и распакованное изображение
- создает растровый контекст того же размера, что и PNG
- выводит CGImage из источника данных PNG в контекст растрового изображения - это должно приводить к полной загрузке и декомпрессии, поскольку фактические значения цвета должны быть помещены где-то, где мы могли бы получить к ним доступ из массива C. Этот шаг до тех пор, пока ссылка forceLoad, на которую вы ссылаетесь, не работает.
- преобразует контекст растрового изображения в изображение
- помещает это изображение в основной поток, предположительно, чтобы стать UIImage
Таким образом, нет непрерывности объекта между загруженной вещью и отображаемой вещью; пиксельные данные проходят через массив C (поэтому, нет возможности для скрытых shenanigans), и только если он был правильно помещен в массив, можно сделать окончательное изображение.
Ответ 2
Хорошо, понял это - с большой помощью Томми. Спасибо!
Если вы создаете свой контекст с помощью
CGContextRef imageContext =
CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace,
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
основной цикл цикла больше не вызывает никаких преобразований в основном потоке.
Отображение теперь маслянистое. (Флаги немного противоречат друг другу, кто-нибудь знает, почему вам нужно выбрать kCGImageAlphaPremultipliedFirst?)
Edit:
Загрузил проект с фиксированной выборкой: http://www.jasamer.com/files/SwapTest-Fixed.zip.
Если у вас проблемы с производительностью изображения, это отличная отправная точка!
Ответ 3
Как насчет UIImage + ImmediateLoad.m?
Он немедленно декодирует изображения. Помимо UIImage -initWithContentsOfFile:, -imageWithCGImage: и CG * являются потокобезопасными после iOS4.