一、谁适合看本文
为了不浪费大家时间,我把这个写在最前面。
id __weak obj1 = nil;
{
id __strong obj2 = [[NSObject alloc] init];
obj1 = obj2;
NSLog(@"%@", obj1);
}
NSLog(@"%@", obj1);
如果你能一下子说出输出什么,并且脑子里清晰的知道在以上代码中谁持有谁,那么,前辈好😃
如果觉得还是有点乱或者反应不过来,那就往下看呗,而且我们是同一类人。🙄并且我向你保证,看完绝对舒畅无比。
注意
本篇文章不会解释内存管理的思考方式!
本篇文章不会解释引用计数的工作原理!
本文会一行行代码分析所有权修饰符的使用以及内存管理的原理。
二、前言
在当下,大多应用都是在ARC环境中开发的,因此内存管理这块知识点常常被人遗忘。但是并不意味着有了ARC,开发人员就不需要了解内存管理,或者说只要大概了解就行了。
最近几天在深入了解苹果的内存管理机制,也有所收货,我打算记录在博客中和大家分享。个人认为,内存管理的思考方式和引用计数的工作原理都比较简单,大致看一下博客就能理解,所以本文不提了。本文主要会举例来介绍四个所有权修饰符,然后分析例子,最后理解内存管理。
三、所有权修饰符
在ARC环境中,id类型和对象类型与c语言类型不同,它的类型上必须附加所有权修饰符。
所有权修饰符一共有四种:
__strong
__weak
__unsafe_unretained
__autoreleasing
下面我会一个个来介绍。
1.__strong
a.默认情况下,修饰符为__strong
在上面所有权修饰符的描述中,有说到类型必须附加所有权修饰符
,但是你是不是有所疑惑,我们平时的代码中,很少有看到这样的修饰符啊。
其实是这样的,在没有明确指定所有权修饰符的情况下,默认用__strong
,所以以下两行代码完全一致。
id obj = [[NSObject alloc] init];
id __strong obj = [[NSObject alloc] init];
b.自己生成并持有的对象情况下__strong的使用
{
// 自己生成并持有对象
id __strong obj = [[NSObject alloc] init]; // 对象A
// obj为强引用,持有对象A
}
// obj的作用域结束了,所以obj释放自己持有的对象A
// 对象A不被持有了,就废弃对象A
如上例子,这是最经常遇到的情况了。短短三行代码,其实做了很多事情。
因为是第一个例子,为了照顾基础差的同学,我再补充一点:
为了便于理解,我们可以认为对象A是一条狗,obj是遛狗的人,强引用就是狗链子。当obj超出作用域的时候,链子断了,狗因为不再有链子拴着它,就逃跑了。
c.不是自己生成并持有的对象情况下__strong的使用
我们知道使用alloc/copy/new等方法或以他们开头的方法可以获得自己生成并持有的对象,而其他方法如array就只能取得非自己生成并持有的对象。(这就是为什么在非ARC中,使用array方法初始化后需要retain才能持有)
那么在这种情况下,系统又做了那些事情呢?
{
// 取得非自己生成并持有对象
id __strong obj = [NSArray array]; // 对象
// obj为强引用,持有NSArray的对象
}
// obj的作用域结束了,所以obj释放自己持有的对象
// 对象不被持有了,就废弃对象
到这里可以发现不管使用[[NSObject alloc] init]
或者[NSArray array]
,对象的所有者和对象的生命周期都是明确的,不会发生错误。
同时我们可以大胆的猜测一下,这就是在ARC环境中,[NSArray array]
和[[NSArray alloc] init]
没差别的原因。
d.__strong修饰的变量之间的赋值
{
id __strong obj0 = [[NSObject alloc] init];
id __strong obj1 = [[NSObject alloc] init];
id __strong obj2 = nil;
obj0 = obj1;
obj2 = obj0;
obj1 = nil;
obj0 = nil;
obj2 = nil;
}
上面的代码是变量之间的相互赋值,你能不能像我刚刚那样一行行分析出来呢?
- (void) test
{
// obj0持有对象A的强引用
id __strong obj0 = [[NSObject alloc] init]; // 对象A
// obj1持有对象B的强引用
id __strong obj1 = [[NSObject alloc] init]; // 对象B
// obj2不持有对象
id __strong obj2 = nil;
// obj0 持有了 obj1持有的对象(对象B) 的强引用
// 也就是说,对象B现在被obj0和obj1同时持有(狗被两条链子拴着。。。)
// 同时,对象A没有被持有了,就废弃了
obj0 = obj1;
// obj2 持有了 obj0持有的对象(对象B) 的强引用
// 也就是说,对象B现在被obj0和obj1还有obj2同时持有 (三条链子了。。。)
obj2 = obj0;
// obj1释放了对象B (剩下两条了)
obj1 = nil;
// obj0释放了对象B (就剩一条了)
obj0 = nil;
// 最后obj2释放了对象B (没绳子了)
obj2 = nil;
// 对象B没有人持有,就销毁了。
}
注意啊,这里可别把持有,销毁,释放这几个动词弄糊涂了。我当初可是在这个坑里呆了好几年呢。
持有:变量对对象的。
释放:变量对对象的。
销毁:系统对对象的。
e.__strong修饰的变量在方法参数上的使用
新建一个继承NSObject的类
// .h
@interface Model : NSObject
{
id __strong obj;
}
- (void)setObj:(id __strong)obj;
@end
//.m
@implementation Model
- (void)setObj:(id)obj
{
_obj = obj;
}
@end
然后使用这个类
- (void) test
{
id __strong model = [[Model alloc] init];
[model setObj:[[NSObject alloc] init]];
}
下面和我一起分析一下吧
- (void) test
{
// model 持有 Model对象
id __strong model = [[Model alloc] init];
// Model对象的obj变量 持有NSObject对象
[model setObj:[[NSObject alloc] init]];
}
// model作用域结束,强引用失效
// 所以Model对象没有持有者,被销毁
// 同时Model对象的obj变量也被废弃,NSObject对象没有持有者
// NSObject对象被废弃
2.__weak
现在看到__weak我的第一反应就是循环引用来了😫
不知道你对循环引用是否彻底了解。下面我们还是和上面例子一样,一行行分析。
a.相互持有问题
这里还是使用1.e创建的类。
- (void) test
{
// model1 持有 ModelA
id __strong model1 = [[Model alloc] init]; //ModelA
// model2 持有 ModelB
id __strong model2 = [[Model alloc] init]; //ModelB
// ModelA的obj变量 持有ModelB强引用
// ModelB现在被model1和ModelA的obj同时持有
[model1 setObj:model2];
// ModelB的obj变量 持有ModelA强引用
// ModelA现在被model2和ModelB的obj同时持有
[model2 setObj:model1];
}
// model1 作用域结束,强引用失效
// 所以自动释放ModelA对象,这时ModelA被ModelB的obj持有(还有一条链子)
// model2 作用域结束,强引用失效
// 所以自动释放ModelB对象,这时ModelB被ModelA的obj持有(还有一条链子)
// 内存泄漏
解决办法:将Model类中obj变量的修饰词变成__weak
// .h
@interface Model : NSObject
{
id __weak obj;
}
- (void)setObj:(id __strong)obj;
其他都不需要变,就可以解决循环引用了,具体分析我就不写了。
b.对象废弃时,弱引用失效并置nil
id __weak obj1 = nil;
{
id __strong obj2 = [[NSObject alloc] init];
obj1 = obj2;
NSLog(@"%@", obj1);
}
NSLog(@"%@", obj1);
以上代码输出如下:
2017-04-21 15:20:10.645 MRCTest[42868:3529966] <NSObject: 0x60800001c180>
2017-04-21 15:20:10.645 MRCTest[42868:3529966] (null)
可以发现第二次输出时候,obj1已经为空了。分析:
id __weak obj1 = [[NSObject alloc] init];
{
// obj2持有NSObject对象强引用
id __strong obj2 = [[NSObject alloc] init];
// obj1持有NSObject对象的弱引用
obj1 = obj2;
NSLog(@"%@", obj1);
}
//obj2作用域结束,obj2释放NSObject对象
//NSObject对象无持有者,被销毁
//obj1弱引用失效,obj1=nil
NSLog(@"%@", obj1);
c.__weak的小结
__weak
可以避免循环引用,也可以通过查看__weak
修饰的变量是否为nil判断赋值的对象是否已经废弃。
但是__weak
只能在iOS5以上以及OS X Lion上使用。在iOS4和OS X Snow Leopard中,只能用__unsafe_unretained
代替。
3.__unsafe_unretained
还是上一个例子
id __unsafe_unretained obj1 = [[NSObject alloc] init];
{
// obj2持有NSObject对象强引用
id __strong obj2 = [[NSObject alloc] init];
// obj1持有NSObject对象的弱引用
obj1 = obj2;
NSLog(@"%@", obj1);
}
//obj2作用域结束,obj2释放NSObject对象
//NSObject对象无持有者,被销毁
NSLog(@"%@", obj1);
// obj1表示的对象已经被废弃
// 出现垂悬指针
结果:
__unsafe__unretained垂悬指针.png程序奔溃。
也就是说,在使用__unsafe__unretained
修饰符时,赋值给附有__strong
修饰符的变量时,需要确保被赋值的对象确实存在。
4.__autoreleasing
这篇文章本应该四月二十三日就发布出来,但是却拖到了今天,就因为这个__autoreleasing
,因为里面涉及到了很多知识点,在总结的时候,自己又搞糊涂了。
因为我认为这篇文章总结的是所有权修饰符,所以在这里不花大篇幅去做实验。我准备新建一篇文章来专门写__autoreleasing
涉及到的知识点。
下面就大致写一下__autoreleasing
的使用。
a.__autoreleasing替代autorelease方法
在ARC中,是不能使用autorelease方法和NSAutoreleasePool类。那么如何使用自动释放池呢?其实�在ARC中,使用@autoreleasepool可以代替NSAutoreleasePool。以下两段代码的作用是一致的。
//在ARC无效时
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[obj autorelease];
[pool drain];
//在ARC有效时
@autoreleasepool {
id __autoreleasing obj2;
obj2 = obj;
}
这里的__autoreleasing
的作用就是告诉编译器,obj2变量是__autoreleasing
类型的,它能延缓obj2释放对象。
b.延缓释放对象
下面举个例子:
id __unsafe_unretained obj0;
@autoreleasepool {
{
id __autoreleasing obj1 = [[NSObject alloc] init];
obj0 = obj1;
}
NSLog(@"%@", obj0);
}
NSLog(@"%@", obj0);
结果:第一行输出有结果,第二行输出时程序奔溃。
分析:
// 定义一个__unsafe_unretained的变量obj0
id __unsafe_unretained obj0;
@autoreleasepool {
{
//obj为__autoreleasing类型的,因此NSObject对象被放入到了自动释放池中
id __autoreleasing obj = [[NSObject alloc] init];
obj0 = obj;
}
// obj的作用域结束,如果obj为strong类型的,就会释放NSObject对象
// 但是这里的obj为__autoreleasing类型,因此不释放
// 正常打印
NSLog(@"%@", obj0);
}
//autoreleasepool块结束,autoreleasepool中的对象被释放
//所以obj0变成了垂悬指针,奔溃
NSLog(@"%@", obj0);
c.__autoreleasing在一些情况下会默认使用
但是像以上的显示的使用__autoreleasing修饰符是比较少见的,有些情况下编译器会自动帮我们加上。
1.对象作为函数的返回值,编译器会自动将其注册到自动释放池。
//由于return使得对象变量超出其作用域,所以该强引用对应的自己持有的对象会被自动释放,但该对象作为函数的返回值,编译器会自动将其注册到autoreleasepool
2.id的指针或对象的指针在没有显示指定时会被附加上_autoreleasing
修饰符。
下面本该举一堆例子,但是就像前面说的,例子放这里:深入内存管理:让人头疼的autorelease
四、实战
说了那么多,可能会觉得无聊,实战才是最能帮助理解的。
实战一
我现在有个需求,现在有一个视图控制器,当它出现后,它的子视图暂时先不显示出来,当用户点击屏幕后再显示出来。
如果我的代码是这样的,能实现吗?
@interface TestViewController ()
@property (nonatomic, weak) UIView *subView;
@end
@implementation TestViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
UIView *view = [[UIView alloc] init];
view.frame = CGRectMake(100, 100, 100, 100);
view.backgroundColor = [UIColor redColor];
self.subView = view;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.view addSubview:self.subView];
}
换一个方法,如果是这样的呢?
@interface TestViewController ()
@property (nonatomic, weak) UIView *subView;
@end
@implementation TestViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
UIView *view = [[UIView alloc] init];
view.frame = CGRectMake(100, 100, 100, 100);
view.backgroundColor = [UIColor redColor];
view.hidden = YES;
[self.view addSubview:view];
self.subView = view;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.subView.hidden = NO;
}
其实这里的重点在于addSubview方法,它会使得self.view持有view,所以即使subView为weak,过了view的作用域后,view对象也不会被销毁。当然在第一种方法中,把subView的修饰词改为strong也是可以的。
实战二
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};
这是AFN的一段代码,其中,在block外使用的是__weak
修饰词,原因大家都知道,而为什么block内使用的是__strong
呢?
其实这样写的目的是为了保证在block执行过程中该变量不会被释放掉。这也是一个绝妙的方法啊。
五、总结
这篇文章很基础,但是在之前学习中很多知识点都被我忽略了。近期也在看《OC高级编程》这本书,希望能在其中巩固基础并了解底层实现。
最后推荐《OC高级编程 iOS与OS X 多线程和内存管理》。这本书虽然很多语句都不通顺,词不达意,但是多看几遍还是能基本理解它的意思的。
六、补充
感谢不上火喝纯净水补充。
在实战二中,使用__weak
和__strong
结合的方法有一点需要注意,那就是在block执行前如果self为空,那么block中不管是weakSelf或strongSelf都为空。
我做了实验:
NSObject *obj = [[NSObject alloc] init];
typeof(obj) __weak weakObj = obj;
NSLog(@"block外1:%@", obj);
void (^testBlock) () = ^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(obj) __strong strongObj = weakObj;
NSLog(@"block内1:%@", strongObj);
[NSThread sleepForTimeInterval:3];
NSLog(@"block内2:%@", strongObj);
});
};
obj = nil;
testBlock();
NSLog(@"block外2:%@", obj);
打印出:
2017-05-02 17:53:16.225 MRCTest[63062:6914426] block外1:<NSObject: 0x6080000087d0>
2017-05-02 17:53:16.225 MRCTest[63062:6914426] block外2:(null)
2017-05-02 17:53:16.225 MRCTest[63062:6914539] block内1:(null)
2017-05-02 17:53:19.228 MRCTest[63062:6914539] block内2:(null)
的确是都为空。
下面代码体现这样组合的作用:
NSObject *obj = [[NSObject alloc] init];
typeof(obj) __weak weakObj = obj;
NSLog(@"block外1:%@", obj);
void (^testBlock) () = ^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(obj) __strong strongObj = weakObj;
NSLog(@"block内1:%@", strongObj);
[NSThread sleepForTimeInterval:3];
NSLog(@"block内2:%@", strongObj);
});
};
testBlock();
sleep(1);
obj = nil;
NSLog(@"block外2:%@", obj);
打印出:
2017-05-02 17:55:48.022 MRCTest[63087:6917651] block外1:<NSObject: 0x60800000dd10>
2017-05-02 17:55:48.022 MRCTest[63087:6917744] block内1:<NSObject: 0x60800000dd10>
2017-05-02 17:55:49.023 MRCTest[63087:6917651] block外2:(null)
2017-05-02 17:55:51.027 MRCTest[63087:6917744] block内2:<NSObject: 0x60800000dd10>
确实能不受外界干扰,继续持有。
网友评论
谢谢您补充,我加上去
其次,block中的临时变量,如果是strong来修饰的话,会把引用对象retain引用计数+1,然后在block作用域结束的时候释放该变量的引用计数。但是这是在该block执行的时候。
所以结果就是,如果block执行前self为nil的话,那么整个block中的使用的weakself 和 strongself都将是nil了。如果执行前不是nil,然后执行到strong临时变量赋值那一句,它的引用计数就加1了,于是别的地方都对self做了release,但还有block中的最后一个引用技术,所以self不会释放。