
儘管APM 越來越火爆,大大小小的專業APM 廠商如雨後春筍般湧現出來,市面上有關APM 的技術文章也非常多,但大部分都只是淺嚐輒止,並未對實現細節進行深挖。本文旨在通過剖析SDK 具體實現細節,揭露知名APM 廠商的iOS SDK 內部工作原理。我相信讀者在閱讀本文之前,也和筆者當初一樣對APM SDK 的實現細節充滿了好奇。幸運的是,您正在讀的這篇文章會帶您一步步揭開APM 的真實脈絡,本文分析的APM SDK 有聽雲, OneAPM和Firebase Performance Monitoring等。筆者才疏學淺,若有訛誤,不客斧正,以求再版,更臻完美。
本篇文章中分析的聽雲SDK 版本是2.3.5,與最新版本會存在些許差異,不過我大致翻看了新版的代碼,差異不大,不影響分析。
頁面渲染的監控,這個需求看似很簡單,但是在實際開發的過程中還是會遇到不少問題。比較容易想到的是通過hook 頁面的幾個關鍵的生命週期方法,例如viewDidLoad 、 viewDidAppear:等,從而計算出頁面渲染時間,最終發現慢加載的頁面。然而如果真正通過上述思路去著手實現的時候,便會遇到難題。在APM SDK 中如何才能hook 應用所有頁面的生命週期的方法呢?如果嘗試hook UIViewController的方法又會如何呢? hook UIViewController的方法明顯不可行,原因是他只會作用UIViewController的方法,而應用中大部分的視圖控制器都是繼承自UIViewController的,所以這種方法不可行。但是聽雲SDK 卻能夠實現。頁面Hook 的邏輯主要是_priv_NBSUIAgent類中實現的,下面是_priv_NBSUIAgent類的定義,其中hook_viewDidLoad等幾個方法便是線索。
@class _priv_NBSUIAgent : NSObject {
+hookUIImage
+hookNSManagedObjectContext
+hookNSJSONSerialization
+hookNSData
+hookNSArray
+hookNSDictionary
+hook_viewDidLoad:
+hook_viewWillAppear:
+hook_viewDidAppear:
+hook_viewWillLayoutSubviews:
+hook_viewDidLayoutSubviews:
+nbs_jump_initialize:
+hookSubOfController
+hookFMDB
+start
}
我們先將目光轉到另外一個更可疑的方法: hookSubOfController ,具體實現如下:
void +[_priv_NBSUIAgent hookSubOfController](void * self, void * _cmd) {
r14 = self;
r12 = [_subMetaClassNamesInMainBundle_c("UIViewController") retain];
var_C0 = r12;
if ((r12 != 0x0) && ([r12 count] != 0x0)) {
var_C8 = object_getClass(r14);
if ([r12 count] != 0x0) {
r15 = @selector(nbs_jump_initialize:);
rdx = 0x0;
do {
var_98 = rdx;
r12 = [[r12 objectAtIndexedSubscript:rdx, rcx, r8] retain];
[r12 release];
if ([r12 respondsToSelector:r15, rcx, r8] == 0x0) {
_hookClass_CopyAMetaMethod();
}
r13 = class_getName(r12);
rax = [NSString stringWithFormat:@"nbs_%s_initialize", r13];
rax = [rax retain];
var_A0 = rax;
rax = NSSelectorFromString(rax);
var_B0 = rax;
rax = objc_retainBlock(__NSConcreteStackBlock);
var_A8 = rax;
r15 = objc_retainBlock(rax);
var_B8 = imp_implementationWithBlock(r15);
[r15 release];
rax = class_getSuperclass(r12);
r15 = objc_retainBlock(__NSConcreteStackBlock);
rbx = objc_retainBlock(r15);
r13 = imp_implementationWithBlock(rbx);
[rbx release];
rcx = r13;
r8 = var_B8;
_nbs_Swizzle_orReplaceWithIMPs(r12, @selector(initialize), var_B0, rcx, r8);
rdi = r15;
r15 = @selector(nbs_jump_initialize:);
[rdi release];
[var_A8 release];
[var_A0 release];
rax = [var_C0 count];
r12 = var_C0;
rdx = var_98 + 0x1;
} while (var_98 + 0x1 < rax);
}
}
[r12 release];
return;
}
從_subMetaClassNamesInMainBundle_c的命名和傳入的"UIViewController" 參數,基本可以推斷這個C 函數是獲取MainBundle 中所有UIViewController的子類。而事實上,如果通過LLDB 在這個函數Call 完之後的那行彙編代碼下斷點,會發現返回的確實是UIViewController子類的數組。下面的if語句判斷r12寄存器不為nil並且r12寄存器的count不等於0才執行if裡面的邏輯,而r12寄存器存放的正是_subMetaClassNamesInMainBundle_c函數的返回值,也就是UIViewController子類的數組。
_subMetaClassNamesInMainBundle_c函數代碼如下:
void _subMetaClassNamesInMainBundle_c(int arg0) {
rbx = objc_getClass(arg0);
rdi = 0x0;
if (rbx == 0x0) goto loc_10001dbde;
loc_10001db4d:
r15 = _classNamesInMainBundle_c(var_2C);
var_38 = [NSMutableArray new];
if (var_2C == 0x0) goto loc_10001dbd2;
loc_10001db77:
r14 = 0x0;
goto loc_10001db7a;
loc_10001db7a:
r13 = objc_getClass(*(r15 + r14 * 0x8));
r12 = r13;
if (r13 == 0x0) goto loc_10001dbc9;
loc_10001db8e:
rax = class_getSuperclass(r12);
if (rax == rbx) goto loc_10001dba5;
loc_10001db9b:
COND = rax != r12;
r12 = rax;
if (COND) goto loc_10001db8e;
loc_10001dbc9:
r14 = r14 + 0x1;
if (r14 < var_2C) goto loc_10001db7a;
loc_10001dbd2:
free(r15);
rdi = var_38;
goto loc_10001dbde;
loc_10001dbde:
[rdi autorelease];
return;
loc_10001dba5:
rax = class_getName(r13);
rax = objc_getMetaClass(rax);
[var_38 addObject:rax];
goto loc_10001dbc9;
}
_subMetaClassNamesInMainBundle_c函數中的loc_10001db4d子例程調用了_classNamesInMainBundle_c函數,該函數代碼如下:
int _classNamesInMainBundle_c(int arg0) {
rbx = [[NSBundle mainBundle] retain];
r15 = [[rbx executablePath] retain];
[rbx release];
rbx = objc_retainAutorelease(r15);
r14 = objc_copyClassNamesForImage([rbx UTF8String], arg0);
[rbx release];
rax = r14;
return rax;
}
_classNamesInMainBundle_c函數的實現顯而易見,無非就是調用objc_copyClassNamesForImage以獲取mainBundle可執行路徑的所有類的名稱,集合的數量賦給了outCount變量,調用方可以使用outCount來對其遍歷。
static inline char ** WDTClassNamesInMainBundle ( unsigned int *outCount) {
NSString *executablePath = [[ NSBundle mainBundle ] executablePath ];
char **classNames = objc_copyClassNamesForImage ([executablePath UTF8String ], outCount);
return classNames;
}如果不在乎細節,那麼_subMetaClassNamesInMainBundle_c函數的實現也很清晰,就是遍歷objc_copyClassNamesForImage函數的返回值,如果item 是UIViewController的子類,則取得該類的metaClass並添加到可變數組var_38中。
接下來再來重點看看裡面的do-while循環語句,循環判斷的語句為var_98 + 0x1 < rax , var_98在循環開始的位置賦值rdx寄存器, rdx寄存器在循環外初始化為0,所以var_98就是計數器,而rax寄存器則是賦值為r12寄存器的count方法,依此得出這個do-while循環實際就是遍歷UIViewController子類的數組。遍歷的行為則是通過_nbs_Swizzle_orReplaceWithIMPs實現initialize和nbs_jump_initialize:的方法交換。
nbs_jump_initialize的代碼如下:
void +[_priv_NBSUIAgent nbs_jump_initialize:](void * self, void * _cmd, void * arg2) {
rbx = arg2;
r15 = self;
r14 = [NSStringFromSelector(rbx) retain];
if ((r14 != 0x0) && ([r14 isEqualToString:@""] == 0x0)) {
[r15 class];
rax = _nbs_getClassImpOf();
(rax)(r15, @selector(initialize));
}
rax = class_getName(r15);
r13 = [[NSString stringWithUTF8String:rax] retain];
rdx = @"_Aspects_";
if ([r13 hasSuffix:rdx] == 0x0) goto loc_100050137;
loc_10005011e:
if (*(int8_t *)_is_tiaoshi_kai == 0x0) goto loc_100050218;
loc_10005012e:
rsi = cfstring__V__A;
goto loc_100050195;
loc_100050195:
__NBSDebugLog(0x3, rsi, rdx, rcx, r8, r9, stack[2048]);
goto loc_100050218;
loc_100050218:
[r13 release];
rdi = r14;
[rdi release];
return;
loc_100050137:
rdx = @"RACSelectorSignal";
if ([r13 hasSuffix:rdx] == 0x0) goto loc_10005016b;
loc_100050152:
if (*(int8_t *)_is_tiaoshi_kai == 0x0) goto loc_100050218;
loc_100050162:
rsi = cfstring__V__R;
goto loc_100050195;
loc_10005016b:
if (_classSelf_isImpOf(r15, "nbs_vc_flag") == 0x0) goto loc_1000501a3;
loc_10005017e:
if (*(int8_t *)_is_tiaoshi_kai == 0x0) goto loc_100050218;
loc_10005018e:
rsi = cfstring____Yh;
goto loc_100050195;
loc_1000501a3:
rbx = objc_retainBlock(void ^(void * _block, void * arg1) {
return;
});
rax = imp_implementationWithBlock(rbx);
class_addMethod(r15, @selector(nbs_vc_flag), rax, "v@:");
[rbx release];
[_priv_NBSUIAgent hook_viewDidLoad:r15];
[_priv_NBSUIAgent hook_viewWillAppear:r15];
[_priv_NBSUIAgent hook_viewDidAppear:r15];
goto loc_100050218;
}
nbs_jump_initialize的代碼有點長,但是從loc_1000501a3的例程可以觀察到主要邏輯會執行hook_viewDidLoad 、 hook_viewWillAppear和hook_viewDidAppear三個方法,從而hook 住UIViewController子類的這三個方法。
先以hook_viewDidLoad:方法為例講解,下面這段代碼可能有點晦澀難懂,需要認真分析
void +[_priv_NBSUIAgent hook_viewDidLoad:](void * self, void * _cmd, void * arg2) {
rax = [_priv_NBSUIHookMatrix class];
var_D8 = _nbs_getInstanceImpOf();
var_D0 = _nbs_getInstanceImpOf();
rbx = class_getName(arg2);
r14 = class_getSuperclass(arg2);
rax = [NSString stringWithFormat:@"nbs_%s_viewDidLoad", rbx];
rax = [rax retain];
var_B8 = rax;
var_C0 = NSSelectorFromString(rax);
r12 = objc_retainBlock(__NSConcreteStackBlock);
var_D0 = imp_implementationWithBlock(r12);
[r12 release];
rbx = objc_retainBlock(__NSConcreteStackBlock);
r14 = imp_implementationWithBlock(rbx);
[rbx release];
_nbs_Swizzle_orReplaceWithIMPs(arg2, @selector(viewDidLoad), var_C0, r14, var_D0);
[var_B8 release];
return;
}
hook_viewDidLoad:方法中的參數arg2即是要hook 的ViewController的類,獲取arg2的類名並賦給rbx寄存器,然後利用rbx構造字符串nbs_%s_viewDidLoad ,如nbs_XXViewController_viewDidLoad ,獲得該字符串的selector 後賦給var_C0 ,下面幾句中的__NSConcreteStackBlock是創建的存儲棧的block 對象,這個block 之後會通過imp_implementationWithBlock方法獲取到IMP 函數指針, _nbs_Swizzle_orReplaceWithIMPs是實現方法交換的函數,參數依次為: arg2是ViewController的類; @selector(viewDidLoad)是viewDidLoad的selector; var_C0是nbs_%s_viewDidLoad的selector, r14是第二個__NSConcreteStackBlock的IMP; var_D0是第一個__NSConcreteStackBlock的IMP。
hook_viewDidLoad:的整個邏輯大致清楚了,不過這裡有個問題為什麼不直接交換兩個IMP,而是要先構造兩個block,然後交換兩個block 的IMP呢?原因是需要將ViewController的父類也就是class_getSuperclass的結果作為參數傳遞給交換後的方法,這樣交換的兩個selector 簽名的參數個數不一致,需要通過構造block 去巧妙的解決這個問題,而事實上第一個__NSConcreteStackBlock的執行的就是_priv_NBSUIHookMatrix的nbs_jump_viewDidLoad:superClass:方法,正如之前所說的,這個方法的參數中有superClass ,至於為什麼需要這個參數,稍後再做介紹。
為什麼第二個__NSConcreteStackBlock的執行的是nbs_jump_viewDidLoad:superClass:方法呢?取消勾選Hopper 的Remove potentially dead code選項,代碼如下:
void +[_priv_NBSUIAgent hook_viewDidLoad:](void * self, void * _cmd, void * arg2) {
rsi = _cmd;
rdi = self;
r12 = _objc_msgSend;
rax = [_priv_NBSUIHookMatrix class];
rsi = @selector(nbs_jump_viewDidLoad:superClass:);
rdi = rax;
var_D8 = _nbs_getInstanceImpOf();
rdi = arg2;
rsi = @selector(viewDidLoad);
var_D0 = _nbs_getInstanceImpOf();
rbx = class_getName(arg2);
r14 = class_getSuperclass(arg2);
LODWORD(rax) = 0x0;
rax = [NSString stringWithFormat:@"nbs_%s_viewDidLoad", rbx];
rax = [rax retain];
var_B8 = rax;
var_C0 = NSSelectorFromString(rax);
var_60 = 0xc0000000;
var_5C = 0x0;
var_58 = ___37+[_priv_NBSUIAgent hook_viewDidLoad:]_block_invoke;
var_50 = ___block_descriptor_tmp;
var_48 = var_D8;
var_40 = @selector(viewDidLoad);
var_38 = var_D0;
var_30 = r14;
r12 = objc_retainBlock(__NSConcreteStackBlock);
var_D0 = imp_implementationWithBlock(r12);
r13 = _objc_release;
rax = [r12 release];
var_A8 = 0xc0000000;
var_A4 = 0x0;
var_A0 = ___37+[_priv_NBSUIAgent hook_viewDidLoad:]_block_invoke_2;
var_98 = ___block_descriptor_tmp47;
var_90 = rbx;
var_88 = var_D8;
var_80 = @selector(viewDidLoad);
var_78 = r14;
var_70 = arg2;
rbx = objc_retainBlock(__NSConcreteStackBlock);
r14 = imp_implementationWithBlock(rbx);
rax = [rbx release];
rax = _nbs_Swizzle_orReplaceWithIMPs(arg2, @selector(viewDidLoad), var_C0, r14, var_D0);
rax = [var_B8 release];
rsp = rsp + 0xb8;
rbx = stack[2047];
r12 = stack[2046];
r13 = stack[2045];
r14 = stack[2044];
r15 = stack[2043];
rbp = stack[2042];
return;
}
再來看_nbs_getInstanceImpOf的代碼:
void _nbs_getInstanceImpOf() {
rax = class_getInstanceMethod(rdi, rsi);
method_getImplementation(rax);
return;
}
_nbs_getInstanceImpOf函數的作用很明顯,獲取rdi類中rsi selector 的IMP,讀者會發現在hook_viewDidLoad:方法中共調用了兩次_nbs_getInstanceImpOf ,第一次rdi是_priv_NBSUIHookMatrix類, rdx是@selector(nbs_jump_viewDidLoad:superClass:) ,第二次rdi是ViewController類, rdx是@selector(viewDidLoad) 。
接下來看第一個__NSConcreteStackBlock ,也就是會調用nbs_jump_viewDidLoad:superClass:的block,代碼如下:
int ___37+[_priv_NBSUIAgent hook_viewDidLoad:]_block_invoke(int arg0, int arg1) {
r8 = *(arg0 + 0x20);
rax = *(arg0 + 0x28);
rdx = *(arg0 + 0x30);
rcx = *(arg0 + 0x38);
rax = (r8)(arg1, rax, rdx, rcx, r8);
return rax;
}
r8寄存器是nbs_jump_viewDidLoad:superClass:的IMP,這段代碼只是調用這個IMP。 IMP 函數的參數與nbs_jump_viewDidLoad:superClass:相同。
void -[_priv_NBSUIHookMatrix nbs_jump_viewDidLoad:superClass:](void * self, void * _cmd, void * * arg2, void * arg3) {
rbx = arg3;
var_70 = arg2;
var_68 = _cmd;
r14 = self;
rax = [self class];
rax = class_getSuperclass(rax);
if ((rbx != 0x0) && (rax != rbx)) {
rax = var_70;
if (rax != 0x0) {
rdi = r14;
(rax)(rdi, @selector(viewDidLoad));
}
else {
NSLog(@"");
[[r14 super] viewDidLoad];
}
}
else {
var_B8 = rbx;
objc_storeWeak(_currentViewController, 0x0);
r14 = 0x0;
[[NSString stringWithFormat:@"%d#loading", 0x0] retain];
r12 = 0x0;
if (0x0 != 0x0) {
rcx = class_getName([r12 class]);
r14 = [[NSString stringWithFormat:@"MobileView/Controller/%s#%@", rcx, @"loading"] retain];
}
var_A0 = r14;
r14 = [[_priv_NBSUILogCenter_assistant alloc] initWithControllerName:r14];
var_80 = r14;
var_60 = _objc_release;
[r14 setTheVC:_objc_release];
[r14 setVC_Address:_objc_release];
[r14 setIsOther:0x0];
[*_controllerStack push:r14];
rbx = [_glb_all_activing_VCS() retain];
var_98 = _objc_msgSend;
[rbx setObject:r14 forKey:_objc_msgSend];
[rbx release];
r12 = [[NSDate date] retain];
[r12 timeIntervalSince1970];
xmm0 = intrinsic_mulsd(xmm0, *0x100066938);
rbx = intrinsic_cvttsd2si(rbx, xmm0);
[r12 release];
[r14 setStartTime:rbx];
rcx = class_getName([var_60 class]);
r13 = [[NSString stringWithFormat:@"%s", rcx] retain];
r14 = [NSStringFromSelector(var_68) retain];
var_88 = [_nbs_embedIn_start() retain];
[r14 release];
[r13 release];
rbx = [[NBSLensInterfaceEventLogger shareObject] retain];
var_78 = rbx;
rax = [NBSLensUITraceSegment new];
var_58 = rax;
rbx = [[rbx theStack] retain];
[rbx push:rax];
[rbx release];
rcx = class_getName([var_60 class]);
r13 = [[NSString stringWithFormat:@"%s", rcx] retain];
r12 = [NSStringFromSelector(var_68) retain];
r14 = [[NSString stringWithFormat:@"%@#%@", r13, r12] retain];
var_A8 = r14;
[r12 release];
rdi = r13;
[rdi release];
[var_58 setSegmentName:r14];
rax = [NSDictionary dictionary];
rax = [rax retain];
var_B0 = rax;
[var_58 setSegmentParam:rax];
rbx = [[NSThread currentThread] retain];
rdx = rbx;
[var_58 setThreadInfomation:rdx];
[rbx release];
rbx = [[NSDate date] retain];
[rbx timeIntervalSince1970];
xmm0 = intrinsic_mulsd(xmm0, *0x100066938);
var_68 = intrinsic_movsd(var_68, xmm0);
[rbx release];
xmm0 = intrinsic_movsd(xmm0, var_68);
[var_58 setStartTime:rdx];
[var_58 setEntryTime:0x0];
r14 = [NBSLensUITraceSegment new];
var_90 = r14;
xmm0 = intrinsic_movsd(xmm0, var_68);
[r14 setStartTime:0x0];
rcx = class_getName([var_60 class]);
r15 = [[NSString stringWithFormat:@"%s", rcx] retain];
rbx = [[NSString stringWithFormat:@"%@#viewLoading", r15] retain];
[r14 setSegmentName:rbx];
[rbx release];
[r15 release];
rcx = var_30;
rax = [NSDictionary dictionaryWithObjects:rbx forKeys:rcx count:0x0];
[r14 setSegmentParam:rax];
rbx = [[NSThread currentThread] retain];
[r14 setThreadInfomation:rbx];
[rbx release];
[r14 setEntryTime:0x0];
rax = var_70;
if (rax != 0x0) {
(rax)(var_60, @selector(viewDidLoad), 0x0, rcx, 0x0);
}
else {
NSLog(@"");
[[var_60 super] viewDidLoad];
}
_nbs_embedIn_finish();
rdx = [var_88 mach_tm2];
[var_80 setFinishTime:rdx];
rbx = [[NSDate date] retain];
[rbx timeIntervalSince1970];
xmm0 = intrinsic_mulsd(xmm0, *0x100066938);
var_70 = intrinsic_movsd(var_70, xmm0);
[rbx release];
xmm0 = intrinsic_movsd(xmm0, var_70);
xmm0 = intrinsic_subsd(xmm0, var_68);
rdx = intrinsic_cvttsd2si(rdx, xmm0);
[var_58 setExitTime:rdx];
rbx = [[var_78 theStack] retain];
rax = [rbx pop];
rax = [rax retain];
[rax release];
[rbx release];
rbx = [[var_78 theStack] retain];
r15 = [rbx isEmpty];
[rbx release];
if (r15 == 0x0) {
rbx = [[var_78 theStack] retain];
r14 = [[rbx peer] retain];
[rbx release];
[r14 startTime];
xmm1 = intrinsic_movsd(xmm1, var_68);
xmm1 = intrinsic_subsd(xmm1, xmm0);
rdx = intrinsic_cvttsd2si(rdx, xmm1);
[var_58 setEntryTime:rdx];
[r14 startTime];
rdx = intrinsic_cvttsd2si(rdx, intrinsic_subsd(intrinsic_movsd(xmm1, var_70), xmm0));
[var_58 setExitTime:rdx];
rbx = [[r14 childSegments] retain];
rdx = var_58;
[rbx addObject:rdx];
[rbx release];
[r14 release];
}
rbx = [[var_90 childSegments] retain];
[rbx addObject:var_58];
[rbx release];
objc_setAssociatedObject(var_60, @"viewLoading", var_90, 0x1);
rax = [*_controllerStack pop];
rax = [rax retain];
[rax release];
rbx = [[_priv_NBSLENS_VCSBuffer sharedObj] retain];
[rbx addObj:var_80];
[rbx release];
rbx = [_glb_all_activing_VCS() retain];
[rbx removeObjectForKey:var_98];
[rbx release];
[var_90 release];
[var_B0 release];
[var_A8 release];
[var_58 release];
[var_78 release];
[var_88 release];
[var_80 release];
[var_A0 release];
[var_98 release];
}
return;
}
啟動時間以Firebase Performance Monitoring SDK 為例進行講解,下文以FPM SDK 作為簡稱方便講述,FPM SDK 實現的是冷啟動時間的統計,主要邏輯在FPRAppActivityTracker類中實現。
首先看類的+load方法,反編譯代碼如下:
void +[FPRAppActivityTracker load](void * self, void * _cmd) {
rax = [NSDate date];
rax = [rax retain];
rdi = *_appStartTime;
*_appStartTime = rax;
[rdi release];
rbx = [[NSNotificationCenter defaultCenter] retain];
[rbx addObserver:self selector:@selector(windowDidBecomeVisible:) name:*_UIWindowDidBecomeVisibleNotification object:0x0];
rdi = rbx;
[rdi release];
return;
}
顯而易見, _appStartTime是一個static 的NSDate實例,用來保存整個應用啟動的開始時間,所以FPM SDK 是在FPRAppActivityTracker的+load標記應用啟動的開始時間。了解+load方法的讀者應該知道該方法是在main函數調用之前的鉤子方法,準確的時間是當鏡像加載到運行時、對+load方法的準備就緒之後,開始調用+load方法。此外不同類的+load方法還與Build Phases->Compile Sources的文件順序有關,我們姑且認為這些對啟動時間的統計沒有顯著的影響。
之後註冊了UIWindowDidBecomeVisibleNotification的通知,這個通知是當UIWindow對象激活時並展示在界面的時候觸發的。讀者可以註冊這個通知,然後用LLDB 打印notification 對象,示例如下:
NSConcreteNotification 0x7fc94a716f50 {name = UIWindowDidBecomeVisibleNotification; object = <UIStatusBarWindow: 0x7fc94a5092a0; frame = (0 0; 320 568); opaque = NO; gestureRecognizers = <NSArray: 0x7fc94a619f30>; layer = <UIWindowLayer: 0x7fc94a513f50>>}
第一次收到UIWindowDidBecomeVisibleNotification通知的時間早於- application:didFinishLaunchingWithOptions:回調,這個通知時狀態欄的window創建時觸發的,這個實現感覺有點取巧,不能確保未來Apple 會不會調整調用的時序。
下面是UIWindowDidBecomeVisibleNotification的官方說明。
Posted when an UIWindow object becomes visible. The notification object is the window object that has become visible. This notification does not contain a userInfo dictionary. Switching between apps does not generate visibility-related notifications for windows. Window visibility changes reflect changes to the window's hidden property and reflect only the window's visibility within the app.
下面是通知的處理方法,我將方法還原成Objective-C 的偽代碼,可以對比反編譯的偽代碼
void +[FPRAppActivityTracker windowDidBecomeVisible:](void * self, void * _cmd, void * arg2) {
var_30 = self;
r13 = _objc_msgSend;
r12 = [[self sharedInstance] retain];
[r12 startAppActivityTracking];
rbx = [[FIRTrace alloc] initInternalTraceWithName:@"_as"];
[r12 setAppStartTrace:rbx];
[rbx release];
r15 = @selector(appStartTrace);
rbx = [_objc_msgSend(r12, r15) retain];
[rbx startWithStartTime:*_appStartTime];
[rbx release];
rbx = [_objc_msgSend(r12, r15) retain];
rcx = *_appStartTime;
rdx = @"_astui";
[rbx startStageNamed:rdx startTime:rcx];
[rbx release];
rax = *(int8_t *)_windowDidBecomeVisible:.FDDStageStarted;
rax = rax & 0x1;
COND = rax != 0x0;
if (!COND) {
r13 = _objc_msgSend;
rcx = *_appStartTime;
rbx = [_objc_msgSend(r12, r15, rdx, rcx) retain];
rdx = @"_astfd";
[rbx startStageNamed:rdx, rcx];
[rbx release];
*(int8_t *)_windowDidBecomeVisible:.FDDStageStarted = 0x1;
}
rbx = [(r13)(@class(NSNotificationCenter), @selector(defaultCenter), rdx, *_UIWindowDidBecomeVisibleNotification) retain];
(r13)(rbx, @selector(removeObserver:name:object:), var_30, *_UIWindowDidBecomeVisibleNotification, 0x0);
[rbx release];
rdi = r12;
[rdi release];
return;
}
+ (void)windowDidBecomeVisible:(NSNotification *)notification {
FPRAppActivityTracker *tracker = [self sharedInstance];
[tracker startAppActivityTracking];
FIRTrace *trace = [[FIRTrace alloc] initInternalTraceWithName:@"_as"];
[tracker setAppStartTrace: trace];
[[tracker appStartTrace] startWithStartTime:_appStartTime];
[[tracker appStartTrace] startStageNamed:@"_astui" startTime:_appStartTime];
if (_windowDidBecomeVisible:.FDDStageStarted) {
[[tracker appStartTrace] startStageNamed:@"_astfd" startTime:_appStartTime];
_windowDidBecomeVisible:.FDDStageStarted = 1;
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIWindowDidBecomeVisibleNotification object:nil];
}
方法的最後會註銷UIWindowDidBecomeVisibleNotification通知,因為該通知會調用多次,而我們只需要他執行一次。首先調用-startAppActivityTracking方法開始追踪APP 的活動,這個方法稍後會深入討論。
首先明確一點,我們這裡討論的網絡請求沒有特殊說明指的都是HTTP 請求。聽雲SDK 實現網絡監控主要使用了兩種方式:第一種是通過hook iOS 網絡編程使用的API,這種方式主要針對的是原生的網絡請求;第二種是繼承NSURLProtocol來實現網絡請求的攔截,這種方式主要針對的是UIWebView 中的網絡請求。
SDK hook 了所有網絡請求中構造NSURLSessionDataTask , NSURLSessionUploadTask和NSURLSessionDownloadTask的API。 hook 的邏輯在C 函數_nbs_hook_NSURLSession中,偽代碼如下:
int _nbs_hook_NSURLSession() {
_nbs_hook_NSURLSessionTask();
r13 = [[_priv_NSURLSession_NBS class] retain];
r14 = [objc_getClass("NSURLSession") retain];
r15 = [objc_getMetaClass(class_getName(r13)) retain];
r12 = [objc_getMetaClass("NSURLSession") retain];
if ((((((_nbs_hookClass_CopyAMethod() != 0x0) && (_nbs_hookClass_CopyAMethod() != 0x0)) && (_nbs_hookClass_CopyAMethod() != 0x0)) && (_nbs_hookClass_CopyAMethod() != 0x0)) && (_nbs_hookClass_CopyAMethod() != 0x0)) && (_nbs_hookClass_CopyAMethod() != 0x0)) {
if (_nbs_hookClass_CopyAMethod() != 0x0) {
if (_nbs_hookClass_CopyAMethod() != 0x0) {
if (_nbs_hookClass_CopyAMethod() != 0x0) {
if (_nbs_hookClass_CopyAMethod() != 0x0) {
_nbs_Swizzle(r14, @selector(dataTaskWithRequest:completionHandler:), @selector(nbs_dataTaskWithRequest:completionHandler:));
_nbs_Swizzle(r14, @selector(downloadTaskWithRequest:completionHandler:), @selector(nbs_downloadTaskWithRequest:completionHandler:));
_nbs_Swizzle(r14, @selector(downloadTaskWithResumeData:completionHandler:), @selector(nbs_downloadTaskWithResumeData:completionHandler:));
_nbs_Swizzle(r14, @selector(uploadTaskWithRequest:fromData:completionHandler:), @selector(nbs_uploadTaskWithRequest:fromData:completionHandler:));
_nbs_Swizzle(r14, @selector(uploadTaskWithRequest:fromFile:completionHandler:), @selector(nbs_uploadTaskWithRequest:fromFile:completionHandler:));
_nbs_Swizzle(r14, @selector(downloadTaskWithRequest:), @selector(nbs_downloadTaskWithRequest:));
_nbs_Swizzle(r14, @selector(uploadTaskWithRequest:fromFile:), @selector(nbs_uploadTaskWithRequest:fromFile:));
_nbs_Swizzle(r14, @selector(uploadTaskWithRequest:fromData:), @selector(nbs_uploadTaskWithRequest:fromData:));
_nbs_Swizzle(r12, @selector(sessionWithConfiguration:delegate:delegateQueue:), @selector(nbs_sessionWithConfiguration:delegate:delegateQueue:));
_nbs_Swizzle(r14, @selector(uploadTaskWithStreamedRequest:), @selector(nbs_uploadTaskWithStreamedRequest:));
}
}
}
}
}
[r12 release];
[r15 release];
[r14 release];
rdi = r13;
rax = [rdi release];
return rax;
}
_nbs_Swizzle 就是聽雲實現Method Swizzling 的C 函數。
從代碼中可以看到除了對上面提到的NSURLSessionDataTask , NSURLSessionUploadTask和NSURLSessionDownloadTask的API 使用了_nbs_Swizzle ,還替換了sessionWithConfiguration:delegate:delegateQueue:方法的實現,稍後會講解為什麼要hook 這個方法。
所有hook 的方法實現被定義在_priv_NSURLSession_NBS類中。
nbs_dataTaskWithRequest:completionHandler:的核心代碼如下:
typedef void (^nbs_URLSessionDataTaskCompletionHandler)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error);
- (NSURLSessionDataTask *)nbs_dataTaskWithRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler {
_priv_NBSHTTPTransaction *httpTransaction = [_priv_NBSHTTPTransaction new];
nbs_URLSessionDataTaskCompletionHandler wrappedCompletionHandler;
__block NSURLSessionDataTask *dataTask;
if (completionHandler) {
wrappedCompletionHandler = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSTimeInterval timeInterval = [[NSDate date] timeIntervalSince1970];
[dataTask.httpTransaction finishAt:timeInterval];
completionHandler(data, response, error);
};
}
dataTask = [self nbs_dataTaskWithRequest:request
completionHandler:wrappedCompletionHandler];
if (dataTask) {
dataTask.httpTransaction = httpTransaction;
}
return dataTask;
}
_priv_NBSHTTPTransaction是SDK 中和HTTP 請求相關的性能參數的Model。該類結構如下:
@class _priv_NBSHTTPTransaction : NSObject {
@property isFileURL
@property tm_dur_dns
@property tm_dur_cnnct
@property tm_pnt_send
@property tm_dur_firstP
@property tm_dur_end
@property tm_dur_ssl
@property sendSize
@property receiveSize
@property headerSize
@property dataSize
@property statusCode
@property errCode
@property contentLength
@property errText
@property url
@property ip
@property contentType
@property anyObj
@property useContentLength
@property netType
@property appData
@property request
@property response
@property responseData
@property urlParams
@property dataBody
@property httpMethodNumber
@property libClassId
@property socketItem
@property threadId
@property cdn_associate
@property connectType
@property cdnVendorName
@property cdn_flg
ivar tm_dur_cnnct
ivar tm_dur_dns
ivar tm_dur_firstP
ivar tm_dur_end
ivar tm_dur_ssl
ivar tm_pnt_send
ivar sendSize
ivar receiveSize
ivar headerSize
ivar dataSize
ivar statusCode
ivar errCode
ivar errText
ivar url
ivar ip
ivar contentType
ivar contentLength
ivar anyObj
ivar useContentLength
ivar netType
ivar appData
ivar response
ivar responseData
ivar urlParams
ivar dataBody
ivar httpMethodNumber
ivar libClassId
ivar socketItem
ivar threadId
ivar cdn_associate
ivar cdn_flg
ivar isFileURL
ivar connectType
ivar cdnVendorName
ivar _request
-clear
-init
-getText
-addIntoArray:
-startWithIP:DNSTime:atTimePoint:withObject:
-updateWithResponse:timePoint:
-updateWithReceiveData:
-updateWithTotalReceiveData:
-updateWithTotalReceiveSize:
-updateSendSize:
-updateWithError:
-finishAt:
-.cxx_destruct
-tm_dur_dns
-setTm_dur_dns:
-tm_pnt_send
-setTm_pnt_send:
-tm_dur_firstP
-setTm_dur_firstP:
-tm_dur_end
-setTm_dur_end:
-tm_dur_cnnct
-setTm_dur_cnnct:
-tm_dur_ssl
-setTm_dur_ssl:
-sendSize
-setSendSize:
-receiveSize
-setReceiveSize:
-errCode
-setErrCode:
-contentLength
-setContentLength:
-statusCode
-setStatusCode:
-headerSize
-setHeaderSize:
-dataSize
-setDataSize:
-url
-setUrl:
-ip
-setIp:
-errText
-setErrText:
-contentType
-setContentType:
-useContentLength
-setUseContentLength:
-netType
-setNetType:
-appData
-setAppData:
-response
-setResponse:
-responseData
-setResponseData:
-anyObj
-setAnyObj:
-urlParams
-setUrlParams:
-dataBody
-setDataBody:
-httpMethodNumber
-setHttpMethodNumber:
-libClassId
-setLibClassId:
-isFileURL
-setIsFileURL:
-socketItem
-setSocketItem:
-threadId
-setThreadId:
-connectType
-setConnectType:
-cdnVendorName
-setCdnVendorName:
-cdn_associate
-setCdn_associate:
-cdn_flg
-setCdn_flg:
-request
-setRequest:
}
下表列出了一些關鍵屬性的含義:
| 屬性 | 含義 |
|---|---|
| tm_pnt_send | 請求開始時間 |
| tm_dur_dns | DNS 解析時間 |
| tm_dur_cnnct | TCP 建立連接時間 |
| tm_dur_firstP | 首包時間 |
| tm_dur_ssl | SSL 握手時間 |
| statusCode | HTTP 狀態碼 |
響應時間是一個很好的量化指標,可以用來衡量用戶請求服務的等待時間,一般將其定義為用戶發送請求開始,到服務端的響應內容到達客戶端的這段時間。
下圖是HTTP 請求詳解圖

