关于我的仓库
- 这篇文章是我为面试准备的iOS基础知识学习中的一篇
- 我将准备面试中找到的所有学习资料,写的Demo,写的博客都放在了这个仓库里iOS-Engineer-Interview
- 欢迎star👏👏
- 其中的博客在简书,CSDN都有发布
- 博客中提到的相关的代码Demo可以在仓库里相应的文件夹里找到
前言
- ARC使用了四种所有权修饰符对引用计数进行管理,使得不再需要手动操作retain,release
- 理解ARC规则,使得对于内存管理的思考方式更进一步,以OC的方式来思考内存管理是理解的难点
- 由于markdown渲染的锅,本文忽略了一些双下划线
准备工作
阅读《Objective-C 高级编程》中的p.29 ~ 78
回顾下内存管理四大原则是不是牢牢记住了
内存管理四大原则
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也能持有
- 不再需要自己持有的对象时释放
- 非自己持有的对象无法释放
ARC规则(p.29 ~ p.65)
__strong修饰符
- __strong是默认修饰符
id obj = [[NSObject alloc] init];
//id __strong obj = [[NSObject alloc] init];
NSObject *obj = [[NSObject alloc] init];
//NSObject __strong *obj = [[NSObject alloc] init];
补充知识:id的本质
- id是struct objc_object结构体指针,可以指向任何OC对象,当然不包括NSInteger等,因为这些数据类型不是OC对象。另外OC的基类,其实不仅仅就NSObject一个,虽然NSObject是绝大数OC对象的基类,但是还有个NSProxy虚类。所以不能说id类型和NSObject *是等价的。
- id类型的实例在编译阶段不会做类型检测,会在运行时确定,所以id类型是运行时的动态类型。类NSObject的实例会编译期要做编译检查,保证指针指向是其NSObject类或其子类,当然实例的具体类型要在运行期确定,这也是iOS的多态的体现。
- 记住:id本身就是个指针【不记住这一点的话,后面看到__autoreleasing可能会像我一样崩溃了🌚】
超出变量作用域 = 废弃
- 这里要开始转换思维模式了,比如这样一句代码id obj = [[NSObject alloc] init];
- 我们这里有两个东西,一个是obj,它是一个指针,指向生成对象;同时,它隐式使用了__strong,它也就持有了该对象,成为对象持有者【也就是说对象的引用计数+1了,还是强调一遍,把引用计数作为辅助手段去理解,还是要从持有与否的角度去看待内存管理】
- 这里我们看p.31最下面这个obj失效的例子
- 记住这个反应链:
- obj超出作用域,obj失效,不再持有该对象
- 失去强引用,对象不在被持有【也就是引用计数从1到0,自动dealloc】
- 对象被释放
- 这里一定要分清,持有者是持有者,对象是对象,两者不是一个东西
__strong对象相互赋值
- 书上这个例子可能注释的还不是很好理解,我这里直接放一张表格,研究三个obj分别持有谁
语句 | obj0 | obj1 | obj2 |
---|---|---|---|
id __strong obj0 = [[NSObject alloc] init];//生成对象A | A | ||
id __strong obj1 = [[NSObject alloc] init];//生成对象B | A | B | |
id __strong obj2 = nil; | A | B | nil |
obj0 = obj1;//obj0强引用对象B;而对象A不再被ojb0引用,被废弃 | B | B | nil |
obj2 = obj0;//obj2强引用对象B(现在obj0,ojb1,obj2都强引用对象B) | B | B | B |
obj1 = nil;//obj1不再强引用对象B | B | nil | B |
obj0 = nil;//obj0不再强引用对象B | nil | nil | B |
obj2 = nil;//obj2不再强引用对象B,不再有任何强引用引用对象B,对象B被废弃 | nil | nil | nil |
- 赋值的本质是强引用的转变,这也可以帮助我们理解为什么要引入这一套规则,我们不能也不用去直接对对象进行操作管理,使用指针和引用计数安全,有效的进行使用
方法参数中使用__strong
- 到这里还是重复确认下,__strong就是默认的修饰符
- 以及这句话很关键废弃Test对象的同时,Test对象的obj_成员也被废除
- 也就是说成员变量的生存周期是与对象同步的
__strong导致的循环引用
- 前面所有的强调加黑的文字都是为了能理解这里的循环引用
补充知识:内存泄漏
- 简单来说,内存泄漏就是在内存该被释放的时候没有释放,导致内存被浪费使用了
- 内存泄漏在iOS开发中轻则影响性能,重则导致crash
赋值阶段
- 这里还是放一张表格,说明这两个test以及其成员持有谁
语句 | test0 | test0.obj | test1 | test1.obj |
---|---|---|---|---|
id test0 = [[Test alloc] init];//生成TestA | TestA | |||
id test1 = [[Test alloc] init];//生成TestB | TestA | TestB | ||
[test0 setObject:test1]; | TestA | TestB | TestB | TestA |
[test1 setObject:test0]; | TestA | TestB | TestB | TestA |
失效阶段
- 这里为了加强理解,直接把各自的引用计数,持有对象都列出来
What happen | TestA引用计数 | TestA持有者 | TestB引用计数 | TestB持有者 |
---|---|---|---|---|
初始状态 | 2 | test0,test1.obj | 2 | test0.obj,test1 |
test0超出作用域 | 1 | test1.obj | 2 | test0.obj,test1 |
test1超出作用域 | 1 | test1.obj | 1 | test0.obj |
为什么test0失效的时候,obj_依然存在
这里还是用引用计数的方式来思考比较好
只有当某个对象的持有者都被释放了,该对象才会被dealloc,而这里的test0,test1仅仅只是指针而已
这点非常重要,如果在你的脑海里想的是test0被释放,他下面的obj_自然不复存在了,请牢记test0仅仅是一个指针,是一个对象的持有者,不是对象本身
造成结果
我要dealloc TestA
- 我们看,testA dealloc不了,因为test1.obj持有了testA
- 而想要废除test1.obj,就如我们上面介绍的,废弃Test对象的同时,Test对象的obj_成员也被废除
- 而test1是TestB内存里的成员变量
- 一句话,想dealloc TestA,必须先dealloc TestB才行
我要dealloc TestB
- 我们看,testB dealloc不了,因为test0.obj持有了testB
- 而想要废除test0.obj,就如我们上面介绍的,废弃Test对象的同时,Test对象的obj_成员也被废除
- 而test0是TestA内存里的成员变量
- 一句话,想dealloc TestB,必须先dealloc TestA才行
对自身的强引用
- 对自身的强引用和上面其实一个意思,test,test.obj同时持有了对象,test超出定义域release,对象引用计数-1
- 而test.obj遇到了和上面一样的难题,如果还是想不通发生了什么,请把上面两段话再认真看看
__weak修饰符
- __weak持有弱引用
- 如果对于strong修饰符理解OK的话,这个weak修饰符其实很好懂
生成__weak的持有者
id __weak obj = [[NSObject alloc] init];
//这里如果直接使用__weak obj来持有对象,由于这里是弱引用,引用计数不会加一,对象随时可能会被dealloc
//后面会讲到,其实内部是用__autoreleasing来维持该对象不被dealloc
id __strong obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
//在这里,obj0先强持有该对象,给它引用计数+1,防止其dealloc
//之后再让obj1去弱持有该对象,达成我们需要的目的
使用__weak的好处
- 如果对于前面循环引用的原因研究理解到位了,应该就能明白为什么__weak能避免循环引用
- __weak不会增加引用计数,相应的对象该dealloc就会dealloc,其中的持有者自然而然就会被release
- 这里可以思考下,如果一个弱引用,一个强引用,这样子的相互引用会导致循环引用吗?
__autoreleasing修饰符
与MRC时比较
- 先回忆一下,MRC中autorelease的使用方法
- 生成并持有NSAutoreleasePool对象。
- 调用已分配对象的autorelease方法。【将对象注册到pool中】
- 废弃NSAutoreleasePool对象。【pool执行drain废除,其中的对象也跟着release】
- 我们会发现,这里出现两个部分,一个pool,一个对象
- 于此相对的,__autoreleasing同样分成两块
自动调用
- MRC中有介绍,像array这样的方法,生成的对象不是由自己持有的,其中就是靠__autoreleasing修饰符去实现
- 当编译器检测到这样的方法命名后,就会自动加上__autoreleasing修饰符
- 那么这里有个注意点就是,strong才是默认的修饰符,我们如果用strong修饰符去接收的对象,当其超出作用域的时候,strong修饰符先失效,走出@autoreleasepool块后,__autoreleasing修饰符失效
weak修饰符与autoreleasing修饰符
- 如同上面提到的,weak修饰符的实现要借助autoreleasing修饰符
id __weak obj1 = obj0;
NSLog(@"class = %@",[obj1 class]);
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@",[tmp class]);//实际访问的是注册到自动释放池的对象
- 由于weak不会增加引用计数,对象难以维持,所以要通过__autoreleasing来维护
- 在使用附有weak修饰符的变量时就必定要使用到autoreleasing修饰符
autoreleasing修饰符无处不在
牢记:只有作为alloc/new/copy/mutableCopy方法的返回值而渠道对象时,能够自己生成并持有对象,其他情况都是"取得非自己生成并持有的对象",换句话说,就轮到我们的autoreleasing修饰符上场了
具体ARC规则
规则
- 不能使用retain/release/retainCount/autorelease
- 不能使用NSAllocateObject/NSDeallocateObject
- 必须遵守内存管理的方法名规则
- 不要显式调用dealloc
- 使用@autorelease块代替NSAutoreleasePool
- 不能使用区域(NSZone)
- 对象型变量不能作为C语言结构体的成员
- 显式转换id和void*
dealloc
- 重写dealloc方法时不需要写[super dealloc]
- dealloc无法释放不属于该对象的一些东西,需要我们重写时加上去,例如
- 通知的观察者,或KVO的观察者
- 对象强委托/引用的解除(例如XMPPMannerger的delegateQueue)
- 做一些其他的注销之类的操作(关闭程序运行期间没有关闭的资源)
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];//移除通知观察者
[[XMPPManager sharedManager] removeFromDelegateQueue:self];//移除委托引用
[ [MyClass shareInstance] doSomething ]//其他操作
}
__bridge
-
__bridge
可以实现Objective-C与C语言变量 和 Objective-C与Core Foundation对象之间的互相转换 -
__bridge
不会改变对象的持有状况,既不会retain
,也不会release
-
__bridge
转换需要慎重分析对象的持有情况,稍不注意就会内存泄漏 -
__bridge_retained
用于将OC变量转换为C语言变量 或 将OC对象转换为Core Foundation对象 -
__bridge_retained
类似于retain
,“被转换的变量”所持有的对象在变量赋值给“转换目标变量”后持有该对象 -
__bridge_transfer
用于将C语言变量转换为OC变量 或 将Core Foundation对象转换为OC对象 -
__bridge_transfer
类似于release
,“被转换的变量”所持有的对象在变量赋值给“转换目标变量”后随之释放
属性声明与所有权修饰符
ARC实现(p.65 ~ p.78)
说明
- 这是一张最新(750.1)objc4库.mm文件列表
- 可以看到,这个里甚至没有作者提到的objc-arr.mm这个文件了
- 还是再强调一次,objc4库Apple一直在不停的更新,所以书里讲到的源码实现可能都和目前最新的脱轨了
- 所以看书的时候还是以了解为主,想理解现在真正的源码实现方式,当然还是要啃最新源码
__strong修饰符实现
//自己持有
{
id __strong obj = [NSObject alloc] init];//obj持有对象
}
id obj = objc_mesgSend(NSObject, @selector(alloc));
objc_msgSend(obj,@selector(init));
objc_release(obj);//超出作用域,释放对象
//非自己持有
{
id __strong obj = [NSMutableArray array];
}
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);//objc_retainAutoreleasedReturnValue的作用:持有对象,将对象注册到autoreleasepool并返回。
objc_release(obj);
+ (id)array
{
return [[NSMutableArray alloc] init];
}
+ (id)array
{
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj,, @selector(init));
return objc_autoreleaseReturnValue(obj);//objc_autoreleaseReturnValue:返回注册到autoreleasepool的对象。
}
objc_retainAutoreleasedReturnValue与objc_autoreleaseReturnValue
两个不一定
- objc_retainAutoreleasedReturnValue不一定非要持有注册到pool里的对象
- objc_autoreleaseReturnValue不一定非要注册到pool中
解释
-
书上的图说的很清楚
-
将这张图分成两部分去看
-
首先是左侧三个灰色箭头代表的普通流程
- object注册到pool中
- 在pool中找到对象,返回
-
然后是右侧三个黑色箭头构成的优化情况,就是当objc_autoreleaseReturnValue后直接objc_retainAutoreleasedReturnValue的情况
- 直接就是objc_autoreleaseReturnValue获取对象
- objc_retainAutoreleasedReturnValue持有返回的对象
这样子,跳过了中间的pool这个中转站,实现优化
-
__weak修饰符实现
objc_storeWeak
- objc_storeWeak(&obj1, obj)会使用weak表来存储使用weak修饰符的变量的地址
- weak表同样也是哈希表,其key是对象的地址,value是附有weak修饰符变量的地址
- 如果第二参数是0的话,就把第一参数里的地址从weak表中删除
注册到autoreleasepool
id obj1;
objc_initWeak(&obj1,obj);//初始化附有__weak的变量
id tmp = objc_loadWeakRetained(&obj1);//取出附有__weak修饰符变量所引用的对象并retain
objc_autorelease(tmp);//将对象注册到autoreleasepool中
objc_destroyWeak(&obj1);//释放附有__weak的变量
- 这里注意,每次都是访问时,注册到pool里,每访问一次注册一次
- 因此书上推荐再将其赋值给一个strong
__autoreleasing修饰符实现
id pool = objc_autoreleasePoolPush();//pool入栈
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);//pool出栈
2019.7.22 更新:关于weak修饰符的一些打印实验
//在ARC中我们可以使用__bridge来查看应用计数
NSObject *obj0 = [[NSObject alloc] init];
printf("retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(obj0)));
NSObject * __weak obj1 = obj0;
printf("retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(obj1)));
printf("retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(obj0)));
// retain count = 1
// retain count = 2
// retain count = 1
- 这里我们可以对与weak修饰符有更深的了解,就如书上p.46页上说的
id __weak obj1 = obj0;
NSLog(@"class = %@",[obj1 class]);
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@",[tmp class]);//实际访问的是注册到自动释放池的对象
- 这也是为什么我们第二次打印出来的retain count = 2,因为我们等同于将对象注册到了autoreleasepool中,因此引用计数+1
- 但是我们第三个打印又变回了1,这说明两件事
- 不是一使用weak修饰符就会直接注册到pool中,是当你访问的时候才会生成一个__autoreleasing tmp,这也是为什么作者取变量名为tmp
- 而接下来就变回1,说明当访问完之后就会直接释放,等同于release了,导致引用计数
- 总结一下就是 ,weak修饰符之所以要生成tmp,只是为了防止该对象无人引用,会直接dealloc,因此使用一个tmp维护住它,当访问结束后,这个也就释放了
2019.7.25晚 更新:《Effective Objective-C 2.0》中的ARC
- 绝不应该说引用计数一定是几,只能说执行的操作是递增了该计数还是递减了该计数
p.121 stringValue
+ (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this:%@", self];
return str;
}
NSString *str1 = [NSString stringValue];
- 这里提到了返回的str对象其保留计数比期望值要多1(+1 retain count)
- 我们来试着解读为什么
- 个人认为这里的意思是stringValue中包装了一个init方法,这个init方法会使该对象引用计数+1,然后当我们返回时,我们需要接收这个返回值,导致引用计数又加了一,也就相当于
NSString *str = [NSString alloc] initWithFormat:@"I am this:%@", self];
NSString *str1 = str;//str1就是接收者,使得该对象又被retain了一次
- 还是很牵强,但又觉得只能这么理解
p.124 三个演示ARC的例子
- 这个方法以"new"开头,既然"person"已经使得引用计数+1了【因为alloc】,那么我们不需要做任何操作了,直接返回就行;也就是说,以alloc开头的方法,只要保证返回的对象引用计数+1了就行
- 该方法不是以那几个开头,因此返回时自动autorelease
- personOne没有注册到pool中,因此超出作用域直接release;personTwo相反
objc_retainAutoreleasedReturnValue与objc_autoreleaseReturnValue
理解
- objc_retainAutoreleasedReturnValue会检验调用者是否会对该对象执行retain,如果会的话就不执行autorelease,直接设置标志符
- objc_autoreleaseReturnValue在检验到标志服后,也不retain了,直接返回对象本身
- 其实这种情况是最常见的了,方法的返回基本上是不会在autorelease了,这也是为什么上面提到对于方法的返回可以删除autorelease部分,直接饮用计数+1就行
疑惑
- 这里我不太理解的是,按道理这样子等于是有两次retain操作,他就算优化也应该减少一次就行了,他怎么就把两次都省了?
打印试验
#import "ViewController.h"
__weak id autoObj;
id stongObj;
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
id __autoreleasing obj = [NSMutableArray arrayWithObject:@"123"];
//NSLog(@"%@", obj);
autoObj = obj;
NSLog(@"1obj:%@", autoObj);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"2obj:%@", autoObj);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"3obj:%@", autoObj);
}
@end
- 结果为123 123 null
- 这里说明确实注册到了pool里,因为viewDidAppear和上面两个不在一个runloop里,导致最后打印不出来
- 我们把__autoreleasing换成strong,结果为123 null null,这里就是执行上面说的优化操作,并没有注册到pool里
- 太神秘了
2019.7.26更新 至是疑始释
- 引用一波明史里朱棣了解到建文帝下落后的描述,正好解释下现在的心情
- 在和哲神交流一波后,真的是突然对很多东西豁然开朗,贴下哲神的博客,接下来他也会更新runtime相关的文章咚个里个呛
关于autorelease
- 其实整个引用计数管理都很清楚明白,retain,release,都简单不过了,只有autorelease比较抽象,难以理解
- 我们这里先想明白,在ARC中,我们不再需要显式调用这几个方法
- autorelease很多时候是为了那些不以alloc开头的方法,他们的要求是自己生成了但自己不持有,因此要使用autorelease,注册到pool中
+ (NSString *)stringValue {
NSString *str = [NSString alloc] initWithFormat:@"I am this:%@", self];
return str;
}
NSString *str1 = [NSString stringValue];
- 在这种情况下,str1明显不应该持有返回的对象,而如果方法里直接就返回了,明显会导致持有了
- 因此这个方法返回的实际上是[str autorelease]
- 这里我们可以这么理解,如果我们直接返回对象,和[str autorelease]都会使得引用计数+1了,区别在于
- NSString *str1 = str,将控制权给了str1,由他控制是否release
- NSString *str1 = [str autorelease],将控制权给了pool,由它决定什么时候release
- 且这个时候最开始的str已经超出了作用域,release了
- 这也就是为什么说只比期望值多了一,期望值是initWithFormat方法里加上的,那个多一就是被pool引用了
疑点
- 目前的疑点还是在于小蓝书的前后不一致,或者是理解还不到位
- 在p125下面提到autorelease与retain都是多余的,如果删除了这两步操作,显然在执行完赋值操作以后,引用计数不变,放这里就是和init时候一样就对了,即与期望值一样了
- 而后面又说,规定从方法中返回的对象其引用计数都比期望值多一就好
- 这个多的一要么是str1 retain了,要么是str autorelease的,如果都删去,还怎么多这个1?
- p126的优化同样也是这个道理,如果两个方法都没走else,也就是和期望值一样,这里前后矛盾让还是无法好好理解
- 所以这里我还是觉得,引入持有这个概念就是双刃剑,虽然前期提到了阅读性,但后面直接导致一直要思考谁持有谁,谁被释放了,不如一直研究对象的引用计数
解
- 其实理解到最后就是ARC时代,为了简化掉多余的操作,所有的运行都是以期望值来决定
- 就以上面这个例子一开始str持有对象,后来理应pool持有对象,最后应该是str1持有对象
- 但从我们的实际需求来说,我们只需要让str1持有对象,因此只要return str,不进行retain之类的操作,就OK了
- 从这个角度出发,可以便于理解这些简化