Objective-c 之 回调模型问题
release 一直都是IOS开发人员最为头疼的事情。 一旦过度释放,App立即"死"给你看。 而console报的信息总是那么让人难以跟进。
这里谈谈自己在实际写代码时遇到的一个问题。
需求是这样的: 系统需要实现与服务端的Http交互。为了在UI体验上更友好一点,一般都是选择使用异步请求。
(或者用新线程发起同步请求)
下面是我之前的一个实现。
Controller 调用Model发送异步请求,提供回调方法,向用户反馈结果。
Model调用 Http 类,发起异步请求, 提供回调方法,处理返回数据。
处理完毕后,再调用controller回调方法,提供最终结果数据。
(因服务端返回的数据可能并不适合直接展示给用户。例如,400错误,5xx错误,或者一些需要另加修饰的其他系统错误,数据结构需要转换等。。。因此在model层做了预处理之后才交回给controller)
大致的类实现如下:
@interface MainPageController : UIViewController{
UserModel* model;
}
@implementation MainPageController{
-(void) getUserInfo{
[model doQueryUser:@”123” delegate:self callbackMethod:@”getUserInfoDone:”];
}
-(void)dealloc{
[model release]; //3)
[super dealloc];
}
}
@interface MainPageController : UIViewController{
NSMutableDictionary* requestInfo; //用以记录controller回调信息
}
@implementation UserModel
-(void) doQueryUser:(NSString*) uId delegate:(id)delegate callbackMethod:(NSString*)callbackMethod{
NSString* reqURL = @"……….";
NSDictionary* params = …;
[HttpWrapper* wrapper = [HttpWrapper sendAsyncReq: reqURL reqMethod:@"POST" params:params delegate:self callbackMethod:@"doQueryUserDone:result"];
[self saveCallbackInfo: wrapper target: delegate selector: callbackMethod];
}
-(void) doQueryUserDone:(HttpWrapper*)wrapper result:(HTTPResult*)result{
id delegate;
SEL callbackMethod;
[self getCallbackInfo: wrapper target: &delegate selector:&callbackMethod];
//2)
//model handle result
// …..
//回调controller的 方法
[delegate performSelector: callbackMethod withObject:result];
}
-(void) saveCallbackInfo:(id)request target:(id)target selector:(NSString*) selector{
NSDictionary* value = [NSDictionary dictionaryWithObjectsAndKeys:target, @"target", selector, @"callback",nil];
[requestInfo setValue:value forKey:[NSString stringWithFormat:@"%x", request]];
}
-(void) getCallbackInfoAndRemove:(id)request target:(id*)target selector:(SEL*)selector{
NSDictionary* value = [requestInfo objectForKey:[NSString stringWithFormat:@"%x", request]];
*target = delegate;
NSString* action = [value objectForKey:@"callback"];
*selector = NSSelectorFromString(action);
[requestInfo removeObjectForKey:[NSString stringWithFormat:@"%x", request]]; //1)
}
问题: 上述类结构表面上看,似乎没什么问题,经初步测试也一切正常。心里正得意,交给测试人员。
测试人员一般都是一些“不正常”的人. 拿到App后随便点点几下,App竟然闪退了。
闪退。。。这是个头疼的问题。
具体操作是这样的, 进入一个界面.(程序推入push一个新的controller),controller自动加载数据,界面显示 activityIndicator。
在controller 未拿到返回数据之前,按返回,回到上一级界面(pop controller)。等一会刚才那个controller的model返回数据时,App就挂掉了。
原因: 经模拟器调试,每次重复同样操作,断点都是停在2) 处理。看变量内容,似乎是已经released掉了,但为何会released掉?
如果是因为controller返回后,controller的dealloc 3) 处 释放了model, 但为何model的回调方法还能被执行?难道是执行到一半的时候恰好被释放掉?这个。。。接下来继续重复测试,还是同样的结果。结论,不可能是恰巧。没理由那么多次都恰巧。(但是不是存在这样的可能,这个,本人偷懒,到现在依然没去证实。) 那为何会被释放? 重新检查了所有代码,都没有任何地方有调用[model release],看来也不是错误过度释放的问题。
经冥思苦想,终于想到个好方法,打印log,具体点说就是打印[model retainCount]. 在所有关键行都插入打印语句。
这个。。。后来证实依旧不可靠。具体大家可以去搜搜有关retainCount的一些文章,在此不再重复。反正最后结论就是,千万不要指望retainCount能准确告诉你,你的对象什么时候被引用了,什么时候被release了,什么时候又retainCount=0了,要被释放了,起码在你需要把它加入到系统的一些类中去的时候。
正在苦逼的时候,反复的测试倒也给了些提示:每次都是在执行完1)之后,就会出错。
经慢慢冷静,整理清整个调用顺序后,终于明白了问题。
首先,进入controller之后,调用model发送了异步请求.在请求未返回之后退出。
返回后,controller被pop出。retainCount并不等於0,因在model中用于记录回调信息 的 requestInfo 对象中有引用。
问题就在于此。当http 请求返回后,model 回调方法doQueryUserDone 需要从 requestInfo 中取controller 的回调信息,取到后清除掉(见1)处代码)。
当1)执行完后,requestInfo释放掉controller引用。 这时controller retainCount ==0 ,系统开始执行controller的dealloc方法。其中会调用model release。 这样,在代码2)处,就会遇到还在执行中,但类对象已释放的情况。结果只有一个,就是闪退。
这个最后说白了,就是犯了个controller 和model 相互引用的问题。 表面看似正常的逻辑,背后是不正常的引用关系。
因异步请求 和 模型中预处理数据 都是一些比较常见的需求。特写此文,以提醒大家不要犯作者这种错误。