写点什么

百度 APP iOS 端包体积 50M 优化实践 (三) 资源优化

作者:百度Geek说
  • 2023-06-06
    上海
  • 本文字数:6201 字

    阅读完需:约 20 分钟

百度APP iOS端包体积50M优化实践(三) 资源优化

01 前言

百度 APP iOS 端包体积优化系列文章的前两篇重点介绍了包体积优化整体方案、各项优化收益和图片优化方案,图片优化是从无用图片、Asset Catalog 和 HEIC 格式三个角度做深度优化。本文重点介绍资源优化,在百度 APP 实践中,资源优化包括大资源优化、无用配置文件和重复资源优化。不管是资源优化还是代码优化,都需要分析 Mach-O 文件,以获取资源和代码的引用关系,本文先详细介绍 Mach-O 文件。


百度 APP iOS 端包体积优化实践系列文章回顾:


百度APP iOS端包体积50M优化实践(一)总览


百度APP iOS端包体积50M优化实践(二) 图片优化

02 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 0x100009980isa        0x1000099a8superclass 0x0 _OBJC_CLASS_$_UIViewControllercache      0x0 __objc_empty_cachevtable     0x0data       0x1000083e8flags          0x90instanceStart  8instanceSize   8reserved       0x0ivarLayout     0x0name           0x100007349 ViewControllerbaseMethods    0x1000082d8entsize 24count   11name    0x100006424 test4types   0x1000073e4 v16@0:8imp     0x100004c58name    0x1000063b4 viewDidLoad*****
复制代码


下面列举 otool 常见命令:


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_t  cpusubtype;  /* 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 字段具体含义

各个字段具体含义如下所示:


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) */#define  LC_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 类型及其具体作用如下面表格所示:


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 */};
复制代码



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。


03 资源优化

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.2 无用的配置文件

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)    for allDir in pathDir:        child = os.path.join('%s/%s' % (path, allDir))        if os.path.isfile(child):            md5 = img_to_md5(child)            # 将md5存入字典            key = md5            file_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
复制代码

04 总结

资源优化是包体积优化的重头戏,优化的过程中影响面可控,所以落地收益比较容易,百度 APP 经过两个季度的优化落地 12M 的收益,基本解决存量资源的优化问题,同时建立资源使用规范和相应的检测流水线解决增量问题。


本文对 Mach-O 文件格式做了系统阐释,并且详细介绍了百度 APP 大资源优化、无用配置文件和重复资源优化方案,后续我们会针对其他优化详细介绍其原理与实现,敬请期待。


—— END——


参考资料:


[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


推荐阅读:


代码级质量技术之基本框架介绍


基于openfaas托管脚本的实践


百度工程师移动开发避坑指南——Swift语言篇


百度工程师移动开发避坑指南——内存泄漏篇


增强型语言模型——走向通用智能的道路?


基于公共信箱的全量消息实现

发布于: 刚刚阅读数: 4
用户头像

百度Geek说

关注

百度官方技术账号 2021-01-22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
百度APP iOS端包体积50M优化实践(三) 资源优化_ios_百度Geek说_InfoQ写作社区