iOS面試題答案 --- 底層

派二星發表於2019-04-04

1. KVO的實現原理

當我們對A類新增監聽的時候,系統會自動生成一個NSKVONotifying_A的子類,這個類重寫了A的class、superclass、deealloc方法和該屬性的Set方法,同時A類的物件的isa指標指向了該虛擬子類。當監聽屬性改變的時候系統呼叫NSSetobjectValueandNotify,這個方法的執行流程是(willchangeValueforkey->改變父類的值->didchangeValueforkey->observeValueForKey:ofObject:change:context:),如果設定automaticallyNotifiesObserversForKey:(NSString *)key為NO的時候則需要手動觸發KVO即手動呼叫willchangeValueforkey和didchangeValueforkey.

func _NSSetObjectValueAndNotify {
    ...
    willchangeValueforkey
    ...
    "
    objc_msgSendSupper '改變父類的值(猜測這樣實現)
    "
    ...
    didchangeValueforkey
    ...
    observeValueForKey:ofObject:change:context: 
}

複製程式碼

Q1: 為什麼重寫系統的class/superclass、deealloc方法

因為該子類為系統自動生成蘋果想偽裝成並沒有這個類 所以重寫class/superclass ,但是呼叫 objc_getClass()這個方法時候依然會暴露,因為這個方法是呼叫呼叫物件的isa指標指向。dealloc則是系統還有一些其他的事情處理

Q2:為什麼KVO要實現一個子類

2. KVC是什麼,他是如何實現的

KVCKVC是由NSKeyValueCoding非正式協議實現的一種機制,物件採用該協議來提供對其屬性的間接訪問。當一個物件符合鍵值編碼時,它的屬性可以通過一個簡潔、統一的訊息傳遞介面通過字串引數來定址。這種間接訪問機制補充了例項變數及其關聯的訪問方法所提供的直接訪問。

valueForkey的查詢流程

  1. 在例項中搜尋找到的類裡面的相關方法,其名稱依次為get<Key>, <key>, is<Key>, or _<key>。如果找到,則呼叫它,並繼續執行步驟5的結果。否則繼續下一步。
  2. 如果沒有找到那麼搜尋countOf<Key> and objectIn<Key>AtIndex:,如果找到了其中的第一個和另外兩個中的至少一個,則建立一個collection代理物件,該物件響應所有NSarray方法並返回該物件, 如果沒有找到陣列相關方法那麼搜尋countOf<Key>, enumeratorOf<Key>, and memberOf<Key>:,如果實現了其中一個則該物件響應所有Set方法的,
  3. 如果上述方法都沒有找到那麼判斷accessInstanceVariablesDirectly 屬性是否為yes,如果為yes則查詢**_<key>, _is<Key>, <key>, or is<Key>**屬性,如果找到直接返回變數值
  4. 如果都沒找到那麼且accessInstanceVariablesDirectly為false,那麼執行valueForUndefinedKey:方法,呼叫valueForUndefinedKey:,預設情況下,這會引發異常,但NSObject的子類可能提供關鍵的特定行為

setValueForkey的實現

