京东商城iOS客户端安装包瘦身实践

一.概述

随着业务的快速增加,商城app的大小也在迅速增加,一度超过了300M。安装包大小的不断增加对app下载成本,推广效率产生了比较大的影响。从2018年9月份我们对商城app开始了为期二期的专项瘦身工作:一期从V7.2.0-V7.5.2版本,共计瘦身46M(设备:iPhone X,iOS12、13)。为了进一步减小包大小,同时为了建立长效机制,从今年5月份开始了第二期的专项优化工作,二期优化从最高的V8.1.0版本的272M到现在的V8.3.6共计已完成瘦身41M,当然二期优化还在继续推进中。

在本次安装包瘦身过程中我们也遇到了不少坑,当然也积累了些经验,现在分享给大家。

二.优化方向

优化指标

itunes connect上有两种包大小显示:“Download Size”“Install Size”“Download Size”即下载包大小,超过150M需要使用无线网下载的限制就是这个大小(现在已经放宽到200M);“Install Size”即安装后占用的磁盘空间大小,在appstore上显示的也是这个大小,用户往往会误认为这是下载安装包消耗的流量大小。 所以一开始我们就将“Install Size”作为了优化指标。“Install Size”减小后,“Download Size”自然也会减小。

优化方向

ipa 解压后,JD4iPhone.app主要的成分如下:

  • Frameworks,动态库存放路径;
  • PlugIns,插件存放路径,如today extension;
  • 可执行文件;
  • Assets.car,Asset Catalog编译产物;
  • react.bundle,内置的ReactNative业务;
  • bundle,主要存放资源文件;
  • 其他文件;

这些都是包大小的主要影响因素,优化工作都是围绕这些元素进行,后续的监控点也主要是这些元素。

三.优化措施

1.动态库

由于iOS8对可执行文件__Text字段60M限制,商城App多个版本逼近60M大关,所以有不少模块以动态库的形式集成到工程中。在做启动优化的时候,流量较低的库都采用手动方式加载了,以优化前的V8.1.0 版本为例,共计19个动态库,总大小超过了100M(包含arm64armv7架构)。所以动态库的优化成了瘦身工作的重要组成部分。那么怎么给动态库瘦身呢?

1.1 梳理动态库使用方,精简代码

梳理之前我们以为使用精简代码的方式优化动态库会比较容易,比如去除无用代码,提取公共代码。后来发现这些库大部分来自第三方厂家...nm命令查看这部分库里面有很多重复的类库,因为都是商用的库,虽然努力推动着去优化,但是目前看来收益并不是很高。

1.2 strip

经过调研,我们了解到有两种去除动态库多余符号(符号表等)的方式:

  • 在链接时去除,即在动态库工程中Other Linker Flags中添加-s参数,经过测试:不管是在启动时加载,还是手动方式加载动态库都没问题。于是准备使用这个方案。然而,在执行的时候发现了一个严重的问题:加了此参数后,不能生成完整的dsym文件,这会影响崩溃后符号的解析。于是此方案作罢。

  • 使用strip -x命令处理动态库。因为是对动态库产物进行处理,所以不会对dsym产生影响,经过测试,strip后的动态库,也可以使用dsym文件找到符号。于是我们尝试在工程中添加脚本统一处理工程中的动态库。在添加脚本的时候遇到个问题:动态库被拷贝到沙盒的时候会签名,而我们的strip操作发生在这个后面。在debug环境下,加载动态库的时候会提示签名后动态库被修改的错误。而在release导出包的时候会重新对动态库进行签名。所以在release下不会有问题。最终,我们修改了脚本,只在release环境下,执行strip操作:

    if [ $CONFIGURATION == Release ]; then 
        strip -x dylib路径 
    fi
    

经过strip处理后共计减少28M(arm64+armv7),瘦身效果明显。

2.资源文件

