1

画像を非同期でダウンロードして Core Data に保存しようとしています。ステップ 1 は、json ファイルをダウンロードして解析し、フィード内の各オブジェクトのコア データにエンティティを保存することです。その部分はうまく機能しています。

Core Data に 10 個の Bird オブジェクトがあるとします。各 Bird には、名前、説明など、および独自のエンティティである BirdImage との対多関係があります。BirdImage には、「image_url」属性 (文字列) と「image」属性 (Transformable) があります。

さて、鳥の写真を表示するアプリの画面にたどり着いたら、まずBirdImageの「image」属性を確認します。null でない場合は、whateverBirdEntity.image を UIImageView の画像として設定します。null の場合は、画像をダウンロードする必要があります。次のようなコードでは:

@property (nonatomic, strong) AssetRequest *assetRequest; //this is just a wrapper for an asset url, cache policy, and time out
@property (nonatomic, strong) NSURLRequest *assetURLRequest;
@property (nonatomic, strong) NSURLConnection *assetConnection;
@property (nonatomic, strong) NSMutableData *assetConnectionData;
@property (nonatomic, strong) BirdImage *imageEntity;
@property (nonatomic, strong) NSManagedObjectContext *objectContext;

...

- (void)load {

    dispatch_async(dispatchQueue, ^{

        //Check for the image in Core Data

        self.objectContext = [[NSManagedObjectContext alloc]
                              initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        self.objectContext.parentContext = [[CoreDataController sharedController] managedObjectContext];


        NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:@"BirdImage"];
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"image_url = %@", [self.assetRequest.assetURL absoluteString]];
        [fetch setPredicate:predicate];

        NSArray *objects = [self.objectContext executeFetchRequest:fetch error:nil];

        if ([objects count] > 0)
        {
            BirdImage *birdImage = [objects objectAtIndex:0];
            if (birdImage.image) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    BirdAsset *asset = [[BirdAsset alloc] init];

                    asset.url = [NSURL URLWithString:birdImage.image_url];
                    asset.image = birdImage.image;
                    if (self.successBlock)
                        self.successBlock(asset); //the caller will use asset.image for the UIImageView
                });

                return;
            }else{
                //no image found, need to download it
                self.imageEntity = birdImage; //this is the entity I want to re-save in Core Data once the image finishes downloading

                dispatch_async(dispatch_get_main_queue(), ^{

                    self.assetURLRequest = [NSURLRequest requestWithURL:self.assetRequest.assetURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:self.assetRequest.timeOut];

                    self.assetConnection = [[NSURLConnection alloc] initWithRequest:self.assetURLRequest delegate:self startImmediately:NO];
                    [self.assetConnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
                    [self.assetConnection start];

                });
            }

        }

        }];


    });    
}

次に、ダウンロードが完了すると:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {

    dispatch_async(dispatchQueue, ^{

        UIImage *resultImage = [UIImage decompressImageFromData:self.assetConnectionData];
        NSData *resultData = UIImagePNGRepresentation(resultImage);

        DLog(@"saving to core data: %@", self.imageEntity.image_url); //THIS HAPPENS 10 TIMES (every time)

        self.imageEntity.image = resultImage;

            @try {
                NSError *saveError = nil;
                if (![self.objectContext save:&saveError])
                    NSLog(@"saveError %@", saveError);
            }
            @catch (NSException *exception) {
                NSLog(@"Exception: %@", exception);
            }


            [[CoreDataController sharedController] saveContext];


            BirdAsset *finalAsset = [[BirdAsset alloc] init];
            finalAsset.data = resultData;
            finalAsset.image = resultImage;
            finalAsset.url = [NSURL URLWithString:self.imageEntity.image_url];


            DLog(@"SUCCESS"); //THIS HAPPENS anywhere from 4-7 times. I never get all 10 images.

            dispatch_async(dispatch_get_main_queue(), ^{

                if (self.successBlock)
                    self.successBlock(finalAsset);
            });


    });
}

画像は正常にダウンロードされており、データベースを調べると、BirdImage の「画像」ごとに BLOB データが表示されます。問題は、10 個の画像のうち、ランダムな数の画像が実際に表示されることです (最初の実行時に 4 ~ 7 個のいずれか)。その後、もう一度この画面に戻ると、アプリはロックされ、エラー メッセージやクラッシュは発生しません。ある種の Core Data ロックであると思われます。

「コンテキストを作成したのと同じスレッドからコンテキストにアクセスする」必要があることはわかっています。しかし、異なるメソッド (上記の load および connectionDidFinishLoading メソッドなど) でコンテキストにアクセスしている場合、どうすれば同じスレッドを使用できますか? つまり、ダウンロードが完了したときに画像のスレッドセーフな CoreData コンテキスト保存を実行するようにコードを変更するにはどうすればよいでしょうか?

4

2 に答える 2

1

まず第一に、あなたの画像が別のスレッドでダウンロードされているかどうかわかりません

 dispatch_async(dispatch_get_main_queue(), ^{
       self.assetURLRequest = [NSURLRequest requestWithURL:self.assetRequest.assetURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:self.assetRequest.timeOut];
       self.assetConnection = [[NSURLConnection alloc] initWithRequest:self.assetURLRequest delegate:self startImmediately:NO];
       [self.assetConnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
       [self.assetConnection start];

  });

dispatch_get_main_queue()メインスレッドに関連付けられているメインキューを[NSRunLoop currentRunLoop]返すため、メインスレッドの実行ループが返されますが、これはあまり良くありません。

blob第二に、データベースのサイズが劇的に増加し、クエリやその他の操作の実行に時間がかかるため、画像をデータベースに保存することはお勧めできません。そのため、画像をローカル (ドキュメント ディレクトリ) に保存するか、一定期間キャッシュする必要があります。画像へのパスのみをデータベースに保存します。

第三に、self.imageEntity = birdImageこれは安全ではありません。この行が複数回呼び出され、1 つの画像のみがダウンロードされる可能性があるため、エンティティへの参照が失われます。これが画像が完全にダウンロードされない主な理由だと思います。

AFNetworking第 4 に、useに依存する必要がありますAFImageRequestOperation。これにより、非同期ダウンロードが処理され、画像の URL をエンティティの URL と比較して画像を保存できます。

于 2013-06-04T19:27:02.793 に答える