iOS中内存管理的观察

标签: 编程学习 iOS学习
iOS程序的内存布局

iOS 中的内存区域分为以下几个: 代码段 TEXT:编译之后的代码;

数据段 DATA ① 字符串常量:⽐如NSString *str = @"123"; ② 已初始化数据:已初始化的全局变量、静态变量等; ③ 未初始化数据:未初始化的全局变量、静态变量等;

栈 STACK:函数调⽤开销,⽐如局部变量。分配的内存空间地址越来越⼩;

堆 HEAP:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越⼤

Tagged Pointer

我们知道一个对象分配的内存地址是 16 个字节,再加上一个指针 8 个字节,总共 24 个字节,比较浪费内存空间的,所以出现了Tagged Pointer。 从 64bit 开始,iOS引⼊了Tagged Pointer⽤于优化NSNumber、NSDate、NSString等⼩对象的存储,在没有使⽤Tagged Pointer之前, 这些小对象需要动态分配内存、维护引⽤计数等,指针存储的是堆中对象的地址值,使⽤Tagged Pointer之后,这些小对象指针⾥⾯存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中,当指针不够存储数据的时候,才会使⽤动态分配内存的⽅式来存储数据,objc_msgSend能识别Tagged Pointer,⽐如NSNumberintValue⽅法,直接从指针提取数据,节省了以前的调⽤开销 在 iOS 平台,最⾼有效位是 1(第64bit), 在 Mac平台,最低有效位是 1 的对象是Tagged Pointer对象。

可以观察两个对象的isa,一个是空,一个是 __NSCFNumber,说明前面的小对象使用的是 Tagged Pointer 技术。

OC对象的内存管理

iOS的内存管理中,使用引用计数来进行内存管理,秉持着:谁创建谁释放,谁引用谁管理的原则 MRC 手动引用计数管理,就是我们在创建使用对象之后,只要是调⽤alloc、new、copy、mutableCopy⽅法返回了⼀个对象,在不需要这个对象时,要调⽤release或者autorelease来释放它。

NSString *str1 = [[NSString alloc] initWithFormat:@"123"];
NSString *str2 = [str1 copy];
NSMutableString *str3 = [str2 mutavleCopy];

[str1 release];
[str2 release];
[str3 release];

MRC 下 setter 方法的写法,需要考虑内存问题,比如下面的案例

- (void)setDog:(NSObject *)dog {
   if(_dog != dog) {  //不判断会引起僵尸对象
       [_dog release]; // 不释放会引起内存泄漏
       _dog = [dog retain]; // 引用计数加一,保证外面的释放的时候不至于没有对象
   }
}

ARC 自动引用计数管理,就是系统利用 LLVM 编译器自动帮我们生成 release/retain/autorelease这些代码,利用 Runtime 帮我们实现弱引用等,在 LLVMRuntime 的相互作用下实现自动内存管理。 ARC 中禁止手动调用 retain/release/retainCount/dealloc,新增了weak/strong 属性关键字

深拷贝与浅拷贝 深拷贝是对象拷贝,浅拷贝是指针拷贝,系统提供的对象拷贝如下

自定义对象如果想实现拷贝,那需要实现 NSCoping 协议

- (id)copyWithZone:(NSZone *)zone {
    MrcModel *model = [MrcModel new];
    model.name = self.name;
    return model;
}

引用计数 学习 isa 结构的时候学习过 isa 共用体的结构

引用计数是存储在 extra_rc 中的,如果不够存,has_sidetable_rc 这个参数会变成 1,那么引用计数会存储在一个叫 SideTable 中的 refcnts 散列表中。

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}

弱引用管理 被声明为 __weak 的对象指针,经过编译器的编译之后,会调用 objc_initWeak 函数,然后经过一系列的调用栈,最终在weak_register_no_lock()这个函数内进行弱引用变量的添加,通过哈希算法来进行位置查找,而这个位置就是上一步中看到的 SideTable 中的 weak_table 散列表中。

当对象要释放的时候,如果存在弱引用,则会先把弱引用表中存储的该对象的引用全部清除掉,清除时是同样的查找步骤,只不过是调用 weak_clear_no_lock() 这个函数。

自动释放池

⾃动释放池是以栈为节点以双向链表形式组合而成的数据结构。主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage,调⽤了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。

每个AutoreleasePoolPage对象占⽤4096字节内存,除了⽤来存放它内部的成员变量,剩下的空间⽤来存放 autorelease 对象的地址。

调用了 AutoreleasePoolPage::Push() 之后会先把一个叫 POOL_BOUNDARY 的值插入 AutoReleasePoolPage 中,后面调用的每一个 autorelease 都会往 AutoReleasePage 中写入一个 autorelease 包裹的对象。

将要释放的时候调用 AutoReleasePoolPage::Pop(),传入的地址是 Push 的返回值,其实就是 POOL_BOUNDARY 的地址,会从最后⼀个⼊栈的对象开始发送release消息,直到遇到这个 POOL_BOUNDARY

AutoReleasePoolPagenext 指针指向的是下一个能够给存储对象的位置。

如果有多层嵌套的话会插入多个 POOL_BOUNDARY

Runloop与Autorelease iOS在主线程的 Runloop 中注册了2个 Observer 第1个监听kCFRunLoopEntry事件,会调⽤objc_autoreleasePoolPush(); 第2个监听kCFRunLoopBeforeWaiting事件,会调⽤objc_autoreleasePoolPop()、objc_autoreleasePoolPush(),同时也监听了kCFRunLoopBeforeExit事件,会调⽤objc_autoreleasePoolPop()

由此可见,方法中的局部变量并不一定会立即释放,这要看 ARC 环境下编译器如何处理,如果添加的是 release,那么方法结束立即释放,如果添加的是 autorelease,那么会在当次 Runloop 结束的时候释放。