
APMはますます人気が高まっていますが、あらゆるサイズのプロのAPMメーカーが雨の後にキノコのように出現しました。また、市場にはAPMに関する多くの技術記事もありますが、それらのほとんどは単純な味であり、実装の詳細を深く掘り下げていません。この記事の目的は、SDKの特定の実装の詳細を分析することにより、有名なAPMメーカーのiOS SDKの内部作業原則を公開することを目的としています。この記事を読む前に、読者は著者と同じようにAPM SDKの実装の詳細に興味があったと思います。幸いなことに、あなたが読んでいる記事は、APMの真のコンテキストを段階的に明らかにするためにあなたを連れて行きます。この記事で分析されたAPM SDKには、 Tingyun 、 Oneapm 、 Firebaseのパフォーマンス監視などが含まれます。著者は少し才能があり、知識が豊富です。間違いがある場合、彼はそれらを再版し、それらをより完全にするためにそれらを修正しません。
この記事で分析された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
}
まず、別のより疑わしい方法に注意を向けましょう。FookSubofController、特定の実装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のサブクラスであると基本的に推測できます。実際、LLDBを介して関数呼び出しが完了した後に組み立てラインをブレークポイントすると、返された配列は確かにUIViewControllerのサブクラスであることがわかります。次のifステートメントは、 r12レジスタがnilではなく、 r12レジスタのcountがifが実行される前に0に等しくないことを決定します。 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実装も非常に明確です。アイテムがUIViewControllerのサブクラスである場合、クラスのmetaClassが取得され、可変配列var_38に追加されます。
次に、内部のdo-whileループステートメントに焦点を当てましょう。ループ判断のステートメントはvar_98 + 0x1 < raxです。 var_98ループの開始時にrdxレジスタを割り当て、 rdxレジスタはループの外側の0に初期化されるため、 var_98カウンターであり、 raxレジスタはr12レジスタに割り当てられたcountメソッドです。これに基づいてdo-while Loopは実際にUIViewControllerサブクラスの配列を横断しています。トラバーサルの動作は、 initializeと_nbs_Swizzle_orReplaceWithIMPsを交換することです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のルーチンから、メインロジックは3つの方法、 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のクラス名nbs_XXViewController_viewDidLoad取得し、 rbxレジスタに割り当ててから、 rbxを使用してnbs_%s_viewDidLoadを構築して、stringの選択var_C0を取得します。次の文の__NSConcreteStackBlockは、作成されたストレージスタックのブロックオブジェクトです。このブロックは、 imp_implementationWithBlockメソッドを介してIMP関数ポインターを取得します。 _nbs_Swizzle_orReplaceWithIMPsメソッド交換を実装する関数であり、パラメーターは次のとおりです。Arg2 arg2 ViewControllerのクラスです。 @selector(viewDidLoad)はviewDidLoadのセレクターです。 var_C0はnbs_%s_viewDidLoadのセレクターであり、 r14 2番目の__NSConcreteStackBlockのIMPです。 var_D0は、最初の__NSConcreteStackBlockのIMPです。
hook_viewDidLoad:ほぼ明確ですが、ここに疑問がありますが、なぜ2つのインプを直接交換しないのか、代わりに2つのブロックを構築してから2つのブロックのインプを交換しますか?その理由は、 ViewControllerの親クラス、つまりclass_getSuperclassの結果は、交換された方法のパラメーターとして渡す必要があるためです。このようにして、2つの交換されたセレクターによって署名されたパラメーターの数は一貫性がありません。この問題は、ブロックを構築することで巧みに解決する必要があります。実際、最初の__NSConcreteStackBlock nbs_jump_viewDidLoad:superClass: _priv_NBSUIHookMatrixのメソッドを実行します。前述のように、この方法のパラメーターにはsuperClassがあります。このパラメーターが必要な理由については、後で紹介します。
2番目の__NSConcreteStackBlock nbs_jump_viewDidLoad:superClass: methodを実行するのはなぜですか? 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セレクターのIMPを取得するために、読者は_nbs_getInstanceImpOf hook_viewDidLoad: methodで2回呼び出されていることがわかります。最初のrdiは_priv_NBSUIHookMatrixクラス、 rdxは@selector(nbs_jump_viewDidLoad:superClass:) 、2番目の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レジスタは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はアプリケーションスタートアップ全体の開始時間を節約するために使用される静的なNSDateインスタンスであるため、FPM SDKは、 FPRAppActivityTrackerの+loadでアプリケーションスタートアップの開始時間をマークします。 +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: callback、この通知がステータスバーのwindowが作成されたときにトリガーされました。この実装は少し注意が必要であり、Appleが将来の呼び出しのタイミングを調整することを保証することはできません。
以下は、 UIWindowDidBecomeVisibleNotificationの公式説明です。
UiWindowオブジェクトが表示されたときに投稿されます。通知オブジェクトは、表示されたウィンドウオブジェクトです。この通知には、userinfo辞書が含まれていません。アプリ間を切り替えても、Windowsの可視性関連の通知は生成されません。ウィンドウの可視性の変更は、ウィンドウの非表示のプロパティの変更を反映し、アプリ内のウィンドウの可視性のみを反映しています。
以下は、通知を処理する方法です。この方法は、分解された擬似コードを比較できる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 Notificationをログアウトします。まず、 -startAppActivityTrackingメソッドを呼び出して、アプリのアクティビティの追跡を開始します。この方法については、後で詳しく説明します。
まず、ここで説明しているネットワークリクエストには、HTTPリクエストを参照する特別な指示がないことは明らかです。 Tingyun SDKは主に2つの方法を使用してネットワーク監視を実装します。1つ目は、主にネイティブネットワークリクエストをターゲットにするiOSネットワークプログラミングで使用されるAPIをフックすることです。 2つ目は、主にuiwebviewでネットワーク要求をターゲットにするネットワーク要求を実装するためにNSURLProtocol継承することです。
SDKは、すべてのネットワークリクエストでNSURLSessionDataTask 、 NSURLSessionUploadTask 、 NSURLSessionDownloadTask構築するためのAPIをフックします。フックのロジックは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関数です。
コードから、 NSURLSessionDataTask 、 NSURLSessionUploadTask 、およびNSURLSessionDownloadTask APIに_nbs_Swizzle使用することに加えて、 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は、SDKのHTTP要求に関連するパフォーマンスパラメーターのモデルです。このクラス構造は次のとおりです。
@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での応答時間の機能を開発した後、この方法で結果の正しさを確認することもできます。もちろん、2つの実装方法は完全に異なるため、SDKで得られる時間はチャールズと正確に等しくはありませんが、それらの違いは妥当な範囲内でなければなりません。この側面については、以下で詳しく説明します。
上記の紹介を通して、アイデアを簡単に考えることができます。フック要求が発行されたときに関数を介して、リクエスト時間を記録し、iOS SDKにコールバック応答をフックし、終了時間を記録し、差を計算してこのリクエストの応答時間を取得します。 Tingyunの一般的な考えにも同じことが言えますが、注意が必要な詳細はたくさんあります。次に、その特定の実装計画について詳しく説明しましょう。
リスニングクラウドは、 _nbs_hook_NSURLSessionTask関数の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フレームワークでは、一部のクラスは実際にはNSDictionaryやNSArrayなどのクラスクラスターであることがわかっています。 NSURLSessionTaskはクラスファミリでもあり、継承チェーンはシステムバージョンによって異なるため、明らかにNSURLSessionTaskクラスを直接フックすることはできません。ここでは、 ephemeralSessionConfigurationメソッドを通じてはかないセッションを構築するために、巧妙な方法が採用されています。デフォルトセッションに似ていますが、ディスクにデータは保存されず、すべてのキャッシュ、Cookie、資格情報などがRAMに保存され、セッションに関連付けられています。このようにして、セッションが無効なときに自動的にクリアされます。次に、この短いセッションを通じて、セッションオブジェクトが作成され、タスクオブジェクトが最終的に構築され、実際のクラスがこのタスクオブジェクトを介して取得されます。
上記の巧妙なアプローチは、もともとTingyunによって作成されたものではありません。実際には、Afnetworkingのアプローチを指します。 AfnetWorkingに通知を追加するために、Hook NSURLSessionTaskのresumeとsuspend方法も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は、システムバージョンに応じて、Hook __NSCFLocalSessionTask 、NSURLSESSIONTASK、__NSCFURLSESSIONTASKと区別します。個人的には、リスニングクラウドの実装は、フレックスのハードコーディングよりもエレガントであると感じています。 __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を交換する実装は、blockの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: Methodを呼び出すネットワークリクエストの場合、 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);
}