閱讀源碼的樂趣

jopen 9年前發布 | 11K 次閱讀 源碼

閱讀源碼尤其是優秀的源碼是一件很有樂趣的事情,可以拓寬視野,提高品位,鍛煉思維,就像間接地在跟作者溝通一樣。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 等。

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

      原文出處: Limboy 

       本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
       轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
       本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!