
แม้ว่า APM จะได้รับความนิยมมากขึ้นเรื่อย ๆ แต่ผู้ผลิต APM มืออาชีพทุกขนาดได้กลายเป็นเห็ดหลังฝนตกและยังมีบทความทางเทคนิคมากมายเกี่ยวกับ APM ในตลาด แต่ส่วนใหญ่เป็นเพียงรสชาติที่เรียบง่ายและไม่ได้ขุดลึกลงไปในรายละเอียดการดำเนินการ บทความนี้มีวัตถุประสงค์เพื่อเปิดเผยหลักการการทำงานภายในของ iOS SDK ของผู้ผลิต APM ที่รู้จักกันดีโดยการวิเคราะห์รายละเอียดการใช้งานเฉพาะของ SDK ฉันเชื่อว่าก่อนที่จะอ่านบทความนี้ผู้อ่านก็อยากรู้เกี่ยวกับรายละเอียดการใช้งานของ APM SDK เช่นเดียวกับผู้เขียน โชคดีที่บทความที่คุณกำลังอ่านจะนำคุณไปสู่การเปิดเผยบริบทที่แท้จริงของ APM ทีละขั้นตอน APM SDKs ที่วิเคราะห์ในบทความนี้ ได้แก่ Tingyun , ONEAPM และ การตรวจสอบประสิทธิภาพของ Firebase ฯลฯ ผู้เขียนมีความสามารถและมีความรู้เล็กน้อย หากมีข้อผิดพลาดใด ๆ เขาจะไม่ถูกต้องเพื่อที่จะพิมพ์ซ้ำและทำให้พวกเขาสมบูรณ์แบบมากขึ้น
รุ่น Tingyun SDK วิเคราะห์ในบทความนี้คือ 2.3.5 ซึ่งแตกต่างจากเวอร์ชันล่าสุดเล็กน้อย อย่างไรก็ตามฉันอ่านรหัสของเวอร์ชันใหม่โดยประมาณ แต่ความแตกต่างไม่ใหญ่และไม่ส่งผลกระทบต่อการวิเคราะห์
การตรวจสอบการเรนเดอร์หน้าดูเหมือนง่ายมาก แต่จะยังคงมีปัญหามากมายในกระบวนการพัฒนาจริง สิ่งที่ง่ายกว่าที่จะคิดคือวิธีวงจรชีวิตที่สำคัญหลายวิธีของหน้า Hook เช่น viewDidLoad , viewDidAppear: ฯลฯ เพื่อคำนวณเวลาการเรนเดอร์หน้าและในที่สุดก็พบหน้าเว็บที่โหลดช้า อย่างไรก็ตามหากคุณเริ่มบรรลุเป้าหมายผ่านแนวคิดข้างต้นคุณจะประสบปัญหา ฉันจะขอวงจรชีวิตของทุกหน้าได้อย่างไรใน APM SDK ได้อย่างไร? ถ้าฉันพยายามเชื่อมต่อ UIViewController ล่ะ? วิธีการ hook UIViewController นั้นไม่สามารถทำได้เพราะมันใช้งานได้กับวิธี UIViewController เท่านั้นและคอนโทรลเลอร์มุมมองส่วนใหญ่ในแอปพลิเคชันที่สืบทอดมาจาก UIViewController ดังนั้นวิธีนี้จึงไม่สามารถทำได้ แต่ Tingyun SDK สามารถนำไปใช้ได้ ตรรกะของเบ็ดหน้าส่วนใหญ่จะถูกนำไปใช้ในคลาส _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 นี้เป็นคลาสย่อยของ UIViewController ทั้งหมดใน Mainbundle ในความเป็นจริงถ้าคุณเบรกพอยต์สายการประกอบหลังจากการเรียกใช้ฟังก์ชันเสร็จสิ้นผ่าน LLDB คุณจะพบว่าอาร์เรย์ที่ส่งคืนนั้นเป็นคลาสย่อยของ UIViewController คำสั่งต่อไปนี้ if กำหนดว่าการลงทะเบียน r12 นั้นไม่ได้ nil และ count ของการลงทะเบียน r12 ไม่เท่ากับ 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;
}
รูทีนย่อย loc_10001db4d ในฟังก์ชั่น _subMetaClassNamesInMainBundle_c เรียกฟังก์ชัน _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 หากรายการเป็นคลาสย่อยของ UIViewController ดังนั้น metaClass ของคลาสจะได้รับและเพิ่มลงในอาเรย์ตัวแปร var_38
ถัดไปให้มุ่งเน้นไปที่คำสั่งวน do-while ที่อยู่ภายใน คำสั่งสำหรับการตัดสินแบบวนรอบคือ var_98 + 0x1 < rax var_98 กำหนด rdx register ที่จุดเริ่มต้นของลูปและการลงทะเบียน rdx จะเริ่มต้นเป็น 0 นอกลูปดังนั้น var_98 เป็นตัวนับและการลงทะเบียน rax เป็นวิธี count ที่กำหนดให้กับ r12 register จากสิ่งนี้ลูป do-while กำลังผ่านการสำรวจอาร์เรย์ของคลาสย่อย UIViewController พฤติกรรมการสำรวจคือการแลกเปลี่ยน initialize และ nbs_jump_initialize: วิธีการผ่าน _nbs_Swizzle_orReplaceWithIMPs
รหัสสำหรับ 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 ดังนั้นการเชื่อม 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 ในเมธอดคือคลาสของ ViewController เป็น hook รับชื่อคลาสของ arg2 และกำหนดให้กับการลงทะเบียน rbx จากนั้นใช้ rbx เพื่อสร้างสตริง var_C0 nbs_%s_viewDidLoad เช่น nbs_XXViewController_viewDidLoad __NSConcreteStackBlock ในประโยคต่อไปนี้เป็นวัตถุบล็อกของสแต็กที่เก็บข้อมูลที่สร้างขึ้น บล็อกนี้จะได้รับตัวชี้ฟังก์ชั่น IMP ผ่านวิธีการ imp_implementationWithBlock _nbs_Swizzle_orReplaceWithIMPs เป็นฟังก์ชันที่ใช้วิธีการแลกเปลี่ยนวิธีการและพารามิเตอร์คือ: arg2 เป็นคลาสของ ViewController ; @selector(viewDidLoad) เป็นตัวเลือกของ viewDidLoad ; var_C0 เป็นตัวเลือกของ nbs_%s_viewDidLoad , r14 เป็น imp ของ __NSConcreteStackBlock ที่สอง; var_D0 เป็น IMP ของ __NSConcreteStackBlock แรก
ตรรกะทั้งหมดของ hook_viewDidLoad: มีความชัดเจนประมาณ แต่มีคำถามที่นี่ว่าทำไมไม่แลกเปลี่ยนสอง imps โดยตรง แต่แทนที่จะสร้างสองบล็อกก่อนแล้วแลกเปลี่ยน IMPs ของสองบล็อก? เหตุผลก็คือผลลัพธ์ของคลาสพาเรนต์ของ ViewController นั่นคือ class_getSuperclass จำเป็นต้องส่งผ่านเป็นพารามิเตอร์ไปยังวิธีการแลกเปลี่ยน ด้วยวิธีนี้จำนวนพารามิเตอร์ที่ลงนามโดยตัวเลือกที่แลกเปลี่ยนทั้งสองนั้นไม่สอดคล้องกันและปัญหานี้จะต้องได้รับการแก้ไขอย่างชาญฉลาดโดยการสร้างบล็อก ในความเป็นจริง __NSConcreteStackBlock แรกดำเนินการ nbs_jump_viewDidLoad:superClass: วิธีการของ _priv_NBSUIHookMatrix ดังที่ได้กล่าวไว้ก่อนหน้านี้มี superClass ในพารามิเตอร์ของวิธีนี้ สำหรับสาเหตุที่จำเป็นต้องใช้พารามิเตอร์นี้ฉันจะแนะนำในภายหลัง
ทำไม __NSConcreteStackBlock ที่สองจึงเรียกใช้งาน nbs_jump_viewDidLoad:superClass: เมธอด? ยกเลิกการเลือกตัวเลือก Remove potentially dead code ของ Hopper รหัสมีดังนี้:
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 นั้นชัดเจนมาก ในการรับ IMP ของตัวเลือก rsi ในคลาส rdi ผู้อ่านจะพบว่า _nbs_getInstanceImpOf ถูกเรียกสองครั้งใน hook_viewDidLoad: เมธอด rdi แรกคือคลาส _priv_NBSUIHookMatrix , rdx คือ @selector(nbs_jump_viewDidLoad:superClass:) , rdi ที่สองคือคลาส ViewController rdx คือ @selector(viewDidLoad)
จากนั้นมาดู __NSConcreteStackBlock แรกซึ่งหมายถึงบล็อกที่จะเรียก nbs_jump_viewDidLoad:superClass: รหัสมีดังนี้:
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 เป็น IMP ของ nbs_jump_viewDidLoad:superClass: และรหัสนี้เรียกใช้ 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 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 เป็นอินสแตนซ์ NSDate แบบคงที่ที่ใช้ในการบันทึกเวลาเริ่มต้นของการเริ่มต้นแอปพลิเคชันทั้งหมดดังนั้น FPM SDK จะทำเครื่องหมายเวลาเริ่มต้นของการเริ่มต้นแอปพลิเคชันใน +load ของ FPRAppActivityTracker ผู้อ่านที่เข้าใจวิธี +load ควรรู้ว่าวิธีนี้เป็นวิธีการเบ็ดก่อนที่จะเรียกฟังก์ชัน main เวลาที่แน่นอนคือเมื่อภาพถูกโหลดไปยังรันไทม์และวิธี +load พร้อมแล้วจากนั้นจะเรียกใช้วิธี +load นอกจากนี้ยังมีวิธี +load ประเภทที่แตกต่างกันยังเกี่ยวข้องกับลำดับไฟล์ของ Build Phases->Compile Sources เราเชื่อว่าสิ่งเหล่านี้ไม่มีผลกระทบอย่างมีนัยสำคัญต่อสถิติของเวลาเริ่มต้น
หลังจากนั้นการแจ้งเตือนของ UIWindowDidBecomeVisibleNotification จะลงทะเบียน การแจ้งเตือนนี้จะถูกกระตุ้นเมื่อวัตถุ UIWindow เปิดใช้งานและแสดงบนอินเทอร์เฟซ ผู้อ่านสามารถลงทะเบียนการแจ้งเตือนนี้แล้วพิมพ์วัตถุการแจ้งเตือนด้วย LLDB ตัวอย่างมีดังนี้:
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
โพสต์เมื่อมองเห็นวัตถุ uiwindow วัตถุการแจ้งเตือนคือวัตถุหน้าต่างที่มองเห็นได้ การแจ้งเตือนนี้ไม่มีพจนานุกรม UserInfo การสลับระหว่างแอพไม่ได้สร้างการแจ้งเตือนที่เกี่ยวข้องกับการมองเห็นสำหรับ Windows การเปลี่ยนแปลงการมองเห็นของหน้าต่างสะท้อนให้เห็นถึงการเปลี่ยนแปลงคุณสมบัติที่ซ่อนอยู่ของหน้าต่างและสะท้อนให้เห็นถึงการมองเห็นของหน้าต่างภายในแอพเท่านั้น
ต่อไปนี้เป็นวิธีการจัดการการแจ้งเตือน ฉันกู้คืนวิธีการไปยังรหัส Pseudo-Code 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 เพื่อเริ่มติดตามกิจกรรมของแอพ วิธีนี้จะกล่าวถึงในเชิงลึกในภายหลัง
ก่อนอื่นเรามีความชัดเจนว่าคำขอเครือข่ายที่เรากำลังพูดถึงที่นี่ไม่มีคำแนะนำพิเศษใด ๆ ที่จะอ้างถึงคำขอ HTTP Tingyun SDK ส่วนใหญ่ใช้สองวิธีในการใช้งานการตรวจสอบเครือข่าย: ครั้งแรกคือการเชื่อมต่อ API ที่ใช้โดยการเขียนโปรแกรมเครือข่าย iOS ซึ่งส่วนใหญ่กำหนดเป้าหมายคำขอเครือข่ายดั้งเดิม ประการที่สองคือการสืบทอด NSURLProtocol เพื่อใช้คำขอเครือข่ายซึ่งส่วนใหญ่กำหนดเป้าหมายคำขอเครือข่ายใน UIWebView
SDK เชื่อมต่อ API สำหรับการสร้าง NSURLSessionDataTask , NSURLSessionUploadTask และ NSURLSessionDownloadTask ในคำขอเครือข่ายทั้งหมด ตรรกะของ 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 เป็นฟังก์ชั่น C ที่ฟังคลาวด์เพื่อใช้เมธอด
จากรหัสเราจะเห็นได้ว่านอกเหนือจากการใช้ _nbs_Swizzle สำหรับ NSURLSessionDataTask , NSURLSessionUploadTask และ NSURLSessionDownloadTask sessionWithConfiguration:delegate:delegateQueue: ที่กล่าวถึงข้างต้น ฉันจะอธิบายว่าทำไมวิธีการนี้จึงถูกตะขอในภายหลัง
การใช้วิธีการของ 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 เป็นรูปแบบของพารามิเตอร์ประสิทธิภาพที่เกี่ยวข้องกับคำขอ HTTP ใน SDK โครงสร้างคลาสนี้มีดังนี้:
@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 |
| รหัสสถานะ | รหัสสถานะ http |
เวลาตอบสนองเป็นตัวบ่งชี้เชิงปริมาณที่ดีซึ่งสามารถใช้ในการวัดเวลารอคอยสำหรับผู้ใช้ในการขอบริการ โดยทั่วไปจะถูกกำหนดเป็นช่วงเวลาที่ผู้ใช้ส่งคำขอและเนื้อหาการตอบกลับบนเซิร์ฟเวอร์ถึงไคลเอนต์
รูปต่อไปนี้เป็นคำอธิบายโดยละเอียดของคำขอ HTTP

