货拉拉 iOS 包大小优化探索与实践

4,140 阅读23分钟

hll.png

一. 背景介绍

过去几年,货拉拉业务高速发展,作为核心业务入口的用户端App,在业务与技术上也在与时俱进不断升级迭代,以满足用户多样化的需求,随之而来也遇到了移动端开发同学都会遇到的问题:App需要“瘦身”了,站在用户的角度,以下几个指标是比较容易被感知到的:

  1. 安装包大小
  2. 启动速度
  3. 稳定性
  4. 耗损
  5. 流畅度

包大小意义非凡,它是App重要的基础体验指标之一,用户要安装App首先要面对的是App的包大小,它影响着用户的安装率,安装成功率,卸载率,在2019谷歌开发者大会上,谷歌给出了一个很详细的数据,包体大小每上升6MB,应用下载转化率就会下降1%,不同地区转化率略有差异,每减少10MB ,全球平均下载转化率会提升1.75%。

本文将为大家介绍货拉拉用户端在优化安装包大小方向做的一些探索与尝试,希望能给大家在自家App包大小优化方面提供一些借鉴和思路。

  1. 包大小概念

App包大小可以理解成安装包(即.app文件)大小或者下载包(即.ipa文件)大小,两者的关系是,下载包(即:ipa文件)是用户从App Store商店下载时的压缩包大小,要查看该数据需要在App Connect后台进行查看,当压缩包下载完成后会自动解压,安装到用户的手机上,安装完毕后就变成安装包(即.app文件),占用用户手机的磁盘空间;

一般而言,安装包(即.app文件)会比下载包(即.ipa文件)大很多,安装包大小优化后,下载包自然会相对减小,这里我们将安装包作为本次讨论的技术指标。

  1. 重视包大小

一款App在迭代的过程中,随着新需求的不断丰富,势必会造成包体积逐渐增大,App早期由于功能比较简单,而且优化空间有限,用户感知并不明显,包大小可能不受重视,但随着App功能不断丰富和多元化,包大小自然水涨船高,当大到一定值后,则会在一些方面产生消极影响:

  1. App Store OTA 下载大小限制:

苹果公司为了避免用户的运营商流量超限,限制了用户直接通过运营商流量从 AppStore 下载 App 的最大大小,这将一定程度上影响新用户转化以及老用户的用户体验和升级率,该限制大小历年都会做出一定的调整 :

由2017年9月的 100M 到2019年5月的 150M,iOS13系统发布之后,iOS13 及以上用户可以使用流量下载超出 200MB 的 App, 但需要用户「设置」选择策略「超过 200MB 请求许可」,但iOS13 系统以下用户仍然无法流量下载。

  1. App __TEXT 段大小限制:

除了以上限制,苹果公司对App的可执行文件(即Mach-O文件)的__TEXT段大小也做了严格限制要求,超出这个限制将无法通过App Store的审核:

  1. iOS 7 之前,二进制文件中所有的 __TEXT 段总和不得超过 80 MB;
  2. iOS 7.x 至 iOS 8.x,二进制文件中每个特定CPU架构的 __TEXT 段不得超过 60 MB;
  3. iOS 9.0 之后,二进制文件中所有的 __TEXT 段总和不得超过 500 MB。
  1. App性能问题

App包大小的劣性增长,主要原因有较多冗余的代码文件,资源文件(如图片,字体文件,db文件等)以及低效的代码逻辑,这些因素不仅影响包大小,还波及App的其他性能指标,如启动时长,流畅度(FPS),Crash率,甚至业务的深层次迭代效率;

二. 苹果为我们做了哪些

  1. App Thinning

App Thinning 苹果公司推出的一项可以改善 App 下载进程的新技术,主要是为了解决用户下载 App 耗费过高流量的问题 ,它可以让 App Store 和操作系统在安装、更新及运行 iOS 或者 watchOS 的 App 等场景时,通过一系列的优化,尽可能减少安装包的大小,仅下载所需的资源,减少 App 的占用空间,从而节省设备的存储空间。

