写点什么

检测 NSObject 对象持有的强指针

  • 2019-12-10
  • 本文字数:7022 字

    阅读完需:约 23 分钟

检测 NSObject 对象持有的强指针

关注仓库,及时获得更新:iOS-Source-Code-Analyze


在上一篇文章中介绍了 FBRetainCycleDetector 的基本工作原理,这一篇文章中我们开始分析它是如何从每一个对象中获得它持有的强指针的。


如果没有看第一篇文章这里还是最好看一下,了解一下 FBRetainCycleDetector 的工作原理,如何在 iOS 中解决循环引用的问题


FBRetainCycleDetector 获取对象的强指针是通过 FBObjectiveCObject 类的 - allRetainedObjects 方法,这一方法是通过其父类 FBObjectiveCGraphElement 继承过来的,只是内部有着不同的实现。

allRetainedObjects 方法

我们会以 XXObject 为例演示 - allRetainedObjects 方法的调用过程:


Objective-C


#import <Foundation/Foundation.h>
@interface XXObject : NSObject
@property (nonatomic, strong) id first;@property (nonatomic, weak) id second;@property (nonatomic, strong) id third;@property (nonatomic, strong) id forth;@property (nonatomic, weak) id fifth;@property (nonatomic, strong) id sixth;
@end
复制代码


使用 FBRetainCycleDetector 的代码如下:


Objective-C


XXObject *object = [[XXObject alloc] init];
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];[detector addCandidate:object];__unused NSSet *cycles = [detector findRetainCycles];
复制代码


FBObjectiveCObject 中,- allRetainedObjects 方法只是调用了 - _unfilteredRetainedObjects,然后进行了过滤,文章主要会对 - _unfilteredRetainedObjects 的实现进行分析:


Objective-C


- (NSSet *)allRetainedObjects {  NSArray *unfiltered = [self _unfilteredRetainedObjects];  return [self filterObjects:unfiltered];}
复制代码


方法 - _unfilteredRetainedObjects 的实现代码还是比较多的,这里会将代码分成几个部分,首先是最重要的部分:如何得到对象持有的强引用:


Objective-C


- (NSArray *)_unfilteredRetainedObjects  NSArray *strongIvars = FBGetObjectStrongReferences(self.object, self.configuration.layoutCache);
NSMutableArray *retainedObjects = [[[super allRetainedObjects] allObjects] mutableCopy];
for (id<FBObjectReference> ref in strongIvars) { id referencedObject = [ref objectReferenceFromObject:self.object];
if (referencedObject) { NSArray<NSString *> *namePath = [ref namePath]; FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, referencedObject, self.configuration, namePath); if (element) { [retainedObjects addObject:element]; } } }
...}
复制代码


获取强引用是通过 FBGetObjectStrongReferences 这一函数:


Objective-C


NSArray<id<FBObjectReference>> *FBGetObjectStrongReferences(id obj,                              NSMutableDictionary<Class, NSArray<id<FBObjectReference>> *> *layoutCache) {  NSMutableArray<id<FBObjectReference>> *array = [NSMutableArray new];
__unsafe_unretained Class previousClass = nil; __unsafe_unretained Class currentClass = object_getClass(obj);
while (previousClass != currentClass) { NSArray<id<FBObjectReference>> *ivars;
if (layoutCache && currentClass) { ivars = layoutCache[currentClass]; }
if (!ivars) { ivars = FBGetStrongReferencesForClass(currentClass); if (layoutCache && currentClass) { layoutCache[(id<NSCopying>)currentClass] = ivars; } } [array addObjectsFromArray:ivars];
previousClass = currentClass; currentClass = class_getSuperclass(currentClass); }
return [array copy];}
复制代码


上面代码的核心部分是执行 FBGetStrongReferencesForClass 返回 currentClass 中的强引用,只是在这里我们递归地查找了所有父类的指针,并且加入了缓存以加速查找强引用的过程,接下来就是从对象的结构中获取强引用的过程了:


Objective-C


static NSArray<id<FBObjectReference>> *FBGetStrongReferencesForClass(Class aCls) {  NSArray<id<FBObjectReference>> *ivars = [FBGetClassReferences(aCls) filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {    if ([evaluatedObject isKindOfClass:[FBIvarReference class]]) {      FBIvarReference *wrapper = evaluatedObject;      return wrapper.type != FBUnknownType;    }    return YES;  }]];
const uint8_t *fullLayout = class_getIvarLayout(aCls);
if (!fullLayout) { return nil; }
NSUInteger minimumIndex = FBGetMinimumIvarIndex(aCls); NSIndexSet *parsedLayout = FBGetLayoutAsIndexesForDescription(minimumIndex, fullLayout);
NSArray<id<FBObjectReference>> *filteredIvars = [ivars filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id<FBObjectReference> evaluatedObject, NSDictionary *bindings) { return [parsedLayout containsIndex:[evaluatedObject indexInIvarLayout]]; }]];
return filteredIvars;}
复制代码


