[iOS]ARC下循环引用的问题

2013-8-30

最初

最近在开发应用时碰到使用ASIHttpRequest后在某些机器上发不出请求的问题,项目开启了ARC,代码是这样写的:

@implement MainController
- (void) fetchUrl{
    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];
    [request setCompletionBlock:^{
        NSLog(@"completed");
    }];
    [request startAsynchronous];
}
@end

后来发现原因是request这个变量在退出这个函数后就被释放了,自然发不出请求。因为用了ARC,没法手动调用[request retain]让这个变量不被释放,所以只能把这个变量变成实例变量,让Controller实例存在的过程中一直持有这个变量不释放。

@interface MainController {
     ASIHTTPRequest *request;
}
@end

@implement MainController
- (void) fetchUrl{
    request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];
    [request setCompletionBlock:^{
        [self complete];
    }];
    [request setFailedBlock:^{
          NSLog(@"failed");
    }];
    [request startAsynchronous];
}
@end

问题一

这下发送请求没问题了,但出了另一个问题,XCode编译后提示[self complete]这一行可能会导致循环引用。因为MainController实例持有request, request持有completionBlock,completionBlock又持有MainController,导致循环引用,MainController实例在外界引用计数为0时仍无法被释放,因为自身的变量request里持有MainController实例的引用,其引用计数永远大于1。

导致这样循环引用的原因是在completionBlock里调用的self是一个strong类的引用,会使self引用计数+1,可以保证在调用过程self不会被释放,但在这里不需要这样的保证,可以声明另一个__weak变量指向self,这样在block使用这个变量就不会导致self引用计数+1,不会导致循环引用。

@implement MainController
- (void) fetchUrl{
     request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];
     __weak id this = self;

    [request setCompletionBlock:^{
        [this complete];
    }];
    [request startAsynchronous];
}
@end

这样循环引用问题就解决了,不过__weak只支持iOS5.0以上,5以下的要用__unsafe_unretain代替__weak,区别是对象被释放后__weak声明的变量会指向nil,安全点,__unsafe_unretain不会,变成野指针容易导致应用crash。

问题二

如果在block只是调用下MainController的方法,上面的解决方法就够了,但我的需求是在block里要调用到很多实例变量,包括赋值:

@interface MainController {
     ASIHTTPRequest *request;
     BOOL isLoading;
     UIView *loadingView;
}
@end

@implement MainController
- (void) fetchUrl{
    request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];

    [request setCompletionBlock:^{
        isLoading = NO;
        loadingView.hidden = NO;
    }];
    [request startAsynchronous];
}
@end

XCode提示说isLoading = NO和loadingView.hidden = NO两行都可能导致循环引用,这下难办了,对于loadingView,是可以跟self一样再声明一个__weak引用给block用,但像isLoading这样需要赋值的没法这样做,而且使用的实例变量多的情况下每个都另外声明__weak变量也是很烦。想半天想到三个办法:

1

实例变量全部加上get set方法,通过声明的__weak变量访问,缺点是破坏了封装性,把原本私有的实例变量变成公有。

@interface MainController {
     ASIHTTPRequest *request;
}
@property (nonatomic, strong) UIView *loadingView;
@property (nonatomic, assign) BOOL isLoading;
@end

@implement MainController
@synthesize loadingView, isLoading;

- (void) fetchUrl{
     request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];
     __weak id this = self;

    [request setCompletionBlock:^{
        this.isLoading = NO;
        this.loadingView.hidden = NO;
    }];
    [request startAsynchronous];
}
@end

2

在类里声明一个方法专门处理,缺点是麻烦,每一个回调都要另外声明一个实例方法,代码变丑。

@interface MainController {
     ASIHTTPRequest *request;
     BOOL isLoading;
     UIView *loadingView;
}
@end

@implement MainController
- (void) complete:(ASIHttpRequest *)request
{
        isLoading = NO;
        loadingView.hidden = NO;
}
- (void) fetchUrl{
     request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];
     __weak id this = self;
     __weak ASIHttpRequest *_request = request;

    [request setCompletionBlock:^{
        [this complete:request];
    }];
    [request startAsynchronous];
}
@end

3

在block结束手动释放request。在循环引用里出现的问题是MainController外部引用计数为0时它仍不能释放,但如果我们通过手动设置request=nil,导致request变量指向的对象引用计数为0被释放,它对MainController的引用也就释放了,MainController在外部引用计数为0时就可以正常释放了,解决了循环引用的问题。这个做法的缺点是XCode的警告提示还存在着。

@interface MainController {
     ASIHTTPRequest *request;
     BOOL isLoading;
     UIView *loadingView;
}
@end

@implement MainController
- (void) fetchUrl{
    request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];
    [request setCompletionBlock:^{
        isLoading = NO;
        loadingView.hidden = NO;
        request = nil;
    }];
    [request startAsynchronous];
}
@end

不知还有没有更好的方法?

分类:技术文章 Tags:
评论

*

*

2013年8月31日 10:01

– (void) fetchUrl{
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];
[request setCompletionBlock:^{
NSLog(@”completed”);
}];
[request startAsynchronous];
}

startAsynchronous 会retain request吧

2013年8月31日 10:04

没细究里面的实现,实际情况是不会,在有些机子上被释放了没法请求

2013年10月29日 15:03

* 通过引用的方式访问对象的,self被retain;
* dispatch_async(queue, ^{
// instanceVariable is used by reference, self is retained
doSomethingWithObject(instanceVariable);
});

* 通过值访问对象的,被访问的对象被retain;
* id localVariable = instanceVariable;
dispatch_async(queue, ^{
// localVariable is used by value, localVariable is retained (not self)
doSomethingWithObject(localVariable);
});
网上有种这样说法 那第二种可不可以直接拿到block内部使用?

2014年5月23日 15:27

– (void) fetchUrl{
__weak ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];
[request setCompletionBlock:^{
NSLog(@”completed”);
}];

[request startAsynchronous];
}
我这样写真机测试能请求成功,但是发布后下载下来就请求不了,把__weak改称__bloak就可以,但是有retain的警告

2014年10月18日 11:05

著名开源代码AFNetworking 的解决方案:

__weak __typeof(&*self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(&*weakSelf)strongSelf = weakSelf;
if (!strongSelf) {
return;
}

strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};