目錄
- 一求摇、查看APP啟動(dòng)耗時(shí)
- 二炫贤、虛擬內(nèi)存和物理內(nèi)存
- 三瑟曲、二進(jìn)制重排原理
- 四综膀、實(shí)現(xiàn)二進(jìn)制重排
- 五歉秫、Clang插樁
- 六、其他問題
一温治、查看APP啟動(dòng)耗時(shí)
main
函數(shù)之前的處理為pre-mian
階段饭庞,這篇文章主要分析這個(gè)階段。
添加DYLD_PRINT_STATISTICS
參數(shù)打印出pre-mian
階段的耗時(shí)情況:
各時(shí)段處理耗時(shí)分析:
-
Total pre-main time
: 總耗時(shí) -
dylib loading time
: 動(dòng)態(tài)庫(kù)載入耗時(shí) -
rebase/binding time
:rebase
表示地址偏移修正(ASLR
)熬荆,binding
表示符號(hào)綁定 -
ObjC setup time
: OC類注冊(cè)耗時(shí) -
initializer time
: 執(zhí)行load
和構(gòu)造函數(shù)
的耗時(shí)
slowest intializers
: -
libSystem.B.dylib
: 系統(tǒng)的 -
libMainThreadChecker.dylib
: -
XXXXX
: 項(xiàng)目主程序耗時(shí)
pre-main優(yōu)化方向:
- 官方建議非系統(tǒng)動(dòng)態(tài)庫(kù)的加載個(gè)數(shù)不超過6個(gè)舟山,多于6個(gè)就要考慮動(dòng)態(tài)庫(kù)的合并;
- 減少
OC
類,減少C++
虛函數(shù)- 減少
load
方法和構(gòu)造函數(shù)
main方法之后優(yōu)化方向:
- 延遲初始化卤恳、懶加載
- 刪除不使用類捏顺、方法、圖片資源
- 盡量不用
XIB
和Storyboard
纬黎,特別是首屏界面
參考:
iOS 腳本查看項(xiàng)目中未使用的類幅骄、iOS 腳本查看項(xiàng)目未使用到的方法、iOS 腳本查找項(xiàng)目中無(wú)用資源腳本原理
二本今、虛擬內(nèi)存和物理內(nèi)存
1拆座、虛擬內(nèi)存和物理內(nèi)存的區(qū)別
當(dāng)我們向系統(tǒng)申請(qǐng)內(nèi)存時(shí),系統(tǒng)并不會(huì)給你返回物理內(nèi)存的地址冠息,而是給你一個(gè)虛擬內(nèi)存地址挪凑。CPU
讀取數(shù)據(jù)時(shí)也是通過內(nèi)存管理單元MMU
將虛擬地址映射到物理內(nèi)存地址。每個(gè)進(jìn)程都擁有相同大小的虛擬地址空間逛艰,對(duì)于32位
的進(jìn)程躏碳,可以擁有4GB
的虛擬內(nèi)存,64位
進(jìn)程則更多散怖,可達(dá)18EB
菇绵。只有我們開始使用申請(qǐng)到的虛擬內(nèi)存時(shí),系統(tǒng)才會(huì)將虛擬地址映射到物理地址上镇眷,從而讓程序使用真實(shí)的物理內(nèi)存咬最。
2、內(nèi)存分頁(yè)
系統(tǒng)會(huì)對(duì)虛擬內(nèi)存和物理內(nèi)存進(jìn)行分頁(yè)欠动,虛擬內(nèi)存到物理內(nèi)存的映射都是以頁(yè)為最小粒度的永乌。在OSX
和早期的iOS
系統(tǒng)中,物理和虛擬內(nèi)存都按照4KB
的大小進(jìn)行分頁(yè)具伍。iOS
近期的系統(tǒng)中翅雏,基于A7
和A8
處理器的系統(tǒng),物理內(nèi)存按照4KB
分頁(yè)人芽,虛擬內(nèi)存按照16KB
分頁(yè)望几。基于A9
處理器的系統(tǒng)啼肩,物理和虛擬內(nèi)存都是以16KB
進(jìn)行分頁(yè)橄妆。(終端輸入PAGESIZE
可以查看到macOS的分頁(yè)大小)衙伶。
系統(tǒng)將內(nèi)存頁(yè)分為三種狀態(tài)祈坠。
- 活躍內(nèi)存頁(yè)
(active pages)
- 這種內(nèi)存頁(yè)已經(jīng)被映射到物理內(nèi)存中害碾,而且近期被訪問過,處于活躍狀態(tài)赦拘。 - 非活躍內(nèi)存頁(yè)
(inactive pages)
- 這種內(nèi)存頁(yè)已經(jīng)被映射到物理內(nèi)存中慌随,但是近期沒有被訪問過。 - 可用的內(nèi)存頁(yè)
(free pages)
- 沒有關(guān)聯(lián)到虛擬內(nèi)存頁(yè)的物理內(nèi)存頁(yè)集合躺同。
當(dāng)可用的內(nèi)存頁(yè)降低到一定的閥值時(shí)阁猜,系統(tǒng)就會(huì)采取低內(nèi)存應(yīng)對(duì)措施,在OSX
中蹋艺,系統(tǒng)會(huì)將非活躍內(nèi)存頁(yè)交換到硬盤上剃袍,而在iOS
中,則會(huì)觸發(fā)Memory Warning
捎谨,如果你的App
沒有處理低內(nèi)存警告并且還在后臺(tái)占用太多內(nèi)存民效,則有可能被殺掉。
3涛救、如何解決內(nèi)存浪費(fèi)的畏邢?
應(yīng)用程序加載到內(nèi)存中時(shí),并不會(huì)全部加載到物理內(nèi)存中检吆,屬于懶加載舒萎,用哪一部分就加載那一部分。當(dāng)訪問進(jìn)程的內(nèi)存地址時(shí)蹭沛,首先看頁(yè)表臂寝,查看所要訪問的對(duì)應(yīng)頁(yè)表是否已經(jīng)加載到內(nèi)存中。如果這一頁(yè)沒有在物理內(nèi)存中時(shí)摊灭,操作系統(tǒng)會(huì)阻塞當(dāng)前進(jìn)程交煞,發(fā)出一個(gè)缺頁(yè)異常/缺頁(yè)中斷(pagefault)
,讓后將磁盤中對(duì)應(yīng)頁(yè)的數(shù)據(jù)加載到內(nèi)存中斟或,完成虛擬內(nèi)存和物理內(nèi)存的映射素征。
當(dāng)前進(jìn)程的頁(yè)表數(shù)據(jù)加載到物理內(nèi)存中時(shí),不一定是連續(xù)的萝挤,也有可能會(huì)覆蓋其他進(jìn)程的不活躍頁(yè)御毅,這樣的按需分配,極大提高內(nèi)存的使用效率怜珍。
4端蛆、虛擬內(nèi)存的安全問題
虛擬內(nèi)存通過頁(yè)表映射到物理內(nèi)存上,因此直接訪問物理地址并不能實(shí)際正確的拿到進(jìn)程的數(shù)據(jù)酥泛,但是進(jìn)程的虛擬內(nèi)存地址相對(duì)于自己來(lái)說也是絕對(duì)的今豆,不管程序運(yùn)行多少次嫌拣,如果訪問同一個(gè)函數(shù),它在虛擬內(nèi)存中的地址都是一樣的這樣也存在安全問題(比如直接靜態(tài)注入)呆躲。
這樣也出現(xiàn)了新的技術(shù)--ASLR(Address Space Layout Randomization)
异逐。
每次虛擬內(nèi)存在加載之前,都加一個(gè)隨機(jī)偏移值插掂。
三灰瞻、二進(jìn)制重排原理
1、什么是二進(jìn)制重排
缺頁(yè)中斷/缺頁(yè)異常:內(nèi)存分頁(yè)管理辅甥,每一頁(yè)加載的時(shí)候都會(huì)發(fā)生酝润。
在iOS中,在加載缺頁(yè)內(nèi)存的時(shí)候璃弄,不僅發(fā)生缺頁(yè)阻塞從磁盤中加載數(shù)據(jù)要销,還要對(duì)加載的這頁(yè)做簽名驗(yàn)證。
在App
使用中不會(huì)發(fā)生大量的pagefault
夏块,我們一般感受不到這個(gè)過程疏咐。但是在啟動(dòng)時(shí),程序有大量的代碼需要加載拨扶、執(zhí)行凳鬓,那么這個(gè)缺頁(yè)中斷有可能就很明顯了。
如何優(yōu)化患民?
假如我的App
只有10頁(yè)
數(shù)據(jù)缩举,但是啟動(dòng)的時(shí)候需要加載的代碼分散放在1、3匹颤、5
頁(yè)仅孩。因?yàn)榇a在Mach-o
文件中的位置是根據(jù)文件加載生成的順序來(lái)決定。那么這時(shí)候App
啟動(dòng)需要運(yùn)行的代碼放在3
個(gè)虛擬內(nèi)存頁(yè)中就會(huì)出現(xiàn)3
次pagefault
印蓖。
如果我們將需要啟動(dòng)用的代碼全部放在第1
頁(yè)中辽慕,那么App
啟動(dòng)時(shí)便只會(huì)觸發(fā)一次pagefault
,App
啟動(dòng)加載的數(shù)據(jù)也會(huì)變少赦肃,這樣極大減少進(jìn)程的阻塞溅蛉。這就是二進(jìn)制重排的原理。
2他宛、查看pagefault
Xcode
提供相關(guān)的調(diào)試工具船侧,打開Instruments-System Trace
,選中手機(jī)中的App
厅各,點(diǎn)擊System Trace
左上角開始記錄后會(huì)自動(dòng)打開手機(jī)中的App
镜撩,進(jìn)入首屏后點(diǎn)擊System Trace
左上角停止。查看Main Thread
中虛擬內(nèi)存的File Backed Page In
項(xiàng)目队塘,它代表著啟動(dòng)時(shí)產(chǎn)生的pagefault
次數(shù)袁梗。
查看
pagefault
次數(shù)時(shí)受App
冷啟動(dòng)熱啟動(dòng)影響很大宜鸯,可以先開啟幾個(gè)其他App
然后等一段時(shí)間再點(diǎn)擊System Trace
左上角開啟記錄。
二進(jìn)制重排的優(yōu)化是發(fā)生在編譯鏈接階段遮怜,對(duì)即將生成的二進(jìn)制可執(zhí)行文件進(jìn)行重排淋袖。
Xcode
使用的連接器叫ld
它可以指向一個(gè)order_file
文件,在這個(gè)文件中指定排列符號(hào)奈泪,那么Xcode
在編譯時(shí)會(huì)按照指定的排列編譯出可執(zhí)行的文件适贸,蘋果objc
源碼項(xiàng)目中的libobjc.order
文件就是實(shí)現(xiàn)二進(jìn)制重排功能的灸芳。
四涝桅、實(shí)現(xiàn)二進(jìn)制重排
1、查看方法排列順序
新建測(cè)試項(xiàng)目Test_TracingPCs
在項(xiàng)目的build settings
中搜索link map
開啟這個(gè)文件的輸出
重新編譯后就可以在工程的build
目錄里面找到一份link map
文件
路徑如下:
Xcode -> DerivedData-> 項(xiàng)目名-> Build-> Intermediates.noindex-> 項(xiàng)目名.build-> Debug-iphoneos-> 項(xiàng)目名.build-> 項(xiàng)目名-LinkMap-normal-arm64.txt
這個(gè)文件里面就記錄一些鏈接.o
的文件烙样、Mach-o
文件里的一些信息冯遂、符號(hào)信息symbols
等等…
注意,這個(gè)symbols
就是關(guān)注的要點(diǎn):默認(rèn)情況下它是按照Build Phases-Compile Sources
中編譯文件從上至下排序以及類中方法從上至下排序谒获。
2蛤肌、通過order
文件重新排列加載順序:
在項(xiàng)目根目錄創(chuàng)建lcj.order
文件,在工程配置中添加.order
文件的路徑./lcj.order
后批狱,讓編譯器按照指定的順序重新排列二進(jìn)制文件裸准,把最需要加載的代碼段放在內(nèi)存頁(yè)靠前的位置。
這里只是演示了讓viewcontroller
中的幾個(gè)自定義方法優(yōu)先靠排列在內(nèi)存分頁(yè)中,實(shí)際中一個(gè)App
啟動(dòng)時(shí)的page fault
可能多達(dá)幾千次赔硫,那么需要重排的函數(shù)遠(yuǎn)不止這一點(diǎn)炒俱。
五、Clang插樁
1爪膊、引入Clang插樁
由于項(xiàng)目中存在大量的函數(shù)方法調(diào)用权悟,此外還有Block、Swift推盛、C峦阁、C++函數(shù)
,因此僅僅HOOK msgSend
方法不可行耘成。因?yàn)?code>Clang會(huì)讀取所有代碼榔昔,分析AST
中所有節(jié)點(diǎn),所有通過Clang插樁可以實(shí)現(xiàn)100%的符號(hào)覆蓋瘪菌。
抖音研發(fā)實(shí)踐:基于二進(jìn)制文件重排的解決方案
官方插樁工具-Tracing PCs
Clang Documentation
Tracing PCs
2撒会、使用Tracing PCs
根據(jù)官方文檔添加-fsanitize-coverage=trace-pc-guard
標(biāo)記
ViewController.m
中添加兩個(gè)官方文檔中的方法實(shí)現(xiàn):
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
char PcDescr[1024];
//__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
-[ViewController viewDidLoad]
前添加斷點(diǎn),運(yùn)行項(xiàng)目控嗜,到斷點(diǎn)后打開匯編斷點(diǎn)(菜單欄Debug->Debug Workflow->Always Show Disassembly)
結(jié)合匯編中插入的__sanitizer_cov_trace_pc_guard
代碼和控制臺(tái)打印的信息分析可知:添加-fsanitize-coverage=trace-pc-guard
標(biāo)記后Clang
會(huì)在中間代碼IR
中的每個(gè)方法茧彤、Block等調(diào)用邊緣插入__sanitizer_cov_trace_pc_guard
方法的調(diào)用。
所以Clang
插樁插入的就是__sanitizer_cov_trace_pc_guard
方法調(diào)用疆栏。
3曾掂、修改__sanitizer_cov_trace_pc_guard方法惫谤,獲取函數(shù)的調(diào)用方法名
#import <dlfcn.h>
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//排除load方法
//if (!*guard) return;
//當(dāng)前函數(shù)返回到上一個(gè)方法繼續(xù)執(zhí)行的地址
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("fname:%s \nfbase:%p \nsname:%s \nsaddr:%p\n\n\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
char PcDescr[1024];
//__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
}
點(diǎn)擊屏幕輸出:
fname:/private/var/containers/Bundle/Application/BAE470B2.../Test_TracingPCs.app/Test_TracingPCs
fbase:0x10236c000
sname:-[ViewController touchesBegan:withEvent:]
saddr:0x102371ad4
4、將獲取到的符號(hào)寫入到. order
文件中
#import <libkern/OSAtomic.h>//用于定義原子隊(duì)列
//定義原子隊(duì)列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號(hào)結(jié)構(gòu)體
typedef struct{
void *pc;
void *next;
} SYNode;
+ (void)load {
}
- (void)viewDidLoad {
[super viewDidLoad];
testCFunc();
[self testOCFunc];
}
- (void)testOCFunc {
NSLog(@"OC函數(shù)");
}
void testCFunc() {
CJBlock();
}
void(^CJBlock)(void) = ^(void) {
NSLog(@"Block");
};
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//排除load方法
//if (!*guard) return;
//當(dāng)前函數(shù)返回到上一個(gè)方法繼續(xù)執(zhí)行的地址
void *PC = __builtin_return_address(0);
//創(chuàng)建結(jié)構(gòu)體!
SYNode * node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//該方法在子線程中調(diào)用珠洗,因此需要使用線程安全的Atomic原子隊(duì)列
//加入結(jié)構(gòu)
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//定義數(shù)組
NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
while (YES) {//一次循環(huán)!也會(huì)被HOOK一次!!(Tracing PCs只要有跳轉(zhuǎn)(匯編中b/bl指令)就會(huì)被HOOK)
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if (node == NULL) {
break;
}
Dl_info info = {0};
dladdr(node->pc, &info);
printf("%s \n",info.dli_sname);
NSString *name = @(info.dli_sname);
free(node);
//C函數(shù)前需加 _
BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
if (![symbolNames containsObject:symbolName]) {
[symbolNames addObject:symbolName];
}
}
//反向數(shù)組
symbolNames = (NSMutableArray<NSString *>*)[[symbolNames reverseObjectEnumerator] allObjects];
//去掉當(dāng)前方法
[symbolNames removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//數(shù)組轉(zhuǎn)成字符串
NSString *funcStr = [symbolNames componentsJoinedByString:@"\n"];
//字符串寫入文件
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lcj.order"];
//文件內(nèi)容
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
由于Tracing PCs只要有跳轉(zhuǎn)(匯編中b/bl指令)就會(huì)被HOOK溜歪,因此while也會(huì)被HOOK,為了避免循環(huán)調(diào)用需要修改
Other C Flags
為:-fsanitize-coverage=func,trace-pc-guard
運(yùn)行后點(diǎn)擊屏幕拿到.order文件
六许蓖、其他問題
1蝴猪、Swift 工程 / 混編工程問題
通過上面的方法可以拿到OC
項(xiàng)目中的符號(hào),想要拿到Swift
中的符號(hào)還需要做以下配置:
-sanitize-coverage=func
-sanitize=undefined
2膊爪、cocoapod 工程問題
對(duì)于cocoapod
工程引入的庫(kù) , 由于針對(duì)不同的target
自阱。那么我們?cè)谥鞒绦蛑械?code>target添加的編譯設(shè)置Write Link Map File , -fsanitize-coverage=func,trace-pc-guard
以及order file
等設(shè)置肯定是不會(huì)生效的。解決方法就是針對(duì)需要的target
去做對(duì)應(yīng)的設(shè)置即可(配置target
自己的Order File
)米酬。
對(duì)于直接手動(dòng)導(dǎo)入到工程里的SDK
, 不管是靜態(tài)庫(kù).a
還是動(dòng)態(tài)庫(kù)
沛豌, 默認(rèn)在主工程設(shè)置就可以可以拿到符號(hào)的。
手動(dòng)導(dǎo)入的三方庫(kù)如果沒有導(dǎo)入使用的話 , 是不會(huì)加載的赃额,添加了
load
方法也是如此加派。
參考
iOS 優(yōu)化篇 - 啟動(dòng)優(yōu)化之Clang插樁實(shí)現(xiàn)二進(jìn)制重排