web-dev-qa-db-ja.com

iOSで完了ハンドラはどのように機能しますか?

完了ハンドラとブロックを理解しようとしています。私は、完了ハンドラーがなくても、多くのディーププログラミングにブロックを使用できると思いますが、完了ハンドラーはブロックに基づいていると思います。 (基本的に、完了ハンドラーはブロックを必要としますが、その逆は必要ありません)。

だから私は古いTwitterフレームワークについてこのコードをインターネットで見ました:

[twitterFeed performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
        if (!error) {
            self.successLabel.text = @"Tweeted Successfully";
            [self playTweetSound];
        } else {
            // Show alert
        }
        // Stop indicator 
        sharedApplication.networkActivityIndicatorVisible = NO;
    }];

ここでは、(TWRequestを実行して)処理を行い、responseData&urlResponse&errorで終了したときに戻るメソッドを呼び出しています。戻ったときだけ、テストが許可されたブロックを実行し、アクティビティインジケーターを停止します。パーフェクト!

今、これは私が機能する別のアプリのために持っている設定ですが、私はピースをまとめようとしています:

@interface
Define an ivar
typedef void (^Handler)(NSArray *users);
Declare the method
+(void)fetchUsersWithCompletionHandler:(Handler)handler;

@implementation
+(void)fetchUsersWithCompletionHandler:(Handler)handler {
    //...Code to create NSURLRequest omitted...
    __block NSArray *usersArray = [[NSArray alloc] init];

    //A. Executes the request 
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{

        // Peform the request
        NSURLResponse *response;
        NSError *error = nil;
        NSData *receivedData = [NSURLConnection sendSynchronousRequest:request
                                                     returningResponse:&response
                                                                 error:&error];
        // Deal with your error
        if (error) {
            }
            NSLog(@"Error %@", error);
            return;
        }
        // Else deal with data
        NSString *responseString = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];
        usersArray = [NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:NSASCIIStringEncoding] options:0 error:nil];

        // Checks for handler & returns usersArray to main thread - but where does handler come from & how does it know to wait tip usersArray is populated?
        if (handler){
            dispatch_sync(dispatch_get_main_queue(), ^{
            handler(usersArray);
            });
        }
    });
}

これが私の理解です:

  1. fetchUsersWithCompletionHandlerは、performRequestWithHandlerのホモログです。
  2. 残念ながら、途中でGCD呼び出しがあるため、これはもう少し複雑です...

しかし、基本的には、リクエストが実行されてエラーが処理され、データが処理されてから、ハンドラーがチェックされます。私の質問は、このハンドラ部分はどのように機能するのですか?私はそれが存在するかどうかを理解し、メインキューに送り返し、usersArrayを返します。しかし、usersArrayが入力されるまで待機することをどのようにして知るのでしょうか。私が混乱しているのは、この場合のmethod:blockの中に別のブロック、dispatch_async呼び出しがあることです。私が探しているのは、実際に処理を行い、responseDataとurlResponseを返すようになっていることを知っているロジックだと思います。同じアプリではないことはわかっていますが、performRequestWithHandlerのコードが表示されません。

18
marciokoko

基本的にこの場合、それはそのように機能します:

  1. FetchUsersWithCompletionHandler:は、好きなスレッドから呼び出します(おそらくメインのスレッドを形成します)。
  2. それはNSURLRequestを初期化し、次に呼び出します:dispatch_async(dispatch_get_global_queue ...これは基本的にブロックを作成し、バックグラウンドキューでの処理のためにそれをスケジュールします。
  3. これはdispath_asyncなので、現在のスレッドはfetchUsersWithCompletionHandler:メソッドを離れます。

    ...
    バックグラウンドキューに空きリソースができるまで時間が経過します
    ...

  4. そして今、バックグラウンドキューが解放されると、スケジュールされた操作を消費します(注:synchronousリクエストを実行するため、データを待機します):

    NSURLResponse *response;
    NSError *error = nil;
    NSData *receivedData = [NSURLConnection sendSynchronousRequest:request
                                                 returningResponse:&response
                                                             error:&error];
    ...
    
  5. データが来ると、sersArrayが入力されます。

  6. コードはこの部分に続きます:

    if (handler){
        dispatch_sync(dispatch_get_main_queue(), ^{
            handler(usersArray);
        });
    }
    
  7. ハンドラーを指定した場合、メインキューでの呼び出し用にブロックがスケジュールされます。これはdispatch_syncなので、メインスレッドがブロックで完了するまで、現在のスレッドでの実行は続行されません。この時点で、バックグラウンドスレッドは辛抱強く待機しています。

    ...
    別の瞬間が過ぎる
    ...

  8. これで、メインキューにいくつかの空きリソースがあるため、ブロックの上で消費し、このコードを実行します(以前に入力されたusersArrayを「ハンドラー」に渡します)。

    handler(usersArray);
    
  9. 完了すると、ブロックから戻り、メインキューにあるものはすべて消費し続けます。

  10. メインスレッドはブロックで行われるため、バックグラウンドスレッド(dispatch_syncでスタック)もさらに続行できます。この場合、単にブロックから戻ります。

編集:あなたが尋ねた質問に関しては:

  1. メイン/バックグラウンドキューが常にビジーであるようではなく、単にビジーである可能性があります。 (バックグラウンドキューがメインのような並列操作をサポートしていないと仮定します)。メインスレッドで実行されている次のコードを想像してください。

        dispatch_async(dispatch_get_main_queue(), ^{
            //here task #1 that takes 10 seconds to run
            NSLog(@"Task #1 finished");
        });
        NSLog(@"Task #1 scheduled");
    
        dispatch_async(dispatch_get_main_queue(), ^{
            //here task #2 that takes 5s to run
            NSLog(@"Task #2 finished");
        });
        NSLog(@"Task #2 scheduled");
    

どちらもdispatch_async呼び出しであるため、これらを次々に実行するようにスケジュールします。ただし、タスク#2はすぐにメインキューによって処理されません。最初に現在の実行ループを終了する必要があるためです。次に、最初にタスク#1を終了する必要があります。

したがって、ログ出力は次のようになります。

Task #1 scheduled
Task #2 scheduled
Task #1 finished
Task #2 finished

2.あなたは持っています:

typedef void (^Handler)(NSArray *users);

Handlerの戻り値の型を持ち、NSArray *をパラメーターとして受け入れるvoidとして定義されたブロックタイプを宣言します。

その後、あなたはあなたの機能を持っています:

+(void)fetchUsersWithCompletionHandler:(Handler)handler

これは、タイプHandlerのパラメーターブロックとして受け取り、ローカル名handlerを使用してそれにアクセスできるようにします。

そしてステップ#8:

handler(usersArray);

これはhandlerブロックを直接呼び出し(C/C++関数を呼び出しているように)、usersArrayをパラメーターとしてそれに渡します。

29
deekay