一. 前言
百度APP iOS端包体积优化系列文案的前两篇重点介绍了包体积优化整体方法、各项优化收益和照片优化方法,照片优化是从没用照片、Asset Catalog和HEIC格式三个方向做深度优化。本文重点介绍资源优化,在百度APP实践中,资源优化包含大资源优化、没用配置文件和重复资源优化。不管是资源优化还是代码优化,都必须分析Mach-O文件,以获取资源和代码的引用关系,本文先仔细介绍Mach-O文件。
百度APP iOS端包体积优化实践系列文案回顾:
《百度APP iOS端包体积50M优化实践(一)总览》:https://mp.weixin.qq.com/s/ANbFzg7X932o-iDpa8FcxQ
《百度APP iOS端包体积50M优化实践(二) 照片优化》:https://mp.weixin.qq.com/s/RR7sjhkuTFgUp7S5E8ECMw
二. Mach-O文件详解
丨2.1 简介Mach-O为Mach Object文件格式的缩写,用于记录可执行文件、目的代码、动态库和内存转储的文件格式,是运用于Mac以及iOS系统上。
丨2.2 分析Mach-O文件的工具
丨2.2.1 MachOView分析
MachOView下载位置: http://sourceforge.net/projects/machoview/
MachOView源码位置:https://github.com/gdbinit/MachOView
用MachOView能查看MachO文件信息,起步MachOView,在状态栏中点击file,打开MachO文件,如下图所示。
丨2.2.2 otool命令查看
mac自带otool工具,otool -arch arm64 -ov xxx.app/xxx,可获取所有项目的类结构及定义的办法,示例代码如下所示:
Contents of (__DATA,__objc_classlist) section0000000100008238 0x100009980 isa 0x1000099a8 superclass 0x0 _OBJC_CLASS_$_UIViewController cache 0x0 __objc_empty_cache vtable 0x0 data 0x1000083e8 flags 0x90 instanceStart 8 instanceSize 8 reserved 0x0 ivarLayout 0x0 name 0x100007349 ViewController baseMethods 0x1000082d8 entsize 24 count 11 name 0x100006424 test4 types 0x1000073e4 v16@0:8 imp 0x100004c58 name 0x1000063b4 viewDidLoad *****
下面列举otool平常命令:
命令
功能
otool -f xxx.app/xxx查看fat headers信息otool -a xxx.app/xxx查看archive header信息otool -h xxx.app/xxx查看Mach-O头结构otool -l xxx.app/xxx查看load commandsotool -L xxx.app/xxx
查看依赖的动态库,包含动态库名叫作、
当前版本号、兼容版本号 otool -t -v xxx.app/xxx查看text section otool -d xxx.app/xxx查看data sectionotool -o xxx.app/xxx查看Objective-C segmentotool -I xxx.app/xxx查看symbol tableotool -v -s __TEXT __cstring获取所有静态字符串
otool -v -s
__TEXT __objc_methname
xxx.app/xxx 获取所有办法名叫作
丨2.3 查看文件格式
采用file命令能够查看文件格式,lipo -info可查看该Mach-O文件支持的详细CPU架构。
~ % file /Users/ycx/Desktop/demo.app/demo/Users/ycx/Desktop/demo.app/demo: Mach-O 64-bit executable arm64~ % lipo -info /Users/ycx/Desktop/demo.app/demoNon-fat file: /Users/ycx/Desktop/demo.app/demo is architecture: arm64
丨2.4 文件结构
丨2.4.1 总体结构
Mach-O文件重点由三部分构成Header、LoadCommands、Data,在MachO文件的末尾,还有Loader Info信息,暗示可执行文件依赖的字符串表,符号表等信息。
丨2.4.2 Header(头部)
丨2.4.2.1 数据结构
Header(头部): 用于描述当前Mach-O文件的基本信息(CPU类型、文件类型等),XNU代码路径:EXTERNAL_HEADERS/mach-o/loader.h,数据结构如下所示: struct mach_header_64 { uint32_t magic; /* mach magic number identifier */ cpu_type_t cputype; /* cpu specifier */ cpu_subtype_tcpusubtype;/* machine specifier */ uint32_t filetype; /* type of file */ 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 */};
丨2.4.2.2 查看字段值
命令otool -hv可查看Header每一个字段值。 % otool -hv demodemo:Mach header magic cputype cpusubtype caps filetype ncmds sizeofcmds flagsMH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 22 3040 NOUNDEFS DYLDLINK TWOLEVEL PIE用MachOView查看Header数据值:
丨2.4.2.3 字段详细含义
各个字段详细含义如下所示: 字段说明magic
魔数头,系统加载器经过该字段快速判断文件类型
armv7:FEEDFACE
arm64:FEEDFACF cputypeCPU类型cpusubtypeCPU指定子类型,inter、arm、powerpc等filetype
说明文件类型(可执行文件、库文件、核心转储文件、内核扩展文件、DYSM文件、动态库等)
MH_OBJECT 编译过程中产生的 obj文件
MH_EXECUTE 可执行二进制文件
MH_CORE CoreDump
MH_DYLIB 动态库
MH_DYLINKER 连接器linker
MH_KEXT_BUNDLE 内核扩展文件 ncmds加载命令的条数sizeofcmds加载命令长度flags
dyld加载时的标志位
MH-NOUNDEFS暗示:目的无未定义的符号,不存在链接依赖
MH-DYLDLINK暗示:该目的文件是dyld的输入文件
MH-TWOLEVEL暗示:动态加载二级名叫作空间
MH-PIE暗示:位置空间布局随机化
丨2.4.3 LoadCommands(加载命令)
丨2.4.3.1 数据结构LoadCommands(加载命令): 用于描述文件的组织架构和在虚拟内存中的布局方式,告诉操作系统怎样加载Mach-O文件中的数据。XNU代码路径:EXTERNAL_HEADERS/mach-o/loader.h,数据结构如下所示,其中cmd表率加载命令类型,cmdsize表率加载命令体积,在load_command数据结构后面加一个特定结构体信息,区别的cmd类型,结构体亦区别。struct load_command { uint32_t cmd; /* type of load command */ uint32_t cmdsize; /* total size of command in bytes */};/* Constants for the cmd field of all load commands, the type */#define LC_SEGMENT 0x1 /* segment of this file to be mapped */#define LC_SYMTAB 0x2 /* link-edit stab symbol table info */#define LC_SYMSEG 0x3 /* link-edit gdb symbol table info (obsolete) */#defineLC_THREAD 0x4/* thread */#define LC_UNIXTHREAD 0x5 /* unix thread (includes a stack) */#define LC_LOADFVMLIB 0x6 /* load a specified fixed VM shared library */#define LC_IDFVMLIB 0x7 /* fixed VM shared library identification */#define LC_IDENT 0x8 /* object identification info (obsolete) */#define LC_FVMFILE 0x9 /* fixed VM file inclusion (internal use) */#define LC_PREPAGE 0xa /* prepage command (internal use) */#define LC_DYSYMTAB 0xb /* dynamic link-edit symbol table info */#define LC_LOAD_DYLIB 0xc /* load a dynamically linked shared library */#define LC_ID_DYLIB 0xd /* dynamically linked shared lib ident */#define LC_LOAD_DYLINKER 0xe /* load a dynamic linker */#define LC_ID_DYLINKER 0xf /* dynamic linker identification */#define LC_PREBOUND_DYLIB 0x10 /* modules prebound for a dynamically */*****
丨2.4.3.2 查看字段值
用otool -lv命令能够看到该字段所有信息,如左下图所示,另外,咱们亦可用MachOView工具可更直观地观察详细字段,如右下图所示。
丨2.4.3.3 cmd类型及其详细功效平常的cmd类型及其详细功效如下面表格所示:类型功效LC_SEGMENT/LC_SEGMENT_64将文件中的段映射到进程位置空问中LC_DYLD_INFO_ONLY动态库信息,按照该命令是真正动态库绑定,位置重定向要紧的信息LC_SYMTAB符号表信息LC DYSYMTAB动态符号表信息LC_LOAD_DYLINKER加载动态链接器LC_UUID文件的独一标识,crash解析中亦会有,去匹配dysm文件和crash文件LC_VERSION_MIN_IPHONEOS二进制文件需求的最低操作系统版本 (iOS Deployment Target)LC_MAIN程序主线程的入口位置LC_ENCRYPTION_INFO_64加密信息,查看文件是不是加密,倘若已加密必须砸壳LC_LOAD_DYLIB加载的动态库,包含动态库位置和名叫作,当前版本号,兼容版本号LC_FUNCTION_STARTS函数初始位置表LC_CODE_SIGNATURE代码签名信息
丨2.4.3.4 LC_SEGMENT_64
丨2.4.3.4.1 数据结构
在众多cmd命令中,咱们必须重点关注的是LC_SEGMENT/LC_SEGMENT_64,LC_SEGMENT是32位,LC_SEGMENT_64是64位,日前主流机型是LC_SEGMENT_64。LC_SEGMENT_64功效是怎样将Data中的各个Segment加载入内存中,而和咱们APP关联的代码及数据,大部分位置于各个Segment中。其数据结构名叫作是segment_command_64,XNU代码路径:EXTERNAL_HEADERS/mach-o/loader.h,源码如下所示: struct segment_command_64 { /* for 64-bit architectures */ uint32_t cmd; /* LC_SEGMENT_64 */ uint32_t cmdsize; /* includes sizeof section_64 structs */ 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 */ uint32_t flags; /* flags */};字段含义Segment Name
Segment名叫作 VM Address该段被加载后在进程位置空间中的虚拟位置VM Size段的虚拟内存体积File Offset
该段在文件中的偏移 File Size段在文件中的体积maxprot段页面所必须的最高内存守护(可读 可写 可执行)initprot段页面初始的内存守护nsects段中包括section的数量flags其他标志位Mach-O文件有多个段(Segment),每一个段有区别的功能,每一个段又按区别功能划分为多个区(section),四个Segment为__PAGEZERO、__TEXT、_DATA和_LINKEDIT,下面仔细介绍。丨2.4.3.4.2 _PAGEZERO
__PAGEZERO Segment是空指针陷阱段,重点是用来捉捕NULL指针的引用,是Mach内核虚拟出来的,是Mach-O加载进内存之后附加的一起区域,maxprot和initprot值都为VM_PROT_NONE,暗示它不可读,不可写,倘若拜访__PAGEZERO段,会导致程序崩溃。从上图能够发掘,VM Size是4GB,然则真实的File Size体积是0,它只是一个规律上的段,在Data中,基本无对应的内容,亦无占用任何硬盘空间。丨2.4.3.4.3 _TEXT
__TEXT Segment对应的便是代码段,下图是一张示例截图,其有11个Section,该段对应的内容加载到内存的过程是:从File Offset起始加载体积为File Size的文件,从虚拟位置VM Address起始装填,体积亦是VM Size,VM Size跟文件体积File Size是相同的,咱们发掘其File Offset为0,在Mach-O文件布局中,__TEXT类型的Segment前面有_PAGEZERO类型的Segment,但_PAGEZERO段的File Offse和File Size为0,因此__TEXT段的File Offset为0。
maxprot和initprot值都为VM_PROT_READ和VM_PROT_EXECUTE,代码段权限是只读和可执行,防止在内存中被修改。
丨2.4.3.4.4 _DATA
__DATA Segment对应的便是数据段,maxprot和initprot值都为VM_PROT_READ和VM_PROT_WRITE,数据段权限是可读和可写。
丨2.4.3.4.5 _LINKEDIT
__LINKEDIT Segment用于描述链接信息段,指向存放 link 操作必要的数据段。丨2.4.4 Data(数据段)
Mach-O的Data部分,其实是真正存储APP二进制数据的地区,前面的header和load command,仅是供给文件的说明以及加载信息的功能。 Data(数据段): 重点是代码、数据,包括了Load commands中必须的各个段(Segment)的数据,每一个Segment能够有多个Section,下面列举有些平常的 Section。在Data(数据段)中,大写的字符串(如__TEXT)表率的是Segment,小写的字符串(如__objc_methtype)表率的是Section。Section用途__TEXT.__text
主程序代码 __TEXT.__cstringC 语言字符串__TEXT.__constconst 重要字修饰的常量__TEXT.__stubs
用于 Stub 的占位代码,非常多地区叫作之为桩代码。 __TEXT.__stubs_helper当 Stub 没法找到真正的符号位置后的最后指向__TEXT.__objc_methnameObjective-C 办法名叫作__TEXT.__objc_methtypeObjective-C 办法类型__TEXT.__objc_classnameObjective-C 类名叫作__DATA.__data初始化过的可变数据__DATA.__la_symbol_ptrlazy binding 的指针表,表中的指针一起始都指向 __stub_helper__DATA.nl_symbol_ptr非 lazy binding 的指针表,每一个表项中的指针都指向一个在装载过程中,被动态链设备搜索完成的符号__DATA.__const无初始化过的常量__DATA.__cfstring程序中运用的 Core Foundation 字符串(CFStringRefs)__DATA.__bssBSS,存放为初始化的全局变量,即常说的静态内存分配__DATA.__common无初始化过的符号声明__DATA.__objc_classlistObjective-C 类列表__DATA.__objc_protolistObjective-C 所有的protocol__DATA.__objc_imginfoObjective-C 镜像信息__DATA.__objc_selfrefsObjective-C self 引用__DATA.__objc_protorefsObjective-C 原型引用__DATA.__objc_superrefsObjective-C 超类引用
三. 资源优化
丨3.1 简介
做为一个航母级别的APP,百度APP技术栈丰富多样,市面上平常的技术框架都有运用,如Hybrid框架、小程序框架、React Native框架、KMM和端智能。另外,百度APP做为日活过亿的APP,为满足用户繁杂多变的需求,拥有的功能包罗万象,如搜索、Feed、短视频、直播、购物、小说、地图、网盘、美颜、人脸识别、AR库等,引起内置的大块资源(大于40K)就有26M,拥有很大的优化空间,资源优化分为三个部分,分别是大资源优化、没用配置文件和重复资源优化,本章节接下来仔细介绍各个模块的优化方法。
丨3.2 大资源优化
丨3.2.1 获取大资源
资源指的是plist、js、css、json、端智能模型文件等,因这些文件和照片在优化方式差异很大,因此把两者区掰开来。获取大资源重点途径是递归遍历ipa包的所有资源,体积大于指定阈值的文件便是咱们要针对性优化的大资源,在百度APP优化实践中咱们选择了40K做为阈值,参考脚本如下所示: def findBigResources(path,threshold): pathDir = os.listdir(path) for allDir in pathDir:child = os.path.join(%s%s % (path, allDir)) if os.path.isfile(child): # 获取读到的文件的后缀 end = os.path.splitext(child)[-1] # 过滤掉dylib系统库和asset.car if end != ".dylib" and end != ".car": temp = os.path.getsize(child) # 转换单位:B -> KB fileLen = temp / 1024 if fileLen > threshold: #print(end) print(child + " length is " + str(fileLen)); else: # 递归遍历子目录 child = child + "/" findBigResources(child,threshold)
丨3.2.2 优化办法
异步下载:只要APP首次起步时不必须加载该资源,或即使首次起步必须加载然则运用频率不高,那样该资源就能够走异步下载;
资源压缩:当APP首次起步必须加载且频率较高的状况下,能够对大块资源先进行压缩内置APP,起步周期异步线程解压再运用;
丨3.3 没用的配置文件
丨3.3.1 获取配置文件
从ipa包中获取plist、json、txt、xib等配置文件,百度技术方法采用的是排除法,由于实践中发掘配置文件格式千奇百怪,非常多业务模块出于安全思虑自定义各样后缀文件,没法穷举,因此采用了排除法。针对照片资源咱们有专门的优化办法,因此首要将png、webp、gif、jpg排除掉,JS&CSS资源是通常HTML加载的,在mach-o文件中TEXT字段静态字符串常量不会有表现,因此亦必须排除掉,最后获取到的便是咱们必须的配置文件,参考脚本如下所示: def findProfileResources(path): pathDir = os.listdir(path) for allDir in pathDir: child = os.path.join(%s%s % (path, allDir)) if os.path.isfile(child): # 获取读到的文件的后缀 end = os.path.splitext(child)[-1] if end != ".dylib" and end != ".car" and end != ".png" and end != ".webp" and end != ".gif" and end != ".js" and end != ".css": print(child + " 后缀 " + end) else: # 递归遍历子目录 child = child + "/" findProfileResources(child)
丨3.3.2 mach-o文件获取静态字符串常量咱们加载配置文件的代码经过编译链接最后都会以字符串形式存储到mach-o文件中,详细是TEXT字段静态字符串常量__cstring中,用otool命令能够获取,参考脚本如下所示: lines = os.popen(/usr/bin/otool -v -s __TEXT __cstring %s % path).readlines()
丨3.3.3 获取没用配置文件前面获取的集合做diff,获取没用配置文件,确认没误后删除以减少包体积。倘若你的资源名是拼接运用的,就没法命中,因此删除资源必定要逐个确认。
丨3.3.4 JS&CSS没用文件排查JS&CSS文件拥有特殊性,OC代码能够引用,HTML文件亦能够加载引用,照片亦是这种状况,然则上面说到的mach-o文件中TEXT字段只能覆盖OC文件的引用方式,而HTML加载才是主流场景,为此针对这种case百度APP采用跟没用照片检测类似的处理方法。
丨3.4 重复资源优化
从iPA包中获取所有资源文件,经过MD5判断资源是不是重复,参考脚本如下所示: def get_file_library(path, file_dict): pathDir = os.listdir(path) forallDirin pathDir: child = os.path.join(%s/%s % (path, allDir)) if os.path.isfile(child): md5 = img_to_md5(child) # 将md5存入字典 key = md5file_dict.setdefault(key, []).append(allDir) continue get_file_library(child, file_dict)def img_to_md5(path): fd = open(path, rb) fmd5 = hashlib.md5(fd.read()).hexdigest() fd.close() return fmd5
四. 总结
资源优化是包体积优化的重头戏,优化的过程中影响面可控,因此落地收益比较容易,百度APP经过两个季度的优化落地12M的收益,基本处理存量资源的优化问题,同期创立资源运用规范和相应的检测流水线处理增量问题。
本文对Mach-O文件格式做了系统阐释,并且仔细介绍了百度APP大资源优化、没用配置文件和重复资源优化方法,后续咱们会针对其他优化仔细介绍其原理与实现,敬请期待。
参考链接
[1]、Mach内核介绍:https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/Mach/Mach.html
[2]、《深入解析Mac OS X & iOS操作系统》
[3]、XNU源码:https://github.com/apple/darwin-xnu
[4]、Mach-O介绍:https://alexdremov.me/mystery-of-mach-o-object-file-builders/
[5]、初识Mach-O文件:https://www.jianshu.com/p/81928c705c88
|