iOS 热更新框架 OCPack 中 ARC 的处理机制

1,092 阅读6分钟

更多精彩文章,请关注作者的微信公众号:码工笔记

上一篇 OCPack 技术方案总结发出后,有同学私信问 ARC 的具体实现细节,正好之前也没有好好总结这一块,于是有了这一篇文章。

在开始这篇文章之前,建议没有看过上一篇文章的同学先看一下,链接如下:

自研 iOS 热更新机制——OCPack技术方案总结

以下正文开始。

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
  • 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 它

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,然后存储到左值变量中
  • 初始化
    • 左值变量中存入空指针
      • 如果对象类型为 __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 编写)。