Mengelola beberapa koneksi NSURLConnection asynchronous

88

Saya memiliki banyak kode berulang di kelas saya yang terlihat seperti berikut:

NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
                                                              delegate:self];

Masalah dengan permintaan asinkron adalah ketika Anda memiliki berbagai permintaan yang keluar, dan Anda memiliki delegasi yang ditugaskan untuk memperlakukan mereka semua sebagai satu entitas, banyak kode bercabang dan jelek mulai merumuskan:

Jenis data apa yang kami dapatkan kembali? Jika berisi ini, lakukan itu, jika tidak, lakukan yang lain. Menurut saya, akan bermanfaat jika dapat menandai permintaan asinkron ini, seperti Anda dapat menandai tampilan dengan ID.

Saya ingin tahu strategi apa yang paling efisien untuk mengelola kelas yang menangani beberapa permintaan asinkron.

Coocoo4Cocoa
sumber

Jawaban:

77

Saya melacak tanggapan dalam CFMutableDictionaryRef yang dikunci oleh NSURLConnection yang terkait dengannya. yaitu:

connectionToInfoMapping =
    CFDictionaryCreateMutable(
        kCFAllocatorDefault,
        0,
        &kCFTypeDictionaryKeyCallBacks,
        &kCFTypeDictionaryValueCallBacks);

Mungkin tampak aneh untuk menggunakan ini daripada NSMutableDictionary tetapi saya melakukannya karena CFDictionary ini hanya mempertahankan kuncinya (NSURLConnection) sedangkan NSDictionary menyalin kuncinya (dan NSURLConnection tidak mendukung penyalinan).

Setelah selesai:

CFDictionaryAddValue(
    connectionToInfoMapping,
    connection,
    [NSMutableDictionary
        dictionaryWithObject:[NSMutableData data]
        forKey:@"receivedData"]);

dan sekarang saya memiliki kamus data "info" untuk setiap sambungan yang dapat saya gunakan untuk melacak informasi tentang sambungan dan kamus "info" sudah berisi objek data yang dapat berubah yang dapat saya gunakan untuk menyimpan data balasan saat masuk.

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    NSMutableDictionary *connectionInfo =
        CFDictionaryGetValue(connectionToInfoMapping, connection);
    [[connectionInfo objectForKey:@"receivedData"] appendData:data];
}
Matt Gallagher
sumber
Karena ada kemungkinan bahwa dua atau lebih koneksi asinkron dapat memasuki metode delegasi dalam satu waktu, adakah hal khusus yang perlu dilakukan seseorang untuk memastikan perilaku yang benar?
PlagueHammer
(Saya telah membuat pertanyaan baru di sini menanyakan ini: stackoverflow.com/questions/1192294/… )
PlagueHammer
3
Ini bukan thread safe jika delegasi dipanggil dari beberapa thread. Anda harus menggunakan kunci pengecualian bersama untuk melindungi struktur data. Solusi yang lebih baik adalah membuat subclass NSURLConnection dan menambahkan respons dan referensi data sebagai variabel instan. Saya memberikan jawaban yang lebih rinci menjelaskan hal ini pada pertanyaan Nocturne: stackoverflow.com/questions/1192294/…
James Wald
4
Aldi ... ini aman untuk benang asalkan Anda memulai semua koneksi dari utas yang sama (yang dapat Anda lakukan dengan mudah dengan menjalankan metode koneksi awal Anda menggunakan performSelector: onThread: withObject: waitUntilDone :). Menempatkan semua koneksi dalam NSOperationQueue memiliki masalah yang berbeda jika Anda mencoba untuk memulai lebih banyak koneksi daripada operasi bersamaan maksimum antrian (operasi masuk antrian alih-alih berjalan secara bersamaan). NSOperationQueue berfungsi dengan baik untuk operasi terikat CPU, tetapi untuk operasi terikat jaringan, lebih baik Anda menggunakan pendekatan yang tidak menggunakan kumpulan utas ukuran tetap.
Matt Gallagher
1
Hanya ingin membagikannya untuk iOS 6.0 dan yang lebih baru, Anda dapat menggunakan a [NSMapTable weakToStrongObjectsMapTable]alih - alih a CFMutableDictionaryRefdan menghemat kerumitan. Bekerja dengan baik untuk saya.
Shay Aviv
19