App Thinning 有三种方式,包括:App SlicingBitcodeOn-Demand Resources

  1. App Slicing,很多应用需要在不同尺寸的设备上运行,针对这些不同的设备,它们内含不同的独立资源,而大部分是你的设备不需要的,App store会针对不同的设备创建不同的变体,适用到不同的设备。

需要注意,图片资源必须提供多分辨率的图片采用Asset Catalog管理,才能在Slicing中发挥作用。

  1. On-Demand Resources,按需加载资源,在下载App的时候,App中包含的不重要资源先不下载,等到需要时,再由系统向苹果的服务器Server发送请求,下载指定的资源包,以减少初装 App 的包大小,主要用在游戏多关卡场景,玩家解锁游戏的特定关卡后,可以下载新关卡及这个关卡相关的特定资源。此外,玩家已经通过的关卡可以被移除以便节约设备上的存储空间。
  2. Bitcode,一个编译好的程序的中间表示形式,App开启Bitcode,传到 iTunes Connect后会在 App Store中进行链接和编译,生成一个中间表现形式(Bitcode),再将这个Bitcode编译为可执行的64位或32位程序,若后续苹果新增一种CPU架构,那么苹果可重新优化我们程序的二进制文件以进行适配,而不需我们重新提交新版本到App Store上,此优化不明显(需要注意的是,使用Xcode14构建时包时,若开启BitCode,苹果将限制提交到App Store)。

三. 我们在包大小上的探索

1. 分析安装包

安装包构成由以下三个部分:

  1. Mach-O可执行文件:主要包含项目的核心业务代码及静态库
  2. 资源文件:

图片资源:包括Assets.car,Bundle文件,GIF,JPG,PNG,JSON等文件

静态WEB资源:JS,CSS,HTML

视图资源:XIB,Storyboard

音视频资源:mp4,mp3,caf等

其他资源:签名文件,字体文件,PLIST文件,db文件,国际化资源文件等等

  1. Framework,应用程序依赖的动态库

我们从以上三个部分进行一定程度的优化,下图是我们收集的优化策略,有些需依赖其他部门的支持,如资源动态化,需要后端或者运维的支持,在实施中存在可行性及收益比两方面考虑,具体能否落地需看公司实际情况。

2. 编译器优化

  1. 去掉无用架构

可通过Xcode的Excluded Architectures项添加需排除的CPU架构,常见的架构及机型搭配:

  • i386 架构,支持32位模拟器,已不再使用;
  • x86_64 架构,支持64位模拟器,现在都是64位的模拟器;
  • armv7, armv7s 架构,支持32位真机,基本上淘汰了;
  • arm64 架构,支持64位真机,在iPhone5S开始支持,苹果用户基本都是这个架构

可以在Xcode的Excluded Architectures配置项增加armv7,armv7s,让我们构建的包只支持arm64架构,虽然苹果有App Slicing机制也会做指定机型的切割,但此操作优化了我们平时的编译时长和构建产物大小;

  1. Optimization Level

  1. 选项None[-O0]:编译器不会优化代码,意味着更快的编译速度和更多的调试信息,默认在 Debug 模式下开启;
  2. Fast[-O, O1]: 编译器会优化代码性能并且最小限度影响编译时间,此选项在编译时会占用更多的内存;
  3. Faster[-O2]:编译器会开启不依赖空间/时间折衷所有优化选项。在此,编译器不会展开循环或者函数内联。此选项会增加编译时间并且提高代码执行效率;
  4. Fastest[-O3]:编译器会开启所有的优化选项来提升代码执行效率。此模式编译器会执行函数内联使生成的可执行文件变得更大。一般不推荐使用此模式;
  5. Fastest Smallest[-Os]:编译器会开启除了会明显增加包大小以外的所有优化选项。默认在 Release 模式下开启;
  6. Fastest, Aggressive Optimization[-Ofast]:启动 -O3中的所有优化,可能会开启一些违反语言标准的一些优化选项。一般不推荐使用此模式。
  1. Strip Link Product

Strip Link Product会受到Deployment Postprocessing设置的影响,只有当Deployment Postprocessing设置为YES时,该选项才会生效,当Strip Link Product为YES可以帮我们优化掉Strip Style中设置的不需要的符号信息,但调试App时断点不会中断,App Crash后控制台也无法看到具体的类名和方法名,一般Debug模式设置为NO,Release模式设置YES。

