写点什么

检测 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:56779

评论

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

4 个 JavaScript 最基础的问题 —— Eric Elliott

掘金安东尼

JavaScript 面试 前端 7月月更

云计算和大数据的关系以及区别详细讲解

行云管家

云计算 大大数据

jdbc自带MySQL连接池实践

FunTester

python小知识-rethinking python 生成器

AIWeker

Python python小知识 7月月更

模块七作业 - 王者荣耀商城异地多活架构设计

Elvis FAN

龙蜥开发者说:社区首个支持 LoongArch架构的操作系统构建之路 | 第 9 期

OpenAnolis小助手

开源 操作系统 龙蜥开发者说 龙芯 LoongArch 架构

音视频开发进阶|第四讲:音频自动增益控制 AGC

ZEGO即构

音视频开发 AGC

全新出品!阿里P5工程师~P8架构师晋升路线揭秘

程序员小毕

Java 程序员 面试 架构师 学习路线

B站挂了登上全网热搜!技术人员为你还原前因后果

雨果

Google上网神器Ghelper

源字节1号

软件开发 小程序开发

Okaleido或杀出NFT重围,你看好它吗?

鳄鱼视界

不会吧!钉钉都下载了,你还不知道可以这样玩?

Jianmu

钉钉 持续集成 自动化运维 建木CI 通知

uni-app进阶之内嵌应用【day14】

恒山其若陋兮

7月月更

ABAP-OOALV实现

桥下本有油菜花

SAP abap

如何快速有效的定位应用抖动问题?| 龙蜥技术

OpenAnolis小助手

Linux 系统 龙蜥技术 SysAK 抖动

TiKV & TiFlash 加速复杂业务查询

TiDB 社区干货传送门

还在羡慕其它平台有跨店满减,其实你也可以!

CRMEB

《黑客与画家》作者:18 个会杀死初创公司的错误

雨果

创业者

SAP Fiori Launchpad 应用的两个实用技巧分享

汪子熙

JavaScript SAP SAP UI5 ui5 7月月更

TPC藏宝计划质押系统开发(Dapp)

薇電13242772558

智能合约 dapp

Kyligence 正式支持 Amazon EMR Serverless,构建高效低成本云上数据分析

Kyligence

Cloud Kyligence Amazon EMR

nacos注册中心之服务注册

急需上岸的小谢

7月月更

向量化执行引擎框架 Gluten 宣布正式开源,并亮相 Spark 技术峰会

Kyligence

spark Gluten

MySQL数据库优化

五分钟学大数据

MySQL 7月月更

2022年浙江省等保备案流程指南

行云管家

等保 等保备案

异想天开 | 假如用中文写代码,是一种什么体验?

雨果

程序员 开发者

Lite Actor:方舟Actor并发模型的轻量级优化

HarmonyOS开发者

HarmonyOS

边无际 Shifu IoT 开源开发框架 助力物联网应用开发加速十倍

亚马逊云科技 (Amazon Web Services)

开源 Kubernetes 物联网 应用开发

查找——B-树

乔乔

7月月更

声网传输层协议 AUT 的总结与展望丨Dev for Dev 专栏

声网

传输协议 Dev for Dev

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