FBRetainCycleDetector解析

一. 原理分析

FBRetainCycleDetector的原理:是基于DFS算法,把整个对象之间的强引用关系当做图进行处理,查找其中的环,就找到了循环引用。

二. 检测NSObject对象持有的强指针

1. 如何确定对象类型

@encode(type-name)返回类型的字符串编码,在确定循环引用关系的过程中,只有三种编码字符串存在强引用关系:

编码.jpg

判断代码:

- (FBType)_convertEncodingToType:(const char *)typeEncoding {
    if (typeEncoding[0] == '{') return FBStructType;

    if (typeEncoding[0] == '@') {
        if (strncmp(typeEncoding, "@?", 2) == 0) return FBBlockType;
        return FBObjectType;
    }

    return FBUnknownType;
}

2. 获取Ivar的引用属性

class_getIvarLayout可以返回一个字符串用来描述成员变量的布局信息。假设存在类:

@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\x12\x11,以此为例,字符串表示共存在0+1+1+2+1+1 总共6个成员变量。其中一个\xAB表示存在A个非强引用属性,和B个强引用属性,因此该布局字符串也可表示为:

- 0个非强引用属性,1个强引用属性
- 1个非强引用属性,2个强引用属性
- 1个非强引用属性,1个强引用属性

3. 获取变量布局偏移

ivar_getOffset可以获取成员变量在类结构中的偏移地址,由于ivar是指针类型,通过offset/sizeof(void *)可以获取偏移数量。通过range的方式可以用来匹配某个成员变量是否属于强引用属性:

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这一集合中,就可以返回了。

二. 关联属性强引用的获取

FBRetainCycleDetector在对关联对象进行追踪时,通过fishhook第三方库hook了关联对象的两个C函数,objc_setAssociatedObjectobjc_removeAssociatedObjects,然后重新实现了关联对象模块,将通过OBJC_ASSOCIATION_RETAINOBJC_ASSOCIATION_RETAIN_NONATOMIC策略,保存起来,只追踪强引用的属性。

三. Block引用属性获取

首先需要声明一个类似block的结构体类型,用于强制类型转换:

struct BlockLiteral {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct BlockDescriptor *descriptor;
    // imported variables
};

block引用的对象总是基于block地址偏移整个结构体的size,并且被持有的对象按照强引用在前,弱引用在后的顺序排列

这里block变量其实只是一个指向结构体的指针,所以大小为8,结构体的大小为32

struct BlockDescriptor {
    unsigned long int reserved;                // NULL
    unsigned long int size;
    // optional helper functions
    void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
    void (*dispose_helper)(void *src);         // IFF (1<<25)
    const char *signature;                     // IFF (1<<30)
};

block中存在两个helper函数协助完成copy到堆上和释放对象引用的工作,后者会对所有的strong类型的对象进行一次release调用,因此可以通过手动调用dispose_helper的方式来识别block强引用的对象:

最后就是用于从dispose_helper的接收类FBBlockStrongRelationDetector,它的实例在接收release消息时,并不会真正的释放,只会将标记_strongYES.

- (oneway void)release {
    _strong = YES;
}

- (oneway void)trueRelease {
    [super release];
}

真正调用trueRelease的时候才会向对象发送release消息。

如果block持有另一个block对象,FBBlockStrongRelationDetector也会将自身伪装成一个假的block防止在接收到关于block释放的消息时发生crash.

struct _block_byref_block;
@interface FBBlockStrongRelationDetector : NSObject {
    // __block fakery
    void *forwarding;
    int flags;   //refcount;
    int size;
    void (*byref_keep)(struct _block_byref_block *dst, struct _block_byref_block *src);
    void (*byref_dispose)(struct _block_byref_block *);
    void *captured[16];
}

1. 获取block强引用对象

static NSIndexSet *_GetBlockStrongLayout(void *block) {
    struct BlockLiteral *blockLiteral = block;

    /// 如果拥有CPP析构器,说明持有的对象可能没有按照指针大小对齐,很难检测到所有对象
    /// 如果没有dispose函数,说明无法retain对象,因此也无法测试强引用了哪些对象
    if ((blockLiteral->flags & BLOCK_HAS_CTOR)
    || !(blockLiteral->flags & BLOCK_HAS_COPY_DISPOSE)) {
        return nil;
    }

    /// 通过获取dispose_helper,并且将一个mock的对象数组传入进去,检测哪些mock对象会被release
    void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper;
    const size_t ptrSize = sizeof(void *);
    const size_t elements = (blockLiteral->descriptor->size + ptrSize - 1) / ptrSize;

    void *obj[elements];
    void *detectors[elements];

    /// 遍历所有mock对象,如果标记位为YES,说明被release,此时对应位置的变量被block强引用
    for (size_t i = 0; i < elements; ++i) {
        FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new];
        obj[i] = detectors[i] = detector;
    }

    @autoreleasepool {
        dispose_helper(obj);
    }

    NSMutableIndexSet *layout = [NSMutableIndexSet indexSet];

    for (size_t i = 0; i < elements; ++i) {
        FBBlockStrongRelationDetector *detector = (FBBlockStrongRelationDetector *)(detectors[i]);
        if (detector.isStrong) {
            [layout addIndex:i];
        }
        [detector trueRelease];
    }

    return layout;
}

四. DFS深度遍历检测是否存在循环引用

采用stack的方式,对某个对象获取所有被其强引用的对象,然后依次以递归思想将这些对象入栈,如果某次入栈的对象已经存在stack中,说明该对象在stack中的位置直到当前为止,存在引用环.

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_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<FBNodeEnumerator *> *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] < stackDepth) {
                        [stack addObject:firstAdjacent];
                    }
                }
            } else {
                [stack removeLastObject];
                [objectsOnPath removeObject:top];
            }
        }
    }
    return retainCycles;
}

五. 总结

FBRetainCycleDetector进行循环引用检测的思路如下:

  • 对于正常类的类的成员变量,通过runtimeclass_getIvarLayout获取描述该类成员变量的布局信息,然后通过ivar_getOffset遍历获取成员变量在类结构中的偏移地址,然后获取强引用变量的集合。

  • 对于关联对象的成员变量,通过fishhook第三方库hook了关联对象的两个C函数,objc_setAssociatedObjectobjc_removeAssociatedObjects,然后重新实现了关联对象模块,将通过OBJC_ASSOCIATION_RETAINOBJC_ASSOCIATION_RETAIN_NONATOMIC策略进行关联的对象保存起来,只追踪强引用的属性。

  • 对于block持有的强引用变量的获取,依据block引用的对象总是基于block地址偏移整个结构体的size,并且被持有的对象按照强引用在前,弱引用在后的顺序排列,因为block强引用的对象都会进行copy到堆上和release对象引用的操作,因此可以通过接收类FBBlockStrongRelationDetector构造detector对象,然后用blockdispose_helper方法调用,判断如果detector对象调用release方法,就说明当前对象是强引用对象,然后获取block持有的所有强引用变量的集合

  • 对所有检测到的强引用变量,利用DFS(深度优先搜索),采用stack栈的形式,将遍历到的对象依次入栈,如果某次入栈的对象已经存在stack中,说明该对象在stack中的位置直到当前为止,存在引用环.

这里只是做了过程的总结,详细分析可以参考以下文章:

如何在 iOS 中解决循环引用的问题
检测 NSObject 对象持有的强指针
如何实现 iOS 中的 Associated Object
iOS 中的 block 是如何持有对象的

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