iOS——APP启动流程

APP启动

APP启动主要分为两个阶段:pre-main和main之后,而APP的启动优化也主要是在这两个阶段进行的。
main之后的优化:1. 减少不必要的任务,2.必要的任务延迟执行,例如放在控制器界面等等。

APP启动的大致过程:
APP启动 -> 加载libSystem -> Runtime注册回调函数 -> 加载image(镜像文件) -> 执行map_images和load_images方法 -> 调用main函数。

查看pre-main耗时,添加DYLD_PRINT_STATISTICS到(Edit Scheme -> Run -> Arguments -> Environment Variables)就可以在控制台看到耗时

缺页错误

我们应该知道:任何程序能运行都是因为存在物理内存,也就是说,程序加入到物理内存才能得以运行,也就是虚拟内存映射到物理内存。这个过程是个使用懒加载方式完成系统到CPU的交互(翻译)的过程。
而这个过程因为懒加载映射方式的缘故,它是“有多少拿多少”,所以我们会通过一页一页的方式也就是page的方式去加载的,iOS的页的大小是16kb,而macOS是4kb。
也是因为是懒加载的方式,所以如果需要用到的时候发现物理内存中没有,就会报出“page fault”的缺页错误,然后缺的页会再加载放入物理内存。这个过程很短,可能30ms,也可能是10ms。

pre-main(main函数前)

pre-main 指的是在程序的 main() 函数执行之前进行的一些初始化工作。这个过程发生在程序的启动阶段,具体是在操作系统加载可执行文件后,调用 main() 函数之前。
例如:加载我们需要的库啊,系统自己调用加载一些依赖库啊,加载类到内存中去啊,加载分类方法并插入到类的方法列表中啊等等

二进制重排

二进制重排是一种优化应用启动性能的技术。它的核心思想是通过重新排列二进制文件中的函数顺序,使得在应用启动时需要频繁调用的函数被排列在一起,从而减少缺页错误(page fault)并加快启动速度。
比如说,当我们启动APP时,就会需要加载很多的页,正常都会有几千页,虽然一页耗时少,但是那个时刻要加载那么多页数,耗时会更长了。我们可以根据InstrumentsSystem Trace找到Main Thread进行查看应用的page in也就是启动加载页数。苹果自用了二进制重排方案就可以优化这个的耗时,例如抖音的二进制重排,怎么找到所有的函数加载,将不必须在前面执行的函数放在后面。

二进制重排的难点

难点在于如何获取并确定这些函数的顺序。

二进制重排的流程

二进制重排流程

  1. 应用程序的启动时刻所加载的顺序是按照Build Phases的Compile Sources的顺序
  2. 去Build Settings中搜索 Write Link Map File设置为YES,就是写入。然后就是Path to Link Map File的地址。
  3. 找到build里面的txt格式的文件,如果是模拟器则为x86_64结尾的。这个就是现在的执行顺序
  4. 打开终端,cd到目录下创建order文件,例如:touch test.order
  5. 将你想要排序的函数依次写进去,然后再在Build Settings中的Order File的路径填写为test.order的文件路径,最后编译一下。

dyld、动态链接器

dyld在各种库加载映射到内存中去起到了至关重要的作用。
我们要研究dyld从APP启动到进入main函数究竟是怎么做的?

dyld流程剖析

我们看这个流程是为了看APP启动到main函数前,也就是dyld是如何将images(镜像文件:如动静态库等)链接到内存中去的。而在objc_init的时候是做了什么操作去调起dyld,以及dyld又如何回调至objc中。

我们根据查看底层的调用栈显示+load方法的调用流程为:_dyld_start->dyldbootstrap::start->dyld::_main->dyld::initializeMainExecutable->ImageLoader::runInitializers->ImageLoader::processInitializers->ImageLoader::recursiveInitialization->dyld::notifySingle->load_images->+[ViewController load]

_dyld_start

_dyld_start是启动时的入口点,它是用汇编语言实现的。
最主要的就是调用start方法,以及dyld层加载结束后调用我们的main方法。
这个函数的主要作用是调用dyldbootstrap::start函数。

dyldbootstrap::start

这个函数也是中间过程,不必知道详细,只知道通过这个函数调用到dyld::_main函数了。