จากรูปด้านบนเราสามารถสังเกตได้ว่าเวลาตอบสนองรวมถึงเวลาแก้ไขชื่อโดเมน DNS เวลาการเชื่อมต่อที่กำหนดไว้กับเซิร์ฟเวอร์เวลาในการประมวลผลเซิร์ฟเวอร์และเวลาที่การตอบกลับมาถึงไคลเอนต์
หากคุณใช้ Charles เพื่อสกัดกั้นคำขอ HTTP คุณสามารถดูข้อมูลเวลาตอบสนองในคอลัมน์เวลาของแท็บภาพรวม Duration ในรูปด้านล่างแสดงถึงเวลาตอบสนองทั้งหมดของการร้องขอซึ่งรวมถึง DNS (เวลาแก้ไขชื่อโดเมน DNS), Connect (เวลาสร้างการเชื่อมต่อ) และ SSL Handshake (เวลาจับมือ SSL) ที่กล่าวถึงข้างต้น เนื่องจากคำขอนี้เป็นคำขอ HTTP ฟิลด์ SSL Handshake จึงว่างเปล่า

ในความเป็นจริงหลังจากพัฒนาคุณลักษณะของเวลาตอบสนองใน SDK เรายังสามารถตรวจสอบความถูกต้องของผลลัพธ์ด้วยวิธีนี้ แน่นอนว่าเวลาที่ได้รับใน SDK นั้นไม่เท่ากับ Charles อย่างแน่นอนเนื่องจากวิธีการดำเนินการทั้งสองนั้นแตกต่างกันอย่างสิ้นเชิง แต่ความแตกต่างระหว่างพวกเขาควรอยู่ในช่วงที่เหมาะสม ด้านนี้จะมีการหารือในรายละเอียดด้านล่าง
ผ่านการแนะนำข้างต้นเราสามารถนึกถึงแนวคิดได้อย่างง่ายดาย: ผ่านฟังก์ชั่นเมื่อมีการออกคำขอเบ็ดบันทึกเวลาการร้องขอจากนั้นขอตอบกลับการเรียกกลับใน iOS SDK บันทึกเวลาสิ้นสุดและคำนวณความแตกต่างเพื่อรับเวลาตอบสนองของคำขอนี้ เช่นเดียวกับความคิดทั่วไปของ Tingyun แต่มีรายละเอียดมากมายที่ต้องให้ความสนใจ ลองหารือเกี่ยวกับแผนการดำเนินงานเฉพาะในรายละเอียดถัดไป
การฟังคลาวด์เป็นวิธี resume ของ NSURLSessionTask ในฟังก์ชัน _nbs_hook_NSURLSessionTask เพื่อให้ได้จุดประสงค์ในการบันทึกจุดเริ่มต้นของคำขอ
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;
}
กู้คืน pseudocode ข้างต้นเป็นรหัสวัตถุประสงค์ -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 บางชั้นเรียนเป็นกลุ่มชั้นเรียนเช่น NSDictionary และ NSArray NSURLSessionTask ยังเป็นตระกูลในชั้นเรียนและห่วงโซ่การสืบทอดนั้นแตกต่างกันในรุ่นระบบที่แตกต่างกันดังนั้นเห็นได้ชัดว่าคุณไม่สามารถขอคลาส NSURLSessionTask ได้โดยตรง วิธีการที่ชาญฉลาดถูกนำมาใช้ที่นี่เพื่อสร้างเซสชันชั่วคราวผ่านวิธี ephemeralSessionConfiguration มันคล้ายกับเซสชันเริ่มต้น แต่ไม่มีการเก็บข้อมูลไว้ในดิสก์และแคช, คุกกี้, ข้อมูลรับรอง ฯลฯ ทั้งหมดจะถูกบันทึกไว้ใน RAM และเชื่อมโยงกับเซสชัน วิธีนี้พวกเขาจะถูกล้างโดยอัตโนมัติเมื่อเซสชันไม่ถูกต้อง จากนั้นผ่านเซสชันสั้น ๆ นี้วัตถุเซสชันจะถูกสร้างขึ้นและในที่สุดวัตถุงานก็ถูกสร้างขึ้นและคลาสจริงจะได้รับผ่านวัตถุงานนี้
วิธีการที่ชาญฉลาดข้างต้นไม่ได้ถูกสร้างขึ้นโดย Tingyun มันหมายถึงวิธีการของ Afnetworking เพื่อเพิ่มการแจ้งเตือนใน Afnetworking วิธี resume และ suspend ของ Hook NSURLSessionTask ยังถูกนำไปใช้ใน AFURLSessionManager
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];
}
วิธี
class_copyMethodListจะถูกเรียกในวิธี_nbs_slow_isClassItSelfHasMethodเพื่อรับรายการวิธีการของคลาสนี้ โปรดทราบว่ารายการวิธีการที่ได้รับจากวิธีนี้ไม่มีวิธีการของคลาสแม่ดังนั้นวิธี_nbs_slow_isClassItSelfHasMethodจริง ๆ แล้วจะตัดสินว่าคลาสclsนั้นมี@selector(resume)หรือไม่
ในความเป็นจริงตรรกะข้างต้นยังถูกนำไปใช้ใน Library Open Source Library แต่มีความแตกต่างเล็กน้อยในการใช้งาน 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 และบล็อกที่ถูกแทนที่มีดังนี้:
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: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: เมธอดคุณต้องเชื่อมต่อ URLSession:task:didCompleteWithError: วิธีการของ NSURLSessionTaskDelegate
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);
}