
على الرغم من أن APM أصبحت أكثر وأكثر شعبية ، فقد ظهرت شركات APM المحترفة من جميع الأحجام مثل الفطر بعد هطول أمطار ، وهناك أيضًا العديد من المقالات الفنية في APM في السوق ، فإن معظمهم مجرد ذوق بسيط ولا يحفرون في تفاصيل التنفيذ. تهدف هذه المقالة إلى فضح مبدأ العمل الداخلي لـ iOS SDK لمصنعي APM المعروفين من خلال تحليل تفاصيل التنفيذ المحددة لـ SDK. أعتقد أنه قبل قراءة هذا المقال ، كان القراء فضوليين أيضًا حول تفاصيل تنفيذ APM SDK تمامًا مثل المؤلف. لحسن الحظ ، فإن المقالة التي تقرأها ستأخذك إلى الكشف عن السياق الحقيقي لـ APM خطوة بخطوة. تشمل APM SDKs التي تم تحليلها في هذه المقالة Tingyun و ONEAPM و Firebase Performance ، وما إلى ذلك. المؤلف موهوب بعض الشيء ودراية. إذا كانت هناك أي أخطاء ، فلن يصححها من أجل إعادة طبعها وجعلها أكثر كمالا.
إصدار Tingyun SDK الذي تم تحليله في هذه المقالة هو 2.3.5 ، وهو ما يختلف قليلاً عن أحدث إصدار. ومع ذلك ، قرأت تقريبًا رمز الإصدار الجديد ، لكن الفرق ليس كبيرًا ولا يؤثر على التحليل.
يبدو مراقبة عرض الصفحة بسيطًا للغاية ، ولكن لا يزال هناك العديد من المشكلات في عملية التطوير الفعلية. ما هو أسهل للتفكير هو العديد من أساليب دورة الحياة الرئيسية لصفحات الخطاف ، مثل 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 للفئة وإضافتها إلى Array var_38 المتغير.
بعد ذلك ، دعونا نركز على بيان do-while في الداخل. بيان حكم الحلقة هو var_98 + 0x1 < rax . يقوم var_98 بتعيين سجل rdx في بداية الحلقة ، ويتم تهيئة سجل rdx إلى 0 خارج الحلقة ، لذلك var_98 هو العداد ، وسجل rax هو طريقة count المعينة لسجل r12 . استنادًا إلى هذا 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 لربطها ، والحصول على اسم فئة arg2 وتعيينه إلى سجل rbx ، ثم استخدم rbx لإنشاء سلسلة nbs_%s_viewDidLoad ، مثل nbs_XXViewController_viewDidLoad ، الحصول على صانع السلسلة وتعيينه إلى var_C0 . __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: واضح تقريبًا ، ولكن هناك سؤال هنا لماذا لا تتبادل مباشرة اثنين من الإفصاحين ، ولكن بدلاً من ذلك ، قم ببناء كتلتين أولاً ، ثم تبادل الأسماء الخاصة بكتلين؟ والسبب هو أن نتيجة فئة الوالدين لـ ViewController ، أي class_getSuperclass ، يجب تمريرها كمعلمات إلى الطريقة المتبادلة. وبهذه الطريقة ، يكون عدد المعلمات الموقعة من قبل المحددين المتبادلتين غير متسق ، ويجب حل هذه المشكلة بذكاء عن طريق بناء كتلة. في الواقع ، يقوم أول __NSConcreteStackBlock بتنفيذ nbs_jump_viewDidLoad:superClass: طريقة _priv_NBSUIHookMatrix . كما ذكرنا من قبل ، هناك superClass في معلمات هذه الطريقة. بالنسبة إلى سبب حاجة إلى هذه المعلمة ، سأقدمها لاحقًا.
لماذا يقوم __NSConcreteStackBlock الثاني بتنفيذ nbs_jump_viewDidLoad:superClass: الطريقة؟ قم بإلغاء تحديد خيار 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 واضحة للغاية. للحصول على 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 هي نفس 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. تعكس تغييرات رؤية النافذة التغييرات في خاصية النافذة المخفية وتعكس فقط رؤية النافذة داخل التطبيق.
فيما يلي طريقة التعامل مع الإخطارات. لقد استعادت طريقة الكود الكاذب الهدف 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 بشكل أساسي طريقتين لتنفيذ مراقبة الشبكة: الأول هو ربط واجهة برمجة التطبيقات التي تستخدمها برمجة شبكة iOS ، والتي تستهدف بشكل أساسي طلبات الشبكة الأصلية ؛ والثاني هو ورث NSURLProtocol لتنفيذ طلبات الشبكة ، والتي تستهدف بشكل أساسي طلبات الشبكة في UIWebView.
يربط SDK واجهة برمجة التطبيقات لبناء NSURLSessionDataTask و NSURLSessionUploadTask و NSURLSessionDownloadTask في جميع طلبات الشبكة. يكون منطق الخطاف في وظيفة 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 التي تستمع إلى السحابة لتنفيذ طريقة swizzling.
من الكود ، يمكننا أن نرى أنه بالإضافة إلى استخدام _nbs_Swizzle في NSURLSessionDataTask و NSURLSessionUploadTask و NSURLSessionDownloadTask API المذكورة أعلاه ، فإنه يحل محل تنفيذ sessionWithConfiguration:delegate:delegateQueue: الطريقة. سأشرح لماذا تم توصيل هذه الطريقة لاحقًا.
يتم تعريف جميع تطبيقات طريقة الخطاف في فئة _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 لا يساوي تمامًا تشارلز ، لأن طريقتين للتنفيذ مختلفان تمامًا ، ولكن يجب أن يكون الفرق بينهما ضمن نطاق معقول. سيتم مناقشة هذا الجانب بالتفصيل أدناه.
من خلال المقدمة أعلاه ، يمكننا بسهولة التفكير في فكرة: من خلال الوظيفة عند إصدار طلب الخطاف ، قم بتسجيل وقت الطلب ، ثم ربط استجابة رد الاتصال في 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;
}
استعادة الرمز الكاذب أعلاه إلى رمز الهدف-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).
في الواقع ، يتم تنفيذ المنطق أعلاه أيضًا في مكتبة المصدر المفتوح 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 ، والكتلة المستبدلة كما يلي:
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: Method ، فإن رد الاتصال المكتمل في 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);
}