检测 NSObject 对象持有的强指针

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

Follow: Draveness · Github

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

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

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

allRetainedObjects 方法

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

#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 的代码如下:

XXObject *object = [[XXObject alloc] init];

FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:object];
__unused NSSet *cycles = [detector findRetainCycles];

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

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

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

- (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 这一函数:

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 中的强引用,只是在这里我们递归地查找了所有父类的指针,并且加入了缓存以加速查找强引用的过程,接下来就是从对象的结构中获取强引用的过程了:

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 方法的实现。

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 中保存的各种属性全部保存起来:

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: 将类型编码转换为枚举类型:

- (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 中的所有元素:

get-ivars

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

<__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 方法:

get-ivar-layout

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

(lldb) po fullLayout
"\x01\x12\x11"

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

@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 方法获取变量索引的最小值:

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

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 谓词来进行过滤就可以了:

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

====

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

- (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

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 协议,但是在这里并不会对它们进行处理:

- (NSArray *)_unfilteredRetainedObjects {
    ...

    if ([NSStringFromClass(aCls) hasPrefix:@"__NSCF"]) {
        return retainedObjects;
    }

    if (class_isMetaClass(aCls)) {
        return nil;
    }
    
    ...
}

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

- (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 将不需要加入集合中的元素过滤掉:

- (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,它为我们提供了与属性引用强弱有关的信息,帮助筛选强引用。

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

Follow: Draveness · Github

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容