Saya memiliki proyek di mana saya memiliki dua NSURLConnections yang berbeda, dan ingin menggunakan delegasi yang sama. Apa yang saya lakukan adalah membuat dua properti di kelas saya, satu untuk setiap koneksi. Kemudian dalam metode delegasi, saya memeriksa untuk melihat apakah hubungannya


- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    if (connection == self.savingConnection) {
        [self.savingReturnedData appendData:data];
    }
    else {
        [self.sharingReturnedData appendData:data];
    }
}

Ini juga memungkinkan saya untuk membatalkan koneksi tertentu berdasarkan nama saat diperlukan.

jbarnhart.dll
sumber
hati-hati ini bermasalah karena akan memiliki kondisi balapan
adit
Bagaimana Anda menetapkan nama (savingConnection dan sharingReturnedData) untuk setiap koneksi di tempat pertama?
jsherk
@adit, tidak, tidak ada ketentuan balapan yang melekat pada kode ini. Anda harus berusaha keras dengan kode pembuatan koneksi untuk membuat kondisi balapan
Mike Abdullah
'solusi' Anda persis seperti pertanyaan awal yang ingin dihindari, mengutip dari atas: '... banyak kode bercabang dan jelek mulai
terbentuk
1
@adit Mengapa ini mengarah pada kondisi balapan? Ini konsep baru bagi saya.
guptron
16

Subclassing NSURLConnection untuk menyimpan data yang bersih, lebih sedikit kode daripada beberapa jawaban lainnya, lebih fleksibel, dan membutuhkan lebih sedikit pemikiran tentang manajemen referensi.

// DataURLConnection.h
#import <Foundation/Foundation.h>
@interface DataURLConnection : NSURLConnection
@property(nonatomic, strong) NSMutableData *data;
@end

// DataURLConnection.m
#import "DataURLConnection.h"
@implementation DataURLConnection
@synthesize data;
@end

Gunakan seperti yang Anda lakukan NSURLConnection dan kumpulkan data dalam properti datanya:

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    ((DataURLConnection *)connection).data = [[NSMutableData alloc] init];
}

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

Itu dia.

Jika Anda ingin melangkah lebih jauh, Anda dapat menambahkan blok yang berfungsi sebagai panggilan balik hanya dengan beberapa baris kode lagi:

// Add to DataURLConnection.h/.m
@property(nonatomic, copy) void (^onComplete)();

Atur seperti ini:

