前言
上篇文章Swift 指针重点介绍了指针的类别和对应的应用场景,本篇文章接着介绍Swift中的内存管理,以及Runtime的一些应用场景,尽管Swift是一门静态语言。
一、内存管理
和OC一样,Swift中也是通过引用计数的方式来管理对象的内存的,之前的文章Swift编译流程 & Swift类中,也分析过引用计数refCounts,它是类RefCounts类型,好比一个指针,占8字节大小。接下来我们重点看看强引用、弱引用和循环引用这几个主要场景。
1.1 强引用
首先我们看一个例子👇
class LGTeacher {
    var age: Int = 18
    var name: String = "Luoji"
}
var t = LGTeacher()
var t1 = t
var t2 = t
x/8g查看变量t的内存👇

可以看到,t的引用计数是0x0000000600000003,why?不应该是个单独的数字吗?
接下来还是要回到类RefCounts,查看这个类的定义👇

类RefCounts其实是个模板类,我们来看看传入的模板类型是什么?
回到refCounts的定义👇 它是InlineRefCounts类型
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  InlineRefCounts refCounts
接着搜索InlineRefCounts👇
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
所以类RefCounts是模板类,InlineRefCounts是InlineRefCountBits的别名,接着我们看看InlineRefCountBits的定义👇
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
同理,InlineRefCountBits又是RefCountIsInline的别名,通过模板类RefCountBitsT,最终我们定位到RefCountBitsT👇

这个模板类中,只有一个成员bits,它的实质是RefCountBitsInt中的type属性取的一个别名,所以bits的真正类型是uint64_t即64位整型数组👇

至此,我们分析得出结论
referCounts的本质是
64位整型数组。
接下来,我们看看Swift底层创建对象的过程_swift_allocObject_👇

其中调用了new (object) HeapObject(metadata);👇

定位到refCounts对应的构造是InlineRefCounts::Initialized👇
  enum Initialized_t { Initialized };
  // Refcount of a new object is 1.
  constexpr RefCounts(Initialized_t)
    : refCounts(RefCountBits(0, 1)) {}
Initialized是一个枚举Initialized_t,而Initialized_t又是模板类RefCounts的类型T对应的是RefCountBits(0, 1),最终定位到RefCountBits👇

之前我们分析过,referCounts的本质是RefCountBitsInt中的type属性,而RefCountBitsInt又是模板类RefCountBitsT的模板类型T,所以RefCountBits(0, 1)实质调用的是模板类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))
{ }
strongExtraCount传值为0,unownedCount传值为1。
完整的bits位域结构体👇

大致分布图👇

其中需要重点关注UnownedRefCount和StrongExtraRefCount。
那么至此,我们把样例中的引用计数值0x0000000600000003用二进制展示👇

可见,33位置开始的强引用计数StrongExtraRefCount为0011,转换成十进制就是3。
SIL层验证
我们查看样例的SIL层代码👇
swiftc -emit-sil xx.swift | xcrun swift-demangle >> ./xx.sil && vscode xx.sil

SIL官方文档中关于copy_addr的解释👇

其中的strong_retain对应的就是swift_retain,其内部是一个宏定义,内部是_swift_retain_,其实现是对object的引用计数作+1操作👇
//内部是一个宏定义
HeapObject *swift::swift_retain(HeapObject *object) {
  CALL_IMPL(swift_retain, (object));
}
👇
//本质调用的就是 _swift_retain_
static HeapObject *_swift_retain_(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
  if (isValidPointerForNativeRetain(object))
    object->refCounts.increment(1);
  return object;
}
👇
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;
    }
    //64位bits
    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));
  }
