从一个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. 一些代码设计上的收获, 不展开了.

参考:


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

查看所有标签

猜你喜欢:

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

Reversing

Reversing

艾拉姆(Eilam,E.) / 韩琪、杨艳、王玉英、李娜 / 电子工业出版社 / 2007-9 / 79.00元

本书描述的是在逆向与反逆向之间展开的一场旷日持久的拉锯战。作者Eldad Eilam以一个解说人的身份为我们详尽地评述了双方使用的每一招每一式的优点与不足。 书中包含的主要内容有:操作系统的逆向工程;.NET平台上的逆向工程;逆向未公开的文件格式和网络协议;逆向工程的合法性问题;拷贝保护和数字版权管理技术的逆向工程;防止别人对你的代码实施逆向工程的各种技术;恶意程序的逆向工程;反编译器的基本......一起来看看 《Reversing》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具