本文将提供一种静态分析的方式,用于查找可执行文件中未使用的方法,源码连接:xuezhulian/selectorsunref。git
分析Mach-o
文件中的__DATA __objc_selrefs
段获得使用到的方法,经过otool
找出实现的全部方法。取差集获得未使用的方法。而后过滤setter和getter,过滤协议方法,再加上一些其它的过滤规则获得最终的结果。github
def unref_selectors(path): ref_sels = ref_selectors(path) imp_sels = imp_selectors(path) protocol_sels = protocol_selectors(path) unref_sels = set() for sel in imp_sels: if ignore_selectors(sel): continue #protocol sels will not apppear in selrefs section if sel not in ref_sels and sel not in protocol_sels: unref_sels = unref_sels.union(filter_selectors(imp_sels[sel])) return unref_sels 复制代码
使用otool -v -s
输出__DATA __objc_selrefs
段的信息:bash
def ref_selectors(path): re_selrefs = re.compile('__TEXT:__objc_methname:(.+)') ref_sels = set() lines = os.popen('/usr/bin/otool -v -s __DATA __objc_selrefs %s' % path).readlines() for line in lines: results = re_selrefs.findall(line) if results: ref_sels.add(results[0]) return ref_sels 复制代码
输出示例:markdown
00000001030f7ce8 __TEXT:__objc_methname:getMessageRequestFromQQ:
00000001030f7cf0 __TEXT:__objc_methname:SendMessageToQQRequest:
00000001030f7cf8 __TEXT:__objc_methname:responseToGetMessageFromQQ:
00000001030f7d00 __TEXT:__objc_methname:responseToShowMessageFromQQ:
复制代码
匹配__TEXT:__objc_methname:(.+)
获得使用到的方法。数据结构
使用otool -oV
输出可执行文件的详细信息, 在__DATA,__objc_classlist
这个段里面记录了类实现的方法的相关信息:app
Contents of (__DATA,__objc_classlist) section 0000000102bdc190 0x103117798 _OBJC_CLASS_$_EpisodeDetailStatusCell isa 0x103117770 _OBJC_METACLASS_$_EpisodeDetailStatusCell superclass 0x103152988 _OBJC_CLASS_$_TableViewCell cache 0x0 __objc_empty_cache vtable 0x0 data 0x102be84c0 (struct class_ro_t *) flags 0x184 RO_HAS_CXX_STRUCTORS instanceStart 8 instanceSize 16 reserved 0x0 ivarLayout 0x102a2a78f layout map: 0x01 name 0x102a2a775 TTEpisodeDetailStatusCell baseMethods 0x102be83d0 (struct method_list_t *) entsize 24 count 7 name 0x1028606b7 setupConstraintsAdditional types 0x102a489fe v16@0:8 imp 0x10000c1a8 -[TTEpisodeDetailStatusCell setupConstraintsAdditional] name 0x1028606d2 setupUpdateConstraintsAdditional types 0x102a489fe v16@0:8 imp 0x10000c7b8 -[TTEpisodeDetailStatusCell setupUpdateConstraintsAdditional] name 0x1028606f3 bindDataWithEpisode:replayInfo: types 0x102a48a20 v32@0:8@16@24 imp 0x10000d014 -[TTEpisodeDetailStatusCell bindDataWithEpisode:replayInfo:] ... ... 复制代码
经过匹配\s*imp 0x\w+ ([+|-]\[.+\s(.+)\])
获得实现的方法,存储的数据结构{sel:set("-[class sel]","-[class sel]")}
。oop
for line in os.popen('/usr/bin/otool -oV %s' % path).xreadlines(): results = re_sel_imp.findall(line) if results: (class_sel, sel) = results[0] if sel in imp_sels: imp_sels[sel].add(class_sel) else: imp_sels[sel] = set([class_sel]) 复制代码
直接对ivar
赋值,不会触发property
的setter
和getter
,这些方法即便不被调用,也不可以删除。 otool -oV
能够输出类的protertieslist
:post
baseProperties 0x102be84a8 entsize 16 count 1 name 0x10293aaa5 pinkPointView attributes 0x10293aab3 T@"UIView",&,N,V_pinkPointView 复制代码
匹配baseProperties
区间,经过\s*name 0x\w+ (.+)
匹配类的属性,此时也就获得了对应的setter和getter方法。spa
#delete setter and getter methods as ivar assignment will not trigger them if re_properties_start.findall(line): is_properties_area = True if re_properties_end.findall(line): is_properties_area = False if is_properties_area: property_result = re_property.findall(line) if property_result: property_name = property_result[0] if property_name and property_name in imp_sels: #properties layout in mach-o is after func imp imp_sels.pop(property_name) setter = 'set' + property_name[0].upper() + property_name[1:] + ':' if setter in imp_sels: imp_sels.pop(setter) 复制代码
协议调用的方法不会出如今__DATA __objc_selrefs
这个段里面,过滤协议方法采用的策略是找到相应的.h
文件,正则匹配文件中包含的协议方法。3d
def header_protocol_selectors(file_path): protocol_sels = set() file = open(file_path, 'r') is_protocol_area = False for line in file.readlines(): #delete description line = re.sub('\".*\"', '', line) #delete annotation line = re.sub('//.*', '', line) #match @protocol if re.compile('\s*@protocol\s*\w+').findall(line): is_protocol_area = True #match @end if re.compile('\s*@end').findall(line): is_protocol_area = False #match sel if is_protocol_area and re.compile('\s*[-|+]\s*\(').findall(line): sel_content_match_result = None if ':' in line: #match sel with parameters sel_content_match_result = re.compile('\w+\s*:').findall(line) else: #match sel without parameters sel_content_match_result = re.compile('\w+\s*;').findall(line) if sel_content_match_result: protocol_sels.add(''.join(sel_content_match_result).replace(';', '')) file.close() return protocol_sels 复制代码
otool -L
能够打印可执行文件引用到的library
,加上公共前缀/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'
,获得绝对路径。使用find
命令递归查找该目录下全部的.h
文件。
#get system librareis lines = os.popen('otool -L ' + path).readlines() for line in lines: line = line.strip() #delete description line = re.sub('\(.*\)', '', line).strip() if line.startswith('/System/Library/'): library_dir = system_base_dir + '/'.join(line.split('/')[0:-1]) if os.path.isdir(library_dir): header_files = header_files.union(os.popen('find %s -name \"*.h\"' % library_dir).readlines()) 复制代码
otool -oV
的输出来看,baseProtocols
会包含协议的方法,可是一些pod
仓库经过.a
文件导入到宿主工程,这个时候拿不到方法的符号。最终过滤自定义协议方法的时候采用的策略和系统协议方法相同。递归遍历工程目录(脚本须要输入的第二个参数)下的.h
文件,匹配协议方法。
header_files = header_files.union(os.popen('find %s -name \"*.h\"' % project_dir).readlines()) for header_path in header_files: header_protocol_sels = header_protocol_selectors(header_path) if header_protocol_sels: protocol_sels = protocol_sels.union(header_protocol_sels) 复制代码
根据输出的结果,对一些系统方法进行了过滤。
def ignore_selectors(sel): if sel == '.cxx_destruct': return True if sel == 'load': return True return False 复制代码
为了过滤第三方库的方法,只保留了带有某些前缀的类的方法,这里须要根据实际状况自行修改reserved_prefixs
。
def filter_selectors(sels): filter_sels = set() for sel in sels: for prefix in reserved_prefixs: if sel.startswith(prefix): filter_sels.add(sel) return filter_sels 复制代码
最终结果保存在脚本路径下的selectorunref.txt
文件中。和以前整理过的iOS代码瘦身实践:删除无用的类 同样,这个方式只能作静态分析,对动态调用无效,最终是否须要删除,还须要手动确认。