iOS代码瘦身实践之如何删除无用的类

前言

本文将提供一种静态分析的方式,用于查找可执行文件Mach-o中未使用的类,源码链接:xuezhulian/classunref (本地下载)。

Mach-o文件中__DATA __objc_classrefs段记录了引用类的地址,__DATA __objc_classlist段记录了所有类的地址,取差集可以得到未使用的类的地址,然后进行符号化,就可以得到未被引用的类信息。

引用类地址

可以通过Mac自带的工具otool打印Mach-o中的段信息,需要注意的是模拟器和真机对应的可执行文件,数据的存储方式不同需要加以区分。

可以通过file命令获取到arch。

#binary_file_arch: distinguish Big-Endian and Little-Endian
#file -b output example: Mach-O 64-bit executable arm64
binary_file_arch = os.popen('file -b ' + path).read().split(' ')[-1].strip()

在取类地址的时候区分x86_64和arm。

def pointers_from_binary(line, binary_file_arch):
 line = line[16:].strip().split(' ')
 pointers = set()
 if binary_file_arch == 'x86_64':
  #untreated line example:00000001030cec80	d8 75 15 03 01 00 00 00 68 77 15 03 01 00 00 00
  pointers.add(''.join(line[4:8][::-1] + line[0:4][::-1]))
  pointers.add(''.join(line[12:16][::-1] + line[8:12][::-1]))
  return pointers
 #arm64 confirmed,armv7 arm7s unconfirmed
 if binary_file_arch.startswith('arm'):
  #untreated line example:00000001030bcd20	03138580 00000001 03138878 00000001
  pointers.add(line[1] + line[0])
  pointers.add(line[3] + line[2])
  return pointers
 return None

通过otool -v -s __DATA __objc_classrefs获取到引用类的地址。

def class_ref_pointers(path, binary_file_arch):
  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)
    ref_pointers = ref_pointers.union(pointers)
  return ref_pointers

所有类地址

通过otool -v -s __DATA __objc_classlist获取所有类的地址。

def class_list_pointers(path, binary_file_arch):
  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)
    list_pointers = list_pointers.union(pointers)
  return list_pointers

取差集

用所有类信息减去引用类的信息,此时我们可以拿到未使用类的地址信息。

unref_pointers = class_list_pointers(path, binary_file_arch) - class_ref_pointers(path, binary_file_arch)

符号化

通过nm -nm命令可以得到地址和对应的类名字。

def class_symbols(path):
  symbols = {}
  #class symbol format from nm: 0000000103113f68 (__DATA,__objc_data) external _OBJC_CLASS_$_EpisodeStatusDetailItemView
  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]
      symbols[address] = symbol
  return symbols

过滤

在实际分析的过程中发现,如果一个类的子类被实例化,父类未被实例化,此时父类不会出现在__objc_classrefs这个段里,在未使用的类中需要将这一部分父类过滤出去。使用otool -oV可以获取到类的继承关系。

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{9} _OBJC_CLASS_\$_(.+)")
  #subclass example: 0000000102bd8070 0x103113f68 _OBJC_CLASS_$_TTEpisodeStatusDetailItemView
  #superclass example: superclass 0x10313bb80 _OBJC_CLASS_$_TTBaseControl
  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]

    if len(subclass_name) > 0 and len(superclass_name) > 0:
      if superclass_name in unref_symbols and subclass_name not in unref_symbols:
        unref_symbols.remove(superclass_name)
      superclass_name = ""
      subclass_name = ""
  return unref_symbols

为了防止一些三方库的误伤,还可以去过滤一些前缀,或者是是仅保留带有某些前缀的类。

 for unref_pointer in unref_pointers:
    if unref_pointer in symbols:
      unref_symbol = symbols[unref_pointer]
      if len(reserved_prefix) > 0 and not unref_symbol.startswith(reserved_prefix):
        continue
      if len(filter_prefix) > 0 and unref_symbol.startswith(filter_prefix):
        continue
      unref_symbols.add(unref_symbol)

