Swift底层原理-内存管理

Swift底层原理-内存管理

  • Swift语言延续了和Objective-C语言一样的思路进行内存管理,都是采用引用计数的方式来管理实例的内存空间;

  • 在结构体与类中我们了解到Swift对象本质是一个HeapObject结构体指针。HeapObject结构中有两个成员变量,metadatarefCountsmetadata 是指向元数据对象的指针,里面存储着类的信息,比如属性信息,虚函数表等。而 refCounts 通过名称可以知道,它是一个引用计数信息相关的东西。接下来我们研究一下 refCounts

refCounts

  • 在源码HeapObject.h文件中,我们可以找到HeapObject结构体中关于refCounts的定义
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  InlineRefCounts refCounts

/// The Swift heap-object header.
/// This must match RefCountedStructTy in IRGen.
struct HeapObject {
  /// This is always a valid pointer to a metadata object.
  HeapMetadata const *metadata;

  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;

  // 省略部分代码
}
  • 我们看到 refCounts 的类型为 InlineRefCounts,在RefCount.h文件中找到 InlineRefCounts 的定义:
typedef RefCounts<InlineRefCountBits> InlineRefCounts;

RefCounts

  • 发现InlineRefCounts是一个模版类:RefCounts,接收一个InlineRefCountBits类型的范型
template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;

  // Out-of-line slow paths.

  LLVM_ATTRIBUTE_NOINLINE
  void incrementSlow(RefCountBits oldbits, uint32_t inc) SWIFT_CC(PreserveMost);

  LLVM_ATTRIBUTE_NOINLINE
  void incrementNonAtomicSlow(RefCountBits oldbits, uint32_t inc);

  LLVM_ATTRIBUTE_NOINLINE
  bool tryIncrementSlow(RefCountBits oldbits);

  LLVM_ATTRIBUTE_NOINLINE
  bool tryIncrementNonAtomicSlow(RefCountBits oldbits);

  LLVM_ATTRIBUTE_NOINLINE
  void incrementUnownedSlow(uint32_t inc);

  public:
  enum Initialized_t { Initialized };
  enum Immortal_t { Immortal };
  // 省略部分方法
}
  • 根据RefCounts的定义我们发现,其实质上是在操作我们传递的泛型参数InlineRefCountBits
  • 我们看一下InlineRefCountBits的定义
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
  • 它也是一个模板函数,并且也有一个参数 RefCountIsInline,而RefCountIsInline其实就是true。我们重点看一下RefCountBitsT的结构
template <RefCountInlinedness refcountIsInline>
class RefCountBitsT {

  friend class RefCountBitsT<RefCountIsInline>;
  friend class RefCountBitsT<RefCountNotInline>;
  
  static const RefCountInlinedness Inlinedness = refcountIsInline;

  typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
    BitsType;
  typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::SignedType
    SignedBitsType;
  typedef RefCountBitOffsets<sizeof(BitsType)>
    Offsets;

  BitsType bits;
    
  // 省略部分代码
}
  • RefCountBitsT中,发现只有一个bits属性,而该属性是由RefCountBitsIntType属性定义的;
  • 我们来看一下RefCountBitsInt的结构:
template <RefCountInlinedness refcountIsInline>
struct RefCountBitsInt<refcountIsInline, 8> {
  typedef uint64_t Type;
  typedef int64_t SignedType;
};
  • 可以看到,Type 的类型是一个 uint64_t 的位域信息,在这个 uint64_t 的位域信息中存储着运行生命周期的相关引用计数

引用计数初始化流程

  • 我们创建一个新的实例对象时,他的引用计数是多少呢?从源码中我们找到HeapObject的初始化方法:
static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                       size_t requiredSize,
                                       size_t requiredAlignmentMask) {
  assert(isAlignmentMask(requiredAlignmentMask));
  auto object = reinterpret_cast<HeapObject *>(
      swift_slowAlloc(requiredSize, requiredAlignmentMask));

  // NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
  // check on the placement new allocator which we have observed on Windows,
  // Linux, and macOS.
  new (object) HeapObject(metadata);

  // If leak tracking is enabled, start tracking this object.
  SWIFT_LEAKS_START_TRACKING_OBJECT(object);

  SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);

  return object;
}
  • 调用了HeapObject初始化方法
constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }
  • refCounts赋值了Initialized,我们继续分析发现Initialized是一个枚举类型Initialized_t
enum Initialized_t { Initialized };
enum Immortal_t { Immortal };

// RefCounts must be trivially constructible to avoid ObjC++
// destruction overhead at runtime. Use RefCounts(Initialized)
// to produce an initialized instance.
RefCounts() = default;

