Swift之深入解析内存管理的底层原理

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift之深入解析内存管理的底层原理相关的知识,希望对你有一定的参考价值。

一、Swift 内存管理

① ARC
  • 跟 OC 一样,Swift 也是采用基于引用计数的 ARC 内存管理方案(针对堆空间);
  • Swift 的 ARC 中有三种引用:
    • 强引用(strong reference):默认情况下,引用都是强引用;
    • 弱引用(weak reference):通过 weak 定义弱引用;
      • 必须是可选类型的 var,因为实例销毁后,ARC 会自动将弱引用设置为 nil;
      • ARC 自动给弱引用设置 nil 时,不会触发属性观察器;
    • 无主引用(unowned reference):通过 unowned 定义无主引用;
      • 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似 OC 中的 unsafe_unretained);
      • 视图在实例销毁后访问无主引用,会产生运行时错误(野指针)。
② weak/unowned 使用限制
  • weak、unowned 只能用在类实例上面,这是因为一般只有类实例放堆空间,结构体、枚举一般都是不放在堆空间的;
	class Car {
	}
	
	protocol Actions : AnyObject {
	}
	
	weak var c0: Car?
	weak var c1: AnyObject?
	weak var c3: Actions?
	
	unowned var c4: Car?
	unowned var c5: AnyObject?
	unowned var c6: Actions?
  • 上面代码编译都是没问题的,AnyObject 是可以代表任意类类型,协议 Actions 也是可以的,因为它后面是 Actions :AnyObject,意思就是它的协议只能被类类型遵守。
  • 若协议 Actions 去掉后面的冒号,c3 和 c6 是编译不通过的,因为此协议有可能被结构体、枚举遵守,而 weak、unowned 只能用在类实例上面,所以编译器提前抛出错误,Swift 是强安全语言。
③ Autoreleasepool
  • 在 Swift 中,Autoreleasepool 是保留的,变成了一个全局的函数:
	public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result
  • 使用如下:
	class Car {
	    var name : String?
	    init(name :String?) {
	        self.name = name;
	    }
	    func drive() {
	    }
	}
	autoreleasepool {
	    let car = Car(name: "宝马")
	    car.drive()
	}
  • 内存开销较大的场景(比如数千对经纬度数据在地图上绘制公交路线轨迹),可以使用自动释放池。
④ 循环引用(Reference Cycle)
  • weak/unowned 都可以解决循环引用的问题,但是 unowned 要比 weak 少一些性能消耗;weak 在实例销毁的时候设置了一遍 weak 引用为 nil,所以在性能上会多一部分消耗;
  • 在生命周期中可能会变为 nil 的对象,使用 weak;初始化赋值后再也不会改变为 nil 的对象,使用 unowned;
  • 闭包表达式默认会对用到的外层对象产生额外的强引用(对外层对象进行了 retain 操作),如下:
	class Person {
	    var read:(() -> ())?
	    func listen() {
	        print("listen")
	    }
	    deinit {
	        print("deinit")
	    }
	}
	func test() {
	    let p = Person()
	    p.read = {
	        p.listen()
	    }
	}
	print(1)
	test()
	print(2)
  • 运行后,打印结果只有 1 和 2,没有 deinit,说明 p 对象一直没有被销毁,可以看到问题出在这里:
	p.read = {
		p.listen()
	}
  • 对象 p 里的 read 方法强引用了闭包,而闭包里也强引用了对象 p,两者形成循环引用,对象 p 也就无法被释放销毁。
  • 在 p.read 处打上断点,进入汇编调试,看看是否有强引用(retain),强引用下引用计数器变化如下所示:

在这里插入图片描述

  • 注释掉上面代码里的 p.read = { p.listen() },再看汇编:

