正在加载今日诗词....
May 6, 2021

查看 iOS 中所有堆区对象

iOS 内存工具中 经常会有 查看某一个对象的所有引用者, 该对象实例化了多少等需求, 而实现这些需求, 首要的就是获取所有的堆区对象, 如何处理呢?

需求: 堆区对象的查找

寻找已有的开源项目实现 FLEX 中的 FLEXHeapEnumerator

FLEX 代码的参考

FLEXHeapEnumerator


static CFMutableSetRef registeredClasses;

// Mimics the objective-c object structure for checking if a range of memory is an object.
typedef struct {
    Class isa;
} flex_maybe_object_t;

@implementation FLEXHeapEnumerator

static void range_callback(task_t task, void *context, unsigned type, vm_range_t *ranges, unsigned rangeCount) {
    if (!context) {
        return;
    }
    
    for (unsigned int i = 0; i < rangeCount; i++) {
        vm_range_t range = ranges[i];
        flex_maybe_object_t *tryObject = (flex_maybe_object_t *)range.address;
        Class tryClass = NULL;
        // 这里 iOS 真机 arm64 为了优化 isa 的内存, 仅仅使用了一部分来表示指针, 剩余部分会表示比如引用计数, 使用 arc 之类的东西, 所以需要特殊处理来获取 tryClass
#ifdef __arm64__
        // See http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
        extern uint64_t objc_debug_isa_class_mask WEAK_IMPORT_ATTRIBUTE;
        tryClass = (__bridge Class)((void *)((uint64_t)tryObject->isa & objc_debug_isa_class_mask));
#else
        tryClass = tryObject->isa;
#endif
        // If the class pointer matches one in our set of class pointers from the runtime, then we should have an object.
        if (CFSetContainsValue(registeredClasses, (__bridge const void *)(tryClass))) {
            (*(flex_object_enumeration_block_t __unsafe_unretained *)context)((__bridge id)tryObject, tryClass);
        }
    }
}

static kern_return_t reader(__unused task_t remote_task, vm_address_t remote_address, __unused vm_size_t size, void **local_memory) {
    *local_memory = (void *)remote_address;
    return KERN_SUCCESS;
}


/// 遍历堆区已分配的内存
/// @param block 回调
/// 疑问 ① 关于 lock 的使用中, enumerator 前后 和回调中 都进行了 加解锁 , 有些递归的效果, 该处代码能正确运行, 那应该是递归锁了
+ (void)enumerateLiveObjectsUsingBlock:(flex_object_enumeration_block_t)block {
    if (!block) {
        return;
    }
    
    // Refresh the class list on every call in case classes are added to the runtime.
    // 获取最新的所有被注册到 runtime 中的对象 objc_copyClassList
    [self updateRegisteredClasses];
    
    // Inspired by:
    // https://llvm.org/svn/llvm-project/lldb/tags/RELEASE_34/final/examples/darwin/heap_find/heap/heap_find.cpp
    // https://gist.github.com/samdmarshall/17f4e66b5e2e579fd396
    
    vm_address_t *zones = NULL;
    unsigned int zoneCount = 0;
    // 获取所有的堆区 malloc 内存空间 zones
    // ② 定义 memory_reader_t 函数 -> reader , 与直接传 NULL 有什么区别
    // typedef kern_return_t memory_reader_t(task_t remote_task, vm_address_t remote_address, vm_size_t size, void **local_memory);
    // ③ 这里使用 TASK_NULL , 底层实现是如果处理的, 与 mach_task_self() 有什么区别
    kern_return_t result = malloc_get_all_zones(TASK_NULL, reader, &zones, &zoneCount);
    
    if (result == KERN_SUCCESS) {
        for (unsigned int i = 0; i < zoneCount; i++) {
            malloc_zone_t *zone = (malloc_zone_t *)zones[i];
            // 内存空间 内省 返回的结构体中 包含了很多内存反射相关的函数: 比如 枚举内存,读取相关的安全锁
            malloc_introspection_t *introspection = zone->introspect;

            // This may explain why some zone functions are
            // sometimes invalid; perhaps not all zones support them?
            if (!introspection) {
                continue;
            }

            void (*lock_zone)(malloc_zone_t *zone)   = introspection->force_lock;
            void (*unlock_zone)(malloc_zone_t *zone) = introspection->force_unlock;

            // Callback has to unlock the zone so we freely allocate memory inside the given block
            // !!!! 特别注意这里内部也进行了 加解锁, 看来是多次回调 多次加解锁, 不知道为什么这样操作 
            flex_object_enumeration_block_t callback = ^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
                unlock_zone(zone);
                block(object, actualClass);
                lock_zone(zone);
            };
            // 指针安全可读 ; 使用内存方面的底层指针 需要谨慎 ; 原因如下 NOTES
            BOOL lockZoneValid = FLEXPointerIsReadable(lock_zone);
            BOOL unlockZoneValid =  FLEXPointerIsReadable(unlock_zone);

            // NOTES:  There is little documentation on when and why
            // any of these function pointers might be NULL
            // or garbage, so we resort to checking for NULL
            // and whether the pointer is readable
            if (introspection->enumerator && lockZoneValid && unlockZoneValid) {
                // 锁定这块区间
                lock_zone(zone);
                // 遍历可读的内存 (allocated pointers 已分配的内存地址)
                // 这里的 callBack 与 range_callback 的处理是对应的处理结果 息息相关的
                introspection->enumerator(TASK_NULL, (void *)&callback, MALLOC_PTR_IN_USE_RANGE_TYPE, (vm_address_t)zone, reader, &range_callback);
                // 解锁该区间
                unlock_zone(zone);
            }
        }
    }
}

