iOS底層原理(一):OC的本質(zhì)、KVO原理、Category原理、Block原理

一、OC對(duì)象的本質(zhì)
知識(shí)點(diǎn)
    1. 我們平時(shí)編寫(xiě)的OC代碼,底層實(shí)現(xiàn)其實(shí)都是C\C++代碼,OC的對(duì)象、類底層都是由C\C++結(jié)構(gòu)體實(shí)現(xiàn)的
    1. 可以通過(guò)以下命令,可以將OC代碼轉(zhuǎn)換成C++代碼,這種轉(zhuǎn)換并不是準(zhǔn)確的,因?yàn)閺腃lang新版本開(kāi)始會(huì)將iOS代碼轉(zhuǎn)換成中間代碼,而不是C++代碼,所以轉(zhuǎn)成C++的代碼僅供參考(PS:大部分情況下還是準(zhǔn)確的,想要特別精準(zhǔn)就需要通過(guò)查看匯編代碼來(lái)實(shí)現(xiàn)了)
將Objective-C代碼轉(zhuǎn)換為C\C++代碼:
xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc  OC源文件  -o  輸出的CPP文件
如果需要鏈接其他框架,使用-framework參數(shù)。比如-framework UIKit
    1. 凡是繼承自NSObject的對(duì)象,都會(huì)自帶一個(gè)類型是Classisa成員變量,將其轉(zhuǎn)成C++,就可以看到NSObject本質(zhì)上是一個(gè)叫做NSObject_IMPL的結(jié)構(gòu)體,其成員變量isa本質(zhì)上也是一個(gè)指向objc_class結(jié)構(gòu)體的指針,如下所示:
      NSObject對(duì)象的本質(zhì)
    1. 一個(gè)NSObject對(duì)象在內(nèi)存中的布局如下所示,堆空間不但會(huì)存放子類的成員變量,還會(huì)存放類對(duì)象的isa指針
      image.png
    1. 系統(tǒng)會(huì)給一個(gè)NSObject對(duì)象分配16個(gè)字節(jié)的內(nèi)存,而NSObject對(duì)象實(shí)際只占用了8個(gè)字節(jié)的內(nèi)存,占用的這8個(gè)字節(jié)的就是isa指針,剩下8個(gè)字節(jié)是系統(tǒng)為了內(nèi)存對(duì)齊而分配的,如下所示:
創(chuàng)建一個(gè)實(shí)例對(duì)象,至少需要多少內(nèi)存?
#import <objc/runtime.h>
class_getInstanceSize([NSObject class]);

創(chuàng)建一個(gè)實(shí)例對(duì)象,實(shí)際上分配了多少內(nèi)存?
#import <malloc/malloc.h>
malloc_size((__bridge const void *)obj);
image.png
    1. OC對(duì)象主要可以分為三類:instance實(shí)例對(duì)象、class類對(duì)象、meta-class元類對(duì)象,他們之間的關(guān)系,可以用一張經(jīng)典的圖來(lái)表示,如下所示:
image.png

上圖中的關(guān)系用文字表示是這樣的:

- instance的isa指向class

- class的isa指向meta-class

- meta-class的isa指向基類的meta-class

- class的superclass指向父類的class,如果沒(méi)有父類,superclass指針為nil

- meta-class的superclass指向父類的meta-class,基類的meta-class的superclass指向基類的class

- instance調(diào)用對(duì)象方法的軌跡:isa找到class,方法不存在,就通過(guò)superclass找父類

