iOS底层收集

iOS 底层 - 性能优化之启动和电池能耗

2020-04-20  本文已影响0人  水中的蓝天

本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗漏或未能一一列举原文出处还望见谅与指出,另文章内容如有不妥之处还望指教,万分感谢 !

写在前面:

Mach-O文件简介

常见的Mach-O文件有:

dyld简介

在iOS系统中,几乎所有的程序都会用到动态库,而动态库在加载的时候都需要用dyld(位于/usr/lib/dyld)程序进行链接。很多系统库几乎都是每个程序都要用到的,与其在每个程序运行的时候一个一个将这些动态库都加载进来,还不如先把它们打包好,一次加载进来来的快。

可执行文件:

动态库:

系统使用动态链接有几点好处:

代码共用:很多程序都动态链接了这些 lib,但它们在内存和磁盘中中只有一份。 易于维护:由于被依赖的 lib 是程序执行时才链接的,所以这些 lib 很容易做更新,比如libSystem.dyliblibSystem.B.dylib 的替身,哪天想升级直接换成libSystem.C.dylib 然后再替换替身就行了。 减少可执行文件体积:相比静态链接,动态链接在编译时不需要打进去,所以可执行文件的体积要小很多。

动态链接优点.jpg

如上图所示,不同进程之间共用系统dylib的_TEXT区,但是各自维护对应的_DATA区。

所有动态链接库和我们App中的静态库.a和所有类文件编译后的.o文件最终都是由dyld,Apple的动态链接器来加载到内存中。每个image都是由一个叫做ImageLoader的类来负责加载(一一对应)

1. 应用启动

APP启动时间长短,直接会影响用户对APP的第一体验;如果启动时间过长,不但会影响体验导致用户直接奥利给,同时可能会触发苹果的 watch dog机制 kill 掉APP;这就很尴尬了,掉粉啊这 ! APP启动卡死接着直接崩溃了。。。。。。

APP的启动可以分为2种

在衡量APP的启动时间之前先了解下,APP的启动流程:

APP的启动流程.png

Mach-O文件加载

Mach-O文件结构.png

mach-o文件有如下几个部分组成:

APP的启动从用户态来说可以分为三个阶段,即 dyld加载依赖库runtime初始化main函数 总结如下

Launch time = dyld + runtime + main()

当然也有部分同学将其分为两个阶段:main()之前main()之后;这都没有问题,只是概括粒度的差别而已;也可以直接将其分为:内核态用户态

APP加载过程:

  1. 系统会开启一个进程,然后读取可执行文件(Mach-O文件),从里面获得dyld的路径
  2. 加载dyld,dyld先初始化运行环境,开启缓存策略,加载(递归)程序相关依赖库(其中也包含我们的可执行文件)到内存中,并生成相应的镜像

C++静态对象初始化构造器initializer

  1. 对依赖库进行链接 -->link(),调用每个依赖库的初始化方法Initalizer,在这一步,runtime被初始化---->
    (libSystem.dylib库libdispatch_init里调用了runtime的初始化方法_objc_init);
    runtime初始化后不会闲着,在_objc_init注册了几个通知,从dyld这里接手了几个活,其中包括:
    初始化相应依赖库里的类结构
    调用依赖库里所有的load方法

  2. 当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中 所有类进行类结构初始化然后调用所有的load方法

  1. dyld返回main函数地址,这时进程进入就绪状态;main函数被调用后进程进入执行状态,至此便来到了熟悉的程序入口。
    这些事情大多数在 dyld:_main 方法中被发生;

main():调用UIKit库中的UIApplicationMain()找到应用的委托方法执行开发者自定义的任务,比如:获取主控制器显示到UIWindow

  1. Xcode提供的主函数调用UIKit的UIApplicationMain函数

  2. UIApplicationMain函数创建UIApplication对象和你的 AppDelegate

  3. UIKit从主故事板nib文件 加载应用程序的默认入口。

  4. UIKit调用AppDelegate的: willFinishLaunchingWithOptions:方法。

  5. UIKit执行状态恢复,它调用你的AppDelegateUIWindow的附加方法。

  6. UIKit调用AppDelegate的: didFinishLaunchingWithOptions:方法。

  7. 初始化完成后,系统使用场景委托或应用程序委托来显示UI并管理应用程序的生命周期。

总结:

APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
并由runtime负责加载成objc定义的结构
所有初始化工作结束后,dyld就会调用main函数
接下来就是UIApplicationMain函数AppDelegateapplication:didFinishLaunchingWithOptions:方法

补充:

dyld是苹果操作系统一个重要组成部分,而且令人兴奋的是,它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式(下载地址:Source Browser),了解系统加载动态库的细节。

dyld详细流程:

XNU加载程序可执行文件后,通过分析文件来获得dyld所在路径来加载dyld,同时也把当前主程序的Mach-O头部信息给了dyld;有了头部信息,加载器就可以从头开始,遍历整个Mach-O文件的信息,获取LoadCommands(加载命令)、Data(数据、代码);有了这些就正式开始搭建初始化程序环境 ! ! ! !

共九个步骤如下:

第一步: 设置运行环境,处理环境变量

第二步:初始化主程序

第三步:加载共享缓存

第四步:加载插入的动态库

第五步:链接主程序

rebase和bind

rebase修复的是指向当前镜像内部的资源指针。
bind修复的是指向镜像外部的资源指针。

由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这两步来修复镜像中的资源指针,来指向正确的地址。

rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO。bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算

备注:recursiveBind()完成递归绑定符号表的操作。此处的符号表针对的是非延迟加载的符号表,它的核心是调用了doBind(),在ImageLoaderMachOCompressed中,该函数读取映像动态链接信息的bind_offbind_size来确定需要绑定的数据偏移与大小,然后挨个对它们进行绑定,绑定操作具体使用bindAt()函数,它主要通过调用resolve()解析完符号表后,调用bindLocation()完成最终的绑定操作,需要绑定的符号信息有三种:

BIND_TYPE_POINTER:需要绑定的是一个指针。直接将计算好的新值屿值即可。
BIND_TYPE_TEXT_ABSOLUTE32:一个32位的值。取计算的值的低32位赋值过去。
BIND_TYPE_TEXT_PCREL32:重定位符号。需要使用新值减掉需要修正的地址值来计算出重定位值。

第六步:链接插入的动态库

第七步:执行弱符号绑定

第八步:执行初始化方法

第九步:查找程序入口函数并返回

到这里,dyld整个加载动态库的过程就算完成了。

2. 启动时间优化主要针对冷启动

查看main()函数执行之前的耗时:

Xcode提供通过添加环境变量来打印APP启动时间分析

添加环境变量.png

输出结果示例

Total pre-main time:  36.22 milliseconds (100.0%)
         dylib loading time:  14.43 milliseconds (42.1%)
        rebase/binding time:   1.82 milliseconds (5.3%)
            ObjC setup time:   3.89 milliseconds (11.3%)
           initializer time:  13.99 milliseconds (40.9%)
           slowest intializers :
             libSystem.B.dylib :   3.20 milliseconds (6.4%)
   libBacktraceRecording.dylib :   3.90 milliseconds (8.4%)
    libMainThreadChecker.dylib :   6.55 milliseconds (19.1%)
-------------------------   分析    ------------------------
pre: previous  在…以前
在执行main函数之前所用的时间:36.22毫秒
                                    动态库加载:14.43 毫秒
                                    rebase绑定:1.82毫秒
                                ObjC结构准备:3.89毫秒
                                             初始化:13.99毫秒
比较慢的加载:
 libSystem.B.dylib :  3.20 毫秒
 libBacktraceRecording.dylib :   3.90 毫秒
 libMainThreadChecker.dylib :  6.55 毫秒

优化方案:

dyld优化:

虚函数

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

runtime

main

3. 电池能耗

手机电池电量是极其有限的,没有电的手机就像一块没有实际作用的模型;一个APP如果对电池消耗影响很大就可能会被用户奥利给 !

一般开发中对电池消耗比较大的几个方面:

  1. CPU处理,Processing; 高频的处理会加快电量的消耗

  2. 网络,Networking, 长连接发送接收数据,手机需要持续的保持信号接收和发送对手机电量的消耗会比较大;比如:微信、QQ等APP

  3. 定位,Location; 持续定位不断的获取GPS信息,和刷新对电量消耗也会很快 比如:高德导航、百度地图等软件使用起来电量就消耗很快

  4. 图像,Graphics ; GPU的图像渲染是会占用大量的资源,同时也很耗电

可以通过对一下方面做出相应的优化措施:

  1. 尽量不要频繁写入小数据,可以把小数据整理在空闲时一次性写入;在不影响结果的情况下

  2. 读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API, 用dispatch_io系统会优化对磁盘访问;

  3. 数据量比较的情况,不建议直接存储在文件里;可以考虑是用数据库,比如SQLiteCoreData; 因为数据库对数据读写都是有优化过的,有响应的算法,会比直接读写更有优势的多

  4. 网络优化

    • 减少、压缩网络数据,比如网络传输早期用XML:体积比较大 ,后来使用JSON: 体积就比较小;现在也有人在用protocl buffer这种格式传输,但前提是服务器也使用相同的格式接收
    • 上传图片,文件等数据先进行压缩,或者分片上传
    • 如果多次请求的结果相同,尽量使用缓存
    • 断点续传,100MB的文件,下载了一半,突然关机了;下次下载的时候可以从50MB开始继续下载,不需要从头开始
    • 如果网络状态变为不可用或者是未知网络时,不要尝试频繁执行网络请求;
    • 让用户可以取消长时间运行或者网速很慢的网络操作;并设置合适的超时时间
    • 批量传输,比如 下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。 如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封下载
  5. 定位优化

    • 如果只需要快速确定用户位置,最好用CoreLocation框架中CLLocationManagerrequestLocation方法获取位置信息;
      此方法优势:定位完成后,会自动让定位硬件断电
    • 如果不是导航应用,尽量不要实时获取定位;定位完毕就关掉定位服务
    • 尽量降低定位精度,比如尽量不要使用精度较高的kCLLocationAccuracyBest
    • 需要用到后台定位时,尽量设置pausesLocationUpdatesAutomaticallyYES :用来停止当用户静止时位置的自动更新;
  6. 硬件检测优化

    • 用户移动、摇晃、倾斜设备是,会产生动作(motion)事件; 这些事件由加速度计、陀螺仪、磁力计等硬件检测;在不需要检测的场景,应该及时关闭这些的使用

今日头条iOS客户端启动速度优化
iOS 底层 - 性能优化之CPU、GPU
iOS 底层 - 性能优化之安装包瘦身(App Thinning)

上一篇下一篇

猜你喜欢

热点阅读