├── README.md └── classunrefs.py /README.md: -------------------------------------------------------------------------------- 1 | # classunref 2 | 查找OC中未使用的类 3 | 4 | 执行 python3 classunrefs.py 5 | 6 | 输入的第一个参数为xxx.app,可以把Xcode products目录下的xxx.app拖到命令行,这个参数是为了拿到.app下的mach-o文件,分析使用的类和未使用的类。 7 | 8 | -------------------------------------------------------------------------------- /classunrefs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import re 5 | import sys 6 | 7 | def verified_app_path(path): 8 | if path.endswith('.app'): 9 | # appname = path.split('/')[-1].split('.')[0] 10 | appname = os.path.splitext(path)[0].split('/')[-1] 11 | path = os.path.join(path, appname) 12 | if not os.path.isfile(path): 13 | return None 14 | if not os.popen('file -b ' + path).read().startswith('Mach-O'): 15 | return None 16 | return path 17 | 18 | 19 | def pointers_from_binary(line, binary_file_arch): 20 | if len(line) < 16: 21 | return None 22 | line = line[16:].strip().split(' ') 23 | pointers = set() 24 | if binary_file_arch == 'x86_64': 25 | #untreated line example:00000001030cec80 d8 75 15 03 01 00 00 00 68 77 15 03 01 00 00 00 26 | if len(line) >= 8: 27 | pointers.add(''.join(line[4:8][::-1] + line[0:4][::-1])) 28 | if len(line) >= 16: 29 | pointers.add(''.join(line[12:16][::-1] + line[8:12][::-1])) 30 | return pointers 31 | #arm64 confirmed,armv7 arm7s unconfirmed 32 | if binary_file_arch.startswith('arm'): 33 | #untreated line example:00000001030bcd20 03138580 00000001 03138878 00000001 34 | if len(line) >= 2: 35 | pointers.add(line[1] + line[0]) 36 | if len(line) >= 4: 37 | pointers.add(line[3] + line[2]) 38 | return pointers 39 | return None 40 | 41 | 42 | def class_ref_pointers(path, binary_file_arch): 43 | print('Get class ref pointers...') 44 | ref_pointers = set() 45 | lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classrefs %s' % path).readlines() 46 | for line in lines: 47 | pointers = pointers_from_binary(line, binary_file_arch) 48 | if not pointers: 49 | continue 50 | ref_pointers = ref_pointers.union(pointers) 51 | if len(ref_pointers) == 0: 52 | exit('Error:class ref pointers null') 53 | return ref_pointers 54 | 55 | 56 | def class_list_pointers(path, binary_file_arch): 57 | print('Get class list pointers...') 58 | list_pointers = set() 59 | lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classlist %s' % path).readlines() 60 | for line in lines: 61 | pointers = pointers_from_binary(line, binary_file_arch) 62 | if not pointers: 63 | continue 64 | list_pointers = list_pointers.union(pointers) 65 | if len(list_pointers) == 0: 66 | exit('Error:class list pointers null') 67 | return list_pointers 68 | 69 | 70 | def class_symbols(path): 71 | print('Get class symbols...') 72 | symbols = {} 73 | #class symbol format from nm: 0000000103113f68 (__DATA,__objc_data) external _OBJC_CLASS_$_TTEpisodeStatusDetailItemView 74 | re_class_name = re.compile('(\w{16}) .* _OBJC_CLASS_\$_(.+)') 75 | lines = os.popen('nm -nm %s' % path).readlines() 76 | for line in lines: 77 | result = re_class_name.findall(line) 78 | if result: 79 | (address, symbol) = result[0] 80 | symbols[address] = symbol 81 | if len(symbols) == 0: 82 | exit('Error:class symbols null') 83 | return symbols 84 | 85 | def filter_super_class(unref_symbols): 86 | re_subclass_name = re.compile("\w{16} 0x\w{9} _OBJC_CLASS_\$_(.+)") 87 | re_superclass_name = re.compile("\s*superclass 0x\w{9} _OBJC_CLASS_\$_(.+)") 88 | #subclass example: 0000000102bd8070 0x103113f68 _OBJC_CLASS_$_TTEpisodeStatusDetailItemView 89 | #superclass example: superclass 0x10313bb80 _OBJC_CLASS_$_TTBaseControl 90 | lines = os.popen("/usr/bin/otool -oV %s" % path).readlines() 91 | subclass_name = "" 92 | superclass_name = "" 93 | for line in lines: 94 | subclass_match_result = re_subclass_name.findall(line) 95 | if subclass_match_result: 96 | subclass_name = subclass_match_result[0] 97 | superclass_match_result = re_superclass_name.findall(line) 98 | if superclass_match_result: 99 | superclass_name = superclass_match_result[0] 100 | 101 | if len(subclass_name) > 0 and len(superclass_name) > 0: 102 | if superclass_name in unref_symbols and subclass_name not in unref_symbols: 103 | unref_symbols.remove(superclass_name) 104 | superclass_name = "" 105 | subclass_name = "" 106 | return unref_symbols 107 | 108 | def class_unref_symbols(path,reserved_prefix,filter_prefix): 109 | #binary_file_arch: distinguish Big-Endian and Little-Endian 110 | #file -b output example: Mach-O 64-bit executable arm64 111 | binary_file_arch = os.popen('file -b ' + path).read().split(' ')[-1].strip() 112 | unref_pointers = class_list_pointers(path, binary_file_arch) - class_ref_pointers(path, binary_file_arch) 113 | if len(unref_pointers) == 0: 114 | exit('Finish:class unref null') 115 | 116 | symbols = class_symbols(path) 117 | unref_symbols = set() 118 | for unref_pointer in unref_pointers: 119 | if unref_pointer in symbols: 120 | unref_symbol = symbols[unref_pointer] 121 | if len(reserved_prefix) > 0 and not unref_symbol.startswith(reserved_prefix): 122 | continue 123 | if len(filter_prefix) > 0 and unref_symbol.startswith(filter_prefix): 124 | continue 125 | unref_symbols.add(unref_symbol) 126 | if len(unref_symbols) == 0: 127 | exit('Finish:class unref null') 128 | return filter_super_class(unref_symbols) 129 | 130 | 131 | if __name__ == '__main__': 132 | path = input('Please input app path\nFor example:/Users/yuencong/Library/Developer/Xcode/DerivedData/***/Build/Products/Dev-iphoneos/***.app\n').strip() 133 | path = verified_app_path(path) 134 | if not path: 135 | sys.exit('Error:invalid app path') 136 | 137 | reserved_prefix = '' 138 | filter_prefix = '' 139 | unref_symbols = class_unref_symbols(path, reserved_prefix, filter_prefix) 140 | script_path = sys.path[0].strip() 141 | 142 | f = open(script_path + '/result.txt','w') 143 | f.write('classunrefs count: %d\n' % len(unref_symbols)) 144 | f.write('Precondition: reserve class startwiths \'%s\', filter class startwiths \'%s\'.\n\n' %(reserved_prefix, filter_prefix)) 145 | for unref_symbol in unref_symbols: 146 | print('classunref: ' + unref_symbol) 147 | f.write(unref_symbol + "\n") 148 | f.close() 149 | 150 | print('Done! result.txt already stored in script dir.') 151 | --------------------------------------------------------------------------------