Apple不断放宽在蜂窝网络下,从AppStore下载App的大小限制,大App们的大小都逼近或超过200MB,甚至突破250MB;
2013年9月,iOS 7正式版后,蜂窝网络下App下载大小的限制,从 50 MB 提升至 100 MB;
2017年9月,iOS11正式版本后,限制从 100 MB 提升至 150 MB,并在2019年5月下旬,将 150 MB"默默"放宽到200MB;
2019年9月,iOS13正式版本后,直接放开了蜂窝网络下App下载大小的限制,主要流量够用,随便下;
2020年4月1号了,我们日常用得比较多的App中,微信/手淘/美团App大小突破250MB;滴滴/抖音/快手App大小突破200MB;支付宝App逼近200MB(190MB+),美团外卖App一股清流,才115MB+。
根据Apple的审核要求,上传App Store的iap的可执行文件有大小限制,这里的可执行文件大小不是指二进制(Mach-O)文件大小,而是指二进制(Mach-O)文件中__TEXT
部分的大小。
IOS 7版本之前, 二进制文件中所有__TEXT
部分总和不得超过80MB;
iOS 7.X 至 iOS 8.X,二进制文件中,每个 Architecture Slice(架构片段)中的__TEXT
部分不得超过60MB
iOS 9.0之后,二进制文件中所有__TEXT
部分的总和不超过500 MB;具体可参考最大构建版本文件大小
2020年4月1号了,几乎所有的iOS App兼容的最低版本都是iOS 9起步,如:微信/美团/美团外卖iOS App最低支持iOS10,支付宝/手淘/滴滴/抖音/快手iOS App最低支持iOS 9。
__TEXT
部分的限制,可以优先业务迭代,有人力的情况下,再去做包瘦身;Mach-O文件
周边的知识:Mach-O文件本身、 分析工具和Link Map File等。Mach-O
格式全称为Mach Object文件格式的缩写,是MacOS或者iOS上可执行的程序格式,类似于Windows上的PE格式 (Portable Executable),linux上的ELF格式 (Executable and Linking Format)。Mach-O文件
的分类有如下5类:
Mach-O文件主要包括三部分内容: Header(头部)、Load Commands(加载命令)、Data(数据区)
Header(头部),指明了 CPU 架构、大小端序、文件类型、Load Commands 个数等一些基本信息,Headers 能帮助校验 Mach-O 合法性和定位文件的运行环境,64位架构为例,Header结构定义如下:
struct mach_header_64 { uint32_t magic; /* mach magic number identifier 魔数,用于快速确认该文件用于64位还是32位 */ cpu_type_t cputype; /* cpu specifier,CPU**类型,比如 arm */ cpu_subtype_t cpusubtype; /* machine specifier,对应的具体类型,比如arm64、armv7 */ uint32_t filetype; /* type of file,文件类型,比如可执行文件、库文件、Dsym文件,demo中是2 `MH_EXECUTE`,代表可执行文件*/ uint32_t ncmds; /* number of load commands 加载命令条数 */ uint32_t sizeofcmds; /* the size of all the load commands 所有加载命令的大小 */ uint32_t flags; /* flags 标志位 */ uint32_t reserved; /* reserved 保留字段 */ }; 复制代码
Load Commands(加载命令),包含 Mach-O 里命令类型信息,名称和二进制文件的位置;以64位架构为例,Load Commands结构定义如下:
struct segment_command_64 { /* for 64-bit architectures */ uint32_t cmd; /* cmd是Load commands的类型,LC_SEGMENT_64代表将文件中64位的段映射到进程的地址空间*/ uint32_t cmdsize; /* includes sizeof section_64 structs 代表load command的大小 */ char segname[16]; /* segment name */ uint64_t vmaddr; /* memory address of this segment 段的虚拟内存地址 */ uint64_t vmsize; /* memory size of this segment 段的虚拟内存大小 */ uint64_t fileoff; /* file offset of this segment 段在文件中偏移量 */ uint64_t filesize; /* amount to map from the file 段在文件中的大小 */ vm_prot_t maxprot; /* maximum VM protection */ vm_prot_t initprot; /* initial VM protection */ uint32_t nsects; /* number of sections in segment 标示了Segment中有多少secetion */ uint32_t flags; /* flags */ }; 复制代码
和
LC_SEGMENT` 是加载的主要命令, 他们指导内核来设置进程的内存空间;Data(数据区)由Segment 的数据组成,是 Mach-O
中 占比最多的部分,有代码有数据,比如符号表。Data 共三个 Segment:__TEXT
(包含执行代码以及其他只读数据)、__DATA
(程序数据,该段可写)、__LINKEDIT
(包含链接器使用的符号以及其他表)。
其中,__TEXT
和 __DATA
对应一个或多个 Section,__LINKEDIT
没有 Section,需要配合 LC_SYMTAB
来解析 symbol table 和 string table。这些里面是 Mach-O 的主要数据。
以64位架构为例,Section的结构定义如下:
struct section_64 { /* for 64-bit architectures */ char sectname[16]; /* name of this section 比如_text、stubs */ char segname[16]; /* segment this section goes in 该section所属的segment,比如__TEXT*/ uint64_t addr; /* memory address of this section 该section在内存的起始位置 */ uint64_t size; /* size in bytes of this section 该section的大小*/ uint32_t offset; /* file offset of this section 该section的文件偏移*/ uint32_t align; /* section alignment (power of 2) 字节大小对齐*/ uint32_t reloff; /* file offset of relocation entries 重定位入口的文件偏移 */ uint32_t nreloc; /* number of relocation entries 需要重定位的入口数量 */ uint32_t flags; /* flags (section type and attributes) 包含section的type和attributes*/ uint32_t reserved1; /* reserved (for offset or index) */ uint32_t reserved2; /* reserved (for count or sizeof) */ uint32_t reserved3; /* reserved */ }; 复制代码
备注:__TEXT
代表的是Segment,小写的__text
代表 Section
FatFile/FatBinary
直译“胖二进制”,是一个由不同的编译架构的Mach-O产物合成的集合体。一个架构的Mach-O
只能在相同架构的机器或者模拟器上用,为了支持不同架构需要一个集合体。管理Fat File的工具, 可以查看CPU架构, 提取特定架构,整合和拆分库文件
常用的方法如下:
# 【查】看胖二进制支持的CPU架构列表 lipo -info xxxx.a/xxxx.framework/xxxx # 【拆】从胖二进制中提取特定CPU架构的二进制 lipo lxx.a -thin cpu_type(armv7s/arm64等) -output xx_cpu_type.a # 【合】整合成Fat文件 lipo -create xxxx1 xxxx2 -output xxxxfat #【删】移除掉特定的cpu架构的文件 lipo -remove cpu_type(armv7s/arm64等) xxxx -output xxxx 复制代码
常用来创建、修改库,从库中提出单个模块。
常使用ar命令解压.a文件,但是如果直接解压第三方SDK的.a文件(如微信SDK),会遇到xxx.a is a fat file (use libtool(1) or lipo(1) and ar(1) on it)
的错误。
这是因为这类.a文件是一个胖二进制,包含了多个CPU架构,需要先使用lipo文件来提取特定的CPU架构的二进制文件,使用如下:
# 拆分出个arm64架构的二进制 lipo xx. a -thin arm64 -output xx_arm64.a # 解压.a文件 ar -x xx_arm64.a 复制代码
被用于显示二进制目标文件的符号表(display name list (symbol table))
常用的方法如下:
# 得到Mach-O中的程序符号表 nm path # 目标文件的所有符号 nm -nm path 复制代码
用来判断是否包含字符串
常用的方法如下:
# 检查是否包含xxx字符串: grep -r "xxx” path 复制代码
otool
(object file displaying tool),可以对指定目标文件或者库文件以特定的方法解析显示,是分析Mach-O文件的利器。(一般安装了Xcode,默认安装了otool)
otool -h app_name.app/app_name 复制代码
otool -l app_name.app/app_name 复制代码
otool -L app_name.app/app_name 复制代码
otool -l app_name.app/app_name | grep crypt 复制代码
# 获取所有类的地址 otool -v -s __DATA __objc_classlist app_name.app/app_name # 获取所有引用类的地址 otool -v -s __DATA __objc_classrefs app_name.app/app_name 复制代码
otool
固然方便,但是也可以使用MachOView工具来查看Mach-O文件,更加直观,很方便看到 Mach-O文件header、 load commands等信息,具体使用见Mach-O文件浏览器---MachOViewMach-O
文件的class信息;它利用OC语言的Runtime特性,将存储在Mach-O文件中的头文件信息提取出来,并生成对应的.h文件。$HOME/custom-tool/bin
目录下export PATH=$HOME/custom-tool/bin/:$PATH
,然后保存并退出source ~/.bash_profile
;获取ipa
文件,修改后缀名为.zip
,解压后,获取Payload
文件中的app文件;
需要注意的是,从App Store下载的app文件都是经过加密的,可执行文件被加上了一层外壳,class-dump无法直接作用于这样的文件。需要使用其它方式将外壳破坏才可以。
将app文件放到指定目录下,进入该目录,执行如下命令
# 导出Mach-O头文件(头文件内容按名字排序) class-dump -H Mach-O文件路径 -o 头文件存放目录 复制代码
补充统计文件和文件夹数的命令
# 查看某个文件下的文件个数,包括子文件里的 ls -lR|grep "^-"|wc -l # 查看某文件下的文件夹的个数,包括子文件夹里的 ls -lR|grep "^d"|wc -l 复制代码
工程->Build Setting->Write Link Map File
为YES,Build后生成Link Map File文件的功能;还可以通过设置Path to Link Map File
,指定Link Map File
存放的路径。Path & Arch:Path是可执行文件的路径,Arch是架构类型。
# Path: /Users/xxx/Library/Developer/Xcode/DerivedData/..../app_name.app/app_name # Arch: arm64 复制代码
Object Files:生成二进制用到的link单元(包括.o文件和dylib库)的路径和文件编号;通过类编号可以对应到具体的类。在后面的Symbols部分,我们会用到类编号。
# Object files: [ 0] linker synthesized [ 1] /Users/xxxx/Library/Developer/Xcode/DerivedData/..../AppDelegate.o [ 2] /Users/xxxx/Library/Developer/Xcode/DerivedData/..../main.o # ... 复制代码
Sections: 记录Mach-O中每个Segment/section的地址范围。Mach-O中有三类的Segement,Segement划分成了不同的Section,不同的Section存储着不同的信息:Segement主要有三类:__TEXT
、__DATA
和__LINKEDIT
__TEXT
包含 Mach header,被执行的代码和只读常量(如C 字符串),只读可执行__DATA
包含全局变量,静态变量等,可读写__LINKEDIT
包含包含了加载程序的『元数据』,比如函数的名称和地址,只读。# 第一列是Section起始位置,第二列是Section占用内存大小,第三列是Segment类型,第四列是Section类型。 # Sections: # Address Size Segment Section 0x100002780 0x0129617D __TEXT __text 0x1012988FE 0x000015E4 __TEXT __stubs # ... 复制代码
Symbols: 按顺序记录每个符号的地址范围
# Symbols: // __text代码区 # Address Size File Name 0x100002780 0x00000450 [ 2] -[UIButton(SSEdgeInsets) setImageUpTitleDownWithSpacing:] 0x100002BD0 0x00000070 [ 2] _UIEdgeInsetsMake # ... 复制代码
Address
确定分布的区域,如__TEXT段的__text区
(存储着代码),__TEXT段的__objc_methname区
(存储着方法名)、__DATA的__objc_classlist区
(存储所有的类)等;Address
,还可以通过符号表找到对应出具体的方法名Name
(方法名越长,最终占用的内存也越大)File
编号找到代码属于哪个类;__objc_classlist区
的size值都是8,区域里存储的值都是一个指针,指向了类的虚拟地址。_objc_classname
(所有类名)和__objc_classrefs
(引用到的类)的差集找到未引用的类(未引用的类未必是未使用的类)objc_methname
(所有的方法)和__objc_selrefs
(引用的方法)的差别,找到未引用的方法(未引用的方法未必是未使用的方法)iOS安装包瘦身小记 -- 此文写的比较早,缺失了二进制瘦身这个大内容,后面补充下。
Apple 将 iOS AppStore 下载限制从 150M 提高至 200M
iOS 指令集架构 armv6、armv7、armv7s、arm64、arm64e、x86_64、i386