- class調(diào)用類方法的軌跡:isa找meta-class,方法不存在,就通過(guò)superclass找父類
    1. 64bit開(kāi)始,也就是采用了ARM64處理器之后,OC的isa被改進(jìn)成了union共用體,其isa指針并不是直接指向類對(duì)象或者元類對(duì)象的,而是要通過(guò)一個(gè)&ISA_MASK的位運(yùn)算,才能獲取到真正的類對(duì)象或者元類對(duì)象的地址;優(yōu)化之后的isa指針,每一位都有其含義,雖然這么優(yōu)化節(jié)省的內(nèi)存不多,但是對(duì)于使用頻率如此之高的isa指針來(lái)說(shuō),還是非常有意義的,至于為什么要經(jīng)過(guò)&ISA_MASK位運(yùn)算才能拿到類對(duì)象的真正地址,我們后續(xù)再說(shuō)
      isa & ISA_MASK
    1. isa & ISA_MASK是指向objc_class結(jié)構(gòu)體的,我們從objc4源碼摘出來(lái)objc_class的結(jié)構(gòu)如下所示,我們可以看出objc_class結(jié)構(gòu)體內(nèi)部,保存了isa、superclass、方法緩存、class_rw_t可讀寫(xiě)的類信息(方法列表 、屬性列表 、協(xié)議列表) 、class_ro_t只讀的類信息(類名、成員變量列表)等信息
      image.png

PS: 這里的方法列表、屬性列表、協(xié)議列表都?xì)w屬于class_rw_t可讀寫(xiě)的類信息中,而成員變量列表歸屬于class_ro_t只讀的類信息中,所以方法列表、屬性列表、協(xié)議列表都是可以通過(guò)RunTime動(dòng)態(tài)增加的,而成員變量列表就不能動(dòng)態(tài)增加?。。?strong>這就是Category為什么只能增加方法,不能增加成員變量的核心原因(通過(guò)關(guān)聯(lián)方式增加的成員變量是通過(guò)全局變量來(lái)存儲(chǔ)的)

面試題
    1. 一個(gè)NSObject對(duì)象占用多少字節(jié)的內(nèi)存?

    答:一個(gè)NSObject實(shí)際占用8個(gè)字節(jié),是用來(lái)存放isa指針的,而系統(tǒng)分配了16個(gè)字節(jié),額外多的8個(gè)字節(jié)是為了內(nèi)存對(duì)齊而分配的

    1. 對(duì)象的isa指針指向哪里?

    答:實(shí)例對(duì)象的isa指向類對(duì)象,類對(duì)象的isa指向元類對(duì)象,元類對(duì)象的isa指向基類的元類對(duì)象

    1. OC對(duì)象的類信息存放在哪里?

    答:OC對(duì)象的實(shí)例方法列表、屬性列表、協(xié)議列表等信息存放在類對(duì)象中;OC對(duì)象的類方法列表存放在元類中;成員變量具體的值存放在實(shí)例對(duì)象中。

二、KVO
知識(shí)點(diǎn)
    1. KVO用于監(jiān)聽(tīng)某個(gè)對(duì)象屬性的值是否改變,未使用KVO監(jiān)聽(tīng)對(duì)象時(shí),NSObject對(duì)象實(shí)例對(duì)象和類對(duì)象內(nèi)存布局如下:
實(shí)例對(duì)象和類對(duì)象的內(nèi)存布局.png
    1. 使用了KVO監(jiān)聽(tīng)的對(duì)象,其實(shí)例對(duì)象和類對(duì)象的內(nèi)存布局如下所示:
      image.png
    1. KVO本質(zhì)上會(huì)生成一個(gè)NSKVONotifying_XXX的派生類,如上圖所示,MJPerson的實(shí)例對(duì)象isa指針會(huì)指向該派生類,該派生類的superClass指針會(huì)指向MJPerson的類對(duì)象,KVO的機(jī)制還會(huì)在派生類中重寫(xiě)此屬性的set方法,在set方法中,先調(diào)用willChangeValueForKey方法,再調(diào)用原來(lái)的setter實(shí)現(xiàn),再調(diào)用didChangeValueForKey方法
