阅读源码的乐趣

阅读源码的乐趣

阅读源码尤其是优秀的源码是一件很有乐趣的事情,可以拓宽视野,提高品位,锻炼思维,就像间接地在跟作者沟通一样。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|-Categories|-DataSources|-Layouts|-ViewControllers|-Views|-Application

看来关键的信息都在 Framework 里了,那如何切入呢?反其道而行之,先来看看这些 Framework 是怎么用的,最直接的就从 ViewController 入手。那就先来看看 AAPLCatListViewController 这个类吧,如果没猜错的话,应该是展示喵咪列表(直观的名字很重要)。

果然很小,居然只有 140 行,如果不分离的话,1400 行也是可以轻松达到的。看到定义了一个 AAPLSegmentedDataSource,脑海里大概可以想象出是一个可以切换 Tag 的页面,接着又看到了两个 DataSource,那这两个页面的数据源应该就是它们了。

@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];}

看起来是苹果自己实现了一个 KVO Wrapper,果然他们自己也无法忍受原生的KVO,哈哈。接着到了 ViewDidLoad,新建了两个 DataSource,那新建的时候都干了些什么?

-(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;}

所以只是初始化,然后设置一些信息,Nothing Special。然后看到了 AAPLLayoutSectionMetrics ,看起来是设置 Layout 的一些显示信息,如 height / backgroundColor 之类的。

最后创建了一个 KVO 来监测 selectedDataSource 的变化,界面上做相应的调整。

接下来看看 AAPLCatListDataSource 的实现,一进去发现

@interfaceAAPLCatListDataSource:AAPLBasicDataSource/// Is this list showing the favorites or all available cats?@property(nonatomic)BOOLshowingFavorites;@end

看来 AAPLBasicDataSource 一定做了很多事,进入到 AAPLBasicDataSource.m 文件,看到这个方法

-(void)setShowingFavorites:(BOOL)showingFavorites{if(showingFavorites==_showingFavorites)return;_showingFavorites=showingFavorites;[selfresetContent];[selfsetNeedsLoadContent];if(showingFavorites)[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(observeFavoriteToggledNotification:)name:AAPLCatFavoriteToggledNotificationNameobject:nil];}

注意到有一个setNeedsLoadContent方法,看起来数据的载入应该是通过这个方法来触发的,进去看看

-(void)setNeedsLoadContent{[NSObjectcancelPreviousPerformRequestsWithTarget:selfselector:@selector(loadContent)object:nil];[selfperformSelector:@selector(loadContent)withObject:nilafterDelay:0];}

第一个方法没怎么接触过,查一下文档先,原来是可以取消之前通过performSelector:withObject:afterDelay:触发的方法,为了加深印象,顺便 Google 一下这个方法,原来performSelector:withObject:afterDelay在方法被执行前,会持有 Object,方法执行后在解除对 Object 的持有,如果不小心多次调用这个方法就有可能导致内存泄露,所以在调用此方法前先 cancel 一下是个好习惯。

再来看看这个loadContent都做了什么

-(void)loadContent{// To be implemented by subclasses…}

看来需要在子类实现这个方法,那就到 AAPLCatListDataSource 里看看这个方法都做了什么

-(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:方法,进去看看,这个方法做了什么

-(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);}

简单说来就是生成了一个 loading,然后把 loading 传给 block,那loadingWithCompletionHandler:这个方法又做了什么

+(instancetype)loadingWithCompletionHandler:(void(^)(NSString*state,NSError*error,AAPLLoadingUpdateBlockupdate))handler{NSParameterAssert(handler!=nil);AAPLLoading*loading=[[selfalloc]init];loading.block=handler;loading.current=YES;returnloading;}

所以就是生成一个 loading 实例,然后把 handler 存到 block 属性里。既然存了,那将来某个时候一定会用到,从名字上来看,应该是 loading 完成时会被调用,搜索 block 关键字,发现只有在下面这个方法中 block 才会被调用

-(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;}

既然是 _ 开头,那应该是内部方法,对外封装了几种状态,如ignore,done,updateWithContent:等。

咦,这里为什么要先把*block 赋给一个临时变量 block,然后再把 _block 设为 nil呢?看起来像是为了解决某种内存问题。如果直接 *block(newState, error, update) 会怎样?哦,虽然这里没有出现 self,但 _block 是一个 instance 变量,所以在 ^{} 里会对 self 进行强引用。而如果赋给一个临时变量,那么只会对这个临时变量强引用,就不会出现循环引用的情况。

AAPLLoading 看的差不多了,再出来看loadContentWithBlock:,注意到在 CompletionHandler 里,有这么一段

[selfendLoadingWithState:newStateerror:errorupdate:^{AAPLDataSource*me=weakself;if(update&&me)update(me);}];

这里的 self 是 AAPLDataSource (Block嵌套多了,还真是容易晕啊),来看看endLoadingWithState:error:update这个方法都做了什么

-(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];}

设置一些状态,然后在恰当的时机调用 update block,咦,这里有个 dispatchblockt 没怎么见过,查了一下原来是一个内置的空传值和空返回的block。

看了下enqueuePendingUpdateBlock,会把现在的这个 update 结合之前的 updateBlock,形成一个新的 updateBlock,应该就是视频里提到的当所有的 DataSource 都载入完时,统一执行之前的 update block

notifyBatchUpdate:所做的是看一下 Delegate 是否响应dataSource:performBatchUpdate:complete:如果响应则走你,不然挨个执行 update / complete。

看完了loadContentWithBlock再来看看这个 Block 里面都做了什么,大意是根据 self.showingFavorites 来切换不同的数据源,这里看到了一个新的类 AAPLDataAccessManager,看起来像是统一的数据层,瞄一眼

@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

果然如此,将来数据的载入形式有变化,或需要做缓存啥的,都可以在这一层处理,其他部分不会感觉到变化。

这一轮看下来已经有不少信息量了,来简单捋一下:

[SegmentedDataSource setNeedsLoadContent]↓[CatListDataSource loadContent]↓[DataSource loadContentWithBlock:]↓
创建 loading,设置 loading 完成后要做的事 → 拿到数据后放到 updateQueue 里,等全部拿到再执行 batchUpdate
                ↓
执行 loadContentBlock → 使用 DataAccessManager 去获取数据,拿到后交给 loading

到这里,我们还没有运行 Project 看效果,因为我觉得代码包含的信息会更丰富,而且这么看下来后,对于界面会长啥样也有个大概的了解。

这只是开始,继续挖掘下去还会有不少好东西,比如 Favorite 按钮的处理,它是通过 Responder Chain 而不是 Delegate 来实现的,也是一个思路。通过有限状态机来管理 loading 状态也是很有意思的实现。

如果有兴趣,可以看下 ComposedDataSource,先不看实现,如果要自己写大概会是什么思路,比如当调用[UICollectionView cellForItemAtIndexPath:]时,如何找到对应的 DataSource,找到之后如何渲染对应的 Cell 等。

所以看源码真的是一件很有意思的事情,像一场冒险,总是会有意外收获,可能在不知不觉中,能力就得到了提升。

版权归原作者所有
原文地址:http://limboy.me/ios/2013/08/05/internal-implementation-of-kvo.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,073评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,946评论 4 60
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,679评论 6 342
  • 我微笑 它便微笑 我哭泣 它陪我哭泣 哪怕啊 最后还要我 去擦干它的...
    孙浒胡阅读 96评论 0 0
  • 第三十三章评估结果的逆转这一章节引出了单一评估与联合评估,涉及到的匹配强度的问题,又一次的回到前面讲的内容,奚恺元...
    我叫黄小贱阅读 575评论 0 0