静态分析还是基于Mach-O文件,默认已经了解Mach-O相关概念。这篇主要说实际过程中遇到的一些问题。
Mach-O静态分析遇到的问题
常规逻辑我们认为Mach-O中未使用的类是:unused_class = __objc_classlist – __objc_classrefs,实践过程中这样得到的结果存在一些偏差。主要有以下几个情况,记录在__objc_classrefs
的section中:
1、+load方法
这个类重写了+load方法实现了一些逻辑。
解决:__DATA __objc_nlclslist
中存放了实现了+load方法的类信息,所以:
unused_class = __objc_classlist – __objc_classrefs – __objc_nlclslist
def get_imp_load_class_points(path, binary_file_arch):
list_pointers = set()
lines = os.popen("/usr/bin/otool -v -s __DATA __objc_nlclslist %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:
return None
return list_pointers
复制代码
2、动态调用的类
OC是动态语言,可以通过字符串动态实例化一个类。例如以字符串硬编码形式的路由注册,最终使用NSClassFromString(@"ClassName")
方式调用,xib视图通过类名初始化。
def get_cstring_list(path):
cstring_list = set()
re_cstring = re.compile("(\w{16}) (.+)")
lines = os.popen("/usr/bin/otool -v -s __TEXT __cstring %s" % path).readlines()
for line in lines:
result = re_cstring.findall(line)
if result:
(address, cstring) = result[0]
cstring = cstring.lstrip()
if len(cstring):
cstring_list.add(cstring)
if len(cstring_list) == 0:
return None
return cstring_list
复制代码
解决:这个问题就可以通过获取Mach-O中定义的字符串常量来一定程度上过滤掉,暂时还没有比较完美的方案,__TEXT __cstring
段中存放了代码中定义的C/OC的字符串常量,这个字符常量如果在unused_class中,认为可能会被动态调用,我们把这个类保留下来。这个部分可能会相对耗时,毕竟对整个项目的字符串的遍历,根据实际时间可以一定程度上的优化时长
3、仅作为属性/成员变量使用
作为属性和成员变量可以认为是同一类,属性会自动的生成成员变量的相关代码
4、作为基类
某个类作为基类,实际使用的是它的子类,基类本身并未被使用到,这个时候也不会记录。例如网络请求类,通过子类继承方式发起请求
解决3&4:otool -ov命令可以获取Mach-O详细信息,逐行读取内容可以拿到某一个类的信息,里面包含继承关系、元类、方法、属性、成员变量等等信息,假设父类在unused_class中,子类不在unused_class,则认为父类也是使用的
def filter_super_class_and_ivars(unref_class_list):
re_subclass_name = re.compile("\w{16} 0x\w{9} _OBJC_CLASS_\$_(.+)") # 子类
re_superclass_name = re.compile("\s*superclass 0x\w{9} _OBJC_CLASS_\$_(.+)") # 父类
re_ivars_type_name = re.compile(" {12}type {6}0x\w{9} @\"(.+)\"") # 成员变量
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_match_result = re_superclass_name.findall(line)
if superclass_match_result:
superclass_name = superclass_match_result[0]
ivars_type_name_result = re_ivars_type_name.findall(line)
if ivars_type_name_result:
ivars_type_name = ivars_type_name_result[0]
if ivars_type_name in unref_class_list:
unref_class_list.remove(ivars_type_name)
if len(subclass_name) > 0 and len(superclass_name) > 0:
if superclass_name in unref_class_list and subclass_name not in unref_class_list:
unref_syunref_class_listmbols.remove(superclass_name)
superclass_name = ""
subclass_name = ""
return unref_class_list
复制代码
5、B类仅在A类中初始化,但A类未使用,B类是否在ref_class?
答案是肯定的,这种情况需要多次分析才能找到这类的unused_class
组件化工程中如何归类
当分析得到未使用类是一堆无序的类名就有点抓狂,这些都属于不同的组件,所以想有什么办法可以给它按组件来归类,方便各个业务方认领。
归类
cocoapods提供了在install之后hook一些方法,通过Pod::Installer可以获取所有pod对应Taget以及对应的source_build_phase,也就是Xcode中对应的Compile Sources,再和未使用类匹配一下,缺点就是一些定义的内部类会找不到对应的组件。在podfile中实现以下:
post_install do |installer|
installer.pods_project.targets.each do |target|
if !target.name.include?('Pods-')
if !target.instance_of? Xcodeproj::Project::Object::PBXAggregateTarget
puts("-pod name:" + target.name + "\n")
source_files = target.source_build_phase.files
source_files.each do |file|
puts("--class name:" + file.display_name + "\n")
end
puts("\n")
end
end
end
end
复制代码
移除引用
为了快速验证结果也可以使用脚本移除这些类,github.com/CocoaPods/X… 是Cocoapods中来创建和修改Xcode工程文件的组件,以下代码是用来移除Xcode中Build Phases->Compile Sources、Header
target.source_build_phase.remove_file_reference(file.file_ref)
target.headers_build_phase.remove_file_reference(file.file_ref)
installer.pods_project.targets.each do |target|
if !target.name.include?('Pods-')
if !target.instance_of? Xcodeproj::Project::Object::PBXAggregateTarget
unused_classes = unused_class_hash[target.name]
if unused_classes != nil
puts ("Target: #{target.name}".green)
source_files = target.source_build_phase.files
source_files.each do |file|
file_name = file.display_name[0..(file.display_name.length - 3)] # 移除后缀
if unused_classes.include?(file_name)
# 移除 source
target.source_build_phase.remove_file_reference(file.file_ref)
target.source_build_phase.remove_build_file(file)
puts ("-remove source reference: #{file_name}".red)
# 删除XXX-umbrella.h的引用
remove_umbrella_headers(target.name, file_name)
# 移除在代码的引用,只遍历类本身所在组件
delete_code_reference("./Pods/#{target.name}", file_name)
end
end
headers_files = target.headers_build_phase.files
headers_files.each do |file|
file_name = file.display_name[0..(file.display_name.length - 3)] # 移除后缀
if unused_classes.include?(file_name)
# 移除 headers
target.headers_build_phase.remove_file_reference(file.file_ref)
target.headers_build_phase.remove_build_file(file)
end
end
end
end
end
end
end
复制代码
结尾
当然最终确认这个类是否移除还是需要谨慎的校验代码,以上的情况应该可以大部分覆盖和解决unused_class = __objc_classlist – __objc_classrefs的误差。