2010-10-23 1 views
21

Sto utilizzando il metodo di download asincrono di Erica Sadun (link qui per il file di progetto: download), tuttavia il suo metodo non funziona con file di grandi dimensioni (50 MB o superiore). Se provo a scaricare un file sopra i 50 mb, di solito si blocca a causa di un crash di memoria. Posso comunque modificare questo codice in modo che funzioni anche con file di grandi dimensioni? Ecco il codice che ho nei Rami DownloadHelper (che è già nel link download):Download di un file di grandi dimensioni - iPhone SDK

.h

@protocol DownloadHelperDelegate <NSObject> 
@optional 
- (void) didReceiveData: (NSData *) theData; 
- (void) didReceiveFilename: (NSString *) aName; 
- (void) dataDownloadFailed: (NSString *) reason; 
- (void) dataDownloadAtPercent: (NSNumber *) aPercent; 
@end 

@interface DownloadHelper : NSObject 
{ 
    NSURLResponse *response; 
    NSMutableData *data; 
    NSString *urlString; 
    NSURLConnection *urlconnection; 
    id <DownloadHelperDelegate> delegate; 
    BOOL isDownloading; 
} 
@property (retain) NSURLResponse *response; 
@property (retain) NSURLConnection *urlconnection; 
@property (retain) NSMutableData *data; 
@property (retain) NSString *urlString; 
@property (retain) id delegate; 
@property (assign) BOOL isDownloading; 

+ (DownloadHelper *) sharedInstance; 
+ (void) download:(NSString *) aURLString; 
+ (void) cancel; 
@end 

.m

#define DELEGATE_CALLBACK(X, Y) if (sharedInstance.delegate && [sharedInstance.delegate respondsToSelector:@selector(X)]) [sharedInstance.delegate performSelector:@selector(X) withObject:Y]; 
#define NUMBER(X) [NSNumber numberWithFloat:X] 

static DownloadHelper *sharedInstance = nil; 

@implementation DownloadHelper 
@synthesize response; 
@synthesize data; 
@synthesize delegate; 
@synthesize urlString; 
@synthesize urlconnection; 
@synthesize isDownloading; 

- (void) start 
{ 
    self.isDownloading = NO; 

    NSURL *url = [NSURL URLWithString:self.urlString]; 
    if (!url) 
    { 
     NSString *reason = [NSString stringWithFormat:@"Could not create URL from string %@", self.urlString]; 
     DELEGATE_CALLBACK(dataDownloadFailed:, reason); 
     return; 
    } 

    NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:url]; 
    if (!theRequest) 
    { 
     NSString *reason = [NSString stringWithFormat:@"Could not create URL request from string %@", self.urlString]; 
     DELEGATE_CALLBACK(dataDownloadFailed:, reason); 
     return; 
    } 

    self.urlconnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self]; 
    if (!self.urlconnection) 
    { 
     NSString *reason = [NSString stringWithFormat:@"URL connection failed for string %@", self.urlString]; 
     DELEGATE_CALLBACK(dataDownloadFailed:, reason); 
     return; 
    } 

    self.isDownloading = YES; 

    // Create the new data object 
    self.data = [NSMutableData data]; 
    self.response = nil; 

    [self.urlconnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 
} 

- (void) cleanup 
{ 
    self.data = nil; 
    self.response = nil; 
    self.urlconnection = nil; 
    self.urlString = nil; 
    self.isDownloading = NO; 
} 

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)aResponse 
{ 
    // store the response information 
    self.response = aResponse; 

    // Check for bad connection 
    if ([aResponse expectedContentLength] < 0) 
    { 
     NSString *reason = [NSString stringWithFormat:@"Invalid URL [%@]", self.urlString]; 
     DELEGATE_CALLBACK(dataDownloadFailed:, reason); 
     [connection cancel]; 
     [self cleanup]; 
     return; 
    } 

    if ([aResponse suggestedFilename]) 
     DELEGATE_CALLBACK(didReceiveFilename:, [aResponse suggestedFilename]); 
} 

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)theData 
{ 
    // append the new data and update the delegate 
    [self.data appendData:theData]; 
    if (self.response) 
    { 
     float expectedLength = [self.response expectedContentLength]; 
     float currentLength = self.data.length; 
     float percent = currentLength/expectedLength; 
     DELEGATE_CALLBACK(dataDownloadAtPercent:, NUMBER(percent)); 
    } 
} 

