Swift Runtime-引用计数

前言

在Swift Runtime-初探一文里,我们初步研究了对象的内存结构.有metadataRefcount.接下来我们要研究Refcount,.为什么不是metadata呢?因为Refcount相对于metadata比较简单,让我们的研究由浅入深.

正文

首先在HeapObject结构体里定义这么一个属性:RefCounts refCounts;表示引用计数.

struct HeapObject {
  /// This is always a valid pointer to a metadata object.
  HeapMetadata const *metadata;
  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
  ......
};

SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS为宏定义,搜索源码可以得到:InlineRefCounts refCounts.而InlineRefCounts又是RefCounts类型,
所以可以最终替换为:

struct HeapObject {
  /// This is always a valid pointer to a metadata object.
  HeapMetadata const *metadata;
  RefCounts refCounts;
  ......
};

RefCounts又是什么呢?我们继续搜索RefCounts的定义:

template    //RefCountBits代表一种类型
class RefCounts {
  std::atomic refCounts;  //可以通过RefCounts<类型>来传入类型
  ......略
}

查看源码可知,RefCounts即声明一个RefCounts类,其属性refCountInlineRefCountBits类型.接下来需要查找InlineRefCountBits的定义:

typedef RefCountBitsT InlineRefCountBits

RefCountBitsT又是接受模板参数的定义,查阅RefCountBitsT的定义,发现RefCountBitsT就是承载引用计数的最终数据结构了.那为什么以上一系列定义都采用了模板参数,其实阅读源码的时候就发现,一共有两种RefCounts,只是通过RefCounts,RefCountsBits共享了部分对引用数据操作的实现.缺点是代码阅读起来非常难受. 优点是可以减少重复代码.

HeapObject {
    isa
    InlineRefCounts {
      atomic {
        strong RC + unowned RC + flags
        或者
        HeapObjectSideTableEntry*
      }
    }
  }

  HeapObjectSideTableEntry {
    SideTableRefCounts {
      object pointer
      atomic {
        strong RC + unowned RC + weak RC + flags
      }
    }   
  }

InlineRefCounts

让我们先从相对简单的InlineRefCountBits,即RefCountBitsT入手.
仔细阅读源码,RefCountBitsT代表的是64位长的一段位域,通过field value = (bits & mask) >> shift的方式来得到想要的值.举一个例子,比如针对这个位域,源码里定义了
很多mask,其中一个PureSwiftDeallocMask定义如下:

static const uint64_t PureSwiftDeallocMask = maskForField(PureSwiftDealloc);

maskForField是一个宏定义# define maskForField(name) (((uint64_t(1)<,那么上面的代码就可以转换成:

