从一个crash理解weak的延迟释放

栏目: IOS · 发布时间: 5年前

内容简介:网络库Operation的再简单看一下外部是怎么操作的引起crash:至此我们找到了crash的原因, 修改的方法有很多, 上面我们有说到Operation对Manager其实是弱引用的, 只是为了扩展性?(大概)才通过NSInvocation调用本该是自己的delagte回调, 其实如果牺牲一点扩展性在现在代码的使用

最近在项目内部碰到一个野指针crash, 说起来这个crash也很简单, 就是一个在声明一个对象的时候用的是assign, 然后对象释放了还继续调用改对象的方法就crash了. 先来看一个最简单的野指针示意图

从一个crash理解weak的延迟释放

A对C对像是强持有, B对C对象的申明是assign, 当A释放了C以后, B在调用C的xxx方法就会野指针crash. 对于开发者来说, 很少会把对象申明成assign, 所以这种写法引发的crash是比较好排查的, 下面看另一种示意图.

从一个crash理解weak的延迟释放

这里B对D是弱引用, C对D是assign引用, 笔者这里示意图里想表明的是C其实是通过B的某些调用才触发对assign对象D的方法调用, 同样的原因唯一对D强引用的A如果在这个过程中释放了D就会造成crash, 说到这里可能会问, 什么场景需要这么费劲的调用, 看上去B也可以直接对D进行调用而且weak不会造成野指针不是吗. 还原一下当时的crash场景.

从一个crash理解weak的延迟释放

简单说就是A对象通过一个manager(或者viewModel之类的),发出一个网络请求, 图2中的B在这个场景中其实是一个网络库, manager是网络请求的delegate, 负责网络请求之后的回调, 这个网络库在设计的时候为了扩展性考虑在触发回调的时候使用了一个NSInvocation. 关键代码:

@interface SSHttpOperation()
...
//网络回调的target
@property(nonatomic, weak)id target;
//外部在发网络请求的时候可以随意设置回调方法, 达到扩展性
@property(nonatomic, assign)SEL didFinishSelector;
...
@end

@implementation SSHttpOperation
- (void)notifyWithResult:(NSDictionary*)result error:(NSError*)error continueWhenCancel:(BOOL)continueWhenCancel {
	//网络请求回来, 进行外部业务回调
    ...
       if(target && [target respondsToSelector:didFinishSelector])  {
           NSMethodSignature *signature = [target methodSignatureForSelector:didFinishSelector];
           NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
           [invocation setTarget:target]; // 1
           [invocation setSelector:didFinishSelector];
           id wself = self;
           [invocation setArgument:&wself atIndex:2];
           [invocation setArgument:&result atIndex:3];
           [invocation setArgument:&logicError atIndex:4];
           [invocation setArgument:&userInfo atIndex:5];
           [invocation invoke];

       }
      ...
}
@end

网络库Operation的 关键 代码其实就是这个, 代码 1 的位置 [invocation setTarget:target] , 点开 NSInvocation.h 的头文件可以看到 NSInvocationtarget 的申明是assign…

再简单看一下外部是怎么操作的引起crash:

Object A

[self.pgcActionManager changePGCAccount:mediaID likeStatus:!hasSubscribed extraTrack:extraTrack likeBlock:^(BOOL isLiked, NSError *error, NSArray<TTVUserAccount *> *recommendUsers) {
       // 一堆业务操作 再block的最后把这个manager置为nil
       ...
       self.pgcActionManager = nil; // 2
   }];
Manager

 
- (void)changePGCAccount:(NSString *)mediaID likeStatus:(BOOL)changeToLike likeBlock:(SSPGCActionManagerLikeBlock)likeBlock{
	...
	self.likeOperation = [SSHttpOperation httpOperationWithURLString:url getParameter:nil postParameter:parameterDict userInfo:userInfo];
    [_likeOperation setQueuePriority:NSOperationQueuePriorityHigh];
    [_likeOperation setFinishTarget:self selector:@selector(operation:finishedResult:error:userInfo:)];
    [SSOperationManager addOperation:_likeOperation];
}

