iOS开发中,内存管理是不可避免的。鉴于当下MRC已经远去多时,本篇学习笔记主要针对ARC下的内存管理进行实践。程序员
内存理论篇:编程
内存理论实践篇:数组
缘由:bash
- 通常继承了 NSObject 的对象,存储在操做系统到 堆 里边。(PS:并不是全部都是这样,建立的字符串有时候根据建立方式、位置,也会存储到 常量区)
- 操做系统的 堆:通常由程序员分配释放,若程序员不释放,结束时可能由系统回收,分配方式相似数据结构的链表。
- 操做系统的 栈:由操做系统自动分配释放内存,存放函数的参数值、局部变量值等。其操做方式相似数据结构中的 栈(先进后出)。
示例:数据结构
int main(int argc, const char *argv []) {
@autoreleasepool {
int a = 10; // 栈
int b = 20; // 栈
// p: 栈
// Person 对象(计数器 == 1):堆
Person *p = [[Person alloc] init];
}
// 通过上面代码后,栈里的变量 a、b、p 都会被回收
// 可是堆里的 Person 对象仍会留在内存中,由于它的计数器依然是 1
return 0;
}
复制代码
block内存分为三种类型:app
- 一、_NSConcreteGlobalBlock(全局)
当咱们声明一个block时,若是这个block没有捕获外部的变量,那么这个block就位于全局区,此时对_NSConcreteGlobalBlock的retain、copy、release操做都无效。ARC和MRC环境下都是如此。框架
示例:声明并定义一个全局区block函数
void (^myBlock) (int x); myBlock = ^(int number) { int result = number + 100; NSLog(@"result: %d",result); }; myBlock(10); 复制代码
- 二、_NSConcreteStackBlock(栈)
栈区block咱们平时编程基本不会遇到!由于在ARC环境下,当咱们声明而且定义了一个block,而且没有为Block添加额外的修饰符(默认是__strong修饰符),若是该Block捕获了外部的变量,实质上是有一个从_NSConcreteStackBlock转变到_NSConcreteMallocBlock的过程,只不过是系统帮咱们完成了copy操做,将栈区的block迁移到堆区,延长了Block的生命周期。对于栈区block而言,栈block在当函数退出的时候,该空间就会被回收。oop
那何时在ARC的环境下出现_NSConcreteStackBlock呢?若是咱们在声明一个block的时候,使用了__weak或者__unsafe__unretained的修饰符,那么系统就不会为咱们作copy的操做,不会将其迁移到堆区。下面咱们实验一下:post
__weak void (^myBlock1) (int n) = ^(int num) { int result = num + n; NSLog(@"result: %d",result); }; myBlock1(12); NSLog(@"myBlock1: %@",myBlock1); //打印结果 "myBlock1:<__NSStackBlock__:0x7ffff50726c0>" //结论:被__weak修饰的myBlock1捕获了外部变量n,成为一个栈区的block 复制代码
void (^myBlock1) (int n) = ^(int num) { int result = num + n; NSLog(@"result: %d",result); }; myBlock1(12); NSLog(@"默认myBlock1: %@",myBlock1); //打印结果 "默认myBlock1:<__NSMallocBlock__:0x604000259020>" //结论:不使用__weak修饰,在默认修饰符环境下,捕获了外部变量的block位于堆区 复制代码
咱们能够手动地去执行copy方法,验证系统为咱们作的隐式转换:
__weak void (^myBlock1) (int n) = ^(int num) { int result = num + n; NSLog(@"result: %d",result); }; myBlock1(12); NSLog(@"手动copy myBlock1: %@",[myBlock1 copy]); //打印结果 "手动copy myBlock1:<__NSMallocBlock__:0x60000025c020>" //结论:手动执行copy方法以后,block被迁移到了堆区 复制代码
- 三、_NSConcreteMallocBlock(堆)
在MRC环境下,咱们须要手动调用copy方法才能够将block迁移到堆区,而在ARC环境下,__strong修饰的(默认)block只要捕获了外部变量就会位于堆区,NSMallocBlock支持retain、release,会对其引用计数+1或 -1。
- 一、当使用局部变量时,须要添加__block
__blockintnum =100; self.tBlock= ^(int n) { num = num + n; NSLog(@"%d",num); }; self.tBlock(100); 复制代码
- 二、当使用全局变量时,须要时使用__weak typeof(self)weakSelf = self修饰,不然会形成循环引用
__weak typeof(self)weakSelf = self; weakSelf.qBlock= ^(NSString*str) { NSLog(@"%@",weakSelf.nameStr); }; 复制代码
- 三、 这样循环问题是解决了,可是又会致使一个新的问题,假如在block有一个耗时操做,在这个过程self被销毁了,而weakself也会随着self的销毁而销毁,block又要对weakself进行某些操做,这是拿到的weakself就是nil了。(缘由请参考iOS-内存管理-理论篇 __weak -内存理论)
__weak typeof(self)weakSelf = self; weakSelf.qBlock= ^(NSString*str) { _strong__typeof(self) strongSelf = weakSelf; NSLog(@"%@",strongSelf.nameStr); }; self.tBlock(100); 复制代码
block做为属性,使用copy修饰时(strong修饰符不会改变block内存类型),所以使用copy或strong修饰均可以。block中使用weak通常是为了防止循环引用,为了不重复,在这里就不过多介绍weak的使用。
项目当中使用block尽可能不要嵌套,若是实在嵌套也请控制在一层。否则很容易形成内存泄露或是地狱回调。特别是若是是用block进行数据传递,多层嵌套的block很容易形成数据缺失,app崩溃,并且项目复杂之后很难排查。
Runtime维护了一个weak表,用于存储指向某个对象的全部weak指针,对于 weak 对象会放入一个 hash 表中,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。 当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到全部以a为键的 weak 对象,从而设置为 nil。
注:因为可能多个weak指针指向同一个对象,因此value为一个数组
weak 的实现原理能够归纳如下三步:
- 一、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。 示例代码:
{ id __weak obj1 = obj; } 复制代码
当咱们初始化一个weak变量时,runtime会调用objc_initWeak函数。这个函数在Clang中的声明以下:
id objc_initWeak(id *object, id value); 复制代码
其具体实现以下:
id objc_initWeak(id *object, id value) { *object = 0; return objc_storeWeak(object, value); } 复制代码
示例代码轮换成编译器的模拟代码以下:
id obj1; objc_initWeak(&obj1, obj); 复制代码
所以,这里所作的事是先将obj1初始化为0(nil),而后将obj1的地址及obj做为参数传递给objc_storeWeak函数。objc_initWeak函数有一个前提条件:就是object必须是一个没有被注册为__weak对象的有效指针。而value则能够是null,也能够指向一个有效的对象。
- 二、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数。
objc_storeWeak() 的做用是更新指针指向,建立对应的弱引用表。
- 三、释放时,调用clearDeallocating函数。
clearDeallocating函数首先根据对象地址获取全部weak指针地址的数组,而后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
在dealloc的时候,会将weak属性的值会自动设置为nil
- 一、系统经过runloop建立的autoreleasePool
runloop 能够说是iOS 系统的灵魂。内存管理/UI 刷新/触摸事件这些功能都须要 runloop 去管理和实现。runloop是经过线程建立的,和线程保持一对一的关系,其关系是保存在一个全局的 Dictionary 里。线程刚建立时并无 RunLoop,若是你不主动获取,那它一直都不会有。RunLoop 的建立是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
runloop和autoreleasePool又是什么关系呢?对象又是何时释放的?
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 建立自动释放池。其 order 是-2147483647,优先级最高,保证建立释放池发生在其余全部回调以前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并建立新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其余全部回调以后。
在主线程执行的代码,一般是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 建立好的 AutoreleasePool 环绕着,因此不会出现内存泄漏,开发者也没必要显示建立 Pool 了。
- 二、手动autoreleasePool
咱们能够经过@autoreleasepool {}方式手动建立autoreleasepool对象,那么这个对象何时释放呢?答案是除了autoreleasepool的大括号就释放了。
- 三、子线程的autoreleasepool对象的管理?
线程刚建立时并无 RunLoop,若是你不主动获取,那它一直都不会有。因此在咱们建立子线程的时候,若是没有获取runloop,那么也就没用经过runloop来建立autoreleasepool,那么咱们的autorelease对象是怎么管理的,会不会存在内存泄漏呢?答案是否认的,当子线程有autoreleasepool的时候,autorelease对象经过其来管理,若是没有autoreleasepool,会经过调用 autoreleaseNoPage 方法,将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!这部分咱们能够看下runtime中NSObject.mm的部分,有相关代码。
static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && !page->full()) { return page->add(obj); } else if (page) { return autoreleaseFullPage(obj, page); } else { //调用 autoreleaseNoPage 方法管理autorelease对象。 return autoreleaseNoPage(obj); } } 复制代码
- 一、使用autorelease有什么好处呢?
- 不在关心对象的释放时间
- 不在关心何时调用 release
- 二、autorelease 的建立方法
- 使用 NSAutoreleasePool 来建立:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 建立自动释放池 [pool release]; // [pool drain]; 销毁自动释放池 复制代码
- 使用 @autoreleasepool 建立
@autoreleasepool { //开始表明建立自动释放池 } //结束表明销毁自动释放池 复制代码
- 三、autorelease 的使用方法
- NSAutoreleasePool 用法:
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init]; Person *p = [[[Person alloc] init] autorelease]; [autoreleasePool drain]; 复制代码
- @autoreleasepool 用法:
@autoreleasepool { // 建立一个自动释放池 Person *p = [[Person new] autorelease]; // 将代码写到这里就放入了自动释放池 } // 销毁自动释放池(会给池子中全部对象发送一条release消息) 复制代码
- 四、autorelease 的注意事项
- 并非放到自动释放池代码中,就会自动加入自动释放池
错误案例1 @autoreleasepool { // 由于没有调用 autorelease 方法,因此对象没有加入到自动释放池 Person *p = [[Person alloc] init]; [p run]; } 复制代码
- 在自动释放池的外部调用 autorelease 不会被加入到自动释放池中。autorelease 是一个方法,只有在自动释放池中调用才有效
错误案例2 @autoreleasepool { } // 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会放到释放池 Person *p = [[[Person alloc] init] autorelease]; [p run]; // 正确案例1 @autoreleasepool { Person *p = [[[Person alloc] init] autorelease]; } // 正确案例2 Person *p = [[Person alloc] init]; @autoreleasepool { [p autorelease]; } 复制代码
- 五、autorelease 经典错误案例实际当中容易犯的错误
自动释放池内不宜放占用内存比较大的对象
- 尽可能避免对大内存使用该方法,对这种延迟释放机制,仍是尽可能少用。
- 不要把大量循环操做放到一个 autoreleasepool 之间,这样会形成内存峰值的上升
// 内存暴涨 @autoreleasepool { for (int i = 0; i < 99999; ++i) { //若是Person对象内存占用大这种写法在少许循环中就会形成严重内存泄露 Person *p = [[[Person alloc] init] autorelease]; } } // 内存不会暴涨 for (int i = 0; i < 99999; ++i) { @autoreleasepool { Person *p = [[[Person alloc] init] autorelease]; } } 复制代码