寫在前面
前面篇章說了那么多的原理,那本篇就拿說說OC相關(guān)的題目吧...
一、Runtime Asssociate方法關(guān)聯(lián)的對象,需要我們手動釋放嗎?
當我們對象釋放時,會調(diào)用dealloc
- 1、C++函數(shù)釋放 :
objc_cxxDestruct
- 2、移除關(guān)聯(lián)屬性:
_object_remove_assocations
- 3、將弱引用自動設(shè)置
nil
:weak_clear_no_lock(&table.weak_table, (id)this)
; - 4、引用計數(shù)處理:
table.refcnts.erase(this)
- 5、銷毀對象:
free(obj)
所以,關(guān)聯(lián)對象
不需要我們手動移除,會在對象析構(gòu)即dealloc
時釋放.
dealloc的原理詳解我們將在內(nèi)存管理章節(jié)詳細講解,這里先附上一張dealloc
流程圖
二、方法的調(diào)用順序
類的方法 和 分類方法 重名,如和調(diào)用,是什么情況?
- 如果同名方法是
普通方法
,包括initialize
--先調(diào)用分類方法
- 因為分類的方法是在類
realize
實現(xiàn)之后再attach
進去的,插在類的方法的前面,所以優(yōu)先調(diào)用分類的方法
(注意:不是分類覆蓋主類!?。?/li> -
initialize
方法什么時候調(diào)用?initialize
方法也是主動調(diào)用,即第一次消息發(fā)送時
調(diào)用,為了不影響整個load
,可以將需要提前加載的數(shù)據(jù)
寫到initialize
中
- 因為分類的方法是在類
- 如果同名方法是
load方法
-- 先主類load
,后分類load
(分類之間,看編譯的順序)可以參考iOS之武功秘籍⑧: 類和分類加載過程文章中的load_images
原理分析
三、Runtime是什么?
-
runtime
是由C
和C++
匯編實現(xiàn)的一套API
,為OC
語言加入了面向?qū)ο?/code>、
以及運行時的功能
- 運行時是指
將數(shù)據(jù)類型的確定由編譯時
推遲到了運行時
- 舉例:
extension
和category
的區(qū)別
- 舉例:
- 平時編寫的
OC
代碼,在程序運行的過程中,其實最終會轉(zhuǎn)換成runtime
的C語言
代碼,runtime
是OC
的幕后工作者
1、category 類別、分類
- 專門用來給類添加新的方法
- 不能給類添加成員屬性,添加了成員屬性,也無法取到
- 注意:其實可以通過
runtime
給分類添加屬性,即屬性關(guān)聯(lián),重寫setter
、getter
方法 - 分類中用
@property
定義變量,只會生成變量的setter
、getter
方法的聲明,不能生成方法實現(xiàn) 和 帶下劃線的成員變量
2、extension 類擴展
- 可以說成是
特殊的分類
,也可稱作匿名分類
- 可以
給類添加成員屬性
,但是是私有變量
- 可以
給類添加方法
,也是私有方法
四、方法的本質(zhì),sel是什么?IMP是什么?兩者之間的關(guān)系又是什么?
方法的本質(zhì):發(fā)送消息
,發(fā)送消息會有以下幾個流程
- 1.快速查找流程——通過匯編
objc_msgSend
查找緩存cache_t
是否有imp
實現(xiàn) - 2.慢速查找流程——通過
C++
中lookUpImpOrForward
遞歸查找當前類和父類的rw
中methodlist
的方法 - 3.查找不到消息:動態(tài)方法解析——通過調(diào)用
resolveInstanceMethod
和resolveClassMethod
來動態(tài)方法決議——實現(xiàn)消息動態(tài)處理 - 4.快速轉(zhuǎn)發(fā)流程——通過
CoreFoundation
來觸發(fā)消息轉(zhuǎn)發(fā)流程,forwardingTargetForSelector
實現(xiàn)快速轉(zhuǎn)發(fā),由其他對象來實現(xiàn)處理方法 - 5.慢速轉(zhuǎn)發(fā)流程——先調(diào)用
methodSignatureForSelector
獲取到方法的簽名,生成對應(yīng)的invocation
;再通過forwardInvocation
來進行處理
SEL
是方法編號,也是方法名,在dyld
加載鏡像到內(nèi)存時,通過_read_image
方法加載到內(nèi)存的表中了
imp
是函數(shù)實現(xiàn)指針
,找imp
就是找函數(shù)的過程
SEL
和IMP
的關(guān)系就可以解釋為:
-
sel
就相當于本書的目錄標題
-
imp
就相當于書本的頁碼
-
具體的函數(shù)
就是具體頁碼對應(yīng)的內(nèi)容
比如我們想在《程序員的自我修養(yǎng)——鏈接、裝載與庫》一書中找到“動態(tài)鏈接”(SEL)
,肯定會翻到179頁(IMP)
,179頁會開始講述具體內(nèi)容(函數(shù)實現(xiàn))
五、能否向編譯后得到的類中增加實例變量?能否向運行時創(chuàng)建的類中添加實例變量?
具體情況具體分析:
- 編譯好的類不能添加實例變量
- 運行時創(chuàng)建的類可以添加實例變量,但若已注冊到內(nèi)存中就不行了
原因:
- 編譯好的實例變量存儲的位置在
ro
,而ro
是在編譯時就已經(jīng)確定了的 - ?旦編譯完成,內(nèi)存結(jié)構(gòu)就完全確定就?法修改
- 只能修改
rw
中的方法或者可以通過關(guān)聯(lián)對象的方式來添加屬性
六、[self class]和[super class]的區(qū)別以及原理分析
[self class]
就是發(fā)送消息objc_msgSend
,消息接收者是self
,方法編號class
[super class]
本質(zhì)就是objc_msgSendSuper
,消息的接收者還是self
,方法編號class
,在運行時,底層調(diào)用的是_objc_msgSendSuper2
只是
objc_msgSendSuper2
會更快,直接跳過self
的查找
代碼調(diào)試
TCJStudent
中的init
方法中打印這兩種class
調(diào)用,TCJStudent
繼續(xù)自TCJPerson
.
有點出乎意料,[self class]
點進去來到NSObject.mm
文件查看源碼
其底層是獲取對象的isa
,當前的對象是TCJStudent
,其isa
是同名的TCJStudent
,所以[self class]
打印的是TCJStudent
[super class]
中,其中super
是語法的 關(guān)鍵字
,可以通過clang
看super
的本質(zhì),這是編譯時的底層源碼,其中第一個參數(shù)是消息接收者,是一個__rw_objc_super
結(jié)構(gòu)
底層源碼中搜索__rw_objc_super
,是一個中間結(jié)構(gòu)體
在objc4-818.2
源碼中搜索objc_msgSendSuper
,查看其隱藏參數(shù)
搜索struct objc_super
通過clang
的底層編譯代碼可知,當前消息的接收者 等于 self
,而self
等于 TCJStudent
,所以 [super class]
進入class
方法源碼后,其中的self
是init
后的實例對象,實例對象的isa
指向的是本類,即消息接收者是TCJStudent
本類.
我們再來看[super class]
在運行時是否如上一步的底層編碼所示,是objc_msgSendSuper
,打開匯編調(diào)試,調(diào)試結(jié)果如下
搜索objc_msgSendSuper2
,從注釋得知,是從 類開始查找,而不是父類
查看objc_msgSendSuper2
的匯編源碼,是從superclass
中的cache
中查找方法
所以,最完整的回答如下
[self class]
方法調(diào)用的本質(zhì)是發(fā)送消息
,調(diào)用class
的消息流程,拿到元類的類型,在這里是因為類已經(jīng)加載到內(nèi)存,所以在讀取時是一個字符串類型,這個字符串類型是在map_images
的readClass
時已經(jīng)加入表中,所以打印為TCJStudent
[super class]
打印的是TCJStudent
,原因是當前的super
是一個關(guān)鍵字,在這里只調(diào)用objc_msgSendSuper2
,其實他的消息接收者和[self class]
是一模一樣的,所以返回的是TCJStudent
七、內(nèi)存平移問題
① 原始題
程序能否運行?是否正常輸出?
運行結(jié)果與普通初始化對象一模一樣,可面試的時候不可能只說能或不能,還要說出個所以然來
[person saySomething]
的本質(zhì)是對象發(fā)送消息
,那么當前的person
是什么?
-
person
的isa
指向類TCJPerson
即person的首地址
指向TCJPerson的首地址
,我們可以通過TCJPerson
的內(nèi)存平移找到cache
,在cache
中查找方法
-
[(__bridge id)obj saySomething]
中的obj
是來自于TCJPerson
這個類,然后有一個指針obj
,將其指向TCJPerson的首地址
.
所以,person是指向TCJPerson
類的結(jié)構(gòu),obj
也是指向TCJPerson
類的結(jié)構(gòu),然后都是在TCJPerson
類中的methodList
中查找方法.
② 拓展一
修改打印方法saySomething
——不但打印方法,同時打印屬性cj_name
為什么會出現(xiàn)打印不一致的情況?
其中person
方式的cj_name
是由于self
指向person
的內(nèi)存結(jié)構(gòu),然后通過內(nèi)存平移8字節(jié),取出去cj_name
,即self指針首地址平移8字節(jié)獲得
.
其中的cls
方式中的obj
指針中沒有其他的,所以obj
表示8字節(jié)指針
,self.cj_name
的獲取,相當于obj首地址的指針也需要平移8字節(jié)找cj_name
,那么此時的obj
的指針地址是多少?平移8字節(jié)
獲取的是什么?
obj
是一個指針,是存在棧
中的,棧是一個先進后出
的結(jié)構(gòu),參數(shù)傳入就是一個不斷壓棧的過程,其中隱藏參數(shù)會壓入棧
,且每個函數(shù)都會有兩個隱藏參數(shù)(id self,sel _cmd)
,可以通過clang
查看底層編譯,隱藏參數(shù)壓棧
的過程,其地址是遞減
的,而棧是從高地址->低地址
分配的,即在棧中,參數(shù)會從前往后一直壓.
super
通過clang
查看底層的編譯,是objc_msgSendSuper
,其第一個參數(shù)是一個結(jié)構(gòu)體__rw_objc_super(self,class_getSuperclass)
,那么結(jié)構(gòu)體中的屬性是如何壓棧的?可以通過自定義一個結(jié)構(gòu)體,判斷結(jié)構(gòu)體內(nèi)部成員的壓棧情況
從打印結(jié)果可以得出20
先加入,再加入10
,因此結(jié)構(gòu)體內(nèi)部的壓棧
情況是 低地址->高地址
遞增的,棧中結(jié)構(gòu)體內(nèi)部的成員
是反向壓入棧
,即低地址->高地址
是遞增的.
所以到目前為止,棧中從高地址到低地址
的順序的:self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - obj - person
-
self
和_cmd
是viewDidLoad
方法的兩個隱藏參數(shù)
,是高地址->低地址
正向壓棧的 -
class_getSuperClass
和self
為objc_msgSendSuper2
中的結(jié)構(gòu)體成員,是從最后一個成員變量,即低地址->高地址
反向壓棧的
那我們來打印下棧的存儲情況:
① obj
的棧的存儲情況
② person
的棧的存儲情況
③ obj
與person
一起的棧的存儲情況
其中為什么class_getSuperclass
是ViewController
,因為objc_msgSendSuper2
返回的是當前類
,兩個self
,并不是同一個self
,而是棧的指針不同,但是指向同一片內(nèi)存空間
-
[(__bridge id)obj saySomething]
調(diào)用時,此時的obj
是TCJPerson: 0x7ffee0d57048
,所以saySomething
方法中傳入的self
還是TCJPerson
,但并不是我們通常認為的TCJPerson
,是我們當前傳入的消息接收者
,即TCJPerson: 0x7ffee0d57048
,是TCJPerson
的實例對象,此時的操作與普通的TCJPerson
是一致的,即TCJPerson
的地址內(nèi)存平移8字節(jié)
. - 普通
person
流程:person -> cj_name - 內(nèi)存平移8字節(jié)
-
obj
流程:0x7ffee0d57048 + 0x80 -> 0x7ffee0d57050
,即為self
,指向<ViewController: 0x7ffee0d57050>
其中 person
與 TCJPerson
的關(guān)系是 person
是以TCJPerson
為模板的實例化對象,即alloc
有一個指針地址,指向isa
,isa
指向TCJPerson
,它們之間關(guān)聯(lián)是有一個isa指向
.
而obj
也是指向TCJPerson
的關(guān)系,編譯器會認為obj
也是TCJPerson
的一個實例化對象,即obj
相當于isa
,即首地址,指向TCJPerson
,具有和person
一樣的效果,簡單來說,我們已經(jīng)完全將編譯器騙過了,即obj
也有cj_name
.由于person
查找cj_name
是通過內(nèi)存平移8字節(jié)
,所以obj
也是通過內(nèi)存平移8字節(jié)
去查找cj_name
.
③ 拓展二
修改viewDidLoad
——在obj
前面加個臨時字符串變量
同樣道理,在obj
入棧前已經(jīng)有了temp
變量,此時訪問self.cj_name
就會訪問到temp
④ 拓展三
去掉臨時變量,TCJPerson
類新增字符串屬性cj_hobby
,打印方法改為打印cj_hobby
,運行
ViewController
就是obj
偏移16字節(jié)
拿到的super_class
.
⑤ 拓展四
將TCJPerson
類新增字符串屬性cj_hobby
,改成int
類型,打印
這種情況就是野指針——指針偏移的offset
不正確,獲取不到對應(yīng)變量的首地址.
八、Runtime是如何實現(xiàn)weak的,為什么可以自動置nil
- 1、通過
SideTable
找到我們的weak_table
- 2、
weak_table
根據(jù)referent
找到或者創(chuàng)建weak_entry_t
- 3、然后
append_referrer(entry,referrer)
將我的新弱引用的對象加進去entry
- 4、最后
weak_entry_insert
,把entry
加入到我們的weak_table
在weak
一行打下斷點運行項目
Xcode
菜單欄Debug->Debug Workflow->Always show Disassembly
打上勾查看匯編——匯編代碼會來到libobjc
庫的objc_initWeak
① weak創(chuàng)建過程
①.1 objc_initWeak
-
location
:表示__weak指針
的地址(我們研究的就是__weak指針
指向的內(nèi)容怎么置為nil
) -
newObj
:所引用的對象,即例子中的person
①.2 storeWeak
-
HaveOld
:weak指針
之前是否已經(jīng)指向了一個弱引用 -
HaveNew
:weak指針
是否需要指向一個新引用 -
CrashIfDeallocating
:如果被弱引用的對象正在析構(gòu),此時再弱引用該對象,是否應(yīng)該crash
storeWeak
最主要的兩個邏輯點
由于是第一次調(diào)用,所以走
haveNew
分支——獲取到的是新的散列表SideTable
,主要執(zhí)行了weak_register_no_lock
方法來進行插入
①.3 weak_register_no_lock
- 主要進行了
isTaggedPointer
和deallocating
條件判斷 - 將被弱引用對象所在的
weak_table
中的weak_entry_t
哈希數(shù)組中取出對應(yīng)的weak_entry_t
- 如果
weak_entry_t
不存在,則會新建一個并插入 - 如果存在就將指向被弱引用對象地址的指針
referrer
通過函數(shù)append_referrer
插入到對應(yīng)的weak_entry_t
引用數(shù)組
①.4 append_referrer
找到弱引用對象的對應(yīng)的weak_entry_t
哈希數(shù)組中插入
② weak創(chuàng)建流程
③ weak銷毀過程
由于弱引用在析構(gòu)
dealloc
時自動置空,所以查看dealloc
的底層實現(xiàn)
- _objc_rootDealloc->rootDealloc
- rootDealloc->object_dispose
- object_dispose->objc_destructInstance
- objc_destructInstance->clearDeallocating
- clearDeallocating->sidetable_clearDeallocating
- weak_clear_no_lock->table.refcnts.erase
④ weak銷毀流程
九、利用runtime-API創(chuàng)建對象
① API介紹
①.1 動態(tài)創(chuàng)建類
①.2 添加成員變量
①.3 注冊到內(nèi)存
①.4 添加屬性變量
①.5 添加方法
② 整體使用
③ 注意事項
- 記得導(dǎo)入
<objc/runtime.h>
- 添加成員變量
class_addIvar
必須在objc_registerClassPair
前,因為注冊到內(nèi)存時ro
已經(jīng)確定了,不能再往ivars
添加 - 添加屬性變量
class_addProperty
可以在注冊內(nèi)存前后,因為是往rw
中添加的 -
class_addProperty
中“屬性的屬性”——nonatomic/copy
是根據(jù)屬性的類型變化而變化的 -
class_addProperty
不會自動生成setter
和getter
方法,因此直接調(diào)用KVC
會崩潰- 不只可以通過
KVC
打印來檢驗,也可以下斷點查看ro、rw
的結(jié)構(gòu)來檢驗
- 不只可以通過
十、Method Swizzing坑點
① method-swizzling 是什么?
method-swizzling
的含義是方法交換
,其主要作用是在運行時將一個方法的實現(xiàn)替換成另一個方法的實現(xiàn)
,這就是我們常說的iOS黑魔法
.
在OC
中就是利用method-swizzling
實現(xiàn)AOP
,其中AOP
(Aspect Oriented Programming
,面向切面編程)是一種編程的思想,區(qū)別于OOP
(面向?qū)ο缶幊蹋?
-
OOP
和AOP
都是一種編程的思想 -
OOP
編程思想更加傾向于對業(yè)務(wù)模塊的封裝
,劃分出更加清晰的邏輯單元 - 而
AOP
是面向切面進行提取封裝
,提取各個模塊中的公共部分
,提高模塊的復(fù)用率
,降低業(yè)務(wù)之間的耦合性.
每個類都維護著一個方法列表
,即methodList
,methodList
中有不同的方法即Method
,每個方法中包含了方法的sel
和IMP
,方法交換就是將sel
和imp
原本的對應(yīng)斷開,并將sel
和新的IMP
生成對應(yīng)關(guān)系.
如下圖所示,交換前后的sel
和IMP
的對應(yīng)關(guān)系
② method-swizzling涉及的相關(guān)API
- 通過
sel
獲取方法Method
-
class_getInstanceMethod:
獲取實例方法 -
class_getClassMethod:
獲取類方法
-
-
method_getImplementation:
獲取一個方法的實現(xiàn) -
method_setImplementation:
設(shè)置一個方法的實現(xiàn) -
method_getTypeEncoding:
獲取方法實現(xiàn)的編碼類型 -
class_addMethod:
添加方法實現(xiàn) -
class_replaceMethod:
用一個方法的實現(xiàn),替換另一個方法的實現(xiàn),即aIMP
指向bIMP
,但是bIMP
不一定指向aIMP
-
method_exchangeImplementations:
交換兩個方法的實現(xiàn),即aIMP -> bIMP
,bIMP -> aIMP
③ 坑點1:method-swizzling使用過程中的一次性問題
所謂的一次性就是:mehod-swizzling
寫在load
方法中,而load
方法會主動調(diào)用多次
,這樣會導(dǎo)致方法的重復(fù)交換
,使方法sel
的指向又恢復(fù)成原來的imp
的問題
解決方案
可以通過單例設(shè)計
原則,使方法交換只執(zhí)行一次
,在OC
中可以通過dispatch_once
實現(xiàn)單例
④ 坑點2:子類沒有實現(xiàn),父類實現(xiàn)了
- 父類
TCJPerson
類中有-personInstanceMethod
方法,子類TCJStudent
類沒有重寫 - 子類
TCJStudent
類新建分類做了方法交換,新方法中調(diào)用舊方法 -
TCJPerson
類、TCJStudent
類調(diào)用-personInstanceMethod
子類打印出結(jié)果,而父類調(diào)用卻崩潰了,為什么會這樣呢?
-
[student personInstanceMethod];
中不報錯是因為student
中的imp
交換成了cj_studentInstanceMethod
,而TCJStudent
中有這個方法(在TCJ
分類中),所以不會報錯. - 崩潰的點在于
[person personInstanceMethod];
,其本質(zhì)原因:TCJStudent
的分類TCJ
中進行了方法交換,將person
中imp
交換成了TCJStudent
中的cj_studentInstanceMethod
,然后需要去TCJPerson
中的找cj_studentInstanceMethod
,但是TCJPerson
中沒有cj_studentInstanceMethod
方法,即相關(guān)的imp
找不到,所以就崩潰了
優(yōu)化:避免imp找不到
通過class_addMethod
嘗試添加你要交換的方法
- 如果
添加成功
,即類中沒有這個方法,則通過class_replaceMethod
進行替換,其內(nèi)部會調(diào)用class_addMethod
進行添加 - 如果添加不成功,即類中有這個方法,則通過
method_exchangeImplementations
進行交換
這樣就不會報錯了.
下面是class_replaceMethod
、class_addMethod
和method_exchangeImplementations
的源碼實現(xiàn)
其中class_replaceMethod
和class_addMethod
中都調(diào)用了addMethod
方法,區(qū)別在于bool值
的判斷,下面是addMethod
的源碼實現(xiàn)
⑤ 坑點3:子類沒有實現(xiàn),父類也沒有實現(xiàn),下面的調(diào)用有什么問題?
在上面測試代碼的基礎(chǔ)上加入父類TCJPerson
的personInstanceMethod
的方法只寫了方法聲明,沒有方法實現(xiàn),卻做了方法交換——會造成死循環(huán)
原因是 棧溢出,遞歸死循環(huán)
了,那么為什么會發(fā)生遞歸呢?----主要是因為 personInstanceMethod
沒有實現(xiàn),然后在方法交換時,始終都找不到oriMethod
,然后交換了寂寞,即交換失敗,當我們調(diào)用personInstanceMethod(oriMethod)
時,也就是oriMethod
會進入TCJ分類
中cj_studentInstanceMethod
方法,然后這個方法中又調(diào)用了cj_studentInstanceMethod
,此時的cj_studentInstanceMethod
并沒有指向oriMethod
,然后導(dǎo)致了自己調(diào)自己,即遞歸死循環(huán)
優(yōu)化:避免遞歸死循環(huán)
如果oriMethod
為空,為了避免方法交換沒有意義,而被廢棄,需要做一些事情
- 通過
class_addMethod
給oriSEL
添加swiMethod
方法 - 通過
method_setImplementation
將swiMethod
的IMP
指向不做任何事的空實現(xiàn)
⑥ method-swizzling - 類方法
類方法和實例方法的method-swizzling
的原理是類似的,唯一的區(qū)別是類方法存在元類
中,所以可以做如下操作
- 需要通過
class_getClassMethod
方法獲取類方法 - 在調(diào)用
class_addMethod
和class_replaceMethod
方法添加和替換時,需要傳入的類是元類
,元類可以通過object_getClass
方法獲取類的元類
⑦ method-swizzling的應(yīng)用
method-swizzling
最常用的應(yīng)用是防止數(shù)組、字典等越界崩潰
問題
在iOS
中NSNumber
、NSArray
、NSDictionary
等這些類都是類簇,一個NSArray
的實現(xiàn)可能由多個類組成.所以如果想對NSArray
進行Swizzling
,必須獲取到其“真身”進行Swizzling
,直接對NSArray
進行操作是無效的.
下面列舉了NSArray
和NSDictionary
本類的類名,可以通過Runtime
函數(shù)取出本類.
⑧ 注意事項
使用Method Swizzling
有以下注意事項:
- 盡可能在
+load
方法中交換方法 - 最好使用
單例
保證只交換一次 - 自定義方法名不能產(chǎn)生沖突
- 對于系統(tǒng)方法要調(diào)用原始實現(xiàn),避免對系統(tǒng)產(chǎn)生影響
- 做好注釋(因為方法交換比較繞)
- 迫不得已情況下才去使用方法交換
寫在后面
和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.