做者:bool周 原文连接:按下 ⌘ + R 后发生的事情html
做为一名 coder,天天的工做不是解 bug,就是写 bug。有些东西,了解了并不必定有利于写 bug,可是有利于解 bug。前端
对于一个工程,当你按下 ⌘ + R
到主界面显示出来,你可曾想过这一过程发生了哪些事情?这些原理性的东西,对咱们 coding 并无直接帮助,了解与否均可以 coding。可是一个 coder 的工做不仅是 coding,还有 debug。了解这些东西,对咱们排查一些问题颇有帮助。ios
按照阶段划分,这一过程大体能够划为三个阶段:编译阶段、APP 启动阶段、图层渲染阶段。下面针对这三个过程进行详细描述。git
学过编译原理的同窗都应该知道,编译主要分为四个过程:预处理、编译、汇编、连接。下面大体也是按照这个路子来。iOS 编译过程,使用的 clang 作前端,LLVM 做为后端进行完成的。使用 clang 处理前几个阶段,LLVM 处理后面几个阶段。github
又称为预编译,主要作一些文本替换工做。处理 #
开头的指令,例如:面试
例如咱们在代码中定义了以下宏:objective-c
#define APP_VERSION "V1.0.0"
int main(int argc, char * argv[]) {
char *version = APP_VERSION;
printf("app version is %s",version);
}
复制代码
使用 clang -E main.m
进行宏展开的预处理结果以下:sql
int main(int argc, char * argv[]) {
char *version = "V1.0.0";
printf("version is %s",version);
return 0;
}
复制代码
宏的使用有不少坑,尽可能用其余方式代替。macos
完成预处理后,词法分析器(也叫扫描器)会对 .m 中的源代码进行从左到右扫描,按照语言的词法规则识别各种单词、关键字,并生成对应的单词的属性字。例以下面一段代码:bootstrap
#define APP_VERSION "V1.0.0"
int main(int argc, char * argv[]) {
char *version = APP_VERSION;
printf("version is %s",version);
return 0;
}
复制代码
通过预处理阶段,而后使用 clang 命令 clang -Xclang -dump-tokens main.m
进行扫描分析,导出结果以下:
int 'int' [StartOfLine] Loc=<main.m:14:1>
identifier 'main' [LeadingSpace] Loc=<main.m:14:5>
l_paren '(' Loc=<main.m:14:9>
int 'int' Loc=<main.m:14:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:14:14>
comma ',' Loc=<main.m:14:18>
char 'char' [LeadingSpace] Loc=<main.m:14:20>
star '*' [LeadingSpace] Loc=<main.m:14:25>
identifier 'argv' [LeadingSpace] Loc=<main.m:14:27>
l_square '[' Loc=<main.m:14:31>
r_square ']' Loc=<main.m:14:32>
r_paren ')' Loc=<main.m:14:33>
l_brace '{' [LeadingSpace] Loc=<main.m:14:35>
char 'char' [StartOfLine] [LeadingSpace] Loc=<main.m:18:5>
star '*' [LeadingSpace] Loc=<main.m:18:10>
identifier 'version' Loc=<main.m:18:11>
equal '=' [LeadingSpace] Loc=<main.m:18:19>
string_literal '"V1.0.0"' [LeadingSpace] Loc=<main.m:18:21 <Spelling=main.m:12:21>>
semi ';' Loc=<main.m:18:32>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:19:5>
l_paren '(' Loc=<main.m:19:11>
string_literal '"version is %s"' Loc=<main.m:19:12>
comma ',' Loc=<main.m:19:27>
identifier 'version' Loc=<main.m:19:28>
r_paren ')' Loc=<main.m:19:35>
semi ';' Loc=<main.m:19:36>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:20:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:20:12>
semi ';' Loc=<main.m:20:13>
r_brace '}' [StartOfLine] Loc=<main.m:21:1>
eof '' Loc=<main.m:21:2>
复制代码
从上面能够看出每一个单词或者字符,都标记出了具体列数和行数,这样若是在编译过程当中遇到什么问题,clang 能够快速定位错误在代码中的位置。
接下来是进行语法分析。经过这一阶段,会将上一阶段的导出的结果解析成一棵抽象语法树(abstract syntax tree – AST)。假设咱们的源代码以下,而且已经通过了预处理:
#define APP_VERSION "V1.0.0"
int main(int argc, char * argv[]) {
char *version = APP_VERSION;
printf("version is %s",version);
return 0;
}
复制代码
使用 clang 命令 clang -Xclang -ast-dump -fsyntax-only mian.m
处理事后,输入的语法树以下:
...
FunctionDecl 0x7ffe55884228 <main.m:14:1, line:21:1> line:14:5 main 'int (int, char **)'
|-ParmVarDecl 0x7ffe55884028 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7ffe55884110 <col:20, col:32> col:27 argv 'char **':'char **'
`-CompoundStmt 0x7ffe55884568 <col:35, line:21:1>
|-DeclStmt 0x7ffe55884390 <line:18:5, col:32>
| `-VarDecl 0x7ffe558842e8 <col:5, line:12:21> line:18:11 used version 'char *' cinit
| `-ImplicitCastExpr 0x7ffe55884378 <line:12:21> 'char *' <ArrayToPointerDecay>
| `-StringLiteral 0x7ffe55884348 <col:21> 'char [7]' lvalue "V1.0.0"
|-CallExpr 0x7ffe558844b0 <line:19:5, col:35> 'int'
| |-ImplicitCastExpr 0x7ffe55884498 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7ffe558843a8 <col:5> 'int (const char *, ...)' Function 0x7ffe55088570 'printf' 'int (const char *, ...)'
| |-ImplicitCastExpr 0x7ffe55884500 <col:12> 'const char *' <BitCast>
| | `-ImplicitCastExpr 0x7ffe558844e8 <col:12> 'char *' <ArrayToPointerDecay>
| | `-StringLiteral 0x7ffe55884408 <col:12> 'char [14]' lvalue "version is %s"
| `-ImplicitCastExpr 0x7ffe55884518 <col:28> 'char *' <LValueToRValue>
| `-DeclRefExpr 0x7ffe55884440 <col:28> 'char *' lvalue Var 0x7ffe558842e8 'version' 'char *'
`-ReturnStmt 0x7ffe55884550 <line:20:5, col:12>
`-IntegerLiteral 0x7ffe55884530 <col:12> 'int' 0
复制代码
抽象语法树中每个节点也标记出了在源码中的具体位置,便于问题定位。抽象语法树的相关知识有不少,这里就不详细解释了。
把源码转化为抽象语法树以后,编译器就能够对这个树进行分析处理。静态分析会对代码进行错误检查,如出现方法被调用可是未定义、定义可是未使用的变量等,以此提升代码质量。固然,还能够经过使用 Xcode 自带的静态分析工具(Product -> Analyze)或者一些第三方的静态分析工具(例如 Facebook 的 infer进行深度分析。
有时候编译器自带的静态分析,并不能知足咱们的平常开发需求。所以咱们能够经过使用脚本定制一套分析方案,放到集成环境中。每次提交代码时,会触发脚本进行静态分析,若是出现错误边报出警告,而且提交代码失败。依次过高开发质量。
若是有兴趣,能够看一下 clang 静态分析源码,看其中对哪些语法作了静态分析。
使用 clang 完成预处理和分析以后,接着会生成 LLVM 代码。仍是以前那段代码:
#define APP_VERSION "V1.0.0"
int main(int argc, char * argv[]) {
char *version = APP_VERSION;
printf("version is %s",version);
return 0;
}
复制代码
咱们能够用 clang 命令 clang -O3 -S -emit-llvm main.m -o main.ll
进行转化,而后打开以后看到内容以下:
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.13.0"
@.str = private unnamed_addr constant [7 x i8] c"V1.0.0\00", align 1
@.str.1 = private unnamed_addr constant [14 x i8] c"version is %s\00", align 1
; Function Attrs: nounwind ssp uwtable
// main 方法
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
%3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str.1, i64 0, i64 0), i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str, i64 0, i64 0))
ret i32 0
}
; Function Attrs: nounwind
declare i32 @printf(i8* nocapture readonly, ...) local_unnamed_addr #1
attributes #0 = { nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nounwind "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5}
!llvm.ident = !{!6}
!0 = !{i32 1, !"Objective-C Version", i32 2}
!1 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA, __objc_imageinfo, regular, no_dead_strip"}
!3 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!4 = !{i32 1, !"Objective-C Class Properties", i32 64}
!5 = !{i32 1, !"PIC Level", i32 2}
!6 = !{!"Apple LLVM version 9.0.0 (clang-900.0.39.2)"}
复制代码
能够简单看一下 main 方法,看不懂无所谓,我也看不懂。只是了解这个过程就能够了。
接下来 LLVM 会对代码进行编译优化,例如针对全局变量优化、循环优化、尾递归优化等,这些我了解的不是太多,因此不能乱说。想要了解的同窗,能够看一下这篇文章:《LLVM 全时优化》。
最后就是输出汇编代码。
在这一阶段,汇编器将可读的汇编代码转化为机器代码。最终产物就是 以 .o 结尾的目标文件。
针对下部分代码:
#define APP_VERSION "V1.0.0"
int main(int argc, char * argv[]) {
char *version = APP_VERSION;
printf("version is %s",version);
return 0;
}
复制代码
咱们可使用 clang 命令 clang -c main.m
生成目标文件 mian.o。我就不写打开后的内容了,都是二进制,也看不懂。
这一阶段是将上个阶段生成的目标文件和引用的静态库连接起来,最终生成可执行文件。
咱们能够用 clang 命令 clang main.m
生成可执行文件 a.out (不指定名字默认命名为 a.out)。而后使用 file a.out
命令查看其类型:
a.out: Mach-O 64-bit executable x86_64
复制代码
能够看出可执行文件类型为 Mach-O
类型,在 MAC OS 和 iOS 平台的可执行文件都是这种类型。由于我使用的是模拟器,因此处理器指令集为 x86_64
。
至此编译阶段完成。
最后咱们先来看一下 Xcode 中的 build 日志,完整的看一遍这个过程。打开 Xcode 的 Log Navigator,选中 Build 这一项咱们能够看到此次 build 的日志:
日志是按照 target 进行分段的。当前工程中,经过 Pod 引入了 YYCache
、YYImage
、AFNetworking
三个库,除此以外还有一个 Pods-Test
和项目自己的 target。每一个 target 之间的日志格式都是同样的,所以咱们只针对一个 target 进行分析。这里只针对项目自己 target,也就是 Test
进行分析。也就是下面这个样子:
看着很乱套,整理完以后,屡一下大概是这个流程:
Check Pods Manifest.lock
。这里咱们针对第 4 步详细说一下。咱们选取其中一个文件 ViewController.m
的日志进行分析:
将 log 信息整理一下:
1. CompileC /.../Test.build/Objects-normal/x86_64/ViewController.o Test/ViewController.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler
2. cd /Users/zhoubo/Test
3. export LANG=en_US.US-ASCII
export PATH="/Applications/Xcode.app/Contents/Developer/../sbin"
4. clang -x objective-c
-arch x86_64 -fmessage-length=0...
-fobjc-arc...
-Wno-missing-field-initializers...
-DDEBUG=1...
-isysroot .../iPhoneSimulator11.2.sdk
-I ONE PATH
-F ONE PATH
-c /../ViewController.m
-o /../ViewController.o
复制代码
对应解释以下:
clang 命令开始:
-x : 所使用语言,此处为 Objective-C
-arch x86_64 : 处理器指令集为 x86_64
-fobjc-arc : 一系列以 -f 开头,指定此文件使用 ARC 环境。你能够经过 Build Phases 设置对每一个文件是否支持 ARC。
-Wno-missing-field-initializers : 一系列以 -w 开头指令,编译警告选项,能够经过这个指令定制编译选项
-DDEBUG=1 : 一些以 -D 开头的,指的是预编译宏。
-isysroot .../iPhoneSimulator11.2.sdk : 编译时采用的 iOS SDK 版本。
-I : 把编译信息写入文件
-F : 连接过程当中所须要的 framework
-c : 编译文件
-o : 编译中间产物
复制代码
每次咱们编译事后,都会生成一个 dSYM 文件。这个文件中,存储了 16 进制的函数地址映射表。在 APP 执行的二进制文件中,是经过地址来调用方法的。当发生了 crash,能够经过 dSYM 文件进行地址映射,找到具体的函数调用栈。
上个阶段,最终产物为可执行文件,文件格式为 Mach-o。这一阶段,就以这个文件开始,详细描述一下 APP 启动过程。
这一过程分为多个阶段,简单梳理一下,可使大脑有一个清晰的脑回路,不至于越看越懵逼。
官方的一张流程图:
在讲述整个过程以前,先解释两个概念:Mach-O 文件 和 dyld。
Mach-O 是一种文件格式,主要用于 iOS、MacOS、WatchOS 等 Apple 操做系统。这种文件格式可用于一下几种文件:
Mach-O 文件的格式以下:
上个阶段中咱们知道如何产生可执行文件(a.out),这里咱们能够用 size 工具来查看这个可执行文件的 segment 内容,执行以下命令:
xcrun size -x -l -m a.out
复制代码
能够获得以下结果:
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x43 (addr 0x100000f30 offset 3888)
Section __stubs: 0x6 (addr 0x100000f74 offset 3956)
Section __stub_helper: 0x1a (addr 0x100000f7c offset 3964)
Section __cstring: 0x15 (addr 0x100000f96 offset 3990)
Section __unwind_info: 0x48 (addr 0x100000fac offset 4012)
total 0xc0
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
Section __objc_imageinfo: 0x8 (addr 0x100001018 offset 4120)
total 0x20
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000
复制代码
长话短说:
Segment __PAGEZERO
。大小为 4GB,规定进程地址空间的前 4GB 被映射为不可读不可写不可执行。Segment __TEXT
。包含可执行的代码,以只读和可执行方式映射。Segment __DATA
。包含了将会被更改的数据,以可读写和不可执行方式映射。Segment __LINKEDIT
。包含了方法和变量的元数据,代码签名等信息。动态加载器(dynamic loader)。它是开源的,若是有兴趣,你能够阅读它的源码。dyld1 已通过时,不用去理解。目前大多用的是 dyld2。在 WWDC2017 上 Apple 新推出了 dyld3,目前只在 iOS 系统 App 上使用,后面应该会普及。这一阶段最后会详细介绍一下 dyld3,这里就不描述了。
下面开始正式讲解启动过程。
点击 APP 以后,到加载 dyld 动态加载器这一过程当中,系统作了不少事情,大致分为以下图几个阶段:
大部分同窗没有深刻研究过这部份内容,我也没有深刻研究过。因此我尽可能复杂问题简单化,以最简单的方式将这些过程讲述明白。
load_init_program
函数加载系统初始化的进程。而后再方法内调用 load_init_program_at_path
。经过 load_init_program_at_path
方法调用 __mac_execve
。__mac_execve
函数会启动新的进程和 task,调用 exec_activate_image
。exec_activate_image
函数会按照二进制的格式分发映射内存的函数。Mach-O
文件会由 exec_mach_imgact
处理。exec_mach_imgact
函数中,会检测 Mach-O
header,解析其架构等信息,文件是否合法等;先拷贝 Mach-O
文件到内存中;而后拷贝 Mach-O
文件到内存中;以后是 dyld 相关处理工做;最后释放资源。load_machfile
函数负责 Mach-O
文件加载相关工做。为当前 task 分配可执行内存;加载 Mach-O
中 load command 部分的命令;进制数据段执行,防止溢出漏洞攻击,设置 ASLR 等;最后为 exec_mach_imgact
回传结果。parse_machfile
根据 load_command
的信息选择不一样函数加载数据。其中使用的是 switch-case
语句,处理的类型有 LC_LOAD_DYLINKER
、LC_ENCRYPTION_INFO_64
等。LC_LOAD_DYLINKER
。进入这个 case 三次,并存在 dylinker_command
命令,以后会执行 load_dylinker()
加载 dyld。在 dyld 的源码中,有一个 dyldStartup.s 文件。这个文件针对不一样的 CPU 架构,定义了不一样的启动方法,大同小异。这里会执行到 __dyld_start
方法,而后调用 dyldbootstrap::start()
方法,最终调用到 dyld.cppp 中的 dyld::_main()
方法。部分代码以下:
__dyld_start:
pushq $0 # push a zero for debugger end of frames marker
movq %rsp,%rbp # pointer to base of kernel frame
andq $-16,%rsp # force SSE alignment
# call dyldbootstrap::start(app_mh, argc, argv, slide)
movq 8(%rbp),%rdi # param1 = mh into %rdi
movl 16(%rbp),%esi # param2 = argc into %esi
leaq 24(%rbp),%rdx # param3 = &argv[0] into %rdx
movq __dyld_start_static(%rip), %r8
leaq __dyld_start(%rip), %rcx
subq %r8, %rcx # param4 = slide into %rcx
call __ZN13dyldbootstrap5startEPK12macho_headeriPPKcl
# clean up stack and jump to result
movq %rbp,%rsp # restore the unaligned stack pointer
addq $16,%rsp # remove the mh argument, and debugger end frame marker
movq $0,%rbp # restore ebp back to zero
jmp *%rax # jump to the entry point
复制代码
_main()
方法包含了 App 的启动流程,最终返回应用程序 main
方法的地址,这里省略代码,只标注流程:
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[])
{
// 上下文创建,初始化必要参数,解析环境变量等
......
try {
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
sMainExecutable->setNeverUnload();
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.processIsRestricted = sProcessIsRestricted;
// load shared cache
checkSharedRegionDisable();
#if DYLD_SHARED_CACHE_SUPPORT
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
mapSharedCache();
#endif
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
......
// link main executable
gLinkContext.linkingMainExecutable = true;
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL));
gLinkContext.linkingMainExecutable = false;
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
// get main address
result = (uintptr_t)sMainExecutable->getMain();
......
return result;
}
复制代码
上文提到过,image 实际是 Mach-O
文件的一种,包括 Executable,Dylib 或者 Bundle。在上节的 dyld::_main()
函数中能够看出,dyld 会经过调用 instantiateFromLoadedImage
选择imageLoader
加载对应可执行文件。
而后经过 mapSharedCache()
函数将 /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64
共享的动态库加载到内存,这也是不一样的 App 实现动态库共享机制,不一样的 App 的虚拟内存中共享动态库会经过系统的 vm_map 来映射同一块物理内存,从而实现共享动态库。
以后会调用 loadInsertedDylib()
函数加载环境变量 DYLD_INSERT_LIBRARIES
中的动态库。loadInsertedDylib
动态库并未作太多工做,主要工做都是调用 load
函数来处理,dlopen
也会调用 load
函数来进行动态库加载。
再后面调用 link()
函数递归连接程序所依赖的库。通常一个 App 所依赖的动态库在 100-400 个左右。使用命令 otool -L Test
能够查看 Test 工程所须要的动态库以下:
/usr/lib/libsqlite3.dylib (compatibility version 9.0.0, current version 274.6.0)
/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11)
/System/Library/Frameworks/Accelerate.framework/Accelerate (compatibility version 1.0.0, current version 4.0.0)
/System/Library/Frameworks/AssetsLibrary.framework/AssetsLibrary (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 1450.14.0)
/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics (compatibility version 64.0.0, current version 1129.2.1)
/System/Library/Frameworks/ImageIO.framework/ImageIO (compatibility version 1.0.0, current version 0.0.0)
/System/Library/Frameworks/MobileCoreServices.framework/MobileCoreServices (compatibility version 1.0.0, current version 822.19.0)
/System/Library/Frameworks/QuartzCore.framework/QuartzCore (compatibility version 1.2.0, current version 1.11.0)
/System/Library/Frameworks/Security.framework/Security (compatibility version 1.0.0, current version 58286.32.2)
/System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration (compatibility version 1.0.0, current version 963.30.1)
/System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 3698.33.6)
/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1450.14.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libSystem.dylib (compatibility version 1.0.0, current version 1252.0.0)
复制代码
对于 CocoaPods 中的第三方库,通常是以静态库的方式加载,因此使用
otool -L [文件名]
并不会看到 Pod 中的库。可是若是 Podfile 中加入了use_frameworks!
,即以动态库方式加载,才会看到,也就是上面所示。
最后,获取到应用程序 main 函数地址,返回。
这两个过程,并非在上面 _main()
方法返回以后进行的,而是在上一节中 “link main executable” 这一步进行的。
Apple 为了保证应用安全,应用了两种技术:ASLR (Address space layout randomization) 和 Code sign。
ASLR 是指 “地址空间布局随机化”。App 启动的时候,程序会被映射到一个逻辑地址空间。若是这个地址固定,很容易根据地址+偏移量计算出函数地址,被攻击。 ASLR 使得这个地址是随机的,防止攻击者直接定位攻击代码位置。
Code sign 是指代码签名。Apple 使用两层非对称加密,以保证 App 的安全安装。在进行 Code sign 时,是针对每一个 page 进行加密,这样在 dyld 加载时,能够针对每一个 page 进行独立验证。
由于使用 ASLR 致使的地址随机,须要加上偏移量才是真正方法地址。调用的一个方法,这个方法的地址可能属于 Mach-O 文件内部,也可能属于其余 Mach-O 文件。
Rebase 是修复内部符号地址,即修复的是指向当前 Mach-O 文件内部的资源指针,修复过程只是加一个偏移量就能够。
Bind 是修复外部符号地址,即修复的是指向外部 Mach-O 文件指针。这一过程须要查询符号表,指向其余 Mach-O 文件,比较耗费时间。
官方给出的一张图以下:
简言之就是,前面步骤加载动态库时地址指偏了,这里进行 fix-up,不然调不到。
至此,Mach-O 的加载就完事儿了,下面就是 iOS 系统的事情了。
Objc 是一门动态语言,这一步主要来加载 Runtime 相关的东西。主要作一下几件事情:
这一步主要处理自定义的一些类和方法。大部分系统类的 Runtime 初始化已经在 Rebase 和 Bind 中完成了。
这一步进行一些类的初始化。这是一个递归过程,先将依赖的动态库初始化,再对本身自定义的类初始化。主要作的事情有:
+[load]
方法。__attribute__(constructor)
的方法。Swift 用已经干掉了
+load
方法,官方建议使用initialize
方法,减小 App 启动时间。
千辛万苦,咱们终于来到了 main()
方法。
基于 C 的程序通常都以 main()
方法为入口,iOS 系统会为你自动建立 main()
方法。代码很简单:
int main(int argc, char * argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
复制代码
这里用的 UIApplicationMain
方法声明以下:
UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nonnull * _Null_unspecified argv, NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);
复制代码
UIApplication
类型或者其子类。若是为 nil,则使用 UIApplication
类。UIApplicationDelegate
协议。UIApplication
对象,并根据 delegateClassName 建立 delegate 对象,将这个对象赋值给 UIApplication
对象的 delegate 属性。关于 AppDelegate
中的一些方法:
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 通知进程已启动,可是还未完成显示。
return YES;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 启动完成,程序准备开始运行。页面显示前最后一次操做机会。
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
// App 失去焦点,进入非活动状态。主要实例有:来电话,某些系统弹窗,双击 home 键,下拉显示系统通知栏,上拉显示系统控制中心等。
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
// App 进入后台。
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// App 进入前台。冷启动不会收到这个通知。
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// App 得到焦点,处于活动状态。冷热启动都会收到这个通知。
}
- (void)applicationWillTerminate:(UIApplication *)application {
// 应用将要退出时,能够在这个方法中保存数据和一些退出前清理工做。
}
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
// 收到内存警告,释放一些内存。
}
@end
复制代码
上文说有详细讲一下 dyld3,放到这里了。dyld3 是 WWDC 2017 介绍的新的动态加载器。与 dyld2 对好比下图:
二者的区别,通俗一点说就是:dyld2 全部的过程都是在启动时进行的,每次启动都会讲全部过程走一遍;dyld3 分红了两部分,虚线上面的部分在 App 下载安装和版本更新时执行并将结果写入缓存,虚线下面的部分在每次 App 启动执行。
这样减小了 dyld 加载步骤,也就加快了 APP 启动时间。不过目前 dyld3 只在 Apple 系统 App 才会使用,开发者不能使用。后面应该会普及。
根据上面的分析过程,咱们能够大致总结出,若是要针对 App 作启动优化,能够从哪些方面入手:
__DATA
中的指针数量。+[load]
方法中写东西,减小 __atribute__((constructor))
,减小非基本类型 C++ 静态常量建立。作了一堆准备工做,可算是到了渲染展现界面了。
图层的布局过程(这里指自动布局),主要分为三步:设置约束、更新布局、渲染视图。这里会结合 view controller 的生命周期来说解。
在程序启动时,会将 App 放到 Main Run Loop 中来响应和处理用户交互事件。关于 RunLoop,简单说来就是一个循环,只要 App 未被杀死,这个循环就一直存在。每一次循环能够认为是一个迭代周期,这个周期中会相应和处理用户交互事件。当完成了各类事件处理以后控制流回到 Main Run Loop 那个时间点,开始更新视图,更新完进入下一个循环。整个过程以下图所示:
在 update cycle 这个阶段,系统会根据计算出来的新的 frame 对视图进行重绘。这个过程很快,因此用户感受不到延迟卡顿。由于视图的更新是按照周期来的,因此有时候修改了约束、添加了视图或者修改了 frame 并不会当即重绘视图。接下来就详细介绍这一过程。
一个视图的 frame 包含了视图的位置和大小,经过这个 frame(和当前坐标系) 能够肯定视图的具体位置。约束的本质就是设置一系列的关系,计算布局时会将这些关系转化为一系列线性方程式,经过线性方程式求解得出 x,y,width,height,从而肯定视图位置。这一阶段是从下向上(from subview to super view),为下一步布局准备消息。
updateConstraints()
这个方法用来在自动布局中动态改变视图约束。通常状况下,这个方法只应该被重载,不该该手动调用。在开发过程当中,一些静态约束,能够在视图初始化方法或者 viewDidLoad()
方法中设置;对于一些动态约束,例如 UILabel
有时须要随着文案字数改变大小,须要动态修改约束,这时候能够重载此方法,将动态修改约束代码写在次方法里。
还有一些操做会将视图标记,在下一个 update cycle 中自动触发这个方法:
setNeedsUpdateConstraints()
若是你但愿视图在下一个 update cycle 中必定要调用 updateConstraints()
方法,你能够调用此方法,这样就给视图打上一个标记,若是有必要在下一个 update cycle 便会调用 updateConstraints()
方法。
这里说“若是有必要“,是由于若是系统检测视图没有任何变化,即便标记了,也不会调用此方法,避免耗费性能。因此标记了,只是告诉系统到时候 check 一下,是否要更新约束。下面一些方法同理。
updateConstraintsIfNeeded()
若是你不想等到 run loop 末尾,进入 update cycle 的时候,再去检查标记并更新约束。你想马上检查被打上标记的视图,更新约束,能够调用此方法。一样的,调用此方法只会检查那些被标记的视图,若是有必要,才会调用 updateConstraints()
方法。
invalidateIntrinsicContentSize()
有些视图(例如 UILabel)有 intrinsicContentSize
属性,这是根据视图内容获得的固有大小。你也能够经过重载来自定义这个大小,重载以后,你须要调用 invalidateIntrinsicContentSize()
方法来标记 intrinsicContentSize
已通过期,须要再下一个 update cycle 中从新计算。
根据约束计算出视图大小和位置,下一步就是布局。这一部分是从上向下(from super view to subview),使用上一步计算出来的大小和位置去设置视图的 center 和 bounds。
layoutSubviews()
这个方法会对视图和其子视图进行从新定位和大小调整。这个方法很昂贵,由于它会处理当前视图和其自视图的布局状况,还会调用自视图的 layoutSubviews()
,层层调用。一样,这个方法只应该被重载,不该该手动调用。当你须要更新视图 frame 时,能够重载这个方法。
一些操做可能会触发这个方法,间接触发比手动调用资源消耗要小得多。有如下几种状况会触发此方法:
这些状况有的会告诉系统视图 frame 须要从新计算,从而调用 layoutSubviews()
,也有的会直接触发 layoutSubviews()
方法。
setNeedsLayout()
此方法会将视图标记,告诉系统视图的布局须要从新计算。而后再下一个 update cycle 中,系统就会调用视图的 layoutSubviews()
方法。一样的,若是有必要,系统才会去调用。
layoutIfNeeded()
setNeedsLayout
是标记视图,在下个 update cycle 中可能会调用 layoutSubviews()
方法。而 layoutIfNeeded()
是告诉系统当即调用 layoutSubviews()
方法。固然,调用了 layoutIfNeeded()
方法只会,系统会 check 视图是否有必要刷新,若是有必要,系统才会调用 layoutSubviews()
方法。若是你再同一个 run loop 中调用了两次 layoutIfNeeded()
,两次之间没有视图更新,那么第二次则不会触发 layoutSubviews()
。
在作约束动画时,这个方法颇有用。在动画以前,调用此方法以确保其余视图已经更新。而后在 animation block 中设置新的约束后,调用此方法来动画到新的状态。例如:
[self.view layoutIfNeeded];
[UIView animateWithDuration:1.0 animations:^{
[self changeConstraints];
[self.view layoutIfNeeded];
}];
复制代码
视图的显示包含了颜色、文本、图片和 Core Graphics 绘制等。与约束、布局两个步骤相似,这里也有一些方法用来刷新渲染。这一过程是从上向下(from super view to subview)。
draw(_:)
UIView 的 draw
方法(OC 中的 drawRect)用来绘制视图显示的内容,只做用于当前视图,不会影响子视图。依然,这个方法应该经过其余方法触发,而不该该手动调用。
setNeedsDisplay()
这个方法相似于布局中的 setNeedsLayout()
。调用此方法会将视图标记,而后在下一个 update cycle 系统遍历被标记的视图,调用其 draw()
方法进行重绘。大部分 UI 组件若是有更新,都会进行标记,在下个 update cycle 进行重绘。通常不须要显式调用此方法。
这一步骤没有相似于 layoutIfNeeded()
这样的方法来当即刷新。一般等到下一个 update cycle 再刷新也没影响。
布局过程并非单向的,而是一个 约束-布局 的迭代过程。布局过程有可能会影响约束,从而触发 updateConstraints()
。只要肯定好布局,判断是否须要重绘,而后展现。这一轮完毕后进入下一个 runloop。它们的大致流程以下:
上面说的这三个过程的方法,有些相似,记起来比较乱,能够经过下面的表格对比记忆:
方法做用 | 约束 | 布局 | 渲染 | |
---|---|---|---|---|
刷新方法,能够重载,不可直接调用 | updateConstraints | layoutSubviews | draw | |
标记刷新方法,使视图在下一个 update cycle 调用刷新方法 | setNeedsUpdateConstraints invalidateIntrinsicContentSize |
setNeedsLayout | setNeedsDisplay | |
updateConstraintsIfNeeded | layoutIfNeeded | |||
触发刷新方法的操做 | 激活/禁用约束 改变约束的大小或者优先级 改变视图层级 |
修改视图大小 添加视图 (addSubview) UIScrollView 滚动 设备旋转 更新视图约束 |
修改视图 bounds |
校招找工做时,常常被问到 VC 的生命周期。最近面试其余人,也常常问这个问题。不管是校招时候的我,仍是我面试的其余人,哪怕是工做三五年的,都回答很差这个问题。
这是一个基础问题,没有太多技术难度,应该掌握。
以方法调用顺序描述单个 View Controller 生命周期,依次为:
load
类加载时调用,在 main 函数以前。
initialize
类第一次初始化时调用,在main 函数以后。
类初始化相关方法[initWithCoder:]
在使用 storeboard 调用。[initWithNibName: bundle:]
在使用自定义 nib 文件时调用。还有其余 init 方法则是普通初始化类时调用。
loadView
开始加载视图,在这以前都没有视图。除非手动调用,不然在 View Controller 生命周期只会调用一次。在
viewDidLoad
View Controller 生命周期中只会调用一次。类中成员变量、子视图等一些数据的初始化都放在这个方法里。
viewWillAppear
视图将要展现前调用。
viewWillLayoutSubviews
将要对子视图进行布局。
viewDidLayoutSubviews
已完成子视图布局,第一时间拿到 view 的具体 frame。一些依赖布局或者大小的代码都应该放在这个方法。放在以前的方法中,视图尚未布局,frame 都是 0;放在后面的方法中,可能由于一些改动,布局或者位置变量发生改变。
viewDidAppear
视图显示完成调用。
viewWillDisappear
视图即将消失时调用。
viewDidDisappear
视图已经消失时调用。
dealloc
View Controller 被释放时调用。
不一样的转场方式,两个 VC 之间方法调用顺序不一样。常见的有如下几种方式:
Navigation
push 操做
New viewDidAppear
Pop 操做(上一步的 New 在这里变为 Current,下同)
Current viewWillDisappear
Page Curling (UIPageViewControllerTransitionStylePageCurl)
Normal 正常翻页操做
New viewDidAppear
Canceled 翻到一半取消
New viewWillAppear
Page Scrolling (UIPageViewControllerTransitionStyleScroll)
Normal 正常滑动翻页操做
Current viewDidDisappear
Canceled 滑到一半取消
New viewWillAppear
能够看出,不一样的专场方式,两个 View Cotroller 之间的生命周期方法调用顺序是不同的。很混乱是吧,不用强记,只须要知道这个 case,在开发是注意就行了。
以上基本就是一个工程从编译到启动的全部过程。深刻理解这一过程,能够帮助咱们更好的开发。由于文章比较长,中间不免有一些纰漏。若是发现请指出,我会尽快修改。