接着搜索incrementStrongExtraRefCount,定义如下👇
LLVM_NODISCARD LLVM_ATTRIBUTE_ALWAYS_INLINE
bool incrementStrongExtraRefCount(uint32_t inc) {
// This deliberately overflows into the UseSlowRC field.
// 对inc做强制类型转换为 BitsType
// 其中 BitsType(inc) << Offsets::StrongExtraRefCountShift 等价于 1<<33位,16进制为 0x200000000
//这里的 bits += 0x200000000,将对应的33-63转换为10进制,为
bits += BitsType(inc) << Offsets::StrongExtraRefCountShift;
return (SignedBitsType(bits) >= 0);
}
所以,以t的refCounts为例(其中62-33位是strongCount,每次增加强引用计数增加都是在33-62位上增加的,固定的增量为1左移33位,即0x200000000。
为何t的引用计数是0x0000000600000003
- 当代码运行到
var t = LGTeacher()时,t的refCounts是 0x0000000200000003 - 
var t1 = t时,refCounts是 0x0000000400000003 = 0x0000000200000003 + 0x200000000 - 
var t2 = t时,refCounts是0x0000000600000003 = 0x0000000400000003 + 0x200000000 
Swift与OC初始化时的引用计数
我们注意到,var t = LGTeacher()此时已经有了引用计数,所以👇
- OC中创建实例对象时为
0 - Swift中创建实例对象时默认为
1 
CFGetRetainCOunt
可以通过CFGetRetainCOunt获取引用计数,应用到上面的例子,运行查看👇

如果把上述代码放入方法中运行,则👇

t的引用计数会再次增加。
1.2 弱引用
接下来,我们来看看弱引用,还是先看下面示例👇
class LGTeacher {
    var age: Int = 18
    var name: String = "Luoji"
    var stu: LGStudent?
}
class LGStudent {
    var age = 20
    var teacher: LGTeacher?
}
func test(){
    var t = LGTeacher()
    weak var t1 = t
    
    print("end")
}
test()
运行👇

t的引用计数是0xc0000000200abbca,why?接下来我们来看看原因👇
弱引用声明的变量是一个
可选值,因为在程序运行过程中是允许将当前变量设置为nil的
首先在t1处打上断点,查看汇编

我们锁定到swift_weakInit,接着在源码中搜索swift_weakInit👇
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
// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}
看到这里,就比较明朗了,系统会创建一个sideTable,创建成功的话,side->incrementWeak()增加弱引用计数,失败则return nullptr。看来重点就是这个allocateSideTable了👇

通过上图的底层流程分析,我们可以get到关键的2点👇
- 通过
HeapObjectSideTableEntry初始化散列表👇 
class HeapObjectSideTableEntry {
  // FIXME: does object need to be atomic?
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;
...
}
上述源码中可知
弱引用对象对应的引用计数refCounts是SideTableRefCounts类型
而强引用对象的是InlineRefCounts类型
接下来我们看看SideTableRefCounts👇
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
继续搜索SideTableRefCountBits👇

里面包含了成员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位域信息。
综合1 和 2两点的论述可得出:
64位用于记录原有引用计数32位用于记录弱引用计数
为何t的引用计数是0xc0000000200abbca
上述分析中我们知道,在InlineRefCountBits初始化散列表的数据时,执行了(reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits这句代码,而
static const size_t SideTableUnusedLowBits = 3;
对side右移了3位,所以此时,将0xc0000000200abbca左移3位是0x10055DE50,就是散列表的地址。再x/8g查看👇

小结
对于HeapObject来说,其refCounts有两种计算情况:
- 无弱引用:
strongCount(强引用计数)+unownedCount(无主引用计数) - 有弱引用:
object+xxx+(strongCount + unownedCount)+weakCount 
1.3 循环引用
循环引用也是一个很经典的面试题,按照惯例,我们还是先看看案例👇
var age = 10
let clourse = {
    age += 1
}
clourse()
print(age)
<!--打印结果-->
11
从输出结果中可以看出:闭包内部对变量的修改会改变外部原始变量的值,主要原因是闭包会捕获外部变量,这个与OC中的block是一致的。
deinit
接着,我们看看deinit的作用
class LGTeacher {
    deinit {
        print("LGTeacher deinit")
    }
}
func test(){
    var t = LGTeacher()
}
test()
<!--打印结果-->
LGTeacher deinit
可见,deinit是在当前实例对象即将被回收时触发。
接下来,我们把age放到类中,闭包中再去修改时会怎样👇

一样,没有问题,如果将闭包那块代码放入函数中呢👇
func test(){
    var t = LGTeacher()
    let clourse = {
        t.age += 1
    }
    clourse()
}
test()

运行结果发现,闭包对 t 并没有强引用,直接被释放了。我们继续修改👇
- 在类LGTeacher中添加闭包
completionBlock 
class LGTeacher {
    var age = 18
    
    var completionBlock: (() ->())?
    
    deinit {
        print("LGTeacher deinit")
    }
}
- 在
completionBlock中修改age 
func test(){
    var t = CJLTeacher()
    t.completionBlock = {
        t.age += 1
    }
}
test()
运行👇

从运行结果发现,t.age还是18,并且没有执行deinit方法,所以这里存在循环引用!
如何解决循环引用
有两种方式:
- 
weak修饰闭包传入的参数 
func test(){
    var t = LGTeacher()
    t.completionBlock = { [weak t] in
        t?.age += 1
    }
}
因为weak修饰后的变量是optional类型,所以t?.age += 1。
- 
unowned修饰闭包参数 
func test(){
    var t = LGTeacher()
    t.completionBlock = { [unowned t] in
        t.age += 1
    }
}
捕获列表
什么是捕获列表?例如上面的代码[weak t] 或 [unowned t] ,有以下特点:
- 定义在参数列表之前
 - 
[变量]写成用逗号连来的表达式列表,并用方括号括起来 - 如果使用
捕获列表,那么即使省略参数名称、参数类型和返回类型,也必须使用in关键字 
捕获的值的变化
看以下示例,输出什么?👇
func test(){
    var age = 0
    var height = 0.0
    let clourse = {[age] in
        print(age)
        print(height)
    }
    age = 10
    height = 1.85
    clourse()
}

age被捕获了,即使后面改变了它的值,但是结果还是0,而未被捕获的height的值却发生了变化。所以,从这个结果中可知:
对于捕获列表中的每个
常量,闭包会利用周围范围内具有相同名称的常量/变量,来初始化捕获列表中定义的常量。
上述结论大致可以分为以下几点:
- 捕获列表中的常量是
值拷贝,而不是引用拷贝 - 捕获列表中的常量的相当于
复制了变量age的值 - 捕获列表中的常量是
只读的,即不可修改 
二、Swift中的Runtime场景
Swift是一门静态语言,本身不具备动态性,不像OC有Runtime运行时的机制(此处指OC提供运行时API供程序员操作)。但由于Swift兼容OC,所以可以转成OC类和函数,利用OC的运行时机制,来实现动态性。
2.1 探索
老规矩,先上示例代码,
class LGTeacher {
    var age: Int = 18
    func teach(){
        print("teach")
    }
}
let t = LGTeacher()
func test(){
    var methodCount: UInt32 = 0
    let methodList = class_copyMethodList(LGTeacher.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(LGTeacher.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()
代码很检点,test()方法中通过class_copyMethodList 和 class_copyPropertyList变量方法名称和属性名称,并打印出来。我们运行来看看👇

并没有打印出来,下面我们来试试修改代码,让其能打印出来。
- 修改1:给方法和属性添加
@objc修饰 
class LGTeacher {
    @objc var age: Int = 18
    @objc func teach(){
        print("teach")
    }
}

可以打印。
- 修改2:类LGTeacher
继承NSObject,不用@objc修饰 
class LGTeacher: NSObject{
    var age: Int = 18
    func teach(){
        print("teach")
    }
}

只打印了初始化方法,是因为在swift.h文件中暴露出来的只有init方法。
注意:如果要让OC调用,那么必须 继承NSObject + @objc修饰👇
class LGTeacher: NSObject{
    @objc var age: Int = 18
    @objc func teach(){
        print("teach")
    }
}

- 修改3:去掉@objc修饰,改成dynamic修饰
 
class LGTeacher: NSObject{
    dynamic var age: Int = 18
    dynamic func teach(){
        print("teach")
    }
}

和第2种情况一样。
*修改4:同时用@objc 和 dynamic修饰方法
class LGTeacher: NSObject{
    dynamic var age: Int = 18
    @objc dynamic func teach(){
        print("teach")
    }
}

可以输出方法名称。
小结
- 对于
纯Swift类来说,没有动态特性dynamic(因为Swift是静态语言),方法和属性不加任何修饰符的情况下,不具备runtime特性,此时的方法调度,依旧是函数表调度,即·V_Table调度。 - 
纯swift类的方法和属性添加@objc修饰的情况下,可通过runtime API获取到,但是在OC中是无法调度的,原因是swift.h文件中没有该Swift类的声明。 - 对于
继承NSObject类来说,如果想要动态的获取当前属性+方法,必须在其声明前添加@objc关键字,如果想要使用方法交换,还必须在属性+方法前添加dynamic关键字,否则当前属性+方法只是暴露给OC使用,而不具备任何动态特性。 
2.2 元类型、AnyClass、Self
元类型
主要是Any 和 AnyObject这两个关键字。
- AnyObject :可代表
类的Instance实例、类的类型、类遵守的协议,但struct❌不行。 
class LGPerson {
   var age = 18
}
// 1. 类实例
var p1: AnyObject =  LGPerson()
// 2. 类的类型
var p2: AnyObject = LGPerson.self
// 3. 类遵守的协议 (继承AnyObjec)
protocol JSONMap: AnyObject { }
// 4. struct不是AnyObject类型
// struct报错: [Non-class type 'HTJSON' cannot conform to class protocol 'JSONMap']
 struct HTJSON: JSONMap { }
// 5. 基础类型强转为Class,就属于AnyObject
var age: AnyObject = 10 as NSNumber  // Int不属于AnyObject,强转NSNumber就属于AnyObject

- Any:Any比AnyObject代表的范围更广,不仅支持
类实例对象、类类型、类协议,还支持struct、函数以及Optioanl可选类型。 
// 1. 类实例
var p1: Any =  LGPerson()
// 2. 类的类型
var p2: Any = LGPerson.self
// 3. 类遵守的协议 (继承AnyObjec)
protocol JSONMap: Any { }
// 4. struct
struct LGJSON: JSONMap { }
// 5. 函数
func test() {}
// 6. struct对象
let s = LGJSON()
// 7. 可选类型
let option: LGPerson? = nil
// Any类型的数组
var array: [Any] = [1,                // Int
                   "2",              // String
                   LGPerson.self,    // class类型
                   p1,               // 类的实例对象
                   JSONMap.self,     // 协议本身
                   LGJSON.self,      // struct类型
                   s,                // struct实例对象
                   option,           // 可选值
                   test()            // 函数
                   ]
print(array)

通过上述[Any]数组,我们可以看到Any可指代范围是有多广。
option是可选类型,所以会有警告,可以通过option as Any消除该警告。
AnyClass
AnyClass仅代表类的类型。
// 1. 类实例
var p1: AnyObject =  LGPerson()
// 2. 类的类型
var p2: AnyObject = LGPerson.self
// 3. 类遵守的协议 (继承AnyObjec)
protocol JSONMap: AnyObject { }
class LGTest: JSONMap { }
var p3: JSONMap = LGTest()
// Any类型的数组
var array: [AnyObject] = [ LGPerson.self,    // class类型
                           p1,               // 类的实例对象
                           p3                // 遵守AnyObject协议的类对象也符合(类对象本身符合)
                         ]

即使array是接受AnyObject所有对象,但实际只存储了类的类型。
Self
与Self有关的关键字有T.self 和 T.Type。在讲这两个之前,我们先来看看type(of:)这个方法的作用。
- type(of:)
用于获取一个值的动态类型。 
var age = 10
// 编译器任务value接收Any类型
func test(_ value: Any) {
   
   // type(of:)可获取真实类型
   print(type(of: value))    // 打印Int
   
}
test(age)
编译期时,value的类型是Any类型👇

而运行期时,type(of:)获取的是真实类型👇

- type(of:)三种特殊的应用场景
- 
继承场景:type(of:)是读取真实调用的对象 
 - 
 
class LGPerson { }
class LGStudent: LGPerson { }
func test(_ value: LGPerson) {
   print(type(of: value)) 
}
var person = LGPerson()
var student = LGStudent()
test(person)
test(student)

- 
遵循协议的场景:type(of:)也是读取真实调用的对象 
protocol TestProtocol { }
class LGPerson: TestProtocol { }
func test(_ value: TestProtocol) {
   print(type(of: value))
}
var p = LGPerson()
var p1: TestProtocol = LGPerson()
test(p)
test(p1)

注意:p1是LGPerson类型,并不是TestProtocol协议类型。
- 使用
泛型T时的场景:type(of:)读取的就是T类型 
protocol TestProtocol { }
class LGPerson: TestProtocol { }
func test<T>(_ value: T) {
   print(type(of: value))
}
var p = LGPerson()
var p1: TestProtocol = LGPerson()
test(p)
test(p1) 

这种情况下,p1是TestProtocol协议类型。
如果想让p1取到的是LGPerson类型,需要改动代码👇

弄清楚了type(of:)的作用后,我们再回过头看T.self 和 T.Type。
T.self
如果T是实例对象,就返回实例本身。如果T是类,就返回metadata(首地址:类的类型)。示例代码👇
class LGPerson {
    var age = 18
}
struct LGTest {
    var name = "test"
}
// 1. class实例对象,返回对象本身
var p =  LGPerson().self
print(type(of: p))
// 2. class类型, 返回class类型
var pClass = LGPerson.self
print(type(of: pClass))
// 3. struct实例对象,返回对象本身
var t = LGTest().self
print(type(of: t))
// 4. struct类型,返回struct类型
var tStruct = LGTest.self
print(type(of: tStruct))

T.Type
T.Type就是一种类型,T.self是T.Type类型。(使用type(of:)读取)。上述例子中的👇


总结
本篇文章主要讲了2大知识点:内存管理 和 Runtime相关,内存管理中主要分析了,在底层源码中,强引用对象和弱引用对象的引用计数的计算方式的区别,通过示例证明了引用计数位域的存储。接着讲到了Runtime的场景,OC与Swift混编时会用到,最后讲述了下元类型的几种特殊的应用场景。