
Bien que l'APM devienne de plus en plus populaire, les fabricants d'APM professionnels de toutes tailles ont émergé comme des champignons après une pluie, et il y a aussi de nombreux articles techniques sur APM sur le marché, la plupart d'entre eux ne sont qu'un goût simple et ne se penchent pas profondément dans les détails de la mise en œuvre. Cet article vise à exposer le principe de travail interne du SDK iOS des fabricants APM bien connus en analysant les détails de mise en œuvre spécifiques du SDK. Je crois qu'avant de lire cet article, les lecteurs étaient également curieux de savoir les détails de la mise en œuvre du SDK APM, tout comme l'auteur. Heureusement, l'article que vous lisez vous amènera à découvrir le véritable contexte d'APM étape par étape. Les SDK APM analysés dans cet article comprennent la surveillance des performances Tingyun , ONEAPM et Firebase , etc. L'auteur est un peu talentueux et compétent. S'il y a des erreurs, il ne les corrigera pas afin de les réimprimer et de les rendre plus parfaits.
La version du SDK Tingyun analysé dans cet article est 2.3.5, qui est légèrement différente de la dernière version. Cependant, je lis grossièrement le code de la nouvelle version, mais la différence n'est pas grande et n'affecte pas l'analyse.
La surveillance du rendu des pages semble très simple, mais il y aura toujours de nombreux problèmes dans le processus de développement réel. Ce qui est plus facile à penser, ce sont plusieurs méthodes clés du cycle de vie des pages de crochet, telles que viewDidLoad , viewDidAppear: etc., pour calculer le temps de rendu de la page et enfin trouver la page lente. Cependant, si vous commencez vraiment à y parvenir à travers les idées ci-dessus, vous rencontrerez des difficultés. Comment puis-je accrocher le cycle de vie de toutes les pages appliqués dans le SDK APM? Et si j'essaye d'accrocher UIViewController ? La méthode Hook UIViewController n'est évidemment pas possible car elle ne fonctionne que sur UIViewController , et la plupart des contrôleurs de visualisation dans l'héritage de l'application de UIViewController , donc cette méthode n'est pas possible. Mais le SDK Tingyun peut être mis en œuvre. La logique du crochet de page est principalement implémentée dans la classe _priv_NBSUIAgent . Ce qui suit est la définition de la classe _priv_NBSUIAgent , parmi laquelle hook_viewDidLoad et d'autres méthodes sont des indices.
@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
}
Tournons d'abord notre attention vers une autre méthode plus suspecte: hookSubOfController , l'implémentation spécifique est la suivante:
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;
}
De la dénomination de _subMetaClassNamesInMainBundle_c et du paramètre entrant "uiViewController", il peut être essentiellement déduit que cette fonction C est une sous-classe de tous UIViewController dans MainBundle. En fait, si vous avez la rupture de la chaîne de montage une fois l'appel de fonction terminé via LLDB, vous constaterez que le tableau renvoyé est en effet une sous-classe d' UIViewController . L'instruction if suivante détermine que le registre r12 n'est pas nil et count du registre r12 n'est pas égal à 0 avant que la logique if ne soit exécutée. Le registre r12 stocke la valeur de retour de la fonction _subMetaClassNamesInMainBundle_c , qui est le tableau de UIViewController .
_subMetaClassNamesInMainBundle_c est le suivant:
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;
}
Le sous-programme loc_10001db4d dans _subMetaClassNamesInMainBundle_c appelle la fonction _classNamesInMainBundle_c , et le code de fonction est le suivant:
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;
}
L'implémentation de la fonction _classNamesInMainBundle_c est évidente, ce n'est rien de plus que d'appeler objc_copyClassNamesForImage pour obtenir les noms de toutes les classes mainBundle . Le nombre d'ensembles est attribué à la variable outCount , et l'appelant peut utiliser outCount pour le traverser.
static inline char ** WDTClassNamesInMainBundle ( unsigned int *outCount) {
NSString *executablePath = [[ NSBundle mainBundle ] executablePath ];
char **classNames = objc_copyClassNamesForImage ([executablePath UTF8String ], outCount);
return classNames;
} Si vous ne vous souciez pas des détails, la mise en œuvre de la fonction _subMetaClassNamesInMainBundle_c est également très claire, ce qui est de traverser la valeur de retour de la fonction objc_copyClassNamesForImage . Si l'élément est une sous-classe d' UIViewController , metaClass de la classe est obtenu et ajouté au tableau variable var_38 .
Ensuite, concentrons-nous sur l'instruction do-while LOOP à l'intérieur. L'instruction pour le jugement de boucle est var_98 + 0x1 < rax . var_98 attribue le registre rdx au début de la boucle, et le registre rdx est initialisé à 0 en dehors de la boucle, donc var_98 est le compteur, et le registre rax est count attribuée au registre r12 . Sur la base de cela do-while traverse en fait le tableau de UIViewController . Le comportement de traversée consiste à échanger initialize et nbs_jump_initialize: Méthodes via _nbs_Swizzle_orReplaceWithIMPs .
Le code pour nbs_jump_initialize est le suivant:
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;
}
Le code de nbs_jump_initialize est un peu long, mais à partir de la routine de loc_1000501a3 , on peut observer que la logique principale exécutera trois méthodes, hook_viewDidLoad , hook_viewWillAppear et hook_viewDidAppear , en terminant ainsi ces trois méthodes des sous-classes UIViewController .
Tout d'abord, utilisez hook_viewDidLoad: Méthode comme exemple à expliquer. Le code suivant peut être un peu obscur et nécessite une analyse minutieuse.
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: Le paramètre arg2 dans la méthode est la classe du ViewController pour accrocher, obtenir le nom de classe de arg2 et l'affecter au registre rbx , puis utiliser rbx pour construire la chaîne nbs_%s_viewDidLoad , telle que nbs_XXViewController_viewDidLoad , obtenir le sélecteur de la chaîne et l'attribuer à var_C0 . __NSConcreteStackBlock dans les phrases suivantes est l'objet de bloc de la pile de stockage créée. Ce bloc obtiendra ensuite le pointeur de la fonction IMP via la méthode imp_implementationWithBlock . _nbs_Swizzle_orReplaceWithIMPs est une fonction qui implémente l'échange de méthode, et les paramètres sont: arg2 est la classe de ViewController ; @selector(viewDidLoad) est le sélecteur de viewDidLoad ; var_C0 est le sélecteur de nbs_%s_viewDidLoad , r14 est le SIM du deuxième __NSConcreteStackBlock ; var_D0 est le SIM du premier __NSConcreteStackBlock .
Toute la logique de hook_viewDidLoad: est à peu près claire, mais il y a une question ici pourquoi ne pas échanger directement deux IMPS, mais construire d'abord deux blocs, puis échanger les imps de deux blocs? La raison en est que le résultat de la classe parent de ViewController , c'est-à-dire class_getSuperclass , doit être transmis en tant que paramètres à la méthode échangée. De cette façon, le nombre de paramètres signés par les deux sélecteurs échangés est incohérent, et ce problème doit être résolu intelligemment en construisant un bloc. En fait, le premier __NSConcreteStackBlock exécute nbs_jump_viewDidLoad:superClass: méthode de _priv_NBSUIHookMatrix . Comme mentionné précédemment, il y a superClass dans les paramètres de cette méthode. Quant à savoir pourquoi ce paramètre est nécessaire, je le présenterai plus tard.
Pourquoi le deuxième __NSConcreteStackBlock exécute-t-il nbs_jump_viewDidLoad:superClass: Méthode? Décochez l'option Remove potentially dead code de Hopper, le code est le suivant:
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;
}
Regardons le code de _nbs_getInstanceImpOf :
void _nbs_getInstanceImpOf() {
rax = class_getInstanceMethod(rdi, rsi);
method_getImplementation(rax);
return;
}
La fonction de la fonction _nbs_getInstanceImpOf est très évidente. Pour obtenir le SIM du sélecteur rsi dans rdi , les lecteurs constatera que _nbs_getInstanceImpOf a été appelé deux fois dans hook_viewDidLoad: Méthode. Le premier rdi est la classe _priv_NBSUIHookMatrix , rdx est @selector(nbs_jump_viewDidLoad:superClass:) , le deuxième rdi est ViewController , rdx est @selector(viewDidLoad) .
Ensuite, regardons le premier __NSConcreteStackBlock , ce qui signifie le bloc qui appellera nbs_jump_viewDidLoad:superClass: Le code est le suivant:
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;
}
Le registre r8 est le SIM de nbs_jump_viewDidLoad:superClass: et ce code appelle ce problème. Les paramètres de la fonction IMP sont les mêmes que 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;
}
Le temps de démarrage est expliqué avec le SDK de surveillance des performances Firebase comme exemple. Le SDK FPM est utilisé comme abréviation pour le décrire. Le SDK FPM met en œuvre des statistiques sur le temps de démarrage froid, et la logique principale est implémentée dans FPRAppActivityTracker .
Regardez d'abord la méthode +load de la classe, et le code de décompilation est le suivant:
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;
}
De toute évidence, _appStartTime est une instance NSDate statique utilisée pour enregistrer l'heure de début de l'ensemble du démarrage de l'application, de sorte que le SDK FPM marque l'heure de début du démarrage de l'application dans la +load de FPRAppActivityTracker . Les lecteurs qui comprennent la méthode +load doivent savoir que la méthode est une méthode de crochet avant que main ne soit appelée. L'heure exacte est lorsque l'image est chargée à l'exécution et que la méthode +load est prête, puis +load sera lancée pour être appelée. De plus, différents types de méthodes +load sont également liés à l'ordre de fichier des Build Phases->Compile Sources . Nous pensons que ceux-ci n'ont pas d'impact significatif sur les statistiques du temps de démarrage.
Ensuite, la notification de UIWindowDidBecomeVisibleNotification est enregistrée. Cette notification est déclenchée lorsque UIWindow est activé et affiché sur l'interface. Les lecteurs peuvent enregistrer cette notification, puis imprimer l'objet de notification avec LLDB. L'exemple est le suivant:
NSConcreteNotification 0x7fc94a716f50 {name = UIWindowDidBecomeVisibleNotification; object = <UIStatusBarWindow: 0x7fc94a5092a0; frame = (0 0; 320 568); opaque = NO; gestureRecognizers = <NSArray: 0x7fc94a619f30>; layer = <UIWindowLayer: 0x7fc94a513f50>>}
La première fois que j'ai reçu la notification UIWindowDidBecomeVisibleNotification était antérieure à - application:didFinishLaunchingWithOptions: Rappel, cette notification a été déclenchée lorsque window de la barre de statut a été créée. Cette implémentation semble un peu délicate et ne peut garantir qu'Apple ajustera le calendrier de l'appel à l'avenir.
Vous trouverez ci-dessous la description officielle de UIWindowDidBecomeVisibleNotification .
Publié lorsqu'un objet UiWindow devient visible. L'objet de notification est l'objet de fenêtre qui est devenu visible. Cette notification ne contient pas de dictionnaire UserInfo. La commutation entre les applications ne génère pas de notifications liées à la visibilité pour Windows. Les changements de visibilité de la fenêtre reflètent les modifications de la propriété cachée de la fenêtre et ne reflètent que la visibilité de la fenêtre dans l'application.
Ce qui suit est la méthode de gestion des notifications. J'ai restauré la méthode pour objectif-C pseudo-code, qui peut comparer le pseudo-code décompilé.
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];
}
La méthode déconnectera la notification UIWindowDidBecomeVisibleNotification à la fin, car la notification sera appelée plusieurs fois, et nous n'en avons besoin que pour l'exécuter une fois. Tout d'abord, appelez la méthode -startAppActivityTracking pour commencer à suivre l'activité de l'application. Cette méthode sera discutée en profondeur plus tard.
Tout d'abord, nous sommes clairs que les demandes de réseau dont nous discutons ici n'ont pas d'instructions spéciales pour se référer aux demandes HTTP. Tingyun SDK utilise principalement deux méthodes pour implémenter la surveillance du réseau: la première consiste à accrocher l'API utilisé par la programmation réseau iOS, qui cible principalement les demandes de réseau natives; La seconde consiste à hériter de NSURLProtocol pour implémenter les demandes de réseau, qui cible principalement les demandes de réseau dans UIWebView.
Le SDK accroche l'API pour la construction NSURLSessionDataTask , NSURLSessionUploadTask et NSURLSessionDownloadTask dans toutes les demandes de réseau. La logique de Hook est dans la fonction C _nbs_hook_NSURLSession , et le pseudo-code est le suivant:
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 est la fonction C qui écoute le cloud pour implémenter la méthode Swizzling.
À partir du code, nous pouvons voir qu'en plus d'utiliser _nbs_Swizzle pour NSURLSessionDataTask , les API NSURLSessionUploadTask et NSURLSessionDownloadTask mentionnées ci-dessus, il remplace également la méthode de sessionWithConfiguration:delegate:delegateQueue: Méthode. J'expliquerai pourquoi cette méthode est accrochée plus tard.
Toutes les implémentations de la méthode de crochet sont définies dans la classe _priv_NSURLSession_NBS .
Le code central de nbs_dataTaskWithRequest:completionHandler: est le suivant:
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 est un modèle de paramètres de performance liés aux demandes HTTP dans le SDK. Cette structure de classe est la suivante:
@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:
}
Le tableau suivant répertorie les significations de certains attributs clés:
| propriété | signification |
|---|---|
| TM_PNT_SEND | Demander l'heure de début |
| TM_DUR_DNS | Temps de résolution DNS |
| TM_DUR_CNNCT | Temps d'établissement de la connexion TCP |
| tm_dur_firstp | Temps de premier pack |
| TM_DUR_SSL | Temps de poignée de main SSL |
| code de statut | Code d'état HTTP |
Le temps de réponse est un bon indicateur quantitatif, qui peut être utilisé pour mesurer le temps d'attente pour que les utilisateurs demandent des services. Il est généralement défini comme la période où l'utilisateur envoie une demande et le contenu de réponse sur le serveur atteint le client.
Le chiffre suivant est une explication détaillée des demandes HTTP