// Refcount of a new object is 1.
constexpr RefCounts(Initialized_t)
    : refCounts(RefCountBits(0, 1)) {}
  • 而根据注释得知,一个新的实例被创建时,传入的是RefCountBits(0,1),并且我们可以看到 refCounts 函数的参数传的不就是前面提到RefCountBitsT类型参数,我们找到RefCountBitsT初始化方法
LLVM_ATTRIBUTE_ALWAYS_INLINE
constexpr
RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
  : bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
    (BitsType(1)                << Offsets::PureSwiftDeallocShift) |
    (BitsType(unownedCount)     << Offsets::UnownedRefCountShift))
{ }
  • 已知外部调用RefCountBitsT初始化方法,strongExtraCount 传 0,unownedCount 传 1。
  • 然后我们去查看几个偏移的定义
# define shiftAfterField(name) (name##Shift + name##BitCount)

template <>
struct RefCountBitOffsets<8> {  
  static const size_t PureSwiftDeallocShift = 0;
  static const size_t PureSwiftDeallocBitCount = 1;
  static const uint64_t PureSwiftDeallocMask = maskForField(PureSwiftDealloc);

  static const size_t UnownedRefCountShift = shiftAfterField(PureSwiftDealloc);
  static const size_t UnownedRefCountBitCount = 31;
  static const uint64_t UnownedRefCountMask = maskForField(UnownedRefCount);

  static const size_t StrongExtraRefCountShift = shiftAfterField(IsDeiniting);
  static const size_t StrongExtraRefCountBitCount = 30;
  static const uint64_t StrongExtraRefCountMask = maskForField(StrongExtraRefCount);
    
    // 结果分析
  StrongExtraRefCountShift = shiftAfterField(IsDeiniting)
                           = IsDeinitingShift + IsDeinitingBitCount
                           = shiftAfterField(UnownedRefCount) + 1
                           = UnownedRefCountShift + UnownedRefCountBitCount + 1
                           = shiftAfterField(PureSwiftDealloc) + 31 + 1
                           = PureSwiftDeallocShift + PureSwiftDeallocBitCount + 31 + 1
                           = 0 + 1 + 31 + 1 = 33
}
  • 通过上面计算得到: Offsets::StrongExtraRefCountShift = 33,Offsets::PureSwiftDeallocShift = 0,Offsets::UnownedRefCountShift = 1
  • 知道了这三个值的之后,我们开始计算RefCountBitsT的初始化方法调用 bits 的值:
0 << 33 | 1 << 0 | 1 << 1;
0 | 1 | 2 = 3;
  • 最终bits存储信息如下:

[图片上传失败...(image-b8859d-1670758320398)]

  • 0位:标识是否是永久的
  • 1-31位:存储无主引用
  • 32位:标识当前类是否正在析构
  • 33-62位:标识强引用
  • 63位:是否使用SlowRC

强引用

  • 默认情况下,引用都是强引用。通过前面对refCounts的结构分析,得知它是存储引用计数信息的东西,在创建一个对象之后它的初始值为 0x0000000000000003

  • 如果我对这个实例对象进行多个引用,引用计数会增加,那这个强引用是如何添加的?

  • 底层会通过调用_swift_retain_方法

static HeapObject *_swift_retain_(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
  if (isValidPointerForNativeRetain(object))
    object->refCounts.increment(1);
  return object;
}
  • 在进行强引用的时候,本质上是调用 refCountsincrement 方法,也就是引用计数 +1。我们来看一下 increment 的实现:
void increment(uint32_t inc = 1) {
    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);

    // constant propagation will remove this in swift_retain, it should only
    // be present in swift_retain_n
    if (inc != 1 && oldbits.isImmortal(true)) {
        return;
    }

    RefCountBits newbits;
    do {
        newbits = oldbits;
        bool fast = newbits.incrementStrongExtraRefCount(inc);
        if (SWIFT_UNLIKELY(!fast)) {
            if (oldbits.isImmortal(false))
                return;
            return incrementSlow(oldbits, inc);
        }
    } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                              std::memory_order_relaxed));
}
  • increment 中调用了 incrementStrongExtraRefCount,我们再去看看incrementStrongExtraRefCount实现
LLVM_NODISCARD LLVM_ATTRIBUTE_ALWAYS_INLINE
    bool incrementStrongExtraRefCount(uint32_t inc) {
    // This deliberately overflows into the UseSlowRC field.
    bits += BitsType(inc) << Offsets::StrongExtraRefCountShift;
    return (SignedBitsType(bits) >= 0);
}
  • 此时inc1StrongExtraRefCountShift根据之前的计算为33
  • 1 << 33为结果为8589934592,其对应的十六进制为0x200000000
  • 到这里就实现了引用计数+1的操作