dyld::_main (重要

到这里已经是dyld重中之重了,这个函数的代码行数为849近1000行代码。其实上面的函数调用栈的最大作用也就是引导我们到这里。而这里也大概就是dyld的执行流程了,包括主程序的实例化再到通知进入程序的main函数这个过程。

_main做的事:
第一步:设置运行环境。
第二步:加载共享缓存。
第三部:dyld2/dyld3(ClosureMode闭包模式)加载程序。
第四步:实例化主程序。
第五步:加载插入动态库。
第六步:链接主程序和动态库。
第七步:弱绑定主程序。
第八步:执行初始化。
第九步:返回main函数。

大致流程总结

  1. 条件准备:环境,平台,版本,路径,主机信息等等;
  2. 确定是否有共享缓存并去加载(一般是非模拟器情况)
  3. 载入GDB调试器通知。(老版本的不重要,没用,不知道这个名词没关系)
  4. 添加dyld到UUID列表中,启用堆栈符号化。(没用,不需要知道)
  5. 实例化主程序,instantiateFromLoadedImage(镜像文件加载器,就是以mach-o的header方式加载主程序镜像。)
  6. 加载任何插入的库,(使用loadInsertedDylib)
  7. link(链接)主程序
  8. link 镜像文件(前面插入的库)
  9. 弱引用绑定主程序
  10. (最重要)运行所有初始化的程序。(使用initializeMainExecutable)
  11. 通知dyld可以进入main函数了。(使用notifyMonitoringDyldMain)

初始化流程源码剖析

initializeMainExecutable和runInitializers和processInitializers

我们可以根据上面的调用栈的顺序知道,dyld::_main之后调用的就是dyld::initializeMainExecutable,同时根据上面的流程知道这一步也是最重要的一步,但是实际上这段代码和runInitializersprocessInitializers只是起到中间作用,它们最终调用的recursiveInitialization 才是我们真正重要的一步。

recursiveInitialization
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
……if ( fState < dyld_image_state_dependents_initialized-1 ) {uint8_t oldState = fState;// break cyclesfState = dyld_image_state_dependents_initialized-1;try {// initialize lower level libraries first// 优先初始化依赖的底层的库for(unsigned int i=0; i < libraryCount(); ++i) {ImageLoader* dependentImage = libImage(i);if ( dependentImage != NULL ) {
……else if ( dependentImage->fDepth >= fDepth ) {//依赖文件递归初始化dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);}}}       
……fState = dyld_image_state_dependents_initialized;oldState = fState;//这里调用传递的状态是dyld_image_state_dependents_initialized,image传递的是自己。也就是最后调用了自己的+load。从libobjc.A.dylib开始调用。context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);// initialize this image//初始化镜像文件,调用c++构造函数。libSystem的libSystem_initializer就是在这里调用的。会调用到objc_init中。_dyld_objc_notify_register 中会调用自身的+load方法,然后c++构造函数。//1.调用libSystem_initializer->objc_init 注册回调。//2._dyld_objc_notify_register中调用 map_images,load_images,这里是首先初始化一些系统库,调用系统库的load_images。比如libdispatch.dylib,libsystem_featureflags.dylib,libsystem_trace.dylib,libxpc.dylib。//3.自身的c++构造函数bool hasInitializers = this->doInitialization(context);// let anyone know we finished initializing this imagefState = dyld_image_state_initialized;oldState = fState;//这里调用不到+load方法。 notifySingle内部fState==dyld_image_state_dependents_initialized 才调用+load。context.notifySingle(dyld_image_state_initialized, this, NULL);
……}
……}recursiveSpinUnLock();
}

程序需要初始化的动态库image是从libImage()中获取,而libImage()的数据是在链接动态库的时recursiveLoadLibraries中的setLibImage保存的image
整个过程是一个递归的过程,先初始化最底层的依赖库,再逐步初始化到自己。
**调用notifySingle最终调用到了objc中所有的+load方法。**这里第一个notifySingle调用的是+load方法,第二个notifySingle由于参数是dyld_image_state_initialized不会调用到+load方法。这里的dyld_image_state_dependents_initialized意思是依赖文件初始化完毕了,可以初始化自己了。
调用doInitialization最终调用了c++的系统构造函数。先调用的是libSystem_initializer -> objc_init进行注册回调。在回调中调用了map_imagesload_images(+load)。这里的load_images是调用一些加载一些系统库,比如:libdisp

notifySingle

notifySingle是一个函数指针,在setContext函数里赋值。

map_images与load_images什么时候调用

因为每个镜像文件的加载时机我们是不知道的,所以当镜像文件加载完毕后得有个回调(下句柄)告诉其处理完毕,接下来dyld得需要有个状态去标识,所以我们必须要用notifySingle进行通知。

map_images :镜像文件的加载,引出read_images。该方法很重要
load_images :load方法的加载

