如何在 iOS 中解决循环引用的问题
稍有常识的人都知道在 iOS 开发时,我们经常会遇到循环引用的问题,比如两个强指针相互引用,但是这种简单的情况作为稍有经验的开发者都会轻松地查找出来。
但是遇到下面这样的情况,如果只看其实现代码,也很难仅仅凭借肉眼上的观察以及简单的推理就能分析出其中存在的循环引用问题,更何况真实情况往往比这复杂的多:
testObject1.object = testObject2;
testObject1.secondObject = testObject3;
testObject2.object = testObject4;
testObject2.secondObject = testObject5;
testObject3.object = testObject1;
testObject5.object = testObject6;
testObject4.object = testObject1;
testObject5.secondObject = testObject7;
testObject7.object = testObject2;
上述代码确实是存在循环引用的问题:
detector-retain-objects
这一次分享的内容就是用于检测循环引用的框架 FBRetainCycleDetector 我们会分几个部分来分析 FBRetainCycleDetector 是如何工作的:
检测循环引用的基本原理以及过程
检测设计 NSObject 对象的循环引用问题
检测涉及 Associated Object 关联对象的循环引用问题
检测涉及 Block 的循环引用问题
这是四篇文章中的第一篇,我们会以类 FBRetainCycleDetector 的 - findRetainCycles 方法为入口,分析其实现原理以及运行过程。
简单介绍一下 FBRetainCycleDetector 的使用方法:
_RCDTestClass *testObject = [_RCDTestClass new];
testObject.object = testObject;
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:testObject];
NSSet *retainCycles = [detector findRetainCycles];
NSLog(@"%@", retainCycles);
初始化一个 FBRetainCycleDetector 的实例
调用 - addCandidate: 方法添加潜在的泄露对象
执行 - findRetainCycles 返回 retainCycles
在控制台中的输出是这样的:
2016-07-29 15:26:42.043 xctest[30610:1003493] {(
(
"-> _object -> _RCDTestClass "
)
)}
说明 FBRetainCycleDetector 在代码中发现了循环引用。
findRetainCycles 的实现
在具体开始分析 FBRetainCycleDetector 代码之前,我们可以先观察一下方法 findRetainCycles 的调用栈:
- (NSSet *> *)findRetainCycles
└── - (NSSet *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length
└── - (NSSet *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement stackDepth:(NSUInteger)stackDepth
└── - (instancetype)initWithObject:(FBObjectiveCGraphElement *)object
└── - (FBNodeEnumerator *)nextObject
├── - (NSArray *)_unwrapCycle:(NSArray *)cycle
├── - (NSArray *)_shiftToUnifiedCycle:(NSArray *)array
└── - (void)addObject:(ObjectType)anObject;
调用栈中最上面的两个简单方法的实现都是比较容易理解的:
- (NSSet *> *)findRetainCycles {
return [self findRetainCyclesWithMaxCycleLength:kFBRetainCycleDetectorDefaultStackDepth];
}
- (NSSet *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length {
NSMutableSet *> *allRetainCycles = [NSMutableSet new];
for (FBObjectiveCGraphElement *graphElement in _candidates) {
NSSet *> *retainCycles = [self _findRetainCyclesInObject:graphElement
stackDepth:length];
[allRetainCycles unionSet:retainCycles];
}
[_candidates removeAllObjects];
return allRetainCycles;
}
- findRetainCycles 调用了 - findRetainCyclesWithMaxCycleLength: 传入了 kFBRetainCycleDetectorDefaultStackDepth 参数来限制查找的深度,如果超过该深度(默认为 10)就不会继续处理下去了(查找的深度的增加会对性能有非常严重的影响)。
在 - findRetainCyclesWithMaxCycleLength: 中,我们会遍历所有潜在的内存泄露对象 candidate,执行整个框架中最核心的方法 - _findRetainCyclesInObject:stackDepth:,由于这个方法的实现太长,这里会分几块对其进行介绍,并会省略其中的注释:
- (NSSet *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement
stackDepth:(NSUInteger)stackDepth {
NSMutableSet *> *retainCycles = [NSMutableSet new];
FBNodeEnumerator *wrappedObject = [[FBNodeEnumerator alloc] initWithObject:graphElement];
NSMutableArray *stack = [NSMutableArray new];
NSMutableSet *objectsOnPath = [NSMutableSet new];
...
}
其实整个对象的相互引用情况可以看做一个有向图,对象之间的引用就是图的 Edge,每一个对象就是 Vertex,查找循环引用的过程就是在整个有向图中查找环的过程,所以在这里我们使用 DFS 来扫面图中的环,这些环就是对象之间的循环引用。
文章中并不会介绍 DFS 的原理,如果对 DFS 不了解的读者可以看一下这个视频,或者找以下相关资料了解一下 DFS 的实现。
接下来就是 DFS 的实现:
- (NSSet *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement
stackDepth:(NSUInteger)stackDepth {
...
[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]) {
NSUInteger index = [stack indexOfObject:firstAdjacent];
NSInteger length = [stack count] - index;
if (index == NSNotFound) {
shouldPushToStack = YES;
} else {
NSRange cycleRange = NSMakeRange(index, length);
NSMutableArray *cycle = [[stack subarrayWithRange:cycleRange] mutableCopy];
[cycle replaceObjectAtIndex:0 withObject:firstAdjacent];
[retainCycles addObject:[self _shiftToUnifiedCycle:[self _unwrapCycle:cycle]]];
}
} else {
shouldPushToStack = YES;
}
if (shouldPushToStack) {
if ([stack count]
这里其实就是对 DFS 的具体实现,其中比较重要的有两点,一是使用 nextObject 获取下一个需要遍历的对象,二是对查找到的环进行处理和筛选;在这两点之中,第一点相对重要,因为 nextObject 的实现是调用 allRetainedObjects 方法获取被当前对象持有的对象,如果没有这个方法,我们就无法获取当前对象的邻接结点,更无从谈起遍历了:
- (FBNodeEnumerator *)nextObject {
if (!_object) {
return nil;
} else if (!_retainedObjectsSnapshot) {
_retainedObjectsSnapshot = [_object allRetainedObjects];
_enumerator = [_retainedObjectsSnapshot objectEnumerator];
}
FBObjectiveCGraphElement *next = [_enumerator nextObject];
if (next) {
return [[FBNodeEnumerator alloc] initWithObject:next];
}
return nil;
}
基本上所有图中的对象 FBObjectiveCGraphElement 以及它的子类 FBObjectiveCBlock FBObjectiveCObject 和 FBObjectiveCNSCFTimer 都实现了这个方法返回其持有的对象数组。获取数组之后,就再把其中的对象包装成新的 FBNodeEnumerator 实例,也就是下一个 Vertex。
因为使用 - subarrayWithRange: 方法获取的数组中的对象都是 FBNodeEnumerator 的实例,还需要一定的处理才能返回:
(NSArray)_unwrapCycle:(NSArray> *)cycle
(NSArray)_shiftToUnifiedCycle:(NSArray> *)array
- _unwrapCycle: 的作用是将数组中的每一个 FBNodeEnumerator 实例转换成 FBObjectiveCGraphElement:
- (NSArray *)_unwrapCycle:(NSArray *)cycle {
NSMutableArray *unwrappedArray = [NSMutableArray new];
for (FBNodeEnumerator *wrapped in cycle) {
[unwrappedArray addObject:wrapped.object];
}
return unwrappedArray;
}
- _shiftToUnifiedCycle: 方法将每一个环中的元素按照地址递增以及字母顺序来排序,方法签名很好的说明了它们的功能,两个方法的代码就不展示了,它们的实现没有什么值得注意的地方:
- (NSArray *)_shiftToUnifiedCycle:(NSArray *)array {
return [self _shiftToLowestLexicographically:[self _shiftBufferToLowestAddress:array]];
}
方法的作用是防止出现相同环的不同表示方式,比如说下面的两个环其实是完全相同的:
-> object1 -> object2
-> object2 -> object1
在获取图中的环并排序好之后,就可以讲这些环 union 一下,去除其中重复的元素,最后返回所有查找到的循环引用了。
总结
到目前为止整个 FBRetainCycleDetector 的原理介绍大概就结束了,其原理完全是基于 DFS 算法:把整个对象的之间的引用情况当做图进行处理,查找其中的环,就找到了循环引用。不过原理真的很简单,如果这个 lib 的实现仅仅是这样的话,我也不会写几篇文章来专门分析这个框架,真正让我感兴趣的还是 - allRetainedObjects 方法在各种对象以及 block 中获得它们强引用的对象的过程,这也是之后的文章要分析的主要内容。
如何在 iOS 中解决循环引用的问题的更多相关文章
- swift闭包中解决循环引用的问题
swift中可以通过三种方法解决循环引用的问题 利用类似oc方法解决循环引用weak var weakSelf = self weak var weakSelf = self loadData = { ...
- ios 中的循环引用问题及解决
循环引用,指的是多个对象相互引用时,使得引用形成一个环形,导致外部无法真正是否掉这块环形内存.其实有点类似死锁. 举个例子:A->B->C->....->X->B - ...
- iOS中Block循环引用的问题
说到循环引用问题,想必大家都碰到过吧,比如在使用Block的时候,使用__weakSelf来代替self解决等,但是对于这个,还是有不少可以探索的点,下面我就来说下,希望对大家有所帮助. 是否所有的B ...
- iOS中block循环引用问题
1.block是控制器对象的一个属性,则在block内部使用self将会引起循环应用 typedef void(^TestBlock)(); @interface SecondViewControll ...
- iOS容易造成循环引用的三种场景
iOS容易造成循环引用的三种场景 ARC已经出来很久了,自动释放内存的确很方便,但是并非绝对安全绝对不会产生内存泄露.导致iOS对象无法按预期释放的一个无形杀手是--循环引用.循环引用可以简单理解为 ...
- 深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用(上)
深入研究Block捕获外部变量和__block实现原理 前言 在上篇中,仔细分析了一下Block的实现原理以及__block捕获外部变量的原理.然而实际使用Block过程中,还是会遇到一些问题,比如R ...
- Swift-闭包使用及解决循环引用问题
Swift中闭包使用参考OC中block使用,基本一致 // 闭包类型 首先写(参数列表)->(返回值类型) func loadData(callBack : (jsonData:String) ...
- Flask-分开Models解决循环引用
Flask-分开Models解决循环引用 在之前我们测试中,所有语句都在同一个文件中,但随着项目越来越大,管理起来有所不便,所以将Models分离.基本的文件结构如下 \—–app.py\—–mode ...
- 【C++】智能指针简述(五):解决循环引用的weak_ptr
总结一下前文内容: 1.智能指针通过RAII方法来管理指针:构造对象时,完成资源初始化;析构对象时,对资源进行清理及汕尾. 2.auto_ptr,通过“转移所有权”来防止析构一块内存多次.(如何转移? ...
随机推荐
- 彩色网页变黑白色CSS代码变黑白色调!
<style> html { -webkit-filter: grayscale(%); -moz-filter: grayscale(%); -ms-filter: grayscale( ...
- JavaScript基础大全篇
本章内容: 简介 定义 注释 引入文件 变量 运算符 算术运算符 比较运算符 逻辑运算符 数据类型 数字 字符串 布尔类型 数组 Math 语句 条件语句(if.switch) 循环语句(for.fo ...
- 变形虫mysql的负载均衡 读写分离
变形虫概述 图片来自 http://docs.hexnova.com/amoeba/amoeba-products.html 文档上说也可以做mongdb的分布式. 应用: 具有负载均衡.高可用 ...
- 《Python基础教程(第二版)》学习笔记 -> 第三章 使用字符串
本章讲话介绍如何使用字符串格式化其他的值,并简单了解一下利用字符串的分割.联接.搜索等方法能做些什么. 基本字符串操作 所有标准的序列操作(索引.分片.乘法.判断成员资格.求长度.取最大最小值)对字符 ...
- MAC下显示或者隐藏文件的命令
显示Mac隐藏文件的命令:defaults write com.apple.finder AppleShowAllFiles -bool true 隐藏Mac隐藏文件的命令:defaults writ ...
- cloudera manager 及CDH卸载
记录用户数据路径 删除用户数据 中列出的用户数据路径 /var/lib/flume-ng /var/lib/hadoop* /var/lib/hue /var/lib/navigator /var/l ...
- 神经网络的学习 Neural Networks learing
1.一些基本符号 2.COST函数 ================Backpropagation Algorithm============= 1.要计算的东西 2.向前传递向量图,但为了计算上图的 ...
- bzoj 1412 [ZJOI2009]狼和羊的故事(最小割)
[题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=1412 [题意] 在一个n*m的格子中,将羊和狼隔开的最小代价. [思路] 最小割. 由 ...
- xampp配置host和httpd可以随意访问任何本机的地址
1.修改host 不管你用的是什么系统,windows, mac,电脑上都会有一个 hosts 文件,修改这个文件,可以改变主机名所对应的 ip 地址.比如你安装了 Web 开发环境(MAMP 或 W ...
- JQuery上传插件Uploadify API详解
一.相关key值介绍uploader:uploadify.swf文件的相对路径,该swf文件是一个带有文字BROWSE的按钮,点击后淡出打开文件对话框,默认值:uploadify.swf. scrip ...