内存管理:不看白不看,看了就是赚
一、iOS的内存管理方式
1、小对象的内存管理 -- Tagged Pointer
2、普通对象的内存管理 -- 引用计数
2.1 引用计数是什么
2.2 引用计数存储在哪里
2.3 iOS具体是怎么通过引用计数来进行对象的内存管理的
二、MRC是什么,我们需要做什么
三、ARC是什么,帮我们做了什么
1、指针修饰符
1.1
__strong
指针修饰符1.2
__weak
指针修饰符2、属性修饰符
2.1 原子性——
atomic
、nonatomic
2.2 读写权限——
readwrite
、readonly
2.3 内存管理语义——
assign
、retain
、copy
、strong
、weak
、unsafe_unretained
一、iOS的内存管理方式
iOS的内存管理方式有哪些?iOS针对不同的场景有不同的内存管理方式,主要有以下两种:
- 小对象的内存管理 -- Tagged Pointer
- 普通对象的内存管理 -- 引用计数
1、小对象的内存管理 -- Tagged Pointer
64位操作系统以后,iOS引入了Tagged Pointer,用来优化NSNumber
、NSString
、NSDate
等小对象的内存管理。
引入Tagged Pointer之前,小对象和普通对象一样需要在堆区开辟内存,并把对象的内存地址赋值给相应的指针,然后维护对象的引用计数、释放内存等。比如我们创建一个int
类型的NSNumber
对象:
NSNumber *number = @11;
系统就需要在堆区开辟16个字节的内存来存储“11”这个值,还需要开辟8个字节的内存来存储这个对象的地址,你看本来最多占4个字节的int
类型数据就至少占了24个字节,太浪费内存了,而且这还没考虑维护对象的引用计数、释放内存等开销。
引入Tagged Pointer之后,小对象就不再需要像普通对象那样在堆区开辟内存、维护引用计数、释放内存了,而是直接把值存进number
指针里,也就是说number
指针这8个字节里存储的不再是一个地址了,而是:Tag + Data,Tag用来标记小对象是什么类型(如NSNumber
、NSString
、NSDate
等),Data就是小对象的值,指针什么时候创建小对象就什么时候创建,指针什么时候销毁小对象就什么时候销毁。除非小对象的值大到指针存不下,小对象才会恢复为原来普通对象那样的内存管理方式。这样系统就只需要开辟8个字节的内存来存储小对象的类型和值,很大程度上节省了内存,也减小了维护对象的引用计数、释放内存等开销。
我们举例来验证一下:
- (void)viewDidLoad {
[super viewDidLoad];
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *number4 = @10;
NSNumber *number5 = @11;
NSNumber *number6 = @12;
NSNumber *number7 = @(0xFFFFFFFFFFFFFFF);
NSLog(@"number1:%p", number1);
NSLog(@"number2:%p", number2);
NSLog(@"number3:%p", number3);
NSLog(@"number4:%p", number4);
NSLog(@"number5:%p", number5);
NSLog(@"number6:%p", number6);
NSLog(@"number7:%p", number7);
}
// 控制台打印:
number1:0xb000000000000012
number2:0xb000000000000022
number3:0xb000000000000032
number4:0xb0000000000000a2
number5:0xb0000000000000b2
number6:0xb0000000000000c2
number7:0x174226480
可见number1
~ number6
指针是Tagged Pointer,它们5 ~ 8位的“1”、“2”、“3”、“a”、“b”、“c”分别存储着小对象的值,而61 ~ 64位的“b”和1 ~ 4位的“2”可能就是标记小对象是个NSNumber
类型。而number7
因为值大到指针存不下,恢复为原来普通对象那样的内存管理方式了。(如果一个对象的指针的最高位——即第64位——是1,那么它就是个Tagged Pointer,否则就不是。)
从上面的分析我们知道小对象已经不是一个普通对象了,因为它不像普通对象那样拥有isa
指针来指向它所属的类,那小对象是怎么调用方法的呢?
NSNumber *number = @11;
NSLog(@"%d", [number intValue]);
首先因为number
指针是NSNumber
类型的,所以它调用intValue
方法在编译时是不会报错的。然后在运行时objc_msgSend
函数内部会做判断,如果发现对象是个小对象,就会直接从指针里把数据抽出来或直接把数据存进指针里,而不是走方法调用流程,这也在一定程度上减小了方法调用的开销。
我们举例来验证一下:
// 开辟多个线程去修改name属性
for (int i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.name = [[NSString alloc] initWithFormat:@"abcdefghijk"];
});
}
for (int i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.name = [[NSString alloc] initWithFormat:@"abc"];
});
}
第一段代码会崩掉,因为@"abcdefghijk"
这个NSString
小对象大到指针存不下,恢复为原来普通对象那样的内存管理方式了,所以那句代码就是正常地调用setter
方法来修改name
属性,而setter
方法的内部实现为:
- (void)setName:(NSString *)name {
if (_name != name) { // 新旧对象不一样时
[_name release]; // 释放旧对象
_name = [name copy]; // 复制新对象
}
}
所以在不加锁的情况下,就很有可能有多条线程几乎同时调用[_name release]
,那对一个已经销毁了的_name
再次调用release
,程序当然会崩掉了。
而第二段代码可以正常运行,因为@"abc"
这个NSString
小对象不够大,把它赋值给name
指针时,objc_msgSend
函数内部会直接把它存进name
指针里,而不是走setter
方法。
2、普通对象的内存管理 -- 引用计数
2.1 引用计数是什么
iOS是通过引用计数来进行对象的内存管理的。所谓引用计数是指我们每创建一个对象,系统就会为该对象分配一个整数,用来表征当前有多少人想使用该对象。那这就引出两个问题:
- 创建对象时,系统为对象分配的这个整数存储在哪里呢?也就是说引用计数存储在哪里呢?因为我们知道OC对象内部只有一个固定的成员变量
isa
,并没有一个引用计数的成员变量啊! - iOS具体是怎么通过引用计数来进行对象的内存管理的?
2.2 引用计数存储在哪里
64位操作系统以前,对象的isa
指针还没经过内存优化,对象的引用计数是直接存储在引用计数表里的。(因为现在都是64位操作系统了,所以这种情况我们知道一下但不做过多额外的分析)
64位操作系统以后,对象的isa
指针经过了内存优化,它不再直接是一个指针,而是一个共用体,64位中只有33位用来存储对象所属类的地址信息,还有19位用来存储(对象的引用计数 - 1),还有1位用来标记引用计数表里是否有当前对象的引用计数。具体地说,对象的引用计数会首先存储在它的isa
共用体里——extra_rc
变量,但是isa
共用体里引用计数的存值范围为0~255,一旦对象的引用计数超过了255,这个变量就会溢出,此时系统会把这个变量置为128,会把引用计数表里是否有当前对象的引用计数的标记——has_sidetable_rc
变量——置为1,并把另外的128个引用计数挪到引用计数表里存储。那下一次再增加对象的引用计数时,就依旧增加的是isa
共用体里的引用计数(因为它已经被置为128了,不再是溢出状态了),直到再次溢出,系统再挪128个引用计数到引用计数表里存储,如此循环。因此我们就看到在这种情况下,系统其实是不会直接去操作引用计数表里的引用计数的,而总是在操作isa
共用体里的引用计数,直到溢出时才从isa
共用体里挪128个到引用计数表里存储。
-
isa
共用体
struct objc_object {
isa_t isa; // 一个isa_t类型的共用体
// 自定义的成员变量,存储着该对象这些成员变量具体的值
NSSring *_name; // “张三”
NSSring *_sex; // “男”
int _age; // 33
}
union isa_t {
Class cls;
unsigned long bits; // 8个字节,64位
struct { // 其实所有的数据都存储在成员变量bits里面,因为外界只访问它,而这个结构体则仅仅是用位域来增加代码的可读性,让我们看到bits里面相应的位上存储着谁的数据
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
unsigned long nonpointer : 1; // isa是否经过内存优化
unsigned long has_assoc : 1;
unsigned long has_cxx_dtor : 1;
unsigned long shiftcls : 33; // 对象所属类的地址信息
unsigned long magic : 6;
unsigned long weakly_referenced : 1;
unsigned long deallocating : 1;
unsigned long has_sidetable_rc : 1; // 引用计数表里是否有当前对象的引用计数
unsigned long extra_rc : 19; // 对象的引用计数 - 1
# endif
};
};
-
SideTables
-->SideTable
--> 引用计数表、弱引用表
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
struct SideTable {
spinlock_t slock; // 自旋锁
RefcountMap refcnts; // 引用计数表
weak_table_t weak_table;
}
typedef objc::DenseMap<objc_object */*对象的内存地址*/, unsigned long/*对象的引用计数*/> RefcountMap;
struct weak_table_t {
weak_entry_t *weak_entries; // 这个其实才是弱引用表,表中元素为weak_entry_t结构体
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct weak_entry_t {
objc_object *obj; // 对象的内存地址
weak_referrer_t *referrers; // 指向该对象的弱指针数组——即所有指向该对象的弱指针(其实存储的是弱指针对应那块内存的地址,但是我们直接理解为弱指针是没有问题的)
}
// 例如:
id obj = [[NSObject alloc] init];
__weak id weakObj1 = obj;
__weak id weakObj2 = obj;
__weak id weakObj3 = obj;
// NSObject对象的weak_entry为:
struct weak_entry_t {
objc_object *obj; // 对象的内存地址
weak_referrer_t *referrers; // 指向该对象的弱指针数组
} NSObjectWeakEntry = {
obj;
[weakObj1, weakObj2, weakObj3]
}
SideTables
是一个全局的散列表,它里面存储着64个SideTable
结构体,而每个SideTable
结构体里又存储着1个引用计数表和1个弱引用表,所以项目中一共会有64个引用计数表和64个弱引用表。引用计数表也是一个散列表,表中元素就是一个字典——对象的内存地址为key
,对象的引用计数为value
,引用计数表里面存储着很多对象的引用计数。弱引用表也是一个散列表,表中元素就是一个结构体——一个成员变量是对象的内存地址,另一个成员变量是指向该对象的弱指针数组,弱引用表里面存储着很多对象的弱指针数组。
所以如果我们想要找到对象的引用计数和弱指针数组,就要首先把对象的内存地址通过某种散列算法得到一个index
,就可以在SideTables
里找到对象的引用计数和弱指针数组所在的SideTable
结构体,这也就是找到了对象的引用计数和弱指针数组所在的引用计数表和弱引用表,然后再次把对象的内存地址通过某种散列算法得到一个index
,就可以在引用计数表里找到对象的引用计数,在弱引用表里找到对象的弱指针数组了。
关键词:散列表、表中元素、表中元素唯一标识、散列算法、
index
散列表(Hash Table,也叫哈希表),就是把表中元素的唯一标识通过某种算法得到一个
index
,然后通过这个index
直接访问表中元素的一种数据结构,这样就不用遍历了,因此可以大大提高数据查找的效率。实现这个算法的函数叫作散列函数,存储数据的数组叫作散列表(但这个数组不是普通的数组,它的元素可以不连续存储,因此散列表就有可能造成内存的空闲,它是一个典型的“以空间换时间”的例子)。散列表的核心就在于散列算法。
2.3 iOS具体是怎么通过引用计数来进行对象的内存管理的
主要是通过alloc
、new
、copy
、mutableCopy
,retain
,release
、autorelease
,dealloc
这几个方法操作引用计数,来进行对象的内存管理的,即:
- 我们调用
alloc
、new
、copy
、mutableCopy
新创建一个对象,系统会给它分配一定的内存,对象的引用计数为1。
// NSObject.mm
+ (id)alloc {
return _objc_rootAlloc(self);
}
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
static id callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
return class_createInstance(cls, 0);
}
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
static id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct = true, size_t *outAllocatedSize = nil)
{
// 计算对象需要占用的内存
size_t size = cls->instanceSize(extraBytes);
// 调用calloc函数给对象分配1块size大小的内存
id obj = (id)calloc(1, size);
// 初始化对象的isa
obj->initInstanceIsa(cls, hasCxxDtor);
// 返回对象的内存地址
return obj;
}
size_t instanceSize(size_t extraBytes) {
// alignedInstanceSize = 对象(结构体)经过内存对齐后的实际大小,extraBytes = 0
size_t size = alignedInstanceSize() + extraBytes;
// 系统要求所有的对象至少占用16个字节
if (size < 16) size = 16;
return size;
}
void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
initIsa(cls, true, hasCxxDtor);
}
void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
isa_t newisa(0);
/*
isa赋初值
#define ISA_MAGIC_MASK 0x0000 03f0 0000 0001
转换成二进制0b
64~49位:0000 0000 0000 0000
48~33位:0000 0011 1111 0000
32~17位:0000 0000 0000 0000
16~01位:0000 0000 0000 0001
*/
newisa.bits = ISA_MAGIC_VALUE;
// 标记当前对象是否使用过C++析构函数
newisa.has_cxx_dtor = hasCxxDtor;
// 把cls的值——即对象所属类的地址——右移三位,初始化进isa共用体的shiftcls中
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
// 可见用alloc、new、copy、mutableCopy创建对象时,系统其实并不会把isa共用体里的引用计数置为1(extra_rc在isa的64位~46位上,为0的),所以我们说对象的isa共用体里存储的是(对象的引用计数 - 1)
}
// copy和mutableCopy则需要我们自己遵守NSCopying和NSMutableCopying协议,并实现copyWithZone:和mutableCopyWithZone:方法,它们内部还是会调用alloc那一套方法,不过copy和mutableCopy针对的是一个新对象
// 但其实我们自己定义的类不存在什么mutableCopy,mutableCopy仅仅是给系统NSString、NSArray、NSDictionary等部分类留的
@interface INEPerson () <NSCopying>
@property (nonatomic, copy) NSString *name;
@end
@implementation INEPerson
- (id)copyWithZone:(NSZone *)zone {
INEPerson *person = [[INEPerson allocWithZone:zone] init];
// 一些属性的赋值......
person.name = self.name;
return person;
}
@end
- 如果有别的人想使用该对象,就调用
retain
方法把对象的引用计数+1,即持有该对象。
// NSObject.mm
- (id)retain {
return self->rootRetain();
}
id objc_object::rootRetain()
{
return rootRetain(false, false);
}
id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) { // 如果是Tagged Pointer,它不是通过引用计数来进行内存管理的,所以不需要对引用计数+1,直接返回对象自己
return (id)this;
}
// 拿到对象的isa共用体
isa_t newisa = LoadExclusive(&isa.bits);
if (!newisa.nonpointer) { // 如果对象的isa是没经过内存优化的,那么它的引用计数就直接存储在引用计数表里
// 去引用计数表里让它的引用计数+1
sidetable_retain();
}
// 否则就表明对象的isa是经过内存优化的,那么它的引用计数就首先存储在isa共用体里
// 用来标识extra_rc是否上溢(extra_rc的存值范围为0~255,即最多存储256个引用计数)
uintptr_t carry;
// 首先去isa共用体里,让对象的引用计数+1
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
if (carry) { // 所以如果extra_rc上溢了
// 就保留一半的引用计数——即128个,RC_HALF = 128——在共用体里,另外的128个准备复制到引用计数表里存储
newisa.extra_rc = RC_HALF;
// 并把引用计数表里是否有当前对象的引用计数标识has_sidetable_rc置为1
newisa.has_sidetable_rc = true;
// 更新一下对象的isa共用体
isa = newisa;
// 把另外的128个引用计数复制到引用计数表里存储(由此可见对象下一次调用retain时,加得还是共用体里的引用计数,因为它已经不再处于溢出状态了,直到再次溢出再复制128个引用计数到引用计数表里存储,如此循环)
sidetable_addExtraRC_nolock(RC_HALF);
}
// 否则就表明extra_rc没有溢出,一切正常
// 返回对象自己
return (id)this;
}
// 去引用计数表里让它的引用计数+1
id objc_object::sidetable_retain()
{
SideTable& table = SideTables()[this];
RefcountMap refcnts = table.refcnts;
table.lock();
size_t& refcntStorage = refcnts[this];
refcntStorage += SIDE_TABLE_RC_ONE;
table.unlock();
return (id)this;
}
// 把另外的128个引用计数复制到引用计数表里存储
bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
// 首先把当前对象的内存地址通过某种散列算法得到一个index,就可以在SideTables里找到对象的引用计数所在的SideTable结构体
SideTable& table = SideTables()[this];
// 这也就是找到了对象的引用计数所在的引用计数表
RefcountMap refcnts = table.refcnts;
// 然后再次把当前对象的内存地址通过某种散列算法得到一个index,就可以在引用计数表里找到对象的引用计数
size_t& refcntStorage = refcnts[this];
size_t oldRefcnt = refcntStorage;
// 引用计数表里的引用计数有64位,但是最低1位是用来标记当前对象是否有弱引用,最低2位是用来标记当前对象是否正在释放,所以一共有62位用来存储引用计数,这足够大了!!!
assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
uintptr_t carry;
// 让引用计数+128
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
// 引用计数表这里也判断了是否溢出
if (carry) {
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
}
else {
refcntStorage = newRefcnt;
return false;
}
}
- 如果有人不想使用该对象了,就调用
release
或autorelease
方法把对象的引用计数-1,即释放该对象。
// NSObject.mm
- (void)release {
self->rootRelease();
}
bool objc_object::rootRelease()
{
return rootRelease(true, false);
}
bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;
isa_t newisa = LoadExclusive(&isa.bits);
if (!newisa.nonpointer) {
// 去引用计数表里让它的引用计数-1
return sidetable_release(performDealloc);
}
// 用来标识extra_rc是否下溢——即是否减为-1(因为extra_rc存储的是(引用计数 - 1),所以减为0的时候说明引用计数为1,还有人引用它,没事儿)
uintptr_t carry;
// 首先去isa共用体里,让对象的引用计数-1
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
if (carry) { // 所以如果extra_rc下溢了:
goto underflow; // 跳转到underflow处执行
}
// 表明没有下溢,结束
return false;
underflow: // extra_rc下溢了:
newisa = LoadExclusive(&isa.bits);
if (newisa.has_sidetable_rc) { // 如果引用计数表里有当前对象的引用计数,说明还有人使用该对象
// 尝试从引用计数表搬回来128个引用计数
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
if (borrowed) { // 如果搬成功了
// 存进去
newisa.extra_rc = borrowed - 1;
// 更新一下对象的isa共用体
isa = newisa;
} else { // 搬失败了,说明引用计数表里的引用计数也为0了(可能是被上一次搬完了)
// 走dealloc方法销毁该对象
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
} else { // 引用计数表里没有当前对象的引用计数,说明没人使用该对象了
// 走dealloc方法销毁该对象
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return true;
}
// 尝试从引用计数表搬回来128个引用计数
size_t objc_object::sidetable_subExtraRC_nolock(size_t delta_rc)
{
SideTable& table = SideTables()[this];
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end() || it->second == 0) { // 引用计数表里的引用计数被上一次搬完了
return 0;
}
// 引用计数表里的引用计数-128,搬出去
size_t oldRefcnt = it->second;
size_t newRefcnt = oldRefcnt - (delta_rc << SIDE_TABLE_RC_SHIFT);
it->second = newRefcnt;
return delta_rc;
}
关于
autorelease
和autoreleasepool
就暂时理解这么一点,更底层的东西有空再说:
release
会立即使对象的引用计数-1,而autorelease
则不会,它仅仅是把该对象注册到了autoreleasepool
中,当autoreleasepool
销毁时系统会自动让池中所有的对象都调用一下release
,这时对象的引用计数才-1。- 而
autoreleasepool
又是在RunLoop休眠或退出时销毁的,当然如果是我们自己创建的@autoreleasepool{}
,出了大括号——即出了@autoreleasepool{}
的生命周期,它就会销毁。- 只要不是用
alloc
、new
、copy
、mutableCopy
方法创建的对象,而是用类方法创建的对象,方法内部都调用了autorelease
,都是autorelease
对象。
- 如果对象的引用计数减为0了,就代表没有人想使用该对象,系统就会调用
dealloc
方法销毁它,并释放它对应的内存,对象一经销毁就绝对不能再访问了,因为它的内存随时可能被移作它用。
// NSObject.mm
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj)
{
obj->rootDealloc();
}
void objc_object::rootDealloc()
{
if (
!isa.has_cxx_dtor && // 如果当前对象没使用过C++析构函数
!isa.has_assoc && // 如果当前对象没有关联对象
!isa.weakly_referenced && // 如果弱引用表里没有当前对象的弱指针数组
!isa.has_sidetable_rc // 如果引用计数表里没有当前对象的引用计数
)
{
// 就直接销毁对象,并释放它对应的内存,即我们之前说的对象销毁时会更快
free(this);
} else {
// 否则就慢慢销毁
object_dispose(this);
}
}
id object_dispose(id obj)
{
objc_destructInstance(obj);
// 销毁对象,并释放它对应的内存,
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// 如果当前对象使用过C++析构函数
bool cxx = obj->hasCxxDtor();
// 如果当前对象有关联对象
bool assoc = obj->hasAssociatedObjects();
// 要按顺序销毁哦
if (cxx) object_cxxDestruct(obj); // 销毁C++析构函数相关的东西
if (assoc) _object_remove_assocations(obj); // 移除关联对象
obj->clearDeallocating();
}
return obj;
}
void objc_object::clearDeallocating()
{
clearDeallocating_slow();
}
void objc_object::clearDeallocating_slow()
{
// 获取SideTable
SideTable& table = SideTables()[this];
if (isa.weakly_referenced) { // 如果弱引用表里有当前对象的弱指针数组
// 把弱引用表里所有指向该对象的弱指针都置为nil,并移除,从此弱引用表里就没有该对象的弱指针数组(关于弱指针(弱引用)更多详细的内容,见下面__weak指针的实现原理)
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) { // 如果引用计数表里有当前对象的引用计数
// 从引用计数表里把该对象的引用计数给抹掉,从此引用计数表里就没有该对象的引用计数了
table.refcnts.erase(this);
}
}
二、MRC是什么,我们需要做什么
MRC是指手动管理引用计数,即需要我们程序员自己手动调用上面那几个alloc
、new
、copy
、mutableCopy
,retain
,release
、autorelease
,dealloc
方法来操作引用计数,从而完成对象的内存管理。具体地说,MRC下我们需要做到以下三点:
- 但凡调用了
alloc
、new
、copy
、mutableCopy
创建对象的地方,在不想使用对象时,要调用release
、autorelease
来释放对象;但凡调用了retain
使对象引用计数+1的地方,在不想使用对象时,要调用release
、autorelease
来使对象的引用计数-1。
- (void)viewDidLoad {
[super viewDidLoad];
// 调用了alloc、new、copy、mutableCopy创建对象
NSArray *arr = [[NSArray alloc] init];
NSArray *arr1 = [NSArray new];
NSArray *arr2 = [arr copy];
NSMutableArray *arr3 = [arr1 mutableCopy];
// 调用release、autorelease来释放对象
[arr release];
[arr1 release];
[arr2 autorelease];
[arr3 autorelease];
}
- (void)viewDidLoad {
[super viewDidLoad];
NSArray *arr = [NSArray array];
// 调用了retain使对象引用计数+1
[arr retain];
// 要调用release、autorelease来使对象的引用计数-1
[arr release];
// [arr autorelease];
}
- 我们还要处理好循环引用问题。
// INEMan.h
#import <Foundation/Foundation.h>
@class INEWoman;
@interface INEMan : NSObject
- (void)setWoman:(INEWoman *)woman;
@end
// INEMan.m
#import "INEMan.h"
#import "INEWoman.h"
@implementation INEMan {
INEWoman *_woman;
}
- (void)setWoman:(INEWoman *)woman {
if (_woman != woman) { // 新旧对象不一样时
[_woman release]; // 释放旧对象
_woman = [woman retain]; // 持有新对象
}
}
- (void)dealloc {
NSLog(@"%s", __func__);
[self setWoman:nil];
[super dealloc];
}
@end
// INEWoman.h
#import <Foundation/Foundation.h>
@class INEMan;
@interface INEWoman : NSObject
- (void)setMan:(INEMan *)man;
@end
// INEWoman.m
#import "INEWoman.h"
#import "INEMan.h"
@implementation INEWoman {
INEMan *_man;
}
- (void)setMan:(INEMan *)man {
_man = man; // 为了避免循环引用,这里不能retain
}
- (void)dealloc {
NSLog(@"%s", __func__);
[self setMan:nil];
[super dealloc];
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
INEMan *man = [[INEMan alloc] init];
INEWoman *woman = [[INEWoman alloc] init];
[man setWoman:woman];
[woman setMan:man];
[man release];
[woman release];
}
// 控制台打印:两个对象都可以正常销毁
-[INEWoman dealloc]
-[INEMan dealloc]
- 我们还要处理好
setter
方法内部的内存管理,并在dealloc
方法里释放父类及当前类所有对象类型的成员变量。
@implementation INEPerson {
int _age;
NSString *_name;
INEDog *_dog;
}
- (void)setAge:(int)age {
_age = age; // 直接赋值
}
- (void)setName:(NSString *)name {
if (_name != name) { // 新旧对象不一样时
[_name release]; // 释放旧对象
_name = [name copy]; // 复制新对象
}
}
- (void)setDog:(INEDog *)dog {
if (_dog != dog) { // 新旧对象不一样时
[_dog release]; // 释放旧对象
_dog = [dog retain]; // 持有新对象
}
}
- (void)dealloc {
// 释放当前类所有对象类型的成员变量
[self setName:nil];
[self setDog:nil];
// 释放父类所有对象类型的成员变量
[super dealloc];
}
@end
三、ARC是什么,帮我们做了什么
ARC是指自动管理引用计数,即编译器会在合适的地方自动帮我们插入retain
、release
、autorelease
等方法的调用,从而完成对象的内存管理。但实际上除了编译器之外,ARC还用到了Runtime,比如weak
指针的清空。具体地说,针对MRC的三点,ARC帮我们做了如下三点:
- 利用
__strong
指针修饰符,编译器会在合适的地方自动帮我们插入retain
、release
、autorelease
等方法的调用。 - 利用
__weak
指针修饰符和Runtime,来处理循环引用问题。 - 利用属性修饰符,编译器帮我们生成特定的
setter
方法并处理好其内部的内存管理,还会自动在dealloc
方法里释放父类及当前类所有对象类型的成员变量。
1、指针修饰符
1.1 __strong
指针修饰符
但凡是用__strong
修饰的指针,在超出其作用域时,编译器会自动帮我们插入一次release
或autorelease
的调用。
// ARC下
{
__strong id obj = [[NSObject alloc] init];
__strong id arr = [NSArray array];
}
等价于:
// MRC下
{
id obj = [[NSObject alloc] init];
id arr = [[NSArray alloc] init];
[obj release];
[arr autorelease];
}
而在指针赋值时,编译器会自动帮我们插入一次retain
的调用。
// ARC下
{
__strong id obj = [[NSObject alloc] init];
__strong id obj1 = obj;
}
等价于:
// MRC下
{
id obj = [[NSObject alloc] init];
id obj1 = [obj retain];
[obj release];
[obj1 release];
}
所以说正是利用__strong
指针修饰符,编译器才会在合适的地方自动帮我们插入retain
、release
、autorelease
等方法的调用,而所有的指针默认都是用__strong
修饰的。
1.2 __weak
指针修饰符
看起来有了__strong
,编译器就可以很好地进行内存管理了呀!但遗憾的是,__strong
无法解决引用计数式内存管理必然会导致的“循环引用”问题。
// INEMan.h
#import <Foundation/Foundation.h>
@class INEWoman;
@interface INEMan : NSObject {
__strong INEWoman *_woman; // 强引用
}
- (void)setWoman:(INEWoman *)woman;
@end
// INEMan.m
#import "INEMan.h"
@implementation INEMan
- (void)setWoman:(INEWoman *)woman {
_woman = woman;
}
- (void)dealloc {
NSLog(@"%s", __func__);
}
@end
// INEWoman.h
#import <Foundation/Foundation.h>
@class INEMan;
@interface INEWoman : NSObject {
__strong INEMan *_man; // 强引用
}
- (void)setMan:(INEMan *)man;
@end
// INEWoman.m
#import "INEWoman.h"
@implementation INEWoman
- (void)setMan:(INEMan *)man {
_man = man;
}
- (void)dealloc {
NSLog(@"%s", __func__);
}
@end
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
INEMan *man = [[INEMan alloc] init];
INEWoman *woman = [[INEWoman alloc] init];
[man setWoman:woman];
[woman setMan:man];
}
// 控制台打印:无
viewDidLoad
执行完,Man
对象和Woman
对象的dealloc
方法都没走,也就是说它们俩都没销毁,这就是因为它们俩形成了循环引用,导致了内存泄漏。
但是只要我们把循环引用中的一个强指针的换成弱指针,就可以解决问题。
@interface INEMan : NSObject {
__weak INEWoman *_woman; // 弱引用
}
或者:
@interface INEWoman : NSObject {
__weak INEMan *_man; // 强引用
}
为什么能解决呢?这就要来看看__weak
指针的实现原理:
-
__weak
指针是专门用来解决循环引用问题的,它不是通过引用计数来管理对象的,而是通过弱引用表。具体地说: - 当我们把一个强指针赋值给一个弱指针时,编译器并不会自动帮我们插入
retain
使对象的引用计数+1,而是把这个弱指针和当前对象的内存地址捆绑在一起,通过两次散列算法找到相应弱引用表里的弱指针数组,把这个弱指针存储到弱指针数组里。这样我们通过这些弱指针既可以正常使用该对象,又无需顾虑是不是要在什么时候再把对象的引用计数-1、以免对象一直有引用计数而销毁不掉,它压根儿就没参与引用计数那一套嘛。 - 而当对象销毁时,又会去通过两次散列算法找到相应弱引用表里弱指针数组,把所有指向该对象的弱指针都置为
nil
并移除。
源码就不贴了,可以自己去看,有了上面那些源码分析,看起来应该是不费力气的。这里只提供个源码入口,我们定义一个__weak
指针,系统其实调用了objc_initWeak
函数。
id obj = [[NSObject alloc] init];
__weak id obj1 = obj;
等价于:
id obj = [[NSObject alloc] init];
id obj1;
objc_initWeak(&obj1, obj);
// NSObject.mm
// weakObj:弱指针(其实是弱指针对应那块内存的地址,但我们直接理解为弱指针是没有问题的)
// obj:当前对象的内存地址
id objc_initWeak(id *weakObj, id obj)
{
// ......
}
2、属性修饰符
属性修饰符一共有三对儿:原子性、读写权限和内存管理语义,属性修饰符主要影响就是编译器为属性生成的setter
、getter
方法上。(虽然只有内存管理语义和内存管理相关,此处不妨一起回顾一下)
2.1 原子性
atomic
、nonatomic
-
atomic
:默认为atomic
,使用atomic
修饰的属性,编译器为该属性生成的setter
、getter
方法内部是加了锁的。
@property (atomic, strong) NSMutableArray *array;
- (void)setArray:(NSMutableArray *)array {
// 加锁
_array = array;
// 解锁
}
- (NSMutableArray *)array {
// 加锁
return _array;
// 解锁
}
但这仅仅是保证我们调用setter
、getter
方法访问属性这一步是线程安全的,它没法保证我们使用属性的线程安全,比如我们调用[self.array addObject:xxx]
,self.array
访问属性这一步是线程安全的,但addObject:
使用属性这一步是线程不安全的。
// 线程1
[self.array addObject:@"11"];
// 线程2
[self.array addObject:@"12"];
等价于
// 线程1
[[self array] addObject:@"11"];
// 线程2
[[self array] addObject:@"12"];
所以为了保证使用属性的线程安全,我们还得在需要的地方自己加锁,这样一来使用atomic
修饰属性就是多此一举了,而且setter
、getter
方法的调用通常是很频繁的,内部加锁的话会很耗性能。
// 线程1
// 加锁
[self.array addObject:@"11"];
// 解锁
// 线程2
// 加锁
[self.array addObject:@"12"];
// 解锁
-
nonatomic
:因此我们在实际开发中总是使用nonatomic
。
2.2 读写权限
readwrite
、readonly
-
readwrite
:默认为readwrite
,代表该属性可读可写,编译器会为该属性生成setter
、getter
方法的声明与实现。 -
readonly
:代表该属性只能读取不能写入,编译器会为该属性生成setter
、getter
方法的声明与getter
方法的实现。
2.3 内存管理语义
- MRC下有:
assign
、retain
、copy
。- ARC下又新增了:
strong
、weak
、unsafe_unretained
。
-
assign
:assign
一般用来修饰基本数据类型。使用assign
修饰的属性,编译器为该属性生成的setter
方法内部只会进行简单的赋值操作。
- (void)setAge:(int)age {
// 简单的赋值操作
_age = age;
}
-
retain
:retain
一般用来修饰对象类型。使用retain
修饰的属性,编译器为该属性生成的setter
方法内部会调用一下retain
方法,使对象的引用计数+1。
- (void)setDog:(INEDog *)dog {
if (_dog != dog) { // 新旧对象不一样时
[_dog release]; // 释放旧对象
_dog = [dog retain]; // 持有新对象
}
}
-
copy
:copy
一般用来修饰不可变属性和block。使用copy
修饰的属性,编译器为该属性生成的setter
方法内部会调用一下copy
方法,生成一个新对象,新对象的引用计数为1,而旧对象的引用计数不变。
- (void)setName:(NSString *)name {
if (_name != name) { // 新旧对象不一样时
[_name release]; // 释放旧对象
_name = [name copy]; // 复制新对象
}
}
-
strong
:默认为strong
,大多数情况下和retain
的效果是一样的,修饰block时和copy
的效果是一样的,strong
一般用来修饰对象类型。 -
weak
:weak
一般用来修饰代理对象和NSTimer
,以免造成循环引用;还用来修饰xib或sb拖出来的控件,因为这些控件已经被它们拖出来所在的父视图所持有了,不必再用变量持有。 -
unsafe_unretained
:和assign
的效果是一样的,如果你非要用它们来修饰对象类型,那也只好说它们和weak
的功能类似,但weak
修饰的属性在对象销毁时会被被置为nil
,比较安全,而unsafe_unretained
和assign
修饰的属性则不会,所以容易出现野指针。