【面試-1】Runtime Asssociate方法關(guān)聯(lián)的對象嗦随,需要在dealloc中釋放?
當我們對象釋放時,會調(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 源碼
dealloc的源碼查找路徑為:dealloc
-> _objc_rootDealloc
-> rootDealloc
-> object_dispose
(釋放對象)-> objc_destructInstance
-> _object_remove_assocations
-
在objc源碼中搜索
dealloc
的源碼實現(xiàn)
-
進入
_objc_rootDealloc
源碼實現(xiàn)盯质,主要是對對象進行析構(gòu)
-
進入
rootDealloc
源碼實現(xiàn),發(fā)現(xiàn)其中有關(guān)聯(lián)屬性時設(shè)置bool值
袭蝗,當有這些條件時唤殴,需要進入else流程
-
進入
object_dispose
源碼實現(xiàn),主要是銷毀實例對象
-
進入
objc_destructInstance
源碼實現(xiàn)到腥,在這里有移除關(guān)聯(lián)屬性的方法
-
進入
_object_remove_assocations
源碼朵逝,關(guān)聯(lián)屬性的移除,主要是從全局哈希map中找到相關(guān)對象的迭代器乡范,然后將迭代器中關(guān)聯(lián)屬性配名,從頭到尾的移除
【面試-2】方法的調(diào)用順序
類的方法 和 分類方法 重名啤咽,如果調(diào)用,是什么情況渠脉?
-
如果同名方法是
普通方法
宇整,包括initialize
-- 先調(diào)用分類方法因為
分類的方法是在類realize之后 attach進去的
,插在類的方法的前面芋膘,所以優(yōu)先調(diào)用分類的方法
(注意:不是分類覆蓋主類A矍唷!)initialize
方法什么時候調(diào)用为朋?initialize
方法也是主動調(diào)用臂拓,即第一次消息時
調(diào)用,為了不影響整個load习寸,可以將需要提前加載的數(shù)據(jù)
寫到initialize
中
-
如果同名方法是
load
方法 -- 先主類load
胶惰,后分類load
(分類之間,看編譯的順序)- 原因:參考iOS-底層原理 18:類的加載(下)文章中的
load_images
原理分析
- 原因:參考iOS-底層原理 18:類的加載(下)文章中的
【面試-3】Runtime是什么霞溪?
runtime
是由C和C++
匯編實現(xiàn)的一套API
孵滞,為OC語言加入了面向?qū)ο蟆⒁约斑\行時的功能
-
運行時是指將
數(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 類擴展
可以說成是
特殊的分類
蕉堰,也可稱作匿名分類
可以
給類添加成員屬性
凌净,但是是私有變量
可以
給類添加方法
,也是私有方法
【面試-4】方法的本質(zhì)屋讶,sel是什么冰寻?IMP是什么?兩者之間的關(guān)系又是什么皿渗?
-
方法的本質(zhì):
發(fā)送消息
斩芭,消息會有以下幾個流程快速查找(
objc_msgSend
) - cache_t緩存消息中查找慢速查找 - 遞歸自己|父類 -
lookUpImpOrForward
查找不到消息:動態(tài)方法解析 -
resolveInstanceMethod
消息快速轉(zhuǎn)發(fā) -
forwardingTargetForSelector
消息慢速轉(zhuǎn)發(fā) -
methodSignatureForSelector & forwardInvocation
sel
是方法編號
- 在read_images
期間就編譯進了內(nèi)存imp
是函數(shù)實現(xiàn)指針
轻腺,找imp就是找函數(shù)的過程
sel
相當于 一本書的目錄title
imp
相當于 書本的頁碼
-
查找
具體的函數(shù)
就是想看這本書具體篇章的內(nèi)容1、首先知道想看什么划乖,即目錄 title - sel
2贬养、根據(jù)目錄找到對應(yīng)的頁碼 - imp
3、通過頁碼去翻到具體的內(nèi)容
【面試-5】能否向編譯后得到的類中增加實例變量琴庵?能否向運行時創(chuàng)建的類中添加實例變量
1误算、
不能
向編譯后的得到的類中增加實例變量2、
只要類沒有注冊到內(nèi)存還是可以添加的
3迷殿、可以
添加屬性+方法
【原因】:編譯好的實例變量存儲的位置是ro儿礼,一旦編譯完成,內(nèi)存結(jié)構(gòu)就完全確定了
【經(jīng)典面試-6】 [self class]和[super class]的區(qū)別以及原理分析
[self class]
就是發(fā)送消息objc_msgSend
贪庙,消息接收者是self
蜘犁,方法編號class
[super class]
本質(zhì)就是objc_msgSendSuper
,消息的接收者還是self
止邮,方法編號class
这橙,在運行時,底層調(diào)用的是_objc_msgSendSuper2
【重點5寂G!】只是
objc_msgSendSuper2
會更快撩匕,直接跳過self的查找
代碼調(diào)試
-
LGTeacher
中的init
方法中打印這兩種class調(diào)用
運行程序鹰晨,打印結(jié)果如下
進入
[self class]
中的class
源碼
- (Class)class {
return object_getClass(self);
}
??
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
其底層是獲取對象的isa
,當前的對象是LGTeacher
止毕,其isa是同名的LGTeacher
模蜡,所以[self class]
打印的是LGTeacher
- [super class]中,其中
super
是語法的關(guān)鍵字
扁凛,可以通過clang
看super
的本質(zhì)忍疾,這是編譯時
的底層源碼,其中第一個參數(shù)是消息接收者谨朝,是一個__rw_objc_super
結(jié)構(gòu)
- 底層源碼中搜索
__rw_objc_super
卤妒,是一個中間結(jié)構(gòu)體
- objc中搜索
objc_msgSendSuper
,查看其隱藏參數(shù)
- 搜索
struct objc_super
通過clang
的底層編譯代碼可知字币,當前消息的接收者
等于self
则披,而self
等于LGTeacher
,所以[super class]
進入class
方法源碼后洗出,其中的self是init后的實例對象
士复,實例對象的isa
指向的是本類,即消息接收者是LGTeacher本類
- 底層源碼中搜索
- 我們再來看[super class]在運行時是否如上一步的底層編碼所示翩活,是
objc_msgSendSuper
判没,打開匯編調(diào)試蜓萄,調(diào)試結(jié)果如下
- 搜索
objc_msgSendSuper2
,從注釋得知澄峰,是從 類開始查找
嫉沽,而不是父類
- 查看
objc_msgSendSuper2
的匯編源碼,是從superclass
中的cache
中查找方法
- 搜索
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
ldp p0, p16, [x0] // p0 = real receiver, p16 = class 取出receiver 和 class
ldr p16, [x16, #SUPERCLASS] // p16 = class->superclass
CacheLookup NORMAL, _objc_msgSendSuper2//cache中查找--快速查找
END_ENTRY _objc_msgSendSuper2
完整回答
所以俏竞,最完整的回答如下
[self class]
方法調(diào)用的本質(zhì)是發(fā)送消息
绸硕,調(diào)用class的消息流程,拿到元類的類型
魂毁,在這里是因為類已經(jīng)加載到內(nèi)存玻佩,所以在讀取時是一個字符串類型,這個字符串類型是在map_images
的readClass
時已經(jīng)加入表中席楚,所以打印為LGTeacher
[super class]
打印的是LGTeacher
咬崔,原因是當前的super是一個關(guān)鍵字,在這里只調(diào)用objc_msgSendSuper2
烦秩,其實他的消息接收者和[self class]
是一模一樣的垮斯,所以返回的是LGTeacher
【面試-7】內(nèi)存平移問題
Class cls = [LGPerson class];
void *kc = &cls; //
[(__bridge id)kc saySomething];
LGPerson中有一個屬性 kc_name
和一個實例方法saySomething
,通過上面代碼這種方式只祠,能否調(diào)用實例方法兜蠕?為什么?
代碼調(diào)試
- 我們在日常開發(fā)中的調(diào)用方式是下面這種
LGPerson *person = [LGPerson alloc];
[person saySomething];
-
通過運行發(fā)現(xiàn)抛寝,是可以執(zhí)行的熊杨,打印結(jié)果如下
-
[person saySomething]
的本質(zhì)是對象發(fā)送消息
,那么當前的person是什么盗舰?-
person
的isa
指向類LGPerson
即person的首地址 指向 LGPerson的首地址
晶府,我們可以通過LGPerson的內(nèi)存平移找到cache
,在cache中查找方法
-
-
[(__bridge id)kc saySomething]
中的kc
是來自于LGPerson
這個類钻趋,然后有一個指針kc
郊霎,將其指向LGPerson的首地址
所以,person
是指向LGPerson
類的結(jié)構(gòu)爷绘,kc
也是指向LGPerson
類的結(jié)構(gòu),然后都是在LGPerson
中的methodList
中查找方法
修改:saySomething里面有屬性 self.kc_name 的打印
代碼如下所示
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.kc_name);
}
//下面這兩種方式調(diào)用
//方式一
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
//方式二:常規(guī)調(diào)用
LGPerson *person = [LGPerson alloc];
[person saySomething];
- 查看這兩種調(diào)用方式的打印結(jié)果进倍,如下所示
kc
方式的調(diào)用打印的kc_name
是<ViewController: 0x7fe29170b560>
-
person
方式的調(diào)用打印的kc_name
是(null)
為什么會出現(xiàn)打印不一致的情況土至?
-
其中person方式的
kc_name
是由于self指向person的內(nèi)存結(jié)構(gòu)
,然后通過內(nèi)存平移8字節(jié)猾昆,取出去kc_name
陶因,即self指針首地址平移8字節(jié)獲得
-
【方式一】其中
kc
指針中沒有任何,所以kc表示8字節(jié)指針
垂蜗,self.kc_name
的獲取楷扬,相當于kc首地址的指針也需要平移8字節(jié)找kc_name
解幽,那么此時的kc的指針地址是多少?平移8字節(jié)獲取的是什么烘苹?-
kc
是一個指針躲株,是存在棧
中的,棧是一個先進后出
的結(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)部成員的壓棧情況p &person3
p *(NSNumber **)0x00007ffee83a8090
-
p *(NSNumber **)0x00007ffee83a8098
所以圖中可以得出 20先加入,再加入10丰滑,因此結(jié)構(gòu)體內(nèi)部
的壓棧情況是低地址->高地址
顾犹,遞增
的,棧中結(jié)構(gòu)體內(nèi)部
的成員是反向
壓入棧褒墨,即低地址->高地址
炫刷,是遞增的,
-
-
所以到目前為止郁妈,棧中
從高地址到低地址
的順序的:self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person
self
和_cmd
是viewDidLoad
方法的兩個隱藏參數(shù)浑玛,是高地址->低地址正向壓棧
的class_getSuperClass
和self
為objc_msgSendSuper2
中的結(jié)構(gòu)體成員,是從最后一個成員變量噩咪,即低地址->高地址反向壓棧
的
可以通過下面這段代碼打印下棧的存儲是否如上面所說
void *sp = (void *)&self;
void *end = (void *)&person;
long count = (sp - end) / 0x8;
for (long i = 0; i<count; i++) {
void *address = sp - 0x8 * i;
if ( i == 1) {
NSLog(@"%p : %s",address, *(char **)address);
}else{
NSLog(@"%p : %@",address, *(void **)address);
}
}
運行結(jié)果如下
其中為什么
class_getSuperclass
是ViewController
顾彰,因為objc_msgSendSuper2
返回的是當前類
,兩個self
胃碾,并不是同一個self涨享,而是棧的指針不同,但是指向同一片內(nèi)存空間
-
[(__bridge id)kc saySomething]
調(diào)用時仆百,此時的kc是LGPerson: 0x7ffeec381098
厕隧,所以saySomething
方法中傳入的self
還是LGPerson,但并不是我們通常認為的LGPerson,使我們當前傳入的消息接收者
吁讨,即LGPerson: 0x7ffeec381098
髓迎,是LGPerson的實例對象,此時的操作與普通的LGPerson是一致的建丧,即LGPerson的地址內(nèi)存平移8字節(jié)
普通person流程:
person -> kc_name - 內(nèi)存平移8字節(jié)
-
kc流程:
0x7ffeec381098 + 0x80 -> 0x7ffeec3810a0
,即為self
排龄,指向<ViewController: 0x7fac45514f50>
,如下圖所示
其中 person
與 LGPerson
的關(guān)系是 person是以LGPerson為模板的實例化對象茶鹃,即alloc有一個指針地址涣雕,指向isa,isa指向LGPerson
闭翩,它們之間關(guān)聯(lián)是有一個isa指向
挣郭,
而kc也是指向LGPerson的關(guān)系,編譯器會認為 kc也是LGPerson的一個實例化對象
疗韵,即kc相當于isa兑障,即首地址,指向LGPerson
蕉汪,具有和person一樣的效果流译,簡單來說,我們已經(jīng)完全將編譯器騙過了者疤,即kc
也有kc_name
福澡。由于person查找kc_name是通過內(nèi)存平移8字節(jié)
,所以kc也是通過內(nèi)存平移8字節(jié)去查找kc_name
哪些東西在棧里 哪些在堆里
alloc
的對象 都在堆
中指針驹马、對象
在棧
中革砸,例如person指向的空間
在堆
中,person所在的空間在棧中臨時變量
在棧
中屬性值
在堆
糯累,屬性隨對象是在棧
中
注意:
堆
是從小到大算利,即低地址->高地址棧是從大到小,即從高地址->低地址分配
函數(shù)隱藏參數(shù)會
從前往后
一直壓泳姐,即從高地址->低地址 開始入棧
效拭,結(jié)構(gòu)體內(nèi)部的成員是
從低地址->高地址
一般情況下,內(nèi)存地址有如下規(guī)則
0x60
開頭表示在堆
中
0x70
開頭的地址表示在棧
中
0x10
開頭的地址表示在全局區(qū)域
中
【面試-8】 Runtime是如何實現(xiàn)weak的胖秒,為什么可以自動置nil
1缎患、通過
SideTable
找到我們的weak_table
2、
weak_table
根據(jù)referent
找到或者創(chuàng)建weak_entry_t
3阎肝、然后
append_referrer(entry挤渔,referrer)
將我的新弱引用的對象加進去entry4、最后
weak_entry_insert
盗痒,把entry
加入到我們的weak_table
底層源碼調(diào)用流程如下圖所示