swift 進(jìn)階:內(nèi)存管理 & Runtime

swift 進(jìn)階之路:學(xué)習(xí)大綱

swift引用計(jì)數(shù):

  • swift對象都是以HeapObject為模板創(chuàng)建,其中HeapObject的模板中第二個元素,是refCount引用計(jì)數(shù)屬性,該屬性記錄了strong(強(qiáng)引用計(jì)數(shù))和unowned(弱引用計(jì)數(shù))等信息。
  • weak修飾的對象,會另外生成WeakReference對象,內(nèi)部HeapObjectSideTableEntry散列表類- - 在原heapObject類的基礎(chǔ)上,重新記錄了refCount(管理strong和unowned引用計(jì)數(shù))并新增了weakBits弱引用計(jì)數(shù)

swift與OC強(qiáng)引用計(jì)數(shù)對比

  • OC中創(chuàng)建實(shí)例對象時(shí)為0

  • swift中創(chuàng)建實(shí)例對象時(shí)默認(rèn)為1

1. Swift三大引用計(jì)數(shù)(strong、unowned、weak)

  • 不管是哪種引用,持有的都是原對象(從p到p5內(nèi)存地址可以看出)
  • 在每一行執(zhí)行完后,x/4g打印p對象內(nèi)存信息,在第二個地址上,可以清晰感受到,強(qiáng)引用無主引用的引用計(jì)數(shù)在有規(guī)律的增加,而弱引用卻沒有變化。

refCount內(nèi)存布局

  • isImmortal(0)

  • UnownedRefCount(1-31): unowned的引用計(jì)數(shù)

  • isDeinitingMask(32):是否進(jìn)行釋放操作

  • StrongExtraRefCount(33-62): 強(qiáng)引用計(jì)數(shù)

  • UseSlowRC(63)

重點(diǎn)關(guān)注UnownedRefCountStrongExtraRefCount

總結(jié)

對于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(自動引用計(jì)數(shù)管理)的。如果產(chǎn)生循環(huán)引用,我們必須有弱引用機(jī)制去打破循環(huán)

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

  • OC:
    弱引用計(jì)數(shù)是存放在全局維護(hù)散列表中,isa中會記錄是否使用了散列表
    引用計(jì)數(shù)0時(shí),自動觸發(fā)dealloc,會檢查清空當(dāng)前對象散列表計(jì)數(shù)
  • swift:
    弱引用計(jì)數(shù)也是存放在散列表中,但這個散列表不是全局的。