DataURLConnection *con = [[DataURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
con.onComplete = ^{
    [self myMethod:con];
};
[con start];

dan panggil ketika pemuatan selesai seperti ini:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    ((DataURLConnection *)connection).onComplete();
}

Anda dapat memperluas blok untuk menerima parameter atau hanya meneruskan DataURLConnection sebagai argumen ke metode yang membutuhkannya di dalam blok no-args seperti yang ditunjukkan

Pat Niemeyer
sumber
Ini adalah jawaban luar biasa yang bekerja sangat baik untuk kasus saya. Sangat sederhana dan bersih!
jwarrent
8

INI BUKAN JAWABAN BARU. TOLONG BIARKAN SAYA TUNJUKKAN CARA SAYA

Untuk membedakan NSURLConnection yang berbeda dalam metode delegasi kelas yang sama, saya menggunakan NSMutableDictionary, untuk mengatur dan menghapus NSURLConnection, menggunakan (NSString *)descriptionsebagai kuncinya.

Objek yang saya pilih setObject:forKeyadalah URL unik yang digunakan untuk memulai NSURLRequest, NSURLConnectionpenggunaan.

Setelah disetel, NSURLConnection dievaluasi di

-(void)connectionDidFinishLoading:(NSURLConnection *)connection, it can be removed from the dictionary.

// This variable must be able to be referenced from - (void)connectionDidFinishLoading:(NSURLConnection *)connection
NSMutableDictionary *connDictGET = [[NSMutableDictionary alloc] init];
//...//

// You can use any object that can be referenced from - (void)connectionDidFinishLoading:(NSURLConnection *)connection
[connDictGET setObject:anyObjectThatCanBeReferencedFrom forKey:[aConnectionInstanceJustInitiated description]];
//...//

// At the delegate method, evaluate if the passed connection is the specific one which needs to be handled differently
if ([[connDictGET objectForKey:[connection description]] isEqual:anyObjectThatCanBeReferencedFrom]) {
// Do specific work for connection //

}
//...//

// When the connection is no longer needed, use (NSString *)description as key to remove object
[connDictGET removeObjectForKey:[connection description]];
petershine
sumber
5

Salah satu pendekatan yang saya ambil adalah tidak menggunakan objek yang sama sebagai delegasi untuk setiap koneksi. Sebagai gantinya, saya membuat instance baru dari kelas parsing saya untuk setiap koneksi yang dijalankan dan mengatur delegasi ke instance itu.

Brad The App Guy
sumber
Enkapsulasi yang jauh lebih baik sehubungan dengan satu koneksi.
Kedar Paranjape
4

Coba kelas khusus saya, MultipleDownload , yang menangani semua ini untuk Anda.

leonho
sumber
di iOS6 tidak dapat menggunakan NSURLConnection sebagai kunci.
pengguna501836
2

Saya biasanya membuat berbagai kamus. Setiap kamus memiliki sedikit informasi pengenal, objek NSMutableData untuk menyimpan respons, dan koneksi itu sendiri. Ketika metode delegasi koneksi diaktifkan, saya mencari kamus koneksi dan menanganinya sesuai.

Ben Gottlieb
sumber
Ben, bolehkah aku meminta sepotong kode contoh? Saya mencoba membayangkan bagaimana Anda melakukannya, tetapi tidak semuanya ada.
Coocoo4Cocoa
Khususnya Ben, bagaimana Anda mencari kamus? Anda tidak dapat memiliki kamus kamus karena NSURLConnection tidak menerapkan NSCopying (jadi tidak dapat digunakan sebagai kunci).
Adam Ernst
Matt memiliki solusi yang sangat baik di bawah ini dengan menggunakan CFMutableDictionary, tetapi saya menggunakan berbagai kamus. Pencarian membutuhkan iterasi. Ini bukan yang paling efisien, tapi cukup cepat.
Ben Gottlieb
2

Salah satu opsinya adalah subclass NSURLConnection sendiri dan tambahkan -tag atau metode serupa. Desain NSURLConnection sengaja dibuat tanpa tulang jadi ini bisa diterima.

Atau mungkin Anda bisa membuat kelas MyURLConnectionController yang bertanggung jawab untuk membuat dan mengumpulkan data koneksi. Kemudian hanya perlu memberi tahu objek pengontrol utama Anda setelah pemuatan selesai.

Mike Abdullah
sumber
2

di iOS5 dan yang lebih baru, Anda cukup menggunakan metode kelas sendAsynchronousRequest:queue:completionHandler:

Tidak perlu melacak koneksi karena respons kembali di penangan penyelesaian.

Yariv Nissim
sumber
1

Saya suka ASIHTTPRequest .

ruipacheco.dll
sumber
Saya sangat suka implementasi 'blok' di ASIHTTPRequest - ini seperti Anonymous Inner Type di Java. Ini mengalahkan semua solusi lain dalam hal kebersihan kode dan organisasi.
Matt Lyons
1

Seperti yang ditunjukkan oleh jawaban lain, Anda harus menyimpan connectionInfo di suatu tempat dan mencarinya berdasarkan koneksi.

Jenis data yang paling alami untuk ini adalah NSMutableDictionary, tetapi tidak dapat menerimaNSURLConnection sebagai kunci karena koneksi tidak dapat disalin.

Opsi lain untuk digunakan NSURLConnectionssebagai kunci dalam NSMutableDictionarymenggunakan NSValue valueWithNonretainedObject]:

NSMutableDictionary* dict = [NSMutableDictionary dictionary];
NSValue *key = [NSValue valueWithNonretainedObject:aConnection]
/* store: */
[dict setObject:connInfo forKey:key];
/* lookup: */
[dict objectForKey:key];
mfazekas.dll
sumber
0

Saya memutuskan untuk membuat subkelas NSURLConnection dan menambahkan tag, delegasi, dan NSMutabaleData. Saya memiliki kelas DataController yang menangani semua manajemen data, termasuk permintaan. Saya membuat protokol DataControllerDelegate, sehingga tampilan / objek individu dapat mendengarkan DataController untuk mengetahui kapan permintaan mereka selesai, dan jika perlu, berapa banyak yang telah diunduh atau kesalahan. Kelas DataController dapat menggunakan subkelas NSURLConnection untuk memulai permintaan baru, dan menyimpan delegasi yang ingin mendengarkan DataController untuk mengetahui kapan permintaan telah selesai. Ini adalah solusi kerja saya di XCode 4.5.2 dan ios 6.