- (void)connectionDidFinishLoading:(NSURLConnection *)connection 
{ 
    // finished downloading the data, cleaning up 
    self.response = nil; 

    // Delegate is responsible for releasing data 
    if (self.delegate) 
    { 
     NSData *theData = [self.data retain]; 
     DELEGATE_CALLBACK(didReceiveData:, theData); 
    } 
    [self.urlconnection unscheduleFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; 
    [self cleanup]; 
} 

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 
{ 
    self.isDownloading = NO; 
    NSLog(@"Error: Failed connection, %@", [error localizedDescription]); 
    DELEGATE_CALLBACK(dataDownloadFailed:, @"Failed Connection"); 
    [self cleanup]; 
} 

+ (DownloadHelper *) sharedInstance 
{ 
    if(!sharedInstance) sharedInstance = [[self alloc] init]; 
    return sharedInstance; 
} 

+ (void) download:(NSString *) aURLString 
{ 
    if (sharedInstance.isDownloading) 
    { 
     NSLog(@"Error: Cannot start new download until current download finishes"); 
     DELEGATE_CALLBACK(dataDownloadFailed:, @""); 
     return; 
    } 

    sharedInstance.urlString = aURLString; 
    [sharedInstance start]; 
} 

+ (void) cancel 
{ 
    if (sharedInstance.isDownloading) [sharedInstance.urlconnection cancel]; 
} 
@end 

E infine è così che scrivo la file con le due classi sopra:

- (void) didReceiveData: (NSData *) theData 
{ 
    if (![theData writeToFile:self.savePath atomically:YES]) 
     [self doLog:@"Error writing data to file"]; 

    [theData release]; 

} 

Se qualcuno potesse darmi una mano sarei così felice!

Grazie,

Kevin

+2

Ho scritto una libreria per questo, usando il metodo che hai descritto. Lo sto mettendo qui sperando che possa essere utile ad alcune persone, o ispirare loro a scrivere la propria soluzione. Se stai bene, ovviamente. https://github.com/thibaultCha/TCBlobDownload – thibaultcha

risposta

29

Sostituire l'in-memory NSData *data con una NSOutputStream *stream. Nel -start creare il flusso di aggiungere e aprirlo:

stream = [[NSOutputStream alloc] initToFileAtPath:path append:YES]; 
[stream open]; 

Poiché i dati in arrivo, scriverlo al flusso:

NSUInteger left = [theData length]; 
NSUInteger nwr = 0; 
do { 
    nwr = [stream write:[theData bytes] maxLength:left]; 
    if (-1 == nwr) break; 
    left -= nwr; 
} while (left > 0); 
if (left) { 
    NSLog(@"stream error: %@", [stream streamError]); 
} 

Quando hai finito, chiudere il flusso:

[stream close]; 

Un approccio migliore sarebbe aggiungere il flusso oltre ai dati ivar, impostare l'helper come delegato del flusso, bufferizzare i dati in entrata nel data ivar, quindi scaricare i contenuti di ivar dei dati nell'helper wh sempre il flusso invia l'aiuto al suo evento disponibile nello spazio e lo cancella dai dati ivar.

+0

Grazie per la risposta, ma è ancora possibile ottenere informazioni sul download? Come ottenere quanti dati sono stati scaricati? Di solito uso solo: self.data.length ma dal momento che in questo nuovo metodo NSMutableData non c'è, non so come implementarlo. Inoltre (dato che sono un po 'nuovo rispetto all'obiettivo-c), mi sbarazzerò completamente di NSMutableData nel file .h e tutte le sue istanze nelle classi helper? – lab12

+0

Ehi, ho ancora problemi con questo metodo di download. Nel debugger mi dà questo errore: "Errore di flusso: Errore Dominio = NSPOSIXErrorDomain Code = 1" Impossibile completare l'operazione. Operazione non consentita "UserInfo = 0x148660 {} " Non so perché questo appare. Ho il percorso impostato in modo errato? Si suppone di essere una directory o un file? Sarebbe FANTASTICO se potessi fornire un codice di esempio !! – lab12

+0

Pubblica il tuo codice (ad es. In [gist.github.com] (http://gist.github.com/), e posso guardarlo. Il flusso di output dovrebbe essere in un file in una directory in cui si ha accesso in scrittura, come la directory Documenti della tua app. Sembra che il problema sia che stai cercando di scrivere da qualche parte che il sistema non permetterà. –

3

Ho una leggera modifica al codice precedente.

Utilizzare questa funzione, funziona correttamente per me.

+0

Questo scriverà tutti i dati su un flusso quando è finito il download. Il problema che l'OP aveva stava usando tutta la memoria disponibile con download molto grandi, la tua risposta non risolve questo problema. –

0

Prova AFNetworking. E:

NSString *[email protected]"http://yourFileURL.zip"; 
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:yourFileURL]]; 
AFURLConnectionOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; 

NSString *cacheDir = [NSSearchPathForDirectoriesInDomains 
          (NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]; 
NSString *filePath = [cacheDir stringByAppendingPathComponent: 
         @"youFile.zip"]; 

operation.outputStream = [NSOutputStream outputStreamToFileAtPath:filePath append:NO]; 

[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) { 
    //show here your downloading progress if needed 
}]; 

[operation setCompletionBlock:^{ 
    NSLog(@"File successfully downloaded"); 
}]; 

[operation start];