閱讀原始碼尤其是優秀的原始碼是一件很有樂趣的事情,可以拓寬視野,提高品位,鍛鍊思維,就像間接地在跟作者溝通一樣。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,這又是怎麼實現的?
- 資料是怎麼載入的?
大概有了些概念和疑問之後,就可以開啟原始碼痛快看了,先來看看目錄結構 (可以在這裡線上瀏覽)
1 2 3 4 5 6 7 |
|- Framework |- Categories |- DataSources |- Layouts |- ViewControllers |- Views |- Application |
看來關鍵的資訊都在 Framework 裡了,那如何切入呢?反其道而行之,先來看看這些 Framework 是怎麼用的,最直接的就從 ViewController 入手。那就先來看看 AAPLCatListViewController 這個類吧,如果沒猜錯的話,應該是展示喵咪列表(直觀的名字很重要)。
果然很小,居然只有 140 行,如果不分離的話,1400 行也是可以輕鬆達到的。看到定義了一個 AAPLSegmentedDataSource,腦海裡大概可以想象出是一個可以切換 Tag 的頁面,接著又看到了兩個 DataSource,那這兩個頁面的資料來源應該就是它們了。
1 2 3 4 5 6 7 |
@interface APPLCatListViewController () @property (nonatomic, strong) AAPLSegmentedDataSource *segmentedDataSource; @property (nonatomic, strong) AAPLCatListDataSource *catsDataSource; @property (nonatomic, strong) AAPLCatListDataSource *favoriteCatsDataSource; @property (nonatomic, strong) NSIndexPath *selectedIndexPath; @property (nonatomic, strong) id selectedDataSourceObserver; @end |
然後又看到這麼一行
1 2 3 4 |
- (void)dealloc { [self.segmentedDataSource aapl_removeObserver:self.selectedDataSourceObserver]; } |
看起來是蘋果自己實現了一個 KVO Wrapper,果然他們自己也無法忍受原生的KVO,哈哈。接著到了 ViewDidLoad,新建了兩個 DataSource,那新建的時候都幹了些什麼?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (AAPLCatListDataSource *)newAllCatsDataSource { AAPLCatListDataSource *dataSource = [[AAPLCatListDataSource alloc] init]; dataSource.showingFavorites = NO; dataSource.title = NSLocalizedString(@"All", @"Title for available cats list"); dataSource.noContentMessage = NSLocalizedString(@"All the big ...", @"The message to show when no cats are available"); dataSource.noContentTitle = NSLocalizedString(@"No Cats", @"The title to show when no cats are available"); dataSource.errorMessage = NSLocalizedString(@"A problem with the network ....", @"Message to show when unable to load cats"); dataSource.errorTitle = NSLocalizedString(@"Unable To Load Cats", @"Title of message to show when unable to load cats"); return dataSource; } |
所以只是初始化,然後設定一些資訊,Nothing Special。然後看到了 AAPLLayoutSectionMetrics ,看起來是設定 Layout 的一些顯示資訊,如 height / backgroundColor 之類的。
最後建立了一個 KVO 來監測 selectedDataSource 的變化,介面上做相應的調整。
接下來看看 AAPLCatListDataSource 的實現,一進去發現
1 2 3 4 |
@interface AAPLCatListDataSource : AAPLBasicDataSource /// Is this list showing the favorites or all available cats? @property (nonatomic) BOOL showingFavorites; @end |
看來 AAPLBasicDataSource 一定做了很多事,進入到 AAPLBasicDataSource.m 檔案,看到這個方法
1 2 3 4 5 6 7 8 9 10 11 12 |
- (void)setShowingFavorites:(BOOL)showingFavorites { if (showingFavorites == _showingFavorites) return; _showingFavorites = showingFavorites; [self resetContent]; [self setNeedsLoadContent]; if (showingFavorites) [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(observeFavoriteToggledNotification:) name:AAPLCatFavoriteToggledNotificationName object:nil]; } |
注意到有一個 setNeedsLoadContent
方法,看起來資料的載入應該是通過這個方法來觸發的,進去看看
1 2 3 4 5 |
- (void)setNeedsLoadContent { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(loadContent) object:nil]; [self performSelector:@selector(loadContent) withObject:nil afterDelay:0]; } |
第一個方法沒怎麼接觸過,查一下文件先,原來是可以取消之前通過performSelector:withObject:afterDelay:
觸發的方法,為了加深印象,順便 Google 一下這個方法,原來performSelector:withObject:afterDelay
在方法被執行前,會持有 Object,方法執行後在解除對 Object 的持有,如果不小心多次呼叫這個方法就有可能導致記憶體洩露,所以在呼叫此方法前先 cancel 一下是個好習慣。
再來看看這個 loadContent
都做了什麼
1 2 3 4 |
- (void)loadContent { // To be implemented by subclasses… } |
看來需要在子類實現這個方法,那就到 AAPLCatListDataSource 裡看看這個方法都做了什麼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
- (void)loadContent { [self loadContentWithBlock:^(AAPLLoading *loading) { void (^handler)(NSArray *cats, NSError *error) = ^(NSArray *cats, NSError *error) { // Check to make certain a more recent call to load content hasn't superceded this one… if (!loading.current) { [loading ignore]; return; } if (error) { [loading doneWithError:error]; return; } if (cats.count) [loading updateWithContent:^(AAPLCatListDataSource *me) { me.items = cats; }]; else [loading updateWithNoContent:^(AAPLCatListDataSource *me) { me.items = @[]; }]; }; if (self.showingFavorites) [[AAPLDataAccessManager manager] fetchFavoriteCatListWithCompletionHandler:handler]; else [[AAPLDataAccessManager manager] fetchCatListWithCompletionHandler:handler]; }]; } |
使用了 loadContentWithBlock:
方法,進去看看,這個方法做了什麼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
- (void)loadContentWithBlock:(AAPLLoadingBlock)block { [self beginLoading]; __weak typeof(&*self) weakself = self; AAPLLoading *loading = [AAPLLoading loadingWithCompletionHandler:^(NSString *newState, NSError *error, AAPLLoadingUpdateBlock update){ if (!newState) return; [self endLoadingWithState:newState error:error update:^{ AAPLDataSource *me = weakself; if (update && me) update(me); }]; }]; // Tell previous loading instance it's no longer current and remember this loading instance self.loadingInstance.current = NO; self.loadingInstance = loading; // Call the provided block to actually do the load block(loading); } |
簡單說來就是生成了一個 loading,然後把 loading 傳給 block,那 loadingWithCompletionHandler:
這個方法又做了什麼
1 2 3 4 5 6 7 8 |
+ (instancetype)loadingWithCompletionHandler:(void(^)(NSString *state, NSError *error, AAPLLoadingUpdateBlock update))handler { NSParameterAssert(handler != nil); AAPLLoading *loading = [[self alloc] init]; loading.block = handler; loading.current = YES; return loading; } |
所以就是生成一個 loading 例項,然後把 handler 存到 block 屬性裡。既然存了,那將來某個時候一定會用到,從名字上來看,應該是 loading 完成時會被呼叫,搜尋 block 關鍵字,發現只有在下面這個方法中 block 才會被呼叫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- (void)_doneWithNewState:(NSString *)newState error:(NSError *)error update:(AAPLLoadingUpdateBlock)update { #if DEBUG if (!OSAtomicCompareAndSwap32(0, 1, &_complete)) NSAssert(false, @"completion method called more than once"); #endif void (^block)(NSString *state, NSError *error, AAPLLoadingUpdateBlock update) = _block; dispatch_async(dispatch_get_main_queue(), ^{ block(newState, error, update); }); _block = nil; } |
既然是 _ 開頭,那應該是內部方法,對外封裝了幾種狀態,如 ignore
, done
, updateWithContent:
等。
咦,這裡為什麼要先把 block 賦給一個臨時變數 block,然後再把 block 設為 nil呢?看起來像是為了解決某種記憶體問題。如果直接 _block(newState, error, update)
會怎樣?哦,雖然這裡沒有出現 self,但 _block 是一個 instance 變數,所以在 ^{} 裡會對 self 進行強引用。而如果賦給一個臨時變數,那麼只會對這個臨時變數強引用,就不會出現迴圈引用的情況。
AAPLLoading 看的差不多了,再出來看 loadContentWithBlock:
,注意到在 CompletionHandler 裡,有這麼一段
1 2 3 4 5 |
[self endLoadingWithState:newState error:error update:^{ AAPLDataSource *me = weakself; if (update && me) update(me); }]; |
這裡的 self 是 AAPLDataSource (Block巢狀多了,還真是容易暈啊),來看看endLoadingWithState:error:update
這個方法都做了什麼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (void)endLoadingWithState:(NSString *)state error:(NSError *)error update:(dispatch_block_t)update { self.loadingError = error; self.loadingState = state; if (self.shouldDisplayPlaceholder) { if (update) [self enqueuePendingUpdateBlock:update]; } else { [self notifyBatchUpdate:^{ // Run pending updates [self executePendingUpdates]; if (update) update(); }]; } self.loadingComplete = YES; [self notifyContentLoadedWithError:error]; } |
設定一些狀態,然後在恰當的時機呼叫 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,看起來像是統一的資料層,瞄一眼
1 2 3 4 5 6 7 8 9 10 11 12 |
@class AAPLCat; @interface AAPLDataAccessManager : NSObject + (AAPLDataAccessManager *)manager; - (void)fetchCatListWithCompletionHandler:(void(^)(NSArray *cats, NSError *error))handler; - (void)fetchFavoriteCatListWithCompletionHandler:(void(^)(NSArray *cats, NSError *error))handler; - (void)fetchDetailForCat:(AAPLCat *)cat completionHandler:(void(^)(AAPLCat *cat, NSError *error))handler; - (void)fetchSightingsForCat:(AAPLCat *)cat completionHandler:(void(^)(NSArray *sightings, NSError *error))handler; @end |
果然如此,將來資料的載入形式有變化,或需要做快取啥的,都可以在這一層處理,其他部分不會感覺到變化。
這一輪看下來已經有不少資訊量了,來簡單捋一下:
1 2 3 4 5 6 7 8 9 |
[SegmentedDataSource setNeedsLoadContent] ↓ [CatListDataSource loadContent] ↓ [DataSource loadContentWithBlock:] ↓ 建立 loading,設定 loading 完成後要做的事 → 拿到資料後放到 updateQueue 裡,等全部拿到再執行 batchUpdate ↓ 執行 loadContentBlock → 使用 DataAccessManager 去獲取資料,拿到後交給 loading |
到這裡,我們還沒有執行 Project 看效果,因為我覺得程式碼包含的資訊會更豐富,而且這麼看下來後,對於介面會長啥樣也有個大概的瞭解。
這只是開始,繼續挖掘下去還會有不少好東西,比如 Favorite 按鈕的處理,它是通過 Responder Chain 而不是 Delegate 來實現的,也是一個思路。通過有限狀態機來管理 loading 狀態也是很有意思的實現。
如果有興趣,可以看下 ComposedDataSource,先不看實現,如果要自己寫大概會是什麼思路,比如當呼叫[UICollectionView cellForItemAtIndexPath:]
時,如何找到對應的 DataSource,找到之後如何渲染對應的 Cell 等。
所以看原始碼真的是一件很有意思的事情,像一場冒險,總是會有意外收穫,可能在不知不覺中,能力就得到了提升。
–EOF–