题型复现
iOS是怎么管理内存的?
都有哪些与内存管理有关的关键字?有何异同?
什么是ARC?MRC?区别?怎么进行混编?
CF对象又该怎么办呢?
Cocoa和CF框架间对象的转换过程内存所有权是怎么过渡的?
历史沿革
任何一门语言都会创造变量,而这些变量时存储在内存空间的。计算机
(或移动设备)的内存是有限的,因此我们需要对内存的使用做相应的管理,在适当的时机分配内存和释放内存。
在iOS5之前,iOS采用的是MRC(Manual Reference Counting),就是内存的分配和释放(或者说对象的生命周期)都是由我们程序猿手动管理的。这就需要我们务必在,适当的时机去释放已经没有用的对象来增加可用内存空间。
上述对程序猿来说无疑是很麻烦的,稍有不慎就会造成一定的内存问题。因此,Apple特地推出了ARC(Auto Reference Counting),就是对象的内存分配和释放不需要我们去做了。大部分情况下,我们只管创建就好,系统会为我们自动地释放已经不需要的对象。
Reference Count
无论是MRC还是ARC,都有一个Reference Count的概念。那么,他是什么意思呢?
Reference Count,引用计数,是每个对象都有的一个属性。他代表当前引用并持有这个对象的指针数量。只要他不为零说明当前对象还有用,如果某一时刻他的值为零说明系统已经不需要这个对象。这个时候就需要对这个对象释放内存空间。
貌似有点抽象哈,我们举个简单的🌰。
假设某组织成员名额是有限的,当只要有组织内的人支持你表示你有1票,只要有1票就可以出现在组织内部。当支持你的人多了1个,你的票数就会相应加1,反之亦然。当你票数为0时,你就必须离开这个组织,把名额让给其他的人。
我们复盘这个例子,来看看他和内存管理中引用计数的对应关系:
- 某组织:当前程序系统
- 成员名额:内存空间
- 成员:当前程序的各个对象
- 支持某成员的成员:引用(持有)某成员
- 票数:引用计数
- 票数0->1:对象创建出现在程序中
- 票数1->0:对象无用应该得到释放
MRC
首先来看MRC,从上面也能得知内存管理的本质就是引用计数的改变。在MRC中有以下接口可以引起引用计数的改变。
相关操作 | 相关API | ReferenceCount变化 |
---|---|---|
生成 | alloc/new | +1 |
持有 | retain | +1 |
拷贝 | copy/mutableCopy | +1 |
释放 | release | -1 |
销毁 | dealloc | == 0时执行 |
MRC呢就是需要我们在适当的时机去持有或释放对象(执行retain和release语句),典型的代表就是setter和getter方法的写法。
//setter
-(void)setObject:(id) object {
if (_object != object) {
//前提是strong等表示强持有的对象类型
//先释放旧值
[_object release];
//再持有新值
//非字符串对象
[object retain];
//字符串对象
//[object copy]; //或者 [object mutableCopy];
_object = object;
}
}
//getter
- (void)getObject {
//发生外部对象持有h该属性对象
//非字符串对象
[_object retain];
//字符串对象
//[obj copy]; //或者 [obj mutableCopy];
return _object;
}
ARC
ARC本质上还是MRC,只不过之前"Manual"是程序猿自己在做,现在这项工作交给了编译器。编译器会根据语义分析,在适当的时机为程序加上retain和release等语句。
还是拿setter和getter方法作为🌰:
//setter
-(void)setObject:(id) object {
if (_object != object) {
_object = object;
}
}
//getter
- (void)getObject {
//发生外部对象持有h该属性对象
//非字符串对象
[_object retain];
//字符串对象
//[obj copy]; //或者 [obj mutableCopy];
return _object;
}
从我从事iOS开发以来,iOS就已经全面过渡到了ARC时期。大部分OC对象的生命周期管理都不需要程序猿来做了,这对程序猿来说无疑是一件喜事。
属性修饰符
我们在定义属性时往往会有一个关键字来声明该属性与所属对象的持有关系,而这种持有关系的保持又是靠所有权修饰符决定的。每一个属性修饰福都有其对应的所有权修饰符。
OC中主要涉及的修饰符及其对应关系和作用如下:
属性修饰符 | 对应所有权修饰符 | ReferenceCount变化 |
---|---|---|
strong | __strong | +1 |
copy | __strong | +1 |
retain | __strong | +1 |
__unsafe_unretained | __unsafe_unretained | +0 |
assign | __unsafe_unretained | +0 |
weak | __weak | +0 |
__strong
__strong表示强引用。当对__strong修饰的属性进行赋值某个对象时,该对象引用计数会+1。对应的属性修饰符主要有三个:
-
strong 对象类型默认的属性修饰符,如果没有对应关键字声明,说明默认使用他。
-
retain与strong关键字基本相同。唯一的区别是在MRC下对block的修饰,strong等同于copy(会形成__NSMallocBlock),retain则相当于assign(会形成__NSStackBlock)。
-
copy与strong类似,主要用于子类拥有可变类型的不可变对象的修饰上,保护其类型安全性。其他还有类如block等特殊类型的修饰。
首先,我们来看retain和strong的区别。我们都知道一般都会使用copy修饰block(原理以及block分类暂且略过后面会有专门章节来分析),现在假设有这样一段代码:
@property(nonatomic, strong)void(^strongBlock)();
@property(nonatomic, retain)void(^retainBlock)();
//...
NSInteger s = 520;
_strongBlock = ^(void) {
NSLog(@"i am strong block.I catch a int:%ld", s);
};
NSLog(@"_strongBlock Type:%@", [_strongBlock class]);
_retainBlock = ^(void) {
NSLog(@"i am retain block.I catch a int:%ld", s);
};
NSLog(@"_retainBlock Type:%@", [_retainBlock class]);
首先,我们在ARC环境下运行。发现block类型相同,说明ARC下strong和retain修饰block是等效的,等同于copy。
ARC下接着,我们再在MRC下(不会切换MRC的请自行百度)运行。发现retain修饰的block是栈类型的block,说明没有进行堆区拷贝(暂且知道后续会讲原理)。
MRC下接下来,我们再看看copy的特殊之处。我们平时大部分时候使用它修饰NSString类型,但是我们有木有想过为什么不用strong呢?他们同样是属于__strong阵营。Talk is cheap, show you the code!
@property(nonatomic, strong)NSString *strongStr;
@property(nonatomic, copy)NSString *copyingStr;
NSMutableString *mStr = [[NSMutableString alloc] initWithString:@"hello!!!"];
self.strongStr = mStr;
self.copyingStr = mStr;
[mStr appendString:@"HAHAHA"];
NSLog(@"_strongStr :%@", self.strongStr);
NSLog(@"_copyingStr :%@", self.copyingStr);
看下输出结果,我们发现copy修饰的字符串并没有随之改变。而strong修饰的字符串竟然意外的随mStr的改变而改变了,我们在开发中并不希望我们的变量这样突然秘密改变。
copy和strong修饰string其实,我们在打印一下他们的地址就会有新的发现。copy修饰的字符串与strong修饰的相比,和mStr的地址并不相同。这个有涉及到copy修饰符内部会代用copy方法作深浅拷贝(暂作了解后续会深入分析)。
地址比较综上,为了保护NSString的数据安全性,我们需要使用copy修饰符而不是strong!这里可以进行举一反三的思考,我们不难发现NSMutableString是
NSString的子类,而且这种不安全发生在前者赋值给后者的时候。这就很容易联想到NSArray,NSDictionary两个类,其实他们也是需要用copy修饰的。关于他们的例子,读者可以自己写个Demo验证一下。
__unsafe_unretained
__unsafe_unretained表示弱引用,在属性赋值的过程中并不影响赋值对象的引用计数。
- __unsafe_unretained 修饰对象
- assign 修饰诸如int,NSIngter,double等基本数据类型
__weak
__weak也表示弱引用,他是iOS5.0开始引入的一个所有权描述符。那么,既然有了__unsafe_unretained为什么还需要__weak呢?其实他是为了解决__unsafe_unretained有时会引起的crash问题的。__unsafe_unretained修饰的对象被释放后指针并不置nil,这就使得再次访问他就会发生crash。哈哈,是不很快猜到了__weak的特点,没错就是这一点:
- 修饰的对象被销毁后,其指针会置nil
那么,同样有了weak是不是就不需要__unsafe_unretained了呢?答案显然是否定的。原因是除了对象类型还有基础数据类型呢,assign属于__unsafe_unretained阵营。
上面说到__weak比较消耗性能,那么他底层原理是怎么样的呢?有兴趣的可以参看我之前写过的一篇笔记: 《直捣黄龙一探iOS weak实现原理》。了解其原理就会发现,__weak还是比较消耗性能的,不过这一点与可能引起的crash相比不足为道。
__bridge
我们知道虽然有了ARC,但是他只对Cocoa框架的对象管用。我们往往在开发中还会用到CoreFoundation框架,这可是面向C的接口。因此这里还是需要我们用类似MRC那一套进行手动内存管理。
- CFReatin 同Cocoa中的retain
- CFRelease 同Cocoa中的release
有的时候Cocoa里字符串类型为NSString,CoreFoundation为string类型。因此,在两者协同开发时某个类型从一个框架进入另一个框架需要做内存所有权的交接。
- __bridge 只做类型转换,并不包括所有权的交接
- __bridge_retained 类型转换+所有权交接,Cocoa --> CoreFoundation
- __bridge_transfer 类型转换+所有权交接, CoreFoundation --> Cocoa
直接上代码。
//cocoa 字符串
NSString *cocoaString = @"I am cocoa string.";
//转换成CF类型需要做内存所有权的转移。这样即使cocoaStr释放,cfStr也不会受到影响
CFStringRef cfStr = (__bridge_retained CFStringRef)cocoaString;
//或者使用CFBridgingRetain函数
CFStringRef cfStr1 = CFBridgingRetain(cocoaString);
//使用cfStr
//...
//CF下使用完cfStr需要手动释放
CFRelease(cfStr);
//CF字符串
CFStringRef cfString = CFSTR("I am a CF string.");
//转换成cocoa字符串要作所有权转移。实际是对CF下的cfString做了一个release操作
NSString *cocoaStr = (__bridge_transfer NSString *)cfString;
//或者使用CFBridgingRelease函数
CFStringRef cocoaStr1 = CFBridgingRelease(cfString);//CF下使用必有一个retain与之对应
思考题
ARC给我们带来了很大的遍历。但这样就一定不会出现内存相关的问题吗?
--------------------------------------------------------------2019夜
记录的意义
不是所有的总结与记录都有收获
但毫无疑问
我们会在总结中慢慢成长
那些不经意间的知识点将逐渐扎根
挥之不去
你的理解
亦会愈来愈深刻
这才是记录的意义
-----------------------------非著名八线互联网九流程序猿 chaors
网友评论