Swift中的unowned和weak

基础知识

SwiftObjective-C都是利用古老且有效的ARC(Automatic Reference Counting)来管理内存,当实例的引用计数为0时,实例将会被析构,实例占有的内存和资源都将重新变得可用。

但,当两个实例发生循环引用时,那么他们的引用计数会一直大于0,那么他们将不会被析构。

为了解决这个问题,SwiftObjective-C引入了弱引用,弱引用不会被ARC计算。也就是说当一个弱引用指向一个引用类型实例时,引用计数不会增加。

Swift中的弱引用

这里以闭包为例,在OC中,标准的做法是,定义一个弱引用指向闭包外部的实例,然后在闭包内部定义强引用指向这个实例,在闭包执行期间使用它.

类似于下面代码:

__weak typeof(self) weakSelf = self;
    void (^myBlock)(NSString *) = ^(NSString * name) {
        __strong typeof(self) strongSelf = self;
        NSString *name = self.name;
    };

为了更方便的处理循环引用,Swift引入了一个新的概念,用于简化和更加明显的表达在闭包内部,外部变量的捕获:捕获列表(capture list)。使用捕获列表,可以在函数(闭包)的头部定义和指定那些需要用在内部的外部变量,并且指定引用类型(这里是指 unownedweak)。

例子:

不使用捕获列表时,闭包将会创建一个外部变量的强引用。

var i1 = 1, i2 = 1

var fStrong = {
    i1 += 1
    i2 += 2
}

fStrong()
print(i1,i2) //Prints 2 and 3

使用捕获列表,闭包内部会创建一个新的可用常量。如果没有指定常量修饰符,闭包将会简单地拷贝原始值到新的变量中,对于值类型和引用类型都是一样的。

var fCopy = { [i1] in
    print(i1,i2)
}

fStrong()
print(i1,i2) //打印结果是 2 和 3  

fCopy()  //打印结果是 1 和 3

在上面的例子中,在调用fStrong 之前定义函数 fCopy ,在该函数定义的时候,私有常量已经被创建了。正如你所看到的,当调用第二个函数时候,仍然打印 i1 的原始值。

对于外部引用类型的变量,在捕获列表中指定 weakunowned,这个常量将会被初始化为一个弱引用,指向原始值,这种指定的捕获方式就是用来处理循环引用的方式。

class aClass{
    var value = 1
}

var c1 = aClass()
var c2 = aClass()

var fSpec = { [unowned c1, weak c2] in
    c1.value += 1
    if let c2 = c2 {
        c2.value += 1
    }
}

fSpec()
print(c1.value,c2.value) //Prints 2 and 2

两个 aClass 捕获实例的不同的定义方式,决定了它们在闭包中不同的使用方式。

调用步骤

动作 unowned weak
预先调用 #1 对象进行 unowned_retain 操作 创建一个容器,并且对象进行 strong_retain 操作。创建一个可选值,存入到容器中,然后释放可选值
预先调用 #2 strong_retain_unowned,unowned_retain 和 strong_release strong_retain
闭包执行 strong_retain_unowned,unowned_release load_weak, 打开可选值, strong_release
调用之后 unowned_release strong_release
  • unowned_retain:增加堆对象中的 unowned 引用计数。
  • strong_retain_unowned :断言对象的强引用计数大于 0,然后增加这个引用计数。
  • strong_retain:增加对象的强引用计数。
  • load_weak:不是真正的 ARC 调用,但是它将增加可选值指向对象的强引用计数。
  • strong_release:减少对象的强引用计数。如果释放操作把对象强引用计数变为0,对象将被销毁,然后弱引用将被清除。当整个强引用计数和 unowned 引用计数都为0时,对象的内存才会被释放。
  • unowned_release:减少对象的 unowned 引用计数。当整个强引用计数和 unowned 引用计数都为 0 时,对象的内存才会被释放。

使用场景

  • unowned: 引用使用的场景是,原始实例永远不会为 nil,闭包可以直接使用它,并且直接定义为显式解包可选值。当原始实例被析构后,在闭包中使用这个捕获值将导致崩溃
  • 如果捕获原始实例在使用过程中可能为 nil ,必须将引用声明为 weak, 并且在使用之前验证这个引用的有效性。

实现

unomned实现

来源于Swift5.0源码HeapObject.cpp文件。

HeapObject *swift::swift_unownedRetain(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_unownedRetain);
  /// 检测对象是否存在,不存在直接return
  if (!isValidPointerForNativeRetain(object))
    return object;

  /// 将对象的引用计数加1
  object->refCounts.incrementUnowned(1);
  return object;
}

void swift::swift_unownedRelease(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_unownedRelease);
  /// 检测对象是否存在,不存在,直接return
  if (!isValidPointerForNativeRetain(object))
    return;

  // Only class objects can be unowned-retained and unowned-released.
  /// 检测是否为类的对象
  assert(object->metadata->isClassObject());
  assert(static_cast<const ClassMetadata*>(object->metadata)->isTypeMetadata());
  
  /// 检测Unowned引用计数是否能减1
  if (object->refCounts.decrementUnownedShouldFree(1)) {
    auto classMetadata = static_cast<const ClassMetadata*>(object->metadata);
    
    /// 释放Unowned指针,并没有释放该指针指向的内存
    swift_slowDealloc(object, classMetadata->getInstanceSize(),
                      classMetadata->getInstanceAlignMask());
  }
}

