写点什么

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

评论

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

Vue3中常用的Composition(组合)API-初识setup

不觉心动

6 月 优质更文活动

培育开源人才,助力开源生态发展|2023开放原子全球开源峰会校源行分论坛圆满落幕

开放原子开源基金会

开源 开放原子全球开源峰会 开放原子 校源行

在人工智能冲击下,IT部门的生存价值在哪里?

FN0

AIGC

2023-06-18:给定一个长度为N的一维数组scores, 代表0~N-1号员工的初始得分, scores[i] = a, 表示i号员工一开始得分是a, 给定一个长度为M的二维数组operatio

福大大架构师每日一题

golang 算法 rust 福大大架构师每日一题

CSS小技巧之悬停3D发光效果

南城FE

CSS 前端 动画 3D 交互

AI关于AI民主化的高见

FN0

AIGC

漫谈 SAP 产品里页面上的 Checkbox 设计与实现

汪子熙

SAP 前端设计 思爱普 6 月 优质更文活动

如何在 Linux 上列出磁盘?

wljslmz

6 月 优质更文活动

C语言编程—语法练习

芯动大师

C语言 语法 6 月 优质更文活动

基于STM32的铁路自动围栏系统设计

DS小龙哥

6 月 优质更文活动

如何评估大型语言模型(LLM)?

Baihai IDP

人工智能 深度学习 大模型 白海科技 大模型评估

一文读懂ChatGPT的工作原理:大语言模型是个啥?它到底咋工作的?

禅道项目管理

#人工智能 ChatGPT AI 2022

飞书深诺多系统数据同步方案

飞书深诺技术团队

万字详解常用设计模式

越长大越悲伤

设计模式

数字化扶乩的最佳语言是英语?

FN0

AIGC

当 Rokid 遇上函数计算

阿里巴巴云原生

阿里云 云原生 Rokid

IT知识百科:什么是OTN——光传送网?

wljslmz

6 月 优质更文活动

xenomai内核解析--实时linux概述

沐多

RTOS 实时linux xenomai

在 Go 中使用 sqlx 替代 database/sql 操作数据库

江湖十年

数据库 后端 sql Go 语言

Vue3中常用的Composition(组合)API-ref(引用)函数

不觉心动

6 月 优质更文活动

微服务中「组件」集成

Java 架构

Backdata.net 搜索引擎

Larry

搜索引擎 导航网站

MySQL DeadLock -- 二级索引导致的死锁

红袖添香

MySQL 数据库 数据库死锁

Vue3中常用的Composition(组合)API-ref(引用)函数

不觉心动

6 月 优质更文活动

300行代码模拟cdn访问过程

蓝胖子的编程梦

CDN DNS CDN加速 CDN技术 #DNS

Kubernetes集群认证管理

穿过生命散发芬芳

6 月 优质更文活动

做好演讲表达的道法术器(《如何激活你的表达思维》-- 培训收获)(68/100)

hackstoic

领导力 演讲 沟通

前端如何处理「并发」问题?

不叫猫先生

并发 axios 6 月 优质更文活动

华为云CodeArts Build快速上手编译构建-进阶玩家体验

华为云PaaS服务小智

云计算 编译 开发 华为云

Vue3中常用的Composition(组合)API-watch(监视)函数

不觉心动

6 月 优质更文活动

使用华为云AstroZero,不用一行代码,制作端午节加班申请模板

华为云PaaS服务小智

云计算 零代码 华为云

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