在一期优化前通用包(包含多种指令集,分辨率图片,多种机型通用的包)中的图片在 13500 左右,在 V8.1.0 版本通用包中的的图片还11000 张以上。资源文件的数量非常之多,使用场景异常复杂。资源文件的优化自然成了我们花费精力最多的地方,我们需要一整套的方案去应对接下来的优化。

2.1 资源文件的归属

在优化之前,我们需要将不同的资源文件归属到对应的模块,落实到对应的负责人。得益于商城App高度的模块化,资源文件的归属、甚至获取模块代码大小变得很简单:

  • 根据资源文件在本地工程project.pbxproj所在模块的路径和ipa包中的资源文件进行匹配。

  • 分析 linkmap 文件,获取各个静态库组件代码部分在可执行文件中的占比。

2.2 单个资源文件大小统计

以往,我们常常以为本地开发工程中的资源文件大小就是最后安装包中的大小。而经过本次瘦身实践发现:很多图片在本地和安装包中的大小差异非常之大,往往相差几倍。通过调研知道 Apple 为了在优化 iPhone 设备读取 png 图片速度,将 png 转换成 CgBI 非标准的 png 格式:

  • extra critical chunk (CgBI)
  • byteswapped (RGBA -> BGRA) pixel data, presumably for high-speed direct blitting to the framebuffer
  • zlib header, footer, and CRC removed from the IDAT chunk
  • premultiplied alpha (color' = color * alpha / 255)

苹果的优化,对于大多数应用来说都是包大小的负优化,商城也不例外。所以一般的压缩(有损,无损)处理并不能达到很好的瘦身效果。而经过测试,并不是所有的资源文件都会被负优化

  • 放在根目录下png格式的图片。

  • 放在Asset Catalog中的png,jpg格式的图片,其中jpg会转成png。

放在根目录下的jpg,bundle中的png不会被优化,这个规律也在后续优化中起到了重要作用。

终上所述,我们决定使用安装包中的资源文件来统计大小。

2.2.1 Asset Catalog中的文件大小计算

Assest.car 做为 Asset Catalog 的编译产物,苹果也提供了命令行工具assetutil来获取car文件信息:

sudo xcrun --sdk iphoneos assetutil --info xxx/Assets.car > xxx/Assets.json

Assets.json 中的图片详细数据如下:

  {
    "AssetType" : "Image",
    "BitsPerComponent" : 8,
    "ColorModel" : "RGB",
    "Colorspace" : "srgb",
    "Compression" : "lzvn",
    "Encoding" : "ARGB",
    "Idiom" : "universal",
    "Image Type" : "kCoreThemeOnePartScale",
    "Name" : "1001", //xxx.imageset 的文件名
    "NameIdentifier" : 11584,
    "Opaque" : false,
    "PixelHeight" : 48,
    "PixelWidth" : 72,
    "RenditionName" : "1001@3x.png",//工程文件中的实际图片名
    "Scale" : 3,
    "SHA1Digest" : "E34FCAC314E26DE7FF30442AA33E436B242AA4BA",
    "SizeOnDisk" : 800,//占用的磁盘大小,Asset Catalog中的图片编译后的大小取改值。
    "Template Mode" : "automatic"
  },

最终我们使用SizeOnDisk 字段来获取图片大小。使用SizeOnDisk计算精度很高(所有图片的SizeOnDisk相加和car文件大小误差在1M以内)。

对于 Assets.car 的分析,还有个小插曲:我们最开始是使用 cartool 解压。分享模块在更换双十一大促氛围兜底图后,因为部分活动图片大于了32KB(微博分享缩略限制不能超过32KB),触发调用兜底图分享逻辑,分享失败,最后定位是因为 Apple 的负优化,将源大小为22KB,负优化后部分设备上大于了 32KB,而 cartool 解压出的图片大小为 18KB,所以放弃原先使用 cartool,改用 assetutil。

2.2.2 其他的资源文件大小计算

其他的文件大小计算方式相对简单,苹果的APFS文件系统的最小存储单元为4KB,即使只有几十字节大小的文件,占用的空间也是4KB。 对于安装包里面的独立文件我们使用4KB对齐的方式进行大小计算,有些大点的文件磁盘占用空间并不是4的整数倍,但大小相近,影响不大:

Math.ceil(size/4000)*4,size为代码、图片实际大小,单位字节;

在 MB、KB、Byte 之间的换也是对齐 Apple 使用的是 1000 而不是 1024(即1MB = 1000 KB = 1000*1000 Byte)。

2.3 模块所用资源文件大小,数量统计

在介绍模块的资源文件大小统计前,先简单介绍下 App Slicing ,在我们将ipa包提交到iTunes connect,App store会针对不同的设备,系统制作成不同的精简版app:可执行文件,动态库根据不用的指令集,Asset Catalog中的资源文件根据不同的屏幕分辨率进行分发,最终做到按需下载。
apple_app_slicing_illustration.jpg

关于App Slicing 的内容详细叙述,感兴趣的可以查看App Thinning in Xcode

App Slicing只会对在Asset Catalog的资源文件进行分发,而放在根目录,bundle中的资源文件不会分发,所以在统计模块所使用资源文件之前我们需要注意到这个特性。如果以通用包来统计模块使用的资源文件大小,数量,其实并不能真正反映此模块对整个安装包大小的影响。所以我们决定使用iPhone X,iOS11做为参考标准。

要统计单个设备的资源文件使用情况,一个方式是使用adhoc包导出支持单个设备的安装包统计,不过这样的方式需要每次集成后都需要单独打包,因为现在ci并不会出支持单个设备的包。后来我们发现assetutil除了可以导出car文件信息之外,还可以从通用包car文件导出指定设备的car文件,入参较多,经过尝试如下:

sudo xcrun --sdk iphoneos assetutil --idiom phone --subtype 570 --scale 3 --display-gamut srgb --graphicsclass MTL2,2 --graphicsclassfallbacks MTL1,2:GLES2,0 --memory 1 --hostedidioms car,watch xxx/Assets.car -o xxx/thinning_assets.car

从上文知道car以外的资源文件不会分发,获取指定设备的car文件后我们就可以计算出模块所用资源文件大小,数量。

2.4 资源文件优化

2.4.1 大资源文件优化

上文提到了苹果对图片的负优化,大图经过负优化后对安装包的大小影响更大,动辄几百K, 甚至上M 。这也是一期优化通过改造 Assets.car 中的 183 张图片能优化了近 30M 原因。

结合上文中负优化规律,改造处理方案如下:

  • 删除无用或者可以使用其他方案替换的图片;
  • 优先转网络下载,使用默认图/纯色兜底,如楼层背景图;
  • 不能转下载的使用压缩过的jpg格式图片。
  • 图片经过压缩后( 主要是tinypng有损压缩)后放到 bundle 中使用。

二期优化开始,对大资源的处理不在局限于 Assets.car 中的大图(大于50KB),对于放在 bundle 中的大图、音视频、模型文件针对这部分大文件,逐一梳理后并针对性处理,收益很高。

2.4.2 无用图片筛查

对于无用图片,毫无疑问就是删除,商城因为各个组件是以二进制的形式呈现,现有的组件化环境没有提供所有组件一键切换二进制和源码的能力,不能使用现有的源码扫描工具,扫描无用图片。

通过分析现有检测无用图片的原理:根据各种类型的源文件,通过正则表达式获取使用的图片名,获取使用图片集合,扫描所有图片名集合,取所有图片集合和使用图片集合差值获取无用图片集合;

获取到使用图片集合和所有图片集合,就可以过滤出无用图片。
既然扫描源码的路走不通,又不能放任不管,我们反其道而行通过安装包来扫描无用图片:

通过分析安装包中使用图片的文件有可执行文件、.plist、.js、.jsbundle、.nib、.stroyboardc等文件;可分为三类文件:

  • 可执行文件;
  • 可读文件(.plist、.js、.html);
  • 不可读文件(.nib、.storyboardc);

可执行文件通过 otool -v -s __TEXT __cstring 获取可执行文件中的 __TEXT segment 中的 __cstring section。__cstring 包含了可执行文件中的字符串常量(源码中的 @“xxx” 字符串);

不可读文件 .nib 和 .storyboardc 分别是 xib 和 storyboard 的构建产物。ibtool 是xib 和 stroyborad 的编译工具,通过 man 查看具 ibtool 的具体使用方法发现:--flatten NO --compile 组合使用的时候可以生成可运行、可执行的 .nib 和 stroyboardc 文件。

--flatten boolean
    When combined with the --compile option and a value of NO, --flatten instructs ibtool to produce an output file that is both runnable, and editable. This option is typically used when preparing a product for localization. If no value is specified, the default flattening option is YES.

查看 Xcode Build Settings, 提供了配置项:
image.png

商城使用 Cocoapods 管理组件化,在 Pods-xxx-resources.sh 文件也找到了 ibtool 的使用:
image.png

分别在 Build Settings 中设置 Flatten Compile Storyboard Files && Flatten Compile Xib Files 设置为 NO,Pods-xxx-resources.sh 中 ibtool 添加 --flatten NO 的入参后,编译后的 .nib 和 .storyboardc 点击右键显示包内容如下:
image.pngimage.png
designtime.nib、designtime.storyborad 和 xib、stroyborad 一样本质是xml 文件。

(1)针对不同的文件,使用 otool正则和 直接读取将获取到的内容拼接成引用图片的超字符 str,遍历所有图片名是否被 str 包含;
(2)如果包含直接过滤;
(3)如果不包含,再判断是否是 image_%ld 这类相似图片,如果是过滤;
(4)开发人员确定无误后删除,存在部分字符串拼接被误扫的,添加白名单过滤。

最后我们扩展 ResourceHelper 的能力,支持通过构建产物扫描无用图,实践中除了不到二十张图片通过字符串拼接的图片被误扫外(添加白名单过滤),其他图片都验证为无用图片,商城中的无用图也成功的清除和监控起来。

版本涉及组件图片数量对齐磁盘空间后图片总大小
v8.1.0466134.4M
v8.2.4281961.5M

2.4.3 转下载

在进行大图,无用图片处理的同时,我们也给出了便于本地图片转下载的方案,基本功能如下:

  • 模块内内置默认配置文件,支持对不同分辨率的机型加载对应的图片。
{
    "imageId1": {
        "3x": "url1",
        "2x": "url2"
    },
    "imageId2": {
        "3x": "url3",
        "2x": "url4"
    }
   .
   .
   .
}
  • 支持图片url的在线更新。
  • 支持在线的图片降质,webp压缩。

哪些图片适合转下载?

  • 功能性引导图
  • 背景图:如楼层背景,页面背景。
  • 标签,提示类的图片。
  • 其他入口较深的图片。

2.4.4 iconfont

即使经过了图片转下在,无用图片删除,但是工程中的图片数量还是极为可观,其中各种各样的icon图标占了不少的数量。为了进一步减少图片数量,我们引入了iconfont。
iconfont优点:

  • 矢量,缩放不失真。
  • 可以设置颜色。
  • 接入成本低,不需要引入额外的类库。

iconfont 可以解决因为icon大小,颜色不同而重新切图的窘境。
从京东内部的quark平台了解到目前已经可以很好的支持iconfont,目前已有模块在试用,后续我们也将会大规模推广。

2.4.5 监控

为了建立长效机制,我们拟出了资源文件使用规范,同时也搭建了资源分析系统,来跟踪各模块的资源使用情况,主要功能如下:

  • 安装包大小,资源文件大小数量,包成分的展示;
  • 各模块,包括JDReact模块资源文件使用情况的记录展示;
  • 各模块不同版本间的对比;
  • 违反资源文件使用规范模块的邮件触达(开发中);
  • 根据各模块的资源文件使用情况,动态给出优化建议(开发中);

2.5 内置的ReactNative业务

JDReact提供了预置和后装两种发布方式,而为了用户体验,大部分业务模块都选择使用预置包的方式。时间一长,文件的数量就越来越多。由于文件系统的4K对齐,对包大小的影响也是非常大。对内置的ReactNative业务优化如下:

  • 推动流量相对较低的模块转后装方式;
  • 根据资源文件使用规范,推动业务整改;

2.6 可执行文件

因为 iOS8 60M的限制,主执行文件的大小一直比较稳定,所以这部分工作的优先级放到了后面。

2.6.1无用类/方法

无用类通过 otool 逆向Mach-O文件 __DATA.__objc_classlist段和__DATA.__objc_classrefs 段获取所有 OC 类和被引用的类,两个集合差值为无用类集合,结合 nm -nm 得到地址和对应类名符号化无用类类名;根据商城的限制做过滤,规则如下:

  • otool 逆向 __DATA.__objc_nlclslist 获取实现 load 方法的类过滤(RN与原生的桥接类、Swizzle Method 类);
  • 通过 otool 逆向 __TEXT.__cstring 获取所有字符串常量,过滤通过 NSClassFromString 调用的;
  • 子类实例化,父类没有实例化,父类不会出现在中 __objc_classrefs,通过 otool -oV 逆向出类的继承关系,过滤出子类被实例化(NSClassFromString 调用),父类没有实例化(NSClassFromString 调用)的类;
  • 过滤使用 Plist 文件引用的类;

无用方法 通过 otool 逆向 __DATA.__objc_selrefs 段获取使用到的方法,通过 otool -oV 获取实现的所有方法取差值。然后过滤掉 setter、getter、系统方法和协议、自定义的协议、sel 调用。

结合 linkMap 映射出无用的方法和类归属的组件,并且初步量化大小,如下所示:

 业务组件数量大小基础组件数量大小
无用类41214590KB2261212KB
无用方法641434430KB491182259KB

因为基础组件中的无用方法和类,不能确定是否被非商城的 App 使用,只能对业务组件优化,考虑到涉及组件众多,并且收益和工程量不成正比,并且删除方法风险比较大,将无用方法和类优化的优先级后滞。

2.7 插件

在ipa包中我们也注意到了PlugIns目录,这里主要存放一些插件,比如today extension,share extension等,虽然这些插件在整个ipa包中的大小占比不大,但是我们还是决定梳理下有没有优化点。梳理后发现这些插件对于一些基础类库(网络框架,图片加载框架等)的使用都是以拷贝代码的方式加到工程中...我们知道其实这些类库完全可以和主app共享,因为主app中这些库是以动态库的形式使用的。经过优化后,成功的将today extension的大小减少了0.9M(嗯~蚊子虽小...)。

2.8 Xcode配置

除了以上提到的优化点,我们也对Xcode对包大小优化的一些相关配置做了尝试:

2.8.1 Link-Time Optimization(LTO)

苹果在WWDC2016对LTO的介绍如下:

What is Link-Time Optimization (LTO)?
Maximize runtime performance by optimizing at link-time
Inline functions across source files
Remove dead code
Enable powerful whole program optimizations

通过修改Build Settings中的Link-Time Optimization=Incremental,测试后ipa包减少4M,后续经过灰度验证后可以考虑打开。

2.8.2 Compress PNG Files & Remove Text Metadata From PNG Fils

上文提到的 负优化 使png格式图片增大,那么能否关闭负优化?在尝试将 Compress PNG Files 设置为 NO 对包大小没有任何影响,想放弃又不甘心,通过创建新的 Demo 工程测试,通过查看 Build 日志发现是通过copypng 将原 png 图片复制到构建产物根目录的,幸运的是 copypng 不是一个可执行文件,而是一个由 perl 编写的脚本。 copypng部分源码如下:

#!/usr/bin/perl

my $PNGCRUSH = `xcrun -f pngcrush`;
chomp $PNGCRUSH;

my $compress = 0;
my $stripPNGText = 0;
my @FILES = ();

# Gather command line options.
while( @ARGV ) {
    $_ = shift @ARGV;
    next if ( $_ eq "" );
    if ( $_ =~ /-strip-PNG-text/ ) {
        $compress = 1;
        $stripPNGText = 1;
        next;
    }
    if ( $_ eq "-compress" ) {
        $compress = 1;
        next;
    }
}
my @args;
if ( $compress ) {
    @args = ( $PNGCRUSH, "-q", "-iphone", "-f", "0" );
    if ( $stripPNGText ) {
        push ( @args, "-rem", "text" );
    }
    push ( @args, $SRCFILE, $DSTFILE );
} else {
    @args = ( "cp", "$SRCFILE", "$DSTFILE" );
}

其中 Compress PNG Files 和 Remove Text Metadata From PNG Fils 分别对应入参为 -compress 和 -strip-PNG-text。看到源码,即使我们不懂 perl 也应该明白了。 为什么 Compress PNG Files 设置为 NO,不能取消负优化,要想取消根目录下的负优化,需要将 Compress PNG Files 和 Remove Text Metadata From PNG File 都设置为 NO 才能取消

测试将 Compress PNG Files 和 Remove Text Metadata From PNG File 设置为 NO 之后安装包优化 1.6M

同时也探究其对 Assets.car 的影响,通过对比是取消负优化对 Assets.car 的编译工具 actool 的影响,取消后没有 --compress-pngs 的入参。

--compress-pngs
PNGs copied into iOS targets will be processed using pngcrush to optimize reading the images on iOS devices. This has no effect for images that wind up in the compiled CAR file, as it only affects PNG images copied in to the output bundle.

在验证了 Compress PNG Files 和 Remove Text Metadata From PNG File 对 actool 是否有入参 --compress-pngs 的关系 后,我们也验证了对大小的影响,结论是取消负优化,不会影响 Assets.car 的大小

在二期优化过程中通过梳理根目录下的图片,现在只剩下 AppIcon 和 Launch Iamge,对于 Launch Iamge 我们通过 Launch Screen Storyboard 只保留一份启动图优化包大小,不考虑取消负优化。同时通过创建一个新工程只给 Asset Catalog 中添加图片, 查看 Build Settings 是没有这两项配置项,可是 Build 日志 actool 也是有 --compress-pngs 的入参。相信 Apple 已经给我们做了最佳的选择。

2.8.3 Asset Catalog Compiler 之 Optimization

对于 Optimization 中的 space 的优化,在一期就想通过灰度验证是否有其他影响,如果没有影响后启用,因为那时启用精简包可以优化十几M,在后面重点开始优化 Assets.car 后,考虑到启用之后可能会消极的优化 Assets.car 就搁置,到目前商城最新版 8.4.0 以 iPhoneX 的 adhoc 包数据对比重点优化 Assets.car 后,启用也只在 iOS12 以下 1.1M 的影响。

iPhoneXiOS 11iOS 12iOS 13
default17.3M15.5M14.9M
space16.2M15.5M14.9M
time17.3M15.5M14.9M

Apple 在 iOS 12 Optimizing App Assets。space 也不准备启用,还是那句话相信 Apple 已经给我们做了最佳的选择。

3.最后

以上即为商城App包大小优化的主要措施,希望对大家有用。随着业务的快速发展,包大小的增加在所难免。但是在整个业务迭代过程中,我们需要更加合理的设计规范,资源文件使用规范,合理的业务准入准出原则。这样我们的安装包大小才能得到较好的控制,毕竟优化过程也是相当痛苦的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值