0

今日、私は次の質問をしました:ビューが押されたときに iOS ブロックが停止する

私が言及した操作 (OP1) は、実際には NSURLConnection を使用した、私のサーバーへの "http get" です。
さらに調査を重ねた結果、ブロックは実際には「死ぬ」わけではないことがわかりました。実際に何が起こるかというと、ビューがプッシュされた後でも ([NSThread sleep:10] で確認)、リクエストが実際に送信されます (サーバー側でログに記録されます)。サーバーは応答しますが、view2 がプッシュされた場合、アプリ側では何も起こりません! 接続がデリゲートを失ったかのように! 私が見ている別の可能性は、「NSURLConnectionがrsMainLoopに関連しているという事実ですか?」

誰でも助けることができますか?

Pls は次のことを忘れないでください:
0. 操作が完了するまで view2 がプッシュされない限り、すべて正常に動作します。
1. リクエストは非同期に送信されます
2. デリゲートを設定し、ビューが変更されない限り機能します
3. ビュー1は「シングルトン オブジェクト参照」プロパティ「OP1Completed」を使用して操作を開始します
4. ビュー2は OP1 の完了をチェックします「singleton オブジェクト参照」のプロパティ経由
5. ビュー2は、「singleton.OP1Result」プロパティに移動して「結果」を取得します。

編集 1:
いくつかのコードを用意しましょう。最初に、シングルトンの関連コード (「Interaction」という名前) を次に示します。