Strip Style是我们需要去除的符号的类型的选项,包括:

1)All Symbols: 去除所有符号,一般是在主工程中开启;

2)Non-Global Symbols: 去除一些非全局的 Symbol(保留全局符号,Debug Symbols

同样会被去除),链接时会被重定向的那些符号不会被去除,此选项是静态库 / 动态库的建议选项;

3)Debug Symbols: 去除调试符号,去除之后将无法断点调试;

  1. Make Strings Read-Only

复用字符串字面量,减少不必要的生成,也是一种优化形式。

  1. Dead Code Stripping

消除无效代码,C/C++/Swift 等静态语言编译器会在 link 的时候移除未使用的代码,对于OC等动态语言是无效的。

  1. Link-Time Optimization

此设置有三个选项值:

1)默认No 不开启链接期优化

2)Monolithic 生成单个 LTO 文件,每次链接重新生成,无缓存高内存消耗,参数LLVM_LTO=YES

3)Incremental 生成多个 LTO 文件,增量生成,低内存消耗,参数 LLVM_LTO=YES_THIN(推荐)

LTO带来的优化:

1)将一些函数內联化:不用进行调用函数前的压栈、调用函数后的出栈操作,提高运行效率与栈空间利用率;

2)去除了一些无用代码:如果一段代码分布在多个文件中,但是从来没有被使用,普通的 -O3 优化方法不能发现跨中间代码文件的多余代码,因此是一个局部优化。但是 Link-Time Optimization可以在链接时发现跨中间代码文件的多余代码;

3)对程序有全局的优化作用:这是一个相对广泛的概念。举个例子来说,如果一个 if 方法的某个分支永不可能执行,那么在最后生成的二进制文件中就不应该有这个分支的代码。

  1. Asset Catalog Compiler

此配置的Optimization含三个选项,空、time和space,选择space可以优化包大小。

3. 资源优化

  1. 删除无用资源

App包中的资源有:未使用图片(GIF,PNG,JPG,webp等),音视频,Plist文件等等,可以使用业内工具进行扫描,比如:

1)LSUnusedResources,一个可视化客户端工具,使用简单,不做赘述;

2)fdupes命令查找项目中的重复文件,原理是对比不同文件的签名,签名相同的文件就会判定为重复资源,此方式可能存在误判,建议人工复核后进行删除;

这里分享一下我们使用Shell脚本扫描未使用资源的逻辑,由于我们的项目采用了组件化,每个组件的资源都是独立隔开的,所以扫描时需要每个组件单独扫码,逐一清理,每个组件扫描后可得到:

1)确定未使用的资源路径(这里包括GIF,PNG,JPG,Webp等类型文件),此类文件可直接删除;

2)可能未使用的资源路径,一般因文件名带有数字(可能字符串拼接的文件名),此类文件需再次确认

值得一提的是,采用脚本形式会比可视化工具更灵活:

1)可根据需要对脚本匹配的正则进行修改来支持扫描识别出更多类型文件;

2)扫描的文件可输出文件路径,用于再次确认;

3)如项目采用的外部脚本的自动化构建打包,我们的扫描脚本可移植到打包流程(一般用于优化外部三方库)中的资源文件;

4)可继续对脚本进行拓展,将扫描的指定图片进行压缩,这一点也可以嵌入到CI流程中

每次资源清理后,迭代几个版本就会发现工程中又会产生很多无用资源,其中以图片资源居多,所以可以将资源优化这个能力作为优化常规项,定期清理,这一点我们在后面防劣方案中会提到,我们经过定期扫描清理,截止目前我们在清理无用资源这一块的收益是6.73M,后续我们会继续执行,保证项目项目的资源利用率。

我们的脚本流程:

