前言
上篇文章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混編時會用到,最后講述了下元類型
的幾種特殊的應用場景。