-(void)loadAllContextsForUser:(NSString *)username{
userNameAux = username;
_loadingContextsCompleted = NO;
if (contextsLoaderQueue == NULL) {
    contextsLoaderQueue = dispatch_queue_create("contextsLoaderQueue", NULL);
}

dispatch_async(contextsLoaderQueue, ^{
    NSLog(@"Loading all contexts block started");
    [self requestConnectivity];

    dispatch_async(dispatch_get_main_queue(), ^{
        [Util Get:[NSString stringWithFormat:@"%@/userContext?username=%@", Util.azureBaseUrl, [username stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]
     successBlock:^(NSData *data, id jsonData){
         NSLog(@"Loading all contexts block succeeded");
         if([userNameAux isEqualToString:username]){
             _allContextsForCurrentUser = [[NSSet alloc]initWithArray: jsonData];
         }
     } errorBlock:^(NSError *error){
         NSLog(@"%@",error);
     } completeBlock:^{
         NSLog(@"load all contexts for user async block completed.");
         _loadingContextsCompleted = YES;
         [self releaseConnectivity];
     }];
    });

    while (!_loadingContextsCompleted) {
        NSLog(@"loading all contexts block waiting.");
        [NSThread sleepForTimeInterval:.5];
    }
});
NSLog(@"Load All Contexts Dispatched. It should start at any moment if it not already.");
}

そして、これが実際に要求/応答を処理する Util クラスです。

-(id)initGet:(NSString *)resourceURL successBlock:(successBlock_t)successBlock errorBlock:(errorBlock_t)errorBlock completeBlock:(completeBlock_t)completeBlock;{
if(self=[super init]){
    _data=[[NSMutableData alloc]init];
}

_successBlock = [successBlock copy];
_completeBlock = [completeBlock copy];
_errorBlock = [errorBlock copy];

NSURL *url = [NSURL URLWithString:resourceURL];
NSMutableURLRequest *request = [NSURLRequest requestWithURL:url];
[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
//[_conn scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
//[_conn start];
NSLog(@"Request Started.");

return self;
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    [_data setLength:0];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    [_data appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
id jsonObjects = [NSJSONSerialization JSONObjectWithData:_data options:NSJSONReadingMutableContainers error:nil];

id key = [[jsonObjects allKeys] objectAtIndex:0];
id jsonResult = [jsonObjects objectForKey:key];

_successBlock(_data, jsonResult);
_completeBlock();
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
_errorBlock(error);
_completeBlock();
}

最後に、関連する部分 VC1 を示します (VC2 を押し込みます)。

- (IBAction)loginClicked {
NSLog(@"login clicked. Preparing to exibit next view");

UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard_iPhone" bundle:nil];
AuthenticationViewController *viewController = (AuthenticationViewController *)[storyboard instantiateViewControllerWithIdentifier:@"ContextSelectionView"];

NSLog(@"Preparation completed. pushing view now");

[self presentViewController:viewController animated:YES completion:nil];
}
4

5 に答える 5

1

驚かれるかもしれませんが、いくつかの解決策があります。そのうちのいくつかは非常に一般的で、非常に簡単に実装できます ;) この答えはばかげて精巧ですが、問題の実際の解決策は数​​行のコードを超えることはありません. :)

あなたは典型的な「非同期の問題」に遭遇しました - まあ、それは問題というよりはむしろ、最近の典型的なプログラミング作業です。

非同期タスク OP1 があります。これは ViewController 1 (VC1) 内から開始され、不確定な時間の後に最終的に結果またはエラーが生成されます。

OP1の最終的な結果は、後で VC2 で処理する必要があります。

クライアントが最終的な結果を取得する方法はいくつかあります。たとえば、KVO、デリゲート メソッド、完了ブロック、コールバック関数、フューチャーまたはプロミス、通知ごとなどです。

上記のこれらのアプローチには、1 つの共通点があります。それは、呼び出しサイトが非同期結果プロバイダーによって通知されることです (その逆はありません)。

結果が利用可能になるまで結果をポーリングすることは、悪いアプローチです。同様に、セマフォにぶら下がって、結果が「通知される」まで現在のスレッドをブロックすることも、同様に最適ではありません。

おそらく、完了ブロックに精通しているでしょう。結果が利用可能になったときに呼び出しサイトに通知する典型的な非同期メソッドは次のようになります。

typedef void (^completion_block_t)(id result);

- (void) doSomethingAsyncWithCompletion:(completion_block_t)completionHandler;

注:呼び出しサイトは完了ハンドラーを提供しますが、非同期タスクは完了にブロックを呼び出し、その結果 (またはエラー) をブロックの結果パラメーターに渡します。特に明記しない限り、ブロックが実行される実行コンテキスト (スレッドまたはディスパッチ キューまたは NSOperationQueue) は不明です

しかし、あなたの問題について考えると、単純な非同期関数と完了ハンドラーでは実行可能な解決策が得られません。その「メソッド」を VC1 から VC2 に簡単に渡し、後で VC2 の完了ブロックを「アタッチ」することはできません。

幸いなことに、非同期タスクはすべて にカプセル化できますNSOperation。AnNSOperationには、呼び出しサイトまたは他の場所で設定できるプロパティとして完了ブロックがあります。また、NSOperationオブジェクトは VC1 から VC2 に簡単に渡すことができます。VC2 は単純に操作に完了ブロックを追加し、最終的には操作が完了して結果が利用可能になったときに通知を受け取ります。

ただし、これはあなたの問題の実行可能な解決策ですが、実際にはこのアプローチにはいくつかの問題があります。詳しく説明したくありませんが、代わりにさらに優れたものを提案します:「約束」。

「Promise」は、非同期タスクの最終的な結果を表します。つまり、非同期タスクの結果がまだ評価されていなくても、promise は存在します。Promise は、メッセージを送信できる通常のオブジェクトです。したがって、Promise は NSOperations と同じように渡すことができます。Promise は、非同期メソッド/関数の戻り値です。

-(Promise*) doSomethingAsync;

Promise と非同期の関数/メソッド/タスク/操作を一致させないでください。Promise は、タスクの最終的な結果の単なる表現です。

Promise は、最終的には非同期タスクによって解決されなければなりません (MUST )。つまり、タスクは結果値とともに「履行」メッセージを Promise に送信するか、エラーとともに「拒否」メッセージを Promise に送信する必要があります。promise は、タスクから渡された結果値の参照を保持します。

Promise は 1 回だけ解決できます。

最終的な結果を取得するために、クライアントは成功ハンドラエラー ハンドラ を「登録」できます。成功ハンドラーは、タスクが promiseを満たす(つまり、成功した) ときに呼び出され、エラー ハンドラーは、タスクが promise を拒否したときに呼び出され、その理由がエラー オブジェクトとして渡されます。

promise の特定の実装を想定すると、promise の解決は次のようになります。

- (Promise*) task {
    Promise* promise = [Promise new];
    dispatch_async(private_queue, ^{
        ...
        if (success) {
            [promise fulfillWithValue:result];
        }
        else {
            NSError* error = ...; 
            [promise rejectWithReason:error];
        }
    });     
    return promise;
}

クライアントは、次のように、最終的な結果を取得するためにハンドラーを「登録」します。

Promise* promise = [self fetchUsers];

promise.then( <success handler block>, <error handler block> );

成功ハンドラーとエラー ハンドラー ブロックは次のように宣言されます。

typedef  id (^success_handler_block)(id result);
typedef  id (^error_handler_block)(NSError* error);

成功ハンドラーを単に「登録」するには (この場合、非同期タスクは正常に「返されます」)、次のように記述します。

promise.then(^id(id users) {
    NSLog(@"Users:", users);
    return nil;
}, nil);

タスクが成功すると、ハンドラーが呼び出され、ユーザーがコンソールに出力されます。タスクが失敗すると、成功ハンドラーは呼び出されません。

エラーハンドラーを「登録」するだけ(この場合、非同期タスクが失敗する) には、次のように記述します。

promise.then(nil, ^id(NSError* error) {
    NSLog(@"ERROR:", error);
    return nil;
}, nil);

タスクが成功した場合、エラー ハンドラは呼び出されません。タスク (または子タスク) が失敗した場合にのみ、このエラー ハンドラーが呼び出されます。

非同期タスクの結果が最終的に利用可能になると、ハンドラー内のコードは「特定されていない実行コンテキストで」実行されます。つまり、どのスレッドでも実行できます。(注: メインスレッドなど、実行コンテキストを指定する方法があります)。

promise は、複数のハンドラー ペアを登録できます。いつでもどこでも、必要な数のハンドラーを追加できます。これで、実際の問題との関係を理解する必要があります。

VC1 で非同期タスクを開始し、promise を取得できます。次に、この promise を VC2 に渡します。VC2 では、結果が最終的に利用可能になったときに呼び出されるハンドラーを追加できます。

Promise を VC2 に渡すときに結果が実際に既に利用可能な場合、つまり、Promise が既に解決されている場合は心配しないでください。ハンドラーを追加することもでき、それらは適切に (すぐに) 起動されます。

複数のタスクを「チェーン」することもできます。つまり、タスク 1 が終了したときにタスク 2 を 1 回呼び出します。4 つの非同期タスクの「チェーン」または「継続」は次のようになります。

Promise* task4Promise = 
[self task1]
.then(^id(id result1){
    return [task2WithInput:result1];
}, nil)
.then(^id(id result2){
    return [task3WithInput:result2];
}, nil)
.then(^id(id result3){
    return [task4WithInput:result3];
}, nil);

task4Promiseは の最終的な結果を表しtask4WithInput:ます。

taskA が正常に終了したときに並行して開始される taskB と taskC のように、タスクを並行して実行することもできます。

Promise* root = [self taskA];
root.then(^id(id result){
    return [self taskB];
}, nil);
root.then(^id(id result){
    return [self taskC];
}, nil);

このスキームを使用すると、タスクの非循環グラフを定義できます。各タスクは、後続タスク (「親」) の実行の成功に依存します。「エラー」はルートに渡され、最後のエラー ハンドラ (存在する場合) によって処理されます。

Objective-C の実装はいくつかあります。「RXPromise」(GitHub で入手可能) というものを自分で作成しました。最も強力な機能の 1 つは「キャンセル」です。これは promise の標準機能ではありませんが、RXPromise に実装されています。これにより、非同期タスクのツリーを選択的にキャンセルできます。

約束については他にもたくさんあります。特に JavaScript コミュニティでは、Web を検索できます。

于 2013-09-05T21:50:40.737 に答える
1

最初のコントローラーで行われるワークフロー、具体的には、ダウンロードを開始するためにユーザーが行うことと、次のコントローラーが提示される前 (およびそのコントローラーがインスタンス化されるとき) に他のユーザーが行うことを理解しているかどうかはわかりません)。過去に複数のクラスからのダウンロードを必要とするアプリを作成したとき、NSURLConnection を作成し、すべてのコールバックを実装するダウンロード クラスを作成しました。データ (生データまたはエラー オブジェクト) をデリゲートに送り返すためのデリゲート プロトコル メソッドが 1 つあります。