通过find命令递归查找组件工程各目录,grep进行文件名匹配判断文件是否被使用,需要注意,由于我们工程主要采用了OC,定义句柄时,资源引入方式只考虑@""和xib形式引入的资源文件(如图片),可以根据本身项目的图片引入规则对匹配句柄进行调整:

    // 以下为代码片段
    # 如果图片是在.imageset目录下,则不能用图片名判断,而是要用.imageset名,因为图片名可能与.imageset名不同,而代码中使用的却是.imageset名
    if [[ $png =~ ".imageset" ]];then
        v=`echo $png | grep -Eo "/Users(.*).imageset"`
        match_name=`basename $v | awk -F '.imageset' '{print $1}'`
    fi

    pic_size=`wc -c $png | awk '{print $1}'`

    referenced=false
    # 判断图片名称是否引用到
    if grep -q "$match_name" "$maybeImageSentencePath"; then
        referenced=true
    fi

    # 如果图片是带有数字的,就判断为可能是拼接的图片
    contaT=$(echo $match_name | grep "[0-9]")
    if [[ "$contaT" != "" ]];  then
        MaybeUnusedCount=`expr $MaybeUnusedCount + 1`
        echo "$png"  >> $maybeUnusedImageFilePath
        echo "${png}图片可能未用到"
        # 将使用到的图片,超过阀值写入文件
        if [ $pic_size -gt $k_base_size ];then
            echo "大于${k_base_size},写入文件"
            img_kb_size=`awk 'BEGIN{printf "%.2f\n",'$pic_size'/'$k'}'`
            echo "$png ${img_kb_size}KB" >> $mayUnusedExceedPicPath
        fi 
        continue
    fix

    # 图片不在赋值文本中
    if ! $referenced ; then
        UnusedCount=`expr $UnusedCount + 1`
        echo "$png"  >> $unusedImageFilePath
        echo "${png}图片未使用"
        TotalSize=`expr $TotalSize + $pic_size`
        # 将使用到的图片,超过阀值写入文件
        if [ $pic_size -gt $k_base_size ];then
            echo "大于${k_base_size},写入文件"
            img_kb_size=`awk 'BEGIN{printf "%.2f\n",'$pic_size'/'$k'}'`
            echo "$png ${img_kb_size}KB" >> $unusedExceedPicPath
        fi  
        continue
    fi



    # 图片用到了,在imageset目录下,有三张png图片,则需要去掉1x图片
    if [[ $png =~ ".imageset" ]];then
        path=`echo $png | grep -Eo "/Users(.*).imageset"`
        files=$(ls $path)
        count_flag=0
        for filename in $files
        do
            if [[ $filename =~ ".png" ]]; then 
                count_flag=`expr $count_flag + 1`
            fi 
            if [[ "$count_flag" == "3" ]];then 
                echo $png >> $usedContain3Pic
            fi 
        done 
    fi
done

这里我们还添加了通过扫描出超过某个阈值的图片,主要用于查看项目中的大图,大图的优化空间会更大;

 # 将使用到的图片,超过阀值写入文件

    if [ $pic_size -gt $k_base_size ];then

        echo "大于${k_base_size},写入文件"

        img_kb_size=`awk 'BEGIN{printf "%.2f\n",'$pic_size'/'$k'}'`

        echo "$png ${img_kb_size}KB" >> $exceedPicPath

    fi  

另外,第一次实施时会发现存在很多文件未使用,这里可以分享一个简单的脚本,批量进行删除:

delete_path="项目路径"
# `rm -fr $delete_path;mkdir $delete_path`
# filePath="这里填前面扫描后得到的资源路径集合的文本路径"
cat $delete_path | while read line

do  
    if [[ "$line" =~ ".imageset" ]];then 
        img_dir=`echo "$line" | grep -Eo "/Users(.*).imageset"`
        rm -fr $img_dir
    # else 
        # rm "$line"
    fi
    # cp $line $delete_path

done
  1. 图片压缩

图片资源的最好用 Asset Catalog来进行管理,Xcode 构建过程中,在执行到compile asset catalog时,会利用构建Asset Catalog的actool插件会对Asset Catalog中的png图片进行解码,得到 Bitmap 数据,然后再运用actool的编码压缩算法进行编码压缩处理,如果放入到Assets中的是JPG图片,最终也会转成png图片存放到Assets.car中。

