swift底层探索 07 -内存管理(refCount&weak&unowned)

提到内存管理在iOS开发中,就不得不提ARC(自动引用技术)。本文主要讨论的就是ARC在swift中是如何存储、计算,以及循环引用是如何解决的。
[toc]

一, refCount引用计数(强引用 + 无主引用)

先看一段简单的代码

class classModel{
    var age : Int = 18
}
func test() {
    let c = classModel()
    var c1 = c
    var c2 = c
}
test()

通过LLDB添加断点查看当前c对象的内存情况

图一

  • 通过经验该对象的引用计数应该是:3
  • 可是图一中对象内存中refCopunt:0x0000000600000002,以及通过cfGetRetainCount(AnyObject)获取到的引用计算看起来都是不正确的。

1. cfGetRetainCount - sil解析

class classModel{
    var age : Int = 18
}
let temp = classModel()
CFGetRetainCount(temp)

编译后的Sil文件:


图二
  • 通过图二sil文件很简单的看出CFGetRetainCount在调用之前对temp这个变量进行了一次强引用,也就是引用计数加1。所以通过CFGetRetainCount获得的引用计数需要-1才是正确的。这也印证了之前的经验推论。

2. refCount - 类型的源码

swift底层探索 01 - 类初始化&类结构一文中有对swift类的源码进行过简单的解释。

相信你一定会有疑惑:0x0000000600000002是什么?它为什么被叫做refCount,探索方法依旧是翻开源码!

  • 由于源码中涉及多层嵌套+模板类+泛型,所以阅读起来还是有点困难的,建议自己动手试试。swift-5.3.1源码地址
(1) 该方法是swift对象初始化方法
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }
  • 其中refCounts(InlineRefCounts::Initialized)就是refCounts的初始化方法.
  • InlineRefCountsrefCounts的类型.
(2) InlineRefCounts类型
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
  • InlineRefCounts是重命名
  • InlineRefCounts = RefCounts
(3) RefCounts类
template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;
  ...
  //省略方法
}
  • RefCounts是依赖泛型:RefCountBits的模板类。同时发现refCounts的类型也是泛型:RefCountBits
  • 通过第2步,第3步: RefCounts = RefCountBits = InlineRefCountBits
(4) InlineRefCountBits类型
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
  • InlineRefCountBits也是重命名
  • InlineRefCountBits = RefCountBitsT;
(5) RefCountIsInline枚举
enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true };
  • 传入枚举值:RefCountIsInline = true
(6) RefCountBitsT 核心类
template <RefCountInlinedness refcountIsInline>
class RefCountBitsT {
    //内部变量
    BitsType bits;
    //内部变量类型
    typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
    BitsType;

    ...
    //省略无关代码
}
  • 内部只有一个变量bits,类型为BitsType
(7) RefCountBitsInt 结构体
template <RefCountInlinedness refcountIsInline>
struct RefCountBitsInt<refcountIsInline, 8> {
  typedef uint64_t Type;
  typedef int64_t SignedType;
};
  • 根据第6步的传参得到RefCountBitsInt结构,以及Type == uint64_t
(8) 【总结】
  • 通过第1步,第2步,第3步,第4步: InlineRefCounts = RefCounts = RefCountBits = InlineRefCountBits = RefCountBitsT;(该关系并不严谨只是为了解释简单)
  • 通过第6步,第7步: RefCountBitsTbits类型是:uint64_t;
  • refCounts的类型为RefCountBitsT,内部只有一个变量bits类型为uint64_t;
  • RefCountBitsT是模板类,首地址指向唯一内部变量bits;
  • 结论为:uint64_t : refCounts.

3. refCount - 初始化的源码

现在再看0x0000000600000002知道它是一个uint64_t的值,可是内部存储了哪些值还需要查看初始化方法,观察初始化方法做了什么?

(1) 该方法是swift对象初始化方法
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }
  • Initialized初始化
(2) RefCounts初始化方法
template <typename RefCountBits>
class RefCounts {
    std::atomic<RefCountBits> refCounts;
    
    enum Initialized_t { Initialized };
    