2 つのボタンを使用して、ワークフローをシミュレートする簡単なテスト ケースを作成しました。Downloader クラスのインスタンスをインスタンス化し、次のコントローラーを作成し、それをダウンローダーのデリゲートとして設定し、ダウンロードを開始します。2 番目のボタンは、その 2 番目のコントローラーへのプッシュを行います。これは、プッシュがいつ発生しても機能しますが、状況に関連しているかどうかはわかりません (Network Link Conditioner を使用して低速接続をシミュレートしてテストしています)。

最初のコントローラー:

#import "ViewController.h"
#import "ReceivingViewController.h"
#import "Downloader.h"

@interface ViewController ()
@property (strong,nonatomic) ReceivingViewController *receiver;
@end

@implementation ViewController

-(IBAction)buttonClicked:(id)sender {
    Downloader *loader = [Downloader new];
    self.receiver = [self.storyboard instantiateViewControllerWithIdentifier:@"Receiver"];
    loader.delegate = self.receiver;
    [loader startLoad];
}

-(IBAction)goToReceiver:(id)sender {
    [self.navigationController pushViewController:self.receiver animated:YES];
}

ダウンロード クラス .h:

@protocol DownloadCompleted <NSObject>
-(void)downloadedFinished:(id) dataOrError;
@end