File DataController.h yang mendeklarasikan protokol DataControllerDelegate). DataController juga berbentuk tunggal:

@interface DataController : NSObject

@property (strong, nonatomic)NSManagedObjectContext *context;
@property (strong, nonatomic)NSString *accessToken;

+(DataController *)sharedDataController;

-(void)generateAccessTokenWith:(NSString *)email password:(NSString *)password delegate:(id)delegate;

@end

@protocol DataControllerDelegate <NSObject>

-(void)dataFailedtoLoadWithMessage:(NSString *)message;
-(void)dataFinishedLoading;

@end

Metode kunci dalam file DataController.m:

-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    NSURLConnectionWithDelegate *customConnection = (NSURLConnectionWithDelegate *)connection;
    NSLog(@"DidReceiveResponse from %@", customConnection.tag);
    [[customConnection receivedData] setLength:0];
}

-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    NSURLConnectionWithDelegate *customConnection = (NSURLConnectionWithDelegate *)connection;
    NSLog(@"DidReceiveData from %@", customConnection.tag);
    [customConnection.receivedData appendData:data];

}

-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSURLConnectionWithDelegate *customConnection = (NSURLConnectionWithDelegate *)connection;
    NSLog(@"connectionDidFinishLoading from %@", customConnection.tag);
    NSLog(@"Data: %@", customConnection.receivedData);
    [customConnection.dataDelegate dataFinishedLoading];
}

-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSURLConnectionWithDelegate *customConnection = (NSURLConnectionWithDelegate *)connection;
    NSLog(@"DidFailWithError with %@", customConnection.tag);
    NSLog(@"Error: %@", [error localizedDescription]);
    [customConnection.dataDelegate dataFailedtoLoadWithMessage:[error localizedDescription]];
}

Dan untuk memulai permintaan: [[NSURLConnectionWithDelegate alloc] initWithRequest:request delegate:self startImmediately:YES tag:@"Login" dataDelegate:delegate];

NSURLConnectionWithDelegate.h: @protocol DataControllerDelegate;

@interface NSURLConnectionWithDelegate : NSURLConnection

@property (strong, nonatomic) NSString *tag;
@property id <DataControllerDelegate> dataDelegate;
@property (strong, nonatomic) NSMutableData *receivedData;

-(id)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL)startImmediately tag:(NSString *)tag dataDelegate:(id)dataDelegate;

@end

Dan NSURLConnectionWithDelegate.m:

#import "NSURLConnectionWithDelegate.h"

@implementation NSURLConnectionWithDelegate

-(id)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL)startImmediately tag:(NSString *)tag dataDelegate:(id)dataDelegate {
    self = [super initWithRequest:request delegate:delegate startImmediately:startImmediately];
    if (self) {
        self.tag = tag;
        self.dataDelegate = dataDelegate;
        self.receivedData = [[NSMutableData alloc] init];
    }
    return self;
}

@end
Chris Slade
sumber
0

Setiap NSURLConnection memiliki atribut hash, Anda dapat membedakan semuanya dengan atribut ini.

Misalnya saya perlu menjaga informasi tertentu sebelum dan sesudah koneksi, jadi RequestManager saya memiliki NSMutableDictionary untuk melakukan ini.

Sebuah contoh:

// Make Request
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLConnection *c = [[NSURLConnection alloc] initWithRequest:request delegate:self];

// Append Stuffs 
NSMutableDictionary *myStuff = [[NSMutableDictionary alloc] init];
[myStuff setObject:@"obj" forKey:@"key"];
NSNumber *connectionKey = [NSNumber numberWithInt:c.hash];

[connectionDatas setObject:myStuff forKey:connectionKey];

[c start];

Setelah permintaan:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"Received %d bytes of data",[responseData length]);

    NSNumber *connectionKey = [NSNumber numberWithInt:connection.hash];

    NSMutableDictionary *myStuff = [[connectionDatas objectForKey:connectionKey]mutableCopy];
    [connectionDatas removeObjectForKey:connectionKey];
}
eold
sumber