iOS检测未使用的类

两种方法
1、machO文件中 通过 __objc_classlist __objc_classrefs 对比
2、运行时获取到未使用的类(需要大量测试,或者线上测试)
两种方法都不能绝对识别准确,但合并起来准确率很高了,最终再手动确定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <dlfcn.h>
#include <mach-o/dyld.h>
#include <mach-o/getsect.h>

/* 获取 runtime 未使用的类 */
NSMutableSet<NSString *> *runtimeUnusedClass() {
Dl_info info;
dladdr((const void *)&runtimeUnusedClass, &info);

const uint64_t mach_header = (uint64_t)info.dli_fbase;

const struct section_64 *classlist = getsectbynamefromheader_64((const struct mach_header_64 *)mach_header, "__DATA", "__objc_classlist");

if (classlist) {
NSMutableSet *classlistUnused = [[NSMutableSet alloc] init];
// 遍历拿到所有的类
for (UInt64 addr = classlist->offset; addr < classlist->offset + classlist->size; addr += sizeof(const char **)) {
uint64_t baseArrr = mach_header + addr;

//获取类对象指针
uint64_t object_class_addr = *(uint64_t *)(baseArrr);
//
uint64_t object_class_isa = *(uint64_t *)(object_class_addr);
//获取元类对象
uint64_t object_meta_class_addr;
if (object_class_isa & (1 << 0)) {
object_meta_class_addr = object_class_addr;
} else {
object_meta_class_addr = object_class_isa & 0x00007ffffffffff8ULL;
}
//偏移32个字节,拿到 class_data_bits 的地址
uint64_t class_data_bits_addr = object_meta_class_addr + 32;
//取 class_data_bits 的值
uint64_t class_data_bits = *(uint64_t *)(class_data_bits_addr);
//得到 class_rw_t 的指针 (class_rw_t *)(bits & FAST_DATA_MASK);
uint64_t class_rw_addr = (class_data_bits & 0x00007ffffffffff8UL);
//取指针的值,得到的第一个数据就是 flags
uint64_t flags = *(uint64_t *)(class_rw_addr);
//
bool isInitialized = flags & (1 << 29);

if (!isInitialized) {
//打印类会导致类初始化,所以此方法只能使用一次
NSString *class = [NSString stringWithFormat:@"%@", (void *)object_class_addr];
[classlistUnused addObject:class];
}
}
return classlistUnused;
}
return nil;
}

/* 获取 MachO 未使用的类 */
NSMutableSet<NSString *> *machOUnusedClass() {
Dl_info info;
dladdr((const void *)&machOUnusedClass, &info);

const uint64_t mach_header = (uint64_t)info.dli_fbase;

const struct section_64 *classlist = getsectbynamefromheader_64((const struct mach_header_64 *)mach_header, "__DATA", "__objc_classlist");
const struct section_64 *selfrefs = getsectbynamefromheader_64((const struct mach_header_64 *)mach_header, "__DATA", "__objc_classrefs");

if (classlist && selfrefs) {
NSMutableSet *classlistSet = [[NSMutableSet alloc] init];
for (UInt64 addr = classlist->offset; addr < classlist->offset + classlist->size; addr += sizeof(const char **)) {
uint64_t baseArrr = mach_header + addr;
Class cls = (__bridge Class)(*(void **)(baseArrr));
NSString *clsString = [NSString stringWithFormat:@"%@",cls];
[classlistSet addObject:clsString];
}

NSMutableSet *selfrefsSet = [[NSMutableSet alloc] init];
for (UInt64 addr = selfrefs->offset; addr < selfrefs->offset + selfrefs->size; addr += sizeof(const char **)) {
uint64_t baseArrr = mach_header + addr;

Class cls = (__bridge Class)(*(void **)(baseArrr));
while (cls) {
[selfrefsSet addObject:[NSString stringWithFormat:@"%@",cls]];
cls = [cls superclass];
}
}
[classlistSet minusSet:selfrefsSet];

return classlistSet;
}

return nil;
}

缺点

1、runtimeUnusedClass 方法只有在第一次调用有效,而且必须手动浏览所有的页面(可以考虑放到线上收集)
2、两种方法都是不准确的,但未使用的类一定在其中,最终需要手动再次确认。