在这里插入图片描述

  • 可以看出,在 p.read = { p.listen() } 里,闭包对 p 对象进行了强引用也就是 retain 操作,构成了引用计数始终为 1 的情况,无法释放对象。
  • 在闭包表达式的捕获列表声明 weak 或 unowned 引用,解决循环引用问题,如下所示:
	func test() {
	    let p = Person()
	    p.read = {
	        [weak p] in
	        p?.listen()
	    }
	}
	
	func test() {
	    let p = Person()
	    p.read = {
	        [unowned p] in
	        p.listen()
	    }
	}
	
	func test() {
	    let p:Person? = Person()
	    p?.read = {
	        [weak p] in
	        p?.listen()
	    }
	}
	
	func test() {
	    let p:Person? = Person()
	    p?.read = {
	        [unowned p] in
	        p?.listen()
	    }
	}
  • weak 弱引用必须是可选类型,所以,对象 p 后面需要加上 ? ;
  • 若是 unowned 修饰 p,p 后面不加 ?,因为 p 本身就是非可选类型,unowned 默认情况下也就是非可选类型,是跟着 p 执行的。
  • 如下所示,[weak p] 是捕获列表,(age) 是参数列表,捕获列表一般是写在参数列表前面的,in 后面的就是函数体:
	class Person {
	    var read :((Int) -> ())?
	    func listen() {
	        print("listen")
	    }
	    deinit {
	        print("deinit")
	    }
	}
	func test() {
	    let p = Person()
	    p.read = {
	        [weak wp = p](age) in
	        wp?.listen()
	    }
	}
  • 如果想在定义闭包属性的同时引用 self,那么闭包必须是 lazy 的(因为在实例初始化完毕之后才能引用 self):
	class Person {
	    lazy var read :(() -> ()) = {
	        self.listen()
	    }
	    func listen() {
	        print("listen")
	    }
	    deinit {
	        print("deinit")
	    }
	}
	func test() {
	    let p = Person()
	}
  • 运行上面的代码,可以看到会打印 deinit,这说明对象 p 被释放。常规来说,对象 p 有个强引用 read 引用了闭包表达式,闭包表达式里也强引用 self,两者形成循环引用,无法释放对象 p,可现在却被释放了,这是为什么呢?这是因为 read 是 lazy 修饰的,在未调用 p.read 的时候是没有值的,那么它后面的闭包表达也就不存在,自然就无法引用 self,也就不能造成循环引用。
  • 当第一次调用 p.read() 后,才会触发 read 的初始化,创建闭包表达式赋值给 read,就形成了循环引用。解决如下:
	lazy var read:(() -> ()) = {
	        [weak weakSelf = self] in
	        weakSelf?.listen()
	    }
	
	lazy var read:(() -> ()) = {
	        [unowned weakSelf = self] in
	        weakSelf.listen()
	    }
  • 如果 lazy 属性是闭包调用的结果,则不用考虑循环引用的问题(因为闭包调用后,闭包的生命周期也就结束):
	class Person {
	    var age: Int = 10
	    lazy var getAge: Int = {
	        self.age
	    }()
	    deinit {
	        print("deinit")
	    }
	}
	func test() {
	    let p = Person()
	    print(p.getAge)
	}
	test()
⑤ 内存访问冲突(Conflicting Access to Memory)
  • 内存访问冲突会在两个访问满足下面条件时发生:
    • 至少一个是写入操作;
    • 它们访问的是同一块内存;
    • 它们的访问时间重叠(比如在同一个函数内)。
  • 如下所示:
	// 不存在内存访问冲突
	func plus(_ num: inout Int) -> Int {
	    num + 1
	}
	var number = 1
	number = plus(&number)
	
	// 存在内存访问冲突
	var step = 1
	func increment(_ num: inout Int) {
	    // 此处编译没问题,但运行报错
	    // Simultaneous accesses to 0x100008178, but modification requires exclusive access
	    num += step
	}
	increment(&step)
  • increment 函数内的 num += step 产生内存冲突,因为 num 虽然是形参,但外面传的值还是 step 的内存地址,+= 就造成了同一时间对同一份内存进行既读又写的操作,所以造成内存冲突。
  • 上面的解决方式如下:
	var step = 1
	func increment(_ num: inout Int) {
	    num += step
	}
	var temp = step
	increment(&temp)
	step = temp
  • 如果下面条件可以满足,说明重叠访问结构体的属性是安全的:
    • 只访问实例存储属性,不是计算属性或者类属性;
    • 结构体是局部变量而非全局变量;
    • 结构体要么没有被闭包捕获要么只被非逃逸闭包捕获。
	func test() {
	    var aa = AA(x: 1, y: 2)
	    sum(&aa.x, &aa.y)
	}

