乐趣区

关于ios:百度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) section
0000000100008238 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 常见命令:

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/demo
Non-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 demo
demo:
Mach header
      magic  cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_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/Dar…

[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 语言篇

百度工程师挪动开发避坑指南——内存透露篇

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

基于公共信箱的全量音讯实现

退出移动版