iOS代码瘦身实践之如何删除无用的类
前言
本文将提供一种静态分析的方式,用于查找可执行文件Mach-o中未使用的类,源码链接:xuezhulian/classunref (本地下载)。
Mach-o文件中__DATA__objc_classrefs段记录了引用类的地址,__DATA__objc_classlist段记录了所有类的地址,取差集可以得到未使用的类的地址,然后进行符号化,就可以得到未被引用的类信息。
引用类地址
可以通过Mac自带的工具otool打印Mach-o中的段信息,需要注意的是模拟器和真机对应的可执行文件,数据的存储方式不同需要加以区分。
可以通过file命令获取到arch。
#binary_file_arch:distinguishBig-EndianandLittle-Endian
#file-boutputexample:Mach-O64-bitexecutablearm64
binary_file_arch=os.popen('file-b'+path).read().split('')[-1].strip()
在取类地址的时候区分x86_64和arm。
defpointers_from_binary(line,binary_file_arch):
line=line[16:].strip().split('')
pointers=set()
ifbinary_file_arch=='x86_64':
#untreatedlineexample:00000001030cec80 d8751503010000006877150301000000
pointers.add(''.join(line[4:8][::-1]+line[0:4][::-1]))
pointers.add(''.join(line[12:16][::-1]+line[8:12][::-1]))
returnpointers
#arm64confirmed,armv7arm7sunconfirmed
ifbinary_file_arch.startswith('arm'):
#untreatedlineexample:00000001030bcd20 03138580000000010313887800000001
pointers.add(line[1]+line[0])
pointers.add(line[3]+line[2])
returnpointers
returnNone
通过otool-v-s__DATA__objc_classrefs获取到引用类的地址。
defclass_ref_pointers(path,binary_file_arch):
ref_pointers=set()
lines=os.popen('/usr/bin/otool-v-s__DATA__objc_classrefs%s'%path).readlines()
forlineinlines:
pointers=pointers_from_binary(line,binary_file_arch)
ref_pointers=ref_pointers.union(pointers)
returnref_pointers
所有类地址
通过otool-v-s__DATA__objc_classlist获取所有类的地址。
defclass_list_pointers(path,binary_file_arch):
list_pointers=set()
lines=os.popen('/usr/bin/otool-v-s__DATA__objc_classlist%s'%path).readlines()
forlineinlines:
pointers=pointers_from_binary(line,binary_file_arch)
list_pointers=list_pointers.union(pointers)
returnlist_pointers
取差集
用所有类信息减去引用类的信息,此时我们可以拿到未使用类的地址信息。
unref_pointers=class_list_pointers(path,binary_file_arch)-class_ref_pointers(path,binary_file_arch)
符号化
通过nm-nm命令可以得到地址和对应的类名字。
defclass_symbols(path):
symbols={}
#classsymbolformatfromnm:0000000103113f68(__DATA,__objc_data)external_OBJC_CLASS_$_EpisodeStatusDetailItemView
re_class_name=re.compile('(\w{16}).*_OBJC_CLASS_\$_(.+)')
lines=os.popen('nm-nm%s'%path).readlines()
forlineinlines:
result=re_class_name.findall(line)
ifresult:
(address,symbol)=result[0]
symbols[address]=symbol
returnsymbols
过滤
在实际分析的过程中发现,如果一个类的子类被实例化,父类未被实例化,此时父类不会出现在__objc_classrefs这个段里,在未使用的类中需要将这一部分父类过滤出去。使用otool-oV可以获取到类的继承关系。
deffilter_super_class(unref_symbols):
re_subclass_name=re.compile("\w{16}0x\w{9}_OBJC_CLASS_\$_(.+)")
re_superclass_name=re.compile("\s*superclass0x\w{9}_OBJC_CLASS_\$_(.+)")
#subclassexample:0000000102bd80700x103113f68_OBJC_CLASS_$_TTEpisodeStatusDetailItemView
#superclassexample:superclass0x10313bb80_OBJC_CLASS_$_TTBaseControl
lines=os.popen("/usr/bin/otool-oV%s"%path).readlines()
subclass_name=""
superclass_name=""
forlineinlines:
subclass_match_result=re_subclass_name.findall(line)
ifsubclass_match_result:
subclass_name=subclass_match_result[0]
superclass_match_result=re_superclass_name.findall(line)
ifsuperclass_match_result:
superclass_name=superclass_match_result[0]
iflen(subclass_name)>0andlen(superclass_name)>0:
ifsuperclass_nameinunref_symbolsandsubclass_namenotinunref_symbols:
unref_symbols.remove(superclass_name)
superclass_name=""
subclass_name=""
returnunref_symbols
为了防止一些三方库的误伤,还可以去过滤一些前缀,或者是是仅保留带有某些前缀的类。
forunref_pointerinunref_pointers: ifunref_pointerinsymbols: unref_symbol=symbols[unref_pointer] iflen(reserved_prefix)>0andnotunref_symbol.startswith(reserved_prefix): continue iflen(filter_prefix)>0andunref_symbol.startswith(filter_prefix): continue unref_symbols.add(unref_symbol)
最终结果保存在脚本目录下。
script_path=sys.path[0].strip()
f=open(script_path+"/result.txt","w")
f.write("unrefclassnumber:%d\n"%len(unref_symbles))
f.write("\n")
forunref_symbleinunref_symbles:
f.write(unref_symble+"\n")
f.close()
这个思路在一定程度上能够减少代码的冗余,减小包的体积。因为是静态分析,不能包括动态调用的情况,对于需要删除的类需要进一步的确认。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对毛票票的支持。