閱讀原始碼的樂趣

發表於2014-12-19

閱讀原始碼尤其是優秀的原始碼是一件很有樂趣的事情,可以拓寬視野,提高品位,鍛鍊思維,就像間接地在跟作者溝通一樣。Quora 上有一個問題是:TJ-Holowaychunk是如何學習程式設計的,他的回答是

I don’t read books, never went to school, I just read other people’s code and always wonder how things work

如果有足夠的好奇心,並且總想知道「How Things Work」,那麼閱讀原始碼就是個不錯的途徑。

原始碼的複雜度不同,需要投入的時間、使用的方法也不同,以一箇中等複雜度的專案為例,簡單分享下我閱讀原始碼的一些經驗。

WWDC 2014,有一個 Session 是講「Advanced User Interfaces with Collection Views」,之所以選擇這個,是因為它是我們還算熟悉的物件(Collection View),但蘋果用了一些「特殊」的架構來做到程式碼複用,並且減少 VC 的體積,而且使用了部分 iTunes Connect 的原始碼,而不是簡單的演示程式碼。所以決定一窺究竟。

為了有一個大概的感受,先看一遍視訊,不需要領會每個要點,先記錄一些關鍵資訊,方便到時翻原始碼。

  • 這套結構可以處理複雜的 DataSource
  • 可以同時適配 iPhone / iPad
  • 有一個統一的 loading indicator
  • 可以設定某個 Header 是否置頂
  • 可以有一個全域性的 Header
  • 通過聚合 DataSource 的方法來達到程式碼複用,並且只有一個 VC
  • 可以設定聚合形式為 Segmented / Composed
  • layout資訊可以配置,且可以覆蓋
  • 使用了有限狀態機
  • 子 DataSource 在資料載入完成後會有一個 block,所需的 DataSource 都載入完成時,這些 block 會被統一執行
  • Section Metrics 可以設定 Section 的具體表現
  • layout 的資訊會在內部被儲存,避免重複計算 (Snapshot Metrics)
  • Optional Layout Methods 會有意想不到的好效果

產生了一些疑問,比如

  • 多個子 DataSource 被組合成一個 Composed DataSource 時,如何通過 IndexPath 找到對應的 DataSource?
  • 找到之後如何處理?
  • 是否置頂是如何實現的?
  • 如何通過有限狀態機來管理 Loading 狀態?
  • 如果有按鈕,那麼按鈕的點選事件如何處理?
  • Collection View 沒有 headerView,這又是怎麼實現的?
  • 資料是怎麼載入的?

大概有了些概念和疑問之後,就可以開啟原始碼痛快看了,先來看看目錄結構 (可以在這裡線上瀏覽)

看來關鍵的資訊都在 Framework 裡了,那如何切入呢?反其道而行之,先來看看這些 Framework 是怎麼用的,最直接的就從 ViewController 入手。那就先來看看 AAPLCatListViewController 這個類吧,如果沒猜錯的話,應該是展示喵咪列表(直觀的名字很重要)。

果然很小,居然只有 140 行,如果不分離的話,1400 行也是可以輕鬆達到的。看到定義了一個 AAPLSegmentedDataSource,腦海裡大概可以想象出是一個可以切換 Tag 的頁面,接著又看到了兩個 DataSource,那這兩個頁面的資料來源應該就是它們了。

然後又看到這麼一行

看起來是蘋果自己實現了一個 KVO Wrapper,果然他們自己也無法忍受原生的KVO,哈哈。接著到了 ViewDidLoad,新建了兩個 DataSource,那新建的時候都幹了些什麼?

所以只是初始化,然後設定一些資訊,Nothing Special。然後看到了 AAPLLayoutSectionMetrics ,看起來是設定 Layout 的一些顯示資訊,如 height / backgroundColor 之類的。

最後建立了一個 KVO 來監測 selectedDataSource 的變化,介面上做相應的調整。

接下來看看 AAPLCatListDataSource 的實現,一進去發現

看來 AAPLBasicDataSource 一定做了很多事,進入到 AAPLBasicDataSource.m 檔案,看到這個方法

