swift 进阶:内存管理 & Runtime

swift 进阶之路:学习大纲

swift引用计数:

  • swift对象都是以HeapObject为模板创建,其中HeapObject的模板中第二个元素,是refCount引用计数属性,该属性记录了strong(强引用计数)和unowned(弱引用计数)等信息。
  • weak修饰的对象,会另外生成WeakReference对象,内部HeapObjectSideTableEntry散列表类- - 在原heapObject类的基础上,重新记录了refCount(管理strong和unowned引用计数)并新增了weakBits弱引用计数

swift与OC强引用计数对比

  • OC中创建实例对象时为0

  • swift中创建实例对象时默认为1

1. Swift三大引用计数(strong、unowned、weak)

  • 不管是哪种引用,持有的都是原对象(从p到p5内存地址可以看出)
  • 在每一行执行完后,x/4g打印p对象内存信息,在第二个地址上,可以清晰感受到,强引用无主引用的引用计数在有规律的增加,而弱引用却没有变化。

refCount内存布局

  • isImmortal(0)

  • UnownedRefCount(1-31): unowned的引用计数

  • isDeinitingMask(32):是否进行释放操作

  • StrongExtraRefCount(33-62): 强引用计数

  • UseSlowRC(63)

重点关注UnownedRefCountStrongExtraRefCount

总结

对于HeapObject来说,其refCounts有两种:

  • 无弱引用:strongCount+ unownedCount
  • 有弱引用:object + xxx + (strongCount + unownedCount) + weakCount
HeapObject {
    InlineRefCountBit {strong count + unowned count }

    HeapObjectSideTableEntry{
        HeapObject *object
        xxx
        strong Count + unowned Count(uint64_t)//64位
        weak count(uint32_t)//32位
    }
}

弱引用

  • 我们知道swift是使用ARC(自动引用计数管理)的。如果产生循环引用,我们必须有弱引用机制去打破循环

swift中的弱引用,使用weak修饰。与OC不同的是:

  • OC:
    弱引用计数是存放在全局维护散列表中,isa中会记录是否使用了散列表
    引用计数0时,自动触发dealloc,会检查清空当前对象散列表计数
  • swift:
    弱引用计数也是存放在散列表中,但这个散列表不是全局的。
*   如果对象`没有`使用`weak`弱引用,就是单纯的`HeapObject`对象,`没有散列表`。
*   如果使用`weak`弱引用,会变为`WeakReference`对象。这是一个`Optionl(可空对象)`。其结构中自带`散列表计数`区域。
    但`swift`的`散列表`与`refCount`无关联。当`强引用计数`为`0`时,不会触发`散列表`的清空。而是在`下次访问`发现`当前对象不存在(为nil)`时,会清空`散列表计数`。

下面,我们通过案例源码来分析swift弱引用WeakReference对象内存结构

案例:

  • 可以发现:
    weak修饰前,p对象是HeapObject类型,可从refCount中看出强引用计数无主引用计数
    weak修饰后,p对象的类型变了