最终结果保存在脚本目录下。

script_path = sys.path[0].strip()
f = open(script_path+"/result.txt","w")
f.write( "unref class number:  %d\n" % len(unref_symbles))
f.write("\n")
for unref_symble in unref_symbles:
  f.write(unref_symble+"\n")
f.close()

这个思路在一定程度上能够减少代码的冗余,减小包的体积。因为是静态分析,不能包括动态调用的情况,对于需要删除的类需要进一步的确认。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • iOS性能优化教程之页面加载速率详解

    前言 我认为在编码过程中时刻注意性能影响是有必要的,但凡事都有个度,不能为了性能耽误了开发进度.在时间紧急的情况下我们往往采用"quick and dirty"的方案来快速出成果,后面再迭代优化,即所谓的敏捷开发.与之相对应的是传统软件开发中的瀑布流开发流程. 卡顿产生的原因 在 iOS 系统中,图像内容展示到屏幕的过程需要 CPU 和 GPU 共同参与.CPU 负责计算显示内容,比如视图的创建.布局计算.图片解码.文本绘制等.随后 CPU 会将计算好的内容提交到 GPU 去,由 GP

  • 详解优化iOS程序性能的25个方法

    1. 用ARC管理内存 ARC(Automatic ReferenceCounting, 自动引用计数)和iOS5一起发布,它避免了最常见的也就是经常是由于我们忘记释放内存所造成的内存泄露.它自动为你管理retain和release的过程,所以你就不必去手动干预了.忘掉代码段结尾的release简直像记得吃饭一样简单.而ARC会自动在底层为你做这些工作.除了帮你避免内存泄露,ARC还可以帮你提高性能,它能保证释放掉不再需要的对象的内存. 现在所有的iOS程序都用ARC了,这条可以忽略. 2. 在

  • iOS性能优化浅析

    本文将从原理出发,解释卡顿发生的原理,然后会讲解项目中行之有效的几个优化点,最后会展望一下接下来将要尝试的方向.下面进入正题. 屏幕显示的原理 屏幕显示原理 我们知道,远古时代的CRT显示器的显示原理是用电子枪扫描荧光屏来发光.如上图所示,电子枪按照从左到右,然后从上到下的顺序扫描.当电子枪换到新的一行准备进行扫描时,也就是上图A4.B4.C4.D4的位置,显示器会发出一个水平同步信号:而当一帧画面绘制完成后,电子枪回复到原位准备画下一帧前,也就是上图D4的位置,显示器会发出一个垂直同步信号.垂

  • IOS 性能优化中离屏渲染

    GPU屏幕渲染有以下两种方式: On-Screen Rendering 意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行. Off-Screen Rendering 意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作. 特殊的离屏渲染: 如果将不在GPU的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的"离屏渲染"方式: CPU渲染. 如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了

  • IOS开发中加载大量网络图片优化方法

    IOS开发中加载大量网络图片如何优化 1.概述 在IOS下通过URL读一张网络图片并不像其他编程语言那样可以直接把图片路径放到图片路径的位置就ok,而是需要我们通过一段类似流的方式去加载网络图片,接着才能把图片放入图片路径显示.比如: -(UIImage *) getImageFromURL:(NSString *)fileURL { //NSLog(@"执行图片下载函数"); UIImage * result; NSData * data = [NSData dataWithCont

  • iOS多语言本地化流程的优化方案

    前言 多语言本地化,是我们在做IOS项目的时候经常用的,下面根据自己的经验和使用场景,来全面的说说多语言本地化的解决方案.本文从提升效率和减少错误两方面对传统的多语言本地化方式进行了优化,虽然标题是iOS,但其实macOS也通用.下面话不多说了,来一起看看详细的介绍吧. 传统的方法 在 Localizable.strings 中写入多种语言的版本,然后使用 NSLocalizedString 进行本地化: # en.lproj/Localizable.strings "login" =

  • iOS体验性优化之RTL适配右滑返回的实现

    简述 所谓RTL方向布局就是right to left direction.也就是界面中的元素总是按从右往左的方向进行排列布局,大部分国家的书写以及排列习惯都是从左往右,是LTR方向布局,而对于一些阿拉伯国家,文字的书写以及展示的顺序都是从右往左方向的. iOS的导航支持左滑手势返回上一个界面,这是果粉普遍喜欢的一个特性,iOS7之后的APP适配大多会保留这一特性,慢慢的大多用户已经有了这种操作习惯,对于iPhone的无虚拟键,这种操作也能增加比较友好的用户体验. 在公司新项目之前,没有考虑过多

  • iOS开发教程之常见的性能优化技巧

    前言 性能问题的主要原因是什么,原因有相同的,也有不同的,但归根到底,不外乎内存使用.代码效率.合适的策略逻辑.代码质量.安装包体积这一类问题. 但从用户体验的角度去思考,当我们置身处地得把自己当做用户去玩一款应用时候,那么都会在意什么呢?假如正在玩一款手游,首先一定不希望玩着玩着突然闪退,然后就是不希望卡顿,其次就是耗电和耗流量不希望太严重,最后就是安装包希望能小一点.简单归类如下: 快:使用时避免出现卡顿,响应速度快,减少用户等待的时间,满足用户期望. 稳:不要在用户使用过程中崩溃和无响应.

  • iOS代码瘦身实践之如何删除无用的类

    前言 本文将提供一种静态分析的方式,用于查找可执行文件Mach-o中未使用的类,源码链接:xuezhulian/classunref (本地下载). Mach-o文件中__DATA __objc_classrefs段记录了引用类的地址,__DATA __objc_classlist段记录了所有类的地址,取差集可以得到未使用的类的地址,然后进行符号化,就可以得到未被引用的类信息. 引用类地址 可以通过Mac自带的工具otool打印Mach-o中的段信息,需要注意的是模拟器和真机对应的可执行文件,数

  • 微信小程序 限制1M的瘦身技巧与方法详解

    微信小程序瘦身方法: 众所周知,微信小程序在发布的时候,对提交的代码有1M大小的限制!所以,如果你正在写一个功能稍微复杂一点的小程序,就必须得时刻小心注意你的代码是不是快触及这个底线了. 在设计一个小程序之初,我们就需要重点考虑这一点,采取一些方法,来避免太早的遭遇这个问题. 避免使用本地大图片.大资源文件 请避免在小程序的UI上使用大图片,应该尽可能的通过颜色样式来装点你的小程序 或者,使用小而精致的小图标来点缀 如果实在想用大图片,那请不要将图片放进小程序的本地代码中,应该采用从一个远程UR

  • spring boot项目fat jar瘦身的实现

    一.项目背景 spring cloud构建spring boot项目,精细化各项目的层次,达到降低耦合度的目的,项目间基于restful通信. 在对项目打包过程中,使用spring-boot-maven-plugin插件打包,生成的是fat jar,解压该jar包,会发现项目依赖的jar包存放于BOOT-INF下的lib文件夹中,分析多个子项目后会发现,相同的jar包占绝大多数,然后每次部署于线上环境,各系统的共同jar在服务器上其实是重复搁置的,因此自然会想到是否有方法将共同的jar包,或是不

  • maven工程中jar包瘦身的五种方法

    java项目中常用maven工具来进行工程管理,但经常遇到的一个问题是生成的jar包越来越大,编译一次工程越来越慢.怎么有效地去除冗余依赖,给jar包进行瘦身,是一项必备技能.下面介绍在maven工程中jar包瘦身五大法: 一.将环境中已包含的依赖包的scope设置为provided pom中依赖的部分包可能在你程序运行环境中已经包含,此时应该将依赖包的scope设置为provided.如protobuf包如在环境中已包含,则应设置为: <dependency> <groupId>

  • 压缩技术给SQL Server备份文件瘦身

    可是,其体积仍然很庞大.所以,在日常工作中,如何给SQL Server的备份文件瘦身,就是很多数据库管理员所关心的问题了. 也许微软的数据库产品设计专家听到了众多数据库管理员的呼声了吧.在最新的2008版本的SQLServer数据库中,提出了备份压缩的概念.其基本原理跟利用RAR等压缩工具压缩文件一样,可以让原有的备份文件体积更小.这直接带来的好处,就是可以节省服务器的备份空间.另外,若SQLServer数据库配置了异地备份的话,那么也可以节省网络带宽,缩短异地备份的时间,等等. 笔者前不久刚把

  • MySQL的InnoDB扩容及ibdata1文件瘦身方案完全解析

    mysql的innodb扩容 为了添加一个数据文件到表空间中,首先要关闭 MySQL 数据库,编辑 my.cnf 文件,确认innodb ibdata文件的实际情况和my.cnf的配置是否一致,这里有两种情况: 1.my.cnf的配置 innodb_data_file_path=ibdata1:10G;ibdata2:10G:autoextend 如果当前数据库正在使用ibdata1,或者使用ibdata2,但ibdata2没有超过10G,则对my.cnf配置直接改成: innodb_data_

  • 详解SpringBoot迭代发布JAR瘦身配置

    默认情况下,插件 spring-boot-maven-plugin 会把整个项目打包成一个可运行的Jar包(即所谓的Flat Jar),导致了这个Jar包很大(通常有几十M+).如今迭代发布时常有的事情,每次都上传一个如此庞大的文件,会浪费很多时间,有些时候上传的过程中还会出错. 造成Jar包很大的根本原因就是依赖第三方的jar很多,很大,下面我们就把第三方的JAR与项目代码分离,第三方的JAR把移除到lib文件夹中,即可实现为我们的可执行JAR瘦身,配置如下: <plugins> <!

  • SpringBoot项目Jar包如何瘦身部署的实现

    一.背景 SpringBoot 为我们快速开发提供了很好的架子,使得我们只需要少量配置就能开始我们的开发工作,但是当我们需要打包上传部署时,却是很神伤的一个问题,因为打出来的 Jar 包少则十几兆,多则一百来兆,我们需要上传至公网服务器时,是非常慢的,这就引出了今天的主题,SpringBoot项目Jar包如何瘦身部署 1.思路分析 jar,我们可以看出,jar 包里面分为以下三个模块 分为 BOOT-INF,META-INF,org 三个部分,打开 BOOT-INF 可以看到有 classes,

  • 了不起的11个JavaScript代码重构最佳实践小结

    模式和重构之间有着一种与生俱来的关系.从某种角度来看,设计模式的目的就是为许多重构行为提供目标. 1.提炼函数 在JavaScript开发中,我们大部分时间都在与函数打交道,所以我们希望这些函数有着良好的命名,函数体内包含的逻辑清晰明了.如果一个函数过长,不得不加上若干注释才能让这个函数显得易读一些,那这些函数就很有必要进行重构. 如果在函数中有一段代码可以被独立出来,那我们最好把这些代码放进另外一个独立的函数中.这是一种很常见的优化工作,这样做的好处主要有以下几点. 避免出现超大函数. 独立出

  • 详解Redis瘦身指南

    Redis内存回收 Redis 服务器的最大占用内存量由配置项 maxmemory 决定,我们可以通过 config set maxmemory 2GB 的格式来配置.一旦 Redis 内存满,所有引起内存增加的操作都会被返回 error.作为专业 Redis 服务器我们通常将此项设置为0,以服务器系统内存来作为限制: 那么 Redis 使用内存达到了上限怎么办?Redis 为我们提供了几种选项以自动回收内存,可以通过配置项 maxmemory-policy 来配置: noeviction 不回

随机推荐