该方法的实现大约有三个部分:


  1. 调用 FBGetClassReferences 从类中获取它指向的所有引用,无论是强引用或者是弱引用

  2. 调用 FBGetLayoutAsIndexesForDescription 从类的变量布局中获取强引用的位置信息

  3. 使用 NSPredicate 过滤数组中的弱引用

获取类的 Ivar 数组

FBGetClassReferences 方法主要调用 runtime 中的 class_copyIvarList 得到类的所有 ivar


这里省略对结构体属性的处理,因为太过复杂,并且涉及大量的 C++ 代码,有兴趣的读者可以查看 FBGetReferencesForObjectsInStructEncoding 方法的实现。


Objective-C


NSArray<id<FBObjectReference>> *FBGetClassReferences(Class aCls) {  NSMutableArray<id<FBObjectReference>> *result = [NSMutableArray new];
unsigned int count; Ivar *ivars = class_copyIvarList(aCls, &count);
for (unsigned int i = 0; i < count; ++i) { Ivar ivar = ivars[i]; FBIvarReference *wrapper = [[FBIvarReference alloc] initWithIvar:ivar]; [result addObject:wrapper]; } free(ivars);
return [result copy];}
复制代码


上述实现还是非常直接的,遍历 ivars 数组,使用 FBIvarReference 将其包装起来然后加入 result 中,其中的类 FBIvarReference 仅仅起到了一个包装的作用,将 Ivar 中保存的各种属性全部保存起来:


Objective-C


typedef NS_ENUM(NSUInteger, FBType) {  FBObjectType,  FBBlockType,  FBStructType,  FBUnknownType,};
@interface FBIvarReference : NSObject <FBObjectReference>
@property (nonatomic, copy, readonly, nullable) NSString *name;@property (nonatomic, readonly) FBType type;@property (nonatomic, readonly) ptrdiff_t offset;@property (nonatomic, readonly) NSUInteger index;@property (nonatomic, readonly, nonnull) Ivar ivar;
- (nonnull instancetype)initWithIvar:(nonnull Ivar)ivar;
@end
复制代码


包括属性的名称、类型、偏移量以及索引,类型是通过类型编码来获取的,在 FBIvarReference 的实例初始化时,会通过私有方法 - _convertEncodingToType: 将类型编码转换为枚举类型:


Objective-C


- (FBType)_convertEncodingToType:(const char *)typeEncoding {  if (typeEncoding[0] == '{') return FBStructType;
if (typeEncoding[0] == '@') { if (strncmp(typeEncoding, "@?", 2) == 0) return FBBlockType; return FBObjectType; }
return FBUnknownType;}
复制代码


当代码即将从 FBGetClassReferences 方法中返回时,使用 lldb 打印 result 中的所有元素:



上述方法成功地从 XXObject 类中获得了正确的属性数组,不过这些数组中不止包含了强引用,还有被 weak 标记的弱引用:


Objective-C


<__NSArrayM 0x7fdac0f31860>(  [_first,  index: 1],  [_second, index: 2],  [_third,  index: 3],  [_forth,  index: 4],  [_fifth,  index: 5],  [_sixth,  index: 6])
复制代码

获取 Ivar Layout

当我们取出了 XXObject 中所有的属性之后,还需要对其中的属性进行过滤;那么我们如何判断一个属性是强引用还是弱引用呢?Objective-C 中引入了 Ivar Layout 的概念,对类中的各种属性的强弱进行描述。


它是如何工作的呢,我们先继续执行 FBGetStrongReferencesForClass 方法:



在 ObjC 运行时中的 class_getIvarLayout 可以获取某一个类的 Ivar Layout,而 XXObject 的 Ivar Layout 是什么样的呢?


C


(lldb) po fullLayout"\x01\x12\x11"
复制代码


Ivar Layout 就是一系列的字符,每两个一组,比如 \xmn,每一组 Ivar Layout 中第一位表示有 m 个非强属性,第二位表示接下来有 n 个强属性;如果没有明白,我们以 XXObject 为例演示一下:


Objective-C


@interface XXObject : NSObject
@property (nonatomic, strong) id first;@property (nonatomic, weak) id second;@property (nonatomic, strong) id third;@property (nonatomic, strong) id forth;@property (nonatomic, weak) id fifth;@property (nonatomic, strong) id sixth;
@end
复制代码


  • 第一组的 \x01 表示有 0 个非强属性,然后有 1 个强属性 first

  • 第二组的 \x12 表示有 1 个非强属性 second,然后有 2 个强属性 third forth

  • 第三组的 \x11 表示有 1 个非强属性 fifth, 然后有 1 个强属性 sixth


