前言
我們類的底層探索
已經(jīng)告一段落唆铐,我們梳理一下常見的面試題走趋,希望對你有些幫助蘸鲸。
問題
- 1.
runtime
是什么?- 2.
runtime
如何實(shí)現(xiàn)weak
,為什么可以自動置為nil
?- 3.
runtime
Associate方法關(guān)聯(lián)的對象囚戚,是否需要在dealloc
中釋放?- 4.關(guān)聯(lián)對象
AssociationsManager
是否唯一酵熙?- 5.
分類
方法會覆蓋本類
方法嗎?- 6.所有
分類
方法都優(yōu)先于本類
嗎驰坊?- 7.方法的本質(zhì)匾二,
SEL
是什么?IMP
是什么拳芙?兩者之間關(guān)系是什么察藐?- 8.
編譯后
的類能否添加實(shí)例變量?能否向運(yùn)行時創(chuàng)建的類添加實(shí)例變量?- 9.
[self class]
和[super class]
區(qū)別和原理分析- 10.
內(nèi)存平移
問題
問題一:runtime
是什么?
runtime
是由C
和C++
和匯編
實(shí)現(xiàn)的?套API舟扎,為OC
語?加?了?向?qū)ο蠓址桑\(yùn)?時
的功能。
運(yùn)?時(Runtime
)是指將數(shù)據(jù)類型
的確定由編譯時
推遲到了運(yùn)?時
.
舉個例?: extension - category
的區(qū)別(extension
是編譯期就確定了睹限,但是懶加載的category
是在運(yùn)行時動態(tài)加入的)譬猫。
平時我們編寫的OC
代碼讯檐,在程序運(yùn)?過程中,其實(shí)最終會轉(zhuǎn)換成Runtime
的C
語?代碼染服,Runtime 是Object-C
的幕后?作者
問題二:runtime
如何實(shí)現(xiàn)weak
别洪,為什么可以自動置為nil
?
- 通過
SideTable
找到我們的weak_table
-
weak_table
根據(jù)referent
找到或者創(chuàng)建weak_entry_t
- 然后
append_referrer(entry, referrer)
將我的新弱引用的對象
加進(jìn)去entry
- 最后
weak_entry_insert
把entry
加入到我們的weak_table
底層源碼調(diào)用流程如下圖所示
問題三:runtime
Associate方法關(guān)聯(lián)的對象,是否需要在dealloc
中釋放?
當(dāng)我們創(chuàng)建的對象釋放時肌索,會調(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
的源碼實(shí)現(xiàn)
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
- 進(jìn)入
_objc_rootDealloc
源碼實(shí)現(xiàn)梢灭,主要是對對象進(jìn)行析構(gòu)
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
-
進(jìn)入
rootDealloc
源碼實(shí)現(xiàn)夷家,發(fā)現(xiàn)其中有關(guān)聯(lián)屬性時設(shè)置bool值
,當(dāng)有這些條件時敏释,需要進(jìn)入else流程 進(jìn)入
object_dispose
源碼實(shí)現(xiàn)库快,主要是銷毀實(shí)例對象
/***********************************************************************
* object_dispose
* fixme
* Locking: none
**********************************************************************/
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
- 進(jìn)入
objc_destructInstance
源碼實(shí)現(xiàn),在這里有移除關(guān)聯(lián)屬性的方法
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
- 進(jìn)入
_object_remove_assocations
源碼钥顽,關(guān)聯(lián)屬性的移除义屏,主要是從全局哈希map中找到相關(guān)對象的迭代器,然后將迭代器中關(guān)聯(lián)屬性蜂大,從頭到尾的移除
// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
void
_object_remove_assocations(id object)
{
ObjectAssociationMap refs{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
//獲取迭代器
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
//從頭到尾逐個移除
if (i != associations.end()) {
refs.swap(i->second);
associations.erase(i);
}
}
// release everything (outside of the lock).
for (auto &i: refs) {
i.second.releaseHeldValue();
}
}
問題四:關(guān)聯(lián)對象AssociationsManager
是否唯一闽铐?
AssociationsManager
結(jié)構(gòu)中,manager
只是對外代言人奶浦,并不是唯一的兄墅,AssociationsHashMap
哈希表才是唯一的。
1. 運(yùn)行驗(yàn)證:
移除鎖
澳叉,這樣可以同時存在2個manager
了隙咸。
- 加入測試代碼,創(chuàng)建2個
manager
耳高,都調(diào)用get()
扎瓶,發(fā)現(xiàn)2個讀取的associations
是相同地址
。- 證明
AssociationsHashMap
在內(nèi)存中是獨(dú)一份的泌枪,而manager
只是外層包裝概荷,可以創(chuàng)建多個。
問題五:分類
方法會覆蓋本類
方法嗎碌燕?
-
分類方法
會調(diào)用attachLists
误证,將分類方法
插入了本類方法
前面继薛,全都存儲
起來。并不是覆蓋
本類方法愈捅,這個在我們之前的文章中 iOS-類的加載(下)有詳細(xì)的解釋遏考。
問題六:所有分類方法都優(yōu)先于本類嗎?
類的方法 和 分類方法 重名蓝谨,如果調(diào)用灌具,是什么情況?
-
如果同名方法是
普通方法
譬巫,包括initialize
-- 先調(diào)用分類方法因?yàn)?code>分類的方法是在類realize之后 attach進(jìn)去的咖楣,插在類的方法的前面,所以
優(yōu)先調(diào)用分類的方法
(注意:不是分類覆蓋主類B簟S栈摺)initialize
方法什么時候調(diào)用?initialize
方法也是主動調(diào)用咕缎,即第一次消息時
調(diào)用珠十,為了不影響整個load,可以將需要提前加載的數(shù)據(jù)
寫到initialize
中
-
如果同名方法是
load
方法 -- 先主類load
凭豪,后分類load
(分類之間焙蹭,看編譯的順序)- 原因:參考iOS-類的加載(下)文章中的
load_images
原理分析
- 原因:參考iOS-類的加載(下)文章中的
問題七:方法的本質(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
期間就編譯進(jìn)了內(nèi)存
imp
是函數(shù)實(shí)現(xiàn)指針 ,找imp
就是找函數(shù)的過程
打個比方:加入你要從一本字典中查找某個字说庭,那么sel
相當(dāng)于 字典的目錄title
然磷,imp
相當(dāng)于 字典的頁碼。
問題八:編譯后
的類能否添加實(shí)例變量刊驴?能否向運(yùn)行時創(chuàng)建的類添加實(shí)例變量?
1姿搜、不可以。 因?yàn)榫幾g好的實(shí)例變量存放的位置在類的ro
捆憎,一旦編譯完成舅柜,內(nèi)存結(jié)構(gòu)
就完全確定了,無法修改躲惰。
2致份、運(yùn)行時在register
注冊前,可以添加础拨。但是調(diào)用運(yùn)行時register
注冊后氮块,就完成了內(nèi)存
的注入绍载,內(nèi)存結(jié)構(gòu)
確定了,無法修改滔蝉。
問題九:[self class]
和[super class]
區(qū)別和原理分析
[self class]
就是發(fā)送消息objc_msgSend
击儡,消息接受者是self
,方法編號(SEL)是class
[super class]
本質(zhì)是objc_msgSendSuper
蝠引,消息接受者還是self
阳谍,方法編號是class
。
實(shí)際運(yùn)行時螃概,[super class]在匯編層
執(zhí)行的是objc_msgSendSuper2
矫夯,直接從superclass
父類開始搜索,節(jié)約了一輪查找資源
測試代碼:
@interface ZGPerson : NSObject
@end
@implementation ZGPerson
- (instancetype)init {
if (self = [super init]) {
NSLog(@"%@ %@", [self class], [super class]);
}
return self; }
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZGPerson * person = [[ZGPerson alloc] init];
}
return 0;
}
- 打印結(jié)果:
都是ZGPerson
結(jié)果與我想的不一樣,為什么不是ZGPerson
和NSObject
呢谅年?我們查看源碼分析一下
我們查看 [self class]
中的class
源碼
- (Class)class {
return object_getClass(self);
}
??
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
其底層是獲取對象的isa
茧痒,當(dāng)前的對象是ZGPerson
,其isa
是同名的ZGPerson
融蹂,所以[self class]
打印的是ZGPerson
而[super class]
中旺订,其中super
是語法的 關(guān)鍵字
,可以通過clang
看super
的本質(zhì)超燃,clang
生成cpp
編譯文件(clang -rewrite-objc ZGPerson.m -o ZGPerson.cpp)
区拳,打開main.cpp
文件:
底層源碼中搜索__rw_objc_super
,是一個中間結(jié)構(gòu)體
objc中搜索objc_msgSendSuper
意乓,查看其隱藏參數(shù)
搜索struct objc_super
通過clang
的底層編譯代碼可知樱调,當(dāng)前消息的接收者
等于 self
,而self
等于 LGTeacher
届良,所以 [super class]
進(jìn)入class
方法源碼后笆凌,其中的self是init后的實(shí)例對象
,實(shí)例對象的isa
指向的是本類士葫,即消息接收者是LGTeacher本類
-
我們再來看[super class]在運(yùn)行時是否如上一步的底層編碼所示乞而,是
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
總結(jié):
[self class]
方法調(diào)用的本質(zhì)是發(fā)送消息
,調(diào)用class
的消息流程应狱,拿到元類的類型共郭,在這里是因?yàn)轭愐呀?jīng)加載到內(nèi)存,所以在讀取時是一個字符串類型,這個字符串類型是在map_images
的readClass
時已經(jīng)加入表中落塑,所以打印為ZGPerson
[super class]
打印的是ZGPerson
纽疟,原因是當(dāng)前的super
是一個關(guān)鍵字,在這里只調(diào)用objc_msgSendSuper2
憾赁,其實(shí)他的消息接收者和[self class]
是一模一樣的污朽,所以返回的是ZGPerson
問題十:runtime
是什么?內(nèi)存平移問題
Class cls = [LGPerson class];
void *kc = &cls; //
[(__bridge id)kc saySomething];
LGPerson中有一個屬性 kc_name
和一個實(shí)例方法saySomething
,通過上面代碼這種方式龙考,能否調(diào)用實(shí)例方法蟆肆?為什么?
代碼調(diào)試
- 我們在日常開發(fā)中的調(diào)用方式是下面這種
LGPerson *person = [LGPerson alloc];
[person saySomething];
-
通過運(yùn)行發(fā)現(xiàn)晦款,是可以執(zhí)行的炎功,打印結(jié)果如下
-
[person saySomething]
的本質(zhì)是對象發(fā)送消息
,那么當(dāng)前的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
的獲取岩灭,相當(dāng)于kc首地址的指針也需要平移8字節(jié)找kc_name
缨该,那么此時的kc的指針地址是多少?平移8字節(jié)獲取的是什么川背?-
kc
是一個指針,是存在棧
中的蛤袒,棧是一個先進(jìn)后出
的結(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);
}
}
運(yùn)行結(jié)果如下
其中為什么class_getSuperclass
是ViewController
板驳,因?yàn)?code>objc_msgSendSuper2返回的是當(dāng)前類
,兩個self
碍拆,并不是同一個self若治,而是棧的指針不同,但是指向同一片內(nèi)存空間
-
[(__bridge id)kc saySomething]
調(diào)用時感混,此時的kc是LGPerson: 0x7ffeec381098
端幼,所以saySomething
方法中傳入的self
還是LGPerson,但并不是我們通常認(rèn)為的LGPerson弧满,使我們當(dāng)前傳入的消息接收者
婆跑,即LGPerson: 0x7ffeec381098
,是LGPerson的實(shí)例對象庭呜,此時的操作與普通的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為模板的實(shí)例化對象扶关,即alloc有一個指針地址,指向isa数冬,isa指向LGPerson
节槐,它們之間關(guān)聯(lián)是有一個isa指向
,
而kc也是指向LGPerson的關(guān)系,編譯器會認(rèn)為 kc也是LGPerson的一個實(shí)例化對象
铜异,即kc相當(dāng)于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ū)域`中
以上就是全部的內(nèi)容了抄伍,如有錯誤艘刚,還望指正。