以監(jiān)聽(tīng)age屬性為例,KVO的觸發(fā)流程:
[self willChangeValueForKey:@"age"];
super.age = age;  //原來(lái)的方法實(shí)現(xiàn)
[self didChangeValueForKey:@"age"];  
-----didChangeValueForKey方法內(nèi)部會(huì)調(diào)用observer的observeValueForKeyPath:ofObject:change:context:方法
面試題
    1. KVO的本質(zhì)是什么?

    答:利用RuntimeAPI動(dòng)態(tài)實(shí)現(xiàn)了一個(gè)子類,并且讓實(shí)例對(duì)象的isa指向這個(gè)全新的子類,當(dāng)修改實(shí)例對(duì)象的屬性時(shí),會(huì)調(diào)用Foundation的_NSSetXXXValueAndNotify函數(shù),其內(nèi)部的調(diào)用順序是這樣的:willChangesValueForKey、父類原來(lái)的setter、didChangeValueForKey

    1. 如何手動(dòng)觸發(fā)KVO?

    答:手動(dòng)調(diào)用willChangeValueForKey:didChangeVableForKey:方法

    1. 直接修改成員變量會(huì)觸發(fā)KVO嗎?

    答:不會(huì)觸發(fā)KVO

    1. 通過(guò)KVC修改屬性會(huì)觸發(fā)KVO嗎?

    答:會(huì)觸發(fā)KVO

三、Category
知識(shí)點(diǎn)
    1. Category編譯之后的底層結(jié)構(gòu)是struct category_t,每一個(gè)分類都會(huì)對(duì)應(yīng)一個(gè)category_t結(jié)構(gòu)體,通過(guò)閱讀objc4源碼我們可以得知,struct category_t里面存儲(chǔ)著分類的對(duì)象方法、類方法、屬性、協(xié)議信息,如下所示:
      category_t
    1. 從源碼基本可以看出,在category_t結(jié)構(gòu)體中,對(duì)象方法,類方法,協(xié)議,和屬性都可以找到對(duì)應(yīng)的存儲(chǔ)方式。并且我們發(fā)現(xiàn)分類結(jié)構(gòu)體中是不存在成員變量的,因此分類中是不允許添加成員變量的。分類中添加的屬性并不會(huì)幫助我們自動(dòng)生成成員變量,只會(huì)生成get set方法的聲明,需要我們自己去實(shí)現(xiàn)。
    1. 某個(gè)類的Category的加載過(guò)程是這樣的:
    • (1). 首先會(huì)通過(guò)Runtime加載這個(gè)類的所有分類的數(shù)據(jù),包括這個(gè)類的所有分類中的方法列表、屬性列表、協(xié)議列表

    • (2). 然后把這個(gè)類的所有分類的方法列表、屬性列表、協(xié)議列表,分別合并到一個(gè)各自的大數(shù)組中,這里對(duì)分類采用了while(i--)倒敘遍歷,所以后面參與編譯的分類,其數(shù)據(jù)會(huì)放在數(shù)組的前面,然后會(huì)將合并后的大數(shù)組,插入到這個(gè)類的class_rw_t結(jié)構(gòu)體中,整個(gè)流程的源碼如下圖所示:

      將分類中的數(shù)據(jù)合并到大數(shù)組中.png

    • (3). 將分類數(shù)據(jù)(方法列表、屬性列表、協(xié)議列表),插入到類class_rw_t結(jié)構(gòu)體時(shí),調(diào)用了attachLists()方法,在這個(gè)方法內(nèi)部,會(huì)將分類的方法,屬性,協(xié)議列表放在了類對(duì)象中原本存儲(chǔ)的方法,屬性,協(xié)議列表的前面,如下面源碼所示,這樣做的目的是為了保證分類方法優(yōu)先調(diào)用,我們知道當(dāng)分類重寫(xiě)本類的方法時(shí),會(huì)覆蓋本類的方法,其實(shí)本質(zhì)上并不是覆蓋,而是優(yōu)先調(diào)用,本類原來(lái)的方法依然是存在的。

      image.png

    1. 調(diào)用 A 對(duì)象的 B 方法為例,梳理一下整個(gè)調(diào)用流程:
    • 首先從A對(duì)象的內(nèi)存中拿到isa指針,進(jìn)行一次位運(yùn)算isa & ISA_MASK后,拿到真正的A對(duì)象的類地址

    • 然后從方法緩存cache中查找B方法,找到就調(diào)用,找不到的話就從class_rw_t的方法列表methods中查找方法,此時(shí)Runtime早已經(jīng)幫我們把分類的方法列表,插入到了方法列表methods中了,所以我們只需要按順序在methods中查找,找到就調(diào)用,并將其放入方法緩存cache

    • 找不到就通過(guò)superClass指針找到父類,將上述步驟再走一遍,然后一直重復(fù),直到superclass為空為止,如果一直沒(méi)找到B方法,就進(jìn)入動(dòng)態(tài)方法解析和消息轉(zhuǎn)發(fā)

    1. +load方法會(huì)在runtime加載類、分類時(shí)調(diào)用,每個(gè)類、分類+load方法在程序運(yùn)行過(guò)程中只調(diào)用一次,調(diào)用順序如下:
    • (1). 先調(diào)用類的+load方法,按照編譯順序調(diào)用,先編譯的先調(diào)用,調(diào)用子類的. +load方法之前會(huì)先調(diào)用父類的+load方法

    • (2). 再調(diào)用分類的+load方法,按照編譯順序調(diào)用,先編譯的先調(diào)用

      PS: +load方法是根據(jù)方法地址直接調(diào)用的,不會(huì)走objc_msgSend消息發(fā)送流程

    1. +initialize方法會(huì)在類第一次接受到消息時(shí)調(diào)用,調(diào)用順序是:先調(diào)用父類的+initialize方法,在調(diào)用子類的+initialize方法,需要注意的是:
    • 如果子類沒(méi)有實(shí)現(xiàn)+initialize方法,就會(huì)調(diào)用父類的+initialize方法(所以父類的+initialize方法可能會(huì)被調(diào)多次)

    • 如果分類實(shí)現(xiàn)+initialize方法,就會(huì)覆蓋類本身的+initialize方法

    1. 我們知道由于類對(duì)象底層結(jié)構(gòu)的限制,不能將成員變量動(dòng)態(tài)插入到類中,但可以通過(guò)關(guān)聯(lián)對(duì)象來(lái)間接實(shí)現(xiàn),關(guān)聯(lián)對(duì)象的原理是:將成員變量存儲(chǔ)在全局統(tǒng)一的AssociationsManager中,而不是存儲(chǔ)在對(duì)象本身的內(nèi)存中,由AssociationsHashMap來(lái)管理所有被添加到對(duì)象中的關(guān)聯(lián)對(duì)象,其流程如下所示:
      image.png
