前言
上篇文章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混编时会用到,最后讲述了下元类型
的几种特殊的应用场景。