D'après la figure ci-dessus, nous pouvons observer que le temps de réponse comprend le temps de résolution du nom de domaine DNS, le temps de connexion établi avec le serveur, le temps de traitement du serveur et le moment où la réponse arrive au client.
Si vous utilisez Charles pour intercepter les demandes HTTP, vous pouvez afficher les données de temps de réponse dans l'onglet Colonne de synchronisation. Duration de la figure ci-dessous représente le temps de réponse total de la demande, qui comprend également DNS (temps de résolution du nom de domaine DNS), Connect (temps d'établissement de connexion) et SSL Handshake (temps de poignée de main SSL) mentionné ci-dessus. Parce que cette demande est une demande HTTP, le champ de SSL Handshake est laissé vide.

En fait, après avoir développé la fonctionnalité du temps de réponse dans le SDK, nous pouvons également vérifier l'exactitude des résultats de cette manière. Bien sûr, le temps obtenu dans le SDK n'est pas exactement égal à Charles, car les deux méthodes de mise en œuvre sont complètement différentes, mais la différence entre elles devrait être dans une plage raisonnable. Cet aspect sera discuté en détail ci-dessous.
Grâce à l'introduction ci-dessus, nous pouvons facilement penser à une idée: grâce à la fonction lorsque la demande de crochet est émise, enregistrez l'heure de la demande, puis accrochez la réponse de rappel dans le SDK iOS, enregistrez l'heure de fin et calculant la différence pour obtenir le temps de réponse de cette demande. Il en va de même pour l'idée générale de Tingyun, mais il y a de nombreux détails à qui il faut prêter attention. Discutons ensuite de son plan de mise en œuvre spécifique.
Le cloud d'écoute est la méthode du resume de NSURLSESSESSADAK dans la fonction _nbs_hook_NSURLSessionTask pour atteindre le but d'enregistrer le début de la demande.
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;
}
Restaurez le pseudocode ci-dessus vers le code objectif-C comme suit:
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];
}
Nous savons que dans le cadre de Foundation , certaines classes sont en fait des grappes de classes, telles que NSDictionary et NSArray . NSURLSessionTask est également une famille de classe, et les chaînes d'héritage sont différentes dans différentes versions système, donc évidemment, vous ne pouvez pas accrocher directement la classe NSURLSessionTask . Une méthode intelligente est adoptée ici pour construire une session éphémère via la méthode ephemeralSessionConfiguration . Il est similaire à la session par défaut, mais aucune donnée n'est stockée sur le disque, et tous les caches, cookies, informations d'identification, etc. sont enregistrés en RAM et associés à la session. De cette façon, ils seront automatiquement effacés lorsque la session n'est pas valide. Ensuite, grâce à cette brève session, un objet de session est créé et l'objet de tâche est finalement construit et la classe réelle est obtenue via cet objet de tâche.
L'approche intelligente ci-dessus n'est pas créée à l'origine par Tingyun. Il se réfère en fait à l'approche d'Afnetworking. Afin d'ajouter des notifications dans Afnetworking, resume et suspend de Hook NSURLSessionTask sont également mises en œuvre dans 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_copyMethodListsera appelée dans la méthode_nbs_slow_isClassItSelfHasMethodpour obtenir la liste de méthodes de cette classe. Notez que la liste des méthodes obtenue par cette méthode ne contient pas les méthodes de la classe parent, de sorte que_nbs_slow_isClassItSelfHasMethodest de juger si la classeclselle-même contient@selector(resume).
En fait, la logique ci-dessus est également implémentée dans la bibliothèque open source Flex, mais il existe de légères différences dans la mise en œuvre. Flex le distinguera du crochet __NSCFLocalSessionTask , nsurlSessionTask et __nsfurlsessiontask selon la version système. Je pense personnellement que la mise en œuvre du cloud d'écoute est plus élégante que le codage dur de Flex . Parce que __NSCFLocalSessionTask et __NSCFURLSessionTask sont des classes privées, la division et l'épissage sont adoptées pour éviter le rejet d'audit.
+ (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);
});
}
La mise en œuvre du remplacement du resume ci-dessus est implémentée via imp_implementationWithBlock , et le bloc remplacé est le suivant:
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]);
}
}
}
Dans le pseudo-code ci-dessus, la logique non pertinente dans ___nbs_hook_NSURLSessionTask_block_invoke est ignorée. Vous pouvez voir qu'un horodatage est généré et que l'horodatage est utilisé comme paramètre d'entrée [rbx startWithIP:0x0 DNSTime:var_30 atTimePoint:r8 withObject:r9] méthode. rbx est une instance de _priv_NBSHTTPTransaction , et cette instance est obtenue via l'objet associé de NSURLSessionDataTask . La logique de la création _priv_NBSHTTPTransaction et de la définition de l'objet associé se trouve dans le -[_priv_NSURLSession_NBS nbs_dataTaskWithRequest:completionHandler:] Méthode.
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:] la méthode attribuera l'heure du paramètre à sa propriété 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));
}
Bien sûr, en plus du -[_priv_NSURLSession_NBS nbs_dataTaskWithRequest:completionHandler:] Méthode, la méthode suivante contient également cette logique:
nbs_downloadTaskWithRequest:nbs_downloadTaskWithRequest:completionHandler:nbs_downloadTaskWithResumeData:completionHandler:nbs_uploadTaskWithRequest:fromData:completionHandler:nbs_uploadTaskWithRequest:fromFile:completionHandler:nbs_uploadTaskWithRequest:fromFile:nbs_uploadTaskWithRequest:fromData:nbs_uploadTaskWithStreamedRequest: Le temps de réponse final est calculé dans la méthode finishAt et l'a attribué à 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];
}
}
Pour les demandes de réseau initiées en appelant dataTaskWithRequest:completionHandler: Méthode, le rappel terminé est dans completionHandler , de sorte que finishAt doit être appelée dans le rappel terminé. Des méthodes similaires incluent ___72-[_priv_NSURLSession_NBS nbs_downloadTaskWithRequest:completionHandler:]_block_invoke , ___79-[_priv_NSURLSession_NBS nbs_uploadTaskWithRequest:fromData:completionHandler:]_block_invoke et autre méthodes.
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];
}
Cependant, pour les demandes de réseau qui appellent dataTaskWithRequest: Méthode, vous devez accrocher URLSession:task:didCompleteWithError: Méthode de 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);
}