在对 Ivar Layout 有一定了解之后,我们可以继续对 FBGetStrongReferencesForClass 分析了,下面要做的就是使用 Ivar Layout 提供的信息过滤其中的所有非强引用,而这就需要两个方法的帮助,首先需要 FBGetMinimumIvarIndex 方法获取变量索引的最小值:


Objective-C


static NSUInteger FBGetMinimumIvarIndex(__unsafe_unretained Class aCls) {  NSUInteger minimumIndex = 1;  unsigned int count;  Ivar *ivars = class_copyIvarList(aCls, &count);
if (count > 0) { Ivar ivar = ivars[0]; ptrdiff_t offset = ivar_getOffset(ivar); minimumIndex = offset / (sizeof(void *)); }
free(ivars);
return minimumIndex;}
复制代码


然后执行 FBGetLayoutAsIndexesForDescription(minimumIndex, fullLayout) 获取所有强引用的 NSRange


Objective-C


static NSIndexSet *FBGetLayoutAsIndexesForDescription(NSUInteger minimumIndex, const uint8_t *layoutDescription) {  NSMutableIndexSet *interestingIndexes = [NSMutableIndexSet new];  NSUInteger currentIndex = minimumIndex;
while (*layoutDescription != '\x00') { int upperNibble = (*layoutDescription & 0xf0) >> 4; int lowerNibble = *layoutDescription & 0xf;
currentIndex += upperNibble; [interestingIndexes addIndexesInRange:NSMakeRange(currentIndex, lowerNibble)]; currentIndex += lowerNibble;
++layoutDescription; }
return interestingIndexes;}
复制代码


因为高位表示非强引用的数量,所以我们要加上 upperNibble,然后 NSMakeRange(currentIndex, lowerNibble) 就是强引用的范围;略过 lowerNibble 长度的索引,移动 layoutDescription 指针,直到所有的 NSRange 都加入到了 interestingIndexes 这一集合中,就可以返回了。

过滤数组中的弱引用

在上一阶段由于已经获取了强引用的范围,在这里我们直接使用 NSPredicate 谓词来进行过滤就可以了:


Objective-C


NSArray<id<FBObjectReference>> *filteredIvars =[ivars filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id<FBObjectReference> evaluatedObject,                                     NSDictionary *bindings) {  return [parsedLayout containsIndex:[evaluatedObject indexInIvarLayout]];}]];
复制代码



====


接下来,我们回到文章开始的 - _unfilteredRetainedObjects 方法:


Objective-C


- (NSSet *)allRetainedObjects {  NSArray *strongIvars = FBGetObjectStrongReferences(self.object, self.configuration.layoutCache);
NSMutableArray *retainedObjects = [[[super allRetainedObjects] allObjects] mutableCopy];
for (id<FBObjectReference> ref in strongIvars) { id referencedObject = [ref objectReferenceFromObject:self.object];
if (referencedObject) { NSArray<NSString *> *namePath = [ref namePath]; FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, referencedObject, self.configuration, namePath); if (element) { [retainedObjects addObject:element]; } } }
...}
复制代码


FBGetObjectStrongReferences 只是返回 id<FBObjectReference> 对象,还需要 FBWrapObjectGraphElementWithContext 把它进行包装成 FBObjectiveCGraphElement


Objective-C


FBObjectiveCGraphElement *FBWrapObjectGraphElementWithContext(id object,                                FBObjectGraphConfiguration *configuration,                                NSArray<NSString *> *namePath) {  if (FBObjectIsBlock((__bridge void *)object)) {    return [[FBObjectiveCBlock alloc] initWithObject:object                       configuration:configuration                        namePath:namePath];  } else {    if ([object_getClass(object) isSubclassOfClass:[NSTimer class]] &&      configuration.shouldInspectTimers) {      return [[FBObjectiveCNSCFTimer alloc] initWithObject:object                           configuration:configuration                            namePath:namePath];    } else {      return [[FBObjectiveCObject alloc] initWithObject:object                        configuration:configuration                           namePath:namePath];    }  }}
复制代码


最后会把封装好的实例添加到 retainedObjects 数组中。


- _unfilteredRetainedObjects 同时也要处理集合类,比如数组或者字典,但是如果是无缝桥接的 CF 集合,或者是元类,虽然它们可能遵循 NSFastEnumeration 协议,但是在这里并不会对它们进行处理:


Objective-C


- (NSArray *)_unfilteredRetainedObjects {  ...
if ([NSStringFromClass(aCls) hasPrefix:@"__NSCF"]) { return retainedObjects; }
if (class_isMetaClass(aCls)) { return nil; }
...}
复制代码


在遍历内容时,Mutable 的集合类中的元素可能会改变,所以会重试多次以确保集合类中的所有元素都被获取到了:


Objective-C