面試題
    1. Category中有l(wèi)oad方法嘛?load方法什么時(shí)候調(diào)用的?load方法能被繼承嗎?

    答:有l(wèi)oad方法,load方法在runtime加載類、分類時(shí)調(diào)用,load方法可以繼承,一般情況下不會(huì)主動(dòng)調(diào)用load方法,都是讓系統(tǒng)自動(dòng)調(diào)用

    1. load、initialize方法的區(qū)別是什么?

(1). 調(diào)用時(shí)機(jī)不同:

1>. load是runtime加載類和分類的時(shí)候調(diào)用,只會(huì)被調(diào)用一次
2>. initialize是類第一次接收到消息的時(shí)候調(diào)用,當(dāng)子類沒(méi)有initialize方法時(shí),就會(huì)調(diào)用父類的,所以可能會(huì)被調(diào)用多次

(2). 調(diào)用方式不同:

1>.load是通過(guò)函數(shù)地址直接調(diào)用的
2>.initialize是通過(guò)objc_msgSend調(diào)用的

(3). 調(diào)用順序不同:

1>.load:先調(diào)用類的load方法,先編譯的先調(diào)用,在調(diào)用load之前會(huì)先調(diào)用父類的load方法;然后在調(diào)用分類的load方法,也是先編譯的先調(diào)用;分類的load方法不會(huì)覆蓋本類的load方法
2>.initialize:先初始化父類,之后再初始化子類。如果子類沒(méi)有實(shí)現(xiàn)+initialize,會(huì)調(diào)用父類的+initialize(所以父類的+initialize可能會(huì)被調(diào)用多次),如果分類實(shí)現(xiàn)了+initialize,就覆蓋類本身的+initialize調(diào)用。
四、Block
知識(shí)點(diǎn)
    1. Block本質(zhì)上也是一個(gè)OC對(duì)象,內(nèi)部也有一個(gè)isa指針,Block是封裝了函數(shù)調(diào)用和函數(shù)調(diào)用環(huán)境的OC對(duì)象
    1. 為了保證Block內(nèi)部能夠正常訪問(wèn)外部變量,Block有個(gè)變量捕獲機(jī)制,用auto修飾的局部變量是值捕獲,用static修飾的局部變量是指針捕獲,全局變量不會(huì)捕獲(局部變量不加修飾符的話,默認(rèn)是用auto修飾的),規(guī)則和示例如下所示:
      Block捕獲規(guī)則

      Block捕獲示例.png
    1. self是調(diào)用函數(shù)時(shí)傳進(jìn)來(lái)的參數(shù),也是屬于局部變量,所以捕獲的時(shí)候是值捕獲
    1. Block根據(jù)存放的內(nèi)存區(qū)域的不同,有三種類型:存放在全局區(qū)的叫做NSGlobalBlock-全局Block、存放在棧區(qū)的叫做NSStackBlcok-棧Block、存放在堆區(qū)的叫做NSMallocBlock-堆Block,如下圖所示,可以通過(guò)調(diào)用Class方法或者isa指針來(lái)查看Block的具體類型
      image.png
    1. MRC,如果Block內(nèi)部沒(méi)有訪問(wèn)用auto修飾的變量,那么Block就是全局Block;如果Block訪問(wèn)了用auto修飾的變量,那么Block就是棧Block;如果給棧Block使用了Copy,那么就會(huì)將棧Block復(fù)制到堆上,從而變成了堆Block,如下圖所示:
      MRC下Block的類型.png
    1. 由于??臻g的內(nèi)存隨時(shí)都會(huì)被釋放,為了保證數(shù)據(jù)安全,ARC在以下情況下,會(huì)自動(dòng)將Block拷貝到堆上,當(dāng)copy到堆上時(shí),會(huì)自動(dòng)調(diào)用Block內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù),_Block_object_assign會(huì)根據(jù)捕獲的對(duì)象的修飾符(__strong、__weak、__unsafe__unretained)來(lái)做出相應(yīng)的操作,形成強(qiáng)引用或者弱引用
      ARC的這些情況下,棧Block會(huì)自動(dòng)復(fù)制到堆上
    1. 當(dāng)block堆中移除時(shí),會(huì)調(diào)用block內(nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_disposeh函數(shù),_Block_object_disposeh函數(shù)會(huì)自動(dòng)釋放捕獲的變量
      image.png
    1. 當(dāng)Block內(nèi)部訪問(wèn)了auto修飾的變量時(shí),會(huì)根據(jù)Block的類型的不同,而出現(xiàn)不同的情況:
    • 當(dāng)block在棧上時(shí),如果block內(nèi)部訪問(wèn)了auto變量,將始終不會(huì)對(duì)auto變量產(chǎn)生強(qiáng)引用

    • 當(dāng)block在堆上時(shí),如果內(nèi)部訪問(wèn)了auto變量將會(huì)調(diào)用block內(nèi)部的copy函數(shù),copy函數(shù)會(huì)根據(jù)auto變量的修飾符來(lái)決定對(duì)auto變量產(chǎn)生強(qiáng)引用還是弱引用(如果auto變量用__strong修飾,就會(huì)產(chǎn)生強(qiáng)引用;如果用__weak、__unsafe_unretained修飾就會(huì)產(chǎn)生弱引用)

    1. __block修飾符可以解決block內(nèi)部無(wú)法修改auto修飾的變量的值的問(wèn)題,__block修飾符不能修飾全局變量和static靜態(tài)變量,編譯器會(huì)將__block變量包裝成一個(gè)對(duì)象,如下所示:
      __block變量被包裝成了對(duì)象
    1. 當(dāng)Block內(nèi)部訪問(wèn)了__block修飾的變量時(shí),會(huì)根據(jù)Block的類型的不同,而出現(xiàn)不同的情況:
    • 當(dāng)block在棧上時(shí),如果block內(nèi)部訪問(wèn)了__block變量,將始終不會(huì)對(duì)__block變量產(chǎn)生強(qiáng)引用

    • 當(dāng)block在堆上時(shí),如果內(nèi)部訪問(wèn)了__block變量,將會(huì)調(diào)用block內(nèi)部的copy函數(shù),copy函數(shù)會(huì)對(duì)__block變量形成強(qiáng)引用

    1. 學(xué)會(huì)了以上知識(shí)點(diǎn)之后,再來(lái)思考一下經(jīng)典的循環(huán)引用問(wèn)題,是不是就感覺(jué)很簡(jiǎn)單了呢?我們來(lái)一起看一下,所謂循環(huán)引用就是對(duì)象持有Block,而B(niǎo)lock也持有了self,導(dǎo)致雙方都無(wú)法釋放,從而導(dǎo)致內(nèi)存泄漏,如下面代碼所示:
self.block = ^{
     NSLog(@"%@",self);
};
  • 循環(huán)引用分析:在ARC環(huán)境下,block被賦值給了Block類型的self.block成員變量,所以這個(gè)block是堆block;block內(nèi)部訪問(wèn)了self變量,我們知道self是局部變量,不加修飾符的話默認(rèn)是用auto和__strong修飾的;既然block在堆上,并且內(nèi)部訪問(wèn)了auto修飾的變量,那么將會(huì)在block內(nèi)部調(diào)用copy函數(shù),copy函數(shù)會(huì)根據(jù)修飾符進(jìn)行強(qiáng)/弱引用,此處使用__strong修飾的,所以會(huì)對(duì)self進(jìn)行一次強(qiáng)引用,而self對(duì)block產(chǎn)生的也是強(qiáng)引用,所以產(chǎn)生了循環(huán)引用,如下圖所示:

    循環(huán)引用問(wèn)題.png

  • 解決方案:解決起來(lái)也很簡(jiǎn)單,只需要將self換成__weak修飾的就可以了,這樣在調(diào)用block內(nèi)部的copy方法時(shí),對(duì)self產(chǎn)生的就是弱引用了,如以下代碼所示:

__weak typeof(self) weakSelf = self;
self.block = ^{
  NSLog(@"%@",weakSelf);
};
面試題

- 1. Block的本質(zhì)是什么?

答: Block是封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境的OC對(duì)象

- 2. __block的作用是什么?

答:__block可以解決block內(nèi)部無(wú)法修改auto修飾的變量的問(wèn)題,編譯器會(huì)將__block修飾的變量包裝成一個(gè)對(duì)象

- 3. block的屬性修飾詞為什么是copy?

答: block使用copy其實(shí)是MRC留下來(lái)的一個(gè)傳統(tǒng),在MRC下,block創(chuàng)建在棧區(qū), 使用copy就能把它放到堆區(qū), 這樣在作用域外調(diào)用該block程序就不會(huì)崩潰;而在ARC下block的屬性修飾詞是copy和strong都可以,ARC會(huì)在需要的時(shí)候,自動(dòng)幫我們把block從棧上拷貝到堆上

- 4. block中修改NSMutableArray中的元素,需不需要添加__block?

答: 不需要,僅僅修改數(shù)組中的元素是不需要加__block的,如果是給array重新賦值新的數(shù)組,這時(shí)候才需要加__block

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。