架構設計中的循環引用

引用計數是一種經典的內存管理垃圾回收機制,但它最明顯的反作用就是循環引用,致使內存泄漏。循環引用實際上是一個閉環。算法

閉環是什麼

從圖論的角度來講,閉環,其實就是一個有向有環圖。圖中的頂點表示一個對象,每一條邊表示對象之間的關係。若圖中的每條邊都是有方向的,則稱爲有向圖。 若是圖中存在一個頂點,從該頂點出發通過若干條邊能夠回到該點,則這個圖是一個有向有環圖。這個環咱們暫且稱它爲閉環。網絡

除了引用計數內存管理,在工程應用中,不少地方都存在着閉環的影子:架構

  • 死鎖,是指多個進程在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。死鎖的造成必然存在着資源的環形鏈,這也是一個閉環。

閉環的檢測

判斷一個有向圖是否存在環,有多種算法。app

  • 算法1:環中的每一個頂點的度都是>=2,基於此能夠找到環:
    1. 求出圖中全部頂點的度,
    2. 刪除圖中全部度<=1的頂點以及與該頂點相關的邊,把與這些邊相關的頂點的度減一
    3. 若是還有度<=1的頂點重複步驟2
    4. 最後若是還存在未被刪除的頂點,則表示有環;不然沒有環
  • 算法2: 利用深度優先搜索遍歷該圖,若是在遍歷的過程當中,發現某個節點有一條邊指向同一棵生成樹上的祖先節點,則表示存在環: dfs.jpg

facebook最近剛開源的FBRetainCycleDetector就是使用深度優先搜索來檢測iOS應用在運行時是否存在循環應用。它利用ivar layout 找出對象所引用的全部對象,並用有向圖表示,節點就是對象,邊就是對象之間的引用。而後使用深度優先搜索,檢測出其中是否存在循環引用,精簡過的核心代碼以下:spa

[stack addObject:wrappedObject];
while ([stack count] > 0) {
  @autoreleasepool {
    FBNodeEnumerator *top = [stack lastObject];
    [objectsOnPath addObject:top];
    FBNodeEnumerator *firstAdjacent = [top nextObject];
    if (firstAdjacent) {
      BOOL shouldPushToStack = NO;
      if ([objectsOnPath containsObject:firstAdjacent]) {
       // 若是路徑中存在訪問過的點,說明存在環,代表有內存泄漏隱患
      } else {
        shouldPushToStack = YES;
      }
      if (shouldPushToStack) {
          [stack addObject:firstAdjacent];
      }
    } else {
      [stack removeLastObject];
      [objectsOnPath removeObject:top];      
    }
  }
}複製代碼

架構設計應避免閉環

架構的設計當中,大部分場景下, 應當避免閉環。模塊之間的關係應當是線性的,或者樹狀的,而不該該是存在閉環的複雜網絡狀。樹狀的結構意味着系統的層次清晰,模塊職責明確。典型的在線服務三層架構圖以下:架構設計

這就是一個很簡單的樹狀圖,模塊之間劃分得很是清晰。流行的MVC和MVVP其實也是幫咱們梳理這種層次結構。MVC三者的引用關係以下(Controller引用View和Model,View引用Model):設計

這是一個無環圖,假設在圖中增長一條從Model到Controller的邊,就造成了一個閉環。若是你的Model層或者View層代碼中出現了相似這樣的代碼:import "Controller.h",請再審視一遍你的代碼設計。 模塊之間相互引用,意味着模塊之間耦合性高,設計的抽象還不夠。在平常的實踐開發,這種相互引用的引入每每是無意而隱蔽的。可是隨着代碼庫的日益龐大,模塊之間的聯繫日益增多,一旦這種循環引用多了,整個系統的結構將變得無比複雜,雜亂無章。最終系統的結構會變成這樣:code

這個系統一旦出現bug,將牽一髮而動全身,改bug就是拆了東牆補西牆,長此以往,再也沒有人改動這坨代碼,項目華麗地走上了推倒重寫之路。cdn

那麼如何找到模塊之間是否存在閉環呢?咱們能夠將問題簡化成爲文件之間是否存在循環import。這樣能夠寫一個檢測腳本,協助定位和清理模塊之間的關係。對象

  1. 遍歷項目中全部的文件,看每一個文件import了哪些其餘的文件。每一個文件就是一個節點,每次import表示一次引用關係,從而創建一條有向邊。
  2. 創建好有向圖以後,使用深度優先搜索算法檢測圖中是否存在環,若存在,將引用路徑打印出來。

讓代碼庫變得龐大並不難,難的是在業務發展過程當中,一直保持架構的簡單。牢記KISS原則:Keep It Simple and Stupid.

相關文章
相關標籤/搜索