@interface Downloader : NSObject

@property (strong,nonatomic) NSMutableData *receivedData;
@property (weak,nonatomic) id <DownloadCompleted> delegate;

-(void)startLoad;

ダウンローダー .m:

-(void)startLoad {
    NSLog(@"start");
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"] cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:10];
    NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
    if (connection) self.receivedData = [NSMutableData new];
}

-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    self.receivedData.length = 0;
}


-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.receivedData appendData:data];
}


-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.delegate downloadedFinished:error];
}


-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.delegate downloadedFinished:self.receivedData];
}

-(void)dealloc {
    NSLog(@"In Downloader dealloc. loader is: %@",self);
}

2 番目のコントローラー:

@interface ReceivingViewController ()
@property (strong,nonatomic) NSData *theData;
@end

@implementation ReceivingViewController


-(void)downloadedFinished:(id)dataOrError {
    self.theData = (NSData *)dataOrError;
    NSLog(@"%@",self.theData);
}

-(void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"%@",self.theData);
}
于 2013-09-07T02:19:23.643 に答える
0

だから、ここに私が確実にうまくいくと思うものがあります:

フラグを新しいコントローラーに渡します。フラグが未完成の場合は、新しい VC でロードをやり直して、ロードが完了するまでデータが表示されないようにします。

ただし、AFNetworking で非同期呼び出しをディスパッチすると、新しい VC がプッシュされた後もロードが続行されるため、新しい VC がプッシュされるとスレッドが停止するのは奇妙だと思います。おそらく、別のフレームワークを使用している場合は、AFNetworking を使用する必要があります。

そのため、新しい VC がプッシュされた後もスレッドが実際に続行する場合(コードがクラッシュするため、続行しないと思われます)、次のことを試してください。

a) フラグを渡します。操作が終了した場合は、通常どおり続行します
。b) そうでない場合は、何もロードせず、フラグが設定されているかどうかを確認し、設定されている場合はデータを返す、2 つの間である種のデリゲート メソッドを呼び出します。

