更多精彩文章,请关注作者的微信公众号:码工笔记
上一篇 OCPack 技术方案总结发出后,有同学私信问 ARC 的具体实现细节,正好之前也没有好好总结这一块,于是有了这一篇文章。
在开始这篇文章之前,建议没有看过上一篇文章的同学先看一下,链接如下:
以下正文开始。
OCPack 技术方案一个很重要的特点是业务侧直接使用 Objective-C 语言进行开发,而使用 OC 就不能不提到 ARC,那么如何支持 ARC 呢?
一、ARC 相关背景知识
众所周知,ARC 是编译器支持的一套自动在代码中插入 retain/release 等内存管理方法的机制,它能够减轻开发者手动管理内存的负担,极大地提高开发效率。那么编译器是如何实现 ARC 的呢?在 clang 语法树中有直接的 retain/release 操作的结点供我们的 AST 解析器使用吗?
要回答这些问题,就需要深入了解一下 clang 中 ARC 的具体逻辑,详细研究一下其对应的语法树结点和代码实现。
经过调研,在 clang 语法树中并没有直接对应 retain/release 等方法的结点,开启了 ARC 编译开关的代码生成的语法树中只包含一些特殊类型的 cast expr 结点(以及其他 AST 结点中增加的一些类型信息),其位置也并不与实际应该生成的 retain/release 等有直接位置上的关系。这些 cast expr 结点(和类型信息)只是给何时生成 retain/release 提供了一些提示,具体的生成 retain/release 等操作的逻辑分散在 clang 的代码生成模块(codegen)“浩瀚”的代码之中。
要搞清楚 clang codegen 中相关代码的逻辑,首先需要对 ARC 规则有一个更详细的了解。我们需要去看一下 clang 官方文档中的关于 ARC 的语言标准。
二、ARC 语言标准(Specification
)
ARC 语言标准文档将 ARC 的使用场景分为两大部分,记录要点如下:
1. 关于可 retain 的对象:
- 无 retain/release 操作的场景
- 读取一个非 weak 的对象指针
- 给函数或方法传递一个可retain 对象的指针
- 从方法返回值得到一个可 retain 的对象
- Consumed 类型的方法参数
- 参数声明中包含了 ns_consumed 属性
- 被调用者预期得到一个引用计数被 +1 过的对象,并成为其 owner
- ARC 需要在真正调用此方法前自动给它 +1
- ARC 需要在方法调用完成后释放此对象
- 例:init 方法中的 self 参数即属于此类型
- 需要被 retain 的方法返回值
- 声明了 ns_returns_retained 属性
- 调用者预期得到一个被 +1 过的对象,并成为其 owner
- ARC 会在执行 return 语句时(离开当前作用域之前)对返回结果进行 retain
- 当调用者从这种方法得到一个返回值时,ARC会在调用者当前表达式(full-expression)结束时 release 此对象
- 例:alloc, copy, init, mutableCopy, new
- 要想取消此效果,需要添加 __attribute((ns_returns_not_retained)) 属性
- 不被 retain 的返回值
- ARC 在执行 return 表达式时 retain 返回值对象,然后离开局部作用域,然后在保证返回对象成功跨越了方法调用边界后调用 release 来平衡之前的 retain 操作
- 最差情况下这会导致一次 autorelease
- ns_returns_autoreleased 属性表示返回值对象的生命周期至少到当前最顶层的 autorelease pool
- ARC 在执行 return 表达式时 retain 返回值对象,然后离开局部作用域,然后在保证返回对象成功跨越了方法调用边界后调用 release 来平衡之前的 retain 操作
- Bridge 转换
- (__bridge T)op
- 无 retain/release
- (__bridge_retained T)op
- op 必须是支持 retain 操作的对象指针
- T 必须是不能 retain 的指针
- ARC 会 retain op 对象
- (__bridge_transfer T)op
- op 必须是不能 retain 的指针
- T 必须是支持 retain 操作的对象指针
- ARC 会在当前表达式所在的完整表达式结束时 release 它
- (__bridge T)op
2. 指定对象的生命周期
2.1 程序中的关键字
- 变量类型:
- __autoreleasing
- __strong
- __unsafe_unretained
- __weak
- 属性声明和变量类型对应关系
- assign: __unsafe_unretained
- copy: __strong
- retain: __strong
- strong: __strong
- unsafe_unretained: __unsafe_unretained
- weak: __weak
2.2 对象操作
- 读取数据(左值转右值)
- __weak:当前对象会先被 retain,然后在当前表达式所在的完整表达式结束时被 release
- 其他:无特殊操作
- 赋值(=操作符)
- __strong
- retain 新对象
- 读取左值对应的右值(旧对象)
- 新对象地址被存储到左值变量中
- release 旧对象
- __weak
- 左值变量指向新对象
- 如果新对象正在被销毁,则左值变量更新为空指针
- __unsafe_unretained
- 无特殊操作
- __autoreleasing
- 新对象会被先 retain,再 autorelease,然后存储到左值变量中
- __strong
- 初始化
- 左值变量中存入空指针
- 如果对象类型为 __unsafe_unretained,则此步略过不执行
- 如果对象有初始化表达式,则先执行表达式,再使用赋值的逻辑将表达式返回值赋给变量
- 左值变量中存入空指针
- 销毁
- 逻辑与将空指针赋值给变量相同
- Move 操作(c++)
- 略
三、OCPack 中 ARC 相关实现
根据 ARC 标准,结合 clang 源码中对于 ARC 相关逻辑的实现(CGObjC.cpp),OCPack 在基本保证符合标准的情况下对 clang 中复杂的实现进行了简化和适配。
以下分两部分对 OCPack 中 ARC 的实现进行介绍。
3.1 OCPack 中 ARC 相关 AST 结点的处理逻辑
OCPack在遍历语法树时,会对以下包含 ARC 信息的 AST 结点进行特殊处理:
CastExpr
- CK_ARCConsumeObject
- 生成指令:autorelease
- CK_ARCProduceObject
- 生成指令:retain + autorelease 或
- retain_block + autorelease
- 注:此处语义与 clang 有区别:clang 中实现使用 EmitARCRetainScalarExpr,但因为我们只针对单个文件,不能使用 fullexpr 控制外部调用者,故此处使用 autorelease。这样会导致使用 __bridge_retained 时会出现问题,因为结果不是一个+1的对象,而是autorelease的对象,因此本方案暂不支持使用 __bridget_retained 关键字。
- CK_ARCReclaimReturnedObject
- 生成指令:retain + autorelease
- CK_ARCExtendBlockObject
- 生成指令:retain_block + autorelease
- CK_CopyAndAutoReleaseBlockObject
- 生成指令:copy + autorelease
- LValueToRValue
- 指令参数中增加 lvalue 的 arc 类型信息,运行时如果是 weak 则需要运行时调用 loadWeak
ObjcMethodDecl(ParamVarDecl 和 ImplicitParamDecl)
- 方法头部 retain
- strong:无 ns_consume 属性,则 retain
- 方法结束时 release
- strong 的调用 release
- weak 的调用 destroyWeak
- Destroy 操作栈
- 每个方法开始时建立一个对象 destroy 操作栈,每遇到一个需要 release 的变量时在 destroy 栈中添加一条记录(记录需要对哪个符号做何种 release 操作)
- 方法结束(如:return)时,将当前方法的 destroy 栈中的所有记录取出并生成相应的指令
- 在每个 CompoundStmt 开始和结尾也会创建、销毁 RunCleanupsScope,用于处理这段代码中的局部变量的生命周期。
- 另外 @autoreleasepool {} 中也会创建 RunCleanupsScope
varDecl(局部变量)
- init expression 处理
- 生成 assign 指令
- 方法结束时 release
- strong 的调用 release
- weak 的调用 destroyWeak
- 具体时机见上面 ObjCMethodDecl 中描述的 Destroy 操作栈部分
BinaryOperator:Assign
- 指令中增加 lvalue 的 arc 类型信息,运行时根据类型做相应处理:
- weak:storeWeak + loadWeak
- strong: retain new + assign + release old
- autoreleasing: retain + autorelease
- unsafe_unretained: N/A
ObjCAutoreleasePoolStmt
-
生成指令:ARC_AUTORELEASEPOOL_PUSH 和 ARC_AUTORELEASEPOOL_POP
3.2 OCPack 中 ARC 相关具体指令设计
上节中提到的要根据各种具体代码场景生成的 retain/release 等 ARC 指令,具体如下:
- 指令:arc_cmd
- 操作数:
- ARC_RETAIN
- ARC_RELEASE
- ARC_AUTORELEASE
- ARC_INIT_WEAK
- ARC_DESTROY_WEAK
- ARC_LOAD_WEAK
- ARC_RETAIN_AUTORELEASE
- ARC_RETAIN_BLOCK
- ARC_AUTORELEASEPOOL_PUSH
- ARC_AUTORELEASEPOOL_POP
- ARC_COPY
指令功能与操作数名字描述完全相同,此处不再赘述。
注:运行时虚拟机解释到这此指令时执行相应操作(为方便实现,相关实现用 MRC 编写)。