插件actool的压缩算法有 lzfse,palette_img,deepmap2,deepmap_lzfse,zip。

需要注意的是,无损压缩是变换图片的编码压缩算法来减少大小,并未改变像素数据,所以无损压缩的图片不能优化Assets.car的最终大小,但有损压缩是可以,这里说明下我们在图片压缩方面做的几点探索。

常见的图片压缩方式有:

  1. TinyPng,它是一个网页工具,有损压缩,需要联网,不支持批量处理;
  2. TingPNG4Mac,属于Mac的客户端工具,基于TinyPng实现的;
  3. ImageOptim,客户端工具,支持无损压缩和有损压缩两种形式,可自定义压缩方式;
  4. pngquant,命令行工具,有损压缩,编写脚本批量压缩,支持自定义压缩质量参数;
  5. Guetzli,针对数码图像和网页图像的JPEG编码器,支持JPG图片的有损压缩;

由于项目在迭代过程中使用到的图片在切图,选图上的疏忽可能会将较大图片引入进来,所以我们会定期扫描,检查项目中的大图是否合格,保证需要的图片是最优的,经此阶段,我们在项目中图片资源压缩的收益如下:

组件收益大小
主工程1.8M
User子组件4.72M
Common子组件9.17M
搬家子组件3M
地图组件2.66M
IM组件0.2M
其他三方库0.3M

通过第一步的无用资源删除后,项目中存在的图片资源基本上是我们必需的了,此时我们依然采用Shell脚本对各组件的图片进行压缩,通过调研我们发现,pngquant工具是我们需要的,我们采用了pngquant命令来压缩项目中的图片。

pngquant压缩原理:将24位或32位的RGBA的PNG图转换成8位的PNG图,保留全部的alpha通道,生成的图片兼容所有现代web浏览器;

官方说明:

压缩命令:

$ pngquant --quality=20-30 文件名

当quality设置在20-30时,第一次压缩后,在不影响图片质量的情况下,大小一般可优化在60%左右,对于本身很小的图片也可以进行压缩(不过建议将压缩前后的对比图让UI设计师把关)

这个是我们批量压缩User组件图片前后对比:

对于单张图片,视觉上确实看不出压缩的痕迹,参考下图:

压缩前:72K

压缩后:16K

我们脚本流程:

脚本代码:

注意:有些图片本身很小,压缩后反而会增大,所以需要判断一下是否需要压缩,脚本压缩逻辑为:

check_files=`find $input_dir -name '*.png'`
for line in $check_files

do  

    pngquant --quality=20-30 $line
    pre_name=`echo $line | awk -F '.png' '{print $1}'`
    pre_name="${pre_name}-fs8.png"
    pic_size1=`wc -c $line | awk '{print $1}'`
    pic_size2=`wc -c $pre_name | awk '{print $1}'`
    if [[ $pic_size2 -lt $pic_size1 ]];then 
        mv $pre_name $line
    else 
        echo "原图${line},压缩图$pre_name"
        echo $pic_size1
        echo $pic_size2
        echo "压缩后图片大小反而变大,放弃此次压缩"
        cp -fr $line $cannot_zip_dir
        rm $pre_name
    fi 

done

echo "将压缩后的图片拷贝出来..."
check_files=`find $input_dir -name '*.png'`
for line in $check_files
do  
    cp $line "${zip_bundle}/"
done

rm -fr $origin_dir
echo "执行完毕!!!"
exit 1
  1. 静态资源站点化

也可以将一些本地资源托管到自己的服务器或者CDN上,这样也可以优化安装包大小,如H5资源,字体文件,较大的图片,音视频,App主题资源,需要注意的是,最好在不影响App启动效率的基础上评估实施;

  1. 采用webp格式图片

采用webp的理由是WebP 压缩率较高,肉眼看不出差异,支持有损和无损两种压缩模式,根据 Google 的测试,无损压缩后的 WebP 比 PNG 文件少了 26%的体积,有损压缩后的 WebP 图片相比于等效质量指标的 JPEG 图片减少了 25%~34% 的体积,WebP 支持 Alpha 透明和 24-bit 颜色数,不会像 PNG8 那样因为色彩不够而出现毛边。

