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.1MachOView剖析

  • MachOView下载地址:sourceforge.net/projects/ma…

  • MachOView源码地址:github.com/gdbinit/Mac…

用MachOView能检查MachO文件信息,发动MachOView,在状态栏中点击file,翻开MachO文件,如下图所示。

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

2.2.2otool指令检查

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常见指令:

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

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 整体结构

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

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数据值:

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

2.4.2.3 字段详细含义

各个字段详细含义如下所示:

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

2.4.3LoadCommands(加载指令)

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东西可更直观地调查详细字段,如右下图所示。

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

2.4.3.3cmd类型及其详细作用

常见的cmd类型及其详细作用如下面表格所示:

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

2.4.3.4LC_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 */
};

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

Mach-O文件有多个段(Segment),每个段有不同的功用,每个段又按不同功用划分为多个区(section),四个Segment为__PAGEZERO、__TEXT、_DATA和_LINKEDIT,下面详细介绍。

2.4.3.4.2_PAGEZERO

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

__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

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

__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

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

__DATA Segment对应的便是数据段,maxprot和initprot值都为VM_PROT_READ和VM_PROT_WRITE,数据段权限是可读和可写。

2.4.3.4.5_LINKEDIT

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

__LINKEDIT Segment用于描绘链接信息段,指向存放 link 操作必要的数据段。

2.4.4Data(数据段)

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

Mach-O的Data部分,其实是真正存储APP二进制数据的地方,前面的header和load command,仅是提供文件的阐明以及加载信息的功用。

Data(数据段): 首要是代码、数据,包含了Load commands中需求的各个段(Segment)的数据,每个Segment能够有多个Section,下面列举一些常见的 Section。在Data(数据段)中,大写的字符串(如__TEXT)代表的是Segment,小写的字符串(如__objc_methtype)代表的是Section。

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

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.2mach-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.4JS&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内核介绍:developer.apple.com/library/arc…

[2]、《深化解析Mac OS X & iOS操作体系》

[3]、XNU源码:github.com/apple/darwi…

[4]、Mach-O介绍:alexdremov.me/mystery-of-…

[5]、初识Mach-O文件:www.jianshu.com/p/81928c705…

引荐阅览:

代码级质量技能之基本结构介绍

基于openfaas托管脚本的实践

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

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

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

基于公共信箱的全量消息完成