一直不怎么习惯使用一些门户博客,平日里开发时遇到的问题就随手记到印象笔记里.
但是在今天下午真的是遇到了一个令人悲伤的BUG.
我在一个控制器里:
注册了几个通知的监听方法,其中一个通知只在这个控制器(B)里注册了监听,然后在下一个控制器(C)发送通知, 通知的post方只有一个,通知的observer方也只有一个,并没有在其他类里出现这个通知.
由于一些设计上的需求,我需要:
1.在更换支付类型页面(C)发送通知,然后回到B页刷新数据接口,
2.并且当在B页执行 返回/修改商品 操作时,由B页pop回购物车控制器(A).
那么问题来了:
1.当我pop掉B回到A的时候,看似B是从栈里移除掉了,然而当广播发来的时候,B依旧响应了监听方法,这就有点尴尬了,
2.当我在B里进行了N次频繁的修改商品pop回A的操作时,再从购物车控制器(A)push到B里,最后进入C里面更换支付类型发送通知给B,到了B页面接收通知刷新数据,发现B页面刷新数据的监听方法被频繁调用了N次!并且因为商品在被不停修改,只有一次的刷新数据请求返回值是正确的,其他的全是错误的..这TM就很尴尬了...
其实这两个问题是一个问题,只不过第问题2更为复杂一些.原因是当B执行pop操作时,根本没有走到dealloc 方法,从而没有去具体执行B的销毁工作.
ARC下控制器在被pop后移出栈后会被释放,但有些时候会发现控制器出栈的时候不会调用dealloc方法,归根结底,是因为当前控制器被某个对象强引用了,控制器的引用计数不为0,系统无法帮你释放这部分内存。原因大致有以下几点:
1.控制器中NSTimer没有被销毁
当viewController中存在NSTimer时,需要特别注意,当调用
[NSTimerscheduledTimerWithTimeInterval:1.0target:selfselector:@selector(updateTime:) userInfo:nilrepeats:YES];
时,因为 target:self ,也就是引用了当前viewController,导致控制器的引用计数加1,如果没有将这个NSTimer 销毁,它将一直保留该viewController,无法释放,也就不会调用dealloc方法。所以,需要在viewWillDisappear之前需要把控制器用到的NSTimer销毁。
[timer invalidate];//销毁
timertimer =nil;//置nil
2.viewController中的代理不是weak属性
例如@property (nonatomic, weak) id delegate;代理要使用弱引用,因为自定义控件是加载在视图控制器中的,视图控制器view对自定义控件是强引用,如果代理属性设置为strong,则意味着delegate对视图控制器也进行了强引用,会造成循环引用。导致控制器无法被释放,最终导致内存泄漏。
3.viewController中block的循环引用
在ARC下,block会把它里面的所有对象强引用,包括当前控制器self,因此有可能会出现循环引用的问题。比如viewController中有个block属性,在block中又强引用了self或者其他成员变量,那么这个viewController与自己的block属性就形成循环引用,导致viewController无法释放。
1和2 的情况很好检查,你自己的控制器里有没有用 **NSTimer和代理 **你自己很清楚,检查后没有就只有第三种情况了.
第三种情况需要我们仔细跟踪自己的代码来检查是否是出现了循环引用的问题.但是当我们跟踪完之后发现block块代码里没有循环引用的问题时,你会觉得确实TM尴尬...怎么办呢 ? 这里答题说一下我的解决办法.
其实问题是出在当我们从B pop回去的时候,由于强引用问题,B一直没有被销毁,而存在于内存中,我们频繁不停的修改商品后会出现N次B--A--B之间的pop<-->push,会导致内存里产生了N个控制器B的实例.所有的B由于有没有查询到的强引用而一直没有被销毁,所以才会在C发送通知时,已经有了N个B实例了,每一个实例里面的observer观察者都监听到了post,于是触发了N个实例里面的N遍监听响应方法.
但是在导航控制器的栈里面,只要pop出去了,B就会在这个栈里面被移除掉,也就是说在这个场景内,navgation导航条的栈内最多只有一个B的实例存在,并且这个实例(当前的B)就是我们需要的,会显示在屏幕上的那个. 剩下的,我们只需要拿到这个实例,只给这一个实例去做请求去更新数据就可以了.我们只需要在执行observer的监听方法时,只执行这个当前的B的数据更新请求就可以了.下面是具体的做法:
-(void)viewDidLoad{
[super viewDidLoad];
self.title = @"确认下单";
//UI
[self.view addSubview:self.tableView];
[self.view addSubview:self.bottomView];
//Data
[self loadData];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDataSource:) name:@"changePayMethod" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateAgreementIdIfTurnEasy:) name:@"addAgreementIdIfTurnEasy" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateAddressModel:) name:@"comfimOrderUpdateAddressModel" object:nil];
}
-(void)updateDataSource:(NSNotification *)notif{
WEAKSELF
[self.navigationController.childViewControllers enumerateObjectsUsingBlock:^(__kindof UIViewController * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj isKindOfClass:[XXConfirmOrderController class]]) { //当前屏幕上的确认下单页
if (obj == weakSelf) {
//1.刷新支付方式
NSString *payMethod = notif.object;
[weakSelf.confimModel setPayMethod:payMethod];
//2.根据支付方式刷新价格
[weakSelf updateAllPriceByPayType];
}
}
}];
}
-(void)updateAllPriceByPayType{
NSMutableDictionary *dyParam = [NSMutableDictionary dictionary];
NSString *payType = [NSString stringWithFormat:@"%zd",self.confimModel.orderType];
[dyParam setValue:payType forKey:@"payType"];
[dyParam setObject:@"cartGoods" forKey:@"from"]; //来自于购物车的标识符
__block NSMutableArray *cartGoodsMapList = [NSMutableArray array];
[self.goodsModelArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
XXCartGoodMdel *model = obj;
NSString *goodsId = model.goodId;
NSString *goodsName = model.goodsName;
NSNumber * goodsNumber = [NSNumber numberWithInt:model.goodsNumber]; //要number格式
NSString *isChecked = model.isChecked;
NSString *discountId = model.discountId ? model.discountId : @"";
NSString *serviceIds = model.serviceIds ? model.serviceIds : @"";
NSString *isChangeGoodsFlag = model.isChangeGoodsFlag;
NSMutableDictionary *goodDic = [NSMutableDictionary dictionaryWithDictionary:@{@"userId":[AccountTool account].userId ,@"goodsId":goodsId, @"goodsNumber":goodsNumber, @"isChangeGoodsFlag":isChangeGoodsFlag,@"goodsName":goodsName,@"isChecked":isChecked, @"discountId":discountId, @"serviceIds":serviceIds }];
[cartGoodsMapList addObject:goodDic];
}];
[dyParam setValue:cartGoodsMapList forKey:@"cartGoodsMapList"];
[dyParam setObject:[NSString stringWithFormat:@"%@",self.cartModel.original] forKey:@"original"];
[dyParam setObject:[NSString stringWithFormat:@"%@",self.cartModel.cut] forKey:@"cut"];
[dyParam setObject:[NSString stringWithFormat:@"%@",self.cartModel.total] forKey:@"total"];
WEAKHUD
WEAKSELF
[[APIClientFactory sharedManager] requestPriceOfCartByPayTypeWithDynamicParams:dyParam success:^(Response *response) {
[weakHud hideAnimated:YES];
NSDictionary *dic = (NSDictionary *)response.data;
NSNumber *total = [dic objectForKey:@"total"];
if (response.code == 2) {
[JXTAlertTools showTipAlertViewWith:weakSelf title:@"提示" message:response.message buttonTitle:@"知道了" buttonStyle:JXTAlertActionStyleDefault];
}
[weakSelf.confimModel setAllPrice:total.floatValue];
[weakSelf.tableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:2 inSection:0],[NSIndexPath indexPathForRow:3 inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
[weakSelf.bottomView setModel:weakSelf.confimModel];
} failure:^(NSError *error) {
[weakHud hideAnimated:YES];
[HUD showError:error.userInfo[@"message"] toView:weakSelf.view];
}];
}
做完这个处理之后,就能很好的规避到重复进行N次数据刷新请求了,只会保留到我们所看到的屏幕上的当前B页的通知响应.当然这种处理暂时没有办法规避内存中被强引用的其它实例的销毁问题,想处理的话还需要我们进一步细心去跟踪.