前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >swift底层探索 07 -内存管理(refCount&weak&unowned)swift底层探索 07 -内存管理(refCount&weak&unowned)

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

作者头像
用户8893176
发布2021-08-09 11:17:04
1K0
发布2021-08-09 11:17:04
举报
文章被收录于专栏:小黑娃Henry

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

toc

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

先看一段简单的代码

代码语言:javascript
复制
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解析
代码语言:javascript
复制
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对象初始化方法
代码语言:javascript
复制
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }
  • 其中refCounts(InlineRefCounts::Initialized)就是refCounts的初始化方法.
  • InlineRefCountsrefCounts的类型.
(2) InlineRefCounts类型
代码语言:javascript
复制
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
  • InlineRefCounts是重命名
  • InlineRefCounts = RefCounts
(3) RefCounts类
代码语言:javascript
复制
template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;
  ...
  //省略方法
}
  • RefCounts是依赖泛型:RefCountBits的模板类。同时发现refCounts的类型也是泛型:RefCountBits
  • 通过第2步,第3步: RefCounts = RefCountBits = InlineRefCountBits
(4) InlineRefCountBits类型
代码语言:javascript
复制
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
  • InlineRefCountBits也是重命名
  • InlineRefCountBits = RefCountBitsT;
(5) RefCountIsInline枚举
代码语言:javascript
复制
enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true };
  • 传入枚举值:RefCountIsInline = true
(6) RefCountBitsT 核心类
代码语言:javascript
复制
template <RefCountInlinedness refcountIsInline>
class RefCountBitsT {
    //内部变量
    BitsType bits;
    //内部变量类型
    typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
    BitsType;

    ...
    //省略无关代码
}
  • 内部只有一个变量bits,类型为BitsType
(7) RefCountBitsInt 结构体
代码语言:javascript
复制
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对象初始化方法
代码语言:javascript
复制
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }
  • Initialized初始化
(2) RefCounts初始化方法
代码语言:javascript
复制
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初始化方法
代码语言:javascript
复制
  constexpr
  RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
    : bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
           (BitsType(1)                << Offsets::PureSwiftDeallocShift) |
           (BitsType(unownedCount)     << Offsets::UnownedRefCountShift))
  { }
(4)Offsets对应关系

Offsets的关系图:undefined

简书-月月

(5)【总结】
  • 0x0000000600000002就可以拆分为: 5部分。强引用的引用计数位于:33-62
代码语言:javascript
复制
0x0000000600000002 >> 33 // 引用计数 = 3
  • 同样满足之前的论证。
补充1:
  • 初始化并且没有赋值时,引用计数为0,无主引用数为:1。源码中的确也是这样的RefCountBits(0, 1)
补充2:
代码语言:javascript
复制
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源码
代码语言:javascript
复制
//入口函数
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 循环引用

代码语言:javascript
复制
class PersonModel{
    var teach : TeachModel?
}
class TeachModel{
    var person : PersonModel?
}

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

图六

  • deinit方法没有调用,造成了循环引用。
1. weak关键字

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

代码语言:javascript
复制
class PersonModel{
    weak var teach : TeachModel?
}
class TeachModel{
    weak var person : PersonModel?
}

图六

  • 很显然weak是可以的。问题是:weak做了什么呢?
2. weak 实现源码
代码语言:javascript
复制
weak var weakP = PersonModel()

依旧是打开汇编断点.

图七

  • 从图七能看出到weak是调用了swift_weak
swift_weak源码
代码语言:javascript
复制
//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
代码语言:javascript
复制
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}
调用allocateSideTable进行创建
代码语言:javascript
复制
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对象
代码语言:javascript
复制
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)创建

代码语言:javascript
复制
    //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对象创建完成后调用了该函数.

代码语言:javascript
复制
  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. 进行外部变量的值捕获

本次换个例子。

代码语言:javascript
复制
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-解决循环引用
代码语言:javascript
复制
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-捕获外部变量

例如这样的代码:

代码语言:javascript
复制
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是存在栈区的。
几种基本汇编指令详解
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/12/28 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一, refCount引用计数(强引用 + 无主引用)
    • 1. cfGetRetainCount - sil解析
      • 2. refCount - 类型的源码
        • (1) 该方法是swift对象的初始化方法
        • (2) InlineRefCounts类型
        • (3) RefCounts类
        • (4) InlineRefCountBits类型
        • (5) RefCountIsInline枚举
        • (6) RefCountBitsT 核心类
        • (7) RefCountBitsInt 结构体
        • (8) 【总结】
      • 3. refCount - 初始化的源码
        • (1) 该方法是swift对象的初始化方法
        • (2) RefCounts的初始化方法
        • (3) RefCountBitsT的初始化方法
        • (4)Offsets对应关系
        • (5)【总结】
        • 补充1:
        • 补充2:
      • 4. 引用计数增加、减少
        • swift_retain源码
        • increment
        • 具体计算的方法
    • 二, refCount 循环引用
      • 1. weak关键字
        • 2. weak 实现源码
          • swift_weak源码
          • HeapObjectSideTableEntry对象
          • RefCountBitsT初始化
        • lldb验证
          • weak_count 增加
          • 三, 捕获列表
            • 作用1-解决循环引用
              • 作用2-捕获外部变量
                • 闭包捕获之后值发生了什么?
                • 几种基本汇编指令详解
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档