set value:for key:的預設實現:給定key和value引數作為輸入,嘗試在接收呼叫的物件內,使用以下過程將名為key的屬性設定為value(對於非物件屬性,則設定未包裝的value版本,如表示非物件值:

  1. 按此順序查詢名為set<key> 和 **_set<key>**的第方法。如果找到了,使用value值呼叫它並完成。
  2. 如果找不到簡單訪問器,並且類方法accessInstanceVariablesDirectly返回Yes,則按該順序查詢名為 _<key>、_is<key>、<key>或is<key>的例項變數。如果找到,直接用value設定。
  3. 在找不到方法或例項變數時,呼叫setValue:ForUndefinedKey:。預設情況下,這會引發異常,但NSObject的子類可能提供關鍵的特定行為。

3. 請簡單介紹一下Runtime,以及它的原理和應用(訊息傳送機制,動態解析。應用防止崩潰)

OC的程式碼分為編譯時和執行時,Runtime是執行時處理oc的各個事件,比如動態新增類新增屬性,以及他的底層是一套C和C++還有彙編的api. OC的訊息呼叫底層呼叫時objc_msgSend,這個訊息流程是有兩種方式一種是快速傳送一種是慢速傳送,類的本質是結構體,他的構成有 isa和class_rw_t和class_data_bits_t的結構體、superclass,以及cache類,類結構裡的cache_t快取儲存方法的Selector和IMP,它們組成一張雜湊表,通過雜湊表查詢非常快,當查詢的時候如果cache裡面存在則會直接返回,不存在的話則會進入慢速傳送,找到了會在快取裡面存一份。
快速查詢:objc_msgSend是由彙編進行的他的過程是 先是優化isa指標,優化完畢後執行CacheLookup Normal,在CacheLookup裡進行CacheHit如果快取裡有則會返回imp,沒有的話則會繼續走 checkmiss(checkmiss傳送objc_msgSend_uncached)訊息,處理完後執行add,,將查詢到的訊息放到快取裡面,
慢速查詢objc_msgSend_uncached是一個結構體裡面呼叫了 MethodTableLookup 方法列表查詢,然後通過br x 17 返回當前imp,MethodTableLookup內部方法是執行了 這個方法裡面有一個**__class_lookupMethodAndLoadCache3**,這個方法是_class_lookupMethodAndLoadCache3的彙編方法,當呼叫這個方法的時候的到了c++的lookUpImpForward,這個方法顯示宣告瞭一系列的初始化操作,通過遞迴先查詢自身類如果自身類存在則返回,如果不存在則查詢父類,直道為nil為止,中間如果查詢到則返回並快取一份通過log_and_fill_cache,如果都沒有則進入動態方法解析流程
動態訊息解析: 當呼叫者自身和父類沒有實現方法時候會地用 _class_resolveMethod 方法這個方法裡面如果是類方法則執行resolveClassmethod(這個方法其實是元類的resolveinstancemethod),然後一直調到根源類的父類NSObject。如果是例項方法則直接呼叫resolveinstancemethod,如果有處理則處理,如果未處理則呼叫__objc_msgForward_impcache,因為蘋果的這裡面實現是閉源的,可以通過instrumentObjcMessageSends這個函式來進行查詢,找到對應路徑會發現呼叫順訊就是 forwardTragemethodSignutre 最後未處理執行doesnotGesture。如果沒有處理的話彙編後面還會呼叫 __objc_forward_handler來進行處理這個方法是列印出了出錯的堆疊資訊。

4. 什麼是block?(堆block,棧block,全域性block)

block匿名函式,分為堆block,棧block和全域性block,如果block未引用任何變數那麼他是一個全域性block,如果未出作用域則是棧block,若引用了外界屬性,那麼他是一個堆block,block預設是儲存在堆裡的,ARC環境下回 自動執行copy操作。block的本質是 __main_block_impl_0 結構體裡面的結構是isa指標指向堆記憶體或者棧記憶體或者全域性記憶體和 flag引數,isa指標指向自己的類(global malloc stack),有desc結構體描述block的資訊,

Q1:Block是如何從棧區拷貝到堆區的呢。

Block不允許修改外部變數的值。這裡所說的外部變數的值,指的是棧中指標的記憶體地址。__block所起到的作用就是隻要觀察到該變數被block所持有,就將“外部變數”在棧中的記憶體地址放到了堆中。進而在block內部也可以修改外部變數的值。 block對一個值進行引用時他會拷貝到一個新的記憶體地址,因此他修改的值不是原來的外部變數地址,而是修改了外部變數copy到的新的記憶體地址,不影響舊的。block之後再訪問其實訪問的其實訪問的是拷貝完之後的記憶體地址。
那麼 __block做的什麼操作,__Block_byref_a_0 這個指標獲取到了外部引數(棧區)的地址,然後下面的block傳入的是__Block_byref_a_0型別的指標地址,他不再是原來的棧區的地址,因此可以在閉包裡訪問到外部變數,那麼出block作用域後 __Block修飾的變數記憶體地址已經為新的了所以在此訪問就是訪問的新記憶體空間

iOS面試題答案 --- 底層

5. weak的實現原理

Runtime維護了一個weak表,weak是一個雜湊表,key是物件地址,value是weak指標的陣列 當對一個屬性進行weak修飾時 會先呼叫objc_initweak,初始化一個新的weak指標指向地址,新增引用時呼叫objcstroeweak函式,這個函式是更新指標指向,釋放時呼叫cleardeallocing函式,該函式會搜尋以該物件記憶體地址為key,對應的所有value(weak指標陣列)並將其置為nil,後從entry從weak表中刪除。清理物件記錄。

6. 通知和代理,block的區別。

通知一對多,代理一對一,通知基於觀察者實現。

通知只能通知不能接受回撥,代理可以反向通知

delegate執行成本低,block的執行成本高。**block出棧需要將使用的資料從棧記憶體拷貝到堆記憶體,當然物件的話就是加計數,使用完或者block置nil後才消除。delegate只是儲存了一個物件指標,直接回撥,沒有額外消耗。就像C的函式指標,只多做了一個查表動作。

delegate更安全些,比如: 避免迴圈引用。使用 block 時稍微不注意就形成迴圈引用,導致物件釋放不了。這種迴圈引用,一旦出現就比較難檢查出來。而 delegate 的方法是分離開的,並不會引用上下文,因此會更安全些。

7. 說說你讀過的第三方庫工作流程。

SwiftyJSON

這個類是一個JSON結構體初始化的時候可以傳入object、string、dictionary等其他操作單最後的資料結構都是有一個 ojbect 來進行初始化,並且定義有各個型別dictiorary string array bool int double,同時還支援路徑訪問,通過merge操作合併json,如果物件是陣列則直接進行append操作,如果是字典則對字典的key進行賦值,原有則覆蓋,如果value是陣列則進行遞迴呼叫

KingFisher

圖片載入框架

Alamofire

8. OC類的本質是什麼?他的結構是什麼,他是如何初始化的

oc類的本質是結構體,他的結構是裡面有 isa指標,supeprcalss,cache類,data,class_rw_t,class_ro_t, data裡面是這個類所有的資料,包括方法列表屬性列表介面列表,由class_rw_t,和class_ro_t進行維護。

他初始化通過oc原始碼NSObject.mm 檔案可以看到 當進行初始化操作的時候先執行 _objc_rootAlloc ==》callAlloc ==》 class_createInstance ==》 _class_createInstanceFromZone ==》_objc_constructOrFree 獲得該物件,完成後還會執行init操作,而init方法呼叫**_objc_rootInit**,返回了自身

iOS面試題答案 --- 底層
是因為,oc物件的init方法採用了簡單工廠設計模式,他只需要設計一個init功能,由子類進行初始化從而得到不同的物件。

9. Swift類的初始化發生了什麼?

這個需要通過Swift的原始碼進行原始碼分析,這個我暫時還沒找到相關方面。

相關文章