39 votes

Chargement non lent des images dans iOS

J'essaie de charger des images UII dans un fil d'arrière-plan, puis de les afficher sur l'iPad. Cependant, il y a un bégaiement lorsque je règle la propriété view des imageViews sur l'image. J'ai vite compris que le chargement des images est paresseux sur iOS, et j'ai trouvé une solution partielle dans cette question :

Le chargement paresseux de CGImage/UIImage sur le fil de l'interface utilisateur provoque un bégaiement.

Cela force effectivement l'image à être chargée dans le fil de discussion, mais il y a toujours un bégaiement lors de l'affichage de l'image.

Vous pouvez trouver mon exemple de projet ici : http://www.jasamer.com/files/SwapTest.zip (éditer : version fixe ), vérifiez le SwapTestViewController. Essayez de faire glisser l'image pour voir le bégaiement.

Le code de test que j'ai créé et qui bégaie est le suivant (la méthode forceLoad est celle reprise de la question de stack overflow que j'ai postée ci-dessus) :

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's missing here?

        [self performSelectorOnMainThread: @selector(setImage:) withObject: image waitUntilDone: YES];
        [image release];
    }
}];

Il y a deux raisons pour lesquelles je sais que le bégaiement peut être évité :

(1) Apple est capable de charger les images sans bégaiement dans l'application Photos.

(2) Ce code ne provoque pas de bégaiement après que les placeholder1 et placeholder2 ont été affichés une fois dans cette version modifiée du code ci-dessus :

    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];
    }
}];

Cependant, je ne peux pas garder toutes mes images en mémoire.

Cela implique que forceLoad ne fait pas tout le travail - il y a quelque chose d'autre qui se passe avant que les images soient effectivement affichées. Quelqu'un sait-il ce que c'est, et comment je peux le mettre dans le fil d'arrière-plan ?

Merci, Julian

Mise à jour

J'ai utilisé quelques-uns des conseils de Tommys. Ce que j'ai découvert, c'est que c'est CGSConvertBGRA8888toRGBA8888 qui prend autant de temps, donc il semble que ce soit une conversion de couleur qui cause le décalage. Voici la pile d'appels (inversée) de cette méthode.

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         

Les derniers changements de masque de bits qu'il a proposés n'ont rien changé, malheureusement.

40voto

Tommy Points 56749

UIKit ne peut être utilisé que sur le thread principal. Votre code est donc techniquement invalide, puisque vous utilisez UIImage depuis un thread autre que le thread principal. Vous devriez utiliser CoreGraphics seul pour charger (et décoder de manière non aléatoire) les graphiques sur un thread d'arrière-plan, envoyer le CGImageRef au thread principal et le transformer en UIImage à cet endroit. Cela peut sembler fonctionner (mais avec le bégaiement que vous ne voulez pas) dans votre implémentation actuelle, mais ce n'est pas garanti. Il semble y avoir beaucoup de superstitions et de mauvaises pratiques dans ce domaine, il n'est donc pas surprenant que vous ayez réussi à trouver de mauvais conseils...

Il est recommandé de l'exécuter sur un fil d'arrière-plan :

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

Il n'est pas nécessaire de faire le malloc/free si vous êtes sur iOS 4 ou plus, vous pouvez simplement passer NULL comme paramètre pertinent de CGBitmapContextCreate, et laisser CoreGraphics s'occuper de son propre stockage.

Cette solution diffère de celle que vous affichez pour la cause :

  1. crée une CGImage à partir d'une source de données PNG - le chargement paresseux s'applique, donc ce n'est pas nécessairement une image entièrement chargée et décompressée.
  2. crée un contexte bitmap de la même taille que le PNG
  3. dessine l'image CGI de la source de données PNG sur le contexte bitmap - cela devrait forcer le chargement et la décompression complets puisque les valeurs de couleur réelles doivent être placées quelque part où nous pourrions y accéder depuis un tableau C. Cette étape est aussi poussée que le forceLoad auquel vous faites référence.
  4. convertit le contexte bitmap en une image
  5. publie cette image dans le fil de discussion principal, vraisemblablement pour devenir une UIImage.

Il n'y a donc pas de continuité d'objet entre la chose chargée et la chose affichée ; les données des pixels passent par un tableau C (donc, aucune possibilité de manigances cachées) et ce n'est que si elles ont été placées correctement dans le tableau qu'il est possible de faire l'image finale.

14voto

jasamer Points 573

Ok, j'ai trouvé - avec l'aide de Tommy. Merci !

Si vous créez votre contexte avec

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

la boucle d'exécution principale ne provoquera plus de conversions sur le fil principal. L'affichage est maintenant très fluide (les drapeaux sont un peu contre-intuitifs, quelqu'un sait-il pourquoi il faut choisir kCGImageAlphaPremultipliedFirst).

Edit :

J'ai téléchargé l'exemple de projet corrigé : http://www.jasamer.com/files/SwapTest-Fixed.zip . Si vous avez des problèmes de performance d'image, c'est un excellent point de départ !

10voto

Kazuki Sakamoto Points 10100

Et si UIImage+ImmediateLoad.m ?

Il décode les images immédiatement. En outre, UIImage -initWithContentsOfFile :, -imageWithCGImage : et CG* sont thread-safe après iOS4.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X