CGImage / UIImageがUIスレッドで遅延ロードするとスタッターが発生する
-
06-07-2019 - |
質問
私のプログラムは、UIImageViewsを左から右に並べた水平スクロール面を表示します。コードはUIスレッドで実行され、新しく表示されるUIImageViewに新たにロードされたUIImageが割り当てられるようにします。ロードはバックグラウンドスレッドで行われます。
すべての画像はほぼ正常に機能しますが、各画像が見えるようになるとスタッターが発生します。最初は、バックグラウンドワーカーがUIスレッドで何かをロックしていると思いました。私はそれを見るのに多くの時間を費やし、最終的にUIImageが最初に表示されたときにUIImageが余分な遅延処理を行っていることに気付きました。私のワーカースレッドにはJPEGデータを解凍するための明示的なコードがあるため、これは私を困惑させます。
とにかく、思いついたときに、バックグラウンドスレッドで一時的なグラフィックスコンテキストにレンダリングするコードをいくつか書きました。 UIImageは、ワーカースレッドにプリロードされています。これまでのところとても良い。
問題は、新しい「イメージの遅延ロードを強制する」ということです。メソッドは信頼できません。断続的なEXC_BAD_ACCESSが発生します。 UIImageが実際に舞台裏で何をしているのかわかりません。おそらく、JPEGデータを解凍しています。とにかく、メソッドは次のとおりです。
+ (void)forceLazyLoadOfImage: (UIImage*)image
{
CGImageRef imgRef = image.CGImage;
CGFloat currentWidth = CGImageGetWidth(imgRef);
CGFloat currentHeight = CGImageGetHeight(imgRef);
CGRect bounds = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
CGAffineTransform transform = CGAffineTransformIdentity;
CGFloat scaleRatioX = bounds.size.width / currentWidth;
CGFloat scaleRatioY = bounds.size.height / currentHeight;
UIGraphicsBeginImageContext(bounds.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextScaleCTM(context, scaleRatioX, -scaleRatioY);
CGContextTranslateCTM(context, 0, -currentHeight);
CGContextConcatCTM(context, transform);
CGContextDrawImage(context, CGRectMake(0, 0, currentWidth, currentHeight), imgRef);
UIGraphicsEndImageContext();
}
そして、CGContextDrawImage行でEXC_BAD_ACCESSが発生します。質問1:UIスレッド以外のスレッドでこれを実行できますか?質問2:実際に「プリロード」するUIImageとは何ですか?質問3:この問題を解決する公式の方法は何ですか?
すべてを読んでくれてありがとう、どんなアドバイスも大歓迎です!
解決
UIGraphics *
メソッドは、メインスレッドからのみ呼び出されるように設計されています。おそらくあなたのトラブルの原因です。
UIGraphicsBeginImageContext()
を CGBitmapContextCreate()
の呼び出しに置き換えることができます。それはもう少し複雑です(色空間を作成し、作成するのに適切なサイズのバッファを見つけ、それを自分で割り当てる必要があります)。 CG *
メソッドは、別のスレッドから実行しても問題ありません。
UIImageを初期化する方法がわかりませんが、 imageNamed:
または initWithFile:
で初期化する場合、強制的に実行できる場合があります自分でデータをロードしてから initWithData:
を呼び出してロードします。スタッターはおそらく遅延ファイルI / Oが原因であるため、データオブジェクトで初期化してもファイルから読み取るオプションは提供されません。
他のヒント
同じst音の問題がありましたが、いくつかの助けを借りて、ここで適切な解決策を見つけました: iOSでの非遅延画像の読み込み
2つの重要な言及:
- ワーカースレッドでUIKitメソッドを使用しないでください。代わりにCoreGraphicsを使用してください。
- 画像をロードおよび解凍するためのバックグラウンドスレッドがある場合でも、CGBitmapContextに間違ったビットマスクを使用すると、多少のスタッターが発生します。これはあなたが選択しなければならないオプションです(理由はまだわかりません):
-
CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace,
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
ここにサンプルプロジェクトを投稿しました: SwapTest ApplesのPhotosアプリと同じパフォーマンスで画像をロード/表示します。
@jasamerのSwapTest UIImageカテゴリを使用して、ワーカースレッド(NSOperationQueueを使用)で大きなUIImage(約3000x2100 px)を強制的にロードしました。これにより、画像をUIImageViewに設定する際のスタッター時間が許容値(iPad1では約0.5秒)に短縮されます。
ここにSwapTest UIImageカテゴリがあります。ありがとう@jasamer:)
UIImage + ImmediateLoading.hファイル
@interface UIImage (UIImage_ImmediateLoading)
- (UIImage*)initImmediateLoadWithContentsOfFile:(NSString*)path;
+ (UIImage*)imageImmediateLoadWithContentsOfFile:(NSString*)path;
@end
UIImage + ImmediateLoading.mファイル
#import "UIImage+ImmediateLoading.h"
@implementation UIImage (UIImage_ImmediateLoading)
+ (UIImage*)imageImmediateLoadWithContentsOfFile:(NSString*)path {
return [[[UIImage alloc] initImmediateLoadWithContentsOfFile: path] autorelease];
}
- (UIImage*)initImmediateLoadWithContentsOfFile:(NSString*)path {
UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
CGImageRef imageRef = [image CGImage];
CGRect rect = CGRectMake(0.f, 0.f, CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
CGContextRef bitmapContext = CGBitmapContextCreate(NULL,
rect.size.width,
rect.size.height,
CGImageGetBitsPerComponent(imageRef),
CGImageGetBytesPerRow(imageRef),
CGImageGetColorSpace(imageRef),
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little
);
//kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little are the bit flags required so that the main thread doesn't have any conversions to do.
CGContextDrawImage(bitmapContext, rect, imageRef);
CGImageRef decompressedImageRef = CGBitmapContextCreateImage(bitmapContext);
UIImage* decompressedImage = [[UIImage alloc] initWithCGImage: decompressedImageRef];
CGImageRelease(decompressedImageRef);
CGContextRelease(bitmapContext);
[image release];
return decompressedImage;
}
@end
そして、これがNSOpeationQueueを作成し、メインスレッドにイメージを設定する方法です...
// Loads low-res UIImage at a given index and start loading a hi-res one in background.
// After finish loading, set the hi-res image into UIImageView. Remember, we need to
// update UI "on main thread" otherwise its result will be unpredictable.
-(void)loadPageAtIndex:(int)index {
prevPage = index;
//load low-res
imageViewForZoom.image = [images objectAtIndex:index];
//load hi-res on another thread
[operationQueue cancelAllOperations];
NSInvocationOperation *operation = [NSInvocationOperation alloc];
filePath = [imagesHD objectAtIndex:index];
operation = [operation initWithTarget:self selector:@selector(loadHiResImage:) object:[imagesHD objectAtIndex:index]];
[operationQueue addOperation:operation];
[operation release];
operation = nil;
}
// background thread
-(void)loadHiResImage:(NSString*)file {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(@"loading");
// This doesn't load the image.
//UIImage *hiRes = [UIImage imageNamed:file];
// Loads UIImage. There is no UI updating so it should be thread-safe.
UIImage *hiRes = [[UIImage alloc] initImmediateLoadWithContentsOfFile:[[NSBundle mainBundle] pathForResource:file ofType: nil]];
[imageViewForZoom performSelectorOnMainThread:@selector(setImage:) withObject:hiRes waitUntilDone:NO];
[hiRes release];
NSLog(@"loaded");
[pool release];
}
データを使用して画像を初期化しても、同じ問題が発生しました。 (データも遅延してロードされると思いますか?)次のカテゴリを使用して強制的にデコードすることに成功しました:
@interface UIImage (Loading)
- (void) forceLoad;
@end
@implementation UIImage (Loading)
- (void) forceLoad
{
const CGImageRef cgImage = [self CGImage];
const int width = CGImageGetWidth(cgImage);
const int height = CGImageGetHeight(cgImage);
const CGColorSpaceRef colorspace = CGImageGetColorSpace(cgImage);
const CGContextRef context = CGBitmapContextCreate(
NULL, /* Where to store the data. NULL = don’t care */
width, height, /* width & height */
8, width * 4, /* bits per component, bytes per row */
colorspace, kCGImageAlphaNoneSkipFirst);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
CGContextRelease(context);
}
@end