一、背景
内部CICD平台在做APP产物分析,有一项是无用类和无用方法的产出,本篇主要从代码层面通过删除无用类做一些优化
二、方案整理
业界方案
- 第一种:通过otool分析mach-o文件,得出 无用集合类 = 全集合类和引用集合类做差集
- 第二种:通过分析linkmap文件
- 第三种:clang插桩进行代码覆盖率扫描(我的其他文档有介绍,感兴趣的小伙伴可以进入我的主页查找)
本篇文章方案是第一种
三、实践
环境
- otool
- python3以上版本
注意
本篇文章主要是针对OC工程扫描,如果是swift混编工程会有问题
流程图
具体步骤
获取所有类
python
def class_list_pointers(path, binary_file_arch):
print('获取项目中所有的类...')
list_pointers = set()
lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classlist %s' % path).readlines()
for line in lines:
pointers = pointers_from_binary(line, binary_file_arch)
if not pointers:
continue
list_pointers = list_pointers.union(pointers)
if len(list_pointers) == 0:
exit('Error:class list pointers null')
return list_pointers
获取引用类
python
def class_ref_pointers(path, binary_file_arch):
print('获取项目中所有被引用的类...')
ref_pointers = set()
lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classrefs %s' % path).readlines()
for line in lines:
pointers = pointers_from_binary(line, binary_file_arch)
if not pointers:
continue
ref_pointers = ref_pointers.union(pointers)
if len(ref_pointers) == 0:
exit('Error:class ref pointers null')
return ref_pointers
做差集合得到无用类
python
unref_pointers = 获取所有类结果 - 获取引用类结果
对无用类进行符号化
python
def class_symbols(path):
print('通过符号表中的符号,获取类名...')
symbols = {}
# class symbol format from nm: 0000000103113f68 (__DATA,__objc_data) external _OBJC_CLASS_$_TTEpisodeStatusDetailItemView
re_class_name = re.compile('(\w{16}) .* _OBJC_CLASS_\$_(.+)')
lines = os.popen('nm -nm %s' % path).readlines()
for line in lines:
result = re_class_name.findall(line)
if result:
(address, symbol) = result[0]
# print(result)
symbols[address] = symbol
if len(symbols) == 0:
exit('Error:class symbols null')
return symbols
def unuse_class_symbols =(unref_pointers, symbols):
unref_symbols = set()
for unref_pointer in unref_pointers:
if unref_pointer in symbols:
unref_symbol = symbols[unref_pointer]
unref_symbols.add(unref_symbol)
if len(unref_symbols) == 0:
exit('Finish:class unref null')
return unref_symbol
对无用类进一步处理过滤
第一步:根据黑白名单进行过滤
- 如果存在黑名单,删除命中黑名单的类(一般输入系统类、第三方库类名的前缀)
- 如果存在白名单,只保留命中白名单的类(一般输入我们关心库的类名的前缀)
python
# 黑白名单过滤
def filtration_list(unref_symbols, blackList, whiteList):
# 数组拷贝
temp_unref_symbols = list(unref_symbols)
if len(blackList) > 0:
# 如果黑名单存在,那么将在黑名单中的前缀都过滤掉
for unrefSymbol in temp_unref_symbols:
for blackPrefix in blackList:
if unrefSymbol.startswith(blackPrefix) and unrefSymbol in unref_symbols:
unref_symbols.remove(unrefSymbol)
break
# 数组拷贝
temp_array = []
if len(whiteList) > 0:
# 如果白名单存在,只留下白名单中的部分
for unrefSymbol in unref_symbols:
for whitePrefix in whiteList:
if unrefSymbol.startswith(whitePrefix):
temp_array.append(unrefSymbol)
break
unref_symbols = temp_array
return unref_symbols
第二步:过滤通过runtime的形式调用的类,例如使用字符串的形式进行调用
python
# 检测通过runtime的形式,类使用字符串的形式进行调用,如果查到,可以认为用过
def filter_use_string_class(path, unref_symbols):
str_class_name = re.compile("\w{16} (.+)")
# 获取项目中所有的字符串 @"JRClass"
lines = os.popen('/usr/bin/otool -v -s __TEXT __cstring %s' % path).readlines()
for line in lines:
stringArray = str_class_name.findall(line)
if len(stringArray) > 0:
tempStr = stringArray[0]
if tempStr in unref_symbols:
unref_symbols.remove(tempStr)
continue
return unref_symbols
第三步:过滤存在子类被使用的类
python
def filter_super_class(unref_symbols):
re_subclass_name = re.compile("\w{16} 0x\w{9} _OBJC_CLASS_\$_(.+)")
re_superclass_name = re.compile("\s*superclass 0x\w* _OBJC_CLASS_\$_(.+)")
lines = os.popen("/usr/bin/otool -oV %s" % path).readlines()
subclass_name = ""
superclass_name = ""
for line in lines:
subclass_match_result = re_subclass_name.findall(line)
if subclass_match_result:
subclass_name = subclass_match_result[0]
superclass_name = ''
superclass_match_result = re_superclass_name.findall(line)
if superclass_match_result:
superclass_name = superclass_match_result[0]
if len(subclass_name) > 0 and len(superclass_name) > 0:
if superclass_name in unref_symbols and subclass_name not in unref_symbols:
# print("删除的父类 -- %s %s" % (superclass_name, subclass_name))
unref_symbols.remove(superclass_name)
superclass_name = ''
subclass_name = ''
return unref_symbols
第四步:过滤掉有load方法的类
python
def filter_category_use_load_class(path, unref_symbols):
re_load_category_class = re.compile("\s*imp\s*0x\w*\s*[+|-]\[(.+)\(\w*\) load\]")
lines = os.popen("/usr/bin/otool -oV %s" % path).readlines()
for line in lines:
load_category_match_result = re_load_category_class.findall(line)
if len(load_category_match_result) > 0:
re_load_category_class_name = load_category_match_result[0]
if re_load_category_class_name in unref_symbols:
unref_symbols.remove(re_load_category_class_name)
return unref_symbols
根据之前过滤的无用使用类去检索是否存在一些存在一些类的属性里但是没有使用的类,把这一部分进行输出
kotlin
# 查找所有的未使用到的类,是否出现在了相关类的属性中
# 自己作为自己的属性不算
def find_ivars_is_unuse_class(path, unref_sels):
# {'MyTableViewCell':
# [{'ivar_name': 'superModel', 'ivar_type': 'SuperModel'}, {'ivar_name': 'showViewA', 'ivar_type': 'ShowViewA'}, {'ivar_name': 'dataSource111', 'ivar_type': 'NSArray'}],
# 'AppDelegate': [{'ivar_name': 'window', 'ivar_type': 'UIWindow'}]}
imp_ivars_info = find_allclassivars.get_all_class_ivars(path)
temp_list = list(unref_sels)
find_ivars_class_list = []
for unuse_class in temp_list:
for key in imp_ivars_info.keys():
# 当前类包含自己类型的属性不做校验
if key == unuse_class:
continue
else:
ivars_list = imp_ivars_info[key]
is_find = 0
for ivar in ivars_list:
if unuse_class == ivar["ivar_type"]:
unref_symbols.remove(unuse_class)
find_ivars_class_list.append(unuse_class)
is_find = 1
break
if is_find == 1:
break
return unref_symbols, find_ivars_class_list
把无用类列表和存在一些属性里但是没有使用的类列表写入文件
python
def write_to_file(unref_symbols, find_ivars_class_list):
script_path = sys.path[0].strip()
file_name = 'find_class_unRefs.txt'
f = open(script_path + '/' + file_name, 'w')
f.write('查找到未使用的类: %d个,【请在项目中二次确认无误后再进行相关操作】\n' % len(unref_symbols))
num = 1
if len(find_ivars_class_list):
show_title = "\n查找结果:\n只作为其他类的成员变量,不确定有没有真正被使用,请在项目中查看 --------"
print(show_title)
f.write(show_title + "\n")
for name in find_ivars_class_list:
find_ivars_class_str = ("%d : %s" % (num, name))
print(find_ivars_class_str)
f.write(find_ivars_class_str + "\n")
num = num + 1
num = 1
print("--------")
for unref_symbol in unref_symbols:
showStr = ('%d : %s' % (num, unref_symbol))
print(showStr)
f.write(showStr + "\n")
num = num + 1
f.close()
根据工程确认无误后进行删除
四、成果
优化前 | 优化后 |
---|---|
313MB | 311MB |