デリゲートの設定方法についてご不明な点がございましたら、お気軽にお問い合わせください。詳細をご説明いたします。

于 2013-09-05T20:09:33.463 に答える
0

最初の質問のコメントで既に述べたように、おそらく2 つの問題があります。

  1. 設計上の問題
  2. ブロックの原因となるコードの問題。(ただし、コードがないと、これを理解するのは困難です)。

実用的なアプローチを提案しましょう:

たとえば、私たちのシングルトンは、HTTP リクエストを実行する「ローダー」クラスです。ネットワーク要求の状態を決定するプロパティをポーリングする代わりに、状態を要求できる何らかのオブジェクトを返す必要があります。または、要求が終了したときに呼び出される完了ブロックをVC2登録できる場所に戻す必要があります。

は、非同期ネットワーク要求の最終的なNSOperation結果を表すために「使用」できます。しかし、これは少し扱いに​​くいです - サブクラス RequestOperation があるとします:

RequestOperation* requestOp = [[Loader sharedLoader] fetchWithURL:url];

ここで、「requestOp」は、最終的な結果を含むネットワーク リクエストを表します。

この操作は VC1 で取得できます。

ステートレスである可能性があるため、特定の操作について共有ローダーに問い合わせたくない場合があります 。つまり、それ自体はリクエスト操作を追跡しません。Loaderネットワークリクエストを開始するためにクラスを数回使用したいと考えてください- 並行して可能です。では、リクエストの状態について何かを教えてくれる1 つのプロパティを尋ねるとき、どのリクエストを意味するのでしょうか? (うまくいきません)。Loader

したがって、再び作業アプローチと VC1 に戻ります。

VC1RequestOperationで のサブクラスであるオブジェクトを取得したとしますNSOperation。がプロパティ- をRequestOperation持っているとします。これは、リクエスト操作の最終的な応答データを表すオブジェクトです。responseBodyNSData

リクエストの最終的なレスポンス ボディを取得するために、単にプロパティに問い合わせることはできません。接続がまだ実行されている可能性があります。取得nilまたはガベージするか、スレッドをブロックする可能性があります。動作は の実装に依存しRequestOperationます。

解決策は次のとおりです。

VC2:

VC1 がrequestOpを VC2に「渡した」と仮定します(たとえばprepareForSegue:sender:)。

非同期の正しい方法で応答本文を取得するには、いくつかの追加手順が必要です。

NSBlockOperation応答本文を処理するブロックを実行する を作成します。次に例を示します。

NSBlockOperation* handlerOp = [NSBlockOperation blockOperationWithBlock:^{
    NSData* body = requestOp.responseBody;
    dispatch_async(dispatch_get_main_queue(), ^{
        self.model = body;
        [self.tableView reloadData];
    });
}];

次に、 handlerOp をrequestOpに依存させます。つまり、 requestOpが終了したときにhandlerOpの実行を開始します。

[handlerOP addDependency:requestOp];

実行するために、 handlerOpをキューに追加します。

[[NSOperation mainQueue] addOperation:handlerOp];

これはまだ「非同期的に」考える必要があります - これを回避する方法はありません。一番いいのは、実用的なパターンとイディオムに慣れることです。


別のアプローチは、RXPromise を使用することです (サードパーティ ライブラリから)。

VC1:

requestPromise = [Loader fetchWithURL:url];

さて、VC2 で:

VC1 がrequestPromiseを VC2に「渡した」と仮定します(たとえばprepareForSegue:sender:)。

たとえば、viewDidLoad次のようになります。

requestPromise.thenOn(dispatch_get_main_queue(), ^id(id responseBody){
    // executes on main thread!
    self.model = responseBody;
    [self.tableView reloadData];
    return nil;
}, nil);

ボーナス:

必要に応じて、promise に送信することで、いつでもネットワーク リクエストをキャンセルできます。cancel

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.requestPromise cancel];
    self.requestPromise = nil;
}
于 2013-09-06T11:28:35.407 に答える