 constexpr RefCounts(Initialized_t)
    : refCounts(RefCountBits(0, 1)) {}
    ...
    //省略无关代码
}
  • 调用了RefCountBits的初始化方法,根据上一步中的关系对应:RefCountBits = InlineRefCountBits = RefCountBitsT
(3) RefCountBitsT初始化方法
  constexpr
  RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
    : bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
           (BitsType(1)                << Offsets::PureSwiftDeallocShift) |
           (BitsType(unownedCount)     << Offsets::UnownedRefCountShift))
  { }
(4)Offsets对应关系

Offsets的关系图:
简书-月月
(5)【总结】
  • 0x0000000600000002就可以拆分为: 5部分。强引用的引用计数位于:33-62
0x0000000600000002 >> 33 // 引用计数 = 3
  • 同样满足之前的论证。
补充1:
  • 初始化并且没有赋值时,引用计数为0,无主引用数为:1。源码中的确也是这样的RefCountBits(0, 1)
补充2:
class PersonModel{
    var age : Int = 18
}
func test() {
    let c = PersonModel()
    var c1 = c
    var c2 = c
    var c3 = c
    //增加了一个无主引用
    unowned var c4 = c
}
test()
图三-输出结果
  • unowned在本文的解决循环引用中会解释。
  • StrongExtraRefCountShift(33-63位) : 0x0000000800000004右移33位 = 4
  • UnownedRefCountShift(1-31位) : 0x0000000800000004左移32位,右移33位。 = 2

4. 引用计数增加、减少

知道了引用计数的数据结构初始化值,现在就需要知道引用计数是如何增加减少,本文中以增加为例;

通过打开汇编,查看调用堆栈:


图三
  • 发现会执行swift_retain这个函数
swift_retain源码
//入口函数
HeapObject *swift::swift_retain(HeapObject *object) {
  CALL_IMPL(swift_retain, (object));
}

static HeapObject *_swift_retain_(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
  if (isValidPointerForNativeRetain(object))
    //引用计数在该函数进行+1操作
    object->refCounts.increment(1);
  return object;
}
  • 后面源码的阅读会进行断点调试的方式。
increment
图四

通过可执行源码进行调试可执行源码

  • 根据断点证实的确是执行到increment函数,并且新增值是1
具体计算的方法
图五
  • 计算都是从33位开始计算的

二, refCount 循环引用

class PersonModel{
    var teach : TeachModel?
}
class TeachModel{
    var person : PersonModel?
}

面对这样的相互包含的两个类,使用时一定会出现相互引用(循环引用)

图六
  • deinit方法没有调用,造成了循环引用。

1. weak关键字

通过OC的经验,可以将其中一个值改为weak,就可以打破循环引用.

class PersonModel{
    weak var teach : TeachModel?
}
class TeachModel{
    weak var person : PersonModel?
}
图六
  • 很显然weak是可以的。问题是:weak做了什么呢?

2. weak 实现源码

weak var weakP = PersonModel()

依旧是打开汇编断点.

图七

  • 从图七能看出到weak是调用了swift_weak
swift_weak源码
//weak入口函数
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
  ref->nativeInit(value);
  return ref;
}

void nativeInit(HeapObject *object) {
//做一个非空判断
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}
  • 没有找到WeakReference对象的创建,猜测是编译器自动创建的用来管理weak动作.
通过formWeakReference创建HeapObjectSideTableEntry
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}
调用allocateSideTable进行创建
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
    //获取当前对象的原本的引用计数(uInt64_t)
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  
  ...
  
  // FIXME: custom side table allocator
  
  //创建HeapObjectSideTableEntry对象
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
    //RefCountBitsT对象进行初始化
  auto newbits = InlineRefCountBits(side);
  
  do {
    if (oldbits.hasSideTable()) {
      auto result = oldbits.getSideTable();
      delete side;
      return result;
    }
    else if (failIfDeiniting && oldbits.getIsDeiniting()) {
      return nullptr;
    }
    side->initRefCounts(oldbits);
    //通过地址交换完成赋值
  } while (! refCounts.compare_exchange_weak(oldbits, newbits,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
  return side;
}
  • 最终将RefCountBitsT对象(class)的地址和旧值uint_64进行交换。
HeapObjectSideTableEntry对象
class HeapObjectSideTableEntry {
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;
    ...
}

class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
    //weak_count
  uint32_t weakBits;
}