到此已经有了一个对象的 unowned 引用,另外一个指令,strong_retain_unowned 用来创建一个强引用:

HeapObject *swift::swift_unownedRetainStrong(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_unownedRetainStrong);
  if (!isValidPointerForNativeRetain(object))
    return object;
  /// 断言来验证对象是否被弱引用,一旦断言通过,将尝试进行增加强引用计数的操作.
  /// 一旦对象在进程中已经被释放,尝试将会失败。
  assert(object->refCounts.getUnownedCount() &&
         "object is not currently unowned-retained");

  /// 尝试增加引用计数
  if (! object->refCounts.tryIncrement())
    /// 引用计数添加失败
    swift::swift_abortRetainUnowned(object);
  return object;
}

weak

Swift5.0源码

/// 初始化弱引用
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);
}

WeakReferenceBits(HeapObjectSideTableEntry *newValue) {
    setNativeOrNull(newValue);
}
/// 创建一个弱引用表,成功则增加弱引用计数。
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}
/// 创建一个对象的散列表,如果该对象释放了,则返回空
// Return an object's side table, allocating it if necessary.
// Returns null if the object is deiniting.
// SideTableRefCountBits specialization intentionally does not exist.
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 allocator
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
  
  auto newbits = InlineRefCountBits(side);
  
  do {
    if (oldbits.hasSideTable()) {
      // Already have a side table. Return it and delete ours.
      // Read before delete to streamline barriers.
      auto result = oldbits.getSideTable();
      delete side;
      return result;
    }
    else if (failIfDeiniting && oldbits.getIsDeiniting()) {
      // Already past the start of deinit. Do nothing.
      return nullptr;
    }
    
    side->initRefCounts(oldbits);
    
    /// 进行 CAS
  } while (! refCounts.compare_exchange_weak(oldbits, newbits,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
  return side;
}

增加引用计数

// Increment the weak reference count.
  void incrementWeak() {
    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));
  }
  
  bool decrementWeakShouldCleanUp() {
    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
    RefCountBits newbits;

    bool performFree;
    do {
      newbits = oldbits;
      performFree = newbits.decrementWeakRefCount();
    } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                              std::memory_order_relaxed));

    return performFree;
  }

SideTable的数据结构

class HeapObjectSideTableEntry {
  // FIXME: does object need to be atomic?
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;

  public:
  HeapObjectSideTableEntry(HeapObject *newObject)
    : object(newObject), refCounts()
  { }

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Winvalid-offsetof"
  static ptrdiff_t refCountsOffset() {
    return offsetof(HeapObjectSideTableEntry, refCounts);
  }

弱引用的访问:

HeapObject *nativeLoadStrong() {
    auto bits = nativeValue.load(std::memory_order_relaxed);
    return nativeLoadStrongFromBits(bits);
}

HeapObject *nativeLoadStrongFromBits(WeakReferenceBits bits) {
    auto side = bits.getNativeOrNull();
    return side ? side->tryRetain() : nullptr;
}

到这里大家发现一个问题没有,被引用对象释放了为什么还能直接访问 Side Table?其实 Swift ABI 中 Side Table 的生命周期与对象是分离的,当强引用计数为 0 时,只有 HeapObject 被释放了。

只有所有的 weak 引用者都被释放了或相关变量被置 nil 后,Side Table 才能得以释放,详见:

void decrementWeak() {
    // FIXME: assertions
    // FIXME: optimize barriers
    bool cleanup = refCounts.decrementWeakShouldCleanUp();
    if (!cleanup)
      return;

    // Weak ref count is now zero. Delete the side table entry.
    // FREED -> DEAD
    assert(refCounts.getUnownedCount() == 0);
    delete this;
  }

  void decrementWeakNonAtomic() {
    // FIXME: assertions
    // FIXME: optimize barriers
    bool cleanup = refCounts.decrementWeakShouldCleanUpNonAtomic();
    if (!cleanup)
      return;

    // Weak ref count is now zero. Delete the side table entry.
    // FREED -> DEAD
    assert(refCounts.getUnownedCount() == 0);
    delete this;
  }

所以即便使用了弱引用,也不能保证相关内存全部被释放,因为只要 weak 变量不被显式置 nil,Side Table 就会存在。而 ABI 中也有可以提升的地方,那就是如果访问弱引用变量时发现被引用对象已经释放,就将自己的弱引用销毁掉,避免之后重复无意义的 CAS 操作。当然 ABI 不做这个优化,我们也可以在 Swift 代码里做。

以上就是Swift weak 弱引用机制实现方式的一个简单的分析,可见思路与 Objective-C runtime 还是很类似的,都采用与对象匹配的 Side Table 来维护引用计数。不同的地方就是 Objective-C 对象在内存布局中没有 Side Table 指针,而是通过一个全局的 StripedMap 来维护对象和 Side Table 之间的关系,效率没有 Swift 这么高。另外 Objective-C runtime 在对象释放时会将所有的 __weak 变量都 zero-out,而 Swift 并没有

小结

在这个实现中,获取一个强引用需要更多复杂同步操作,在多线程竞争严重的情况下,会带来性能损耗

结论

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

推荐阅读更多精彩内容