iOS代码瘦身-删除无用方法

一、背景

内部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
因为平时大家比较重视,所以优化成果不明显

相关链接

脚本和demo链接

相关推荐
crasowas2 小时前
Flutter问题记录 - 适配Xcode 16和iOS 18
flutter·ios·xcode
2401_852403552 小时前
Mac导入iPhone的照片怎么删除?快速方法讲解
macos·ios·iphone
SchneeDuan2 小时前
iOS六大设计原则&&设计模式
ios·设计模式·cocoa·设计原则
JohnsonXin18 小时前
【兼容性记录】video标签在 IOS 和 安卓中的问题
android·前端·css·ios·h5·兼容性
蒙娜丽宁19 小时前
Go语言错误处理详解
ios·golang·go·xcode·go1.19
名字不要太长 像我这样就好19 小时前
【iOS】push和pop、present和dismiss
学习·macos·ios·objective-c·cocoa
人工智能培训咨询叶梓1 天前
MobiLlama,面向资源受限设备的轻量级全透明GPT模型
人工智能·gpt·语言模型·自然语言处理·性能优化·多模态·轻量级
S0linteeH1 天前
iOS 18 正式上線,但 Apple Intelligence 還要再等一下
ios
S0linteeH1 天前
iOS 18 新功能:控制中心大變身!控制項目自由選配
ios·iphone
AI智东西1 天前
150+个流行的Instagram标签(及如何找到并正确使用它们)
人工智能·ios·chatgpt·iphone