注意到有一個 setNeedsLoadContent 方法,看起來資料的載入應該是通過這個方法來觸發的,進去看看

第一個方法沒怎麼接觸過,查一下文件先,原來是可以取消之前通過performSelector:withObject:afterDelay: 觸發的方法,為了加深印象,順便 Google 一下這個方法,原來performSelector:withObject:afterDelay 在方法被執行前,會持有 Object,方法執行後在解除對 Object 的持有,如果不小心多次呼叫這個方法就有可能導致記憶體洩露,所以在呼叫此方法前先 cancel 一下是個好習慣。

再來看看這個 loadContent 都做了什麼

看來需要在子類實現這個方法,那就到 AAPLCatListDataSource 裡看看這個方法都做了什麼

使用了 loadContentWithBlock: 方法,進去看看,這個方法做了什麼

簡單說來就是生成了一個 loading,然後把 loading 傳給 block,那 loadingWithCompletionHandler: 這個方法又做了什麼

所以就是生成一個 loading 例項,然後把 handler 存到 block 屬性裡。既然存了,那將來某個時候一定會用到,從名字上來看,應該是 loading 完成時會被呼叫,搜尋 block 關鍵字,發現只有在下面這個方法中 block 才會被呼叫

既然是 _ 開頭,那應該是內部方法,對外封裝了幾種狀態,如 ignoredoneupdateWithContent: 等。

咦,這裡為什麼要先把 block 賦給一個臨時變數 block,然後再把 block 設為 nil呢?看起來像是為了解決某種記憶體問題。如果直接 _block(newState, error, update) 會怎樣?哦,雖然這裡沒有出現 self,但 _block 是一個 instance 變數,所以在 ^{} 裡會對 self 進行強引用。而如果賦給一個臨時變數,那麼只會對這個臨時變數強引用,就不會出現迴圈引用的情況。

AAPLLoading 看的差不多了,再出來看 loadContentWithBlock: ,注意到在 CompletionHandler 裡,有這麼一段

這裡的 self 是 AAPLDataSource (Block巢狀多了,還真是容易暈啊),來看看endLoadingWithState:error:update 這個方法都做了什麼

設定一些狀態,然後在恰當的時機呼叫 update block,咦,這裡有個 dispatch_block_t 沒怎麼見過,查了一下原來是一個內建的空傳值和空返回的block。

看了下 enqueuePendingUpdateBlock,會把現在的這個 update 結合之前的 updateBlock,形成一個新的 updateBlock,應該就是視訊裡提到的當所有的 DataSource 都載入完時,統一執行之前的 update block

notifyBatchUpdate: 所做的是看一下 Delegate 是否響應 dataSource:performBatchUpdate:complete: 如果響應則走你,不然挨個執行 update / complete。

看完了 loadContentWithBlock 再來看看這個 Block 裡面都做了什麼,大意是根據 self.showingFavorites 來切換不同的資料來源,這裡看到了一個新的類 AAPLDataAccessManager,看起來像是統一的資料層,瞄一眼

果然如此,將來資料的載入形式有變化,或需要做快取啥的,都可以在這一層處理,其他部分不會感覺到變化。

這一輪看下來已經有不少資訊量了,來簡單捋一下:

到這裡,我們還沒有執行 Project 看效果,因為我覺得程式碼包含的資訊會更豐富,而且這麼看下來後,對於介面會長啥樣也有個大概的瞭解。

這只是開始,繼續挖掘下去還會有不少好東西,比如 Favorite 按鈕的處理,它是通過 Responder Chain 而不是 Delegate 來實現的,也是一個思路。通過有限狀態機來管理 loading 狀態也是很有意思的實現。

如果有興趣,可以看下 ComposedDataSource,先不看實現,如果要自己寫大概會是什麼思路,比如當呼叫[UICollectionView cellForItemAtIndexPath:] 時,如何找到對應的 DataSource,找到之後如何渲染對應的 Cell 等。

所以看原始碼真的是一件很有意思的事情,像一場冒險,總是會有意外收穫,可能在不知不覺中,能力就得到了提升。
–EOF–

相關文章