Swift内存管理

OC一样,Swift中也是通过引用计数的方式来管理对象的内存的。在Swift类 结构探究中,分析过引用计数refCounts,它是RefCounts类型,class类型,占8字节大小。

一、强引用

class Animal {
    var age: Int = 10
    var name: String = "dog"
}
var t = Animal()
var t1 = t
var t2 = t

断点,查看t的的内存,refCounts0x0000000600000003

image.png

1.1

HeapObject开始分析引用计数的真正表示形态,源码 HeapObject -> InlineRefCounts

struct HeapObject {
  HeapMetadata const *metadata;

  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
  ...
}

#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  InlineRefCounts refCounts

进入InlineRefCounts定义,是RefCounts类型的别名,而RefCounts是模板类,真正决定的是传入的类型InlineRefCountBits

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;
  ...
}

InlineRefCountBits,是RefCountBitsT类的别名:

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

RefCountBitsT,有bits属性:

template <RefCountInlinedness refcountIsInline>
class RefCountBitsT {
    ...
      typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
    BitsType;
    ...
    BitsType bits;
    ...
}

template <>
struct RefCountBitsInt<RefCountNotInline, 4> {
  //类型
  typedef uint64_t Type;
  typedef int64_t SignedType;
};

其中bits其实质是将RefCountBitsInt中的type属性取了一个别名,所以bits的真正类型是uint64_t,即64位整型数组

1.2 对象创建

然后,继续分析swift中对象创建的底层方法swift_allocObject,源码:

static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                       size_t requiredSize,
                                       size_t requiredAlignmentMask) {
    ...
    new (object) HeapObject(metadata);
    ...
}

<!--构造函数-->
 constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }

进入Initialized定义,是一个枚举,其对应的refCounts方法中,干事的是RefCountBits

  enum Initialized_t { Initialized };
  
  //对应的RefCounts方法
// Refcount of a new object is 1.
constexpr RefCounts(Initialized_t)
: refCounts(RefCountBits(0, 1)) {}

进入RefCountBits定义,也是一个模板定义

template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;
  ...
}

重点:最终,真正的初始化地方是下面这个,实际上是做了一个位域操作,根据的是Offsets

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))
{ }

referCounts的本质是RefCountBitsInt中的type属性,而RefCountBitsInt又是模板类RefCountBitsT的模板类型T,所以RefCountBits(0, 1)实质调用的是模板类RefCountBitsT构造方法strongExtraCount传值为0unownedCount传值为1
RefCountsBit的结构图,如下所示:

image.png

isImmortal(0)
UnownedRefCount(1-31)unowned的引用计数
isDeinitingMask(32):是否进行释放操作
StrongExtraRefCount(33-62): 强引用计数
UseSlowRC(63)

其中需要重点关注UnownedRefCountStrongExtraRefCount。至此,我们分析一下上面的引用计数值0x0000000600000003,用二进制展示:

image.png

如图,33位置开始的强引用计数StrongExtraRefCount0011,转换成十进制就是3
下面通过sil验证一下:
image.png

关于copy_addrsil文档有解释,其实现是对object的引用计数作+1操作。有兴趣的可以自己去查一下。
需要注意的是:var t = Animal()此时已经有了引用计数,即,OC中创建实例对象时为0;Swift中创建实例对象时默认为1
可以通过CFGetRetainCount获取引用计数:
image.png

二、弱引用

class Animal {
    var age: Int = 10
    var name: String = "cat"
    var dog: Dog?
}

class Dog {
    var age = 20
    var animal: Animal?
}

func test(){
    var t = Animal()
    weak var t1 = t
    
    print("end")
}

test()

查看 t的引用计数:

image.png

弱引用声明变量是一个可选值,因为在程序运行过程中是允许将当前变量设置为nil的。
t1处加断点,查看汇编,发现:swift_weakInit。查看源码,这个函数是由WeakReference来调用的,相当于weak字段编译器声明过程中就自定义了一个WeakReference的对象,其目的在于管理弱引用。

WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
  ref->nativeInit(value);
  return ref;
}

进入nativeInit

void nativeInit(HeapObject *object) {
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}

进入formWeakReference,创建sideTable

template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  //创建 sideTable
  auto side = allocateSideTable(true);
  if (side)
  // 如果创建成功,则增加弱引用
    return side->incrementWeak();
  else
    return nullptr;
}

进入allocateSideTable

template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
  // 1、先拿到原本的引用计数
  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
  //2、创建sideTable
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
  // 3、将创建的地址给到InlineRefCountBits
  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;
}

总结:

  1. 先拿到原本的引用计数;
  2. 创建sideTable
  3. 将创建的sideTable地址给InlineRefCountBits,并查看其初始化方法,根据sideTable地址作了偏移操作并存储到内存,相当于将sideTable直接存储到了64位的变量中。

通过上面的底层流程分析,我们可以get到关键的2点:
通过HeapObjectSideTableEntry初始化散列表

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

上述源码中可知,弱引用对象对应的引用计数refCountsSideTableRefCounts类型,而强引用对象的是InlineRefCounts类型。
接下来我们看看SideTableRefCounts

typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;

继续搜索SideTableRefCountBits

image.png

里面包含了成员uint32_t weakBits,即一个32位域的信息。