二、Swift 内存管理底层分析

① 强引用
  • 现有如下示例代码:
	class YDWTeacher {
	    var age : Int = 18
	    var name : String = "DW"
	}
	var t = YDWTeacher()
	var t1 = t
	var t2 = t
  • 查看 t 的内存情况,为什么其中的 refCounts 是 0x0000000600000003?调试如下:
	po t
	<YDWTeacher 0x10065bb60>
	x/8g 0x10065bb60
	0x10065bb60: 0x10000000100008178 0x0000000600000003
	0x10065bb70: 0x10000000100000012 0x00000000004c4a43
	0x10065bb80: 0xe3000000000000000 0x000000000000005f
	0x10065bb90: 0x000000009a0080001 0x00007fff80bfb718
  • 了解过 Swift 类的底层原理,应该知道类有一个 HeapObject,具体请参考我之前的博客:Swift之深入解析“类”的底层原理,我们通过 HeapObject 类来分析 t 的引用计数。
  • 分析源码 HeapObject -> InlineRefCounts:
	struct HeapObject {
	  HeapMetadata const *metadata;
	
	  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
	  ...
	}
	
	#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \\
	  InlineRefCounts refCounts
  • 进入 InlineRefCounts 定义,可以看到是 RefCounts 类型的别名,而 RefCounts 是模板类,真正决定的是传入的类型 InlineRefCountBits:
	typedef RefCounts<InlineRefCountBits> InlineRefCounts;
	
	template <typename RefCountBits>
	class RefCounts {
	  std::atomic<RefCountBits> refCounts;
	  ...
	}
  • 分析 InlineRefCountBits,它是 RefCountBitsT 类的别名:
	typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
  • 分析 RefCountBitsT,有 bits 属性,bits 其实质是将 RefCountBitsInt 中的 type 属性取了一个别名,所以 bits 的真正类型是 uint64_t,即 64 位整型数组:
	template <RefCountInlinedness refcountIsInline>
	class RefCountBitsT {
	    ...
	      typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
	    BitsType;
	    ...
	    BitsType bits;
	    ...
	}

	template <>
	struct RefCountBitsInt<RefCountNotInline, 4> {
	  // 类型
	  typedef uint64_t Type;
	  typedef int64_t SignedType;
	};
  • 继续查看初始化源码 swift_allocObject:
	static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
	                                       size_t requiredSize,
	                                       size_t requiredAlignmentMask) {
	    ...
	    new (object) HeapObject(metadata);
	    ...
	}

	// 构造函数
	 constexpr HeapObject(HeapMetadata const *newMetadata) 
	    : metadata(newMetadata)
	    , refCounts(InlineRefCounts::Initialized) { 
	    
	  }
  • 进入 Initialized 定义,是一个枚举,其对应的 refCounts 方法中:
    enum Initialized_t { Initialized };
  
    // 对应的RefCounts方法
	// Refcount of a new object is 1.
	constexpr RefCounts(Initialized_t)
	: refCounts(RefCountBits(0, 1)) {}
  • 进入 Initialized 定义,可以看到是一个枚举,其对应的 refCounts 方法,执行 RefCountBits:
    enum Initialized_t { Initialized };
  
    // 对应的RefCounts方法
	// Refcount of a new object is 1.
	constexpr RefCounts(Initialized_t)
	: refCounts(RefCountBits(0, 1)) {}
  • 进入RefCountBits 定义,它也是一个模板定义:
	template <typename RefCountBits>
	class RefCounts {
	  std::atomic<RefCountBits> refCounts;
	  ...
	}
  • 因此初始化实际上执行的是 RefCountBitsT,根据 Offsets 做了一个位域操作:
	LLVM_ATTRIBUTE_ALWAYS_INLINE
	constexpr
	RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
	: bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
	       (BitsType(1)                << Offsets::PureSwiftDeallocShift) |
	       (BitsType(unownedCount)     << Offsets::UnownedRefCountShift))
	{ 
	}
  • RefCountsBit 的结构如下:
    在这里插入图片描述
  • 将上面 t 的 refCounts 用二进制展示,其中强引用计数为 3:

