題目來自部落格:面試百度的記錄,有些問題我能回答一下,不能回答的或有更好的回答我放個相關連結供參考。
一面
- Objective C runtime library:Objective C的物件模型,block的底層實現結構,訊息傳送,訊息轉發,這些都需要背後C一層的描述,記憶體管理。
- Core Data:中多執行緒中處理大量資料同步時的操作。
- Multithreading:什麼時候處理多執行緒,幾種方式,優缺點。
- Delegate, Notification,KVO, other 優缺點
runtime有一點追問,category,method 的實現機制,class的載入過程。
1面整體感覺不錯,40分鐘不到,感覺回答的還可以。被通知一會兒二面。
唐巧前輩說這些都是 iOS 的基礎問題,應該對此深入的瞭解。當初看到時,大部分回答不上來,因為平時沒有好好思考整理過。這裡大部分的概念大多會在學習 OC 的過程中遇到過,但還是得經過寫程式碼才能有更深的理解。反正我當初看那些設計模式是雲裡霧裡,每個字都認識,就是不知道說的什麼。即使現在,有些東西,我也不是很理解。
Objective-C 底層
Objective-C runtime library:Objective-C 的物件模型,Block 的底層實現結構,訊息傳送,訊息轉發,category,method 實現,class load。
runtime 我在平時很少涉及到,沒有系統學習過,而且很多次看了不久就忘了,所以這裡給出一些不錯的文章的連結供參考。這幾個問題在《iOS 7 Programming Pushing the Limits》都有過深入的解讀(我有電子版,是盜版,這裡給出這本書在 Github 的地址,工作後我會把去年看過的盜版書全部補償買回,沒有 iOS 8 的版本,不知道是不是由於盜版太多導致的)。另外,唐巧前輩撰文討論過前兩者:
- 《Objective-C 物件模型及應用》
唐巧在後記中也提到了 iOS 64-bit 帶來的變化:那麼就來看看 Session 404 Advanced in Objective-C ,從36分起講相關的東西,喔,看不懂,那還是看看這個吧,在《iOS 7 Programming Pushing the Limits》的 Further Reading: objc_explain_Non-pointer_isa 部分談論了這個問題。 - 《談 Objective-C Block 的實現》
內容非常翔實,特別是關於 Block 型別的部分,強烈建議做下文章開頭提到的測試:Objective-C Blocks Quiz。 - 訊息傳送和訊息轉發
訊息傳送比較好理解,先了解下 runtime 吧,可以檢視官方文件《Objective-C Runtime Guide》。之前學習其他語言的時候還沒有關注過呼叫函式的背後發生了什麼,在 Objective-C 中,在物件上呼叫方法稱為傳送訊息,比如[receiver message];
這行程式碼,編譯的時候編譯器將之轉換為對 底層 C 函式objc_msgSend
的呼叫:objc_msgSend(receiver, selector)
;在執行時,呼叫哪個方法則完全由 runtime 決定,甚至在執行時可以替換呼叫的方法,這是 Objective-C 被稱為動態語言的根本原因。對於訊息轉發,說實話我現在還不知道這個的應用場景,看到的大部分部落格都是說訊息轉發給了你補救措施來應對沒有沒有實現的方法防止 Crash 或者實現類似多繼承的機制,我有個疑惑,幹嘛不實現那個方法,而要在代價很大的轉發機制裡處理呢。在《Effective Objective-C 2.0》一書第 12 條 tip 中用 @dynamic 演示了實現動態方法解析的例子來說明訊息轉發的意義,老實說,我還是沒有理解這個的意義。這裡有個對官方文件的中文翻譯和一些註解。 - Implement of category and method
找到了來自這位比我厲害得多的90後:《刨根問底Objective-C Runtime(3)- 訊息 和 Category》(文章原來的連結放進來跟簡書的處理有衝突,這裡給的是部落格地址,而不是這篇文章的具體地址,不過很好找)。 - Class load
可以看這篇部落格:《Objective-C Class Loading and Initialization》,看了下作者的 Github,原來是我以前 follow 過的國外程式設計師,看人家的 repo 和星星,質量有保障,再看部落格文章列表,有很多深入底層的內容,一座寶礦啊。另外在《Effective Objective-C 2.0》書中第51節《精簡 initialize 與 load 的實現》中也討論了這個問題,當初看完一頭霧水,如今終於能看懂啦。
Core Data: 大量資料多執行緒同步
這個問題我已經單獨成篇放到這裡了,新增了更多的基礎知識和介紹。
第一步:搭建 Core Data 多執行緒環境
這個問題首先要解決的是搭建 Core Data 多執行緒環境。NSManagedObjectContext
不是執行緒安全的,你不能隨便地開啟一個後臺執行緒訪問 managed object context 進行資料操作就管這叫支援多執行緒了。Core Data 對多執行緒的支援比較好,NSManagedObjectContext
在初始化時可以指定併發模式,有三種選項:
1.NSConfinementConcurrencyType
這種模式是用於向後相容的,使用這種模式時你應該保證不能在其他執行緒使用 context,但這點很難保證,不推薦使用。此模式在 iOS 9中已經被廢棄。
2.NSPrivateQueueConcurrencyType
在一個私有佇列中建立並管理 context。
3.NSMainQueueConcurrencyType
其實這種模式與第 2 種模式比較相似,只不過 context 與主佇列繫結,也因此與應用的 event loop 很親密。當 context 與 UI 更新相關的話就使用這種模式。
搭建多執行緒 Core Data 環境的方案一般如下,建立一個 NSMainQueueConcurrencyType
的 context 用於響應 UI 事件,其他涉及大量資料操作可能會阻塞 UI 的就使用 NSPrivateQueueConcurrencyType
的 context。環境搭建好了,如何實現多執行緒操作?官方文件《Using a Private Queue to Support Concurrency》為我們做了示範,在 private queue 的 context 中進行操作時,應該使用以下方法:
1 2 |
func performBlock(_ block: () -> Void)//在私有佇列中非同步地執行 Blcok func performBlockAndWait(_ block: () -> Void)//在私有佇列中執行 Block 直至操作結束才返回 |
要在不同執行緒中使用 managed object context 時,不需要我們建立後臺執行緒然後訪問 managed object context 進行操作,而是交給 context 自身繫結的私有佇列去處理,我們只需要在上述兩個方法的 Block 中執行操作即可。而且,在 NSMainQueueConcurrencyType
的 context 中也應該使用這種方法執行操作,這樣可以確保 context 本身在主執行緒中進行操作。
第二步:資料的同步操作
多 context 同步最簡單的方案如下:
1 2 3 4 5 6 7 8 9 10 |
NSNotificationCenter.defaultCenter().addObserver(self, selector: "backgroundContextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: backgroundContext) func backgroundContextDidSave(notification: NSNotification){ mainContext.performBlock(){ mainContext.mergeChangesFromContextDidSaveNotification(notification) } } |
NSManagedObjectContext
在執行儲存操作後會發出 NSManagedObjectContextDidSaveNotification
,包含了 context 所有的變化資訊,包括新增的、更新的以及刪除的物件的資訊;而 mergeChangesFromContextDidSaveNotification(_ notification)
方法則用於合併其他 context 中發生的變化。
如果 context 並未觀察其他 context 的 NSManagedObjectContextDidSaveNotification
通知,且儲存時,persistent store 已經被其他 context 更改過,那麼很可能存在差異,此時同步就有了以下幾種選擇:選擇儲存 context 中的版本或者使用 persistent store 的版本替換 context 的版本,又或是將兩者的版本融合。這種同步方式由 NSManagedObjectContext
的 mergePolicy
屬性決定。
1.NSErrorMergePolicy
預設策略,有衝突時儲存失敗,persistent store 和 context 都維持原樣,並返回錯誤資訊,是唯一反饋錯誤資訊的合併策略。
2.NSMergeByPropertyStoreTrumpMergePolicy
當 persistent store 和 context 裡的版本有衝突,persistent store 裡的版本有優先權, context 裡使用 persistent store 裡的版本替換,但是 context 裡沒有衝突的變化則不會受到影響。
3.NSMergeByPropertyObjectTrumpMergePolicy
與上面相反,context 裡的版本有優先權,persistent store 裡使用 context 裡的版本替換,但是 persistent store 裡沒有衝突的變化不受影響。
4.NSOverwriteMergePolicy
用 context 裡的版本強制覆蓋 persistent store 裡的版本。
5.NSRollbackMergePolicy
放棄 context 中的所有變化並使用 persistent store 中的版本進行替換。
同步是件很複雜的事情,實際上還是需要根據實際需要來選擇同步方案。上面兩種方案中第一種概念簡單實現容易,第二種比較複雜相對危險,需要謹慎選擇同步策略。還有一點需要注意,如果需要跨執行緒使用 managed object,那麼不要直接在其他 context 裡使用該 managed object,而應該通過該物件的 objectID 將該物件 fetch 到 context 裡。
最後,搞定大量資料
多執行緒和同步問題解決,最後的難點:大量資料。大量資料意味著需要我們關注記憶體佔用和效能,寫程式碼時需要記得以下規則:
- 1.儘可能快取需要的資料,不相關的資料保持 faults狀態。
- 2.fetch 時儘可能精準,少引入不相關的資料。
- 3.構建多 context 時儘量將同類 managed object 集中,最大限度減少合併需求。
- 4.提升操作效率,對Asynchronous Fetch, Batch Update,Batch Delete 等新特性儘可能利用。
多執行緒程式設計
在 iOS 程式設計中,這幾種情況下需要處理多執行緒:UI 事件必須在主執行緒裡進行,其他的可以放在後臺進行;而進行一些耗時長或阻塞執行緒的任務,最後放進後臺執行緒裡進行。iOS 的多執行緒技術有這麼幾種:執行緒,GCD 和 NSOperationQueue。執行緒這種技術比較複雜,而多執行緒程式設計向來是「複雜必死」,推薦儘可能使用後二者,但執行緒有個後二者沒有的優勢:能夠精確保證任務執行的時間。
GCD 全稱是 Grand Central Dispatch, 是 libdispatch 這個庫的外部代號,基於 C 的底層來實現;而NSOperationQueue,通稱操作佇列,是基於 GCD 實現的。GCD 能做的 NSOperationQueue 基本上都能做,而且還有些 GCD 中不易實現的特性,如掛起、取消任務,雖然在 iOS 8 中,GCD 也提供了取消任務的功能,但在 GCD 中任務的掛起和取消都有較大的侷限性;雖然大多數情況下應該使用抽象級別更高的 API,也就是 NSOperationQueue,但處理一般的後臺任務我偏愛 GCD,主要是 GCD 搭配 Blcok 使用簡單,非常方便。如何選擇,下面兩個連結對此問題的討論值得一看:
另外,還推薦這些文章:objc 的併發程式設計專題《Concurrent Programming》 及中文翻譯版本;雷純鋒的部落格《iOS 併發程式設計之 Operation Queues》;NSHipster 的《NSOperation》。
設計模式
評價 Delegate, Notification, KVO 幾種設計模式的優缺點
我不覺得這個問題是個好問題,與其比較這幾個設計模式的優缺點,不如談它們各自的特點比較好,因為它們是為了解決某類問題才設計出來的,有各自適合的使用場景。另外,給個 iOS 中設計模式的介紹:iOS Design Patterns。
為什麼出題目都喜歡把這三個設計模式拿來對比呢?Notification 和 KVO都是用於協助物件間的通訊:某個物件監聽某個事件的發生,當某個事件發生時,該物件會得到通知然後做出響應。這幾句話大概是以前看過的書本上說的。如果你以前沒接觸過設計模式,第一次學習時總是能夠看到事件、響應這類模糊的詞彙,看得你雲裡霧裡,好吧,我說的是我。 但 delegate,應該說沒有監聽的功能,而是當事件發生或時機到了,要求 delegate 物件做點什麼。剛開始學習 OC 的時候,一本書中將 delegate比喻為助手,那時候不怎麼理解,現在覺得這個比喻十分恰當。雖然delegate 模式在 OC 中隨處可見,在UIViewController 類中廣泛存在,但在開發 FaceAlbum 的過程中只遇到過一次自定義 protocol/delegate 的情況,後來還是用 KVO 取代了。
相對於 Notification 和 KVO 模式,使用 delegate 模式你會明確知道物件的 delegate 能幹什麼,因為要成為 某個物件的delegate,該物件得遵守指定的 protocol,protocol 指定了 delegate 物件需要實現的方法。
Notification和 KVO兩者都需要監聽事件的物件(早期看見事件就犯暈,如今寫來覺得用這個詞挺順手的)去註冊,delegate則需要 delegate 物件遵守指定的 protocol;Notification 中監聽者向一個單例物件NSNotificationCenter註冊,NSNotificationCenter類似一個廣播中心,接受任何物件的註冊,後者則向要監聽的物件註冊,一對一,這兩者都不需要物件之間有聯絡,而 delegate 則需要通訊的物件通過變數聯絡;NSNotification模式裡監聽的物件與被監聽的物件通訊是通過 NSNotificationCenter 這個中介,而KVO 裡,不能說兩者是直接通訊的,我沒有了解過過 KVO 是如何實現通訊的,從表面上看兩者就那麼心靈感應一般,這是系統替我們實現的,而delegate,由於通過變數連線,直接向 delegate 傳送訊息即可,在這點上,NSNotification不需要通訊雙方知道對方,而後兩者則不然;在響應事件時,NSNotification和 KVO 模式裡都是在註冊時指定響應方法,而 delegate 則在 protocol 裡預定義了響應方法。
說了這麼多,不直觀,說個實際場景,比如在 UICollectionView 裡選擇 cell 的時候,希望 title 能夠跟蹤選中 cell 的數量。這裡用NSNotification和 KVO 都能實現,但是我更喜歡 KVO,感覺更優雅,因為使用NSNotification模式的話,選中一個 cell 的時候要在選擇的方法裡手動釋出通知,而 KVO,只要對觀察的屬性實現 KVO 相容的方法就可以了;而delegate,自己做自己的 delegate,呃。而面對一些系統裡的事件,比如鍵盤的出現與消失,圖片庫的變化,使用NSNotification更加自然,因為 KVO 限於對物件屬性的跟蹤。
暫時寫這麼多,推薦部落格《When to use Delegation, Notification, or Observation in iOS》,可能需要梯子。