架构设计中的循环引用

引用计数是一种经典的内存管理垃圾回收机制,但它最明显的反作用就是循环引用,致使内存泄漏。循环引用实际上是一个闭环。算法

闭环是什么

从图论的角度来讲,闭环,其实就是一个有向有环图。图中的顶点表示一个对象,每一条边表示对象之间的关系。若图中的每条边都是有方向的,则称为有向图。 若是图中存在一个顶点,从该顶点出发通过若干条边能够回到该点,则这个图是一个有向有环图。这个环咱们暂且称它为闭环。网络

除了引用计数内存管理,在工程应用中,不少地方都存在着闭环的影子:架构

  • 死锁,是指多个进程在执行过程当中,因为竞争资源或者因为彼此通讯而形成的一种阻塞的现象,若无外力做用,它们都将没法推动下去。死锁的造成必然存在着资源的环形链,这也是一个闭环。

闭环的检测

判断一个有向图是否存在环,有多种算法。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.

相关文章
相关标签/搜索