一、OC對(duì)象的本質(zhì)
知識(shí)點(diǎn)
- 我們平時(shí)編寫(xiě)的OC代碼,底層實(shí)現(xiàn)其實(shí)都是C\C++代碼,OC的對(duì)象、類底層都是由C\C++結(jié)構(gòu)體實(shí)現(xiàn)的
- 可以通過(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
- 凡是繼承自
NSObject
的對(duì)象,都會(huì)自帶一個(gè)類型是Class
的isa
成員變量,將其轉(zhuǎn)成C++,就可以看到NSObject本質(zhì)上是一個(gè)叫做NSObject_IMPL
的結(jié)構(gòu)體,其成員變量isa
本質(zhì)上也是一個(gè)指向objc_class
結(jié)構(gòu)體的指針,如下所示:
NSObject對(duì)象的本質(zhì)
- 凡是繼承自
- 一個(gè)
NSObject
對(duì)象在內(nèi)存中的布局如下所示,堆空間不但會(huì)存放子類的成員變量,還會(huì)存放類對(duì)象的isa指針
image.png
- 一個(gè)
- 系統(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ì)齊而分配的,如下所示:
- 系統(tǒng)會(huì)給一個(gè)
創(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);
- OC對(duì)象主要可以分為三類:
instance實(shí)例對(duì)象、class類對(duì)象、meta-class元類對(duì)象
,他們之間的關(guān)系,可以用一張經(jīng)典的圖來(lái)表示,如下所示:
- OC對(duì)象主要可以分為三類:
上圖中的關(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找父類
- 從
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
- 從
-
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ǔ)的)
面試題
-
- 一個(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ì)齊而分配的
-
- 對(duì)象的isa指針指向哪里?
答:實(shí)例對(duì)象的isa指向類對(duì)象,類對(duì)象的isa指向元類對(duì)象,元類對(duì)象的isa指向基類的元類對(duì)象
-
- OC對(duì)象的類信息存放在哪里?
答:OC對(duì)象的實(shí)例方法列表、屬性列表、協(xié)議列表等信息存放在類對(duì)象中;OC對(duì)象的類方法列表存放在元類中;成員變量具體的值存放在實(shí)例對(duì)象中。
二、KVO
知識(shí)點(diǎn)
-
KVO
用于監(jiān)聽(tīng)某個(gè)對(duì)象屬性的值是否改變,未使用KVO監(jiān)聽(tīng)對(duì)象時(shí),NSObject對(duì)象
的實(shí)例對(duì)象和類對(duì)象的內(nèi)存布局如下:
-
-
使用了KVO監(jiān)聽(tīng)的對(duì)象,其實(shí)例對(duì)象和類對(duì)象的內(nèi)存布局如下所示:
image.png
-
使用了KVO監(jiān)聽(tīng)的對(duì)象,其實(shí)例對(duì)象和類對(duì)象的內(nèi)存布局如下所示:
- 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
方法
- KVO本質(zhì)上會(huì)生成一個(gè)
以監(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:方法
面試題
-
- 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
-
- 如何手動(dòng)觸發(fā)KVO?
答:手動(dòng)調(diào)用
willChangeValueForKey:
和didChangeVableForKey:
方法 -
- 直接修改成員變量會(huì)觸發(fā)KVO嗎?
答:不會(huì)觸發(fā)KVO
-
- 通過(guò)KVC修改屬性會(huì)觸發(fā)KVO嗎?
答:會(huì)觸發(fā)KVO
三、Category
知識(shí)點(diǎn)
- 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
- Category編譯之后的底層結(jié)構(gòu)是
- 從源碼基本可以看出,在
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)。
- 從源碼基本可以看出,在
-
- 某個(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
- 某個(gè)類的
-
- 以
調(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ā)
- 以
-
-
+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ā)送流程
-
-
-
+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方法
-
- 我們知道由于類對(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
- 我們知道由于類對(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)一的
面試題
-
- 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)用
- 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)
-
Block
本質(zhì)上也是一個(gè)OC對(duì)象,內(nèi)部也有一個(gè)isa指針
,Block是封裝了函數(shù)調(diào)用和函數(shù)調(diào)用環(huán)境的OC對(duì)象
-
- 為了保證
Block
內(nèi)部能夠正常訪問(wèn)外部變量,Block有個(gè)變量捕獲機(jī)制,用auto修飾的局部變量是值捕獲,用static修飾的局部變量是指針捕獲,全局變量不會(huì)捕獲(局部變量不加修飾符的話,默認(rèn)是用auto修飾的),規(guī)則和示例如下所示:
Block捕獲規(guī)則
Block捕獲示例.png
- 為了保證
-
self
是調(diào)用函數(shù)時(shí)傳進(jìn)來(lái)的參數(shù),也是屬于局部變量,所以捕獲的時(shí)候是值捕獲
-
-
Block
根據(jù)存放的內(nèi)存區(qū)域的不同,有三種類型:存放在全局區(qū)的叫做NSGlobalBlock-全局Block
、存放在棧區(qū)的叫做NSStackBlcok-棧Block
、存放在堆區(qū)的叫做NSMallocBlock-堆Block
,如下圖所示,可以通過(guò)調(diào)用Class方法或者isa指針來(lái)查看Block的具體類型
image.png
-
-
在
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
-
在
-
由于??臻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ù)制到堆上
-
由于??臻g的內(nèi)存隨時(shí)都會(huì)被釋放,為了保證數(shù)據(jù)安全,ARC在以下情況下,會(huì)自動(dòng)將Block拷貝到堆上,當(dāng)copy到堆上時(shí),會(huì)自動(dòng)調(diào)用Block內(nèi)部的
- 當(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
- 當(dāng)
-
- 當(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)生弱引用)
- 當(dāng)Block內(nèi)部訪問(wèn)了
-
__block修飾符
可以解決block內(nèi)部無(wú)法修改auto修飾的變量的值
的問(wèn)題,__block修飾符
不能修飾全局變量和static靜態(tài)變量,編譯器會(huì)將__block變量
包裝成一個(gè)對(duì)象,如下所示:
__block變量被包裝成了對(duì)象
-
-
- 當(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)引用
- 當(dāng)Block內(nèi)部訪問(wèn)了
- 學(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