在这里插入图片描述

  • SIL 分析
    • 当只有 t 实例变量时,SIL 分析如下:

在这里插入图片描述

    • 当有t + t1 两个变量的时候,查看是否有 strong_retain 操作:
	// SIL 中的 main
	alloc_global @main.t1 : main.YDWTeacher       // id: %8
	%9 = global_addr @main.t1 : main.YDWTeacher : $*YDWTeacher // user: %11
	%10 = begin_access [read] [dynamic] %3 : $*YDWTeacher // users: %12, %11
	copy_addr %10 to [initialization] %9 : $*YDWTeacher // id: %11
	
	// 其中 copy_addr 等价于
	- %new = load s*YDWTeacher
	- strong_retain %new
	- store %new to %9
    • 其中,strong_retain 对应的就是 swift_retain,内部是一个宏定义,实现的是对 object 的引用计数作 +1 操作;
	// 内部宏定义
	HeapObject *swift::swift_retain(HeapObject *object) {
	  CALL_IMPL(swift_retain, (object));
	}

	// 本质调用 _swift_retain_
	static HeapObject *_swift_retain_(HeapObject *object) {
	  SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
	  if (isValidPointerForNativeRetain(object))
	    object->refCounts.increment(1);
	  return object;
	}

	void increment(uint32_t inc = 1) {
	    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
	    
	    // constant propagation will remove this in swift_retain, it should only
	    // be present in swift_retain_n
	    if (inc != 1 && oldbits.isImmortal(true)) {
	      return;
	    }
	    // 64位bits
	    RefCountBits newbits;
	    do {
	      newbits = oldbits;
	      bool fast = newbits.incrementStrongExtraRefCount(inc);
	      if (SWIFT_UNLIKELY(!fast)) {
	        if (oldbits.isImmortal(false))
	          return;
	        return incrementSlow(oldbits, inc);
	      }
	    } while (!refCounts.compare_exchange_weak(oldbits, newbits,
	                                              std::memory_order_relaxed));
	  }
  • 回到 HeapObject,从 InlineRefCounts 进入,其中是 c++ 中的模板定义,是为了更好的抽象,在其中查找 bits(即 decrementStrongExtraRefCount 方法):
	LLVM_NODISCARD LLVM_ATTRIBUTE_ALWAYS_INLINE
	bool incrementStrongExtraRefCount(uint32_t inc) {
	// This deliberately overflows into the UseSlowRC field.
	// 对inc做强制类型转换为 BitsType
	// 其中 BitsType(inc) << Offsets::StrongExtraRefCountShift 等价于 1<<33位,16进制为 0x200000000
	// 这里的 bits += 0x200000000,将对应的33-63转换为10进制,为
	bits += BitsType(inc) << Offsets::StrongExtraRefCountShift;
	return (SignedBitsType(bits) >= 0);
	}
  • 例如以 t 的 refCounts 为例(其中 62-33 位是 strongCount,每次增加强引用计数,都是在 33-62 位上增加的,固定的增量为 1 左移 33 位,即 0x200000000);
    • 只有 t 时的 refCounts 是 0x0000000200000003;
    • t + t1 时的 refCounts 是 0x0000000400000003 = 0x0000000200000003 + 0x200000000;
    • t + t1 + t2 时的 refCounts 是 0x0000000600000003 = 0x0000000400000003 + 0x200000000;
  • 通过 CFGetRetainCount 获取 t 的引用计数,发现依次是 2、3、4,默认增加 1:
	class YDWTeacher {
	    var age : Int = 18
	    var name : String = "DW"
	}
	var t = YDWTeacher()
	print(CFGetRetainCount(t as AnyObject))
	var t1 = t
	print(CFGetRetainCount(t as AnyObject))
	var t2 = t1
	print(CFGetRetainCount(t as AnyObject))

	// 打印如下
	2
	3
	4
  • 为什么强引用计数是增加 0x200000000 呢?这是因为 1 左移 33 位,其中 4 位为一组,换算成 16 进制,剩余的 33-32 位 0x10,转换为 10 进制为 2,其实际增加引用计数就是 1。
  • 需要注意的是:OC 中创建实例对象时引用计数为 0,swift 中创建实例对象时引用计数默认为 1。
