檢測(cè) NSObject 對(duì)象持有的強(qiáng)指針

關(guān)注倉庫父款,及時(shí)獲得更新:iOS-Source-Code-Analyze

Follow: Draveness · Github

在上一篇文章中介紹了 FBRetainCycleDetector 的基本工作原理,這一篇文章中我們開始分析它是如何從每一個(gè)對(duì)象中獲得它持有的強(qiáng)指針的瞻凤。

如果沒有看第一篇文章這里還是最好看一下铛漓,了解一下 FBRetainCycleDetector 的工作原理,如何在 iOS 中解決循環(huán)引用的問題鲫构。

FBRetainCycleDetector 獲取對(duì)象的強(qiáng)指針是通過 FBObjectiveCObject 類的 - allRetainedObjects 方法浓恶,這一方法是通過其父類 FBObjectiveCGraphElement 繼承過來的,只是內(nèi)部有著不同的實(shí)現(xiàn)结笨。

allRetainedObjects 方法

我們會(huì)以 XXObject 為例演示 - allRetainedObjects 方法的調(diào)用過程:

#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 方法只是調(diào)用了 - _unfilteredRetainedObjects,然后進(jìn)行了過濾炕吸,文章主要會(huì)對(duì) - _unfilteredRetainedObjects 的實(shí)現(xiàn)進(jìn)行分析:

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

方法 - _unfilteredRetainedObjects 的實(shí)現(xiàn)代碼還是比較多的伐憾,這里會(huì)將代碼分成幾個(gè)部分,首先是最重要的部分:如何得到對(duì)象持有的強(qiáng)引用:

- (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];
            }
        }
    }
    
    ...
}

獲取強(qiáng)引用是通過 FBGetObjectStrongReferences 這一函數(shù):

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];
}

上面代碼的核心部分是執(zhí)行 FBGetStrongReferencesForClass 返回 currentClass 中的強(qiáng)引用赫模,只是在這里我們遞歸地查找了所有父類的指針树肃,并且加入了緩存以加速查找強(qiáng)引用的過程,接下來就是從對(duì)象的結(jié)構(gòu)中獲取強(qiáng)引用的過程了:

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;
}

該方法的實(shí)現(xiàn)大約有三個(gè)部分:

  1. 調(diào)用 FBGetClassReferences 從類中獲取它指向的所有引用瀑罗,無論是強(qiáng)引用或者是弱引用
  2. 調(diào)用 FBGetLayoutAsIndexesForDescription 從類的變量布局中獲取強(qiáng)引用的位置信息
  3. 使用 NSPredicate 過濾數(shù)組中的弱引用

獲取類的 Ivar 數(shù)組

FBGetClassReferences 方法主要調(diào)用 runtime 中的 class_copyIvarList 得到類的所有 ivar

這里省略對(duì)結(jié)構(gòu)體屬性的處理胸嘴,因?yàn)樘^復(fù)雜,并且涉及大量的C++ 代碼斩祭,有興趣的讀者可以查看 FBGetReferencesForObjectsInStructEncoding 方法的實(shí)現(xiàn)劣像。

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];
}

上述實(shí)現(xiàn)還是非常直接的,遍歷 ivars 數(shù)組摧玫,使用 FBIvarReference 將其包裝起來然后加入 result 中耳奕,其中的類 FBIvarReference 僅僅起到了一個(gè)包裝的作用,將 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 的實(shí)例初始化時(shí)芍躏,會(huì)通過私有方法 - _convertEncodingToType: 將類型編碼轉(zhuǎn)換為枚舉類型:

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

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

    return FBUnknownType;
}

當(dāng)代碼即將從 FBGetClassReferences 方法中返回時(shí)邪乍,使用 lldb 打印 result 中的所有元素:

get-ivars

上述方法成功地從 XXObject 類中獲得了正確的屬性數(shù)組,不過這些數(shù)組中不止包含了強(qiáng)引用纸肉,還有被 weak 標(biāo)記的弱引用:

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

獲取 Ivar Layout

當(dāng)我們?nèi)〕隽?XXObject 中所有的屬性之后溺欧,還需要對(duì)其中的屬性進(jìn)行過濾喊熟;那么我們?nèi)绾闻袛嘁粋€(gè)屬性是強(qiáng)引用還是弱引用呢柏肪?Objective-C 中引入了 Ivar Layout 的概念,對(duì)類中的各種屬性的強(qiáng)弱進(jìn)行描述芥牌。

它是如何工作的呢烦味,我們先繼續(xù)執(zhí)行 FBGetStrongReferencesForClass 方法:

get-ivar-layout

在 ObjC 運(yùn)行時(shí)中的 class_getIvarLayout 可以獲取某一個(gè)類的 Ivar Layout,而 XXObject 的 Ivar Layout 是什么樣的呢壁拉?

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

