本文主要介绍swift中的内存管理,涉及引用计数、弱引用、强引用、循环引用、Runtime等
内存管理 - 强引用
在swift中也是使用ARC来追踪和管理内存的,下面我们通过一个案例来进行分析
class CJLTeacher { var age: Int = 18 var name: String = "CJL" } var t = CJLTeacher() var t1 = t var t2 = t
查看t的内存情况,为什么其中的refCounts是0x0000000600000003?
在分析类时(参考这篇文章Swift-进阶 02:类、对象、属性)有这么一个类HeapObject
,下面继续通过这个类来分析t的引用计数
- 分析源码
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
位整型数组
然后来继续分析swift中对象创建的底层方法swift_allocObject
- 分析初始化源码
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
方法中,
enum Initialized_t { Initialized }; //对应的RefCounts方法 // Refcount of a new object is 1. constexpr RefCounts(Initialized_t) : refCounts(RefCountBits(0, 1)) {}
从这里看出真正干事的是RefCountBits
- 进入
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)) { }
分析RefCountsBit
的结构,如下所示,
- isImmortal(0)
- UnownedRefCount(1-31): unowned的引用计数
- isDeinitingMask(32):是否进行释放操作
- StrongExtraRefCount(33-62): 强引用计数
- UseSlowRC(63)
重点关注UnownedRefCount
和StrongExtraRefCount
- 将t的
refCounts
用二进制展示,其中强引用计数为3
分析SIL代码
- 当只有t实例变量时
- 当有t + t1时,查看是否有
strong_retain
操作
//SIL中的main alloc_global @main.t1 : main.CJLTeacher // id: %8 %9 = global_addr @main.t1 : main.CJLTeacher : $*CJLTeacher // user: %11 %10 = begin_access [read] [dynamic] %3 : $*CJLTeacher // users: %12, %11 copy_addr %10 to [initialization] %9 : $*CJLTeacher // id: %11 //其中copy_addr等价于 - %new = load s*LGTeacher - strong_retain %new - store %new to %9
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)); }
- 回退到
HeapObject
,从InlineRefCounts
进入,其中是c++中的模板定义,是为了更好的抽象,在其中查找bits
(即decrementStrongExtraRefCount
方法)
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
时的refCounts
是 0x0000000200000003 t + t1
时的refCounts
是 0x0000000400000003 = 0x0000000200000003 + 0x200000000t + t1 + t2
时的refCounts
是 0x0000000600000003 = 0x0000000400000003 + 0x200000000- 针对上面的例子,可以通过
CFGetRetainCOunt
获取引用计数,发现依次是 2、3、4,默认多了一个1
如果将t、t1、t2放入函数中,还会再次retain一次
为什么是0x200000000
?
因为1左移33位,其中4位为一组,计算成16进制,剩余的33-32位0x10
,转换为10进制为2
。其实际增加引用技术就是1
swift与OC强引用计数对比
OC
中创建实例对象时为0
swift
中创建实例对象时默认为1
内存管理 - 弱引用
以下面为例:
class CJLTeacher { var age: Int = 18 var name: String = "CJL" var stu: CJLStudent? } class CJLStudent { var age = 20 var teacher: CJLTeacher? } func test(){ var t = CJLTeacher() weak var t1 = t }
查看t
的引用计数变化
- 弱引用声明的变量是一个
可选值
,因为在程序运行过程中是允许将当前变量设置为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位的变量中
所以上面的0xc000000020809a6c
是HeapObjectSideTableEntry
实例对象的内存地址,即散列表的地址
(除去63、62位)
- 查看
HeapObjectSideTableEntry
定义,其中有object
对象、refCounts
- 进入
SideTableRefCounts
,同InlineRefCounts
类似,实际做事的是SideTableRefCountBits
,继承自RefCountBitsT
(存的是uint64_t类型的64位的信息),还有一个uint32_t
的weakBits
,即32位的位域信息
- 64位 用于记录 原有引用计数
- 32位 用于记录 弱引用计数
以0xc000000020809a6c
为例,将62、63位清零,变成0x20809A6C
,然后左移3
位(即InlineRefCountBits
初始化方法),变成0x10404D360
即HeapObjectSideTableEntry对象地址,即散列表地址
,然后通过x/8g
读取
问题:如果此时再加一个强引用t2
查看其refCounts,t2是执行了strong_retain
的
源码查看 _swift_retain_ -> increment -> incrementSlow -> incrementStrong
总结
对于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位 } }
内存管理 - 循环引用
主要是研究闭包捕获外部变量
,以下面代码为例
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
,所以这里有循环引用
循环引用解决方法
有两种方式可以解决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的值
- 捕获列表中的常量是只读的,即不可修改
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..<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(CJLTeacher.self, &count) for i in 0..<numericCast(count) { if let property = proList?[i]{ let propertyName = property_getName(property) print("属性成员属性:\(property)") }else{ print("没有找到你要的属性") } } print("test run") } test()
运行结果如下,发现并没有打印方法和属性
【尝试1】如果给属性 和 方法 都加上 @objc,可以打印吗?
- 从运行结果看,是可以打印,但是由于类并没有暴露给OC,所以OC是无法使用的,这样做是没有意义的
- 【尝试2】如果swift的类
继承NSObject
,没有@objc修饰属性和方法,是否可以打印全部属性+方法?
- 从结果发现获取的只有
init
方法,主要是因为在swift.h
文件中暴露出来的只有init
方法 - 如果想让OC能使用,必须类
继承NSObject
+@objc修饰
属性、方法
如果去掉@objc
修饰属性,将方法改成dynamic
修饰,是否可以打印方法?
从结果可以看出,依旧不能被OC获取到,需要修改为@objc dynamic
修饰
结论
- 对于纯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()
存储类的信息
进入data
,打印bits、superclass
- 从这里可以得出
swift
中有默认基类,即_SwiftObject
- 打印methods
- swift源码中搜索
_SwiftObject
,继承自NSObject
,在内存结构上与OC基本类似的
#if __has_attribute(objc_root_class) __attribute__((__objc_root_class__)) #endif SWIFT_RUNTIME_EXPORT @interface SwiftObject<NSObject> { @private Class isa; //refCounts SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS; }
在之前的文章中Swift-进阶 02:类、对象、属性,其中TargetAnyClassMetadata
继承自TargetHeapMetaData
,其中只有一个属性kind,TargetAnyClassMetadata有四个属性:isa、superclass、cacheData、data
即bits
- 所以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交互的类
元类型、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 { }
例如如果是结构体遵守协议,会报错
需要将struct修改成class
//继承自AnyObject,表示JSONMap协议只有类才可以遵守 protocol JSONMap: AnyObject { } class CJLJSONMap: JSONMap { }
Any
Any
:代表任意类型
,包括 function类型 或者Optional类型,可以理解为AnyObject
是Any
的子集
//如果使用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.self
是T.Type
类型
//此时的self类型是 CJLTeacher.Type var t = CJLTeacher.self
打印结果如下
- 查看t1、t2存储的是什么?
var t = CJLTeacher() //实例对象地址:实例对象.self 返回实例对象本身 var t1 = t.self //存储metadata元类型 var t2 = CJLTeacher.self
type(of:)
type(of:)
:用来获取一个值的动态类型
<!--demo1--> var age = 10 as NSNumber print(type(of: age)) <!--打印结果--> __NSCFNumber <!--demo2--> //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<T>(_ value: T){ let valueType = type(of: value) print(valueType) } <!--打印结果--> CJLTeacher TestProtocol
从结果中发现,打印并不一致,原因是因为当有协议、泛型
时,当前的编译器并不能推断出准确的类型,需要将value转换为Any
,修改后的代码如下:
func test<T>(_ 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..<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(CJLTeacher.self, &count) for i in 0..<numericCast(count) { if let property = proList?[i]{ let propertyName = property_getName(property) print("属性成员属性:\(property)") }else{ print("没有找到你要的属性") } } print("test run") } test()
从结果运行看,并不能,因为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