image
  • 可以看到weak修饰p1对象,变成了optinal可选值
    (不难理解,weak修饰对象改变原对象的引用计数,只是一层可空状态
image
  • 断点汇编可以看到swift_weakInit初始化,swift_weakDestroy释放。
image
  • 常规对象弱引用对象区别:
image

2. 内存管理 - 循环引用

主要是研究闭包捕获外部变量,以下面代码为例

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


11

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

  • 定义一个类,在test函数作用域消失后,会执行init
class CJLTeacher {
    var age = 18
    //反初始化器(当前实例对象即将被回收)
    deinit {
        print("CJLTeacher deinit")
    }
}
func test(){
    var t = CJLTeacher()
}
test()


CJLTeacher deinit

  • 修改例子,通过闭包修改其属性值
class CJLTeacher {
    var age = 18
    //反初始化器(当前实例对象即将被回收)
    deinit {
        print("CJLTeacher deinit")
    }
}
var t = CJLTeacher()
let clourse = {
    t.age += 1
}
clourse()


11

  • 【修改1】将上面例子修改为如下,其中闭包是否对t有强引用?
class CJLTeacher {
    var age = 18
    deinit {
        print("CJLTeacher deinit")
    }
}

func test(){
    var t = CJLTeacher()
    let clourse = {
        t.age += 1
    }
    clourse()
}
test()


CJLTeacher deinit

运行结果发现,闭包对 t 并没有强引用

  • 【修改2】继续修改例子为如下,是否有强引用?
class CJLTeacher {
    var age = 18

    var completionBlock: (() ->())?

    deinit {
        print("CJLTeacher deinit")
    }
}

func test(){
    var t = CJLTeacher()
    t.completionBlock = {
        t.age += 1
    }
}
test()

从运行结果发现,没有执行deinit方法,即没有打印CJLTeacher deinit,所以这里有循环引用

image

循环引用解决方法

有两种方式可以解决swift中的循环引用

  • 【方式一】使用weak修饰闭包传入的参数,其中参数的类型是optional
func test(){
    var t = CJLTeacher()
    t.completionBlock = { [weak t] in
        t?.age += 1
    } 
}

  • 【方式二】使用unowned修饰闭包参数,与weak的区别在于unowned不允许被设置为nil,即总是假定有值
func test(){
    var t = CJLTeacher()
    t.completionBlock = { [unowned t] in
        t.age += 1
    } 
}

捕获列表

  • [weak t] / [unowned t] 在swift中被称为捕获列表

  • 定义在参数列表之前

  • 【书写方式】捕获列表被写成用逗号括起来的表达式列表,并用方括号括起来

  • 如果使用捕获列表,则即使省略参数名称、参数类型和返回类型,也必须使用in关键字

  • [weak t] 就是取t的弱引用对象 类似weakself

请问下面代码的clourse()调用后,输出的结果是什么?

func test(){
    var age = 0
    var height = 0.0
    //将变量age用来初始化捕获列表中的常量age,即将0给了闭包中的age(值拷贝)
    let clourse = {[age] in
        print(age)
        print(height)
    }
    age = 10
    height = 1.85
    clourse()
}


0
1.85

所以从结果中可以得出:对于捕获列表中的每个常量,闭包会利用周围范围内具有相同名称的常量/变量,来初始化捕获列表中定义的常量。有以下几点说明:

  • 捕获列表中的常量是值拷贝,而不是引用

  • 捕获列表中的常量的相当于复制了变量age的值

  • 捕获列表中的常量是只读的,即不可修改

3、 swift中Runtime探索

请问下面代码,会打印方法和属性吗?

class CJLTeacher {
    var age: Int = 18
    func teach(){
        print("teach")
    }
}

let t = CJLTeacher()

func test(){
    var methodCount: UInt32 = 0
    let methodList = class_copyMethodList(CJLTeacher.self, &methodCount)
    for i in 0..

运行结果如下,发现并没有打印方法和属性

image
  • 【尝试1】如果给属性 和 方法 都加上 @objc,可以打印吗?

    image

    从运行结果看,是可以打印,但是由于类并没有暴露给OC,所以OC是无法使用的,这样做是没有意义的

  • 【尝试2】如果swift的类继承NSObject,没有@objc修饰属性和方法,是否可以打印全部属性+方法?

    image

    从结果发现获取的只有init方法,主要是因为在 swift.h文件中暴露出来的只有init方法

  • 如果想让OC能使用,必须类继承NSObject + @objc修饰属性、方法

    image
  • 如果去掉@objc修饰属性,将方法改成dynamic修饰,是否可以打印方法?

    image

    从结果可以看出,依旧不能被OC获取到,需要修改为@objc dynamic修饰

    image

结论

  • 对于纯swift类来说,没有 动态特性dynamic(因为swift静态语言),方法和属性不加任何修饰符的情况下,已经不具备runtime特性,此时的方法调度,依旧是函数表调度即V_Table调度

  • 对于纯swift类,方法和属性添加@objc标识的情况下,可以通过runtime API获取到,但是在OC中是无法进行调度的,原因是因为swift.h文件中没有swift类的声明

  • 对于继承自NSObject类来说,如果想要动态的获取当前属性+方法,必须在其声明前添加 @objc关键字,如果想要使用方法交换,还必须在属性+方法前添加dynamic关键字,否则当前属性+方法只是暴露给OC使用,而不具备任何动态特性

objc源码验证

(由于xcode12.2暂时无法运行objc源码,下列验证图片仅供参考)

  • 进入class_copyMethodList源码,断住,查看此时的cls,其中data()存储类的信息

    image
  • 进入data,打印bits、superclass

    image

    从这里可以得出swift中有默认基类,即_SwiftObject

  • 打印methods

    image
  • swift源码中搜索_SwiftObject,继承自NSObject,在内存结构上与OC基本类似的

#if __has_attribute(objc_root_class)
__attribute__((__objc_root_class__))
#endif
SWIFT_RUNTIME_EXPORT @interface SwiftObject {
 @private
  Class isa;
  //refCounts
  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
}

  • 在swift类的结构中,其中TargetAnyClassMetadata继承自TargetHeapMetaData,其中只有一个属性kind,TargetAnyClassMetadata有四个属性:isa、superclass、cacheData、data即bits

    image

    所以swift为了保留和OC交互,其在底层存储的数据结构上和OC是一致的

  • objc源码中搜索swift_class_t,继承自objc_class,保留了OC模板类的4个属性,其次才是自己的属性

struct swift_class_t : objc_class {
    uint32_t flags;
    uint32_t instanceAddressOffset;
    uint32_t instanceSize;
    uint16_t instanceAlignMask;
    uint16_t reserved;

    uint32_t classSize;
    uint32_t classAddressOffset;
    void *description;
    // ...

    void *baseAddress() {
        return (void *)((uint8_t *)this - classAddressOffset);
    }
};

问题:为什么继承NSObject?:必须通过NSObject声明,来帮助编译器判断,当前类是一个和OC交互的类

4、元类型、AnyClass、Self

AnyObject

  • AnyObject:代表任意类的instance、类的类型、仅类遵守的协议
class CJLTeacher: NSObject {
    var age: Int = 18
}

var t = CJLTeacher()

//此时代表的就是当前CJLTeacher的实例对象
var t1: AnyObject = t

//此时代表的是CJLTeacher这个类的类型
var t2: AnyObject = CJLTeacher.self

//继承自AnyObject,表示JSONMap协议只有类才可以遵守
protocol JSONMap: AnyObject { }

例如如果是结构体遵守协议,会报错

image

需要将struct修改成class

//继承自AnyObject,表示JSONMap协议只有类才可以遵守
protocol JSONMap: AnyObject {

}
class CJLJSONMap: JSONMap {

}

Any

  • Any:代表任意类型,包括 function类型 或者Optional类型,可以理解为AnyObjectAny的子集
//如果使用AnyObject会报错,而Any不会
var array: [Any] = [1, "cjl", "", true]

AnyClass

  • AnyClass:代表任意实例的类型 ,类型是AnyObject.Type
    • 查看定义,是public typealias AnyClass = AnyObject.Type

T.self & T.Type

  • T.self

    • 如果T是实例对象,返回的就是它本身

    • 如果T是类,那么返回的是MetaData

  • T.Type:一种类型

  • T.selfT.Type类型

//此时的self类型是  CJLTeacher.Type
var t = CJLTeacher.self

打印结果如下

  • 查看t1、t2存储的是什么?
var t = CJLTeacher()
//实例对象地址:实例对象.self 返回实例对象本身
var t1 = t.self
//存储metadata元类型
var t2 = CJLTeacher.self

image

type(of:)

  • type(of:):用来获取一个值的动态类型

var age = 10 as NSNumber
print(type(of: age))


__NSCFNumber


//value - static type 静态类型:编译时期确定好的
//type(of:) - dynamic type:Int
var age = 10
//value的静态类型就是Any
func test(_ value: Any){

    print(type(of: value))
}

test(age)


Int

实践

demo1

请问下面这段代码的打印结果是什么?

class CJLTeacher{
    var age = 18
    var double = 1.85
    func teach(){
        print("LGTeacher teach")
    }
}
class CJLPartTimeTeacher: CJLTeacher {
    override func teach() {
        print("CJLPartTimeTeacher teach")
    }
}

func test(_ value: CJLTeacher){
    let valueType = type(of: value)
    value.teach()
    print(value)
}
var t = CJLPartTimeTeacher()
test(t)


CJLPartTimeTeacher teach
CJLTest.CJLPartTimeTeacher

demo2

请问下面代码的打印结果是什么?

protocol TestProtocol {

}
class CJLTeacher: TestProtocol{
    var age = 18
    var double = 1.85
    func teach(){
        print("LGTeacher teach")
    }
}

func test(_ value: TestProtocol){
    let valueType = type(of: value)
    print(valueType)
}
var t = CJLTeacher()
let t1: TestProtocol = CJLTeacher()
test(t)
test(t1)


CJLTeacher
CJLTeacher

  • 如果将test中参数的类型修改为泛型,此时的打印是什么?
func test(_ value: T){
    let valueType = type(of: value)
    print(valueType)
}


CJLTeacher
TestProtocol

从结果中发现,打印并不一致,原因是因为当有协议、泛型时,当前的编译器并不能推断出准确的类型,需要将value转换为Any,修改后的代码如下:

func test(_ value: T){
    let valueType = type(of: value as Any)
    print(valueType)
}


CJLTeacher
CJLTeacher

demo3

在上面的案例中,如果class_getClassMethod中传t.self,可以获取方法列表吗?

func test(){
    var methodCount: UInt32 = 0
    let methodList = class_copyMethodList(t.self, &methodCount)
    for i in 0..

从结果运行看,并不能,因为t.self实例对象本身,即CJLTeacher,并不是CJLTeacher.Type类型

总结

  • 当无弱引用时,HeapObject中的refCounts等于 strongCount + unownedCount

  • 当有弱引用时,HeapObject中的refCounts等于 object + xxx + (strongCount + unownedCount) + weakCount

  • 循环应用可以通过weak / unowned修饰参数来解决

  • swift中闭包的捕获列表值拷贝,即深拷贝,是一个只读的常量

  • swift由于是静态语言,所以属性、方法在不加任何修饰符的情况下时是不具备动态性即Runtime特性的,此时的方法调度是V-Table函数表调度

  • 如果想要OC使用swift类中的方法、属性,需要class继承NSObject,并使用@objc修饰

  • 如果想要使用方法交换,除了继承NSObject+@objc修饰,还必须使用dynamic修饰

  • Any:任意类型,包括function类型、optional类型

  • AnyObject:任意类的instance、类的类型、仅类遵守的协议,可以看作是Any的子类

  • AnyClass:任意实例类型,类型是AnyObject.Type

  • T.self:如果T是实例对象,则表示它本身,如果是类,则表示metadata.T.self的类型是T.Type

参考:https://www.jianshu.com/p/0cc765a325cb

你可能感兴趣的:(swift 进阶:内存管理 & Runtime)