怎么说呢。经典再放送咯。
对象操作 | 对应的Objective-C方法 |
---|---|
生成并持有对象 | alloc/new/copy/mutableCopy等方法 |
持有对象 | retain方法 |
释放对象 | release方法 |
废弃对象 | dealloc方法 |
iOS内存管理方案有三种,我们详细看下每种方案的实现及存在的意义。
标签指针
没有这种管理机制会引起内存浪费,为什么呢?
我们来看下,假设我们要存储一个NSNumber
对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger
的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。
所以一个普通的iOS程序,如果没有Tagged Pointer
对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate
一类的对象所占用的内存会翻倍。如下图所示:
我们再来看看效率上的问题,为了存储和访问一个NSNumber
对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。
为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer
对象。由于NSNumber
、NSDate
一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。
所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer
对象之后,64位CPU下NSNumber
的内存图变成了以下这样:
当8字节可以承载用于表示的数值时,系统就会以Tagged Pointer
的方式生成指针,如果8字节承载不了时,则又用以前的方式来生成普通的指针。以上是关于Tag Pointer
的存储细节。
Tagged Pointer的特点:
Tagged Pointer
特点的介绍:Tagged Pointer
专门用来存储小的对象,例如NSNumber
和NSDate
, 当然NSString
小于60字节的也可以运用了该手段Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已,因为他没有isa
指针。所以,它的内存并不存储在堆中,也不需要malloc
和free
。由此可见,苹果引入Tagged Pointer
,不但减少了64位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。
在64位系统上只需要32位来储存内存地址,而剩下的32位就可以用来做其他的内存管理
non_pointer iSA 的判断条件:
1 : 包含swift代码;
2:sdk版本低于10.11;
3:runtime
读取image时发现这个image包含__objc_rawi sa
段;
4:开发者自己添加了OBJC_DISABLE_NONPOINTER_ISA=YES
到环境变量中;
5:某些不能使用Non-pointer
的类,GCD等;
6:父类关闭。
为了管理所有对象的引用计数和weak
指针,苹果创建了一个全局的SideTables
,虽然名字后面有个"s"不过他其实是一个全局的Hash表
,里面的内容装的都是SideTable
结构体而已。它使用对象的内存地址当它的key
。管理引用计数和weak
指针就靠它了。
因为对象引用计数相关操作应该是原子性的。不然如果多个线程同时去写一个对象的引用计数,那就会造成数据错乱,失去了内存管理的意义。同时又因为内存中对象的数量是非常非常庞大的需要非常频繁的操作SideTables
,所以不能对整个Hash表
加锁。苹果采用了分离锁技术。
下边是SideTabel
的定义:
SideTable
struct SideTable {
//锁
spinlock_t slock;
//强引用相关
RefcountMap refcnts;
//弱引用相关
weak_table_t weak_table;
...
}
当我们通过SideTables[key]
来得到SideTable
的时候,SideTable
的结构如下:
1、一把自旋锁。spinlock_t slock;
自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。
它的作用是在操作引用技术的时候对SideTable
加锁,避免数据错误。
苹果在对锁的选择上可以说是精益求精。苹果知道对于引用计数的操作其实是非常快的。所以选择了虽然不是那么高级但是确实效率高的自旋锁。
2、引用计数器 RefcountMap * refcnts;
对象具体的引用计数数量是记录在这里的。
这里注意RefcountMap
其实是个C++的Map。为什么Hash以后还需要个Map呢?因为内存中对象的数量实在是太庞大了我们通过第一个Hash表只是过滤了第一次,然后我们还需要再通过这个Map才能精确的定位到我们要找的对象的引用计数器。
引用计数器的数据类型是:
typedef __darwin_size_t size_t;
再进一步看它的定义其实是unsigned long
,在32位和64位操作系统中,它分别占用32和64个bit。
苹果经常使用bit mask
技术。这里也不例外。拿32位系统为例的话,可以理解成有32个盒子排成一排横着放在你面前。盒子里可以装0或者1两个数字。我们规定最后边的盒子是低位,左边的盒子是高位。
(1UL<<0)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动0位(就是原地不动):0b0000 0000 0000 0000 0000 0000 0000 0001
(1UL<<1)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动1位:0b0000 0000 0000 0000 0000 0000 0000 0010
下面来分析引用计数器(图中右侧)的结构,从低位到高位。
(1UL<<0)???WEAKLY_REFERENCED
表示是否有弱引用指向这个对象,如果有的话(值为1)在对象释放的时候需要把所有指向它的弱引用都变成nil(相当于其他语言的NULL),避免野指针错误。
(1UL<<1)???DEALLOCATING
表示对象是否正在被释放。1正在释放,0没有
(1UL<<(WORD_BITS-1))???SIDE_TABLE_RC_PINNED
其中WORD_BITS在32位和64位系统的时候分别等于32和64。其实这一位没啥具体意义,就是随着对象的引用计数不断变大。如果这一位都变成1了,就表示引用计数已经最大了不能再增加了。
3、维护weak指针的结构体 weak_table_t * weak_table;
第一层结构体中包含两个元素。
第一个元素weak_entry_t *weak_entries
;是一个数组,上面RefcountMap
是要通过find(key)
来找到精确的元素的。weak_entries
则是通过循环遍历来找到对应的entry
。
(上面管理引用计数器苹果使用的是Map,这里管理weak指针苹果使用的是数组,有兴趣的朋友可以思考一下为什么苹果会分别采用这两种不同的结构)
这个是因为weak
的显著的特征来决定的: 当weak
对象被销毁的时候,要把所有指向该对象的指针都设为nil。
第二个元素num_entries
是用来维护保证数组始终有一个合适的size。比如数组中元素的数量超过3/4的时候将数组的大小乘以2。
第二层weak_entry_t
的结构包含3个部分:
1、referent:被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。
2、referrers:可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。
3、inline_referrers只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。
上面我们介绍了苹果为了更好的内存管理使用的三种不同的内存管理方案,在内部采用了不同的数据结构以达到更高效内存检索。
__strong
修饰符是id
类型和对象类型默认的所有权修饰符。
不论调用哪种方法,强引用修饰的变量会持有该对象,如果已经持有则引用计数不会增加。
__strong
对象相互赋值__strong
修饰符的变量不仅只在变量作用域中,在赋值上也能够正确的管理其对象的所有者。
id __strong obj0 = [[NSObject alloc] init];//生成对象A
id __strong obj1 = [[NSObject alloc] init];//生成对象B
id __strong obj2 = nil;
obj0 = obj1;//obj0强引用对象B;而对象A不再被ojb0引用,被废弃
obj2 = obj0;//obj2强引用对象B(现在obj0,ojb1,obj2都强引用对象B)
obj1 = nil;//obj1不再强引用对象B
obj0 = nil;//obj0不再强引用对象B
obj2 = nil;//obj2不再强引用对象B,不再有任何强引用引用对象B,对象B被废弃
__strong
废弃Test对象的同时,Test对象的obj_成员变量也被废除
即成员变量的生存周期是与对象同步的。
__strong
导致的循环引用
循环引用就是两个对象相互引用导致在该释放的时候没有释放,从而一直占着内存导致内存泄漏
内存泄漏:在内存该被释放的时候没有释放,导致内存被浪费使用了
举个例子:
#import <Foundation/Foundation.h>
@interface StrongTest : NSObject {
id __strong _obj;
}
- (void) setObject:(id __strong) obj;
@end
#import "StrongTest.h"
@implementation StrongTest
- (void) setObject:(id)obj {
_obj = obj;
}
@end
main函数:
#import <Foundation/Foundation.h>
#import "StrongTest.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
StrongTest *testFirst = [[StrongTest alloc] init];//生成Test1
StrongTest *testSecond = [[StrongTest alloc] init];//生成Test2
[testFirst setObject:testSecond];
[testSecond setObject:testFirst];
//打印一下引用计数值
NSLog(@"%lu", CFGetRetainCount((__bridge CFTypeRef)testFirst));
}
return 0;
}
我们创建了两个对象testFirst
和testSecond
每个对象内部都有一个成员变量_obj
其中一个对象的_obj
持有另一个对象
最后就会造成下面的结果:
testFirst
持有Test1
,testFirst.obj
持有Test2
,testSecond
持有Test2
,testSecond.obj
持有Test1
失效阶段的表示:
what happen | Test1引用计数 | Test1持有者 | Test2引用计数 | Test2持有者 |
---|---|---|---|---|
初始状态 | 2 | testFirst, testSecond.obj | 2 | testFirst.obj, testSecond |
testFirst超出作用域 | 1 | testSecond.obj | 2 | testFirst.obj, testSecond |
testSecond超出作用域 | 1 | testSecond.obj | 1 | testFirst.obj |
这样一来,即使两个对象的指针都超出作用域,由于其中彼此的成员变量相互持有彼此的对象而造成循环引用。
__weak
弱引用(引用计数不会加一 对象随时可能会被dealloc)
内部使用__autoreleasing
来维持该对象不被dealloc
(这个也是__weak
修饰符修饰的对象在所指向的对象销毁之后会自动指向nil
的关键所在)
对象的引用计数是记录在一张表上的,不在对象本身或者指针中,系统通过访问这张表来确定是否释放该对象。
将上面相互引用例子中的成员变量变为weak,即可避免相互引用。
weak还有个作用。在持有某对象的弱引用时,若该对象被废弃,则此若引用将自动失效且处于nil
被赋值的状态(空弱引用)。
weak提供弱引用,弱引用不持有对象,NSObject对象会被销毁,所以会报一个警告
我们可以这样使用__weak修饰符的变量,将__strong修饰的对象赋值给__weak修饰的对象,这样就不会发生警告了:
id __strong objTest = [[NSObject alloc] init];
id __weak objTestSecond = objTest;
类似于“强弱共舞”
按以下例子,我们来看一下这两个的引用计数的值:
id __strong obj = [[NSObject alloc] init];
id __weak objTest = obj;
NSLog(@"%lu", CFGetRetainCount((__bridge CFTypeRef)obj));
NSLog(@"%lu", CFGetRetainCount((__bridge CFTypeRef)objTest));
按照常理来说应该是1 1,因为__weak修饰符不持有对象,引用计数值两个都应该是1
打印一下结果:
对比一下就能发现 两行NSLog
的汇编代码并不一样
第二个__weak
修饰符的NSLog
在于开始先loadWeakRetained
然后在打印结束后有一个release操作
在打印__weak
的引用计数时NSLog
先将其以强引用,防止没有打印就释放掉了造成程序的崩溃,在NSLog
结束时,会调用objc_release
使引用计数减一。
__unsafe_unretained修饰符正如其名unsafe所示,是不安全的所有权修饰符。
附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。
为什么是不安全的呢
weak 修饰的指针变量,在指向的内存地址销毁后,会在 Runtime 的机制下,自动置为 nil。 _unsafe_unretain
不会置为 nil
,容易出现 悬垂指针,发生崩溃。但是 __unsafe_unretain
比 __weak
效率高。
悬垂指针 指针指向的内存已经被释放了,但是指针还存在,这就是一个 悬垂指针 或者说 迷途指针。野指针,没有进行初始化的指针,其实都是 野指针
附有__unsafe_unretained
修饰符的变量同附有__weak
修饰符的变量一样,生成的对象会立即释放。
在使用__unsafe_unretained
修饰符时,赋值给附有__strong
修饰符的变量时有必要确保被赋值的对象确实存在,如果不存在,那么程序就会崩溃。
与MRC进行比较:
MRC中autorelease的使用方法:
- 生成并持有
NSAutoreleasePool
对象- 调用已分配对象的
autorelease
方法(将对象注册到pool
中)- 废弃
NSAutoreleasePool
对象
在ARC环境下:
NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init]; 1⃣️
[obj autorelease]; 2⃣️
[pool drain]; 3⃣️
等价于:
@autoreleasepool{ 1⃣️
id __autorelease obj2; obj2 = obj; 2⃣️
} 3⃣️
自动调用:
编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是讲自动将返回值的对象注册到autoreleasepool中
下面情况不使用__autoreleasing修饰符也能使对象注册到autoreleasepool中。
+ (id) array {
return [[NSMutableArray alloc]init];
}
//如下:
+ (id) array {
id obj = [[NSMutableArray alloc]init];
return obj;
}
由于return使得对象变量超出其作用域,所以该强引用对应的自己持有的对象会被自动释放,但该对象作为函数的返回值,编译器也会自动注册到自动释放池
自动调用时的失效过程:
随着obj超出其作用域,强引用失效,所以自动释放自己持有的对象。
同时,随着@autoreleasepool块的结束,注册到autoreleasepool中的所有对象被自动释放。 因为对象的拥有者不存在,所以废弃对象。
weak修饰符与autoreleasing修饰符:
访问__weak修饰符的变量时必须访问注册到autoreleasepool的对象呢,这是因为__weak修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃。如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象存在,因此,在使用附有__weak修饰符的变量时就必定要使用注册到autoreleasepool中的对象。
id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]);
与以下源代码相同
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@", [temp class]);
例1
//这个例子中obj0没有加入到自动释放池中
id obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]);
池中没有出现obj0。
例2
//这个例子中obj0加入了自动释放池中
id __autoreleasing obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]);
池中出现了obj0
例3
id __autoreleasing obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
id __autoreleasing obj2 = obj1;
NSLog(@"class = %@", [obj2 class]);
池中出现了obj0,且看到count为2
例4
id obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
id __autoreleasing obj2 = obj1;
NSLog(@"class = %@", [obj2 class]);
池中出现了obj0
总结: __weak修饰符并不会将对象加入到自动释放池,但是我们使用__weak修饰的对象一定要是本身已经加入到自动释放池的或者后续使用__autoreleasing将__weak所修饰的对象加入释放池
retain/release/retainCount/autorelease
NSAllocateObject/NSDeallocateObject
dealloc
重点:
不能显式调用dealloc
dealloc无法释放不属于该对象的一些东西,需要我们重写时加上去,例如:
In the implementation of dealloc, do not call the implementation of superclass. You should try to avoid using dealloc to manage the lifetime of limited resources, such as file descriptors.
You never send a dealloc message directly. Instead, the dealloc method of the object is called by the runtime.
在dealloc的实现中,不要调用超类的实现。您应该尽量避免使用dealloc管理有限资源(如文件描述符)的生存期。
不要直接发送dealloc消息。与直接发送dealloc消息不同,对象的dealloc方法由运行时调用。
Special Considerations
When not using ARC, your implementation of dealloc must invoke the superclass’s implementation as its last instruction.
特别注意事项
当不使用ARC时,dealloc的实现必须调用父类(super)的实现作为它的最后一条指令 [super dealloc]。
bridge
可以实现Objective-C与C语言变量和Objective-C与CoreFoundation对象之间的互相转换_bridge
不会改变对象的持有状况,既不会retain
,也不会release
_bridge
转换需要慎重分析对象的持有情况,稍不注意就会内存泄漏_bridge_retained
用于将口C变量转换为C语言变量或将OC对象转换为CoreFoundation对象_bridge_retained
类似于retain
,“被转换的变量”所持有的对象在变量赋值给"转换目标变量“后持有该对象_bridge_transfer
用于将C语言变量转换为OC变量或将CoreFoundation
对象转换为OC对象bridge_transfer
类似于release
,“被转换的变量”所持有的对象在变量赋值给“转换目标变量”后随衣释热属性关键字 | 所有权修饰符 |
---|---|
assign | _unsafe_unretained |
copy | __strong |
retain | __strong |
strong | strong |
__unsafe_unretained | __unsafe_unretained |
weak | __weak |