map_images 是在notifyBatchPartial调用的,也就是注册完通知就立马去调用。
load_images 是在notifySingle调用。

请添加图片描述

dyld3或dyld2(ClosureMode闭包模式)加载程序

iOS11引入dyld3闭包模式,以回调的方式加载,闭包模式加载速度更快,效率更高。iOS13后动态库和三方库都使ClosureMode加载。
dyld2和dyld3的调用是在dyld::_main函数中的

dyld3:

使用mainClosure来加载。
找到/创建mainClosure后,通过launchWithClosure启动主程序,启动失败后会有重新创建mainClosure重新启动的逻辑。成功后返回result(主程序入口main函数)。launchWithClosure中的逻辑和dyld2启动主程序逻辑基本相同。

dyld2:启动主程序

实例化主程序instantiateFromLoadedImagesMainExecutable 是通过instantiateFromLoadedImage赋值的,也就是把主程序加入allImages中。
插入&加载动态库 loadInsertedDylib。加载在loadInsertedDylib中调用load(主程序和动态库都会添加到allImagesloadAllImages
链接主程序和链接插入动态库(link,主程序链接在前)。在这个过程中记录了dyld加载的时长。可以通过配置环境变量打印出来。
绑定符号(非懒加载、弱符号),懒加载在调用时绑定。
初始化主程序initializeMainExecutable,这个时候还没有执行到主程序中的代码。
找到主程序入口 LC_MAIN(main函数),然后返回主程序。

1.1 动态库和静态库的认识


1.1.1 介绍

库是已写好的、供开发者使用的可复用代码,每个程序都要依赖很多基础的底层库。从本质上,库是一种可执行代码的二进制形式。可以被操作系统载入内存执行。库分为两种:静态库(.a .lib)和 动态库 (framework .so .dll)。 .a是纯二进制文件,.framework中除了有二进制文件外还有资源文件,.a文件不能直接使用,至少需要.h文件配合,而.framework可以直接使用。 .a + .h + sourceFile = .framework

所谓静态和动态是指链接过程,动静态是相对于编译期和运行期的,静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要载入静态库。而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。

1.1.2 静态库

在链接阶段,会将汇编生成的目标文件.o 与 引用的库一起链接到可执行文件中。对应的链接方式称为 静态链接。 静态库中的所有指令都会包含进最终生成的文件中,静态库不能再包含其他的动态库或静态库,在动态链接库中还可以再包含其他的动态或静态链接库。

静态库.png

如果多个进程需要引用到【静态库】,在内存中就会存在多份拷贝,如上图中进程1 用到了静态库1、5,进程2也用到了静态库1、5,那么静态库1、5在编译期就分别被链接到了进程1和进程2中,假设静态库1占用2M内存,如果有20个这样的进程需要用到静态库1,将占用40M的空间。

特点:

  • 静态库对函数库的链接是在编译期完成的。执行期间代码装载速度快。
  • 使可执行文件变大,浪费空间和资源(占空间)
  • 程序的更新、部署与发布不方便,需要全量更新。如果 某一个静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户。

优缺点: 优点:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖 缺点:由于静态库会存在多分,所以会导致目标程序的体积增大,对内存、性能、速度消耗很大

1.1.3 动态库

动态库在程序构建时并不会链接到目标代码中,而是在运行时才被载入,不同的应用程序如果调用相同的库,那么在内存中只需要有一份该共享库的实例,避免了空间浪费问题。同时也解决了静态库对程序的更新的依赖,用户只需更新动态库即可。

理解:

  • 动态库包含一些可供应用程序或其他动态链接库调用的函数
  • 在应用程序调用一个动态链接库里面的函数的时候,操作系统会将动态链接库的文件映射到进程的地址空间中,这样进程中所有的线程就可以调用动态链接库中的函数了
  • 动态链接库加载完成后,并没有将代码编译到可执行文件中,这个时候动态链接库对于进程来说只是一些被放在地址进程空间附加的代码和数据
  • 动态库在内存中只有一个,操作系统也只会加载一次到内存中。只是针对不同的进程进行各自的映射
  • 代码段在内存中的权限都是只读的,所以多个程序虽然使用同一个动态库,但是并不会修改源代码
  • 动态函数库的名字一般是libxxx.so,相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。

动态库.png

  • 【动态库】在内存中只存在一份拷贝,如果某一进程需要用到动态库,只需在运行时动态载入即可。

特点:

  • 动态库把对一些库函数的链接载入推迟到程序运行时期(占时间)。
  • 可以实现进程之间的资源共享。(因此动态库也称为共享库)
  • 将一些程序升级变得简单,不需要重新编译,属于增量更新。

优缺点:

优点:

  • 减少打包后APP的大小,因为不需要拷贝至目标程序中
  • 共享内存、节约资源,因为同一份库被多个程序使用
  • 通过更新动态库即可更新程序,因为不需要重新编译 缺点:
  • 动态库载入会带来一部分性能损失

注意:系统的.framework是动态库,自己建立的.framework是静态库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/418327.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字化转型的关键指南:《数字化专业知识体系》深度剖析应用策略

数字化浪潮下的企业生存法则 随着全球企业加速数字化转型&#xff0c;如何有效应对技术变革带来的挑战和机遇成为各行业关注的焦点。传统的IT管理模式已经无法满足日益复杂的数字化需求&#xff0c;亟需一种新型、综合的知识体系来引导企业迈向成功。《数字化专业知识体系》&a…

企业如何组建安全稳定的跨国通信网络?

组建一个安全稳定的跨国通信网络对于现代企业来说至关重要&#xff0c;尤其是当企业在全球范围内运营时。以下是一些关键步骤和考虑因素&#xff1a; 需求分析&#xff1a; 确定企业的具体通信需求&#xff0c;包括带宽要求、延迟敏感度、数据类型&#xff08;如语音、视频、文…

生日贺卡录放音芯片,多段音频录音ic生产厂商,NVF04M-32minute

可以录音播放的生日贺卡与传统的纸质贺卡相比&#xff0c;它有着创意以及个性的特点&#xff0c;仅需少量的电子元器件&#xff0c;即可实现录音功能&#xff0c;搭配上文字&#xff0c;让声音存储在生日贺卡里&#xff0c;让贺卡也变得有温度&#xff0c;祝福我想亲口对TA说。…

(二)ASP.NET Core WebAPI项目的启动地址设置

上一篇介绍了ASP.NET Core WebAPI项目创建&#xff0c;可参考&#xff1a; 1.webAPI的访问地址 1) 启动时&#xff0c;选择CoreWebAPI(项目名称)运行项目 可以看到打开浏览器后的地址是&#xff1a;applicationUrl"\"launchUrl 2) 启动时&#xff0c;选择IIS Expre…

仿论坛项目--Kafka,构建TB级异步消息系统

阻塞队列 • BlockingQueue 解决线程通信的问题。阻塞方法&#xff1a;put、take。 • 生产者消费者模式生产者&#xff1a;产生数据的线程。消费者&#xff1a;使用数据的线程。 • 实现类ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue、SynchronousQueue、D…

仕考网:事业编面试全流程介绍

1.进入考场 工作人员会检查考生的身份证、准考证以及随身携带物品&#xff0c;可以带食物和水 2.进入候考室 进入候考室&#xff0c;工作人员会再次确认考生信息 3.抽签 考生到齐后&#xff0c;工作人员会组织考生抽签&#xff0c;登记抽签序号、信息确认、发放号码牌 4.…

基于IMX6ULL的Cortex-A中断原理讲解,以及编写其中断向量表

首先借助STM32我们需要了解中断系统是如何构成的 会有一个中断源&#xff0c;也就是能够向CPU发出中断请求的设备或事件。中断源不分硬件和软件&#xff0c;也就是产生中断信号&#xff0c;就会执行中断服务函数 但是CPU是如何知道中断源产生后就找到对应的中断…

springboot+vue+mybatis智慧篮球馆预约+PPT+论文+讲解+售后

近些年来&#xff0c;随着科技的飞速发展&#xff0c;互联网的普及逐渐延伸到各行各业中&#xff0c;给人们生活带来了十分的便利&#xff0c;智慧篮球馆预约利用计算机网络实现信息化管理&#xff0c;使整个智慧篮球馆预约的发展和服务水平有显著提升。 本文拟采用Eclipse开发…

玄机科技《师兄啊师兄》——动漫宇宙里绽放“黑神话”之光!

当国产3A游戏《黑神话&#xff1a;悟空》以其对中国文化的深情致敬与精湛呈现&#xff0c;点燃了全球玩家们心中的文化之火&#xff0c;震撼游戏界时&#xff0c;独属于动漫界的“黑神话”——玄机科技以科技之名&#xff0c;行文化之实&#xff0c;将这股文化热潮延伸至动漫领…

[羊城杯 2021]Ez_android-快坚持不下去的第五天

找到mainactivity函数 1. 用户名和密码的检查 2. 密码的加密然后 - 1 的操作 for (int i 0; i < bArr.length; i) {bArr[i] (byte) (bArr[i] - 1); } 这段代码通过遍历字节数组中的每个元素&#xff0c;将每个元素的值减去 1&#xff0c;并更新数组。这里的 byte 强制转…

LLM Attention and Rotary Position Embedding(旋转位置编码)

旋转位置编码&#xff08;Rotary Position Embedding&#xff0c;RoPE&#xff09;是一种能够将相对位置信息依赖集成Attention计算里的方法。就是在做词表映射的时候不是单一的进行一个embedding计算&#xff0c;还考虑位置信息。 一些资料 [1] https://arxiv.org/pdf/2104.0…

2024全国大学省数学建模竞赛A题-原创参考论文(部分+第一问代码)

一问题重述 1.1 问题背景 "板凳龙"&#xff0c;又称"盘龙"&#xff0c;是浙闽地区的传统地方民俗文化活动。这种独特的表演艺术形式融合了中国传统龙舞的精髓和地方特色&#xff0c;展现了人们对美好生活的向往和对传统文化的传承。 在板凳龙表演中&am…

版本控制系统Git/Gitlab/GitHub

版本控制系统 git和svn:公司内部的代码仓库&#xff0c;用于存放项目代码&#xff0c;方便整合开发过程 公共代码仓库&#xff1a;github全球 gitee国内 git 分布式 ---没有中心代码库&#xff0c;所有机器之间的地位同等&#xff08;每台机器上都有相同的代码&#xff09; …

18055 主对角线上的元素之和

### 思路 1. 输入一个3行4列的整数矩阵。 2. 计算主对角线上的元素之和。 3. 输出主对角线上的元素之和。 ### 伪代码 1. 初始化一个3行4列的矩阵 matrix。 2. 输入矩阵的元素。 3. 初始化一个变量 sum 为0&#xff0c;用于存储主对角线元素之和。 4. 遍历矩阵的行&#xff0c…

AI产品经理:ai产品经理从零基础到精通,非常详细收藏我这一篇就够了

在互联网的浪潮中&#xff0c;AI人工智能领域无疑是最引人注目的风口。AI产品经理&#xff0c;作为这一领域的新兴岗位&#xff0c;以其高薪、低压力、无年龄限制等优势&#xff0c;吸引了众多互联网从业者的目光。随着GPT等AIGC工具的兴起&#xff0c;AI产品经理的市场需求日益…

企业网银登录提示请确认您已插入工商银行U盾证书的解决方法

昨天受人之托帮小企业财务解决上网银的问题 因为不是专业做这个的&#xff0c;所以只能安装“常识”行事&#xff0c;但结果实在是令人意想不到。 排错的步骤&#xff1a; 同一台电脑上尝试不同浏览器&#xff0c;发现360浏览器的接受度相当普遍&#xff1b;给U盾换不同的连接…

【408 数据结构】第1章绪论

文章目录 绪论考纲DS 基本概念1. 基本概念2. 数据结构三要素 算法&#xff08;时/空间复杂度计算&#xff09;1. 算法概念2. 算法效率的度量时间复杂度&#xff1a;空间复杂度&#xff1a; 小结 绪论 考纲 计算时间复杂度和空间复杂度&#xff08;重点难点&#xff09; DS …

如何使用AI来免费提升你的图片质量

学习如何使用AI免费放大您的图像&#xff0c;可以将那些恼人的低分辨率图像转变为高分辨率的杰作——至少在某种程度上是这样。虽然使用我们用于此任务的应用程序Upscayl需要稍微调整一下不同的模型&#xff0c;但您至少应该能够将图像转换成视觉上更令人愉悦的效果。 Upscayl…

Python教程(二十) : 十分钟入门【PyQt6】

文章目录 专栏列表环境准备1 安装 Python2 安装 PyQt6 创建 PyQt6 项目1 创建项目目录2 创建主 Python 文件 代码书写测试流程1 导入 PyQt6 模块2 创建主窗口类3 创建应用程序实例并运行 核心解析&#xff1a;PyQt6 中的模块示例代码&#xff1a; PyQt6 常用的控件1. QPushButt…

【Linux网络编程八】实现最简单Http服务器(基于Tcp套接字)

基于TCP套接字实现一个最简单的Http服务器 Ⅰ.Http请求和响应格式1.请求格式2.响应格式3.http中请求格式中细节字段4.http中响应格式中细节字段 Ⅱ.域名ip与URLⅢ.web根目录Ⅳ.Http服务器是如何工作的&#xff1f;一.获取请求二.分析请求2.1反序列化2.2解析url 三.构建响应3.1构…