一、背景
内部CICD平台在做APP产物分析,有一项是无用类和无用方法的产出,本篇主要从代码层面通过删除无用方法做一些优化
二、方案整理
通过otool分析mach-o文件
三、实践
环境
- otool
- python3以上版本
注意
本篇文章主要是针对OC工程扫描,如果是swift混编工程会有问题
流程图
具体步骤
第一步:获取所有协议方法列表
python
# 获取protocol中所有的方法
def header_protocol_selectors(file_path):
# 删除路径前后的空格
file_path = file_path.strip()
if not os.path.isfile(file_path):
return None
protocol_sels = set()
file = open(file_path, 'r', encoding='unicode_escape')
is_protocol_area = False
# 开始遍历文件内容
# .decode('UTF8')
for line in file.readlines():
# 删除注释信息
# delete description
# print(line)
# print("***********")
line = re.sub('\".*\"', '', line)
# delete annotation
line = re.sub('//.*', '', line)
# 检测是否是 @protocol
# 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
# - (CGPoint)convertPoint:(CGPoint)point toCoordinateSpace:(id <UICoordinateSpace>)coordinateSpace
if ':' in line:
# match sel with parameters
# 【"convertPoint:","toCoordinateSpace:"]
sel_content_match_result = re.compile('\w+\s*:').findall(line)
else:
# - (void)invalidate;
# match sel without parameters
# invalidate;
sel_content_match_result = re.compile('\w+\s*;').findall(line)
if sel_content_match_result:
# 方法参数拼接
# convertPoint:toCoordinateSpace:
funcList = ''.join(sel_content_match_result).replace(';', '')
protocol_sels.add(funcList)
file.close()
return protocol_sels
# 获取所有protocol定义的方法
def protocol_selectors(path, project_path):
print('获取所有的protocol中的方法...')
header_files = set()
protocol_sels = set()
# 获取当前引用的系统库中的方法列表
system_base_dir = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'
# get system librareis
lines = os.popen('otool -L ' + path).readlines()
for line in lines:
# 去除首尾空格
line = line.strip()
# /System/Library/Frameworks/MediaPlayer.framework/MediaPlayer (compatibility version 1.0.0, current version 1.0.0)
# /System/Library/Frameworks/MediaPlayer.framework/MediaPlayer
# delete description,
line = re.sub('\(.*\)', '', line).strip()
if line.startswith('/System/Library/'):
# [0:-1],获取数组的左起第一个,到倒数最后一个,不包含最后一个,[1,-1)左闭右开
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())
if not os.path.isdir(project_path):
exit('Error: project path error')
# 获取当前路径下面所有的.h文件路径
header_files = header_files.union(os.popen('find %s -name \"*.h\"' % project_path).readlines())
for header_path in header_files:
# 获取所有查找到的文件下面的protocol方法,这些方法,不能用来统计
header_protocol_sels = header_protocol_selectors(header_path)
if header_protocol_sels:
protocol_sels = protocol_sels.union(header_protocol_sels)
return protocol_sels
第二步:从所有协议方法列表里删除命中黑名单的方法
例如:load、c++构造不要扫描
python
def ignore_selectors(sel):
if sel == '.cxx_destruct':
return True
if sel == 'load':
return True
return False
第三步:获取项目所有的方法列表
python
def imp_selectors(path):
print('获取所有的方法,除了setter and getter方法...')
# return struct: {'setupHeaderShadowView':['-[TTBaseViewController setupHeaderShadowView]']}
# imp 0x100001260 -[AppDelegate setWindow:] ==>> -[AppDelegate setWindow:],setWindow:
re_sel_imp = re.compile('\s*imp\s*0x\w+ ([+|-]\[.+\s(.+)\])')
re_properties_start = re.compile('\s*baseProperties 0x\w{9}')
re_properties_end = re.compile('\w{16} 0x\w{9} _OBJC_CLASS_\$_(.+)')
re_property = re.compile('\s*name\s*0x\w+ (.+)')
imp_sels = {}
is_properties_area = False
# "otool - ov"将输出Objective - C类结构及其定义的方法。
for line in os.popen('/usr/bin/otool -oV %s' % path).xreadlines():
results = re_sel_imp.findall(line)
if results:
# imp 0x100001260 -[AppDelegate setWindow:] ==>> [-[AppDelegate setWindow:],setWindow:]
(class_sel, sel) = results[0]
if sel in imp_sels:
imp_sels[sel].add(class_sel)
else:
imp_sels[sel] = set([class_sel])
else:
# delete setter and getter methods as ivar assignment will not trigger them
# 删除相关的set方法
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)
# 拼接set方法
setter = 'set' + property_name[0].upper() + property_name[1:] + ':'
# 干掉set方法
if setter in imp_sels:
imp_sels.pop(setter)
return imp_sels
第四步:获取所有被引用的方法列表
python
def ref_selectors(path):
print('获取所有被调用的方法...')
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
第五步:获取未使用方法列表
python
unref_sels = set()
for sel in imp_sels:
# 如果当前的方法不在protocol中,也不再引用的方法中,那么认为这个方法没有被用到
# 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]))
成果
优化前 | 优化后 |
---|---|
380MB | 376MB |
因为平时大家比较重视,所以优化成果不明显 |