iOS Memory management issue with ARC

2019-09-08 07:28发布

问题:

I've created a class called "ConnectionManager" that will handle all network request and fetch data from the server and after that call to a completion handler.

ConnectionManager.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "UIAlertView+CustomAlertView.h"
#import <Crashlytics/Crashlytics.h>


@interface ConnectionManager : NSObject<NSURLSessionDataDelegate>

@property (weak, nonatomic) NSMutableData *receivedData;
@property (weak, nonatomic) NSURL *url;
@property (weak, nonatomic) NSURLRequest *uploadRequest;
@property (nonatomic, copy) void (^onCompletion)(NSData *data);
@property BOOL log;

-(void)downloadDataFromURL:(NSURL *)url withCompletionHandler:(void (^)(NSData *data))completionHandler;
-(void)uploadDataWithRequest:(NSURLRequest*)request withCompletionHandler:(void (^)(NSData *data))completionHandler;

@end

ConnectionManager.m

#import "ConnectionManager.h"


@implementation ConnectionManager

-(void)uploadDataWithRequest:(NSURLRequest*)request withCompletionHandler:(void (^)(NSData *data))completionHandler{
    // Instantiate a session configuration object.
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

    // Configure Session Configuration
    [configuration setAllowsCellularAccess:YES];

    // Instantiate a session object.
    NSURLSession *session=[NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];

    // Assign request for later call
    self.uploadRequest = request;

    // Create an upload task object to perform the data uploading.
    NSURLSessionUploadTask *task = [session uploadTaskWithRequest:self.uploadRequest fromData:nil];

    // Assign completion handler
    self.onCompletion = completionHandler;

    // Inititate data
    self.receivedData = [[NSMutableData alloc] init];

    // Resume the task.
    [task resume];

}

-(void)downloadDataFromURL:(NSURL *)url withCompletionHandler:(void (^)(NSData *data))completionHandler{
    // Instantiate a session configuration object.
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

    // Configure Session Configuration
    [configuration setAllowsCellularAccess:YES];

    // Instantiate a session object.
    NSURLSession *session=[NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];

    // Assign url for later call
    self.url = url;

    // Create a data task object to perform the data downloading.
    NSURLSessionDataTask *task = [session dataTaskWithURL:self.url];

    // Assign completion handler
    self.onCompletion = completionHandler;

    // Inititate data
    self.receivedData = [[NSMutableData alloc] init];

    // Resume the task.
    [task resume];
}

-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [self.receivedData appendData:data];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        if (error.code == -1003 || error.code == -1009) {
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:@"Unable to connect to the server. Please check your internet connection and try again!" delegate:nil cancelButtonTitle:@"cancel" otherButtonTitles:@"retry",nil];
                [alert showWithCompletion:^(UIAlertView *alertView, NSInteger buttonIndex) {
                    if (buttonIndex==1) {
                        // Retry
                        if (self.url) {
                            NSURLSessionDataTask *retryTask = [session dataTaskWithURL:self.url];
                            [retryTask resume];
                        }else{
                            NSURLSessionUploadTask *task = [session uploadTaskWithRequest:self.uploadRequest fromData:nil];
                            [task resume];
                        }
                    }else{
                        self.onCompletion(nil);
                    }
                }];
            }];
        }else{
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:@"An unkown error occured! Please try again later, thanks for your patience." delegate:nil cancelButtonTitle:@"cancel" otherButtonTitles:@"retry",nil];
                [alert show];
                CLS_LOG(@"Error details: %@",error);
                self.onCompletion(nil);

            }];
        }
    }
    else {
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            self.onCompletion(self.receivedData);
        }];
    }
}

@end

Here is the piece of code I use to call it :

-(void)loadDataFromServer{
    NSString *URLString = [NSString stringWithFormat:@"%@get_people_number?access_token=%@", global.baseURL, global.accessToken];
    URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url = [NSURL URLWithString:URLString];

    ConnectionManager *connectionManager = [[ConnectionManager alloc] init];
    [connectionManager downloadDataFromURL:url withCompletionHandler:^(NSData *data) {
        if (data != nil) {
            // Convert the returned data into an array.
            NSError *error;
            NSDictionary *number = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
            if (error != nil) {
                CLS_LOG(@"Error: %@, Response:%@",error,[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
            }
            else{
                [_mapView updateUIWithData:[number objectForKey:@"number"]];
            }
        }
    }];
}

I figured out when using Instruments that All objects of ConnectionManager type are persistent even after getting the data from server and calling the completion handler.

I tried to change the completion handler property from copy to strong, but I got the same results. Changing it to weak cause a crash and it never be called.

Please someone guide me to the right way.

回答1:

After doing a lot of research, I found that the URL Session object is still alive.

Based on Apple Documentation Life Cycle of URL Session there's two cases:

  1. Using the system provided delegate (by not setting a delegate), here the system will take care of invalidating the NSURLSession object.

  2. Using a custom delegate. When you no longer need a session, invalidate it by calling either invalidateAndCancel (to cancel outstanding tasks) or finishTasksAndInvalidate (to allow outstanding tasks to finish before invalidating the object).

To fix the code above I only changed the delegate method "didCompleteWithError" like the following:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
    if (error.code == -1003 || error.code == -1009) {
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:@"Unable to connect to the server. Please check your internet connection and try again!" delegate:nil cancelButtonTitle:@"cancel" otherButtonTitles:@"retry",nil];
            [alert showWithCompletion:^(UIAlertView *alertView, NSInteger buttonIndex) {
                if (buttonIndex==1) {
                    // Retry
                    if (self.url) {
                        NSURLSessionDataTask *retryTask = [session dataTaskWithURL:self.url];
                        [retryTask resume];
                    }else{
                        NSURLSessionUploadTask *task = [session uploadTaskWithRequest:self.uploadRequest fromData:nil];
                        [task resume];
                    }
                }else{
                    [session finishTasksAndInvalidate];
                    self.onCompletion(nil);
                }
            }];
        }];
    }else{
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:@"An unkown error occured! Please try again later, thanks for your patience." delegate:nil cancelButtonTitle:@"cancel" otherButtonTitles:@"retry",nil];
            [alert show];
            [session finishTasksAndInvalidate];
            self.onCompletion(nil);
        }];
    }
}
else {
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [session finishTasksAndInvalidate];
        self.onCompletion(self.receivedData);
    }];
}

}