WebP 与JPG,PNG 相对比,编解码在CPU的消耗上以及效率上会差一些,如果是服务器图片,编码过程是在服务器后台进行,那么对影响较大的是解码过程,图片加载速度慢一些,这需要根据项目的实际情况在性能和体积上进行评估取舍。

注意:iOS本身暂不支持WebP格式加载,可引入SDWebImage/WebP

转Webp工具:

  1. iSpart腾讯出品,GUI工具;
  2. webp命令行工具: 使用Homebrew进行安装:
$brew install webp
  1. 图标的优化

这种优化方式属于编码过程中需要注意的点,单个的收益不会太大,实践起来会比较繁琐,可根据项目要求进行这类精细化处理。

一般方式有:

  1. 使用 tint color 精简单色图标

单色图标处理(将单色图标渲染模式为UIImageRenderingModeAlwaysTemplate,如返回按钮,tabbar icon图标,会忽略自身颜色信息采用父控件tintColor颜色渲染)

  1. 图标字体(IconFont)替换单色图标
  2. 相似图标整合

4. Mach-O优化

安装包中的Mach-O可执行文件包含我们的核心业务代码,它的大小是由代码量来决定的,所以一般情况下,是需要找出项目中无用方法和无用类进行删除,当然,业界也有对从其他角度对Mach-O的大小进行优化的。

  1. 静态筛查方案

设置Xcode的Build SettingWrite Link Map File为Yes,再指定Path to Link Map File的指向路径可得到编译后的LinkMap文件,这个文件包括Object File,Section,Symbols,包含了所有的类和方法,然后通过otool命令导出项目Mach-O中的__objc_selrefs、 __objc_class_list & __objc_class_refs 做差集找到未使用的Objc类及方法。

Mach-O中片段:

linkMap文件片段:

如果代码采用 C 、C++ 等静态语言编写代码时,编译期已经确定了基本的代码逻辑,所以编译器会帮助我们将没有使用到的代码标记为 Dead code 最终不会打包到安装包中。但Objc 是典型的动态语言,很多逻辑都是在运行时决议的,通过静态扫描的方式会存在较大误差,还需要二次确认,抖音对于这静态结果初筛的得到未使用类的准确性只有 24% (总样本 264 个,命中 64 个)。

2.动态筛查方案

  1. 基于插桩的行级别代码覆盖率:

基于 GCOV 或者 LLVM Profile 二进制的插桩方案可以实现在运行时收集插桩数据来指导无用代码的删除。但插桩方案局限性也显而易见,插桩会劣化二进制本身的大小和性能,同时原生的插桩方案是无法过审上线。数据收集只能局限于线下。

  1. 基于 Runtime 的轻量级运行时「类覆盖率」方案:

Objc 的类首次调用类初始化时,+initialize 被执行,系统会自动标记已被调用,在 metaClass 中 data 的 flags 字段第 29 位就存着这个这个状态。可以使用 flags & RW_INITIALIZED 获取。iOS14 之后这个值的获取方式有变化

#define RW_INITIALIZED (1<<29)

bool isInitialized() {

   return getMeta()->data()->flags & RW_INITIALIZED;

}

上报的数据可以让我们了解我们线上真实的 Class 使用情况,对得到的数据不仅可以用来删减未使用的代码。还可以分辨使用率低的场景,如果是低频且必须的场景可以考虑使用跨端技术这种对原生包大小影响比较小的方案实现。而如果这些场景是某个渗透率很低的需求可以考虑直接下线为其他需求做置换。

5. Framework优化

安装包中Framework目录存放着我们项目依赖的动态库,一般情况下苹果给我们的建议是不超过六个,由于我们的项目是组件化的,有些三方库,二方库通过Cocoapods引入时,是以动态库的形式引入的,动态库虽然本身产物比静态库小,但最终我们的安装包中,动态库以几近拷贝的形式进入到我们的包中,而静态库会经过静态链接进入到我们的可执行文件Mach-O中。

App依赖Dyld构建包流程:

1.探索静态库/动态库对包的影响

