《Objective-C 高级编程 iOS 与 OS X 多线程和内存管理》之笔记整理

一、ARC

1. autorelease

使用 NSMutableArray 类的 array 方法等可以取得谁都不持有的对象,这些方法都是通过 autorelease 而实现的。

2. GNUstep

GNUstep 是 Cocoa 框架的互换框架,可看到源码,参考 ARC 的实现方式。
简单来说就是通过一个 obj_layout 的结构体中的 retained 来保存了引用计数:

  1. 在 Objective-C 的对象中存有引用计数这一整数值;
  2. 调用 alloc 或者是 retain 方法后,引用计数加一;
  3. 调用 release 之后,引用计数减一;
  4. 引用计数数值为0时,调用 dealloc 方法废弃对象。

3. 引用计数两种保存方式对比

GNUstep将引用计数保存在对象占用内存块头部的变量中,而苹果的实现则是保存在引用计数表的记录中,两者各有优点。
通过内存块头部管理的好处是:

  • 少量代码即可完成;
  • 能够统一管理引用计数的内存块和对象的内存块。

通过引用计数表来管理的好处是:

  • 对象用的内存块分配不需要考虑内存块头部;
  • 引用计数表各记录中存有内存块地址,可从各个记录追溯到各对象的内存块;
  • 利用工具检测内存泄露时,引用计数表的各记录也有助于检测各对象的持有者是否存在。

4. NSRunLoop 每次循环过程中 NSAutoreleasePool 对象被生成或者是废弃。

5. ARC 所有权修饰符有 4 种:

__strong(默认)
__weak
__unsafe_unretained
__autoreleasing

6. 所谓内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。

7. ARC 有效情况下的编码规则:

  • 不能使用retain/release/retainCount/autorelease
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 必须遵守内存管理的方法命名规则
  • 不要显式调用dealloc
  • 使用@autoreleasepool块替代NSAutoreleasePool
  • 不能使用区域NSZone
  • 对象型变量不能作为c语言结构体的成员
  • 显式转换id和void*

8. 属性声明的关键字与所有权修饰符的对应关系:

  • assign: __unsafe_unretained
  • copy: __strong(但是赋值的是被复制的对象)
  • retain: __strong
  • strong: __strong
  • unsafe_unretained: __unsafe_unretained
  • weak: __weak

9. ARC 的实现

ARC 是由编译器进行内存管理的,但实际上只有编译器是无法完全胜任的,在此基础上还需要 Objective-C 运行时库的协助:

  • clang (LLVM 编译器)3.0以上
  • objc4 Objective-C 运行时库493.9以上

10. 关于 __weak 修饰符的实现

若附有__weak修饰符的变量所引用的对象被抛弃,则将nil赋值给该变量。
若用附有__weak修饰符的变量,即是试用注册到autoreleasepool中的对象。

将 __strong 修饰的 obj 赋值给 __weak 的 obj1 将会发生什么呢?
eg:id __weak obj1 = obj;

下面👇是编译器的模拟代码:
id obj1;
objc_initWeak(&obj1, obj);//初始化 obj1
objc_destroyWeak(&obj1);//释放 obj1

1.其中 objc_initWeak 函数又会调用 objc_storeWeak 函数
objc_initWeak(&obj1, obj)等同于:
    obj1 = 0;
    objc_storeWeak(&obj1, obj);
    
2.objc_destroyWeak 函数会将 0 作为参数调用 objc_storeWeak 函数 
objc_destroyWeak(&obj1)等同于:
       objc_storeWeak(&obj1, 0);

完整的模拟代码如下:

id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
objc_storeWeak(&obj1, 0);   

//这里第一个storeWeak函数把第二个参数的赋值对象的地址作为键值,将第一个参数的附有__weak修饰符的变量的地址注册到weak表中。
//可以注意到,第二次调用的时候第二个参数为0, 也就是说第二个参数为-0的时候,会把变量的地址从weak表中删除

weak表是什么呢? 此时联想到引用计数表,他们都是利用散列来实现的。 这里的一个键值可以注册多个变量的地址。 就跟一个对象可以同时付给多个附有__weak修饰符的变量。也就是说,如果你用一个废弃对象的地址作为键值来检索,你能够告诉的获取对应的附有__weak修饰符的变量的地址。

当释放对象的时候,废弃掉谁都不持有的对象,程序后续还会出现动作:
1、objc_release
2、因为引用计数为0, 所以执行dealloc
3、_objc_rootDealloc
4、object_dispose
5、objc_destructInstance
6、objc_clear_deallocating