class RefCountBitsT {
    //Uint64_t就是strong_count | unowned_count
    BitsType bits;
}

通过源码分析得出HeapObjectSideTableEntry对象的内存分布

RefCountBitsT初始化

最终保存到实例对象的refcount字段的内容(RefCountBitsT)创建

    //Offsets::SideTableUnusedLowBits = 3
    //SideTableMarkShift 高位 62位
    //UseSlowRCShift 高位 63位
  RefCountBitsT(HeapObjectSideTableEntry* side)
    : bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
           | (BitsType(1) << Offsets::UseSlowRCShift)
           | (BitsType(1) << Offsets::SideTableMarkShift))
  {
    assert(refcountIsInline);
  }
  • 62位,63位改为0 -> 整体左移3位: 就可以得到sideTable对象的地址。

lldb验证

现在知道了refcount字段获取规律,以及sideTable对象的内部结构,现在通过lldb验证一下。

图八

  • 发现被weak修饰之后,refcount变化成sideTable对象地址+高位标识符
图九
  • 将高位62,63变为0后,在左移3位.
图十
  • 0x10325D870这就是sideTable对象地址

weak_count 增加

weakcount是从第二位开始计算的。
formWeakReference函数中出现了side->incrementWeak();sideTable对象创建完成后调用了该函数.

  HeapObjectSideTableEntry* incrementWeak() {
    if (refCounts.isDeiniting())
      return nullptr;
      //没有销毁就调用
    refCounts.incrementWeak();
    return this;
  }
  
  void incrementWeak() {
    //获取当前的sideTable对象
    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
    RefCountBits newbits;
    do {
      newbits = oldbits;
      assert(newbits.getWeakRefCount() != 0);
      //调用核心自增函数
      newbits.incrementWeakRefCount();
      
      if (newbits.getWeakRefCount() < oldbits.getWeakRefCount())
        swift_abortWeakRetainOverflow();
        //通过值交换完成赋值
    } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                              std::memory_order_relaxed));
  }
  
  void incrementWeakRefCount() {
  //就是一个简单++
    weakBits++;
  }
  1. 在声明weak后,调用了incrementWeak自增方法;
  2. incrementWeak方法中获取了sideTable对象;
  3. incrementWeakRefCount完成了weakBits的自增;

注:在weak引用之后,在进行strong强引用后,refCount该如何计算呢?篇幅问题就不展开了,各位可以自己试试。

三, 捕获列表

  • [weak t] / [unowned t] 在swift中被称为捕获列表
  • 作用:
    1. 解决closure的循环引用;
    2. 进行外部变量的值捕获

本次换个例子。

class TeachModel{
    var age = 18
    var closure : (() -> Void)?
    deinit {
        print("deinit")
    }
}
func test() {
    let b = TeachModel()
    b.closure = {
        b.age += 1
    }
    print("end")
}
  • 看到这段代码,deinit会不会执行呢?答案是很显然的,实例对象的闭包和实例对象相互持有,一定是不会释放的。

作用1-解决循环引用

func test() {
    let b = TeachModel()
    b.closure = {[weak b] in
        b?.age += 1
    }
    print("end")
}

func test() {
    let b = TeachModel()
    b.closure = {[unowned b] in
        b?.age += 1
    }
    print("end")
}

执行效果,都可以解决循环引用:


  • weak修饰之后对象会变为

作用2-捕获外部变量

例如这样的代码:

func test() {
    var age = 18
    var height = 1.8
    var name = "Henry"
    
    height = 2.0
    //age,height被闭包进行了捕获
    let closure = {[age, height] in
        print(age)
        print(height)
        print(name)
    }
    
    age = 20
    height = 1.85
    name = "Wan"
    
    //猜猜会输出什么?    
    closure()
}
  • age,height被捕获之后,值虽然被外部修改但不会影响闭包内的值
  • 闭包捕获的值时机为闭包声明之前
闭包捕获之后值发生了什么?

通过打开汇编调试,并查看寄存器堆栈信息.


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

推荐阅读更多精彩内容