瞭解了對象,類,方法等底層實現以後,咱們來看一下咱們開發的App,在代碼完成後到啓動的時候,經歷了哪幾個步驟前端
整體來講,一個APP從編寫完代碼到運行,就經歷了兩大步驟,即編譯和運行,這一章節,主要來看一下APP的進行編譯的。linux
編譯的大致步驟以下:git
iOS的代碼,是經過編譯器將代碼直接編寫成機器碼,而後直接在CPU上運行機器碼的,這樣能使得咱們的app和手機都能效率更高,運行更快。C,C++,OC等語言,都是使用的編譯器,生成相關的可執行文件
。github
與之對應的,是Python,Shell等腳本性語言,它們使用的是解釋器。解釋器會在運行時解釋執行代碼,獲取一段代碼後就會將其翻譯成目標代碼
(就是字節碼(Bytecode)),而後一句一句地執行目標代碼。也就是說是在運行時纔去解析代碼,比直接運行編譯好的可執行文件天然效率就低,可是跑起來以後能夠不用重啓啓動編譯,直接修改代碼便可看到效果,相似熱更新
,能夠幫咱們縮短整個程序的開發週期和功能更新週期。macos
總結來講:windows
目前Xcode使用的編譯器爲LLVM(官方連接)。LLVM 是編譯器工具鏈技術的一個集合。而其中的 lld 項目,就是內置連接器。編譯器會對每一個文件進行編譯,生成 Mach-O(可執行文件)
;連接器會將項目中的多個Mach-O
文件合併成一個。後端
LLVM會執行上述的整個編譯流程,大致流程以下:bash
建立一個工程,使用clang -E main.m
能夠查看預處理階段的所作的工做架構
#import <Foundation/Foundation.h>
#define DEFINEEight 8
int main(){
@autoreleasepool {
int eight = DEFINEEight;
int six = 6;
NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
int rank = eight + six;
NSLog(@"%@ rank %d", site, rank);
}
return 0;
}
複製代碼
# 10 "main.m"
# 1 "./AppDelegate.h" 1
# 11 "./AppDelegate.h"
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@end
# 11 "main.m" 2
int main(int argc, char * argv[]) {
@autoreleasepool {
int eight = 8;
int six = 6;
NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
int rank = eight + six;
NSLog(@"%@ rank %d", site, rank);
}
return 0;
}
複製代碼
預處理主要處理規則以下:app
#define
,並將全部宏定義展開,在源碼中使用的宏定義會被替換爲對應代碼(#include)
所在位置(這個過程是遞歸的)#pragma
編譯器指令,由於編譯器需要使用它們當咱們沒法判斷宏定義是否正確或者頭文件是否包含時能夠查看預編譯後的文件來肯定問題
編譯的過程就是把預處理完的文件進行一些列詞法分析、語法分析、語義分析及優化後生產相應的彙編代碼文件,這個過程每每是咱們整個程序構建的核心部分.
使用clang -Xclang -dump-tokens main.m
來進行詞法分析,獲得以下結果
at '@' [StartOfLine] Loc=<./AppDelegate.h:11:1>
identifier 'interface' Loc=<./AppDelegate.h:11:2>
identifier 'AppDelegate' [LeadingSpace] Loc=<./AppDelegate.h:11:12>
colon ':' [LeadingSpace] Loc=<./AppDelegate.h:11:24>
identifier 'UIResponder' [LeadingSpace] Loc=<./AppDelegate.h:11:26>
less '<' [LeadingSpace] Loc=<./AppDelegate.h:11:38>
identifier 'UIApplicationDelegate' Loc=<./AppDelegate.h:11:39>
greater '>' Loc=<./AppDelegate.h:11:60>
at '@' [StartOfLine] Loc=<./AppDelegate.h:14:1>
identifier 'end' Loc=<./AppDelegate.h:14:2>
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>
...
複製代碼
這一步把源文件中的代碼轉化爲特殊的標記流,源碼被分割成一個一個的字符和單詞,在行尾Loc中都標記出了源碼所在的對應源文件和具體行數,方便在報錯時定位問題
使用clang -Xclang -ast-dump -fsyntax-only main.m
命令來進行語法分析,結果以下
...
| `-PointerType 0x7f9824831b10 'char *'
| `-BuiltinType 0x7f9824830ca0 'char'
|-TypedefDecl 0x7f9825006458 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7f9825006400 'struct __va_list_tag [1]' 1
| `-RecordType 0x7f9825006280 'struct __va_list_tag'
| `-Record 0x7f9825006200 '__va_list_tag'
|-ObjCInterfaceDecl 0x7f98250064a8 <./AppDelegate.h:11:1, line:14:2> line:11:12 AppDelegate
|-FunctionDecl 0x7f98250067e0 <main.m:14:1, line:23:1> line:14:5 main 'int (int, char **)'
| |-ParmVarDecl 0x7f98250065b8 <col:10, col:14> col:14 argc 'int'
| |-ParmVarDecl 0x7f98250066d0 <col:20, col:32> col:27 argv 'char **':'char **'
| `-CompoundStmt 0x7f9825006f28 <col:35, line:23:1>
| |-ObjCAutoreleasePoolStmt 0x7f9825006ee0 <line:15:5, line:21:5>
| | `-CompoundStmt 0x7f9825006eb8 <line:15:22, line:21:5>
| | |-DeclStmt 0x7f9825006960 <line:16:9, col:32>
| | | `-VarDecl 0x7f98250068e0 <col:9, line:12:21> line:16:13 used eight 'int' cinit
| | | `-IntegerLiteral 0x7f9825006940 <line:12:21> 'int' 8
| | |-DeclStmt 0x7f9825006a10 <line:17:9, col:20>
| | | `-VarDecl 0x7f9825006990 <col:9, col:19> col:13 used six 'int' cinit
| | | `-IntegerLiteral 0x7f98250069f0 <col:19> 'int' 6
| | `-DeclStmt 0x7f9825006b30 <line:19:9, col:31>
| | `-VarDecl 0x7f9825006a40 <col:9, col:28> col:13 used rank 'int' cinit
| | `-BinaryOperator 0x7f9825006b10 <col:20, col:28> 'int' '+'
| | |-ImplicitCastExpr 0x7f9825006ae0 <col:20> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7f9825006aa0 <col:20> 'int' lvalue Var 0x7f98250068e0 'eight' 'int'
| | `-ImplicitCastExpr 0x7f9825006af8 <col:28> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7f9825006ac0 <col:28> 'int' lvalue Var 0x7f9825006990 'six' 'int'
| `-ReturnStmt 0x7f9825006f18 <line:22:5, col:12>
| `-IntegerLiteral 0x7f9825006ef8 <col:12> 'int' 0
`-FunctionDecl 0x7f9825006bd0 <line:20:9> col:9 implicit used NSLog 'void (id, ...)' extern
|-ParmVarDecl 0x7f9825006c68 <<invalid sloc>> <invalid sloc> 'id':'id'
`-FormatAttr 0x7f9825006cd0 <col:9> Implicit NSString 1
...
複製代碼
這一步是把詞法分析生成的標記流,解析成一個抽象語法樹(abstract syntax tree -- AST),一樣地,在這裏面每一節點也都標記了其在源碼中的位置。
把源碼轉化爲抽象語法樹以後,編譯器就能夠對這個樹進行分析處理。靜態分析會對代碼進行錯誤檢查,如出現方法被調用可是未定義、定義可是未使用的變量等,以此提升代碼質量。固然,還能夠經過使用 Xcode 自帶的靜態分析工具(Product -> Analyze)
通常會把類型分爲兩類:動態的和靜態的。動態的在運行時作檢查,靜態的在編譯時作檢查。以往,編寫代碼時能夠向任意對象發送任何消息,在運行時,纔會檢查對象是否可以響應這些消息。因爲只是在運行時作此類檢查,因此叫作動態類型。
至於靜態類型,是在編譯時作檢查。當在代碼中使用 ARC 時,編譯器在編譯期間,會作許多的類型檢查:由於編譯器須要知道哪一個對象該如何使用。
在此階段clang會作檢查,最多見的是檢查程序是否發送正確的消息給正確的對象,是否在正確的值上調用了正常函數。若是你給一個單純的 NSObject* 對象發送了一個 hello 消息,那麼 clang 就會報錯,一樣,給屬性設置一個與其自身類型不相符的對象,編譯器會給出一個可能使用不正確的警告。
其餘分析ObjCUnusedIVarsChecker.cpp
是用來檢查是否有定義了,可是從未使用過的變量。ObjCSelfInitChecker.cpp
是檢查在 你的初始化方法中中調用 self 以前,是否已經調用[self initWith...]
或[super init]
了。
LLVM IR
中間產物使用clang -O3 -S -emit-llvm main.m -o main.ll
命令,生成LLVM中間產物IR(生成main.ll文件),IR是編譯過程的前端的輸出後端的輸入。
; 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"
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [3 x i8] c"%d\00", section "__TEXT,__cstring,cstring_literals", align 1
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i32 0, i32 0), i64 2 }, section "__DATA,__cfstring", align 8
; Function Attrs: ssp uwtable
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
%3 = tail call i8* @objc_autoreleasePoolPush() #2
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 1)
tail call void @objc_autoreleasePoolPop(i8* %3)
ret i32 0
}
declare i8* @objc_autoreleasePoolPush() local_unnamed_addr
declare void @NSLog(i8*, ...) local_unnamed_addr #1
declare void @objc_autoreleasePoolPop(i8*) local_unnamed_addr
attributes #0 = { 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 = { "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" }
attributes #2 = { nounwind }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6}
!llvm.ident = !{!7}
!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, !"wchar_size", i32 4}
!6 = !{i32 7, !"PIC Level", i32 2}
!7 = !{!"Apple LLVM version 9.1.0 (clang-902.0.39.2)"}
複製代碼
使用clang -emit-llvm -c main.m -o main.bc
命令,會使用LLVM對代碼進行優化。
使用clang -S -fobjc-arc main.m -o main.s
會生成相對應的彙編代碼
至此,編譯階段完成,將書寫代碼轉換成了機器能夠識別的彙編代碼
彙編器是將彙編代碼轉變成機器能夠執行的指令,每個彙編語句幾乎都對應一條機器指令。因此彙編器的彙編過程相對於編譯器來說比較簡單,它沒有複雜的語法,也沒有語義,也不須要作指令優化,只是根據彙編指令和機器指令的對照表一一翻譯就能夠了。
使用clang -fmodules -c main.m -o main.o
生成對應的目標文件
使用Xcode構建的程序會在DerivedData目錄中找到這個文件,以下圖
連接主要分爲靜態連接
和動態連接
,編譯器階段的連接爲靜態連接,相關動態連接的部分,會在下一章App啓動中講解
這一階段是將上個階段生成的目標文件和引用的靜態庫連接起來,最終生成可執行文件,連接器解決了目標文件和庫之間的連接。
使用clang main.m
生成可執行文件,能夠看出可執行文件類型爲Mach-O類型,在 MAC OS 和 iOS 平臺的可執行文件都是這種類型。
至此,編譯過程所有結束,生成了可執行文件Mach-O
Mach-O 文件裏面的內容,主要就是代碼和數據:代碼是函數的定義;數據是全局變量的定義,包括全局變量的初始值。不論是代碼仍是數據,它們的實例都須要由符號將其關聯起來。
爲何呢?由於 Mach-O 文件裏的那些代碼,好比 if、for、while 生成的機器指令序列,要操做的數據會存儲在某個地方,變量符號就須要綁定到數據的存儲地址。你寫的代碼還會引用其餘的代碼,引用的函數符號也須要綁定到該函數的地址上。
連接器的做用,就是完成變量、函數符號和其地址綁定這樣的任務。而這裏咱們所說的符號,就能夠理解爲變量名和函數名。
項目中文件之間的變量和接口函數都是相互依賴的,因此這時咱們就須要經過連接器將項目中生成的多個 Mach-O 文件的符號和地址綁定起來。
沒有這個綁定過程的話,單個文件生成的 Mach-O 文件是沒法正常運行起來的。由於,若是運行時碰到調用在其餘文件中實現的函數的狀況時,就會找不到這個調用函數的地址,從而沒法繼續執行。
連接器在連接多個目標文件的過程當中,會建立一個符號表,用於記錄全部已定義的和全部未定義的符號。連接時若是出現相同符號的狀況,就會出現「ld: dumplicate symbols」
的錯誤信息;若是在其餘目標文件裏沒有找到符號,就會提示「Undefined symbols」
的錯誤信息。
連接器在整理函數的調用關係時,會以 main 函數爲源頭,跟隨每一個引用,並將其標記爲 live。跟隨完成後,那些未被標記 live 的函數,就是無用函數。而後,連接器能夠經過打開 Dead code stripping 開關,來開啓自動去除無用代碼的功能。而且,這個開關是默認開啓的。
Mach-O
分析Mach-O實際上是Mach Object文件格式的縮寫,是mac以及iOS上可執行文件的格式, 相似於windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)
Mach-O是OS X中二進制文件的原生可執行格式,是傳送代碼的首選格式。可執行格式決定了二進制文件中的代碼和數據讀入內存的順序。代碼和數據的順序會影響內存使用和分頁活動,從而直接影響程序的性能。
Mach-O二進制文件被組織成段。每一個部分包含一個或多個部分。段的大小由它所包含的全部部分的字節數來度量,並四捨五入到下一個虛擬內存頁邊界。所以,一個段老是4096字節或4千字節的倍數,其中4096字節是最小大小。
一、目標文件:.o
二、庫文件:.a .dylib Framework
三、可執行文件:dyld .dsym
MachO能夠是多架構的二進制文件,稱之爲「通用二進制文件」
主要架構有armv7,armv7s,arm64,i386,x86_64,其中iPhone中多數使用arm64
通用二進制文件是蘋果公司提出的一種程序代碼。能同時適用多種架構的二進制文件
Header 包含該二進制文件的通常信息 字節順序、架構類型、加載指令的數量等。 使得能夠快速確認一些信息,好比當前文件用於32位仍是64位,對應的處理器是什麼、文件類型是什麼
可以使用otool -v -h a.out
查看其結構,或者使用MachOView來直接查看
Load commands是一張包含不少內容的表。內容包括區域的位置、符號表、動態符號表等。這一段緊跟Header,加載Mach-O文件時會使用這裏的數據來肯定內存的分佈
LC_LOAD_DYLINKER
LC_LOAD_DYLINKER
該字段標明咱們的MachO是被誰加載進去的。通常狀況下都是dyld
,下一個章節咱們會講dyld
是如何對Mach-o
進行加載的
LC_LOAD_DYLIB
LC_LOAD_DYLIB
該字段標記了全部動態庫的地址,只有在LC_LOAD_DYLIB
中有標記,咱們MachO外部的動態庫(如:Framework)才能被dyld正確的引用,不然dyld不會主動加載
Data 一般是對象文件中最大的部分,包含Segement的具體數據,如靜態C字符串,帶參數/不帶參數的OC方法,帶參數/不帶參數的C函數。
包含 Load commands 中須要的各個 segment,每一個 segment 中又包含多個 section。當運行一個可執行文件時,虛擬內存 (virtual memory) 系統將 segment 映射到進程的地址空間上。
使用xcrun size -x -l -m a.out
查看segment內容,或者MachOView
名稱 | 含義 |
---|---|
Segment __PAGEZERO | 大小爲 4GB,規定進程地址空間的前 4GB 被映射爲不可讀不可寫不可執行 |
Segment __TEXT | 包含可執行的代碼,以只讀和可執行方式映射。 |
Segment __DATA | 包含了將會被更改的數據,以可讀寫和不可執行方式映射。 |
Segment __LINKEDIT | 包含了方法和變量的元數據,代碼簽名等信息。 |