iOS代码瘦身-删除无用类

一、背景

内部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

五、相关链接

脚本和demo链接

相关推荐
B.-6 小时前
Flutter 应用在真机上调试的流程
android·flutter·ios·xcode·android-studio
明月与玄武7 小时前
关于性能测试:数据库的 SQL 性能优化实战
数据库·sql·性能优化
_乐无14 小时前
Unity 性能优化方案
unity·性能优化·游戏引擎
iFlyCai16 小时前
Xcode 16 pod init失败的解决方案
ios·xcode·swift
2402_857589361 天前
Spring Boot编程训练系统:实战开发技巧
数据库·spring boot·性能优化
爱搞技术的猫猫1 天前
实现API接口的自动化
大数据·运维·数据库·性能优化·自动化·产品经理·1024程序员节
郝晨妤1 天前
HarmonyOS和OpenHarmony区别是什么?鸿蒙和安卓IOS的区别是什么?
android·ios·harmonyos·鸿蒙
Hgc558886661 天前
iOS 18.1,未公开的新功能
ios
CocoaKier1 天前
苹果商店下载链接如何获取
ios·apple
EterNity_TiMe_1 天前
【论文复现】STM32设计的物联网智能鱼缸
stm32·单片机·嵌入式硬件·物联网·学习·性能优化