閱讀源碼的樂(lè)趣
閱讀源碼尤其是優(yōu)秀的源碼是一件很有樂(lè)趣的事情,可以拓寬視野,提高品位,鍛煉思維仗颈,就像間接地在跟作者溝通一樣。Quora 上有一個(gè)問(wèn)題是:TJ-Holowaychunk是如何學(xué)習(xí)編程的椎例,他的回答是
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」,那么閱讀源碼就是個(gè)不錯(cuò)的途徑订歪。
源碼的復(fù)雜度不同脖祈,需要投入的時(shí)間、使用的方法也不同刷晋,以一個(gè)中等復(fù)雜度的項(xiàng)目為例盖高,簡(jiǎn)單分享下我閱讀源碼的一些經(jīng)驗(yàn)。
WWDC 2014掏秩,有一個(gè) Session 是講「Advanced User Interfaces with Collection Views」或舞,之所以選擇這個(gè),是因?yàn)樗俏覀冞€算熟悉的對(duì)象(Collection View)蒙幻,但蘋果用了一些「特殊」的架構(gòu)來(lái)做到代碼復(fù)用,并且減少 VC 的體積胆筒,而且使用了部分 iTunes Connect 的源碼邮破,而不是簡(jiǎn)單的演示代碼。所以決定一窺究竟仆救。
為了有一個(gè)大概的感受抒和,先看一遍視頻,不需要領(lǐng)會(huì)每個(gè)要點(diǎn)彤蔽,先記錄一些關(guān)鍵信息摧莽,方便到時(shí)翻源碼。
- 這套結(jié)構(gòu)可以處理復(fù)雜的 DataSource
- 可以同時(shí)適配 iPhone / iPad
- 有一個(gè)統(tǒng)一的 loading indicator
- 可以設(shè)置某個(gè) Header 是否置頂
- 可以有一個(gè)全局的 Header
- 通過(guò)聚合 DataSource 的方法來(lái)達(dá)到代碼復(fù)用顿痪,并且只有一個(gè) VC
- 可以設(shè)置聚合形式為 Segmented / Composed
- layout信息可以配置镊辕,且可以覆蓋
- 使用了有限狀態(tài)機(jī)
- 子 DataSource 在數(shù)據(jù)載入完成后會(huì)有一個(gè) block油够,所需的 DataSource 都載入完成時(shí),這些 block 會(huì)被統(tǒng)一執(zhí)行
- Section Metrics 可以設(shè)置 Section 的具體表現(xiàn)
- layout 的信息會(huì)在內(nèi)部被保存征懈,避免重復(fù)計(jì)算 (Snapshot Metrics)
- Optional Layout Methods 會(huì)有意想不到的好效果
產(chǎn)生了一些疑問(wèn)石咬,比如
- 多個(gè)子 DataSource 被組合成一個(gè) Composed DataSource 時(shí),如何通過(guò) IndexPath 找到對(duì)應(yīng)的 DataSource卖哎?
- 找到之后如何處理鬼悠?
- 是否置頂是如何實(shí)現(xiàn)的?
- 如何通過(guò)有限狀態(tài)機(jī)來(lái)管理 Loading 狀態(tài)亏娜?
- 如果有按鈕焕窝,那么按鈕的點(diǎn)擊事件如何處理?
- Collection View 沒(méi)有 headerView维贺,這又是怎么實(shí)現(xiàn)的袜啃?
- 數(shù)據(jù)是怎么載入的?
大概有了些概念和疑問(wèn)之后幸缕,就可以打開(kāi)源碼痛快看了群发,先來(lái)看看目錄結(jié)構(gòu) (可以在這里在線瀏覽)
|-Framework|-Categories|-DataSources|-Layouts|-ViewControllers|-Views|-Application
看來(lái)關(guān)鍵的信息都在 Framework 里了,那如何切入呢发乔?反其道而行之熟妓,先來(lái)看看這些 Framework 是怎么用的,最直接的就從 ViewController 入手栏尚。那就先來(lái)看看 AAPLCatListViewController 這個(gè)類吧起愈,如果沒(méi)猜錯(cuò)的話,應(yīng)該是展示喵咪列表(直觀的名字很重要)译仗。
果然很小抬虽,居然只有 140 行,如果不分離的話纵菌,1400 行也是可以輕松達(dá)到的阐污。看到定義了一個(gè) AAPLSegmentedDataSource咱圆,腦海里大概可以想象出是一個(gè)可以切換 Tag 的頁(yè)面笛辟,接著又看到了兩個(gè) DataSource,那這兩個(gè)頁(yè)面的數(shù)據(jù)源應(yīng)該就是它們了序苏。
@interfaceAPPLCatListViewController()@property(nonatomic,strong)AAPLSegmentedDataSource*segmentedDataSource;@property(nonatomic,strong)AAPLCatListDataSource*catsDataSource;@property(nonatomic,strong)AAPLCatListDataSource*favoriteCatsDataSource;@property(nonatomic,strong)NSIndexPath*selectedIndexPath;@property(nonatomic,strong)idselectedDataSourceObserver;@end
然后又看到這么一行
-(void)dealloc{[self.segmentedDataSourceaapl_removeObserver:self.selectedDataSourceObserver];}
看起來(lái)是蘋果自己實(shí)現(xiàn)了一個(gè) KVO Wrapper手幢,果然他們自己也無(wú)法忍受原生的KVO,哈哈忱详。接著到了 ViewDidLoad围来,新建了兩個(gè) DataSource,那新建的時(shí)候都干了些什么?
-(AAPLCatListDataSource*)newAllCatsDataSource{AAPLCatListDataSource*dataSource=[[AAPLCatListDataSourcealloc]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");returndataSource;}
所以只是初始化监透,然后設(shè)置一些信息桶错,Nothing Special。然后看到了 AAPLLayoutSectionMetrics 才漆,看起來(lái)是設(shè)置 Layout 的一些顯示信息牛曹,如 height / backgroundColor 之類的。
最后創(chuàng)建了一個(gè) KVO 來(lái)監(jiān)測(cè) selectedDataSource 的變化醇滥,界面上做相應(yīng)的調(diào)整黎比。
接下來(lái)看看 AAPLCatListDataSource 的實(shí)現(xiàn),一進(jìn)去發(fā)現(xiàn)
@interfaceAAPLCatListDataSource:AAPLBasicDataSource/// Is this list showing the favorites or all available cats?@property(nonatomic)BOOLshowingFavorites;@end
看來(lái) AAPLBasicDataSource 一定做了很多事鸳玩,進(jìn)入到 AAPLBasicDataSource.m 文件阅虫,看到這個(gè)方法
-(void)setShowingFavorites:(BOOL)showingFavorites{if(showingFavorites==_showingFavorites)return;_showingFavorites=showingFavorites;[selfresetContent];[selfsetNeedsLoadContent];if(showingFavorites)[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(observeFavoriteToggledNotification:)name:AAPLCatFavoriteToggledNotificationNameobject:nil];}
注意到有一個(gè)setNeedsLoadContent
方法,看起來(lái)數(shù)據(jù)的載入應(yīng)該是通過(guò)這個(gè)方法來(lái)觸發(fā)的不跟,進(jìn)去看看
-(void)setNeedsLoadContent{[NSObjectcancelPreviousPerformRequestsWithTarget:selfselector:@selector(loadContent)object:nil];[selfperformSelector:@selector(loadContent)withObject:nilafterDelay:0];}
第一個(gè)方法沒(méi)怎么接觸過(guò)颓帝,查一下文檔先,原來(lái)是可以取消之前通過(guò)performSelector:withObject:afterDelay:
觸發(fā)的方法窝革,為了加深印象购城,順便 Google 一下這個(gè)方法,原來(lái)performSelector:withObject:afterDelay
在方法被執(zhí)行前虐译,會(huì)持有 Object瘪板,方法執(zhí)行后在解除對(duì) Object 的持有,如果不小心多次調(diào)用這個(gè)方法就有可能導(dǎo)致內(nèi)存泄露漆诽,所以在調(diào)用此方法前先 cancel 一下是個(gè)好習(xí)慣侮攀。
再來(lái)看看這個(gè)loadContent
都做了什么
-(void)loadContent{// To be implemented by subclasses…}
看來(lái)需要在子類實(shí)現(xiàn)這個(gè)方法,那就到 AAPLCatListDataSource 里看看這個(gè)方法都做了什么
-(void)loadContent{[selfloadContentWithBlock:^(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){[loadingignore];return;}if(error){[loadingdoneWithError:error];return;}if(cats.count)[loadingupdateWithContent:^(AAPLCatListDataSource*me){me.items=cats;}];else[loadingupdateWithNoContent:^(AAPLCatListDataSource*me){me.items=@[];}];};if(self.showingFavorites)[[AAPLDataAccessManagermanager]fetchFavoriteCatListWithCompletionHandler:handler];else[[AAPLDataAccessManagermanager]fetchCatListWithCompletionHandler:handler];}];}
使用了loadContentWithBlock:
方法厢拭,進(jìn)去看看兰英,這個(gè)方法做了什么
-(void)loadContentWithBlock:(AAPLLoadingBlock)block{[selfbeginLoading];__weaktypeof(&*self)weakself=self;AAPLLoading*loading=[AAPLLoadingloadingWithCompletionHandler:^(NSString*newState,NSError*error,AAPLLoadingUpdateBlockupdate){if(!newState)return;[selfendLoadingWithState:newStateerror:errorupdate:^{AAPLDataSource*me=weakself;if(update&&me)update(me);}];}];// Tell previous loading instance it's no longer current and remember this loading instanceself.loadingInstance.current=NO;self.loadingInstance=loading;// Call the provided block to actually do the loadblock(loading);}
簡(jiǎn)單說(shuō)來(lái)就是生成了一個(gè) loading,然后把 loading 傳給 block供鸠,那loadingWithCompletionHandler:
這個(gè)方法又做了什么
+(instancetype)loadingWithCompletionHandler:(void(^)(NSString*state,NSError*error,AAPLLoadingUpdateBlockupdate))handler{NSParameterAssert(handler!=nil);AAPLLoading*loading=[[selfalloc]init];loading.block=handler;loading.current=YES;returnloading;}
所以就是生成一個(gè) loading 實(shí)例畦贸,然后把 handler 存到 block 屬性里。既然存了回季,那將來(lái)某個(gè)時(shí)候一定會(huì)用到家制,從名字上來(lái)看,應(yīng)該是 loading 完成時(shí)會(huì)被調(diào)用泡一,搜索 block 關(guān)鍵字,發(fā)現(xiàn)只有在下面這個(gè)方法中 block 才會(huì)被調(diào)用
-(void)_doneWithNewState:(NSString*)newStateerror:(NSError*)errorupdate:(AAPLLoadingUpdateBlock)update{#if DEBUGif(!OSAtomicCompareAndSwap32(0,1,&_complete))NSAssert(false,@"completion method called more than once");#endifvoid(^block)(NSString*state,NSError*error,AAPLLoadingUpdateBlockupdate)=_block;dispatch_async(dispatch_get_main_queue(),^{block(newState,error,update);});_block=nil;}
既然是 _ 開(kāi)頭觅廓,那應(yīng)該是內(nèi)部方法鼻忠,對(duì)外封裝了幾種狀態(tài),如ignore
,done
,updateWithContent:
等。
咦帖蔓,這里為什么要先把*block 賦給一個(gè)臨時(shí)變量 block矮瘟,然后再把 _block 設(shè)為 nil呢?看起來(lái)像是為了解決某種內(nèi)存問(wèn)題塑娇。如果直接 *block(newState, error, update)
會(huì)怎樣澈侠?哦,雖然這里沒(méi)有出現(xiàn) self埋酬,但 _block 是一個(gè) instance 變量哨啃,所以在 ^{} 里會(huì)對(duì) self 進(jìn)行強(qiáng)引用。而如果賦給一個(gè)臨時(shí)變量写妥,那么只會(huì)對(duì)這個(gè)臨時(shí)變量強(qiáng)引用拳球,就不會(huì)出現(xiàn)循環(huán)引用的情況。
AAPLLoading 看的差不多了珍特,再出來(lái)看loadContentWithBlock:
祝峻,注意到在 CompletionHandler 里,有這么一段
[selfendLoadingWithState:newStateerror:errorupdate:^{AAPLDataSource*me=weakself;if(update&&me)update(me);}];
這里的 self 是 AAPLDataSource (Block嵌套多了扎筒,還真是容易暈袄痴摇),來(lái)看看endLoadingWithState:error:update
這個(gè)方法都做了什么
-(void)endLoadingWithState:(NSString*)stateerror:(NSError*)errorupdate:(dispatch_block_t)update{self.loadingError=error;self.loadingState=state;if(self.shouldDisplayPlaceholder){if(update)[selfenqueuePendingUpdateBlock:update];}else{[selfnotifyBatchUpdate:^{// Run pending updates[selfexecutePendingUpdates];if(update)update();}];}self.loadingComplete=YES;[selfnotifyContentLoadedWithError:error];}
設(shè)置一些狀態(tài)嗜桌,然后在恰當(dāng)?shù)臅r(shí)機(jī)調(diào)用 update block奥溺,咦,這里有個(gè) dispatchblockt 沒(méi)怎么見(jiàn)過(guò)症脂,查了一下原來(lái)是一個(gè)內(nèi)置的空傳值和空返回的block谚赎。
看了下enqueuePendingUpdateBlock
,會(huì)把現(xiàn)在的這個(gè) update 結(jié)合之前的 updateBlock诱篷,形成一個(gè)新的 updateBlock壶唤,應(yīng)該就是視頻里提到的當(dāng)所有的 DataSource 都載入完時(shí),統(tǒng)一執(zhí)行之前的 update block
notifyBatchUpdate:
所做的是看一下 Delegate 是否響應(yīng)dataSource:performBatchUpdate:complete:
如果響應(yīng)則走你棕所,不然挨個(gè)執(zhí)行 update / complete闸盔。
看完了loadContentWithBlock
再來(lái)看看這個(gè) Block 里面都做了什么,大意是根據(jù) self.showingFavorites 來(lái)切換不同的數(shù)據(jù)源琳省,這里看到了一個(gè)新的類 AAPLDataAccessManager迎吵,看起來(lái)像是統(tǒng)一的數(shù)據(jù)層,瞄一眼
@classAAPLCat;@interfaceAAPLDataAccessManager:NSObject+(AAPLDataAccessManager*)manager;-(void)fetchCatListWithCompletionHandler:(void(^)(NSArray*cats,NSError*error))handler;-(void)fetchFavoriteCatListWithCompletionHandler:(void(^)(NSArray*cats,NSError*error))handler;-(void)fetchDetailForCat:(AAPLCat*)catcompletionHandler:(void(^)(AAPLCat*cat,NSError*error))handler;-(void)fetchSightingsForCat:(AAPLCat*)catcompletionHandler:(void(^)(NSArray*sightings,NSError*error))handler;@end
果然如此针贬,將來(lái)數(shù)據(jù)的載入形式有變化击费,或需要做緩存啥的,都可以在這一層處理桦他,其他部分不會(huì)感覺(jué)到變化蔫巩。
這一輪看下來(lái)已經(jīng)有不少信息量了,來(lái)簡(jiǎn)單捋一下:
[SegmentedDataSource setNeedsLoadContent]↓[CatListDataSource loadContent]↓[DataSource loadContentWithBlock:]↓
創(chuàng)建 loading,設(shè)置 loading 完成后要做的事 → 拿到數(shù)據(jù)后放到 updateQueue 里圆仔,等全部拿到再執(zhí)行 batchUpdate
↓
執(zhí)行 loadContentBlock → 使用 DataAccessManager 去獲取數(shù)據(jù)垃瞧,拿到后交給 loading
到這里,我們還沒(méi)有運(yùn)行 Project 看效果坪郭,因?yàn)槲矣X(jué)得代碼包含的信息會(huì)更豐富个从,而且這么看下來(lái)后,對(duì)于界面會(huì)長(zhǎng)啥樣也有個(gè)大概的了解歪沃。
這只是開(kāi)始嗦锐,繼續(xù)挖掘下去還會(huì)有不少好東西,比如 Favorite 按鈕的處理绸罗,它是通過(guò) Responder Chain 而不是 Delegate 來(lái)實(shí)現(xiàn)的意推,也是一個(gè)思路。通過(guò)有限狀態(tài)機(jī)來(lái)管理 loading 狀態(tài)也是很有意思的實(shí)現(xiàn)珊蟀。
如果有興趣菊值,可以看下 ComposedDataSource,先不看實(shí)現(xiàn)育灸,如果要自己寫大概會(huì)是什么思路腻窒,比如當(dāng)調(diào)用[UICollectionView cellForItemAtIndexPath:]
時(shí),如何找到對(duì)應(yīng)的 DataSource磅崭,找到之后如何渲染對(duì)應(yīng)的 Cell 等儿子。
所以看源碼真的是一件很有意思的事情,像一場(chǎng)冒險(xiǎn)砸喻,總是會(huì)有意外收獲柔逼,可能在不知不覺(jué)中,能力就得到了提升割岛。
版權(quán)歸原作者所有
原文地址:http://limboy.me/ios/2013/08/05/internal-implementation-of-kvo.html