② 弱引用
  • 现有以下调试代码:
	class YDWTeacher {
	    var age : Int = 25
	    var name : String = "DW"
	    var student : YDWStudent?
	}
	
	class YDWStudent {
	    var age : Int = 15
	    var teacher : YDWTeacher?
	}
	
	func test() {
	    var t = YDWTeacher()
	    weak var t1 = t
	}
  • 查看 t 的引用计数变化:
	po t
	<YDWTeacher 0x10182bff0>
	x/8g 0x10182bff0
	0x10182bff0: 0x00000000100008280 0xc000000020809a6c
	0x10182c000: 0x00000000000000012 0x00000000004c4a43
	0x10182c010: 0xe3000000000000000 0x0000000000000000
	0x10182c020: 0x00000000000000000 0x0000000000000000
  • 弱引用声明的变量是一个可选值,因为在程序运行过程中是允许将当前变量设置为 nil 的;
  • 在 t1 处加断点,汇编调试,可以看到执行了:symbol stub for:swift_weakInit 函数;
  • 查看 swift_weakInit 函数,它是由 WeakReference 来调用的,相当于 weak 字段在编译器声明过程中就自定义了一个 WeakReference 的对象,其目的在于管理弱引用:
	WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
	  ref->nativeInit(value);
	  return ref;
	}
  • 继续进入 nativeInit:
	void nativeInit(HeapObject *object) {
	auto side = object ? object->refCounts.formWeakReference() : nullptr;
	nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
	}
  • 进入 formWeakReference,它创建了 sideTable:
	template <>
	HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference() {
	  // 创建 sideTable
	  auto side = allocateSideTable(true);
	  if (side)
	  	// 如果创建成功,则增加弱引用
	    return side->incrementWeak();
	  else
	    return nullptr;
	}
  • 进入 allocateSideTable 方法:
	template <>
	HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
	{
	  // 先拿到原本的引用计数
	  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
	  
	  // Preflight failures before allocating a new side table.
	  if (oldbits.hasSideTable()) {
	    // Already have a side table. Return it.
	    return oldbits.getSideTable();
	  } 
	  else if (failIfDeiniting && oldbits.getIsDeiniting()) {
	    // Already past the start of deinit. Do nothing.
	    return nullptr;
	  }
	
	  // Preflight passed. Allocate a side table.
	  
	  // FIXME: custom side table allo

以上是关于Swift之深入解析内存管理的底层原理的主要内容,如果未能解决你的问题,请参考以下文章

iOS之深入解析内存管理Tagged Pointer的底层原理

Swift之深入解析“泛型”的底层原理

iOS之深入解析内存管理的引用计数retainCount的底层原理

Swift之深入解析反射Mirror的底层原理

iOS之深入解析内存管理散列表SideTables和弱引用表weak_table的底层原理

Swift之深入解析协议Protocol的底层原理