關鍵時刻,第一時間送達!
問題種類
時間復雜度
在集合里數據量小的情況下時間復雜度對于性能的影響看起來微乎其微。但如果某個開發的功能是一個公共功能,無法預料調用者傳入數據的量時,這個復雜度的優化顯得非常重要了。
上圖列出了各種情況的時間復雜度,比如高效的排序算法一般都是 O(n log n)。接下來看看下圖:
圖中可以看出 O(n) 是個分水嶺,大于它對于性能就具有很大的潛在影響,如果是個公共的接口一定要加上說明,自己調用也要做到心中有數。當然最好是通過算法優化或者使用合適的系統接口方法,權衡內存消耗爭取通過空間來換取時間。
下面通過集合里是否有某個值來舉個例子:
//O(1)
return array[idx] == value;
//O(n)
for (int i = 0; i < count; i++) {
if (array[i] == value) {
return YES;
}
}
return NO;
//O(n2) 找重復的值
for (int i = 0; i < count; i++) {
for (int j = 0; j < count; j++) {
if ( i != j && array[i] == array[j]) {
return YES;
}
}
}
return NO;
那么 OC 里幾種常用集合對象提供的接口方法時間復雜度是怎么樣的。
NSArray / NSMutableArray
首先我們發現他們是有排序,并允許重復元素存在的,那么這么設計就表明了集合存儲沒法使用里面的元素做 hash table 的 key 進行相關的快速操作,。所以不同功能接口方法性能是會有很大的差異。
containsObject:,containsObject:,indexOfObject*,removeObject: 會遍歷里面元素查看是否與之匹對,所以復雜度等于或大于 O(n)
objectAtIndex:,firstObject:,lastObject:,addObject:,removeLastObject: 這些只針對棧頂棧底操作的時間復雜度都是 O(1)
indexOfObject:inSortedRange:options:usingComparator: 使用的是二分查找,時間復雜度是 O(log n)
NSSet / NSMutableSet / NSCountedSet
這些集合類型是無序沒有重復元素。這樣就可以通過 hash table 進行快速的操作。比如 addObject:, removeObject:, containsObject: 都是按照 O(1) 來的。需要注意的是將數組轉成 Set 時會將重復元素合成一個,同時失去排序。
NSDictionary / NSMutableDictionary
和 Set 差不多,多了鍵值對應。添加刪除和查找都是 O(1) 的。需要注意的是 Keys 必須是符合 NSCopying。
用 GCD 來做優化
我們可以通過 GCD 提供的方法來將一些需要耗時操作放到非主線程上做,使得 App 能夠運行的更加流暢響應更快。但是使用 GCD 時需要注意避免可能引起線程爆炸和死鎖的情況,還有非主線程處理任務也不是萬能的,如果一個處理需要消耗大量內存或者大量CPU操作 GCD 也沒法幫你,只能通過將處理進行拆解分步驟分時間進行處理才比較妥當。
異步處理事件
上圖是最典型的異步處理事件的方法
需要耗時長的任務
將 GCD 的 block 通過 dispatch_block_create_with_qos_class 方法指定隊列的 QoS 為 QOS_CLASS_UTILITY。這種 QoS 系統會針對大的計算,I/O,網絡以及復雜數據處理做電量優化。
避免線程爆炸
使用串行隊列
使用 NSOperationQueues 的并發限制方法 NSOperationQueue.maxConcurrentOperationCount
舉個例子,下面的寫法就比較危險,可能會造成線程爆炸和死鎖
for (int i = 0; i < 999; i++) {
dispatch_async(q, ^{...});
}
dispatch_barrier_sync(q, ^{});
那么怎么能夠避免呢?首先可以使用 dispatch_apply
dispatch_apply(999, q, ^(size_t i){...});
或者使用 dispatch_semaphore
#define CONCURRENT_TASKS 4sema = dispatch_semaphore_create(CONCURRENT_TASKS);for (int i = 0; i < 999; i++){ ? ?dispatch_async(q, ^{ ? ? ? ?dispatch_semaphore_signal(sema); ? ?}); ? ?dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);}
GCD 相關 Crash 日志
管理線程問題
Thread 1:: Dispatch queue: com.apple.libdispatch-manager
0 ? libsystem_kernel.dylib ? 0x00007fff8967e08a kevent_qos + 10
1 ? libdispatch.dylib ? ? ? ?0x00007fff8be05811 _dispatch_mgr_invoke + 251
2 ? libdispatch.dylib ? ? ? ?0x00007fff8be05465 _dispatch_mgr_thread + 52
線程閑置時
Thread 6:
0 ? libsystem_kernel.dylib ? ? ? 0x00007fff8967d772 __workq_kernreturn + 10
1 ? libsystem_pthread.dylib ? ? ?0x00007fff8fd317d9 _pthread_wqthread + 1283
2 ? libsystem_pthread.dylib ? ? ?0x00007fff8fd2ed95 start_wqthread + 13
線程活躍時
Thread 3 Crashed:: Dispatch queue:
7 ? libdispatch.dylib ? ? ? ?0x07fff8fcfd323 _dispatch_call_block_and_release
8 ? libdispatch.dylib ? ? ? ?0x07fff8fcf8c13 _dispatch_client_callout + 8
9 ? libdispatch.dylib ? ? ? ?0x07fff8fcfc365 _dispatch_queue_drain + 1100
10 ?libdispatch.dylib ? ? ? ?0x07fff8fcfdecc _dispatch_queue_invoke + 202
11 ?libdispatch.dylib ? ? ? ?0x07fff8fcfb6b7 _dispatch_root_queue_drain + 463
12 ?libdispatch.dylib ? ? ? ?0x07fff8fd09fe4 _dispatch_worker_thread3 + 91
13 ?libsystem_pthread.dylib ?0x07fff93c17637 _pthread_wqthread + 729
14 ?libsystem_pthread.dylib ?0x07fff93c1540d start_wqthread + 13
主線程閑置時
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 ? libsystem_kernel.dylib ? ? 0x00007fff906614de mach_msg_trap + 10
1 ? libsystem_kernel.dylib ? ? 0x00007fff9066064f mach_msg + 55
2 ? com.apple.CoreFoundation ? 0x00007fff9a8c1eb4 __CFRunLoopServiceMachPort
3 ? com.apple.CoreFoundation ? 0x00007fff9a8c137b __CFRunLoopRun + 1371
4 ? com.apple.CoreFoundation ? 0x00007fff9a8c0bd8 CFRunLoopRunSpecific + 296
...
10 ?com.apple.AppKit ? ? ? ? ? 0x00007fff8e823c03 -[NSApplication run] + 594
11 ?com.apple.AppKit ? ? ? ? ? 0x00007fff8e7a0354 NSApplicationMain + 1832
12 ?com.example ? ? ? ? ? ? ? ?0x00000001000013b4 start + 52
主隊列
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
12 ?com.apple.Foundation ? ? ?0x00007fff931157e8 __NSBLOCKOPERATION_IS_CALLING
_OUT_TO_A_BLOCK__ + 7
13 ?com.apple.Foundation ? ? ?0x00007fff931155b5 -[NSBlockOperation main] + 9
14 ?com.apple.Foundation ? ? ?0x00007fff93114a6c -[__NSOperationInternal _
start:] + 653
15 ?com.apple.Foundation ? ? ?0x00007fff93114543 __NSOQSchedule_f + 184
16 ?libdispatch.dylib ? ? ? ? 0x00007fff935d6c13 _dispatch_client_callout + 8
17 ?libdispatch.dylib ? ? ? ? 0x00007fff935e2cbf _dispatch_main_queue_callback
_4CF + 861
18 ?com.apple.CoreFoundation ?0x00007fff8d9223f9 __CFRUNLOOP_IS_SERVICING_THE
_MAIN_DISPATCH_QUEUE__
19 ?com.apple.CoreFoundation ?0x00007fff8d8dd68f __CFRunLoopRun + 2159
20 ?com.apple.CoreFoundation ?0x00007fff8d8dcbd8 CFRunLoopRunSpecific + 296
...
26 ?com.apple.AppKit ? ? ? ? ?0x00007fff999a1bd3 -[NSApplication run] + 594
27 ?com.apple.AppKit ? ? ? ? ?0x00007fff9991e324 NSApplicationMain + 1832
28 ?libdyld.dylib ? ? ? ? ? ? 0x00007fff9480f5c9 start + 1
I/O 性能優化
I/O 是性能消耗大戶,任何的 I/O 操作都會使低功耗狀態被打破,所以減少 I/O 次數是這個性能優化的關鍵點,為了達成這個目下面列出一些方法。
將零碎的內容作為一個整體進行寫入
使用合適的 I/O 操作 API
使用合適的線程
使用 NSCache 做緩存能夠減少 I/O
控制 App 的 Wake 次數
通知,VoIP,定位,藍牙等都會使設備從 Standby 狀態喚起。喚起這個過程會有比較大的消耗,應該避免頻繁發生。通知方面主要要在產品層面多做考慮。定位方面,下面可以看看定位的一些 API 看看它們對性能的不同影響,便于考慮采用合適的接口。
連續的位置更新
[locationManager startUpdatingLocation]
這個方法會時設備一直處于活躍狀態。
延時有效定位
[locationManager allowDeferredLocationUpdatesUntilTraveled: timeout:]
```
高效節能的定位方式,數據會緩存在位置硬件上。適合于跑步應用應該都采用這種方式。
重大位置變化
```objc
[locationManager startMonitoringSignificantLocationChanges]
會更節能,對于那些只有在位置有很大變化的才需要回調的應用可以采用這種,比如天氣應用。
區域監測
[locationManager startMonitoringForRegion:(CLRegion *)]
也是一種節能的定位方式,比如在博物館里按照不同區域監測展示不同信息之類的應用比較適合這種定位。
經常訪問的地方
// Start monitoringlocationManager.startMonitoringVisits()// Stop
monitoring when no longer neededlocationManager.stopMonitoringVisits()
總的來說,不要輕易使用 startUpdatingLocation() 除非萬不得已,盡快的使用 stopUpdatingLocation() 來結束定位還用戶一個節能設備。
內存對于性能的影響
首先 Reclaiming 內存是需要時間的,突然的大量內存需求是會影響響應的。
如何預防這些性能問題,需要刻意預防么
堅持下面幾個原則爭取在編碼階段避免一些性能問題。
優化計算的復雜度從而減少 CPU 的使用
在應用響應交互的時候停止沒必要的任務處理
設置合適的 QoS
將定時器任務合并,讓 CPU 更多時候處于 idle 狀態
那么如果寫需求時來不及注意這些問題做不到預防的話,可以通過自動化代碼檢查的方式來避免這些問題嗎?
如何檢查
根據這些問題在代碼里查,寫工具或用工具自動化查?雖然可以,但是需要考慮的情況太多,現有工具支持不好,自己寫需要考慮的點太多需要花費太長的時間,那么什么方式會比較好呢?
通過監聽主線程方式來監察
首先用 CFRunLoopObserverCreate 創建一個觀察者里面接受 CFRunLoopActivity 的回調,然后用 CFRunLoopAddObserver 將觀察者添加到 CFRunLoopGetMain() 主線程 Runloop 的 kCFRunLoopCommonModes 模式下進行觀察。
接下來創建一個子線程來進行監控,使用 dispatch_semaphore_wait 定義區間時間,標準是 16 或 20 微秒一次監控的話基本可以把影響響應的都找出來。監控結果的標準是根據兩個 Runloop 的狀態 BeforeSources 和 AfterWaiting 在區間時間是否能檢測到來判斷是否卡頓。
如何打印堆棧信息,保存現場
打印堆棧整體思路是獲取線程的信息得到線程的 state 從而得到線程里所有棧的指針,根據這些指針在 符號表里找到對應的描述即符號化解析,這樣就能夠展示出可讀的堆棧信息。具體實現是怎樣的呢?下面詳細說說:
獲取線程的信息
這里首先是要通過 task_threads 取到所有的線程,
thread_act_array_t threads; //int 組成的數組比如 thread[1] = 5635
mach_msg_type_number_t thread_count = 0; //mach_msg_type_number_t 是 int 類型
const task_t this_task = mach_task_self(); //int
//根據當前 task 獲取所有線程
kern_return_t kr = task_threads(this_task, &threads, &thread_count);
遍歷時通過 thread_info 獲取各個線程的詳細信息
SMThreadInfoStruct threadInfoSt = {0};
thread_info_data_t threadInfo;
thread_basic_info_t threadBasicInfo;
mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
if (thread_info((thread_act_t)thread, THREAD_BASIC_INFO, (thread_info_t)thread
Info, &threadInfoCount) == KERN_SUCCESS) {
threadBasicInfo = (thread_basic_info_t)threadInfo;
if (!(threadBasicInfo->flags & TH_FLAGS_IDLE)) {
threadInfoSt.cpuUsage = threadBasicInfo->cpu_usage / 10;
threadInfoSt.userTime = threadBasicInfo->system_time.microseconds;
}
}
uintptr_t buffer[100];
int i = 0;
NSMutableString *reStr = [NSMutableString stringWithFormat:@"Stack of thread:
%u:CPU used: %.1f percent
user time: %d second
", thread, threadInfoSt.
cpuUsage, threadInfoSt.userTime];
獲取線程里所有棧的信息
可以通過 thread_get_state 得到 machine context 里面包含了線程棧里所有的棧指針。
_STRUCT_MCONTEXT machineContext; //線程棧里所有的棧指針
//通過 thread_get_state 獲取完整的 machineContext 信息,包含 thread 狀態信息
mach_msg_type_number_t state_count = smThreadStateCountByCPU();
kern_return_t kr = thread_get_state(thread, smThreadStateByCPU(), (thread_state
_t)&machineContext.__ss, &state_count);
創建一個棧結構體用來保存棧的數據
//為通用回溯設計結構支持棧地址由小到大,地址里存儲上個棧指針的地址
typedef struct SMStackFrame {
const struct SMStackFrame *const previous;
const uintptr_t return_address;
} SMStackFrame;
SMStackFrame stackFrame = {0};
//通過棧基址指針獲取當前棧幀地址
const uintptr_t framePointer = smMachStackBasePointerByCPU(&machineContext);
if (framePointer == 0 || smMemCopySafely((void *)framePointer, &stackFrame,
sizeof(stackFrame)) != KERN_SUCCESS) {
return @"Fail frame pointer";
}
for (; i < 32; i++) {
buffer[i] = stackFrame.return_address;
if (buffer[i] == 0 || stackFrame.previous == 0 || smMemCopySafely(stackFrame
.previous, &stackFrame, sizeof(stackFrame)) != KERN_SUCCESS) {
break;
}
}
符號化
符號化主要思想就是通過棧指針地址減去 Slide 地址得到 ASLR 偏移量,通過這個偏移量可以在 __LINKEDIT segment 查找到字符串和符號表的位置。具體代碼實現如下:
info->dli_fname = NULL;
info->dli_fbase = NULL;
info->dli_sname = NULL;
info->dli_saddr = NULL;
//根據地址獲取是哪個 image
const uint32_t idx = smDyldImageIndexFromAddress(address);
if (idx == UINT_MAX) {
return false;
}
/*
Header
------------------
Load commands
Segment command 1 -------------|
Segment command 2 ? ? ? ? ? ? ?|
------------------ ? ? ? ? ? ? |
Data ? ? ? ? ? ? ? ? ? ? ? ? ? |
Section 1 data |segment 1 <----|
Section 2 data | ? ? ? ? ?<----|
Section 3 data | ? ? ? ? ?<----|
Section 4 data |segment 2
Section 5 data |
... ? ? ? ? ? ?|
Section n data |
*/
/*----------Mach Header---------*/
//根據 image 的序號獲取 mach_header
const struct mach_header* machHeader = _dyld_get_image_header(idx);
//返回 image_index 索引的 image 的虛擬內存地址 slide 的數量,如果 image_index
超出范圍返回0
//動態鏈接器加載 image 時,image 必須映射到未占用地址的進程的虛擬地址空間。動態鏈接器
通過添加一個值到 image 的基地址來實現,這個值是虛擬內存 slide 數量
const uintptr_t imageVMAddressSlide = (uintptr_t)_dyld_get_image_vmaddr
_slide(idx);
/*-----------ASLR 的偏移量---------*/
//https://en.wikipedia.org/wiki/Address_space_layout_randomization
const uintptr_t addressWithSlide = address - imageVMAddressSlide;
//根據 Image 的 Index 來獲取 segment 的基地址
//段定義Mach-O文件中的字節范圍以及動態鏈接器加載應用程序時這些字節映射到虛擬內存中的地址
和內存保護屬性。 因此,段總是虛擬內存頁對齊。 片段包含零個或多個節。
const uintptr_t segmentBase = smSegmentBaseOfImageIndex(idx)
+ imageVMAddressSlide;
if (segmentBase == 0) {
return false;
}
//
info->dli_fname = _dyld_get_image_name(idx);
info->dli_fbase = (void*)machHeader;
/*--------------Mach Segment-------------*/
//地址最匹配的symbol
const nlistByCPU* bestMatch = NULL;
uintptr_t bestDistance = ULONG_MAX;
uintptr_t cmdPointer = smCmdFirstPointerFromMachHeader(machHeader);
if (cmdPointer == 0) {
return false;
}
//遍歷每個 segment 判斷目標地址是否落在該 segment 包含的范圍里
for (uint32_t iCmd = 0; iCmd < machHeader->ncmds; iCmd++) {
const struct load_command* loadCmd = (struct load_command*)cmdPointer;
/*----------目標 Image 的符號表----------*/
//Segment 除了 __TEXT 和 __DATA 外還有 __LINKEDIT segment,它里面包含動態鏈接器的
使用的原始數據,比如符號,字符串和重定位表項。
//LC_SYMTAB 描述了 __LINKEDIT segment 內查找字符串和符號表的位置
if (loadCmd->cmd == LC_SYMTAB) {
//獲取字符串和符號表的虛擬內存偏移量。
const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPointer;
const nlistByCPU* symbolTable = (nlistByCPU*)(segmentBase + symtabCmd->symoff);
const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
for (uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
//如果 n_value 是0,symbol 指向外部對象
if (symbolTable[iSym].n_value != 0) {
//給定的偏移量是文件偏移量,減去 __LINKEDIT segment 的文件偏移量獲得字符串和符號表的虛
擬內存偏移量
uintptr_t symbolBase = symbolTable[iSym].n_value;
uintptr_t currentDistance = addressWithSlide - symbolBase;
//尋找最小的距離 bestDistance,因為 addressWithSlide 是某個方法的指令地址,要大于這個
方法的入口。
//離 addressWithSlide 越近的函數入口越匹配
if ((addressWithSlide >= symbolBase) && (currentDistance <= bestDistance)) {
bestMatch = symbolTable + iSym;
bestDistance = currentDistance;
}
}
}
if (bestMatch != NULL) {
//將虛擬內存偏移量添加到 __LINKEDIT segment 的虛擬內存地址可以提供字符串和符號表的內存
address。
info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddressSlide);
info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un
.n_strx);
if (*info->dli_sname == '_') {
info->dli_sname++;
}
//所有的 symbols 的已經被處理好了
if (info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
info->dli_sname = NULL;
}
break;
}
}
cmdPointer += loadCmd->cmdsize;
}
需要注意的地方
需要注意的是這個程序有消耗性能的地方 thread get state。這個也會被監控檢查出,所以可以過濾掉這樣的堆棧信息。
能夠獲取更多信息的方法
獲取更多信息比如全層級方法調用和每個方法消耗的時間,那么這樣做的好處在哪呢?
可以更細化的測量時間消耗,找到耗時方法,更快的交互操作能使用戶體驗更好,下面是一些可以去衡量的場景:
響應能力
按鈕點擊
手勢操作
Tab 切換
vc 的切換和轉場
可以給優化定個目標,比如滾動和動畫達到 60fps,響應用戶操作在 100ms 內完成。然后逐個檢測出來 fix 掉。
如何獲取到更多信息呢?
通過 hook objc_msgSend 方法能夠獲取所有被調用的方法,記錄深度就能夠得到方法調用的樹狀結構,通過執行前后時間的記錄能夠得到每個方法的耗時,這樣就能獲取一份完整的性能消耗信息了。
hook c 函數可以使用 facebook 的fishhook, 獲取方法調用樹狀結構可以使用InspectiveC,下面對于他們的實現詳細介紹一下:
獲取方法調用樹結構
首先設計兩個結構體,CallRecord 記錄調用方法詳細信息,包括 obj 和 SEL 等,ThreadCallStack 里面需要用 index 記錄當前調用方法樹的深度。有了 SEL 再通過 NSStringFromSelector 就能夠取得方法名,有了 obj 通過 object_getClass 能夠得到 Class 再用 NSStringFromClass 就能夠獲得類名。
// Shared structures.
typedef struct CallRecord_ {
id obj; ? //通過 object_getClass 能夠得到 Class 再通過 NSStringFromClass 能夠得
到類名
SEL _cmd; //通過 NSStringFromSelector 方法能夠得到方法名
uintptr_t lr;
int prevHitIndex;
char isWatchHit;
} CallRecord;
typedef struct ThreadCallStack_ {
FILE *file;
char *spacesStr;
CallRecord *stack;
int allocatedLength;
int index;
int numWatchHits;
int lastPrintedIndex;
int lastHitIndex;
char isLoggingEnabled;
char isCompleteLoggingEnabled;
} ThreadCallStack;
存儲讀取 ThreadCallStack
pthread_setspecific() 可以將私有數據設置在指定線程上,pthread_getspecific() 用來讀取這個私有數據,利用這個特性可以就可以將 ThreadCallStack 的數據和該線程綁定在一起,隨時進行數據的存取。代碼如下:
static inline ThreadCallStack * getThreadCallStack() {
ThreadCallStack *cs = (ThreadCallStack *)pthread_getspecific(threadKey);
//讀取
if (cs == NULL) {
cs = (ThreadCallStack *)malloc(sizeof(ThreadCallStack));
#ifdef MAIN_THREAD_ONLY
cs->file = (pthread_main_np()) ? newFileForThread() : NULL;
#else
cs->file = newFileForThread();
#endif
cs->isLoggingEnabled = (cs->file != NULL);
cs->isCompleteLoggingEnabled = 0;
cs->spacesStr = (char *)malloc(DEFAULT_CALLSTACK_DEPTH + 1);
memset(cs->spacesStr, ' ', DEFAULT_CALLSTACK_DEPTH);
cs->spacesStr[DEFAULT_CALLSTACK_DEPTH] = '';
cs->stack = (CallRecord *)calloc(DEFAULT_CALLSTACK_DEPTH, sizeof(CallRecord));
//分配 CallRecord 默認空間
cs->allocatedLength = DEFAULT_CALLSTACK_DEPTH;
cs->index = cs->lastPrintedIndex = cs->lastHitIndex = -1;
cs->numWatchHits = 0;
pthread_setspecific(threadKey, cs); //保存數據
}
returncs;
}
記錄方法調用深度
因為要記錄深度,而一個方法的調用里會有更多的方法調用,所以方法的調用寫兩個方法分別記錄開始 pushCallRecord 和記錄結束的時刻 popCallRecord,這樣才能夠通過在開始時對深度加一在結束時減一。
//開始時
static inline void pushCallRecord(id obj, uintptr_t lr, SEL _cmd,
ThreadCallStack *cs) {
int nextIndex = (++cs->index); //增加深度
if (nextIndex >= cs->allocatedLength) {
cs->allocatedLength += CALLSTACK_DEPTH_INCREMENT;
cs->stack = (CallRecord *)realloc(cs->stack, cs->allocatedLength *
sizeof(CallRecord));
cs->spacesStr = (char *)realloc(cs->spacesStr, cs->allocatedLength + 1);
memset(cs->spacesStr, ' ', cs->allocatedLength);
cs->spacesStr[cs->allocatedLength] = '';
}
CallRecord *newRecord = &cs->stack[nextIndex];
newRecord->obj = obj;
newRecord->_cmd = _cmd;
newRecord->lr = lr;
newRecord->isWatchHit = 0;
}
//結束時
static inline CallRecord * popCallRecord(ThreadCallStack *cs) {
return &cs->stack[cs->index--]; //減少深度
}
在 objc_msgSend 前后插入執行方法
最后是 hook objc_msgSend 需要在調用前和調用后分別加入 pushCallRecord 和 popCallRecord。因為需要在調用后這個時機插入一個方法,這就需要用到匯編來做到。下面針對 arm64 進行分析,主要思路就是先入棧參數,參數寄存器是 x0 - x7,syscall 的 number 會放到 x8 里。然后交換寄存器中,將用于返回的寄存器 lr 移到 x1 里。先讓 pushCallRecord 能夠執行,再執行原始的 objc_msgSend,保存返回值,最后讓 popCallRecord 能執行。具體代碼如下:
static void replacementObjc_msgSend() {
__asm__ volatile (
// 保存 {q0-q7}
"stp q6, q7, [sp, #-32]!
"
"stp q4, q5, [sp, #-32]!
"
"stp q2, q3, [sp, #-32]!
"
"stp q0, q1, [sp, #-32]!
"
// 保存 {x0-x8, lr}
"stp x8, lr, [sp, #-16]!
"
"stp x6, x7, [sp, #-16]!
"
"stp x4, x5, [sp, #-16]!
"
"stp x2, x3, [sp, #-16]!
"
"stp x0, x1, [sp, #-16]!
"
// 交換參數.
"mov x2, x1
"
"mov x1, lr
"
"mov x3, sp
"
// 調用 preObjc_msgSend
"bl __Z15preObjc_msgSendP11objc_objectmP13objc_selectorP9RegState_
"
"mov x9, x0
"
"mov x10, x1
"
"tst x10, x10
"
// 讀取 {x0-x8, lr}
"ldp x0, x1, [sp], #16
"
"ldp x2, x3, [sp], #16
"
"ldp x4, x5, [sp], #16
"
"ldp x6, x7, [sp], #16
"
"ldp x8, lr, [sp], #16
"
// 讀取 {q0-q7}
"ldp q0, q1, [sp], #32
"
"ldp q2, q3, [sp], #32
"
"ldp q4, q5, [sp], #32
"
"ldp q6, q7, [sp], #32
"
"b.eq Lpassthrough
"
// 調用原始 objc_msgSend.
"blr x9
"
// 保存 {x0-x9}
"stp x0, x1, [sp, #-16]!
"
"stp x2, x3, [sp, #-16]!
"
"stp x4, x5, [sp, #-16]!
"
"stp x6, x7, [sp, #-16]!
"
"stp x8, x9, [sp, #-16]!
"
// 保存 {q0-q7}
"stp q0, q1, [sp, #-32]!
"
"stp q2, q3, [sp, #-32]!
"
"stp q4, q5, [sp, #-32]!
"
"stp q6, q7, [sp, #-32]!
"
// 調用 postObjc_msgSend hook.
"bl __Z16postObjc_msgSendv
"
"mov lr, x0
"
// 讀取 {q0-q7}
"ldp q6, q7, [sp], #32
"
"ldp q4, q5, [sp], #32
"
"ldp q2, q3, [sp], #32
"
"ldp q0, q1, [sp], #32
"
// 讀取 {x0-x9}
"ldp x8, x9, [sp], #16
"
"ldp x6, x7, [sp], #16
"
"ldp x4, x5, [sp], #16
"
"ldp x2, x3, [sp], #16
"
"ldp x0, x1, [sp], #16
"
"ret
"
"Lpassthrough:
"
"br x9"
);
}
記錄時間的方法
為了記錄耗時,這樣就需要在 pushCallRecord 和 popCallRecord 里記錄下時間。下面列出一些計算一段代碼開始到結束的時間的方法
第一種: NSDate 微秒
NSDate* tmpStartData = [NSDate date];
//some code need caculate
double deltaTime = [[NSDate date] timeIntervalSinceDate:tmpStartData];
NSLog(@"cost time: %f s", deltaTime);
第二種:clock_t 微秒clock_t計時所表示的是占用CPU的時鐘單元
clock_t start = clock();
//some code need caculate
clock_t end = clock();
NSLog(@"cost time: %f s", (double)(end - start)/CLOCKS_PER_SEC);
第三種:CFAbsoluteTime 微秒
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
//some code need caculate
CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
NSLog(@"cost time = %f s", end - start); //s
第四種:CFTimeInterval 納秒
CFTimeInterval start = CACurrentMediaTime();
//some code need caculate
CFTimeInterval end = CACurrentMediaTime();
NSLog(@"cost time: %f s", end - start);
```第五種:mach_absolute_time 納秒
```objc
uint64_t start = mach_absolute_time ();
//some code need caculate
uint64_t end = mach_absolute_time ();
uint64_t elapsed = 1e-9 *(end - start);
最后兩種可用,本質區別
NSDate 或 CFAbsoluteTimeGetCurrent() 返回的時鐘時間將會會網絡時間同步,從時鐘 偏移量的角度。mach_absolute_time() 和 CACurrentMediaTime() 是基于內建時鐘的。選擇一種,加到 pushCallRecord 和 popCallRecord 里,相減就能夠獲得耗時。
如何 hook msgsend 方法
那么 objc_msgSend 這個 c 方法是如何 hook 到的呢。首先了解下 dyld 是通過更新 Mach-O 二進制的 __DATA segment 特定的部分中的指針來邦定 lazy 和 non-lazy 符號,通過確認傳遞給 rebind_symbol 里每個符號名稱更新的位置就可以找出對應替換來重新綁定這些符號。下面針對關鍵代碼進行分析:
遍歷 dyld
首先是遍歷 dyld 里的所有的 image,取出 image header 和 slide。注意第一次調用時主要注冊 callback。
if (!_rebindings_head->next) {
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
uint32_t c = _dyld_image_count();
for (uint32_t i = 0; i < c; i++) {
_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr
_slide(i));
}
}
找出符號表相關 Command
接下來需要找到符號表相關的 command,包括 linkedit segment command,symtab command 和 dysymtab command。方法如下:
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
struct symtab_command* symtab_cmd = NULL;
struct dysymtab_command* dysymtab_cmd = NULL;
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
獲得 base 和 indirect 符號表
// Find base symbol/string table addresses
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr
- linkedit_segment->fileoff;
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd
->indirectsymoff);
進行方法替換
有了符號表和傳入的方法替換數組就可以進行符號表訪問指針地址的替換,具體實現如下:
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
for (uint i = 0; i < section->size / sizeof(void *); i++) {
uint32_t symtab_index = indirect_symbol_indices[i];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL
_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL ? | INDIRECT_SYMBOL_ABS)) {
continue;
}
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
char *symbol_name = strtab + strtab_offset;
if (strnlen(symbol_name, 2) < 2) {
continue;
}
struct rebindings_entry *cur = rebindings;
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
Demo
工具已整合到先前做的DecoupleDemo里。使用的話在需要開始檢測的地方添加 [[SMLagMonitor shareInstance] beginMonitor]; 即可。需要檢測所有方法調用的用法就是在需要檢測的地方調用 [SMCallTrace start]; 就可以了,不檢測打印出結果的話調用 stop 和 save 就好了。這里還可以設置最大深度和最小耗時檢測來過濾不需要看到的信息。
本文作者:?? ? ?戴銘
本文鏈接:?? ? ?http://ming1016.github.io/2017/06/20/deeply-ios-performance-optimization/
版權聲明:?? ? ?本博客所有文章除特別聲明外,均采用?CC BY-NC-SA 3.0?許可協議。轉載請注明出處!
iOS開發整理發布,轉載請聯系作者授權
微信掃一掃
關注該公眾號