0%

iOS编译速度优化

背景

公司iOS APP经过N年的迭代,工程呈现的状态如下

基于17款MBP下的测试结果

  1. Clean build一次耗时大概20分钟左右;
  2. 增量编译耗时大概2~3分钟+以上,这跟修改到的代码依赖链有关,但不修改任何代码,光资源的拷贝和编译耗时也占用了一分多钟;
    因此对特卖会iOS的工程进行一次构建优化变得很有必要,毕竟节省一个人的时间不算事,节省团队每个人的时间对于效率的贡献度就很可观了。

分析

xcode构建的过程大概是:预处理(预编译) -> 编译 -> 链接 -> 拷贝(编译)资源和动态库 -> 执行脚本 -> 签名,优化主要是从以下几个步骤着手
例如:如图片所示,预编译耗时有点恐怖(启用xcode new build system后各模块预编译耗时会经常变化,暂不清楚原理)

步骤 优化方案 首次耗时 增量耗时
预处理(预编译) 重点整治相关pch文件,通过优化import的头文件达到优化效果 > 9.5 min < 1s
编译 1.清理无用代码;2.清理无用import; > 7 min 跟修改的代码有关,调用链深的,修改一次后可能 > 5min
链接 暂时无法优化 > 30s < 1s
拷贝(编译)资源 空间换取时间 > 70s > 70s
拷贝动态库 无需优化 > 1s > 0s
执行脚本 无需优化 > 1s > 1s
签名 无需优化 > 5s > 5s

详细方案

拷贝(编译)资源

使用的是空间换取时间思想,主要是避免每次增量都重新编译xib、storyboard、xcassets等,代码如下:

xcassets编译脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
if [[ "${CONFIGURATION}" == "Debug" ]]; then
    # 通过备份资源到临时文件夹xxx-Resources-Input和命令行rsync输出发生改变的资源文件列表,实现资源增量编译(拷贝)
    CHANGED_FILES=$(rsync -ar --log-format='%f' --copy-links --no-relative --exclude '*/.svn/*' --files-from="$ASSETS_TO_BACKUP" / "${TARGET_TEMP_DIR}/Modular-Assets-Input")
 
    ASSETS_OUTPUT="${TARGET_TEMP_DIR}/Modular-Assets-Output"
    mkdir -p "${ASSETS_OUTPUT}"
 
    # 图片发生改变,重新编译
    if [ -n "$CHANGED_FILES" ]; then
      echo "recompile xcassets: ${XCASSET_FILES[@]}"
      compileXcassets "true"
      retval=$?
      if [ "$retval" == 1 ]; then
        IFS="$oldifs"
        exit 1
      fi
 
      echo "rsync -a \"${ASSETS_OUTPUT}/\" \"${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\""
      rsync -a "${ASSETS_OUTPUT}/" "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
    else
      # 手动删除
      if [[ ! -e "$ASSETS_OUTPUT" ]]; then
        echo "recompile xcassets: ${XCASSET_FILES[@]}"
 
        compileXcassets "true"
        retval=$?
        if [ "$retval" == 1 ]; then
          IFS="$oldifs"
          exit 1
        fi
      fi
 
      # 直接拷贝
      echo "rsync -a \"${ASSETS_OUTPUT}/\" \"${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\""
      rsync -a "${ASSETS_OUTPUT}/" "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
    fi
  else
    echo "compile xcassets: ${XCASSET_FILES[@]}"
 
    ASSETS_OUTPUT="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
    compileXcassets
  fi

xib、storyboard等编译脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if [[ "${CONFIGURATION}" == "Debug" ]]; then
  # 通过备份资源到临时文件夹Modular-Resources-Input和命令行rsync输出发生改变的资源文件列表,实现资源增量编译(拷贝)
  CHANGED_FILES=`rsync -ar --log-format='%f' --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_BACKUP" / "${TARGET_TEMP_DIR}/Modular-Resources-Input"`
  install_project_resouces "${CHANGED_FILES[*]}"
 
  if [[ $NOT_COMPILE_INDEX != -1 ]]; then
    # 安装包可能手动删除了
    OUTPUT_RESOURCE="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"${ALL_RESOURCES[$NOT_COMPILE_INDEX]}\"`"
    if [[ ! -e "$OUTPUT_RESOURCE" ]]; then
      echo "May be delete .app package"
      install_project_resouces "${ALL_RESOURCES[*]}" 
    fi
  fi
else
  install_project_resouces "${ALL_RESOURCES[*]}"
fi

预处理(预编译)&编译

其实预处理编译优化和iOS包体优化是类似的,主要是:

  1. 清理无用代码;
  2. 清理无用的import。

模块化

iOS的工程当前已经实现了模块化,基于模块化的思想,可按下面步骤操作,修改后,不管是首次还是增量编译都只会编译自己所负责模块的代码,可以极大地节省时间。

  1. 一个工程输出一个Framework(当前为静态库);
  2. 开发同学checkout自己所负责的模块代码,其他模块通过Framework引入。

基于此我们开发了一套无侵入式构建优化工具:FastDev。支持:

  1. 服务端支持静态库快照功能: 服务端定时作业产生develop分支的各个业务模块的静态库快照,对其管理,并且提供客户端下载;
  2. 客户端提供命令行工具支持快速开发模式:客户端通过命令行交互下载静态库,并且生产相应的快速开发模式工程。

但FastDev当前也有缺陷:

  1. 无法debug其他模块的代码;
  2. 不适合底层代码(网络、数据库等)开发,因为这些依赖是全局性的;
  3. 因为模块间依赖还比较严重,所以会有些夸模块的问题,例如修改了头文件,另一个模块也依赖这个头文件,会触发崩溃之类,因为实际上实现已改变。

对比

步骤 首次耗时 增量耗时 状态
预处理(预编译) 使用FastDev:< 5min,不使用FastDev:> 9.5min < 1s 进行中
编译 使用FastDev:< 5min,不使用FastDev:> 7min 使用FastDev:< 1min,不使用FastDev:> 1min~5min 进行中
拷贝(编译)资源 > 70s < 1s 完成