- (NSArray *)_unfilteredRetainedObjects {  ...
if ([aCls conformsToProtocol:@protocol(NSFastEnumeration)]) {
NSInteger tries = 10; for (NSInteger i = 0; i < tries; ++i) { NSMutableSet *temporaryRetainedObjects = [NSMutableSet new]; @try { for (id subobject in self.object) { [temporaryRetainedObjects addObject:FBWrapObjectGraphElement(subobject, self.configuration)]; [temporaryRetainedObjects addObject:FBWrapObjectGraphElement([self.object objectForKey:subobject], self.configuration)]; } } @catch (NSException *exception) { continue; }
[retainedObjects addObjectsFromArray:[temporaryRetainedObjects allObjects]]; break; } }
return retainedObjects;}
复制代码


这里将遍历集合中的元素的代码放入了 @try 中,如果在遍历时插入了其它元素,就会抛出异常,然后 continue 重新遍历集合,最后返回所有持有的对象。


最后的过滤部分会使用 FBObjectGraphConfiguration 中的 filterBlocks 将不需要加入集合中的元素过滤掉:


Objective-C


- (NSSet *)filterObjects:(NSArray *)objects {  NSMutableSet *filtered = [NSMutableSet new];
for (FBObjectiveCGraphElement *reference in objects) { if (![self _shouldBreakGraphEdgeFromObject:self toObject:reference]) { [filtered addObject:reference]; } }
return filtered;}
- (BOOL)_shouldBreakGraphEdgeFromObject:(FBObjectiveCGraphElement *)fromObject toObject:(FBObjectiveCGraphElement *)toObject { for (FBGraphEdgeFilterBlock filterBlock in _configuration.filterBlocks) { if (filterBlock(fromObject, toObject) == FBGraphEdgeInvalid) return YES; }
return NO;}
复制代码

总结

FBRetainCycleDetector 在对象中查找强引用取决于类的 Ivar Layout,它为我们提供了与属性引用强弱有关的信息,帮助筛选强引用。


本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/retain-cycle2


2019-12-10 17:56853

评论

发布
暂无评论
发现更多内容

1. 商品CPV模型

Joy

5. IM系统

Joy

向场景而生,移动数字化底座WorkPlus支持场景化定制

BeeWorks

华为云耀云服务器L实例:中小企业数字化转型的智选

YG科技

流量为王时代,华为云耀云服务器L实例让网站建设不焦虑

轶天下事

商场如战场!华为云助力企业打赢云服务器之战

平平无奇爱好科技

2. 商品那些模型

Joy

7. 电商全球化架构

Joy

华为云耀云服务器L实例,引领企业数字化新时代

轶天下事

跨境电商独立站太烧钱?中小企业老板快收藏华为云这份省钱秘笈

轶天下事

3. 跨境卖全球框架

Joy

开启数字化转型之选,探寻华为云耀云服务器L实例的独特魅力

YG科技

新手站长网站开发常犯错?有了华为云这款轻量应用服务器不再踩坑

YG科技

超强新品叠加价格特惠,华为云助力小程序开发红海“掘金”

轶天下事

老板抱怨网站投入没效果?掌握这份运营秘诀效果直接起飞

轶天下事

企业建站服务器选“懵逼”了?照着华为云的思路买准没错!

平平无奇爱好科技

6. 账号系统

Joy

真香定律再现!中小企业数字化逃不过华为云轻量应用服务器

平平无奇爱好科技

小程序创业是伪命题?华为云耀云服务器L实例铺开初创企业发展新蓝图

平平无奇爱好科技

独家丨小程序开发支出明细,快Get华为云低成本高效率开发秘诀!

平平无奇爱好科技

拒绝成为“时代的眼泪”,华为云这款服务器赋能企业高效转型

轶天下事

“云”端赛马 “算”出未来丨华为云轻量应用服务器助力企业换道超车

平平无奇爱好科技

4. 爬虫系统

Joy

爬虫实战

阿里云太“氪金”?试试华为云超高性价比轻量应用服务器

平平无奇爱好科技

智能管理、极致性能:华为云耀云服务器L实例引领小程序开发新潮流

平平无奇爱好科技

快速从入门到精通,华为云耀云服务器L实例让小程序开发更简单

平平无奇爱好科技

无脑推荐的轻量应用服务器有没有?华为云是最佳选择

平平无奇爱好科技

小程序开发遭老板砍预算?快用华为云轻量应用服务器保效果!

YG科技

让小程序开发稳步快跑,这款轻量应用服务器凭实力说话

轶天下事

WorkPlus即时通讯软件,以自主安全为底座,连接工作的一切

BeeWorks

华为云耀云服务器L实例:数字化转型的得力伙伴

YG科技

检测 NSObject 对象持有的强指针_语言 & 开发_Draveness_InfoQ精选文章