2009-11-29 5 views
12

Mein Programm zeigt eine horizontale Scroll-Oberfläche mit UIImageViews von links nach rechts gefliest. Code wird auf dem UI-Thread ausgeführt, um sicherzustellen, dass den neu sichtbaren UIImageViews ein frisch geladener UIImage zugewiesen ist. Das Laden geschieht auf einem Hintergrundthread.CGImage/UIImage Lazy Laden auf UI-Thread verursacht Stottern

Alles funktioniert fast gut, außer es gibt ein Stottern, da jedes Bild sichtbar wird. Zuerst dachte ich, mein Hintergrundarbeiter würde etwas im UI-Thread sperren. Ich habe viel Zeit damit verbracht, es zu betrachten, und habe schließlich festgestellt, dass das UIImage beim ersten Öffnen des UI-Threads etwas mehr Lazy verarbeitet. Das verwirrt mich, da mein Worker-Thread expliziten Code zum Dekomprimieren von JPEG-Daten hat.

Wie auch immer, auf eine Ahnung habe ich etwas Code geschrieben, um in einen temporären Grafikkontext auf dem Hintergrund-Thread zu rendern und - sicher genug, das Stottern ging weg. Das UIImage wird jetzt in meinem Worker-Thread vorinstalliert. So weit, ist es gut.

Das Problem ist, dass meine neue "Force Lazy Load Image" -Methode unzuverlässig ist. Es verursacht intermittierende EXC_BAD_ACCESS. Ich habe keine Ahnung, was UIImage hinter den Kulissen tut. Vielleicht dekomprimiert es die JPEG-Daten. Wie dem auch sei, die Methode ist:

+ (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(); 
} 

Und die EXC_BAD_ACCESS geschieht auf der CGContextDrawImage Linie. FRAGE 1: Darf ich das in einem anderen Thread als dem UI-Thread tun? FRAGE 2: Was ist der UIImage eigentlich "pre-loading"? FRAGE 3: Was ist der offizielle Weg, um dieses Problem zu lösen?

Vielen Dank für das Lesen, jeder Rat würde sehr geschätzt werden!

Antwort

7

Die Methoden UIGraphics* können nur vom Hauptthread aufgerufen werden. Sie sind wahrscheinlich die Quelle Ihrer Probleme.

Sie können UIGraphicsBeginImageContext() durch einen Anruf an CGBitmapContextCreate() ersetzen; Es ist ein wenig komplizierter (Sie müssen einen Farbraum erstellen, den richtigen Puffer für die Erstellung ermitteln und ihn selbst zuweisen). Die Methoden CG* können aus einem anderen Thread ausgeführt werden.


Ich bin nicht sicher, wie Sie UIImage sind initialisiert, aber wenn Sie es mit imageNamed: oder initWithFile: dann tun könnten Sie in der Lage sein, sie zu zwingen, durch das Laden der Daten selbst zu laden und dann initWithData: aufrufen. Das Stottern ist wahrscheinlich auf Lazy-Datei-E/A zurückzuführen, so dass das Initialisieren mit einem Datenobjekt nicht die Möglichkeit bietet, von einer Datei zu lesen.

+0

hi, danke, dass du dir die Zeit genommen hast, damit zu helfen. Ich kam schließlich zu dieser Erkenntnis. Ich kann mir nur vorstellen, welche Art von unangenehmem nicht-threadsicherem Caching in diesen UI * -Methoden vor sich geht. Meine Schnittstelle ist jetzt stottern frei ..... – JBx

+0

Ich bin verwirrt ... du sagst imageNamed: verursacht faule Datei IO aber .. das Errichten eines Datengegenstandes nicht? Oder sagen Sie, dass Sie die gepoolten Datenobjekte irgendwo sammeln sollten, um daraus zu ziehen? –

+1

Jasconius: Wenn Sie ein UIImage-Objekt erstellen und ihm den Pfad zu einer Datei zuweisen, lädt es möglicherweise nicht das gesamte Bild in den Speicher, bis Sie es zeichnen. Wenn Sie die Datei mit einem NSData-Objekt in den Arbeitsspeicher laden und dann THAT an UIImage übergeben, hat sie keine andere Wahl, als sie im Arbeitsspeicher zu belassen. Was am besten ist, hängt von der spezifischen Situation ab. – benzado

4

Ich hatte das gleiche Problem, obwohl ich das Bild mit Daten initialisiert. (Ich denke, die Daten lazily geladen wird, auch?) Mir gelungen Decodierung zu zwingen, die folgende Kategorie mit:

@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 
+0

Das schien mir sehr gut zu funktionieren. Vielen Dank!!! : D – Marky

22

ich das gleiche Stottern Problem gehabt haben, mit etwas Hilfe, die ich hier die richtige Lösung heraus : Non-lazy image loading in iOS

Zwei wichtige Dinge zu erwähnen:

  • nicht UIKit Methoden in einem Arbeiter-Thread verwenden. Verwenden Sie stattdessen CoreGraphics.
  • Auch wenn Sie einen Hintergrundthread zum Laden und Dekomprimieren von Bildern haben, haben Sie immer noch ein kleines Stottern, wenn Sie die falsche Bitmaske für Ihren CGBitmapContext verwenden.Dies sind die Optionen, die Sie (es ist immer noch ein wenig unklar mir warum) wählen:

-

CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace, 
          kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little); 

Ich habe ein Beispielprojekt hier gepostet: SwapTest, es hat etwa die gleiche Performace als Apples Photos App zum Laden/Anzeigen von Bildern.

+3

Ich kann nicht verstehen, warum Sie 0 upvotes hatten. Ihre Lösung war die einzige, die für mich arbeitete. Ich würde dir 10 Ups geben, wenn ich könnte. DANKE!!! – Kalle

11

I verwendet SwapTest UIImage der Kategorie @ jasamer meine große UIImage (ca. 3000x2100 Pixel) in einem Arbeitsthread zu zwingen laden (mit NSOperationQueue). Dies reduziert die Stotterzeit, wenn das Bild in die UIImageView auf einen akzeptablen Wert gesetzt wird (etwa 0,5 Sekunden auf iPad1).

Hier SwapTest UIImage Kategorie ... Nochmals vielen Dank @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 

Und diese Datei: So erstelle ich NSOpeationQueue und setze das Image auf Hauptthread ...

// 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]; 
} 
+0

Es gibt immer noch einen Aufruf von UIImage in einem anderen Thread als Ihrem Hauptthread, daher ist diese Lösung nicht sicher. –

+0

Sie könnten CGImageRef imageRef = CGImageCreateWithJPEGDataProvider (CGDataProviderCreateWithFilename ([Pfad UTF8String]), NULL, NO, kCGRenderingIntentDefault) anstelle der UIImage – Tieme

+0

@JorisMans wie ist das Aufrufen von UIImage außerhalb Hauptthread nicht sicher? – Hlung