Ivar Layout 就是一系列的字符谬俄,每?jī)蓚€(gè)一組,比如 \xmn弃理,每一組 Ivar Layout 中第一位表示有 m 個(gè)非強(qiáng)屬性溃论,第二位表示接下來有 n 個(gè)強(qiáng)屬性;如果沒有明白痘昌,我們以 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 個(gè)非強(qiáng)屬性钥勋,然后有 1 個(gè)強(qiáng)屬性 first
  • 第二組的 \x12 表示有 1 個(gè)非強(qiáng)屬性 second,然后有 2 個(gè)強(qiáng)屬性 third forth
  • 第三組的 \x11 表示有 1 個(gè)非強(qiáng)屬性 fifth, 然后有 1 個(gè)強(qiáng)屬性 sixth

在對(duì) Ivar Layout 有一定了解之后辆苔,我們可以繼續(xù)對(duì) FBGetStrongReferencesForClass 分析了算灸,下面要做的就是使用 Ivar Layout 提供的信息過濾其中的所有非強(qiáng)引用,而這就需要兩個(gè)方法的幫助驻啤,首先需要 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;
}

然后執(zhí)行 FBGetLayoutAsIndexesForDescription(minimumIndex, fullLayout) 獲取所有強(qiáng)引用的 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;
}

因?yàn)楦呶槐硎痉菑?qiáng)引用的數(shù)量菲驴,所以我們要加上 upperNibble,然后 NSMakeRange(currentIndex, lowerNibble) 就是強(qiáng)引用的范圍骑冗;略過 lowerNibble 長(zhǎng)度的索引赊瞬,移動(dòng) layoutDescription 指針,直到所有的 NSRange 都加入到了 interestingIndexes 這一集合中贼涩,就可以返回了森逮。

過濾數(shù)組中的弱引用

在上一階段由于已經(jīng)獲取了強(qiáng)引用的范圍,在這里我們直接使用 NSPredicate 謂詞來進(jìn)行過濾就可以了:

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> 對(duì)象褒侧,還需要 FBWrapObjectGraphElementWithContext 把它進(jìn)行包裝成 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];
        }
    }
}

最后會(huì)把封裝好的實(shí)例添加到 retainedObjects 數(shù)組中。

- _unfilteredRetainedObjects 同時(shí)也要處理集合類,比如數(shù)組或者字典闷供,但是如果是無縫橋接的 CF 集合烟央,或者是元類,雖然它們可能遵循 NSFastEnumeration 協(xié)議歪脏,但是在這里并不會(huì)對(duì)它們進(jìn)行處理:

- (NSArray *)_unfilteredRetainedObjects {
    ...

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

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

在遍歷內(nèi)容時(shí)疑俭,Mutable 的集合類中的元素可能會(huì)改變,所以會(huì)重試多次以確保集合類中的所有元素都被獲取到了:

- (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 中婿失,如果在遍歷時(shí)插入了其它元素钞艇,就會(huì)拋出異常,然后 continue 重新遍歷集合豪硅,最后返回所有持有的對(duì)象哩照。

最后的過濾部分會(huì)使用 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;
}

總結(jié)

FBRetainCycleDetector 在對(duì)象中查找強(qiáng)引用取決于類的 Ivar Layout,它為我們提供了與屬性引用強(qiáng)弱有關(guān)的信息懒浮,幫助篩選強(qiáng)引用飘弧。

關(guān)注倉庫,及時(shí)獲得更新:iOS-Source-Code-Analyze

Follow: Draveness · Github

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末砚著,一起剝皮案震驚了整個(gè)濱河市次伶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌稽穆,老刑警劉巖冠王,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件在塔,死亡現(xiàn)場(chǎng)離奇詭異拦惋,居然都是意外死亡隧期,警方通過查閱死者的電腦和手機(jī)赃额,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門绘盟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來价认,“玉大人帝美,你說我怎么就攤上這事腺毫÷畛危” “怎么了吓蘑?”我有些...
    開封第一講書人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)坟冲。 經(jīng)常有香客問我磨镶,道長(zhǎng),這世上最難降的妖魔是什么健提? 我笑而不...
    開封第一講書人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任琳猫,我火速辦了婚禮,結(jié)果婚禮上私痹,老公的妹妹穿的比我還像新娘脐嫂。我一直安慰自己统刮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開白布账千。 她就那樣靜靜地躺著侥蒙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪匀奏。 梳的紋絲不亂的頭發(fā)上鞭衩,一...
    開封第一講書人閱讀 51,590評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音娃善,去河邊找鬼论衍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛聚磺,可吹牛的內(nèi)容都是我干的坯台。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼咧最,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼捂人!你這毒婦竟也來了御雕?” 一聲冷哼從身側(cè)響起矢沿,我...
    開封第一講書人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎酸纲,沒想到半個(gè)月后捣鲸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡闽坡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年栽惶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疾嗅。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡外厂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出代承,到底是詐尸還是另有隱情汁蝶,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布论悴,位于F島的核電站掖棉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏膀估。R本人自食惡果不足惜幔亥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望察纯。 院中可真熱鬧帕棉,春花似錦针肥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瞒窒,卻和暖如春捺僻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背崇裁。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工匕坯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拔稳。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓葛峻,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親巴比。 傳聞我的和親對(duì)象是個(gè)殘疾皇子术奖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容