#pragma mark - 网络回调
- (void)operation:(SSHttpOperation*)operation finishedResult:(NSDictionary*)result error:(NSError*)error userInfo:(id)userInfo{
 // 业务逻辑代码 xxx
 //3
 if (_likeBlock) {
	  _likeBlock(isFollow, followError, recommendUsersArray);
   }
 // 后续的业务逻辑代码
 // 4
 [TTVLoginManager showLoginForFollowWithResult:@"你的关注太多,登录保存一下吧" extraTrackDic:@{@"isOver":@(1)} position:self.position];
}

上面摘抄了一些关键代码, 调用顺序大概是:

Object A业务逻辑触发了[Manager changePGCAccount…]方法

-> Manager通过网络库发请求并通过NSInvocation触发回调 (代码1)
-> 回调方法执行likeBlock (代码3)

-> block内Object A释放Manager (代码2) , Manager dealloc方法也在此时执行

-> likeBlock执行完后由于业务需求调用了self.position (代码4) , 崩擦擦.

weak

至此我们找到了crash的原因, 修改的方法有很多, 上面我们有说到Operation对Manager其实是弱引用的, 只是为了扩展性?(大概)才通过NSInvocation调用本该是自己的delagte回调, 其实如果牺牲一点扩展性在现在代码的使用 NSInvocation 的位置 (代码1) 替换成 [self.target xxxx] 就不会出问题了, 事实上现在我们大部分的代理在使用过程中也是这个思路.

事实证明,直接调确实是没有问题的, 并且有一个有意思的现象, 通过这种调用在 (代码2)self.pgcActionManager = nil; 的时候, 这个Manager没有立即走dealloc, 也就是并没有立即释放, 换句话说, 还有别人在持有这个Manager? 一个常识是weak修饰的对象会在对象释放之后置为nil, 但是这和被修饰对象不立即释放并没有什么关系.

于是想到了《Objective-C高级编程 iOS与OS X多线程和内存管理》一书里提到过的,weak会将被修饰对象加入autoreleasePool达到延迟释放的效果, 笔者在读这一段的时候做过诸多实验, 最后的结论是对这个说法表示怀疑, 但是现在看确实是有延迟释放的效果. 直接看答案吧

{
  id __weak obj1 = obj;
  NSLog(@"%@", obj1);
}
/* iOS5及之前编译器做法 */
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);//错误!!!mistake
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);

/* 现在的编译器做法*/
id obj = objc_msgSend(NSObject, "new");
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(obj1);//objc_loadWeakRetained would increment the reference count to ensure that tmp is alive in the NSLog statement.
NSLog(@"%@", obj1);
objc_release(tmp);
objc_destroyWeak(&obj1);
objc_storeStrong(&obj, 0);//release

现在的编译器在我们使用weak对象的时候会帮我们插入 objc_loadWeakRetained 导致weak对象引用计数+1, 达到在使用weak对象的时候改对象永远不可能被释放, 在我们的case中就是 [self.target xxxx] 这个 xxx 方法中即使将有代码将 target 释放了, 表面上看引用计数好像为0了, 应该释放, 事实上只有在这个方法走完了引用计数才有可能清0. 以前知道这个实现, 但是没注意这个特性, 现在看只想说奈斯 兄dei.

最后

最后总结一下这次改bug的收获:

  1. NSInvocation是有可能引起野指针的, 使用的时候要多加小心, 尤其是在基础库中的使用.当然就正常使用规范来说文中的那个likeBlock应该放在方法末尾执行(这样也不会crash), 只是业务需要在block执行后继续执行一些操作.
  2. weak会通过objc_loadWeakRetained函数被修饰对象引用计数+1, 保证对象在正在使用的过程中不被释放, 这也是和assign非常不一样一个点.
  3. 一些代码设计上的收获, 不展开了.

参考:


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

黑客与画家

黑客与画家

[美] Paul Graham / 阮一峰 / 人民邮电出版社 / 2011-4 / 49.00元

本书是硅谷创业之父Paul Graham 的文集,主要介绍黑客即优秀程序员的爱好和动机,讨论黑客成长、黑客对世界的贡献以及编程语言和黑客工作方法等所有对计算机时代感兴趣的人的一些话题。书中的内容不但有助于了解计算机编程的本质、互联网行业的规则,还会帮助读者了解我们这个时代,迫使读者独立思考。 本书适合所有程序员和互联网创业者,也适合一切对计算机行业感兴趣的读者。一起来看看 《黑客与画家》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试