((uint64_t(1)<

再用相应的值替换:

( (uint64_t(1) << 1) -1 ) << 0
// 用白话文对这段代码解释一下
//首先将uint64_t(1)用二进制表示就是:
//0000000000000000000000000000000000000000000000000000000000000001,  左移1位之后:变成
//0000000000000000000000000000000000000000000000000000000000000010,再减1,又变成
//0000000000000000000000000000000000000000000000000000000000000001,这个结果就是PureSwiftDeallocMask

我再举一个例子,看是如何计算UnownedRefCountMask

UnownedRefCountMask = maskForField(UnownedRefCount);
UnownedRefCountMask = (((uint64_t(1)<

最后计算出所有mask可以得到这样的结论,在InlineRefCountBits类型下,引用计数是这样的位域:


接下来让我打开之前的项目把main.swift修改成如下:

class Person {
    
}

var a = Person()
var b = a
var c = a
print("Hello, World!")

断点停住之后我们打印相应数据,但是这次使用二进制格式输出.这下对于第二段内容就一目了然了:


输出的内容都变成了二进制,我们可以通过(bits & mask) >> shift来计算出对应位域的值.而持有这段位域的RefCountBitsT正是通过这种方式来提供非常多便利的函数来控制相应的值.

SideTableRefCounts

看完InlineRefCounts,再来看一下SideTableRefCounts,那什么时候refcounts会是SideTableRefCounts类型的呢?其实swift源码里已经说了,其中一个比较常见的原因就是有弱引用的形成.上文已经提到SideTableRefCounts的结构:

HeapObject {
    isa
    InlineRefCounts {
      atomic {
        HeapObjectSideTableEntry*
      }
    }
  }

 HeapObjectSideTableEntry {
    SideTableRefCounts {
      object pointer
      atomic {
        strong RC + unowned RC + weak RC + flags
      }
    }   
  }

当变成SideTableRefCounts类型的时候,refcounts不再是位域,而是一个指针,指向HeapObjectSideTableEntry,HeapObjectSideTableEntry里面有类似之前InlineRefCountBitsSideTableRefCountBits,SideTableRefCountBitsInlineRefCountBits多了弱引用计数.还有一个pointer属性,指向sidetable归属的对象.其实HeapObjectSideTableEntry是一个独立的内存区域,对象指向这个sidetable,sidetable也会指向对象.因为side table已知很小,这样就不会有弱引用指向大对象导致的内存浪费,所以问题自然就消失了。这也指明了线程安全问题的简单解决方案:不用提前清零弱引用。既然已知 side table比较小,指向它的弱引用可以持续保留,直到这些引用自身被覆盖或销毁。
以上讨论的是被弱引用对象的处理,那么弱引用的对象是如何处理的呢?我们可以通过指令swiftc -emit-ir main.swift编译swift文件生成IR文件,然后阅读IR文件发现:对于弱引用变量的赋值操作,编译器都会帮你加上对应的操作函数:

  %24 = call %swift.weak* bitcast (%swift.weak* (%swift.weak*, %swift.refcounted*)* @swift_weakAssign to %swift.weak* (%swift.weak*, %T4main6personC*)*)(%swift.weak* returned @"$s4main9bbbbbbbbbAA6personCSgvp", %T4main6personC* %23) #3
  %13 = call %swift.weak* bitcast (%swift.weak* (%swift.weak*, %swift.refcounted*)* @swift_weakInit to %swift.weak* (%swift.weak*, %T4main6personC*)*)(%swift.weak* returned @"$s4main9bbbbbbbbbAA6personCSgvp", %T4main6personC* %12) #3
  //如上就出现了两个函数调用:swift_weakAssign和swift_weakInit,将weak变量和被引用的对象作为参数.

阅读源码发现swift_weakAssignswift_weakInit一类的函数由HeapObject提供支持,一路跟踪发现,weak变量并没有持有被引用对象的指针,而是持有了被引用对象的sidetable,通过访问持有的sidetablepointer指针间接访问引用的对象.这跟Objective-C的weak实现是有区别的,Objective-C的sidetable是存储在一个全局数组里,而Swift则是每个对象都有自己的sidetable.
再来说下swiftsidetable具体操作逻辑:

var a = Object()
weak var b = a //因为a被b弱引用所以生成一个sidetable,同时弱引用计数加一,b也获得这个sidetable
var c = Object()
b = c //b被重新赋值,因为c被弱引用所以生成c的sidetable,同时弱引用计数加一,再被b持有. 因为b有之前a的sidetable,所以对a的sidetable弱引用计数做减一操作,又因为减一之后弱引用计数正好到零,所以a的sidetable会自动销毁.

对象强引用计数归零时,并不会处理自己的sidetable,所以weak变量不会自动置为nil.此时如果weak变量去访问指向的对象,因为sidetable被标记为对象已经销毁,所以代码会返回nil.这里与Objective-C的自动将weak变量的值变成nil有很大区别.

参考

https://juejin.im/post/5c7b835af265da2d881b4457
https://alvinzhu.me/2017/11/15/ios-weak-references.html

你可能感兴趣的:(Swift Runtime-引用计数)