// 查找最新的已注册 class ,并整理到 set 中
+ (void)updateRegisteredClasses {
    if (!registeredClasses) {
        registeredClasses = CFSetCreateMutable(NULL, 0, NULL);
    } else {
        CFSetRemoveAllValues(registeredClasses);
    }
    unsigned int count = 0;
    Class *classes = objc_copyClassList(&count);
    for (unsigned int i = 0; i < count; i++) {
        CFSetAddValue(registeredClasses, (__bridge const void *)(classes[i]));
    }
    free(classes);
}

@end

疑问

关于 lock 的使用中, enumerator 前后 和回调中 都进行了 加解锁, 这里要注意加解锁的顺序, 外部先锁定, 内部先解锁然后加锁 , 最后外部解锁

测试方式 多次解锁, 会出现如下崩溃

BUG IN CLIENT OF LIBPLATFORM: Unlock of an os_unfair_lock not owned by current thread

说明这是一个 os_unfair_lock 也就是如下对应

force_unlock => os_unfair_lock_unlock
force_lock => os_unfair_lock_lock

猜测这里在回调处多次加解锁是为了减少锁的粒度, 性能更好

memory_reader_t

malloc_get_all_zones 中 memory_reader_t 函数 使用自己定义的 reader 函数, 与直接传 NULL 有什么区别

task

malloc_get_all_zones 中使用 task 为 TASK_NULL, 与传 mach_task_self() 有什么分别
测试发现 基本没有差别, 但语义上 TASK_NULL应该是无效 task, mach_task_self 为调用线程的 task port 奇怪

mach_task_self 多了
__CFN_CoalescingDomainHolder
__CFN_PathPolicyManager
__NSCFURLSessionTaskActiveStreamDependencyInfo
少了 boringssl_concrete_boringssl_session_state

测试代码如下:

- (void)testExample {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
    printf("====begin==== \n");
    NSMutableSet *set = [NSMutableSet set];
    [FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
        NSString *clsName = NSStringFromClass(actualClass);
        if (![set containsObject:clsName]) {
            [set addObject:clsName];
        }
    }];
    NSArray *ret = [set.allObjects sortedArrayUsingComparator:^NSComparisonResult(NSString * obj1, NSString * obj2) {
        return [obj1 compare:obj2 options:NSCaseInsensitiveSearch];
    }];
    NSLog(@"ret:%@",ret);
    printf("====end==== \n");
}

应用

查找某对象的引用

+ (instancetype)objectsWithReferencesToObject:(id)object {
    static Class SwiftObjectClass = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SwiftObjectClass = NSClassFromString(@"SwiftObject");
        if (!SwiftObjectClass) {
            SwiftObjectClass = NSClassFromString(@"Swift._SwiftObject");
        }
    });
    // 枚举所有可能的方式 查找引用对象, 厉害了 这可以说是万无一失啊
    // 前提 获取到所有的堆区对象
    NSMutableArray<FLEXObjectRef *> *instances = [NSMutableArray new];
    [FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id tryObject, __unsafe_unretained Class actualClass) {
        // Get all the ivars on the object. Start with the class and and travel up the inheritance chain.
        // Once we find a match, record it and move on to the next object. There's no reason to find multiple matches within the same object.
        Class tryClass = actualClass;
        while (tryClass) {
            unsigned int ivarCount = 0;
            Ivar *ivars = class_copyIvarList(tryClass, &ivarCount);

            for (unsigned int ivarIndex = 0; ivarIndex < ivarCount; ivarIndex++) {
                Ivar ivar = ivars[ivarIndex];
                NSString *typeEncoding = @(ivar_getTypeEncoding(ivar) ?: "");

                if (typeEncoding.flex_typeIsObjectOrClass) {
                    ptrdiff_t offset = ivar_getOffset(ivar);
                    uintptr_t *fieldPointer = (__bridge void *)tryObject + offset;
                    // 这里直接对比生存的类中的 对象 ivar 地址是否和 查找的对象地址一致
                    if (*fieldPointer == (uintptr_t)(__bridge void *)object) {
                        NSString *ivarName = @(ivar_getName(ivar) ?: "???");
                        [instances addObject:[FLEXObjectRef referencing:tryObject ivar:ivarName]];
                        return;
                    }
                }
            }

            free(ivars);
            tryClass = class_getSuperclass(tryClass);//递归类的继承树
        }
    }];

    FLEXObjectListViewController *viewController = [[self alloc]
        initWithReferences:instances
        predicates:self.defaultPredicates
        sectionTitles:self.defaultSectionTitles
    ];
    viewController.title = [NSString stringWithFormat:@"Referencing %@ %p",
        [FLEXRuntimeUtility safeClassNameForObject:object], object
    ];
    return viewController;
}