美文网首页
启动时间优化

启动时间优化

作者: 上帝不在服务区 | 来源:发表于2020-11-02 16:29 被阅读0次

    1、APP启动

    1.1、APP启动为什么这么重要
    • App 启动是和用户的第一个交互过程,所以要尽量缩短这个过程的时间,给用户一个良好的第一印象
    • 启动代表了你的代码的整体性能,如果启动的性能不好,其他部分的性能可能也不会太好
    • 启动会占用 CPU 和内存,从而影响系统性能和电池

    1.2、启动类型

    • Cold Launch 也就是冷启动,冷启动需要满足以下几个条件:

      • 重启之后
      • App 不在内存中
      • 没有相关的进程存在
    • Warm Launch 也就是热启动,热启动需要满足以下几个条件:

      • App 刚被终止
      • App 还没完全从内存中移除
      • 没有相关的进程存在
    • Resume Launch 指的是被挂起的 App 继续的过程,需要满足以下几个条件:

      • App 被挂起
      • App 还全部都在内存中
      • 还存在相关的进程

    1.4、App 启动阶段

    App 启动分为三个阶段

    • 初始化 App 的准备工作
    • 绘制第一帧 App 的准备工作及绘制(这里的第一帧并不是获取到数据之后的第一帧,可以是一张占位视图),这时候用户与App已经可以交互了,比如 tabbar 切换
    • 获取到页面的所有数据之后的完整的绘制第一帧页面

    在这个地方,苹果再次强调了一下,建议「用户从点击 App 图标到可以再次交互,也就是第二阶段结束」的时间最好在 400ms 以内。目前来看,大部分 App 都没有达到这个目标。

    下面我们把上面的三个阶段分成下面6个部分,讲一下这几个阶段做了什么以及有什么可以优化的地方

    15.png
    1.4.1、System Interface

    初始化APP的准备工作,系统主要做了两件事:Load dylibs 和 libSystem init

    在Load dylibs 阶段,开发者还可以做一下优化:

    • 避免连接无用的framworks,在Xcode中检查一下项目中「Linked Frameworks and Librares」部分是否有无用的连接
    • 避免在启动时加载动态库,将项目的Pods以静态编译的方式打包,尤其是Swift项目,这地方时间损耗是很大的
    • 硬链接你的依赖库,这里做了缓存优化

    LibSystem init 部分,主要是加载一些优先级比较低的系统组件,这部分时间是一个固定的成本,所以我们开发人员不需要关心

    Static Runtime Initalization

    这个阶段主要是OC和Swift Runtime的初始化时间,会调用所有的 +load 方法,将类的信息注册到Runtime中

    在这个阶段原则上不建议卡发着做任何事情,所以为了避免一些启动时间的损耗,你可以做一下几个事情:

    • 在Frameworks 开发时,公用专用的初始化API
    • 减少在 +load 中做事情
    • 使用 initialize进行来加载初始化工作
    1.4.2、UIKit Initalization

    这个阶段主要做了两件事情:

    • 实例化 UIApplication和UIApplicationDelegate
    • 开始事件处理和系统集成

    所以这个阶段的优化也比较简单,你需要做两件事:

    • 最大限度的减少UIApplication子类初始化时候的工作
    • 减少UIApplicationDelegate的初始化工作
    1.4.3、Application Initialization

    这个阶段主要是生命周期方法回调,也正是开发者熟悉的部分

    调用UIApplicationDelegate的APP生命周期方法:

    application:willFinishLaunchingWithOptions: 
    application:didFinishLaunchingWithOptions:
    

    和 UIApplicationDelegate 的 UI 生命周期方法:

    applicationDidBecomeActive:
    

    同时,iOS 13 针对 UISceneDelegate 增加了新的回调:

    scene:willConnectToSession:options:
    sceneWillEnterForeground:
    sceneDidBecomeActive:
    

    也会在这个阶段调用。感兴趣的可以关注一下 Getting the Most out of Multitasking 这个 Session,暂时没有视频资源,怀疑是现场演示翻车了,所以没有把视频资源放出来。

    在这个阶段,开发者可以做的优化:

    • 推迟和启动时无关的工作
    • Senens 之间共享资源
    1.4.4、Fisrt Frame Render

    这个阶段主要做了创建、布局和绘制视图的工作,并把准备好的第一帧提交给渲染层渲染。会频繁调用以下几个函数:

     loadView
     viewDidLoad 
     layoutSubviews
    

    在这个阶段,开发者可以做的优化:

    • 减少视图层级,懒加载一些不需要的视图
    • 优化布局,减少约束
    1.4.5、Extend

    大部分 App 都会通过异步的方式获取数据,并最终呈现给用户。我们把这一部分称为 Extend。

    2、动态库转静态库

    苹果建议将应用程序的总启动时间设定在400毫秒以下,并且我们必须在20秒之内完成启动,否则系统会杀死我们的应用程序。我们可以尽量优化应用main函数到didFinishLaunchingWithOptions的时间,但如何调试在调用代码之前发生的启动速度慢的情况呢?

    1.1、Pre-main时间的查看

    在系统执行应用程序的main函数并调用应用程序委托函数(applicationWillFinishLaunching)之前,会发生很多事情。我们可以将DYLD_PRINT_STATISTICS环境变量添加到项目scheme中。

    1.png

    优化前

    Total pre-main time: 2.0 seconds (100.0%)
             dylib loading time: 1.7 seconds (84.3%)
            rebase/binding time:  35.25 milliseconds (1.7%)
                ObjC setup time:  40.95 milliseconds (1.9%)
               initializer time: 244.57 milliseconds (11.9%)
               slowest intializers :
                 libSystem.B.dylib :  12.97 milliseconds (0.6%)
                         Alamofire : 106.12 milliseconds (5.1%)
    

    优化后

    Total pre-main time: 1.2 seconds (100.0%)
             dylib loading time: 1.1 seconds (89.1%)
            rebase/binding time:  23.51 milliseconds (1.8%)
                ObjC setup time:  25.41 milliseconds (1.9%)
               initializer time:  91.64 milliseconds (7.0%)
               slowest intializers :
                 libSystem.B.dylib :   7.15 milliseconds (0.5%)
                     FellorliSwift :  35.71 milliseconds (2.7%)
    

    这是我使用iPhone 5c的运行结果 ,这只是通过staticlib优化从启动2秒时间降低到1.2秒。这里讲一下各部分的作用

    注意:如果你要测试应用的最慢启动时间,记得使用你支持的最慢的设备来进行测试。

    输出显示系统调用应用程序main时所用的总时间,然后是主要步骤的分解。
    WWDC 2016 Session 406优化应用程序启动时间详细介绍了每个步骤以及改进时间的提示,以下是简要的总结说明:

    dylib loading time 动态加载程序查找并读取应用程序使用的依赖动态库。每个库本身都可能有依赖项。虽然苹果系统框架的加载是高度优化的,但加载嵌入式框架可能会很耗时。为了加快动态库的加载速度,苹果建议您使用更少的动态库,或者考虑合并它们。
    * 建议的目标是六个额外的(非系统)框架。

    Rebase/binding time 修正调整镜像内的指针(重新调整)和设置指向图像外符号的指针(绑定)。为了加快重新定位/绑定时间,我们需要更少的指针修复。

    * 如果有大量(大的是20000)Objective-C类、选择器和类别的应用程序可以增加800ms的启动时间。
    * 如果应用程序使用C++代码,那么使用更少的虚拟函数。
    * 使用Swift结构体通常也更快。
    

    ObjC setup time Objective-C运行时需要进行设置类、类别和选择器注册。我们对重新定位绑定时间所做的任何改进也将优化这个设置时间。

    initializer time 运行初始化程序。如果使用了Objective-C的 +load 方法,请将其替换为 +initialize 方法。

    在系统调用main之后,main将依次调用UIApplicationMain和应用程序委托方法。

    1.2、动态库与静态库

    1.2.1、动态库

    我们先来看看工程里面有多少动态库:

    • 在项目的Product文件夹找到我们的工程.app文件,右键选择Show in Finder。

    • 来到相应目录后右键选择显示包内容。

    • 找到Frameworks文件夹,打开。

    • 项目是纯Swift编写,下面都是系统Swift库,我们没法优化,可以不管。

    image-20201026163408822.png
    1.2.2、静态库

    在Pod的工程中,选择我们使用的库,然后点击Build Setting,搜索或者找到Mach-O Type设置,修改Mach-O Type为static Library

    image-20201026163814646.png

    按照上面的步骤,把我们的动态库都转化成静态库,先执行一次Clean Build Folder ,然后重新构建一次

    image-20201026164051274.png

    这里保留了连个OC的库

    1.2.3、遇到的坑
    2.png

    其实这里是CocoaPods的一个配置问题,CocoaPods会在项目中的Build Phases添加一个[CP] Embed Pods Frameworks执行脚本

    "${PODS_ROOT}/Target Support Files/Pods-项目名/Pods-项目名-frameworks.sh"
    

    我们在执行pod install后会生成一个Pods-项目名-frameworks.sh的脚本文件。由于我们是手动修改的Mach-O Type类型,这个脚本中的install_framework仍然会执行,所以我们要把转换成静态库的这些库从Pods-项目名-frameworks.sh文件中删除。

    我们先看一下install_framework 到底干了啥

    # Copies and strips a vendored framework
    install_framework()
    {
      # 设置source变量,三方库构建之后的路径
      if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then
        local source="${BUILT_PRODUCTS_DIR}/$1"
      elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then
        local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")"
      elif [ -r "$1" ]; then
        local source="$1"
      fi
      
      # 设置destination变量,三方库需要移动到的路径
      local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
      
      # 判断source是否为链接文件,需要指向原来的文件
      if [ -L "${source}" ]; then
        echo "Symlinked..."
        source="$(readlink "${source}")"
      fi
      
      # rsync --delete无差异同步,可以简单理解为网盘同步,或者复制
      # 想详细了解rsync,可以在命令行中输入man rsync
      # 这里相当于把source的文件(文件夹)同步到destination
      # 即把*.framework复制到Frameworks文件夹下
      # Use filter instead of exclude so missing patterns don't throw errors.
      echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\""
      rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}"
      
      # 下面是找到二进制文件,即framework的Mach-O
      local basename
      basename="$(basename -s .framework "$1")"
      binary="${destination}/${basename}.framework/${basename}"
    
      if ! [ -r "$binary" ]; then
        binary="${destination}/${basename}"
      elif [ -L "${binary}" ]; then
        echo "Destination binary is symlinked..."
        dirname="$(dirname "${binary}")"
        binary="${dirname}/$(readlink "${binary}")"
      fi
      
      # 去掉无效的架构
      # Strip invalid architectures so "fat" simulator / device frameworks work on device
      if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then
        strip_invalid_archs "$binary"
      fi
      
      # 进行代码签名
      # Resign the code if required by the build settings to avoid unstable apps
      code_sign_if_enabled "${destination}/$(basename "$1")"
      
      # Swift的运行时库,Xcode 7之后就用不到了,可以不管
      # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7.
      if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then
        local swift_runtime_libs
        swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u)
        for lib in $swift_runtime_libs; do
          echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""
          rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"
          code_sign_if_enabled "${destination}/${lib}"
        done
      fi
    }
    

    install_framework是把构建好的 *.framework包复制到App的Frameworks文件夹下

    出现上面的报错就是因为资源没有从 *.framework中转移到App中。

    解决办法:

    既然现在拿到的Bundle是Main Bundle,我们构建之后利用脚本把资源拷贝到APP文件夹不就好了

    install_framework_bundle()
    {
        # 设置source变量,三方库构建之后的路径
        if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then
          local source="${BUILT_PRODUCTS_DIR}/$1"
        elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then
          local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")"
        elif [ -r "$1" ]; then
          local source="$1"
        fi
    
        # 设置destination变量,三方库需要移动到的路径
        local destination="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
    
        # 遍历framework下的文件,找到bundle和图片,有其他资源自己改一下
        for filename in `ls ${source} | grep ".*\.bundle\|.*\.jpg\|.*\.jpeg\|.*\.png"`
        do
          full_path=${source}/${filename}
          # 把资源同步到Main Bundle中
          rsync -abrv --suffix .conflict "${full_path}" "${destination}"
        done
    }
    

    现在我们的操作就是把被静态化的三方库从install_framework方法改为install_framework_bundle:

    if [[ "$CONFIGURATION" == "Debug" ]]; then
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/HandyJSON/HandyJSON.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/Hero/Hero.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
      install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
      install_framework "${BUILT_PRODUCTS_DIR}/MQTTClient/MQTTClient.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/PKHUD/PKHUD.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxAlamofire/RxAlamofire.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxRelay/RxRelay.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/SwiftyUserDefaults/SwiftyUserDefaults.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/Toast-Swift/Toast_Swift.framework"
    fi
    if [[ "$CONFIGURATION" == "Release" ]]; then
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/HandyJSON/HandyJSON.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/Hero/Hero.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
      install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
      install_framework "${BUILT_PRODUCTS_DIR}/MQTTClient/MQTTClient.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/PKHUD/PKHUD.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxAlamofire/RxAlamofire.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxRelay/RxRelay.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/SwiftyUserDefaults/SwiftyUserDefaults.framework"
      install_framework_bundle "${BUILT_PRODUCTS_DIR}/Toast-Swift/Toast_Swift.framework"
    fi
    

    3、修改Mach-O Type到底改变了什么

    3.png

    Podfile文件中配置了use_frameworks!,然后进行pod install,这样生成的就是动态库。

    首先,看一下这个库的Mach-O Type是动态库

    4.png

    执行⌘+B构建之后,我们还是来到Products文件中的app:

    5.png

    在生成的Demo.app文件包上面点右键,选择显示包内容:

    6.png

    打开Framewoks文件夹,我们可以看到里面有我们创建的两个动态Pod1.framework和Pod2.framework。文件夹里面有代码签名、资源、Info.plist、Pod1(Mach-O)、bundle。

    也就是说,如果我们使用的是动态库,在Framewoks文件夹就会看到它的身影,同时主工程的Mach-O文件中是没有相关的代码的。

    7.png

    下面我们修改Build Settings中的Mach-O Type,将其设置为静态库Static Library。

    8.png

    和上面一样我们这边直接替换Pods-Demo-frameworks.sh中install_framework:

    image-20201026170729212.png

    我们看到我们在两个库中创建的类Pod1Object和Pod2Object来到了主工程的Mach-O文件中!
    现在应该明白了:

    • 动态库会和主工程的Mach-O分开存放。

    • 静态库会和主工程的Mach-O合并在一起

    4、静态库带来的问题

    我们看到我们在两个库中创建的类Pod1Object和Pod2Object来到了主工程的Mach-O文件中!
    现在应该明白了:

    • 动态库会和主工程的Mach-O分开存放。

    • 静态库会和主工程的Mach-O合并在一起

    4.1、符号冲突

    回顾下 -ObjC 、 -all_load 、-force_load这三个flag的区别:

    • -ObjC 链接器会加载静态库中所有的Objective-C类和Category;(导致可执行文件变大)

    • -all_load 链接器会加载静态库中所有的Objective-C类和Category(这里和上面一样);当静态库只有Category时 -ObjC会失效,需要使用这个flag;

    • -force_load 加载特定静态库的全部类,与 -all_load类似但是只限定于特定静态库,所以 -force_load需要指定静态库;当两个静态库存在同样的符号时,使用 -all_load会出现 duplicate symbol的错误,此时可以根据情况选择将其中一个库 -force_load。

    我们在Pod1库中复制一份Pod2Object.{h,m},同时在Build Settings中的Other Linker Flags中添加 -all_load。

    9.png

    先执行Clean Build Folder(或⇧+⌘+K),然后再⌘+B进行构建,这时就会出现duplicate symbols报错:

    10.png

    解决办法:
    任意一个或者都不使用静态库。虽然这么说,其实这也是不安全的。如果能改名字就改一下吧。

    11.png
    4.2、Bundle的获取

    我们在Pod1Object和Pod2Object中添加以下方法:

    - (nullable NSBundle *)getBundle {
        return [NSBundle bundleForClass:[self class]];
    }
    

    再在主工程的ViewController中添加:

    - (void)viewDidLoad {
        [super viewDidLoad];
        NSBundle *main = [NSBundle mainBundle];
        NSBundle *pod1 = [[Pod1Object new] getBundle];
        NSBundle *pod2 = [[Pod2Object new] getBundle];
        NSLog(@"%@", main);
        NSLog(@"%@", pod1);
        NSLog(@"%@", pod2);
    }
    

    我们先看一下动态库的情况:

    12.png

    我们看到Main Bundle是我们的App,而我们的Pod1 Bundle和Pod2 Bundle分别是其对应的framework,类似于它们有自己的沙盒。

    我们再来看看静态库:

    13.png

    可以看到3个Bundle都变成了我们的Main Bundle!

    这是因为静态库被合并到了主工程Mach-O文件中:

    [NSBundle bundleForClass:[self class]];
    

    [self class]现在在主工程的Mach-O中,那么上面找到的自然是主工程的Bundle,即Main Bundle。
    这个问题解决起来比符号冲突简单一些,但解决这个问题前,我要先讲一下CocoaPods。

    5、动态库和静态库的选择

    14.png

    参考资料

    [1] WWDC 2019 keynote: https://developer.apple.com/videos/play/wwdc2019/101/

    [2] WWDC2019 - 423 - Optimizing App Launch: https://developer.apple.com/videos/play/wwdc2019/423/

    [3] dyld启动流程: https://leylfl.github.io/2018/05/28/dyld启动流程/

    [4]WWDC2017 - 413 - App Startup Time: Past, Present, and Future: https://developer.apple.com/videos/play/wwdc2017/413/

    [5] Static linking vs dyld3: https://allegro.tech/2018/05/Static-linking-vs-dyld3.html

    [6] WWDC2018 - 220 - High Performance Auto Layout: https://developer.apple.com/videos/play/wwdc2018/220/

    [7] WWDC2019 - 417 - Improving Battery Life and Performance: https://developer.apple.com/videos/play/wwdc2019/417/

    [8] WWDC2017 - 706 - Modernizing Grand Central Dispatch Usage: https://developer.apple.com/videos/play/wwdc2017/706/

    [9] The Talk Show Live From WWDC 2019, With Craig Federighi and Greg Joswiak: https://daringfireball.net/2019/06/the_talk_show_live_from_wwdc_2019

    [10] MetricKit: https://developer.apple.com/documentation/metrickit

    [11 ]WWDC2019 - 417 -Improving Battery Life and Performance: https://developer.apple.com/videos/play/wwdc2019/417/

    相关文章

      网友评论

          本文标题:启动时间优化

          本文链接:https://www.haomeiwen.com/subject/gvnqvktx.html