閱讀源碼的樂趣
閱讀源碼尤其是優秀的源碼是一件很有樂趣的事情,可以拓寬視野,提高品位,鍛煉思維,就像間接地在跟作者溝通一樣。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
</blockquote> </blockquote> </blockquote>如果有足夠的好奇心,并且總想知道「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 會有意想不到的好效果
</ul>產生了一些疑問,比如
- 多個子 DataSource 被組合成一個 Composed DataSource 時,如何通過 IndexPath 找到對應的 DataSource?
- 找到之后如何處理?
- 是否置頂是如何實現的?
- 如何通過有限狀態機來管理 Loading 狀態?
- 如果有按鈕,那么按鈕的點擊事件如何處理?
- Collection View 沒有 headerView,這又是怎么實現的?
- 數據是怎么載入的?
</ul>大概有了些概念和疑問之后,就可以打開源碼痛快看了,先來看看目錄結構 (可以在這里在線瀏覽)
|- Framework |- Categories |- DataSources |- Layouts |- ViewControllers |- Views |- Application看來關鍵的信息都在 Framework 里了,那如何切入呢?反其道而行之,先來看看這些 Framework 是怎么用的,最直接的就從 ViewController 入手。那就先來看看 AAPLCatListViewController 這個類吧,如果沒猜錯的話,應該是展示喵咪列表(直觀的名字很重要)。
果然很小,居然只有 140 行,如果不分離的話,1400 行也是可以輕松達到的。看到定義了一個 AAPLSegmentedDataSource,腦海里大概可以想象出是一個可以切換 Tag 的頁面,接著又看到了兩個 DataSource,那這兩個頁面的數據源應該就是它們了。
@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然后又看到這么一行
- (void)dealloc { [self.segmentedDataSource aapl_removeObserver:self.selectedDataSourceObserver]; }看起來是蘋果自己實現了一個 KVO Wrapper,果然他們自己也無法忍受原生的KVO,哈哈。接著到了 ViewDidLoad,新建了兩個 DataSource,那新建的時候都干了些什么?
- (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;
}</pre></div>
所以只是初始化,然后設置一些信息,Nothing Special。然后看到了 AAPLLayoutSectionMetrics ,看起來是設置 Layout 的一些顯示信息,如 height / backgroundColor 之類的。
最后創建了一個 KVO 來監測 selectedDataSource 的變化,界面上做相應的調整。
接下來看看 AAPLCatListDataSource 的實現,一進去發現
@interface AAPLCatListDataSource : AAPLBasicDataSource /// Is this list showing the favorites or all available cats? @property (nonatomic) BOOL showingFavorites; @end看來 AAPLBasicDataSource 一定做了很多事,進入到 AAPLBasicDataSource.m 文件,看到這個方法
- (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];
}</pre></div>
注意到有一個
setNeedsLoadContent
方法,看起來數據的載入應該是通過這個方法來觸發的,進去看看- (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
都做了什么- (void)loadContent { // To be implemented by subclasses… }看來需要在子類實現這個方法,那就到 AAPLCatListDataSource 里看看這個方法都做了什么
- (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]; }];
}</pre></div>
使用了
loadContentWithBlock:
方法,進去看看,這個方法做了什么- (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);
}</pre></div>
簡單說來就是生成了一個 loading,然后把 loading 傳給 block,那
loadingWithCompletionHandler:
這個方法又做了什么+ (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 才會被調用
- (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;
}</pre></div>
既然是 _ 開頭,那應該是內部方法,對外封裝了幾種狀態,如
ignore
,done
,updateWithContent:
等。咦,這里為什么要先把 block 賦給一個臨時變量 block,然后再把 block 設為 nil呢?看起來像是為了解決某種內存問題。如果直接
_block(newState, error, update)
會怎樣?哦,雖然這里沒有出現 self,但 _block 是一個 instance 變量,所以在 ^{} 里會對 self 進行強引用。而如果賦給一個臨時變量,那么只會對這個臨時變量強引用,就不會出現循環引用的情況。AAPLLoading 看的差不多了,再出來看
loadContentWithBlock:
,注意到在 CompletionHandler 里,有這么一段[self endLoadingWithState:newState error:error update:^{ AAPLDataSource *me = weakself; if (update && me) update(me); }];這里的 self 是 AAPLDataSource (Block嵌套多了,還真是容易暈啊),來看看
endLoadingWithState:error:update
這個方法都做了什么- (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];
}</pre></div>
設置一些狀態,然后在恰當的時機調用 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,看起來像是統一的數據層,瞄一眼@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</pre></div>
果然如此,將來數據的載入形式有變化,或需要做緩存啥的,都可以在這一層處理,其他部分不會感覺到變化。
這一輪看下來已經有不少信息量了,來簡單捋一下:
[SegmentedDataSource setNeedsLoadContent] ↓ [CatListDataSource loadContent] ↓ [DataSource loadContentWithBlock:] ↓ 創建 loading,設置 loading 完成后要做的事 → 拿到數據后放到 updateQueue 里,等全部拿到再執行 batchUpdate ↓ 執行 loadContentBlock → 使用 DataAccessManager 去獲取數據,拿到后交給 loading到這里,我們還沒有運行 Project 看效果,因為我覺得代碼包含的信息會更豐富,而且這么看下來后,對于界面會長啥樣也有個大概的了解。
這只是開始,繼續挖掘下去還會有不少好東西,比如 Favorite 按鈕的處理,它是通過 Responder Chain 而不是 Delegate 來實現的,也是一個思路。通過有限狀態機來管理 loading 狀態也是很有意思的實現。
如果有興趣,可以看下 ComposedDataSource,先不看實現,如果要自己寫大概會是什么思路,比如當調用
[UICollectionView cellForItemAtIndexPath:]
時,如何找到對應的 DataSource,找到之后如何渲染對應的 Cell 等。所以看源碼真的是一件很有意思的事情,像一場冒險,總是會有意外收獲,可能在不知不覺中,能力就得到了提升。
原文出處: Limboy
–EOF–
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!sesese色