条件:同等情况下,使用:AFNetworking,SDWebImage,SensorsAnalyticsSDK分别作为静态库和动态库方式引入工程:

IPA包大小备注
AFNetworkingWebImageSensorsAnalyticsSDK作为动态库引入3.6M
作为静态库引入1M

通过实验,我们发现动态库方式引入后对安装包整体的影响还是比较大的,如果项目中存在较多的动态库,可以考虑转静态库进行优化。

6. 一些编码习惯的建议

1.封装&重构

整理项目中的重复方法和逻辑,下沉到能够通过的模块中,还可以考虑对项目中的老旧耦合高的模块进行重构,一定程度上能减少代码量。

2.字符串常量管理,命名管理

项目中的局部字符串常量转成全局常量字符串,统一管理,字符串的命名设置一定规范,最好不要太长,文本比较长的字符串可以转成文本进行读取或者服务器变量下发。

3.Class Method vs C 函数

通常我们对于一些基础和通用的函数会采用工具类的方式对外暴露。使用类方法完成功能。但当我们采用 Class Method, 这种方式在编译的时候需要生成 Class 的类结构。调用的方法会通过 objc_methodSend。如果采用 C 函数的方式可以减小这部分的开销。如果只是自己组件内部使用的私有的功能性函数还是建议使用 C 函数的方式实现。

4.Property vs IVAR

Objc 对于 Class 的 property,会自动的生成 set、get 方法,比如这个 property 是 Class 的私有属性的时候,我们可以直接使用 ivar 来代替 property。减小这部分的包大小开销。这里需要注意,当我们使用 property 的 getter 实现 LazyLoad 或者 setter 存在一些其他副作用的时候还是需要保留 property 的。

5.尽量避免代码片段复制粘贴

项目中可能有些方法或者函数可以被复用,但往往由于一些原因,如方法不能直接访问或参数不一致等原因最终被拷贝出去另起新方法,特别是组件化项目,组件之间的通信是有一定限制的,容易产生这类问题,建议将这些方法尽可能抽象出来以支持多种情况,并抽离或下沉到公共模块中。

四. 防劣化方案设计

  1. 图片大小把关

    1. @1x图不用集成,只需要2x,3x;
    2. 定义图片名规范,以英文命名,格式为: 图片分类_模块名_功能名,如:ic_orderdetail_scan
    3. 中小型图片建议用png, 可脚本先行压缩或重新切图,再引入到项目中,完成开发后,如有做压缩先让美工进行视觉验收;
    4. 所有图片尽量放xcassets统一管理;
  2. 资源阈值预警

  1. 在构建发布包前,扫描增量资源文件,超过设定的阈值,启动报警机制,评估该资源引入的必要性或引入方式的合理性。
  2. 将扫描各组件未使用的资源的脚本植入打包流水线,打包过程中会执行扫描未使用资源,一旦扫描到就将资源清单发送给模块负责人。
  1. 三方库管理

针对指定的三方SDK图片资源,通过自动化构建发布包时,考虑在打包的打包流水线嵌入脚本进行清理,压缩,以防外部资源的劣化。

  1. 模块预警机制

模块大小增量超过设定的阈值时,启动报警机制,对该模块增加的大小进行评估合理性,是否有优化空间,启发大家反思。

  1. 建立良好的编码规范

制定良好的编码规范,在团队中推广,实行开发人员的结对Review代码(按照制定好的编码规范),每个需求合并到Release发布分支前,先Review审核通过后再进行合并。

五. 结论

本次主要是从编译器优化,资源文件清理,图片压缩,类/方法扫描,Framework治理、编码习惯建议,防劣化设计几个方面介绍了货拉拉iOS用户端在App包大小方面优化的实践,有些思路是大多数App瘦身优化通用的,App包优化是一个循序渐进的过程,建议先易后难,比如先从优化资源文件方面入手,而且大多数情况下,资源文件优化的收益是最大的,最好每次优化后进行回归测试保证线上稳定,最后希望本文能给大家在自家App包大小优化时提供一些借鉴和思路。

参考:

Alibaba.com包大小优化
探索 iOS 编码对包大小的影响
抖音品质建设 - iOS 安装包大小优化实践篇