iOS 底層原理 文章彙總
【面試-1】Runtime Asssociate方法關聯的對象,需要在dealloc中釋放?
當我們對象釋放時,會調用
dealloc
- 1、C++函數釋放 :
objc_cxxDestruct
- 2、移除關聯屬性:
_object_remove_assocations
- 3、将弱引用自動設定nil:
weak_clear_no_lock(&table.weak_table, (id)this);
- 4、引用計數處理:
table.refcnts.erase(this)
- 5、銷毀對象:
free(obj)
是以,
關聯對象
不需要我們手動移除,會在對象析構即
dealloc
時釋放
dealloc 源碼
dealloc的源碼查找路徑為:
dealloc
->
_objc_rootDealloc
->
rootDealloc
->
object_dispose
(釋放對象)->
objc_destructInstance
->
_object_remove_assocations
- 在objc源碼中搜尋
的源碼實作dealloc
- 進入
源碼實作,主要是對_objc_rootDealloc
對象進行析構
- 進入
源碼實作,發現其中有rootDealloc
,當有這些條件時,需要進入else流程關聯屬性時設定bool值
- 進入
源碼實作,主要是object_dispose
銷毀執行個體對象
- 進入
源碼實作,在這裡有移除關聯屬性的方法objc_destructInstance
- 進入
源碼,關聯屬性的移除,主要是_object_remove_assocations
從全局哈希map中找到相關對象的疊代器,然後将疊代器中關聯屬性,從頭到尾的移除
【面試-2】方法的調用順序
類的方法 和 分類方法 重名,如果調用,是什麼情況?
- 如果同名方法是
,包括普通方法
– 先調用分類方法initialize
- 因為
,插在類的方法的前面,是以分類的方法是在類realize之後 attach進去的
(注意:不是分類覆寫主類!!)優先調用分類的方法
-
方法什麼時候調用?initialize
方法也是主動調用,即initialize
調用,為了不影響整個load,可以将需要第一次消息時
寫到提前加載的資料
中initialize
- 因為
- 如果同名方法是
方法 – 先load
,後主類load
(分類之間,看編譯的順序)分類load
- 原因:參考iOS-底層原理 18:類的加載(下)文章中的
原理分析load_images
- 原因:參考iOS-底層原理 18:類的加載(下)文章中的
【面試-3】Runtime是什麼?
-
是由runtime
彙編實作的一套C和C++
,為OC語言加入了API
面向對象、以及運作時的功能
- 運作時是指将
資料類型的确定由編譯時 推遲到了 運作時
- 舉例:extension 和 category 的差別
- 平時編寫的OC代碼,在程式運作的過程中,其實最終會轉換成runtime的C語言代碼,
runtime是OC的幕後工作者
1、category 類别、分類
-
專門用來給類添加新的方法
-
,添加了成員屬性,也無法取到不能給類添加成員屬性
- 注意:其實
,即屬性關聯,重寫setter、getter方法可以通過runtime 給分類添加屬性
- 分類中用
定義變量,@property
變量的隻會生成
方法的setter、getter
,聲明
不能生成方法實作 和 帶下劃線的成員變量
2、extension 類擴充
- 可以說成是
,也可稱作特殊的分類
匿名分類
- 可以
,但給類添加成員屬性
是是私有變量
- 可以
,也給類添加方法
是私有方法
【面試-4】方法的本質,sel是什麼?IMP是什麼?兩者之間的關系又是什麼?
- 方法的本質:
,消息會有以下幾個流程發送消息
- 快速查找(
) - cache_t緩存消息中查找objc_msgSend
- 慢速查找 - 遞歸自己|父類 -
lookUpImpOrForward
- 查找不到消息:動态方法解析 -
resolveInstanceMethod
- 消息快速轉發 -
forwardingTargetForSelector
- 消息慢速轉發 -
methodSignatureForSelector & forwardInvocation
- 快速查找(
-
是sel
- 在方法編号
期間就編譯進了記憶體read_images
-
是imp
,函數實作指針
找imp就是找函數的過程
-
相當于 一本書的sel
目錄title
-
相當于 書本的sel
頁碼
- 查找
就是想看這本書具體篇章的内容具體的函數
- 1、首先知道想看什麼,即目錄 title - sel
- 2、根據目錄找到對應的頁碼 - imp
- 3、通過頁碼去翻到具體的内容
【面試-5】能否向編譯後得到的類中增加執行個體變量?能否向運作時建立的類中添加執行個體變量
- 1、
向編譯後的得到的類中增加執行個體變量不能
- 2、
隻要類沒有注冊到記憶體還是可以添加的
- 3、可以
添加屬性+方法
【原因】:編譯好的執行個體變量存儲的位置是ro,一旦編譯完成,記憶體結構就完全确定了
【經典面試-6】 [self class]和[super class]的差別以及原理分析
-
就是發送消息[self class]
,消息接收者是objc_msgSend
,方法編号self
class
-
本質就是[super class]
,消息的接收者還是objc_msgSendSuper
,方法編号self
,在運作時,底層調用的是class
【重點!!!】_objc_msgSendSuper2
- 隻是
會更快,直接跳過self的查找objc_msgSendSuper2
代碼調試
-
中的LGTeacher
方法中列印這兩種class調用 運作程式,列印結果如下init
- 進入
中的[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
,可以通過關鍵字
看clang
的本質,這是super
的底層源碼,其中第一個參數是消息接收者,是一個編譯時
結構__rw_objc_super
- 底層源碼中搜尋
,是一個中間結構體__rw_objc_super
- objc中搜尋
,檢視其隐藏參數objc_msgSendSuper
- 搜尋
通過struct objc_super
的底層編譯代碼可知,目前clang
等于消息的接收者
,而self
等于self
,是以LGTeacher
進入[super class]
方法源碼後,其中的class
即為self
,是以最後還是LGTeacher
,即擷取LGTeacher的isa
同名LGTeacher元類
- 底層源碼中搜尋
- 我們再來看[super class]在運作時是否如上一步的底層編碼所示,是
,打開彙編調試,調試結果如下objc_msgSendSuper
- 搜尋
,從注釋得知,是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]
,調用class的消息流程,拿到發送消息
,在這裡是因為類已經加載到記憶體,是以在讀取時是一個字元串類型,這個字元串類型是在元類的類型
的map_images
時已經加入表中,是以列印為readClass
LGTeacher
-
列印的是[class class]
,原因是目前的super是一個關鍵字,在這裡隻調用LGTeacher
,其實他的消息接收者和objc_msgSendSuper2
是一模一樣的,是以傳回的是LGTeacher[self class]
【面試-7】記憶體平移問題
Class cls = [LGPerson class];
void *kc = &cls; //
[(__bridge id)kc saySomething];
LGPerson中有一個屬性
kc_name
和一個執行個體方法
saySomething
,通過上面代碼這種方式,能否調用執行個體方法?為什麼?
代碼調試
- 我們在日常開發中的調用方式是下面這種
LGPerson *person = [LGPerson alloc];
[person saySomething];
- 通過運作發現,是可以執行的,列印結果如下
-
的本質是[person saySomething]
,那麼目前的person是什麼?對象發送消息
-
的person
指向類isa
即LGPerson
,我們可以通過LGPerson的記憶體平移找到person的首位址 指向 LGPerson的首位址
,在cache中查找方法cache
-
-
中的[(__bridge id)kc saySomething]
是來自于kc
這個類,然後有一個指針LGPerson
,将其kc
指向LGPerson的首位址
是以,
person
是指向
LGPerson
類的結構,
kc
也是指向
LGPerson
類的結構,然後都是在
LGPerson
中的
methodList
中查找方法
修改:saySomething裡面有屬性 self.kc_name 的列印
代碼如下所示
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.kc_name);
}
//下面這兩種方式調用
//方式一
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
//方式二:正常調用
LGPerson *person = [LGPerson alloc];
[person saySomething];
- 檢視這兩種調用方式的列印結果,如下所示
-
方式的調用列印的kc
是kc_name
<ViewController: 0x7fe29170b560>
-
方式的調用列印的person
是kc_name
(null)
-
為什麼會出現列印不一緻的情況?
- 其中person方式的
是由于kc_name
,然後通過self指向person的記憶體結構
,即記憶體平移8位元組,取出去kc_name
self指針首位址平移8位元組獲得
- 【方式一】其中
指針中沒有任何,是以kc
,kc表示8位元組指針
的擷取,相當于self.kc_name
,那麼此時的kc的指針位址是多少?平移8位元組擷取的是什麼?kc首位址的指針也需要平移8位元組找kc_name
-
是一個指針,是存在kc
中的,棧是一個棧
的結構,參數傳入就是一個不斷壓棧的過程,先進後出
- 其中
,且每個函數都會有兩個隐藏參數隐藏參數會壓入棧
,可以通過(id self,sel _cmd)
檢視底層編譯clang
-
的過程,其位址是隐藏參數壓棧
的,而遞減
的,即棧是從高位址->低位址 配置設定
在棧中,參數會從前往後一直壓
- super通過clang檢視底層的編譯,是
,其第一個參數是一個結構體objc_msgSendSuper
,那麼結構體中的屬性是如何壓棧的?可以通過自定義一個結構體,判斷結構體内部成員的壓棧情況__rw_objc_super(self,class_getSuperclass)
-
p &person3
-
p *(NSNumber **)0x00007ffee83a8090
-
是以圖中可以得出 20先加入,再加入10,是以p *(NSNumber **)0x00007ffee83a8098
的壓棧情況是結構體内部
,低位址->高位址
的,棧中遞增
的成員是結構體内部
壓入棧,即反向
,是遞增的,低位址->高位址
-
- 其中
-
- 是以到目前為止,棧中
的順序的:從高位址到低位址
self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person
-
和self
是_cmd
方法的兩個隐藏參數,是高位址->低位址viewDidLoad
的正向壓棧
-
和class_getSuperClass
為self
中的結構體成員,是從最後一個成員變量,即低位址->高位址objc_msgSendSuper2
的反向壓棧
-
可以通過下面這段代碼列印下棧的存儲是否如上面所說
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);
}
}
運作結果如下
其中為什麼
class_getSuperclass
是
ViewController
,因為
objc_msgSendSuper2
傳回的是
目前類
,兩個
self
,并不是同一個self,而是棧的指針不同,但是指向同一片記憶體空間
-
調用時,此時的kc是[(__bridge id)kc saySomething]
,是以LGPerson: 0x7ffeec381098
方法中傳入的saySomething
還是LGPerson,但并不是我們通常認為的LGPerson,使我們目前self
,即傳入的消息接收者
,是LGPerson的執行個體對象,此時的操作與普通的LGPerson是一緻的,即LGPerson: 0x7ffeec381098
LGPerson的位址記憶體平移8位元組
- 普通person流程:
person -> kc_name - 記憶體平移8位元組
- kc流程:
,即為0x7ffeec381098 + 0x80 -> 0x7ffeec3810a0
,指向self
,如下圖所示<ViewController: 0x7fac45514f50>
- 普通person流程:
其中
person
與
LGPerson
的關系是
person是以LGPerson為模闆的執行個體化對象,即alloc有一個指針位址,指向isa,isa指向LGPerson
,它們之間關聯是有一個
isa指向
,
而kc也是指向LGPerson的關系,編譯器會認為
kc也是LGPerson的一個執行個體化對象
,即
kc相當于isa,即首位址,指向LGPerson
,具有和person一樣的效果,簡單來說,我們已經完全将編譯器騙過了,即
kc
也有
kc_name
。由于
person查找kc_name是通過記憶體平移8位元組
,是以kc也是通過記憶體平移8位元組去查找kc_name
哪些東西在棧裡 哪些在堆裡
-
的對象 都在alloc
中堆
-
在指針、對象
中,例如棧
在person指向的空間
中,person所在的空間在棧中堆
-
在臨時變量
中棧
-
在屬性值
,屬性随對象是在堆
中棧
注意:
是從小到大,即低位址->高位址
堆
- 棧是從大到小,即從高位址->低位址配置設定
- 函數隐藏參數會
一直壓,即
從前往後
,
從高位址->低位址 開始入棧
- 結構體内部的成員是
從低位址->高位址
- 一般情況下,記憶體位址有如下規則
開頭表示在
0x60
中
堆
開頭的位址表示在
0x70
中
棧
開頭的位址表示在
0x10
中
全局區域
### 【面試-8】 Runtime是如何實作weak的,為什麼可以自動置nil
- 1、通過
找到我們的SideTable
weak_table
- 2、
根據weak_table
找到或者建立referent
weak_entry_t
- 3、然後
将我的新弱引用的對象加進去entryappend_referrer(entry,referrer)
- 4、最後
,把weak_entry_insert
加入到我們的entry
weak_table
底層源碼調用流程如下圖所示