目錄
一、查看APP啟動耗時
main
函數之前的處理為pre-mian
階段,這篇文章主要分析這個階段。
添加DYLD_PRINT_STATISTICS
參數打印出pre-mian
階段的耗時情況:
各時段處理耗時分析:
-
Total pre-main time
: 總耗時 -
dylib loading time
: 動態庫載入耗時 -
rebase/binding time
:rebase
表示地址偏移修正(ASLR
),binding
表示符號綁定 -
ObjC setup time
: OC類注冊耗時 -
initializer time
: 執行load
和構造函數
的耗時
slowest intializers
: -
libSystem.B.dylib
: 系統的 -
libMainThreadChecker.dylib
: -
XXXXX
: 項目主程序耗時
pre-main優化方向:
- 官方建議非系統動態庫的加載個數不超過6個,多于6個就要考慮動態庫的合并;
- 減少
OC
類,減少C++
虛函數- 減少
load
方法和構造函數
main方法之后優化方向:
- 延遲初始化、懶加載
- 刪除不使用類、方法、圖片資源
- 盡量不用
XIB
和Storyboard
,特別是首屏界面
參考:
iOS 腳本查看項目中未使用的類、iOS 腳本查看項目未使用到的方法、iOS 腳本查找項目中無用資源腳本原理
二、虛擬內存和物理內存
1、虛擬內存和物理內存的區別
當我們向系統申請內存時,系統并不會給你返回物理內存的地址,而是給你一個虛擬內存地址。CPU
讀取數據時也是通過內存管理單元MMU
將虛擬地址映射到物理內存地址。每個進程都擁有相同大小的虛擬地址空間,對于32位
的進程,可以擁有4GB
的虛擬內存,64位
進程則更多,可達18EB
。只有我們開始使用申請到的虛擬內存時,系統才會將虛擬地址映射到物理地址上,從而讓程序使用真實的物理內存。
2、內存分頁
系統會對虛擬內存和物理內存進行分頁,虛擬內存到物理內存的映射都是以頁為最小粒度的。在OSX
和早期的iOS
系統中,物理和虛擬內存都按照4KB
的大小進行分頁。iOS
近期的系統中,基于A7
和A8
處理器的系統,物理內存按照4KB
分頁,虛擬內存按照16KB
分頁。基于A9
處理器的系統,物理和虛擬內存都是以16KB
進行分頁。(終端輸入PAGESIZE
可以查看到macOS的分頁大小)。
系統將內存頁分為三種狀態。
- 活躍內存頁
(active pages)
- 這種內存頁已經被映射到物理內存中,而且近期被訪問過,處于活躍狀態。 - 非活躍內存頁
(inactive pages)
- 這種內存頁已經被映射到物理內存中,但是近期沒有被訪問過。 - 可用的內存頁
(free pages)
- 沒有關聯到虛擬內存頁的物理內存頁集合。
當可用的內存頁降低到一定的閥值時,系統就會采取低內存應對措施,在OSX
中,系統會將非活躍內存頁交換到硬盤上,而在iOS
中,則會觸發Memory Warning
,如果你的App
沒有處理低內存警告并且還在后臺占用太多內存,則有可能被殺掉。
3、如何解決內存浪費的?
應用程序加載到內存中時,并不會全部加載到物理內存中,屬于懶加載,用哪一部分就加載那一部分。當訪問進程的內存地址時,首先看頁表,查看所要訪問的對應頁表是否已經加載到內存中。如果這一頁沒有在物理內存中時,操作系統會阻塞當前進程,發出一個缺頁異常/缺頁中斷(pagefault)
,讓后將磁盤中對應頁的數據加載到內存中,完成虛擬內存和物理內存的映射。
當前進程的頁表數據加載到物理內存中時,不一定是連續的,也有可能會覆蓋其他進程的不活躍頁,這樣的按需分配,極大提高內存的使用效率。
4、虛擬內存的安全問題
虛擬內存通過頁表映射到物理內存上,因此直接訪問物理地址并不能實際正確的拿到進程的數據,但是進程的虛擬內存地址相對于自己來說也是絕對的,不管程序運行多少次,如果訪問同一個函數,它在虛擬內存中的地址都是一樣的這樣也存在安全問題(比如直接靜態注入)。
這樣也出現了新的技術--ASLR(Address Space Layout Randomization)
。
每次虛擬內存在加載之前,都加一個隨機偏移值。
三、二進制重排原理
1、什么是二進制重排
缺頁中斷/缺頁異常:內存分頁管理,每一頁加載的時候都會發生。
在iOS中,在加載缺頁內存的時候,不僅發生缺頁阻塞從磁盤中加載數據,還要對加載的這頁做簽名驗證。
在App
使用中不會發生大量的pagefault
,我們一般感受不到這個過程。但是在啟動時,程序有大量的代碼需要加載、執行,那么這個缺頁中斷有可能就很明顯了。
如何優化?
假如我的App
只有10頁
數據,但是啟動的時候需要加載的代碼分散放在1、3、5
頁。因為代碼在Mach-o
文件中的位置是根據文件加載生成的順序來決定。那么這時候App
啟動需要運行的代碼放在3
個虛擬內存頁中就會出現3
次pagefault
。
如果我們將需要啟動用的代碼全部放在第1
頁中,那么App
啟動時便只會觸發一次pagefault
,App
啟動加載的數據也會變少,這樣極大減少進程的阻塞。這就是二進制重排的原理。
2、查看pagefault
Xcode
提供相關的調試工具,打開Instruments-System Trace
,選中手機中的App
,點擊System Trace
左上角開始記錄后會自動打開手機中的App
,進入首屏后點擊System Trace
左上角停止。查看Main Thread
中虛擬內存的File Backed Page In
項目,它代表著啟動時產生的pagefault
次數。
查看
pagefault
次數時受App
冷啟動熱啟動影響很大,可以先開啟幾個其他App
然后等一段時間再點擊System Trace
左上角開啟記錄。
二進制重排的優化是發生在編譯鏈接階段,對即將生成的二進制可執行文件進行重排。
Xcode
使用的連接器叫ld
它可以指向一個order_file
文件,在這個文件中指定排列符號,那么Xcode
在編譯時會按照指定的排列編譯出可執行的文件,蘋果objc
源碼項目中的libobjc.order
文件就是實現二進制重排功能的。
四、實現二進制重排
1、查看方法排列順序
新建測試項目Test_TracingPCs
在項目的build settings
中搜索link map
開啟這個文件的輸出
重新編譯后就可以在工程的build
目錄里面找到一份link map
文件
路徑如下:
Xcode -> DerivedData-> 項目名-> Build-> Intermediates.noindex-> 項目名.build-> Debug-iphoneos-> 項目名.build-> 項目名-LinkMap-normal-arm64.txt
這個文件里面就記錄一些鏈接.o
的文件、Mach-o
文件里的一些信息、符號信息symbols
等等…
注意,這個symbols
就是關注的要點:默認情況下它是按照Build Phases-Compile Sources
中編譯文件從上至下排序以及類中方法從上至下排序。
2、通過order
文件重新排列加載順序:
在項目根目錄創建lcj.order
文件,在工程配置中添加.order
文件的路徑./lcj.order
后,讓編譯器按照指定的順序重新排列二進制文件,把最需要加載的代碼段放在內存頁靠前的位置。
這里只是演示了讓viewcontroller
中的幾個自定義方法優先靠排列在內存分頁中,實際中一個App
啟動時的page fault
可能多達幾千次,那么需要重排的函數遠不止這一點。
五、Clang插樁
1、引入Clang插樁
由于項目中存在大量的函數方法調用,此外還有Block、Swift、C、C++函數
,因此僅僅HOOK msgSend
方法不可行。因為Clang
會讀取所有代碼,分析AST
中所有節點,所有通過Clang插樁可以實現100%的符號覆蓋。
官方插樁工具-Tracing PCs
Clang Documentation
Tracing PCs
2、使用Tracing PCs
根據官方文檔添加-fsanitize-coverage=trace-pc-guard
標記
ViewController.m
中添加兩個官方文檔中的方法實現:
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]
前添加斷點,運行項目,到斷點后打開匯編斷點(菜單欄Debug->Debug Workflow->Always Show Disassembly)
結合匯編中插入的__sanitizer_cov_trace_pc_guard
代碼和控制臺打印的信息分析可知:添加-fsanitize-coverage=trace-pc-guard
標記后Clang
會在中間代碼IR
中的每個方法、Block等調用邊緣插入__sanitizer_cov_trace_pc_guard
方法的調用。
所以Clang
插樁插入的就是__sanitizer_cov_trace_pc_guard
方法調用。
3、修改__sanitizer_cov_trace_pc_guard方法,獲取函數的調用方法名
#import <dlfcn.h>
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//排除load方法
//if (!*guard) return;
//當前函數返回到上一個方法繼續執行的地址
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 {
}
點擊屏幕輸出:
fname:/private/var/containers/Bundle/Application/BAE470B2.../Test_TracingPCs.app/Test_TracingPCs
fbase:0x10236c000
sname:-[ViewController touchesBegan:withEvent:]
saddr:0x102371ad4
4、將獲取到的符號寫入到. order
文件中
#import <libkern/OSAtomic.h>//用于定義原子隊列
//定義原子隊列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號結構體
typedef struct{
void *pc;
void *next;
} SYNode;
+ (void)load {
}
- (void)viewDidLoad {
[super viewDidLoad];
testCFunc();
[self testOCFunc];
}
- (void)testOCFunc {
NSLog(@"OC函數");
}
void testCFunc() {
CJBlock();
}
void(^CJBlock)(void) = ^(void) {
NSLog(@"Block");
};
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//排除load方法
//if (!*guard) return;
//當前函數返回到上一個方法繼續執行的地址
void *PC = __builtin_return_address(0);
//創建結構體!
SYNode * node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//該方法在子線程中調用,因此需要使用線程安全的Atomic原子隊列
//加入結構
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//定義數組
NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
while (YES) {//一次循環!也會被HOOK一次!!(Tracing PCs只要有跳轉(匯編中b/bl指令)就會被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函數前需加 _
BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
if (![symbolNames containsObject:symbolName]) {
[symbolNames addObject:symbolName];
}
}
//反向數組
symbolNames = (NSMutableArray<NSString *>*)[[symbolNames reverseObjectEnumerator] allObjects];
//去掉當前方法
[symbolNames removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//數組轉成字符串
NSString *funcStr = [symbolNames componentsJoinedByString:@"\n"];
//字符串寫入文件
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lcj.order"];
//文件內容
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
由于Tracing PCs只要有跳轉(匯編中b/bl指令)就會被HOOK,因此while也會被HOOK,為了避免循環調用需要修改
Other C Flags
為:-fsanitize-coverage=func,trace-pc-guard
運行后點擊屏幕拿到.order文件
六、其他問題
1、Swift 工程 / 混編工程問題
通過上面的方法可以拿到OC
項目中的符號,想要拿到Swift
中的符號還需要做以下配置:
-sanitize-coverage=func
-sanitize=undefined
2、cocoapod 工程問題
對于cocoapod
工程引入的庫 , 由于針對不同的target
。那么我們在主程序中的target
添加的編譯設置Write Link Map File , -fsanitize-coverage=func,trace-pc-guard
以及order file
等設置肯定是不會生效的。解決方法就是針對需要的target
去做對應的設置即可(配置target
自己的Order File
)。
對于直接手動導入到工程里的SDK
, 不管是靜態庫.a
還是動態庫
, 默認在主工程設置就可以可以拿到符號的。
手動導入的三方庫如果沒有導入使用的話 , 是不會加載的,添加了
load
方法也是如此。