弱引用

  • 在实际开发的过程中,我们大多使用的都是强引用,在某些场景下使用强引用,用不好的话会造成循环引用。
  • Swift中我们通过关键字weak来表明一个弱引用;weak关键字的作用是在使用这个实例的时候并不保有此实例的引用。使用weak关键字修饰的引用类型数据在传递时不会使引用计数加1,不会对其引用的实例保持强引用,因此不会阻止ARC释放被引用的实例。
  • 由于弱引用不会保持对实例的引用,所以当实例被释放的时候,弱引用仍旧引用着这个实例也是有可能。因此,ARC会在被引用的实例释放时,自动地将弱引用设置为nil。由于弱引用需要允许设置为nil,因此它一定是可选类型

swift_weakInit

  • weak 修饰之后,变量变成了一个可选项,并且,还会调用一个 swift_weakInit 函数
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
  ref->nativeInit(value);
  return ref;
}
  • 发现用 weak 修饰之后,在内部会生成WeakReference类型的变量,并在 swift_weakInit 中调用 nativeInit 函数。
void nativeInit(HeapObject *object) {
    auto side = object ? object->refCounts.formWeakReference() : nullptr;
    nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}
  • nativeInit方法中调用了formWeakReference()方法,也就意味着形成了弱引用(形成一个散列表):
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}
  • 它本质就是创建了一个散列表
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
  // 去除原有的refCount,也是是64位信息
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  
  // Preflight failures before allocating a new side table.
  // 判断原来的 refCounts 是否有当前的引用计数
  if (oldbits.hasSideTable()) {
    // 如果有直接返回
    return oldbits.getSideTable();
  } 
  else if (failIfDeiniting && oldbits.getIsDeiniting()) {
    // 如果没有并且正在析构直接返回 nil
    return nullptr;
  }

  // Preflight passed. Allocate a side table.
  
  // 创建一个散列表
  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);
    
  } while (! refCounts.compare_exchange_weak(oldbits, newbits,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
  return side;
}
  • 散列表的创建可以分为4步:
    1. 取出原来的 refCounts引用计数的信息。
    2. 判断原来的 refCounts 是否有散列表,如果有直接返回,如果没有并且正在析构直接返回nil
    3. 创建一个散列表。
    4. 对原来的散列表以及正在析构的一些处理。

HeapObjectSideTableEntry

  • 接下来我们来看看这个散列表HeapObjectSideTableEntry
Storage layout:

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

HeapObjectSideTableEntry {
  SideTableRefCounts {
    object pointer
    atomic<SideTableRefCountBits> {
      strong RC + unowned RC + weak RC + flags
    }
  }   
}
  • 可以分析出在Swift中本质上存在两种引用计数:
    • 如果是强引用,那么是strong RC + unowned RC + flags
    • 如果是弱引用,那么是 HeapObjectSideTableEntry
  • 我们看一下HeapObjectSideTableEntry结构
class HeapObjectSideTableEntry {
  // FIXME: does object need to be atomic?
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;

  public:
  HeapObjectSideTableEntry(HeapObject *newObject)
    : object(newObject), refCounts()
  { }
  
  // 省略部分代码
}
  • 可以看到,HeapObjectSideTableEntry中存着对象的指针,并且还有一个 refCounts,而 refCounts 的类型为SideTableRefCounts
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
  • SideTableRefCountBits就是继承自我们前面学过的RefCountBitsT的模版类
class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
  uint32_t weakBits;

  public:
  LLVM_ATTRIBUTE_ALWAYS_INLINE
  SideTableRefCountBits() = default;
}
  • 它多了一个weakBits成员变量。
  • 所以HeapObjectSideTableEntry里边存储的是64位原有的strong RC + unowned RC + flags,再加上32位weak RC
  • 当我们用 weak 修饰之后,这个散列表就会存储对象的指针和引用计数信息相关的东西。

无主引用

  • Swift中可以通过 unowned 定义无主引用,unowned 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC中的 unsafe_unretained)。需要注意的是试图在实例销毁后访问无主引用,会产生运行时错误(野指针)。

  • weakunowned 都能解决循环引用的问题,unowned 要比 weak 少一些性能消耗,那我们如何来选择 weakunowned 呢?

    • 如果强引用的双方生命周期没有任何关系,使用weak
    • 如果其中一个对象销毁,另一个对象也跟着销毁,则使用unowned
  • weak相对于unowned更兼容,更安全,而unowned性能更高;这是因为weak需要操作散列表,而unowned只需要操作64位位域信息;在使用unowned的时候,要确保其修饰的属性一定有值。

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

推荐阅读更多精彩内容