iOS代码瘦身实践:删除无用的方法
本文将提供一种静态分析的方式,用于查找可执行文件中未使用的方法,源码链接:xuezhulian/selectorsunref[1]。
核心思路
分析Mach-o文件中的__DATA __objc_selrefs段得到使用到的方法,通过otool找出实现的所有方法。取差集得到未使用的方法。然后过滤setter和getter,过滤协议方法,再加上一些其它的过滤规则得到最终的结果。
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段的信息:
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
输出示例:
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这个段里面记录了类实现的方法的相关信息:
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]")}。
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])
过滤setter和getter
直接对ivar赋值,不会触发property的setter和getter,这些方法即使不被调用,也不能够删除。otool -oV可以输出类的protertieslist:
baseProperties 0x102be84a8
entsize 16
count 1
name 0x10293aaa5 pinkPointView
attributes 0x10293aab3 T@"UIView",&,N,V_pinkPointView
匹配baseProperties区间,通过\s*name 0x\w+ (.+)匹配类的属性,此时也就得到了对应的setter和getter方法。
#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)
过滤protocol方法
协议调用的方法不会出现在__DATA __objc_selrefs这个段里面,过滤协议方法采用的策略是找到相应的.h文件,正则匹配文件中包含的协议方法。
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
系统.h文件
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())
自定义.h文件
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代码瘦身实践:删除无用的类
一样,这个方式只能做静态分析,对动态调用无效,最终是否需要删除,还需要手动确认。
参考
[1]https://github.com/xuezhulian/selectorsunref