
APM은 점점 더 인기가 높아지고 있지만 모든 크기의 전문 APM 제조업체는 비가 내린 후 버섯처럼 등장했으며 시장에는 APM에 많은 기술 기사가 많이 있습니다. 대부분은 단순한 맛 일 뿐이며 구현 세부 사항에 깊이 파고 들어 가지 않습니다. 이 기사는 SDK의 특정 구현 세부 사항을 분석하여 잘 알려진 APM 제조업체의 iOS SDK의 내부 작업 원칙을 노출시키는 것을 목표로합니다. 이 기사를 읽기 전에 독자들은 저자처럼 APM SDK의 구현 세부 사항에 대해 궁금한 점이 있다고 생각합니다. 다행히도, 당신이 읽고있는 기사는 당신이 APM의 진정한 맥락을 단계별로 밝히게합니다. 이 기사에서 분석 된 APM SDK에는 Tingyun , OneApm 및 Firebase Performance Monitoring 등이 포함됩니다. 저자는 약간 재능 있고 지식이 풍부합니다. 실수가 있다면, 그는 그것을 다시 인쇄하고 더 완벽하게 만들기 위해 그들을 바로 잡지 않을 것입니다.
이 기사에서 분석 된 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 함수는 MainBundle의 모든 UIViewController 의 서브 클래스라고 기본적으로 추론 할 수 있습니다. 실제로, LLDB를 통해 함수 호출이 완료된 후 어셈블리 라인을 중단하면 반환 된 배열은 실제로 UIViewController 의 서브 클래스임을 알 수 있습니다. 다음 if 문은 r12 레지스터가 nil 아니고 r12 레지스터의 count 실행 if 전에 0과 같지 않다고 결정합니다. r12 레지스터는 UIViewController 서브 클래스의 배열 인 _subMetaClassNamesInMainBundle_c 함수의 반환 값을 저장합니다.
_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 함수의 구현은 명백합니다. mainBundle 실행 가능한 경로의 모든 클래스의 이름을 얻는 것은 objc_copyClassNamesForImage 호출하는 것 이상입니다. 세트 수는 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 루프는 실제로 UIViewController 서브 클래스의 배열을 가로 지르고 있습니다. Traversal 동작은 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 의 루틴에서 주요 로직은 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_implementationWithBlock 메소드를 통해 IMP 함수 포인터를 얻습니다. _nbs_Swizzle_orReplaceWithIMPs 메소드 교환을 구현하는 함수이며 매개 변수는 다음과 같습니다. arg2 는 ViewController 의 클래스입니다. @selector(viewDidLoad) 는 viewDidLoad 의 선택기입니다. var_C0 은 nbs_%s_viewDidLoad 의 선택기이며, r14 두 번째 __NSConcreteStackBlock 의 임프입니다. var_D0 은 첫 번째 __NSConcreteStackBlock 의 임프입니다.
hook_viewDidLoad: 대략 명확하지만 여기에는 두 개의 IMP를 직접 교환하지 않고 먼저 두 블록을 구성한 다음 두 블록의 IMP를 교환하는 이유가 있습니다. 그 이유는 ViewController 의 상위 클래스의 결과, 즉 class_getSuperclass 의 결과를 교환 메소드의 매개 변수로 전달해야하기 때문입니다. 이러한 방식으로, 두 교환 된 선택기에 의해 서명 된 매개 변수의 수는 일치하지 않으며,이 문제는 블록을 구성하여 영리하게 해결해야합니다. 실제로, 첫 번째 __NSConcreteStackBlock nbs_jump_viewDidLoad:superClass: _priv_NBSUIHookMatrix 의 메소드. 앞에서 언급 했듯이이 방법의 매개 변수에는 superClass 가 있습니다. 이 매개 변수가 필요한 이유는 나중에 소개하겠습니다.
두 번째 __NSConcreteStackBlock nbs_jump_viewDidLoad:superClass: 메소드? Hopper의 Remove potentially dead code 옵션을 선택 취소하면 코드는 다음과 같습니다.
void +[_priv_NBSUIAgent hook_viewDidLoad:](void * self, void * _cmd, void * arg2) {
rsi = _cmd;
rdi = self;
r12 = _objc_msgSend;
rax = [_priv_NBSUIHookMatrix class];
rsi = @selector(nbs_jump_viewDidLoad:superClass:);
rdi = rax;
var_D8 = _nbs_getInstanceImpOf();
rdi = arg2;
rsi = @selector(viewDidLoad);
var_D0 = _nbs_getInstanceImpOf();
rbx = class_getName(arg2);
r14 = class_getSuperclass(arg2);
LODWORD(rax) = 0x0;
rax = [NSString stringWithFormat:@"nbs_%s_viewDidLoad", rbx];
rax = [rax retain];
var_B8 = rax;
var_C0 = NSSelectorFromString(rax);
var_60 = 0xc0000000;
var_5C = 0x0;
var_58 = ___37+[_priv_NBSUIAgent hook_viewDidLoad:]_block_invoke;
var_50 = ___block_descriptor_tmp;
var_48 = var_D8;
var_40 = @selector(viewDidLoad);
var_38 = var_D0;
var_30 = r14;
r12 = objc_retainBlock(__NSConcreteStackBlock);
var_D0 = imp_implementationWithBlock(r12);
r13 = _objc_release;
rax = [r12 release];
var_A8 = 0xc0000000;
var_A4 = 0x0;
var_A0 = ___37+[_priv_NBSUIAgent hook_viewDidLoad:]_block_invoke_2;
var_98 = ___block_descriptor_tmp47;
var_90 = rbx;
var_88 = var_D8;
var_80 = @selector(viewDidLoad);
var_78 = r14;
var_70 = arg2;
rbx = objc_retainBlock(__NSConcreteStackBlock);
r14 = imp_implementationWithBlock(rbx);
rax = [rbx release];
rax = _nbs_Swizzle_orReplaceWithIMPs(arg2, @selector(viewDidLoad), var_C0, r14, var_D0);
rax = [var_B8 release];
rsp = rsp + 0xb8;
rbx = stack[2047];
r12 = stack[2046];
r13 = stack[2045];
r14 = stack[2044];
r15 = stack[2043];
rbp = stack[2042];
return;
}
_nbs_getInstanceImpOf 의 코드를 살펴 보겠습니다.
void _nbs_getInstanceImpOf() {
rax = class_getInstanceMethod(rdi, rsi);
method_getImplementation(rax);
return;
}
_nbs_getInstanceImpOf 함수의 함수는 매우 분명합니다. rdi 클래스에서 rsi 선택기의 임프를 얻기 위해 리더는 hook_viewDidLoad: method에서 _nbs_getInstanceImpOf 두 번 호출되었음을 알 수 있습니다. 첫 번째 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 레지스터는 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는 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: 콜백,이 알림은 상태 표시 줄의 window 생성 될 때 트리거되었습니다. 이 구현은 약간 까다로워지며 Apple이 향후 통화 타이밍을 조정할 수는 없습니다.
아래는 UIWindowDidBecomeVisibleNotification 의 공식 설명입니다.
Ui -Window 객체가 보이게 될 때 게시됩니다. 알림 객체는 보이는 창 객체입니다. 이 알림에는 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 네트워크 프로그래밍에서 사용하는 API를 사용하여 기본 네트워크 요청을 대상으로하는 것입니다. 두 번째는 NSURLProtocol 상속하여 네트워크 요청을 구현하는 것입니다. 이는 주로 UIWebView의 네트워크 요청을 대상으로합니다.
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: Methods의 구현을 대체하는 것을 볼 수 있습니다. 이 방법이 나중에 푹 빠지는 이유를 설명하겠습니다.
모든 후크 메소드 구현은 _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에서 응답 시간의 기능을 개발 한 후에는 이러한 방식으로 결과의 정확성을 확인할 수도 있습니다. 물론 SDK에서 얻은 시간은 찰스와 정확히 같지 않습니다. 두 구현 방법은 완전히 다르기 때문에 이들 사이의 차이는 합리적인 범위 내에 있어야합니다. 이 측면은 아래에서 자세히 설명합니다.
위의 소개를 통해 아이디어를 쉽게 생각할 수 있습니다. 후크 요청이 발행 될 때의 기능을 통해 요청 시간을 기록한 다음 iOS SDK에서 콜백 응답을 연결하고, 종료 시간을 기록하고, 차이를 계산 하여이 요청의 응답 시간을 얻습니다. Tingyun의 일반적인 아이디어에 대해서도 마찬가지이지만주의를 기울여야 할 많은 세부 사항이 있습니다. 다음에 특정 구현 계획에 대해 자세히 논의합시다.
Listening Cloud는 요청의 시작을 기록하는 목적을 달성하기 위해 _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;
}
위의 pseudocode를 다음과 같이 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 Framework에서 일부 클래스는 실제로 NSDictionary 및 NSArray 와 같은 클래스 클러스터라는 것을 알고 있습니다. NSURLSessionTask 는 또한 클래스 패밀리이며 상속 체인은 시스템 버전에서 다르므로 NSURLSessionTask 클래스를 직접 연결할 수는 없습니다. ephemeralSessionConfiguration 방법을 통해 임시 세션을 구축하기 위해 여기에 영리한 방법이 채택됩니다. 기본 세션과 유사하지만 디스크에 데이터가 저장되지 않으며 모든 캐시, 쿠키, 자격 증명 등이 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에서도 구현되지만 구현에는 약간의 차이가 있습니다. 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: Method를 호출하는 네트워크 요청의 경우 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);
}