從上圖可以觀察到響應時間包括DNS 域名解析時間,與服務器端建立連接時間,服務器處理時間和響應到達客戶端的時間。
如果使用Charles 攔截HTTP 請求,可以在OverView Tab 的Timing 欄看響應時間的數據,如下圖中的Duration表示就是該請求總的響應時間,其中還包含上文提及的DNS (DNS 域名解析時間)、 Connect (建立連接時間)和SSL Handshake (SSL 握手時間),因為這個請求是HTTP 請求,所以SSL Handshake這個字段留空。

事實上當開發完SDK 中響應時間的feature 後,我們也可以通過這種方式來驗收結果的正確性,當然SDK 中獲取的時間與Charles 不可能完全相等,因為兩種的實現方式完全不同,但是他們之間差值應該在一個合理的範圍內。下文會詳細探討這方面。
通過上文的介紹我們很容易想到的一個思路:通過hook 請求發出時的函數,記錄下請求的時間,再hook iOS SDK 中響應的回調,記錄下結束的時間,計算差值即可得到這次請求的響應時間。聽雲的大致思路也是如此,只不過其中還有許多細節需要注意,我們接下來詳細討論它的具體實現方案。
聽雲是在_nbs_hook_NSURLSessionTask函數中hook NSURLSessionTask 的resume方法來達到記錄請求開始的目的。
void _nbs_hook_NSURLSessionTask() {
r14 = _objc_msgSend;
rax = [NSURLSessionConfiguration ephemeralSessionConfiguration];
rax = [rax retain];
var_40 = rax;
rax = [NSURLSession sessionWithConfiguration:rax];
rax = [rax retain];
rdx = 0x0;
var_38 = rax;
rax = [rax dataTaskWithURL:rdx];
rax = [rax retain];
var_30 = rax;
rbx = [rax class];
r12 = @selector(resume);
if (class_getInstanceMethod(rbx, r12) != 0x0) {
r15 = @selector(superclass);
r13 = @selector(resume);
var_48 = r15;
do {
if (_nbs_slow_isClassItSelfHasMethod(rbx, r12) != 0x0) {
r15 = class_getInstanceMethod(rbx, r12);
rax = method_getImplementation(r15);
rax = objc_retainBlock(__NSConcreteStackBlock);
var_50 = imp_implementationWithBlock(rax);
r13 = r13;
[rax release];
rdi = r15;
r15 = var_48;
rax = method_getTypeEncoding(rdi);
rdx = var_50;
rcx = rax;
class_replaceMethod(rbx, r12, rdx, rcx);
}
r14 = _objc_msgSend;
rbx = _objc_msgSend(rbx, r15, rdx, rcx);
rax = class_getInstanceMethod(rbx, r13);
r12 = r13;
} while (rax != 0x0);
}
(r14)(var_30, @selector(cancel), rdx);
(r14)(var_38, @selector(finishTasksAndInvalidate), rdx);
[var_30 release];
[var_38 release];
[var_40 release];
return;
}
將上面偽代碼還原為Objective-C 代碼如下:
void _nbs_hook_NSURLSessionTask() {
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnonnull"
NSURLSessionDataTask *task = [session dataTaskWithURL:nil];
#pragma clang diagnostic pop
Class cls = task.class;
if (class_getInstanceMethod(cls, @selector(resume))) {
Method method;
do {
if (_nbs_slow_isClassItSelfHasMethod(cls, @selector(resume))) {
Method resumeMethod = class_getInstanceMethod(cls, @selector(resume));
IMP imp = imp_implementationWithBlock(^(id self) {
});
class_replaceMethod(cls, @selector(resume), imp, method_getTypeEncoding(resumeMethod));
}
cls = [cls superclass];
method = class_getInstanceMethod(cls, @selector(resume));
} while (method);
}
[task cancel];
[session finishTasksAndInvalidate];
}
我們知道在Foundation框架中有些類其實是類族(Class Cluster),比如NSDictionary和NSArray 。而NSURLSessionTask也是一個類族,在不同的系統版本中繼承鏈不同,所以顯然不能直接hook NSURLSessionTask類,這裡採用了一個巧妙的方法,通過ephemeralSessionConfiguration方法構建一個Ephemeral sessions(短暫的會話),它與默認會話類似,不過不會將任何數據存儲到磁盤中,所有緩存,cookie 和憑據等都保存在RAM 中並與會話相關聯。這樣一來當會話無效時,它們將自動清除。然後通過這個短暫會話創建了一個session 對象,最後構建出task 對象,並通過這個task 對象獲得真正的類。
上面這種巧妙的做法其實並非聽雲獨創,他其實是參考了AFNetworking 的做法。 AFNetworking 中為了加上通知,在AFURLSessionManager中也實現了Hook NSURLSessionTask的resume和suspend方法。
if (NSClassFromString(@"NSURLSessionTask")) {
/**
iOS 7 and iOS 8 differ in NSURLSessionTask implementation, which makes the next bit of code a bit tricky.
Many Unit Tests have been built to validate as much of this behavior has possible.
Here is what we know:
- NSURLSessionTasks are implemented with class clusters, meaning the class you request from the API isn't actually the type of class you will get back.
- Simply referencing `[NSURLSessionTask class]` will not work. You need to ask an `NSURLSession` to actually create an object, and grab the class from there.
- On iOS 7, `localDataTask` is a `__NSCFLocalDataTask`, which inherits from `__NSCFLocalSessionTask`, which inherits from `__NSCFURLSessionTask`.
- On iOS 8, `localDataTask` is a `__NSCFLocalDataTask`, which inherits from `__NSCFLocalSessionTask`, which inherits from `NSURLSessionTask`.
- On iOS 7, `__NSCFLocalSessionTask` and `__NSCFURLSessionTask` are the only two classes that have their own implementations of `resume` and `suspend`, and `__NSCFLocalSessionTask` DOES NOT CALL SUPER. This means both classes need to be swizzled.
- On iOS 8, `NSURLSessionTask` is the only class that implements `resume` and `suspend`. This means this is the only class that needs to be swizzled.
- Because `NSURLSessionTask` is not involved in the class hierarchy for every version of iOS, its easier to add the swizzled methods to a dummy class and manage them there.
Some Assumptions:
- No implementations of `resume` or `suspend` call super. If this were to change in a future version of iOS, we'd need to handle it.
- No background task classes override `resume` or `suspend`
The current solution:
1) Grab an instance of `__NSCFLocalDataTask` by asking an instance of `NSURLSession` for a data task.
2) Grab a pointer to the original implementation of `af_resume`
3) Check to see if the current class has an implementation of resume. If so, continue to step 4.
4) Grab the super class of the current class.
5) Grab a pointer for the current class to the current implementation of `resume`.
6) Grab a pointer for the super class to the current implementation of `resume`.
7) If the current class implementation of `resume` is not equal to the super class implementation of `resume` AND the current implementation of `resume` is not equal to the original implementation of `af_resume`, THEN swizzle the methods
8) Set the current class to the super class, and repeat steps 3-8
*/
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"
NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
#pragma clang diagnostic pop
IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
Class currentClass = [localDataTask class];
while (class_getInstanceMethod(currentClass, @selector(resume))) {
Class superClass = [currentClass superclass];
IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));
IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));
if (classResumeIMP != superclassResumeIMP &&
originalAFResumeIMP != classResumeIMP) {
[self swizzleResumeAndSuspendMethodForClass:currentClass];
}
currentClass = [currentClass superclass];
}
[localDataTask cancel];
[session finishTasksAndInvalidate];
}
_nbs_slow_isClassItSelfHasMethod方法中會調用class_copyMethodList方法獲取這個類的方法列表,注意這個方法獲取的方法列表不包含父類的方法,所以_nbs_slow_isClassItSelfHasMethod方法其實就是判斷cls這個類自己是不是包含@selector(resume)。
事實上,上面的邏輯在開源庫FLEX 中也有實現,只不過實現上略有區別, FLEX會根據系統版本來區分是hook __NSCFLocalSessionTask 、NSURLSessionTask 和__NSCFURLSessionTask,個人感覺聽雲的實現比FLEX的硬編碼更優雅點。因為__NSCFLocalSessionTask和__NSCFURLSessionTask是私有類,所以採取了拆分再拼接的方式來避免審核被拒。
+ (void)injectIntoNSURLSessionTaskResume
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// In iOS 7 resume lives in __NSCFLocalSessionTask
// In iOS 8 resume lives in NSURLSessionTask
// In iOS 9 resume lives in __NSCFURLSessionTask
Class class = Nil;
if (![[NSProcessInfo processInfo] respondsToSelector:@selector(operatingSystemVersion)]) {
class = NSClassFromString([@[@"__", @"NSC", @"FLocalS", @"ession", @"Task"] componentsJoinedByString:@""]);
} else if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 9) {
class = [NSURLSessionTask class];
} else {
class = NSClassFromString([@[@"__", @"NSC", @"FURLS", @"ession", @"Task"] componentsJoinedByString:@""]);
}
SEL selector = @selector(resume);
SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector];
Method originalResume = class_getInstanceMethod(class, selector);
void (^swizzleBlock)(NSURLSessionTask *) = ^(NSURLSessionTask *slf) {
[[FLEXNetworkObserver sharedObserver] URLSessionTaskWillResume:slf];
((void(*)(id, SEL))objc_msgSend)(slf, swizzledSelector);
};
IMP implementation = imp_implementationWithBlock(swizzleBlock);
class_addMethod(class, swizzledSelector, implementation, method_getTypeEncoding(originalResume));
Method newResume = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(originalResume, newResume);
});
}
上面替換原來resume的實現是通過imp_implementationWithBlock實現的,而替換後的block 如下:
void ___nbs_hook_NSURLSessionTask_block_invoke(int arg0, int arg1, int arg2) {
rbx = [[NSDate date] retain];
[rbx timeIntervalSince1970];
var_40 = intrinsic_movsd(var_40, xmm0);
[rbx release];
r15 = _is_tiaoshi_kai;
COND = *(int8_t *)r15 == 0x0;
var_50 = r13;
if (!COND) {
rax = [var_30 URL];
rax = [rax retain];
r14 = r15;
r15 = rax;
rbx = [[r15 absoluteString] retain];
rdx = rbx;
__NBSDebugLog(0x3, @"NSURLSession:start:url:%@", rdx, rcx, r8, r9, stack[2048]);
[rbx release];
rdi = r15;
r15 = r14;
[rdi release];
}
rbx = [objc_getAssociatedObject(r12, @"m_SessAssociatedKey") retain];
if (rbx != 0x0) {
xmm1 = intrinsic_movsd(xmm1, var_40);
xmm1 = intrinsic_mulsd(xmm1, *0x1000b9990);
xmm0 = intrinsic_movsd(xmm0, *0x1000b9da8);
[rbx startWithIP:0x0 DNSTime:var_30 atTimePoint:r8 withObject:r9];
[rbx setRequest:var_30];
[rbx setLibClassId:0x1];
}
else {
if (*(int8_t *)r15 != 0x0) {
__NBSDebugLog(0x3, cfstring_r, rdx, rcx, r8, r9, stack[2048]);
}
}
}
上面偽代碼中將___nbs_hook_NSURLSessionTask_block_invoke中不相關的邏輯忽略了,可以看到其中生成了時間戳,並將該時間戳當做[rbx startWithIP:0x0 DNSTime:var_30 atTimePoint:r8 withObject:r9]方法的入參, rbx為_priv_NBSHTTPTransaction實例,而該實例是通過NSURLSessionDataTask的關聯對象獲取到的。而_priv_NBSHTTPTransaction實例的創建和關聯對象的設置邏輯則在-[_priv_NSURLSession_NBS nbs_dataTaskWithRequest:completionHandler:]方法中。
r12 = [[var_30 nbs_dataTaskWithRequest:r13 completionHandler:0x0] retain];
r15 = [_priv_NBSHTTPTransaction new];
if (r12 != 0x0) {
objc_setAssociatedObject(r12, @"m_SessAssociatedKey", r15, 0x301);
}
[r15 release];
-[_priv_NBSHTTPTransaction startWithIP:DNSTime:atTimePoint:withObject:]方法會將參數的時間賦值給自己的tm_pnt_send屬性。
-[_priv_NBSHTTPTransaction startWithIP:DNSTime:atTimePoint:withObject:] {
var_30 = intrinsic_movsd(var_30, arg4, rdx, arg5);
r12->tm_pnt_send = intrinsic_movsd(r12->tm_pnt_send, intrinsic_movsd(xmm0, var_30));
}
當然除了-[_priv_NSURLSession_NBS nbs_dataTaskWithRequest:completionHandler:]方法外,下面的方法也包含這段邏輯:
nbs_downloadTaskWithRequest:nbs_downloadTaskWithRequest:completionHandler:nbs_downloadTaskWithResumeData:completionHandler:nbs_uploadTaskWithRequest:fromData:completionHandler:nbs_uploadTaskWithRequest:fromFile:completionHandler:nbs_uploadTaskWithRequest:fromFile:nbs_uploadTaskWithRequest:fromData:nbs_uploadTaskWithStreamedRequest:在finishAt方法中計算得到最終的響應時間,並將其賦值給tm_dur_end屬性。
void -[_priv_NBSHTTPTransaction finishAt:](void * self, void * _cmd, double arg2) {
r14 = self;
rbx = [r14 retain];
r14 = @selector(tm_pnt_send);
_objc_msgSend(rbx, r14);
xmm1 = intrinsic_xorpd(xmm1, xmm1);
xmm0 = intrinsic_ucomisd(xmm0, xmm1);
COND = xmm0 <= 0x0;
if (!COND) {
_objc_msgSend(rbx, r14);
xmm1 = intrinsic_movsd(xmm1, var_30);
xmm1 = intrinsic_subsd(xmm1, xmm0);
xmm0 = intrinsic_movapd(xmm0, xmm1);
[rbx setTm_dur_end:rdx];
}
}
對於調用dataTaskWithRequest:completionHandler:方法發起的網絡請求,請求完成的回調在completionHandler中,所以應該在完成的回調中調用finishAt方法。類似的還有___72-[_priv_NSURLSession_NBS nbs_downloadTaskWithRequest ___72-[_priv_NSURLSession_NBS nbs_downloadTaskWithRequest:completionHandler:]_block_invoke ___79-[_priv_NSURLSession_NBS nbs_uploadTaskWithRequest:fromData:completionHandler:]_block_invoke等方法。
int ___68-[_priv_NSURLSession_NBS nbs_dataTaskWithRequest:completionHandler:]_block_invoke(int arg0, int arg1, int arg2, int arg3) {
rdi = *(r12 + 0x20);
xmm0 = intrinsic_movsd(xmm0, var_68);
[rdi finishAt:rdx];
}
然而對於調用dataTaskWithRequest:方法的網絡請求,則需要hook NSURLSessionTaskDelegate的URLSession:task:didCompleteWithError:方法。
void -[_priv_NBSLensAllMethodsDlgt_urlSess nbs_URLSession:task:didCompleteWithError:](void * self, void * _cmd, void * arg2, void * arg3, void * arg4) {
rbx = [[NSDate date] retain];
[rbx timeIntervalSince1970];
xmm0 = intrinsic_mulsd(xmm0, *0x1000b9990);
var_40 = intrinsic_movsd(var_40, xmm0);
[rbx release];
rax = objc_getAssociatedObject(r13, @"m_SessAssociatedKey");
rax = [rax retain];
_objc_msgSend(r12, @selector(finishAt:), var_58);
}