通过InlineRefCountBits初始化散列表的数据

  LLVM_ATTRIBUTE_ALWAYS_INLINE
  RefCountBitsT(HeapObjectSideTableEntry* side)
    : bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
           | (BitsType(1) << Offsets::UseSlowRCShift)
           | (BitsType(1) << Offsets::SideTableMarkShift))
  {
    assert(refcountIsInline);
  }

这里继承的bits构造方法,而bits定义

BitsType bits;

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

强引用一样,来到了RefCountBitsInt,这个之前分析过,就是uint64_t类型,存的是64位域信息。

综合12两点的论述可得出:64位 用于记录 原有引用计数32位 用于记录 弱引用计数

为何t的引用计数是:0xc0000000200e08ba

上述分析中我们知道,在 InlineRefCountBits初始化散列表的数据时,执行了(reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits这句代码,而

static const size_t SideTableUnusedLowBits = 3;

side右移了3位,所以此时,将0xc0000000200e08ba 左移3位0x1007045D0,就是散列表的地址。再x/8g查看:

image.png

三、循环引用

var age = 10
let clourse = {
    age += 1
}
clourse()
print(age)

//打印结果
11

从输出结果中可以看出:闭包内部对变量的修改会改变外部原始变量的值,原因是闭包会捕获外部变量,这个与OC中的block一致的。

deinit
class Animal {
    var age = 10
    deinit {
        print("Animal deinit")
    }
}
func test(){
    var t = Animal()
    let clourse = {
        t.age += 10
    }
    clourse()
    print(t.age)
}
test()

//打印结果
//Animal deinit

可见,deinit是在当前实例对象即将被回收时触发。
接下来,我们把age放到类中,闭包中再去修改时会怎样:

class Animal {
    var age = 10
    deinit {
        print("Animal deinit")
    }
}

var t = Animal()
let clourse = {
    t.age += 10
}
clourse()
print(t.age)

//打印结果
//20
//Animal deinit

Animal类增加闭包属性

class Animal {
    var age = 10
    var completionBlock: (() ->())?
    deinit {
        print("Animal deinit")
    }
}
func test(){
    var t = Animal()
    t.completionBlock = {
        t.age += 10
    }
    print(t.age)
}
test()
//打印结果
//10

从运行结果发现,t.age还是1,并且没有执行deinit方法,这里存在循环引用

循环引用的处理

1 weak修饰闭包传入的参数。weak修饰后的变量是optional类型,所以t?.age += 1

func test(){
    var t = Animal()
    t.completionBlock = { [weak t] in
        t?.age += 10
    }
    print(t.age)
}

2 unowned修饰闭包参数

func test(){
    var t = Animal()
    t.completionBlock = { [unowned t] in
        t.age += 10
    }
    print(t.age)
}

[weak t][unowned t],称为捕获列表。定义在参数列表之前,如果使用捕获列表,那么即使省略参数名称、参数类型和返回类型,也必须使用in关键字

捕获列表的值
func test(){
    var age = 1
    var height = 1.0
    let clourse = {[age] in
        print(age)
        print(height)
    }
    age = 10
    height = 1.85
    clourse()
}
test()

//打印结果
//1
//1.85

如上,age值改变了,height未被捕获,值未变。

捕获列表特点: 捕获列表中的常量是值拷贝,而不是引用拷贝,因此,它是只读的,即不可修改

Runtime

Swift是一门静态语言,本身不具备动态性,不像OCRuntime运行时的机制(此处指OC提供运行时API供程序员操作)。但由于Swift兼容OC,所以可以转成OC类和函数,利用OC的运行时机制,来实现动态性

class Animal {
    var age: Int = 18
    func eat(){
        print("eat")
    }
}

let t = Animal()

func test(){
    var methodCount: UInt32 = 0
    let methodList = class_copyMethodList(Animal.self, &methodCount)
    for i in 0..<numericCast(methodCount) {
        if let method = methodList?[i]{
            let methodName = method_getName(method)
            print("=======方法名称:\(methodName)")
        }else{
            print("not found method")
        }
    }
    
    var count: UInt32 = 0
    let proList = class_copyPropertyList(Animal.self, &count)
    for i in 0..<numericCast(count) {
        if let property = proList?[i]{
            let propertyName = String(utf8String: property_getName(property))
            print("------成员属性名称:\(propertyName!)")
        }else{
            print("没有找到你要的属性")
        }
    }
    print("test run")
}
test()

尝试
1、以上代码,没有打印
2、给方法和属性添加@objc修饰,可以打印
3、类Animal继承NSObject,不用@objc修饰。只打印了初始化方法,因为在swift.h文件中暴露出来的只有init方法。
注意:如果要让OC调用,那么必须 继承NSObject + @objc修饰
4、去掉@objc修饰,改成dynamic修饰 + NSObject,同3。
5、@objc修饰 + dynamic修饰 + NSObject
关于方法调用,参考Swift方法调用

补充

AnyObject:代表任意类的instance,类的类型,类遵守的协议,但struct❌不行。
Any:代表任意类型,包括funcation类型或者Optional类型。
AnyClass:代表任意实例的类型。
T.self:如果T为实例对象,返回的就是它本身T是类,返回的是Metadata
type(of:):用于获取一个值的动态类型,编译期时,value的类型是Any类型运行期时,type(of:)获取的是真实类型

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

推荐阅读更多精彩内容