关于Mach-O静态分析未使用类的问题

静态分析还是基于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的误差。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享