1.趣味思考
ARC机制下对象指针有三种修饰符,分别是__ strong、__ autoreleasing、__ weak。__ autoreleasing大家见的较少,我在这里大概说明关键字的用法,使用具体将会在后面讲到。
__autoreleasing
关键字修饰的对象指针,编译器将会通过objc_retainAutoreleasedReturnValue(id obj)
方法进行‘增加对象计数和调用autorelease将指针值放入autorelease pool中’这两个操作。__weak
修饰符修饰的对象指针是弱持有,不会导致引用计数的变化,在对象被dealloc
时指针被置为nil
,可以避免循环引用。__strong
修饰符修饰的对象指针是强持有,会导致引用计数的变化,持有时retainCount+1,放弃持有时retainCount-1。
分享之前我们先进行趣味思考,看一段代码,预测输出
其中的_objc_autoreleasePoolPrint();
是NSObject.mm中的公开方法,通过调用autorelease pool提供的printAll()方法打印所有的自动释放池内容。autorelease pool逻辑上是大小可变的栈式数据结构,储存对象指针,如图
物理实现上是通过4KB页表加双链表链接实现autorelease pool,如下图。物理实现在图中一目了然,牺牲部分空间储存连接的双向指针及一些特征值。满了就申请一页内存扩展栈空间,供继续压栈使用;当一个autorelease pool drain时,弹出该pool所有对象指针(POOL_SENTINEL
指示一个pool的起始位置,作为弹栈结束指示)。当然autorelease pool中考虑了如果弹栈后hot页(即压栈会用到的页)占用超过一半,原来增长的栈空间不会全部release,而是会预留一个页以免即将遇到的再次申请内存状况带来的无谓消耗。
代码如下
#import
extern void _objc_autoreleasePoolPrint(void);
NSMutableString * test(){
return [[NSMutableString alloc] initWithFormat:@"hello every body"];
}
int main(int argc, const char * argv[]) {
NSMutableString * st = test();
_objc_autoreleasePoolPrint();
NSMutableString * __weak stWeak = st; //块外__weak
_objc_autoreleasePoolPrint();
@autoreleasepool{
NSMutableString * __weak stInWeak = st; //块内__weak
_objc_autoreleasePoolPrint();
NSMutableString * __autoreleasing stInAuto = st; //块内__autoreleasing
_objc_autoreleasePoolPrint();
NSMutableString * __strong stInStrong = st; //块内__strong
_objc_autoreleasePoolPrint();
NSMutableString * stNoModif = st; //块内无修饰符
_objc_autoreleasePoolPrint();
NSLog(@"%@",st);
NSLog(@"%@",stWeak);
NSLog(@"%@",stInWeak);
NSLog(@"%@",stInAuto);
NSLog(@"%@",stInStrong);
NSLog(@"%@",stNoModif);
_objc_autoreleasePoolPrint();
}
_objc_autoreleasePoolPrint();
[[NSRunLoop currentRunLoop] run];
return 0;
}
大家可以思考一下,每次打印autorelease pool出来的对象指针数量及原因,(5-10min)
答案如下
objc[2934]: ##############
objc[2934]: AUTORELEASE POOLS for thread 0x1009ba340
objc[2934]: 1 releases pending.
objc[2934]: [0x101007000] ................ PAGE (hot) (cold)
objc[2934]: [0x101007038] 0x100c4df10 __NSCFString
objc[2934]: ##############
objc[2934]: ##############
objc[2934]: AUTORELEASE POOLS for thread 0x1009ba340
objc[2934]: 1 releases pending.
objc[2934]: [0x101007000] ................ PAGE (hot) (cold)
objc[2934]: [0x101007038] 0x100c4df10 __NSCFString
objc[2934]: ##############
objc[2934]: ##############
objc[2934]: AUTORELEASE POOLS for thread 0x1009ba340
objc[2934]: 2 releases pending.
objc[2934]: [0x101007000] ................ PAGE (hot) (cold)
objc[2934]: [0x101007038] 0x100c4df10 __NSCFString
objc[2934]: [0x101007040] ################ POOL 0x101007040
objc[2934]: ##############
objc[2934]: ##############
objc[2934]: AUTORELEASE POOLS for thread 0x1009ba340
objc[2934]: 3 releases pending.
objc[2934]: [0x101007000] ................ PAGE (hot) (cold)
objc[2934]: [0x101007038] 0x100c4df10 __NSCFString
objc[2934]: [0x101007040] ################ POOL 0x101007040
objc[2934]: [0x101007048] 0x100c4df10 __NSCFString
objc[2934]: ##############
objc[2934]: ##############
objc[2934]: AUTORELEASE POOLS for thread 0x1009ba340
objc[2934]: 3 releases pending.
objc[2934]: [0x101007000] ................ PAGE (hot) (cold)
objc[2934]: [0x101007038] 0x100c4df10 __NSCFString
objc[2934]: [0x101007040] ################ POOL 0x101007040
objc[2934]: [0x101007048] 0x100c4df10 __NSCFString
objc[2934]: ##############
objc[2934]: ##############
objc[2934]: AUTORELEASE POOLS for thread 0x1009ba340
objc[2934]: 3 releases pending.
objc[2934]: [0x101007000] ................ PAGE (hot) (cold)
objc[2934]: [0x101007038] 0x100c4df10 __NSCFString
objc[2934]: [0x101007040] ################ POOL 0x101007040
objc[2934]: [0x101007048] 0x100c4df10 __NSCFString
objc[2934]: ##############
2018-04-15 01:11:21.034998+0800 debug-objc[2934:268908] hello every body
2018-04-15 01:11:21.035301+0800 debug-objc[2934:268908] hello every body
2018-04-15 01:11:21.035331+0800 debug-objc[2934:268908] hello every body
2018-04-15 01:11:21.035348+0800 debug-objc[2934:268908] hello every body
2018-04-15 01:11:21.035361+0800 debug-objc[2934:268908] hello every body
2018-04-15 01:11:21.035369+0800 debug-objc[2934:268908] hello every body
objc[2934]: ##############
objc[2934]: AUTORELEASE POOLS for thread 0x1009ba340
objc[2934]: 3 releases pending.
objc[2934]: [0x101007000] ................ PAGE (hot) (cold)
objc[2934]: [0x101007038] 0x100c4df10 __NSCFString
objc[2934]: [0x101007040] ################ POOL 0x101007040
objc[2934]: [0x101007048] 0x100c4df10 __NSCFString
objc[2934]: ##############
objc[2934]: ##############
objc[2934]: AUTORELEASE POOLS for thread 0x1009ba340
objc[2934]: 1 releases pending.
objc[2934]: [0x101007000] ................ PAGE (hot) (cold)
objc[2934]: [0x101007038] 0x100c4df10 __NSCFString
objc[2934]: ##############
与大家想的是否有出入呢,有出入可以自己先思考下。在分析代码之前我们快速过一下ARC机制的一些知识,过完相信大家会知晓答案如此产生的过程。
2.ARC下的内存管理关键字
(1)引用计数机制与__ strong
OC的堆区对象生成后是由对象指针所持有,通过对象指针进行方法访问,程序通过对象指针进行对象的处理。所以在多个对象指针持有对象的情况下,必须要确定释放对象的正确时机。
必须有一种机制来记录其持有者数量,当持有者数量为0时释放对象。在OC中这一方式称为引用计数机制,即记录持有该对象的对象指针变量数目(retainCount)。每个对象指针拥有对象通过给对象的retainCount
+1确保自己拥有对象(即retain
方法),通过给对象的retainCount
-1确保放弃对象的所有权(即`release方法)。显然retain与release必须成对出现,进行对象指针的声明持有与放弃持有操作。OC通过一个sideTableMap的哈希表来实现引用计数的管理。key是对象指针,值是retainCount。导致计数值改变的对象指针我们称为强持有,即__ strong 来修饰,对象指针默认无修饰情况下__ strong。最后引用计数为0,释放对象的工作也由release方法中调用dealloc完成。
(2)__weak的使用
考虑到两个对象互相引用的情况,通过__ weak来避免无法释放对象的情况。即分为__ strong和__ weak两种,__ weak修饰的指针不占用引用计数,即没有所有权。在对象释放后会置weak 指针为nil,也是通过一个哈希表来进行查找置nil。key是对象指针,找到该对象对应的所有weak变量。
(3)__autoreleasing的使用
再考虑另一种情况,在引用计数机制出现里我们提到,对象指针持有对象必须保证retain和release的成对进行,即我声明了所有权也该我放弃所有权,这样能保证引用计数的正确性,那么考虑不能及时释放的情况,这种情况多是夸作用域需要担忧的。
NSMutableString * test(){
NSMutableString * st = [[NSMutableString alloc] initWithFormat:@"hello every body"];
return st;
}
main(){
NSMutableString * s = test();
}
这里我们申请了一个NSMutableString对象,并且返回了这一对象指针。首先st作为无关键字修饰的指针,根据原则为__strong类型,而返回的st实质是对st复制的临时变量,我们标记为obj
,也会指向对象。
st指针在return st;
语句后作用域结束,生命周期结束前按照管理原则会进行[st release]将retainCount-1表明所有者-1,放弃所有权。但是问题来了,我们显然要保留对象给调用者使用,所以我们复制的临时变量obj
会增加计数值,表明自己持有对象,即[obj retain]。但是接着出现的问题是,无名临时变量obj
在返回给调用者后即消亡,无法在消亡前进行[obj release]放弃所有权的责任。需要有一种机制帮其实现[obj release]使得引用计数-1,放弃所有权,这就是autorelease pool的意义。只要将obj压入autorelease pool,这时obj只进行了声明所有权,即retainCount+1的操作;而本该它完成的放弃所有权,即retainCount-1的任务交由autorelease pool压入的对象指针进行,进行时机是autorelease pool生命周期结束时,依次弹栈,并向pool内对象指针调用release方法。
编译器发现return st;这种返回对象指针变量时,所返回的无名对象指针变量实际会进行类似如下处理。
NSMutableString * test(){
NSMutableString * st = [[NSMutableString alloc] initWithFormat:@"hello every body"];
NSMutableString * __autoreleasing obj = st;
return obj;
}
__ autoreleasing关键字,编译器遇到它会调用[obj objc_retainAutorelease];
这个方法等同于[obj retain]+[obj autorelease];即无名临时变量的声明所有权+压入autorelease pool让其代替进行放弃所有权的责任。
至于对象指针指向其他对象时的行为
//强引用两种
str = obj; //其他对象地址值
str = str; //warning:自赋值操作
//赋值会调用NSObject.mm中的objc_storeStrong,
//其中location是指向该对象指针的指针,obj为新对象地址
//它自己用了strong指针对象变量的指针,调用retain,release进行修改
void
objc_storeStrong(id *location/*对象指针变量*/, id obj)
{
id prev = *location;
if (obj == prev) { //自赋值检测
return;
}
objc_retain(obj); //声明对新对象所有权
*location = obj; //指向新对象
objc_release(prev); //放弃原对象所有权
}
//弱引用两种,自赋值和他值都是修改weakTable为新对象dealloc时置nil。
//autoreleasing两种,不管是否相同,增加所有权计数,压入release pool托管release责任。
所以由autorelease pool托管release责任,解决了retain/release出现跨作用域分隔的问题。通过autorelease pool托管放弃所有权这一步,看起来解决了计数机制的返回对象指针的不成对操作(只retain,没release)问题,但是显然autorelease pool如果释放的迟的话会导致对象的延迟释放。所以我们很少见到作用域内显示使用__ autoreleasing,因为在作用域内,默认的__ strong对象指针能在作用域结束及时release,下面会举一个例子
(4)autorelease pool的使用
autorelease pool负责将不在同一个作用域内的对象指针变量retain/release中的release方法托管,在自身结束后延迟释放。
先看常见的错误代码示范,这一例子我们常见于举例何时使用@autoreleasepool{}块这样的问题中
- (void)useALoadOfNumbers {
for (int j = 0; j < 100000; ++j) {
@autoreleasepool {
for (int i = 0; i < 100; ++i) {
//NSNumber *number = [[NSNumber alloc] initWithInt:(i+j)];
//NSLog(@"number = %p", number);
}
}
}
}
在这样的示例代码中,很多人常常会加一句这样能够避免内存峰值过高的风险,really?显然不是,即使你注释掉@autoreleasepool{}也是基本相同的内存占用,为什么?(可以思考下,1-2min)
看了之前的铺垫内容,我们可能会这样说,答案很简单,number无关键字修饰,编译器默认处理为__ strong,在内层for(){}作用域结束,number的生命周期结束,显然ARC会帮我们插入[number release]放弃所有权,这时引用计数为0,release中调用dealloc方法释放内存,所以不会有所谓的峰值过高风险。那什么时候有。我们只要稍加修改代码
- (void)useALoadOfNumbers {
for (int j = 0; j < 100000; ++j) {
@autoreleasepool
{
for (int i = 0; i < 100; ++i) {
NSMutableString * test = [NSMutableString stringWithFormat:@"hello every body"];; //改为类方法
NSLog(@"%2", test);
// NSNumber *number = [NSNumber numberWithInt:(i+j)]; //改为类方法
// NSLog(@"number = %p", number);
}
}
}
}
等等,这里我们为何注释了NSNumber对象类型?原因一会就讲,这里先略过,在新代码中如果注释掉@autoreleasepool{}块就该会出现所预料的内存峰值,而保留则不会。
原因何在?我们看到我们的对象是通过类方法调用返回对象指针,而类方法实际是通过函数返回对象指针方式,示例如下
+ (id) array
{
return [[NSMutableArray alloc] init];
}
如我们所说,遇到return对象指针编译器会生成一个__ autoreleasing 无名临时变量,调用[obj objc_retainAutorelease];。
现在我们来看替换对象类型的原因,我们得注意不是所有的对象都生成在堆区。所以有些人的示例代码也是错的,即使是大牛,比如在StackOverflow一个问题,[Why is @autoreleasepool still needed with ARC?]上,我们找到了《Effective Objective-C 2.0》作者mattjgalloway的回答[mattjgalloway's answer],方便查看复制如下。
An example of using an auto release pool:
- (void)useALoadOfNumbers { for (int j = 0; j < 10000; ++j) { @autoreleasepool { for (int i = 0; i < 10000; ++i) { NSNumber *number = [NSNumber numberWithInt:(i+j)]; NSLog(@"number = %p", number); } } } }
其实这并不会使得内存出现峰值,为何?因为一个8字节指针内部有很多的闲置bit,利用taggedpointer机制,NSNumber对象会被处理到指针中,成为存活在栈的假对象,有关利用taggedpointer优化的知识在这里就不深入了。这也是我们为何在演示中替换为NSMutableString的原因。输出结果给大家看下来作为佐证。
2018-04-16 10:50:20.315737+0800 test[1360:43066] number = 0x27
2018-04-16 10:50:20.315758+0800 test[1360:43066] number = 0x127
2018-04-16 10:50:20.315773+0800 test[1360:43066] number = 0x227
2018-04-16 10:50:20.315785+0800 test[1360:43066] number = 0x327
2018-04-16 10:50:20.315797+0800 test[1360:43066] number = 0x427
2018-04-16 10:50:20.315809+0800 test[1360:43066] number = 0x527
2018-04-16 10:50:20.315821+0800 test[1360:43066] number = 0x627
2018-04-16 10:50:20.315833+0800 test[1360:43066] number = 0x727
2018-04-16 10:50:20.315856+0800 test[1360:43066] number = 0x827
2018-04-16 10:50:20.315887+0800 test[1360:43066] number = 0x927
2018-04-16 10:50:20.315903+0800 test[1360:43066] number = 0xa27
地址中存了值的假对象就露出原形了。
2.总结流程
(1)__strong * objcStrong:
声明所有权:
[objcStrong retain] —— > self->rootretain() —— > sideTable(self)通过哈希表找到/添加引用计数
— — >retainCount++ ;
放弃所有权:
[objcStrong release] —— > self->rootRelease() — — >sideTable(self)通过哈希表找到引用计数
— — >retainCount -1 — — >if(retainCount)判定计数值— — > dealloc(self) 释放对象
— — >table.refcnts.erase();//删除引用计数器
(2)__weak * objcWeak
注册到哈希表:
storeWeak(&objc, newObj)将弱引用变量地址存入哈希表— — > sideTable(newObj) 找到对象的弱引
用指针地址表位置— — >weak_register_no_lock(*, newObj, &objc, *);注册弱引用指针位置
对象释放时:
dealloc释放对象 — — >weak_clear_no_lock(weak_table_t *weak_table, id referent_id);查找对象的弱引用数组并置弱引用指针nil — — >weak_entry_remove(weak_table, entry);删除对象对于弱引用表
(3)__autorelease *objcAuto
声明所有权并托管释放权:
objc_retainAutoreleasedReturnValue(id objcAuto) — — >[objcAuto retain]增加引用计数
— — > [obj autorelease]调用自动释放方法 — —> poolPage -> push(objcAuto);压入autoreleasepool
3.内存管理小结:
retainCount的本质是记录调用retain的次数,所以确保引用计数与所有者一致必须确保release/retain成对出现。且是在对对象指针赋值时,根据修饰符为__ strong(copy时也是__ strong)、__ autoreleasing、__ weak做出不同的处理,而与对象的生成阶段(即与alloc/new/copy)无关,当然另一个值得注意的是生成的对象未必在堆区,这也是MRC下retainCount出现-1的原因。
-
对于无法及时插入release方法的,通过自动释放池autorelease pool代为release,确保release/retain的成对操作。
来不及调用release就消亡,如函数返回对象指针,包含了类方法,autorelease pool帮助它放弃所有权。
@autoreleasepool{}的使用场景在于大量生成autorelease对象于某一作用域。如如上提到的两种类型。因为每次[obj autorelease]进入的都是顶层pool,合理的插入@autoreleasepool{}能够相对提前放弃我们autorelease对象的所有权,避免内存峰值。而消耗就是存几个对象指针,满的时候也就再多一个4KB的page,获得却很大。
ARC下,编译器帮我们做了前两步,最后一步的@autoreleasepool{}提前释放自己{}内的autorelease对象需要我们自己去取舍处理。另一方面为何苹果公司不推荐使用如类方法这种便利方法的原因,也是其为函数返回对象指针的早夭性质存入autorelease pool,导致延迟释放。但是@autoreleasepool也存在风险,如我们提到的,我们会考虑用autorelease pool去解决对象指针早早抛弃对象离去来不及release的问题,那么一但autorelease pool比我们的__ autoreleasing对象指针先结束,则可能因为对象消亡会遇到bad access的运行错误。
如下面代码编译有无问题,运行有无问题?
#import
extern void _objc_autoreleasePoolPrint(void);
int main()
{
NSMutableString __autoreleasing * autorePointer = nil;
NSMutableString __strong * strongPointer = nil;
NSLog(@"autorePointer is :%p",autorePointer);
_objc_autoreleasePoolPrint();
@autoreleasepool{
_objc_autoreleasePoolPrint(); //输出结果及原因,问题1
NSMutableString * a = [[NSMutableString alloc] initWithFormat:@"hello every body"];
strongPointer = [[NSMutableString alloc] initWithFormat:@"大吉大利,今晚吃鸡"];
autorePointer = a;
NSLog(@"strongPointer address is :%p",autorePointer);
NSLog(@"autorePointer address is :%p",autorePointer);
NSLog(@"%@",strongPointer);
_objc_autoreleasePoolPrint();
}
NSLog(@"%@",strongPointer);
NSLog(@"autorePointer address is :%p",autorePointer);
_objc_autoreleasePoolPrint();
NSUInteger len = [strongPointer length];
NSLog(@"%lu",len);
len = [autorePointer length];
NSLog(@"%lu",len);
return 0;
}
这题还是很有趣的,有一个小点会考察到永昌昨天分享的知识点,即问题1的疑问,答案是当没有真正的对象指针压入时是不会生成栈,这是一种内存优化。所问题1打印【 0 releases pending】,压入autorePointer打印【 2 releases pending】
答案如下
运行会大概率BAD ACCESS错误,Thread 1: EXC_BAD_ACCESS (code=1, address=0x10049690),出现再NSUInteger len = [autorePointer length];
autorePointer is :0x0
objc[4493]: ##############
objc[4493]: 0 releases pending.
objc[4493]: ##############
objc[4493]: ##############
objc[4493]: 0 releases pending.
objc[4493]: ##############
strongPointer address is :0x10043e2b0
autorePointer address is :0x10043e2b0
大吉大利,今晚吃鸡
objc[4493]: ##############
objc[4493]: 2 releases pending.
objc[4493]: ##############
大吉大利,今晚吃鸡
autorePointer address is :0x10043e2b0
objc[4493]: ##############
objc[4493]: 0 releases pending.
objc[4493]: ##############
Thread 1: EXC_BAD_ACCESS (code=1, address=0x18)//出现在[autorePointer length];
总的来说我们极少需要使用@autoreleasepool{}去对内存进行托管释放,分散开的少量的类方法对象进入runloop的自动释放池,在每次runloop更新中释放,大量集中的autorelease对象才会使用到pool块,但是例如遇到上面的bad access问题时候我们应该知道为何如此。
值得注意的是当我们用__weak对象指针进行某些操作时,会调用到objc_loadWeak(id *location);导致出现增加引用计数和autorelease pool托管释放的的情况。
id
objc_loadWeak(id *location)
{
if (!*location) return nil;
return objc_autorelease(objc_loadWeakRetained(location));
}
通过runtime,对此调用的,caller只有NSObject.mm提供的接口里object_getIvar()直接调用该方法,object_getInstanceVariable()通过object_getIvar()间接调用,别的没有caller。方法作用是获取类实例对象中的实例变量的值,具体使用未进行探究,做个标记。
以上就是ARC内存管理的浅析,个人理解,希望大家纠错补充。