*   如果對象`沒有`使用`weak`弱引用,就是單純的`HeapObject`對象,`沒有散列表`。
*   如果使用`weak`弱引用,會變?yōu)閌WeakReference`對象。這是一個`Optionl(可空對象)`。其結(jié)構(gòu)中自帶`散列表計(jì)數(shù)`區(qū)域。
    但`swift`的`散列表`與`refCount`無關(guān)聯(lián)。當(dāng)`強(qiáng)引用計(jì)數(shù)`為`0`時(shí),不會觸發(fā)`散列表`的清空。而是在`下次訪問`發(fā)現(xiàn)`當(dāng)前對象不存在(為nil)`時(shí),會清空`散列表計(jì)數(shù)`。

下面,我們通過案例源碼來分析swift弱引用WeakReference對象內(nèi)存結(jié)構(gòu)

案例:

  • 可以發(fā)現(xiàn):
    weak修飾前,p對象是HeapObject類型,可從refCount中看出強(qiáng)引用計(jì)數(shù)無主引用計(jì)數(shù)
    weak修飾后,p對象的類型變了
image
  • 可以看到weak修飾p1對象,變成了optinal可選值
    (不難理解,weak修飾對象改變原對象的引用計(jì)數(shù),只是一層可空狀態(tài)
image
  • 斷點(diǎn)匯編可以看到swift_weakInit初始化,swift_weakDestroy釋放。
image
  • 常規(guī)對象弱引用對象區(qū)別:
image

2. 內(nèi)存管理 - 循環(huán)引用

主要是研究閉包捕獲外部變量,以下面代碼為例

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

<!--打印結(jié)果-->
11

從輸出結(jié)果中可以看出:閉包內(nèi)部對變量的修改將會改變外部原始變量的值,主要原因是閉包會捕獲外部變量,這個與OC中的block是一致的

  • 定義一個類,在test函數(shù)作用域消失后,會執(zhí)行init
class CJLTeacher {
    var age = 18
    //反初始化器(當(dāng)前實(shí)例對象即將被回收)
    deinit {
        print("CJLTeacher deinit")
    }
}
func test(){
    var t = CJLTeacher()
}
test()

<!--打印結(jié)果-->
CJLTeacher deinit

  • 修改例子,通過閉包修改其屬性值
class CJLTeacher {
    var age = 18
    //反初始化器(當(dāng)前實(shí)例對象即將被回收)
    deinit {
        print("CJLTeacher deinit")
    }
}
var t = CJLTeacher()
let clourse = {
    t.age += 1
}
clourse()

<!--打印結(jié)果-->
11

  • 【修改1】將上面例子修改為如下,其中閉包是否對t有強(qiáng)引用?
class CJLTeacher {
    var age = 18
    deinit {
        print("CJLTeacher deinit")
    }
}

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

<!--運(yùn)行結(jié)果-->
CJLTeacher deinit

運(yùn)行結(jié)果發(fā)現(xiàn),閉包對 t 并沒有強(qiáng)引用

  • 【修改2】繼續(xù)修改例子為如下,是否有強(qiáng)引用?
class CJLTeacher {
    var age = 18

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

    deinit {
        print("CJLTeacher deinit")
    }
}

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

從運(yùn)行結(jié)果發(fā)現(xiàn),沒有執(zhí)行deinit方法,即沒有打印CJLTeacher deinit,所以這里有循環(huán)引用

image

循環(huán)引用解決方法

有兩種方式可以解決swift中的循環(huán)引用

  • 【方式一】使用weak修飾閉包傳入的參數(shù),其中參數(shù)的類型是optional
func test(){
    var t = CJLTeacher()
    t.completionBlock = { [weak t] in
        t?.age += 1
    } 
}

  • 【方式二】使用unowned修飾閉包參數(shù),與weak的區(qū)別在于unowned不允許被設(shè)置為nil,即總是假定有值
func test(){
    var t = CJLTeacher()
    t.completionBlock = { [unowned t] in
        t.age += 1
    } 
}

捕獲列表

  • [weak t] / [unowned t] 在swift中被稱為捕獲列表

  • 定義在參數(shù)列表之前

  • 【書寫方式】捕獲列表被寫成用逗號括起來的表達(dá)式列表,并用方括號括起來

  • 如果使用捕獲列表,則即使省略參數(shù)名稱、參數(shù)類型和返回類型,也必須使用in關(guān)鍵字

  • [weak t] 就是取t的弱引用對象 類似weakself

請問下面代碼的clourse()調(diào)用后,輸出的結(jié)果是什么?

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()
}

<!--打印結(jié)果-->
0
1.85

所以從結(jié)果中可以得出:對于捕獲列表中的每個常量,閉包會利用周圍范圍內(nèi)具有相同名稱的常量/變量,來初始化捕獲列表中定義的常量。有以下幾點(diǎn)說明:

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

  • 捕獲列表中的常量的相當(dāng)于復(fù)制了變量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..<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()

運(yùn)行結(jié)果如下,發(fā)現(xiàn)并沒有打印方法和屬性

image
  • 【嘗試1】如果給屬性 和 方法 都加上 @objc,可以打印嗎?

    image

    從運(yùn)行結(jié)果看,是可以打印,但是由于類并沒有暴露給OC,所以O(shè)C是無法使用的,這樣做是沒有意義的

  • 【嘗試2】如果swift的類繼承NSObject,沒有@objc修飾屬性和方法,是否可以打印全部屬性+方法?

    image

    從結(jié)果發(fā)現(xiàn)獲取的只有init方法,主要是因?yàn)樵?swift.h文件中暴露出來的只有init方法

  • 如果想讓OC能使用,必須類繼承NSObject + @objc修飾屬性、方法

    image
  • 如果去掉@objc修飾屬性,將方法改成dynamic修飾,是否可以打印方法?

    image

    從結(jié)果可以看出,依舊不能被OC獲取到,需要修改為@objc dynamic修飾

    image

結(jié)論

  • 對于純swift類來說,沒有 動態(tài)特性dynamic(因?yàn)?code>swift是靜態(tài)語言),方法和屬性不加任何修飾符的情況下,已經(jīng)不具備runtime特性,此時(shí)的方法調(diào)度,依舊是函數(shù)表調(diào)度即V_Table調(diào)度

  • 對于純swift類,方法和屬性添加@objc標(biāo)識的情況下,可以通過runtime API獲取到,但是在OC中是無法進(jìn)行調(diào)度的,原因是因?yàn)?code>swift.h文件中沒有swift類的聲明

  • 對于繼承自NSObject類來說,如果想要動態(tài)的獲取當(dāng)前屬性+方法,必須在其聲明前添加 @objc關(guān)鍵字,如果想要使用方法交換,還必須在屬性+方法前添加dynamic關(guān)鍵字,否則當(dāng)前屬性+方法只是暴露給OC使用,而不具備任何動態(tài)特性

objc源碼驗(yàn)證

(由于xcode12.2暫時(shí)無法運(yùn)行objc源碼,下列驗(yàn)證圖片僅供參考)

  • 進(jìn)入class_copyMethodList源碼,斷住,查看此時(shí)的cls,其中data()存儲類的信息

    image
  • 進(jìn)入data,打印bits、superclass

    image

    從這里可以得出swift中有默認(rèn)基類,即_SwiftObject

  • 打印methods

    image
  • swift源碼中搜索_SwiftObject,繼承自NSObject,在內(nèi)存結(jié)構(gòu)上與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類的結(jié)構(gòu)中,其中TargetAnyClassMetadata繼承自TargetHeapMetaData,其中只有一個屬性kind,TargetAnyClassMetadata有四個屬性:isa、superclass、cacheData、data即bits

    image

    所以swift為了保留和OC交互,其在底層存儲的數(shù)據(jù)結(jié)構(gòu)上和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聲明,來幫助編譯器判斷,當(dāng)前類是一個和OC交互的類

4、元類型、AnyClass、Self

AnyObject

  • AnyObject:代表任意類的instance、類的類型、僅類遵守的協(xié)議
class CJLTeacher: NSObject {
    var age: Int = 18
}

var t = CJLTeacher()

//此時(shí)代表的就是當(dāng)前CJLTeacher的實(shí)例對象
var t1: AnyObject = t

//此時(shí)代表的是CJLTeacher這個類的類型
var t2: AnyObject = CJLTeacher.self

//繼承自AnyObject,表示JSONMap協(xié)議只有類才可以遵守
protocol JSONMap: AnyObject { }

例如如果是結(jié)構(gòu)體遵守協(xié)議,會報(bào)錯

image

需要將struct修改成class

//繼承自AnyObject,表示JSONMap協(xié)議只有類才可以遵守
protocol JSONMap: AnyObject {

}
class CJLJSONMap: JSONMap {

}

Any

  • Any:代表任意類型,包括 function類型 或者Optional類型,可以理解為AnyObjectAny的子集
//如果使用AnyObject會報(bào)錯,而Any不會
var array: [Any] = [1, "cjl", "", true]

AnyClass

  • AnyClass:代表任意實(shí)例的類型 ,類型是AnyObject.Type
    • 查看定義,是public typealias AnyClass = AnyObject.Type

T.self & T.Type

  • T.self

    • 如果T是實(shí)例對象,返回的就是它本身

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

  • T.Type:一種類型

  • T.selfT.Type類型

//此時(shí)的self類型是  CJLTeacher.Type
var t = CJLTeacher.self

打印結(jié)果如下

  • 查看t1、t2存儲的是什么?
var t = CJLTeacher()
//實(shí)例對象地址:實(shí)例對象.self 返回實(shí)例對象本身
var t1 = t.self
//存儲metadata元類型
var t2 = CJLTeacher.self

image

type(of:)

  • type(of:):用來獲取一個值的動態(tài)類型
<!--demo1-->
var age = 10 as NSNumber
print(type(of: age))

<!--打印結(jié)果-->
__NSCFNumber

<!--demo2-->
//value - static type 靜態(tài)類型:編譯時(shí)期確定好的
//type(of:) - dynamic type:Int
var age = 10
//value的靜態(tài)類型就是Any
func test(_ value: Any){

    print(type(of: value))
}

test(age)

<!--打印結(jié)果-->
Int

實(shí)踐

demo1

請問下面這段代碼的打印結(jié)果是什么?

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)

<!--打印結(jié)果-->
CJLPartTimeTeacher teach
CJLTest.CJLPartTimeTeacher

demo2

請問下面代碼的打印結(jié)果是什么?

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)

<!--打印結(jié)果-->
CJLTeacher
CJLTeacher

  • 如果將test中參數(shù)的類型修改為泛型,此時(shí)的打印是什么?
func test<T>(_ value: T){
    let valueType = type(of: value)
    print(valueType)
}

<!--打印結(jié)果-->
CJLTeacher
TestProtocol

從結(jié)果中發(fā)現(xiàn),打印并不一致,原因是因?yàn)楫?dāng)有協(xié)議、泛型時(shí),當(dāng)前的編譯器并不能推斷出準(zhǔn)確的類型,需要將value轉(zhuǎn)換為Any,修改后的代碼如下:

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

<!--打印結(jié)果-->
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()

從結(jié)果運(yùn)行看,并不能,因?yàn)?code>t.self是實(shí)例對象本身,即CJLTeacher,并不是CJLTeacher.Type類型

總結(jié)

  • 當(dāng)無弱引用時(shí),HeapObject中的refCounts等于 strongCount + unownedCount

  • 當(dāng)有弱引用時(shí),HeapObject中的refCounts等于 object + xxx + (strongCount + unownedCount) + weakCount

  • 循環(huán)應(yīng)用可以通過weak / unowned修飾參數(shù)來解決

  • swift中閉包的捕獲列表值拷貝,即深拷貝,是一個只讀的常量

  • swift由于是靜態(tài)語言,所以屬性、方法在不加任何修飾符的情況下時(shí)是不具備動態(tài)性即Runtime特性的,此時(shí)的方法調(diào)度是V-Table函數(shù)表調(diào)度

  • 如果想要OC使用swift類中的方法、屬性,需要class繼承NSObject,并使用@objc修飾

  • 如果想要使用方法交換,除了繼承NSObject+@objc修飾,還必須使用dynamic修飾

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

  • AnyObject:任意類的instance、類的類型、僅類遵守的協(xié)議,可以看作是Any的子類

  • AnyClass:任意實(shí)例類型,類型是AnyObject.Type

  • T.self:如果T是實(shí)例對象,則表示它本身,如果是類,則表示metadata.T.self的類型是T.Type

參考:http://www.lxweimin.com/p/0cc765a325cb

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內(nèi)容