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)
重点关注UnownedRefCount
和StrongExtraRefCount
总结
对于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对象的类型变了
- 可以看到
weak修饰
的p1对象
,变成了optinal可选值
。
(不难理解,weak修饰
的对象
,不
会改变
原对象的引用计数
,只是多
一层可空
的状态
)
断点
,汇编
可以看到swift_weakInit
初始化,swift_weakDestroy
释放。
常规对象
与弱引用对象
区别:
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
,所以这里有循环引用
循环引用解决方法
有两种方式可以解决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..
运行结果如下,发现并没有打印方法和属性
-
【尝试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 {
@private
Class isa;
//refCounts
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
}
-
在swift类的结构中,其中
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交互的类
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 { }
例如如果是结构体遵守协议,会报错
需要将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:)
:用来获取一个值的动态类型
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