最后调用的 objc_clear_deallocating 函数会出现如下动作:
1、从weak表中获取废弃对象的地址为键值的记录。
2、将包含在记录中的所有附有__weak修饰符变量的地址,赋值为nil。
3、从weak表中删除该记录。
4、从引用计数表中删除废弃对象的地址为键值的记录。

使用附有 __weak 修饰符的变量,即是使用注册到 autoreleasepool 中的对象。
id __weak obj1 = obj 可以转换为如下形式:

id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
objc_destroyWeak(&obj1);

这里增加了objc_loadWeakRetained和objc_autorelease的调用。
1、objc_loadWeakRetained:函数取出附有__weak修饰符变量所引用的对象,并retain。
2、objc_autorelease函数将对象注册到autoreleasepool中。

最后提醒的是:id __weak obj = [[NSObject alloc] init];  
        和  id __unsafe_unretained obj = [[NSObject alloc] init]; 
        
这样是不可以的,前者是因为不能持有对象,后者是obj被赋予的是 悬垂指针。  
虽然在arc中不会造成内存泄露,但是还是不要这样使用的好。

11. 引用计数

  • 获取引用计数值的函数:uintptr_t_objc_rootRetainCount(id obj) , 这个函数可以获取指定对象的引用计数值(ARC中,retainCount已经不能用了),但是也不能完全信任该函数取得的值,对于已经释放的对象以及不正确的对象地址,有时也会返回 1;
  • 函数 _objc_autoreleasePoolPrint函数会观察注册到autoreleasepool中的引用对象。

二、 Blocks

1. Blocks 是 C 语言的扩充功能。

一句话概括 Blocks 的扩充功能:带有自动变量(局部变量)的匿名函数。匿名函数就是不带有名称的函数。

2. C 语言的函数中可能使用的变量:

自动变量(局部变量)
函数的参数
静态变量(静态局部变量)
静态全局变量
全局变量

3. 截获自动变量

  • 只针对Block中使用的自动变量
  • 自动变量的值以成员变量的形式被保存到Block的结构体实例(或者说被其持有),通过__cself被使用;如果是__block变量,则转化成结构体,其指针作为成员变量保存到Block结构体中

4. Block 与 __block 变量的实质

  • Block:栈上 Block 的结构体实例
  • __block:栈上 __block 变量的结构体实例

Block 类及其对应的存储域如下:


Block 类及其对应的存储域.jpeg

Block 的副本


Block 的副本.jpeg

Block 从栈复制到堆时对 __block 变量产生的影响


Block 从栈复制到堆时对 __block 变量产生的影响.jpeg

复制 __block 变量

复制 __block 变量.jpeg

什么时候栈上的Block会被复制到堆上呢?

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

5. Block 循环引用

原因:Block中附有__strong修饰符的对象类型自动变量在从栈复制到堆上时,该对象会被Block所持有。

解决方案:

ARC:通过 __weak 或 __unsafe_unretained 修饰符(iOS4)来替代 __strong 类型的被截获的自动变量通过 __block 说明符和设置nil来打破循环
MRC:通过 __block 说明符指定变量不被Block所retain;ARC下__block说明符的作用仅限于使其能在Block中被赋值。

如果对block做一次copy操作, block的内存就会在堆中

  • 它会对所引用的对象做一次retain操作
  • 非ARC : 如果所引用的对象用了__block修饰, 就不会做retain操作
  • ARC : 如果所引用的对象用了__unsafe_unretained__weak修饰, 就不会做retain操作

GCD

Grand Central Dispatch,是 iOS 目前最常用的多线程处理技术,在此之前一般使用 NSObject 类的 performSelector 系列方法或者 NSTherd 相关方法实现多线程。

1. 使用多线程的弊端

  1. 多个线程更新相同的资源会导致数据的不一致(数据竞争)
  2. 停止等待事件的线程会导致多个线程相互持续等待(死锁)
  3. 使用太多线程会消耗大量内存

2. GCD 相关 API

GCD 的 API

3. Dispatch Queue 的实现依托于:

  1. 用于管理追加的 Block 的 C 语音层实现的 FIFO 队列;
  2. Atomic 函数中实现的用于排他控制的轻量级信号
  3. 用于管理线程的 C 语音层实现的一些容器
    除此之外,GCD 是依托于系统内核级的实现,如下图:


    用于实现 Dispatch Queue 而使用的软件组件

4. Dispatch Source

它是BSD系内核惯有功能kqueue的包装。kqueue是在XUN内核中发生各种事件时,在应用程序编程方执行处理的技术。 
其CPU负荷非常小,尽量不占用资源。kqueue可以说是应用程序处理XUN内核中发生的各种事件的方法中最优秀的一种。

Dispatch Source 的种类入下图:


Dispatch Source 的种类

推荐阅读更多精彩内容