Swift Runtime-引用计数

前言

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

正文

首先在HeapObject结构体里定义这么一个属性:RefCounts<InlineRefCountBits> 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<InlineRefCountBits>类型,
所以可以最终替换为:

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

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

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

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

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits

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

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

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

InlineRefCounts

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

static const uint64_t PureSwiftDeallocMask = maskForField(PureSwiftDealloc);

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

((uint64_t(1)<<PureSwiftDeallocBitCount)-1) << PureSwiftDeallocShift

再用相应的值替换:

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

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

UnownedRefCountMask = maskForField(UnownedRefCount);
UnownedRefCountMask = (((uint64_t(1)<<UnownedRefCountBitCount)-1) << UnownedRefCountShift) //宏替换后
UnownedRefCountMask = (((uint64_t(1)<<31)-1) << 1)
//0000000000000000000000000000000000000000000000000000000000000001,  左移31位之后:
//0000000000000000000000000000000010000000000000000000000000000000,再减1:
//0000000000000000000000000000000001111111111111111111111111111111,再往左移动1位:
//0000000000000000000000000000000011111111111111111111111111111110,这就是UnownedRefCountMask的值了.

最后计算出所有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<InlineRefCountBits> {
        HeapObjectSideTableEntry*
      }
    }
  }

 HeapObjectSideTableEntry {
    SideTableRefCounts {
      object pointer
      atomic<SideTableRefCountBits> {
        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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容