文章将近50000字,拆分为一、二。Github 上完好文章阅览体会更佳,请点击访问 Github

APM 是 Application Performance Monitoring 的缩写,监督和办理软件运用程序的功用和可用性。运用功R % N y p P x g用办理对一个运用的持续安稳运转至关重要。所以这篇文章就从一个 iOS App 的功用办理的纬度谈谈怎么准确监控以及数据怎? . y v R么上报等技能点

App 的功用问题是影响用户体会的重要要素之一。功用问题首要包含:Crash、网络恳求过错或许超时、UI 呼应速度慢、主线程卡顿、CPU 和内存运用率高、耗电量大等等。大多数的问题原a B @ B t 3因在于开发者% P D h过错地运用了线程锁、体系函数、编程标准问题、数据结构等等。处理问题的要害在于尽早的发现和定位问题。

本篇文– [ e o 6 o章侧重总结了 APM 的原因以及怎么搜集数据。APM 数据搜集后结合数据上报机制,依照必定战略上传数据到服务端。服务端消费这些信息并7 ^ q 0 5 z产出陈述。请结合姊妹篇, 总! A t C 3 s : G结了怎么打造一款灵敏e ] P a W可装备、功用强壮的数据上报组件。

一、卡顿监控

卡顿问题,便是在主线程上无法响运用户交互的问题。影响着用户的直接体会,所以针对 App 的卡顿监控是 APM 里边重要的一环。

FPS(fraC l % .me per second)每秒钟的帧改写次数,iPhone 手机以 60 为最佳,iPad 某些类型是 120,也是作为卡顿监控的一项参阅参数,为什么说是参阅参数?由于它不准确。先说P % j n Q $说怎么获取到 FPS。CADisplayLink 是一个体系守时器,会以帧改写频率相同的速率来改写视图。 [CADisplayLink dispW V } ] i X q NlayLinkWithTarget:self sela z q ! 6 m Zector:@sele9 Z 0 . . 9 ; actor(###:)]。至于为什么不准咱们来看看下面的示例代码

_displayLink = [CADk n ! (isplayLink displayLinkWithTarget+ J ^ 9 5 K R:self selector:@/ J S o / @ J @selector(p_dr e { z BisplayLinkTick:j w 9 _ _ o @ ()];
[_displayLink setPaused:YES];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop]X ] s /  C B forMode:NSRunLoopCommonModes]g = N h 6;

代码所示,CADisplayLink+ o ` 目标是被增加到指定的 RunLoop 的某个 Mode 下。所以仍是 CPU 层L g ~面的操作,卡顿的体会– 9 f ] . . S是整个图画烘托的成果:CPU + GPUb { m x M w。请持续往下看

1. 屏幕制作原理

带你打造一套 APM 监控系统(一)

讲讲旧式的 CRT 显现器的原理。 CRT 电子枪依照上面办法,从上到下一行行扫描,扫面完结后显现器就呈现一帧画面,随后电子枪回到初始方位持续下一次B f i 5 ? % * v扫描。为了把显现器的显现进程和体系的视频操控器进行同步,显现器(或许其他硬件)会用硬件时钟产生一系列的守时信号。当电子枪换到新的一行,预备进行扫描时,显现器会宣布一个水平同步信号(horizonal synch= z K *ronization),简称 HSync;当一帧画面制作完结后,电子枪恢复到F F ~ W *原位,预备画下一E = i帧前,显现器会宣布一个笔直同步信号(Vertical synch6 ) ~ @ F Lronization),简称 VSync。显现器一般以固定的频率进行改写x _ * N $ _,这个固定的改写频率便是 V, j ) F h A @ $Sync 信号产生的频率。尽管现在的显现器基本都是液晶显现屏,可是原理坚持不变。w j D T #

带你打造一套 APM 监控系统(一)

一般,屏幕上一张画面的显现是由 CPU、GPU 和显现器是依照上图的办法协同作业的。CPU 依据工v 5 U h M =程师写的代码核算好需, g – T z 9 9求实际的内容(比方视图创立、} 6 E + % 2布局核算、图片解码、文本制作等),然后把核算成果提交到 GPU,GPU 担任图层组成、纹理烘托,随后 GPU 将烘托成果提交到帧缓冲区。随后视频操控器会依照 VSync 信号逐行读取帧缓冲区的] P g i 9 ! 0数据,经过数模转换传递给显现器x ~ 5显现。

在帧缓冲区只要一个的状况下,帧缓冲区的读取和改写都存在功率问题,为了处理功率问题,显现体系会引进2个缓冲区,即双缓冲机制。在这种状况下,GPU 会预先烘托好一帧放入帧缓冲区,让视频操控器来读取,当下一帧烘托好后,GPU 直接把视频操控器的指针指向第二个缓冲区。提高了功率。

现在来看,双缓冲区提高了功率,可是带来了新的问题:当视频操 @ X t ] $ b &控器还未读取完结时,即屏幕内容显现了部分,GPU 将新烘托好的一帧提交到另一个帧缓冲区并把视频操控器的指针指向新的帧缓冲区,视频操控器就会把新的一帧数据的下半段显现到屏幕上,形成画面撕裂的状况。

为了处理这个问题,GPU 一般有一个机制w t % 9 r 7 C 8 t叫笔直同步信号(V-Sync),当敞开笔直同步信号后,GPU 会比及视频操控器发送 V-Sync 信号后,才进行新的一帧的烘托和帧缓冲区的更新。这样的几个机制处理了画面撕裂的状况,也增加了画面流畅度。但需求更多的核算资源

带你打造一套 APM 监控系统(一)

答疑

或许有些人会看到「当敞开笔直同Z / x [ s D 3 3步信号后,GPU 会X # B y # Z G 3比及视频操控器发送 V-Sync 信号后,才进行新的一帧的烘托和帧缓冲0 B S Y y ~区的更新」这儿会想,GPx z 5 % ] u %U 收到 V-Sync 才进行新的一帧烘托j x +和帧缓冲区的更新,那是不是双缓冲区就失掉含义了?

想象一个显现器显现榜首帧图画和第二帧图画的进程。首要在双缓冲区的状况下,GPU 首要烘托好一帧图画存入到帧缓冲区,然后让视频操控器的指针直接直接这个缓冲区,显现榜首帧图画。榜首帧图画的内容显现完结后,视频操控器发送 V-Sync 信号,GPU 收到 V-Sync 信号后烘托第二帧图画并将视频操控器的指针指向第二个帧缓冲区。

看上去第二帧图画是在等榜首帧显现后的视频操控器发送 V-Sync 信号。是吗?真是这样的吗? 想啥呢,当然不是。 否v % a ~ q [ K U则双缓冲区就没有存在的含义了

揭秘。请看下图

带你打造一套 APM 监控系统(一)

当榜首次 V-Sync 信号到来时,先烘托好一帧图画放到帧缓冲区,可是不展现,当收到第二个 V-S, t S f K p = T Fync 信号后读取榜首次烘托好的成果(视频操控器的指针指向榜首个帧缓冲区),并一起烘托新的一帧图画并将成果存入第二个帧缓冲区,等收到第三个 V-Sync 信号后,读y r j $ D取第二个帧缓冲区的内1 4 A l Q容(视频操控器的指针指向第二个帧缓冲区),并开端I w c W第三帧图画的烘托并送入榜首个帧缓冲区,依 f G ?次不断循环往: f K @ N q h | 2复。

请检z | l ! X查材料:Multiple buffe3 i $ ! S 6 Mring

2. 卡顿产生的原因

带你打造一套 APM 监控系统(一)

VSync 信号到来后,体系图形O u & –服务会经过 CADisplayLink 等机制告诉 App,App 主线程开端在 CPU 中m j / A * a – )核算显现内容(视图创立、布局核算、图片解码、文本制作等)。然后将核算的内容提交到 GPU,GPU 经过图层的变换、组成、烘托,随后 GPU 把烘托成果提交到帧缓冲区,等候下一次 VSync 信[ $ o R U 4 n ?号到来再显现* u q ! f = E之前烘托好的成果。在笔直同步机制的状况下,假设在一个 VSync 时刻周期内,CPU 或许 GPU 没有完结内容的提交,就会形成该帧的丢弃,等候下一次时机再显现,这时分屏幕上仍是之前烘托的图画,所以这便是 CPU、GPU 层面界面卡顿的原因。

现在 iOS 设备有双缓存机制,也有三缓冲机制,Android 现在干流是三缓冲机制,在前期是单缓冲机制。
iOS 三缓冲机制比方

CP) k o . KU 和 GPU 资源耗费原因许多,比方目标的频繁创立、特点调整、文件读取、视图层级的调整、布局的核算(AutoLayout 视图个数多了便是线性方程求解难度变大)、图片解码(大图的读取优化)、图画制作、文本烘托、数据库读取(多读W b = ~ Y仍是多写乐观锁、失望锁的场景)、锁的运用(举例:l ) w ( m @ ; * :自旋锁运用不当会浪费 CPU)等方面。开发者依据自身阅历寻觅最优解(这儿不是本文要点)。

3. APM 怎么监控卡顿并上报

CADisplayLink 必定不用了,这个 FPS 仅作为参阅k n g $ S。一般来讲,卡顿的监测有2种计划:监听 RunLoop 状况回调、子线程 ping 主线程

3.1 RunLoop 状况监听的办法

RunLoop 担任监听输入源进行调度处理。比方网络、输入设备、周期性或许推迟g , L Q i工作、异步回调等。RunC m F & L { C / )Loop 会接纳2品种型的输入源:一种是8 } ^ _ % ! ^来自另一个线程或许I ! z来自不同运用的异步音讯(source0工作p b C R :)、另一种是来自预定e # o或许重复间隔的工作。

RunLoop 状况如下图

带你打造一套 APM 监控系统(一)

榜首步:告诉 Observers,RunLoop 要开端进入 loop,紧接着p G N t ` + n + Y进入 loopH C 9 x z b d

if (currentMode->_obserZ e d v Z u .verMask & kCFRunLoopEntry )
// 告诉 Observers: RunLoop 行将进入 loop
_l t n % s_CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 进入loop
result = __CFRunLv 5 CoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);

V ^ , + } w R二步:敞开 do while` . H v A r 循环保活线程,告诉 Observers,RunLoop 触发 Timer 回调、Sourcg ^ P b we0 回调,接着履行被参加的 block

 if (rlm-&gW U / k : P * ?t;_observerMask & kCFRunLoopBeforeTimers)a ` A N z i g
//] P 9 X G ^  告诉 Observers: RunLoop 行将触发 Timer 回调
__CFRunL^ ` [ z z l 9oopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
if (rlm->_observerMask & kCFRunLoopBeforeSources)
//  告诉 Observers: RunU y ~ nLoop 行将触发 Source 回调
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSoT 8 _ V g ~ hurces);
// 履行被y F + u 6 t t参加的block
__CFRunLoopDoBlocks(rl, rlm);

第三步:RunLoop 在触发 Source0 回调后,假设 Source1 是 reag 1 F d R Y z +dy 状况,就会跳转到 handle_msg 去处理音讯9 X o J Q

//  假设有 Source1 (依据port) 处于 ready 状况,直接处理这个 Source1 然后跳转去处理音讯
iI | v 7 Ef (MACH_PORT_NULL != dispatcA L w F DhPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DE2 } N C q vPLOYMENT_TARGET_EMBEDDED_MINI
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServ) 8 n & (iceMachPort(dispat6 i f P * M N TchPort, &msg, sizi L M Y xeof(msg_buffer), &livePort, 0, &voucc  9 v ZherState, NULL)) {
goto handle_msg;
}
#elif DEPLOYMENT_TARGET_- R ]WINDOWS
if (__CFRunLoopWaiz , u Y j  btFor- u & 1M_ P . / 7ultipleObject0 b f F [ ` Ys(9 N .NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
goto handle_msg;
}
#endif
}

第四步:回调触发后,告诉 Observers 行将进入休眠状况

Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 告诉 Observers: RunLoop 的线程行将进入休眠(sleep)
if (!poll &&B p X 3 9amp; (rlm->_obser# e 2 S ~ E _verMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObserve/ f o Irs(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleepingf 3 U ?(rl)X a m E x ^ g Q;

第五步:进入休眠后,会等候 mach_port 音讯,以便再次唤醒P E d + W n。只要以) W V下4种状况才能够被再次唤醒。

  • 依据 pw q V i I Q ] Fort 的 sG S S y i ;ource 工作
  • Timer 时刻到
  • RunLg l N & doop 超时
  • 被调用者唤醒
do {
if (kCFUseColleZ Y L N # V + n fctableAllocator) {
// objc_clear_stack(0);
// <rdar://problem/16393959>
memset(msg_bufferE ! {, 0,& 5 } v @ I = sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
if (modeQueuePort != MA` ? } D # ) i *CH_PORT_- 4 w y D x 3 V 0NULL && livePort == mo^ & $ ; 3 U bdeQueuePort) {
// Drain the internal queue. If one of the callout blocks sets the timerFireQ t M - 1d flag, break out and service the timer.
while (_dispatch_runloop_root_queue_perform_4CF(rlm-&gk  ?t;_queue));
if (rlm->_timerFired) {
// Leave livePort as the queue port, and service timers belk y & b z O [ +ow
rM p & X F s _lm->_time! Y / 9 vrFired =J B =   U false;
break;
} else {
if (msg && msg != (mach_msg_0 ) v & cheader_t *)msg_buffer) free(msg);
}
} else {
// Go ahead and leave the inner looP ; # V ( % .p.
break;
}
} while (1);

第六步:唤醒时告诉 Observer,RunLoop 的线程刚刚被唤醒了

// 告诉 Observers: RunLoop 的线程刚刚被唤醒了
if (!poD k * ? mll && (rlm->_observerMask & kCFRunLoopAfterWaA z L F ]  u 9iting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);B  * I _ c :
// 处理音讯
handle_j d g Nmsg:;
__CFRunLoopSetIgnoreWakeUps(rl);

第七步:RunLoop 唤醒后,处理唤醒时收到的音讯

  • 假设是 TH } M Q c ? – C Iimer 时刻到,则触发 Timer 的回调
  • 假设是 dispatch,则履行 block
  • 假设是 source1 工作,则处理这个工作
#if USE_MK_TIMER_~ R D ] w $ ETOO
// 假设一个 Timer/ ] } 到时刻了,触发这个Timer的回调
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFE : + & A } hRUNLOO0 & OP_WAKEUP_FOR_TIMERu u , S();
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646= Q ?  ` N765860, but it is actuallyB U K observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run lo[ X ? N + ^ 4 e lop timers should b L H 8 Q N d de firing, it appears to be 'too early' for the next timer, and no timers are handled.
// In th. z  h . v c ?is case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. addingV R X or removing timers). The fix for the issue iso h Q F o V N to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
if (!__CFRunLoopDoT{ 8 ^ . M * $ |imers(D - e H k B ^ jrl, rlm, mach_absolute_time())1 5 + l 8 U i) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}u B ~
}
#endif
//  假设有dispatch到main_queue的block,履行) 5 B ablock
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_L 9 MDISPATCH();
__CFRunLoopModeUnlock(rlm);
__CF2 8 t ; 0 g 1Runt L zLoopUnlock(rl);
_CFSe& Y G & I $ 8tTSD(__CFTSDKeyIsInGCDMainQ, (void *Y B q y m | e o 8)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
void *msg = 0;
#endif
__CFRUNLOOP_IS_SE( V ) 6 _RVICg S H 5 T f / !ING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rl; _ g s E p %m);
sourc+ 6 S q U keHandledThisLoop = true;
didDispatchPortLastTime = true;
}
// 假设一个 Source1 (依据port) 宣布工作了,处理这个工作
else {
CFRUNLOOP_WAKEUP_FOR_SOURCE();
// If we receb ! & & x r ! qived a vouche. H * ? G ] , ur from this mach_msg, then put a copy of the neA / p | ? Q } 0w voucher into TSD. CFMachPortBoost will look in0 S Y ~ x { [ the TSD for the voucher. By using the value in the TSB & 6 @ [D we tie the CFMachPortBoost to tX = n 6 2 * shis receiW y P E  | u ! Cved mach_a , y C n jmsg explicitly without a chance for anything in between the t; i + O T 9wo pieces of code to set tg ^ ghe vouchep * @ H h zr again.
voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMe% I & N %ssageHasVoucx p / - } v 5 #her, (void *)voucherCopy, os_release);
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
#ifB H ) / z DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *reply =q ^ z U H NULL;
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_@ N q ! 2 } O n 2size, &reply) || sourceHandledThisLoop;
if (O , z j  A C +NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply-S I i } Q 9>msghG y B k X M : O_size, 0, MACH_PORT_NULL, 0, MA= V f K G { E  tCH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
#elif DEPLOYMENT_TARGET_WINDOWS
sourceHandledThisLoop = __CFRunLoopDoSourcel ~ P r1(rl, rlm, rls) || sourceHandledThisLoop;
#endif

第八步:依据当时 RunLoop 状– ~ ) / J况判别是否需求进入下一个 loop。当被外部强制中止或许 loop 超时,就不持续下一个e / V l loop,否则进入下一个 loop

if (sourceHandledThisLoop && stopAfterHandle) {
// 进入loo} P S , ) @ Pp时参数说处理完工作就回来
retV: d L 9 a 1 l 0 .al = kCFRunLoopRunHandledSource;
} else if (- z h L 0timeout_con: e x W [ S ^ #text-U 9 [ u ] E>termTSR < mach_absolute_time()) {
// 超出传入参数` X g p : i M符号的超时时刻了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFB b NRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
/* s - W ~ c k j/ 被外部调用者强制中止了
retVal = kCFRunLoopRunSt! 9 @ K z , 4 D 6opped;
} else if (rlm->_stopped) {
rlm->_stoppeda Z ^  ( q v = = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// source/timer一个都没9 ! h 6  * e有
retVal = kCFRz 9 v x I ( f 2unLoopRunFinished;
}

完好且带有注释的 RunLoop 代码见此处。 Source1 是 RunLoop 用来处理 Max ~ `ch port 传来的体系工作的,Source0 是用来处理用户工作的。收到 Source1 的体系工作后本质仍是调用 SourceP s d0 工作的处理函数。

带你打造一套 APM 监控系统(一)

RunLoop 6个状况


typedef CF; L I q F a_OPTIONS(CFOptionFlags, CFRu} % V T ) a R wnLoopActivity) {
kCFRunLoopEntry ,           // 进入 loop
kCFRunLoopBefo3 a Z ~ b t 3rE 6 ! L p V : I leTimers ,    // 触发 Timer 回调
kCFRunLoopBeforeSources ,   // 触发 Source0 回调
kCFRunLoopBeforeWaiting ,   // 等候 mach_port 音讯
kCFRunLoopAfterWaiting ),r n 7 b X   // 接纳 mach_port 音] @ , % s ` ~ u p讯
kCFRq b * 1unLoopExit ,            // 退出 loop
kCFRunLoopAllB ) _ r @Activities     // loop 一切状况改动
}

RunLoop 在进入睡觉前的办法履行时刻过长R 8 )而导致无法进入睡觉,或许线程唤醒@ ; ; 2 M q后接纳音讯时刻过长而无法进入下一步,都会阻塞线程。假设是主线程,则表现为卡顿。

一旦发现进入睡觉前的 KCFRH i t s N yunLoopBeforeSources 状况V A ] ^,或许唤醒后 KI ! j , A CFRunLoopAfterWaiting,在设置的时刻阈值内没有改变,则可判别为卡顿,此刻 dump 仓库信息,复原案发现场,从而处理卡顿问题M ] W % + u , R

敞开一个子线程,不V S Q 5 –断进行循环监测是否卡顿了。在 n 次^ n j [都超越卡顿阈值后则认为卡顿了。卡顿之后进行仓库 dump 并上报(具有必i ( 8 ) =定的机制,数据处理鄙人一 part 讲)。

WatchDog 在不同状况下具有不同的值。

  • 发动(Launch):2[ V t0s
  • 恢复(Resume):10s
  • : } c起(_ = e 4Suspend):10s
  • 退出(Quit):6s
  • 后台(Background):3min(在 iOS7 之前能够恳求 10min;之后改为 3min;可连续恳求,最8 } = 2多到x y 3 ~ G ; b b 10min)

卡顿阈值的设置的依据是 WatchDog 的机制。APM3 + 9 [ t S f 体系里边的阈值需求小于 WatchDog 的值,所以取值规模在 [1, 6] 之间,业界一般挑选3秒。

经过 long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) 办法判别是否阻塞主线程,Returns zero on suV ) M G E M /ccess, or non-zero if the timeout occurred. 回来非0则代表超时阻塞了主线程。

带你打造一套 APM 监控系统(一)

或许许多人纳闷 RunLoop 状况那么多,为什么挑选 KCFv | ^ _ ]RunLoopBeforeSources 和 KCFRunLoopAfterWaiting?由于大部分卡顿都是在 KCFRunLoopBeforeSources 和 KCFRunLoopAfte{ Q 7 @ rWaiting 之间。比方 Soug R . ? =rce0 类型的 App 内部工作等

Runloop 检测卡顿流程图Y , 4 ~ b k ! A m如下:

带你打造一套 APM 监控系统(一)

要害代码如下:

/& a 0 u - ^ B F `/ 设置Runloop observer的q ` t 3运转环境
CFRunLoopObserverConte1 i _xt context = {0, (__bridge void *)self, NULL, NULL};
// 创立Runloop obsL s 9 } Merver目标
_observer = CFRux r V p 7 S B MnLoopObserverCreate(kCFAllocatorDefault,
kCFR2 @ $ - & A MunLoopAllActivities,
YES,
0,
&rY B D {unLoopObserverCallBack,
&context);
// 将新建的observer参加到当时thread的runloop
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
// 创立信号
_semaphore = dispatch_semaphore_create(0);
__wF U 6 8eak __typeof(self) weakSelf = self;
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__strong __typeof(weakSelf) strongSelf = weakSelf& : f  ~;
if (!strongSelf) {
return;
}
while (YES) {
if (strongSelf.H . p A 7 y s | 2isCancel) {
return;
}
// N次卡顿超越阈值T记载为一次卡顿
long semaphoreWait = dispatch_U + l J , ; bsemaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, strongSelf.limitMillisecond * NSEC_PER_MSEC))/ E K M;
if (semaphoreWait != 0) {
if (self->_activity == kCFRunLoopBeforeSources || self->_activity == kCFRunLoopAfterWaiting) {
if (++strongSelf.countTime < sx , GtrongSelf.stands~ c # 5 4 {tiu m t G U k Q {llCount){
continue;
}
// 仓库信息 dump 并结合数据上报机制! c 3,依照必定战略上传数e Y u 2 p - h Q据到服务器。仓库 dump 会鄙人面讲解。数据上报会在 [打造功用强壮、灵敏可装备的数据上报组件](https://github.com/FanI : ~ W f 4 TtasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md) 讲
}
}
strongSelf.countTime = 0;
}
});

3.2 子线程 ping 主线程监听的办法

敞开一个子线程,创立一个初始值为0的信号量、一个初始$ t ] F / P V Y值为 YES 的布尔值类型标志位。将设置标志位为 NO 的使命派发到主线程中去w K A W R,子线程休眠阈值时刻,时刻到后判别标志位是否被主线程成功(值为f o | 4 h j o NO),假设没成功则认为猪线程产生了卡顿状况,此刻 dump 仓库信息并结合数据上报机制,依照必定战略上传数据到服务器。数据上报会在 打造功用强壮、灵敏可装备的数据上报组件 讲

while (s= y R & ] ! K -elf.iO S /sCancelled == NO) {
@autoreleasepool {
__block BOOL isMaf x 1 TinThread A : F i ONoRespond = YES;
d: _ p f 7 V / 3 Oispatch_semaphore_t semaphore = dispatch_semaphore_cre* G L V 3ate(0);
dispatch_async(dispatch_get_main_queue(), ^{
isMainThreadNoRespond = NO;
dispatch_semaphore_signal(semaphore);& _ f
});
[NSThread sleepForTimeInterval:self.threshold];
if (isMainThreadNoRespond) {
if (self.handlere D ; p `Block) {
self.handlerBlock(); // 外部在 block 内部 dump 仓库(下面会讲),数据上报
}
}
dispatch_semaphore_wait(semaphore, DISC e o s h I ` BPATCH_TIME_FOREVER);
}
}

4. 仓库6 I W 3 J q dump

办法仓库的获取是一个麻烦事。理一下思路。[NSThread callStaC ~ I ~ckSymbols] 能够获取当时线程的5 u m i e * 调用栈。可是当监控~ g 0 * q F到卡顿产生,需求拿到主线) P w a t程的仓库信息就无能为力了。从任何线程回到主线程这条路走不通。先做个常识回忆。

在核算机科学中,调用仓库是一种栈类型的数据结构,用于存储! $ H T有关核算机程序的线程信息。这种栈也叫做履行仓库、程序仓库、操控仓库、运转时仓库、机器仓库等。调用仓库用于盯梢每个活动的子例v , Y ) 9 l # {程在完结履行后应该回来操控} O ( G的点。

维基百科查找到 “Call Stack” 的一张图和比方,如下

带你打造一套 APM 监控系统(一)

上图标明为一个栈。分为若干个栈帧(Frame): A D p,每y | U E z E Y个栈帧对应一个函数调用。下面蓝色部分标明 DrawSquare 函数,它在履行X H ? 的进r ~ u w Q $程中调用了 DrawLine 函数,用绿色部分标明。

能够看到栈帧由三部分组成:函数参数、回来地址、局部变量。比方在 Draw{ W x j }Square 内部s ! f F m ] Z调用了 DrawLine 函数:榜首要把 D5 k Q G OrawLine 函数需求的参数入栈 q – n P (;第二把回来地址(操控信息。举例:函数 A 内调用函数 B,调用函数B 的下一行代码的地址便是回来地址)入栈;第三函数内部的局部变量也在该栈中存储。

栈指针 Stack Pointer 标明当时栈的顶部,大多部分操作体系都是栈向下生5 H .长,所以栈指针是最小值。帧指针 Frame Pointer 指向的地址中,存储了上一次 Stack Pointer 的值,也便是回来地址。

大多数操作体系中,每个栈帧还保存了上一个栈帧的帧指x j | v针。因而知道当时栈帧的 Stack Pointer 和 Frame Pointer 就能够不断回溯,递归获取栈底的帧。

接下来的进6 d B程便是拿到一切线程的 StacJ A ( n 7k Pointer 和 Frame Pointer。然p z P , Z K后不断回溯,复原案发现场。

5. Mach Task1 / ; l 常识

Mach task:

App 在运转的时分,会对应一个 Mach T* ; eask,而 Task 下或许有多条线程一起履行使命。《OS X and iOS Kernel Ps x R Lrogramg : y sming》 中描绘 Mach Task 为:使命(Task)是一种容器目标,虚拟内存空间和其他资源都是经过这个容器目标办理的,这些资源包含设备和其他句柄。简略概括为:Mack task 是+ m Q %一个机器无关的 thread 的履行环境笼统。

效果: task 能够理解为一个进程,包( 6 6 6含它的线程列表。

结构体:task_threads,将 target_task 使命下的一切线程保存在 act_list 数组中,数组个数为 act_listCnt

kern_return_t task_ts A t ) hreads
(
task_t traget_taso . s 7 Gk,
thread_act_arraym % y #_t *act_list,                     //线程指针列表
mach_msg_type_num5 4 ~ber_t *act_listCnt  //线程个数
)

thread_info:

kern_retuR o G # crn_t thread_info
(
thread_act_t target_act,
thread_flavor_t flavor,
thread_info_t threF ) w I { E |ad_info_out,
mach_msg_type_number_t *thread_info_outCnt
);

怎么获取线程的仓库数据:

体系办法 kern_return_t task_threadc a [ z / ]s(task_inspect_t target_y ] j 1task, thread_act_arz k U dray_t *act_list, mach_msg_type_number_t *act_listCnt); 能够获取到一切的线程,不过这种办法获取到的线程信息是最+ $ K ~ 3底层的 mach 线程

关于每个线程,Z ? B B d @ & 4能够用 kern_retur$ N c * T . gn_t thread_get_state(thread_act_t target_act, thread_state_f, | ~ J e V N @ _lavor_t flavor, thread_state_t old_state, mach_msg_type_number_t *old_stateCnt);- v o = l ; ! % 办法获取它的一切信息,信息填充在 _STRUCT_MCONTEXT 类型的参数# b ( b中,这个办法中有2个参数跟着 CPU 架构不同而不同。所以需求界说宏屏蔽不同 CPU 之间的差异。

_STRUk x ? 7 h B uCT_MCONTEXT 结构体n J O 1 5 D {中,存储了当时线程的 Stack P, P N Aointer 和最顶部栈帧的 Frame poi^ @ Y u 0 ? ! |nter,从而回溯整个线程调用仓库。

可是上述办法拿到的是内核线程,咱们需求的信息是 NSThread,所以需求将内核线程转换为 NSThread。

pthread 的 p 是 POSIX 的缩写,标明「可移植操作u D M | K / G s e体系接口」(; = D R s E / hPortable Operating Syste% 4 L ; n 5 Om Interface)。规划初衷是每个体系都有自己共同的线程模型,且不同体系关1 O L q于线程操作的 API 都不相同。所以 POSIX 的意h C Z图便是供给笼统的 pthread 以及相关 API。这些 API 在不同的操作体系中有不同的完结,可是完结的功用共同。

Unixr d $ @ # r Z 9 体系供给的 t; q / $ask_threadsthread_get_state 操作的都是l E ; Z : n m !内核体系,$ 0 0 s a每个内核线程由 thread_t 类型} 0 : o i ^的 id 仅有标识。pthread 的仅有标识是 pthrA . u T H _ead_t 类型。其间内核线程和 pthread 的转换(即 thread_t 和 pthread_t)很简略,由于 pthread 规划初衷便是「笼统内核线$ m – c F , . I :程」。

memorystatus_action_neededpthread_cL @ c i ) dreate 办法创立线程的回调函数为 nsthreadLauncher

static void *nsthreadLauncher(void* threy r | Vad)
{
NSThread *t = (NSThread*)thread;
[nc postNotificationName: NSThreadDidL n 6 ; L qStartNotification object:t userInfo: nil^ # W P S];
[t _setName: [t name]];
[t main];
[NSThread exit];
return NULL;
}

NSThreadDidStartNotification 其实便是字i = g *符串 @”_NSThreadDidStartNotification”。

<NSThread: 0x...>{[ W 4 S w Onumber = 1, name = main: r Z j j}

为了 NSThread 和内核线程对应起来,只能经过 name 一一对应。 pthrS k * X i 0 ? f lead 的 API pthread_getname_np 也可获取内核线程姓名。np 代表 not POSIX,所以不能跨平台运用。

思路概括为:将 NSThrea/ @ t 5 A % od 的原始姓名存储起来,再将姓名改为某个随机数(时刻戳),然后遍历内核线程 pthread 的姓名,姓名匹配则 NSThread 和内核线程对应了起来。找到后将线程的姓名复原成原本的姓名。关于主线程,由于– M 5 $ 3 k不能运用 p= u ithread* [ b | c S k e_getname_np,所以在当时代码的 load 办法中获取到 thread_t,然后匹配姓名。

static mach_port_t main_thread_id;
+ (void)load {
main_thread_id = mach_. i } b * M ! )thread_self();
}9 / 2 2 B 4 o @

二、 App 发动时刻监控

1. App 发动时刻的监控

运用发动时刻是影响用户体会的重要要素之t i 4 n Z % 7 {一,j U d % f } O l K所以咱们需求– 1 @ M T A L F量化去衡量一个 App 的发动速度到底有多快。发动分为冷发动和热发动。

带你打造一套 APM 监控系统(一)

冷发动:App 没有运转,有必要加载并构建整个运用。完结运用的初始化。冷发动存在较大优化空间。冷发动时刻从 applR E d Y X Vication:n m : t : : G w T didFini% ^ ) s r $ 2shLaunchingWithOptions: 办法开端核算,App 一般在这儿进行各种 SDK 和 App 的根底初始化作业。

热发动:运用现已在后台运转(常见场景:比方用户运用 App 进程中点击 Home 键,再翻开 App),由于某些工作将? 9 K运用唤醒到前台,App 会在 applicationWillEnterForeground: 办法承受运用9 H X ^ f b e J X进入前台的工作

思路比较简略。如下l y 8 l G B ( F

  • 在监控类的 load 办法中先拿到当时的时刻值
  • 监听 AppT 0 W : | M n N 6 发动完结后的告诉 UIApplicationDidFinishLaunchi* [ M PngNotification
  • 收到告诉后拿到当时的时刻
  • 进程1和3的时刻差便是 App 发动时刻。

mach_absolute_time 是一个 CPUy M u 7 ~ ! y @/总线依靠函数,回来一个 CPU 时钟周期数。体系休眠时不会增加。是一个纳秒等级的数字。获取前后2个纳秒后需求转换到秒。需求依据体系时刻的基准,经过 mach_timebasx h j U o N w 4 ue_info 取得。

mach_timebase_info_data_t g_cmmStartupMonitorTimebaseI4 m j u W : GnfoData = 0;
m6 , k | I / F Z Xach_timebase_infG U d H w ,o(&g_cmmStP K 8 % *artupMonitorTimebaseInfoData);
uinZ i Ht64_t timelapse = mach_absolute_time() - g_cmmLoadTime;
double timeSpan = (timelapse * g_cmmStartupMon` 3 k t 1 E o ;itorTimebaseInfoData.numer) / (g_cmmStartupMonitorTimebaseInfoData.denom * 1e9);

2. 线上监控发动时刻就好,可是在开发阶段需求对发动时刻做优化。

要优化发动时刻,就先得知道在发动阶段到底做了什么工作,针对现状作出计划* N ^ v % d ?

pre-maiA q Q , o :n 阶段界说为 App 开端发动到体系调用 main 函数这个阶段;main 阶段界说为 main 函数入口到主 UI 结构的 viewDidAppear。

App 发动进程:

  • 解析 Info.plist:加载相关信息例如闪屏;沙盒树立、权限检查;
  • Mach-O 加载:4 y i @ I假设是胖二进制文件,寻觅适宜当时 CPU 架构的部分;加载一切依靠的 Mach-O 文件(递归~ ~ 5 c调用 Mach-O 加载的办法);界说内部、外部指针引证,例如字符串、函数等;加载分类中的办法;c++ 静态目标加载、调用 Objc 的 +load()8 u /数;履L . + x w q M M行声明为 _attribute((c! 3 Aonstructor)) 的 c 函数;
  • 程序履行:调用 main()) S F h F W R;调用 UIApplicationMain();, { n J @ w g U调用 applicationWillFinishLaunchin` , kg();

Pre-MR y Sain 阶段

带你打造一套 APM 监控系统(一)

Main 阶段

带你打造一套 APM 监控系统(一)

2.1 加载 Dylib

每个动态库的加载,dyld 需求

  • 剖析所依靠的动态库
  • 找到动态库的 Mach-O 文件
  • 翻开文件
  • 验证文件
  • a J K体系核心注册文件签名
  • 对动态库的每一个 segment 调用 mmap()

优化:

  • 削减非体系库的依靠
  • 运用静态库而不是动态库
  • 合并非体系动态库为一个动态库

2.3 ? , i2 Rebase &ampf f m e , V ~ .;& Binding

优化:

  • 削减 Objc 类数量,削减 selecV h o mtor 数量,把未运用的类和函数都能够删掉
  • 削减 c++ 虚函数数量
  • 转而运用 Swift struct(本质便是削减符号的数量)

2.3 Initializers

优化:| j W ) P S ! e

  • 运用 +( % c U V g Minitialize 替代 +loz + q S ] wad
  • 不要运用过 attribute*((constructor)) 将办法显现符号为初始化器,而~ ~ B e j Y E w {是让初始化办法调用时才履行。比方运用 disg V I ! A C 4 –patch_one、pthreadW d + X V R 2 a_once() 或 std::once()。也便是榜首次运用时才初始化,推迟了一部分作业耗时也尽量不要运用 c++ 的静态目标

2. 2 9.4O p 7 2 z 5 pre-main 阶段影响要素

  • 动态库加载越多,发动越慢。
  • ObjC 类越多,函数越多,发动越慢。
  • 可履行文件越大发动越慢。
  • C 的 constructor 函数越多,发动越慢。
  • C++ 静态目标越多,发动越慢。
  • ObjC 的 +load 越多,发动越慢。

优化手段:

  • 削减依靠不必要的库,不管是动态库仍是静态库;假设能够的话8 G ( ] ~,把动态库改形成静态库;假设有必要S H o x L ? i W 6依靠动态库,则把多个非体系的动态库合并v u 2 p y成一个动态库
  • 检查下 fram/ m 6 nework应当设U _ L为optional和requiredi { } , ^ N ] @,假设该framework在当时App支撑的一切iOS体系版别都存在,那么就设为required,否则就设为optional,由于optional会有些额定的检查
  • 合并或许删减一些OC类和函数。关于整理项目中没用到的类,运_ : O . 2 I f 用东西AppCode代码检查功用,查到当时项n ? P e p } ,目中没有用到的类(也能够用依据linkmap文件来剖析,可是准确度不算很高)
    有一个叫做FUI的开源项目能很好的分分出不再运A k % u用的类,准确率非常高,仅有的 [ ` n u 5 o问题是它处理不了动态库和静态库里供给的类,. k 0 q也处理不了C++的类模板
  • 删减一些无用的静态变量
  • 删减没有被调用到或许现已抛弃的办法
  • 将不有必要在 +load 办法中做的工作推迟到 +initialize中,尽量不要用z e D C % ~ o C++ 虚函S Z u K x数(创立虚函 | Z b数表有开支)
  • 类和办法名不要太长:iOS每个类和办法s O + W M名都在 __cstring 段里都存了相应的字符串值,所以类和办法名的长短也是对可履行文件巨细是有影响的
    因仍是 Object-c 的动态特性f p . n } _ i t `,由于需求经过类/办法名反射找到s . L v G r这个类/办法进行调用,Object-c 目标模型会把类/K ^ 4 ]办法姓名符串都保存下来d l @ B K * ; I
  • 用 dispatch_once() 替代一切的 attribute((constructor)) 函数、C+. 3 1+ 静态目标初始化、ObjC 的 +load 函数;
  • 在规划师可承受的规模内紧缩图片的巨细,会有意外收成。
    紧缩图片为什么能加快发动速度呢?由于发动的时分大巨细小的图片加载个十来二十个是很正常的,
    图片小了,IO操作量就小了,发动当然就会快了,比较靠谱的紧缩算法是 TinyPNG。

2.5 main 阶段优化

  • 削减发动初始化的流程。能懒加载就懒加载,能放后台初始化就放后台初始化,能推迟初始化的就推迟初始化,不要卡主线程的发动时刻,现已下线的事务代码直接删去
  • l * = | H化代码逻辑。去除一些非必要的逻辑和代码,减小每个流程所耗b G q 3 | l M费的时刻
  • 发动l g | 3 E O ] (阶段运用多线程来进行初始化,把 CPU 功用发挥最大
  • 运用纯代码而不是 xib 或许 storyboard 来描绘 UI,尤其是主 UI 结构,比方 TabBarController。由于 xib 和 storyboX g C Zard 仍是需求解析成代码来烘q 4 l p X K Z ( B托页面,多了一步。

三、 CPU 运用率监控

1. CPU 架构

CPU(Central Processing Unit)中央处理器,市场上干流的架构U w : ?有 ARM(arm64)、InZ 0 T o N 0 9tel(x86)、j k ] = s zAMD 等。其间 Inr r O n / {tel 运用 CISC(Complex Instruction Set Computer),ARM 运用 RISC(Reduced Ia q F B e 3 $ – %nstruction Set Computer)。差异在于不同的 CPU 规划理念和办法

前期 CPU 悉数是 CISC 架构,规划意图是用最少的机器a E 7 $ h语言指令来完结所需的核算使命。比方关/ u ) 6 m于乘法运算,# a ( 1在 CISC 架构的 CPU 上。一条指令 MUL ADDRA, ADDRB 就能够将内存 ADDRA 和内存 ADDRB 中的数香乘,并将成果存储在 ADDRA 中f j &。做的工作便是:将 ADDRA、ADDRB 中的数据读入到寄存器,相乘的成果写入到内存的操作依靠于 CPU 规划,所以 CISC 架构会增加 CPU 的杂乱性和对 CPU 工艺的要求。

RISC 架构要求软件来指定各个操作h 5 z k e 2进程。比方上面的乘法,指令完结3 9 } U } e vMOVE A, ADDRA;D $ ` MOVE B, ADDRB; MUL A, B; ST[ : Y o S qR ADDRA, A;。这种架构能够下降 CPU 的杂乱性以及允许E u a在同样的工艺水平下生产出功用更加d 6 0 v O N 3 y #强壮的 CPU,可是关于编译器的规划要求v ; Z更高。

现在市场是大部分的 iPhonep k Ra v G : A 9 e E是依据 arm64 架构的。且 arm] C , 7 U 架构能耗低。

2. 获取线程信息

讲完了差异来讲下怎么做 CPU 运用E F Y P k M率的D { N 7 : + P /监控

  • 敞开守时器,依照设定的周期不断履行下面的逻辑
  • 获取当时使命 task。从当时 task 中获取一切的线程信息(线程个数、线程数组)
  • 遍历一切的线程信息,判别是否有线程的 CPU 运用率超越设置[ ` X 0 b的阈值
  • 假设有线程运用率超越阈值,则 dG b 2 U g zump 仓库
  • 拼装数据,上报数据

线程信息结构体

struct threa2 B L T H d_basic_info {
time_value_t    user_time;      /* user run th n } I d # ? Kime(用户运转时长) */
time_value_t    system_time;    /* system run time(体系运转时长) */
integer_t       cpu_usage;      /* scag 3 )led c#  v n Y l x ;pu usage percentage(CPU运用率,上限1000) */
policy_t        poli] +  - |cy;         /*m d b H 3 v O scheduling policy in effect(有效调度战V q ~ - w ) l #略)j % ] ; */
integer_t       run_state;      /* run state (运转状况,见下) */
integer_t       flags;          /* various flags (各式各样的符号) */
integer_t       suspend_count;  /* suspend count for thread(线程挂起次数) */
i| W t ;nteger_t       sleep_time;E h = 0     /* number of seconds that thread
*  has been sleeping(休眠时刻) */
};

代码在讲仓库复原的时分讲过,忘记的看一下上面的剖析

thread_act_array_ta m x p threads;
mach_msg_type_number_t threadCount = 0;
const t$ 7 y 7 o K B F :ask_t thisTask = mach_task_self(c F );
kern_returo . , k a in_t kr = task_threads(thisTask, &threads,E y G d v  &T . x 2 x f;threadCount);
if (kr, F u ^ F k O e != KERN_SUCCESS) {
return ;
}
for (int i = 0; i < threadCoun( & { zt; i++) {
thread_% 2 9 X info_data_t threadInfo;
thread_basic_info_t th.  V N Z 1 %readBaseInfo;
mach_msg_C _ w k S M * O @type_number_t threadInfoCount;
kern_returnz  k * X _ e P /_t krv } . T V & J B = thrV ^ i  L  Nead_info((thread_inspect_t)tj  p +hreads[i], THREAD_BASIC_INFO, (thread_info_t)thre% R s & H R a adInfo, &threadInfoCount);
if (kr == KER^ 2 g g : m Q _ UN_SUCCES? : / a L : hS) {
threadBaseInfo = (thread_basic_info_t)threab v 2 P ,dInfo;
// todo:条件判别,看不理解
if (!(threadBaseInf? I eo->flags & TH_FLAGS_IDLE)) {
integer_t cpuUsage = threadBaseInfo->cpu_usS P S uage / 10;
if (6 Q t G v P 6cpuUr ) L % [sage > CPUMONITORRATE3 / ) v M $ C P ?) {
NSMutableDictionary *CPUMetaDictionary = [NSMutaB ` ) = R b TbleDictionary dictionary];
NSData *CPUP. ^ k K 1 rayloadData = [NSData data];
NSString *backtraceOfAllThread = [Backtrac/ o Q C J beLogger backtraceOfAllThread];
// 1.; / A j : z : v W 拼装卡顿的 Meta 信息
CPUMetaDictionary[@"MONITOR_TYPE"] = CMMonitor; / ^ N /CN U F 8 j !PUType;
// 2. 拼装卡顿的 Payload 信息(一f % n ? 7 i个JSON目标,目标的 Kr K 2ey 为约定好的 SJ Y fTACK_Tj + 2 / & SRACE, value 为 ba@ I Y 4se64 后的仓库信息)
NSDG X j 2 1ata *CPUData = [SAFE_STRING(backtraceOfAllThread) dataUsingEncoding:NS$ e o @UTF8StringEncod o 6 K P  I Nding];
NSString *CPUDataBase64String = [CPUData base64EncodedStringWithOptions:0];
NSDictionary *CO e | _PUPayloadDictionary = @{@"STACK_TRACE": SAFE_STRING(CPUDataBase64String)};
NSError *error;
// NSJSONWritingOptions 参数必定要传0,由于服务端需求( { _ 5 l 0依据 \n 处理逻辑,传递 0 则生成的 json 串不带 \n
NSData *parsedData = [NSJSONSerialization dataWithJSONObject:CPUPayloadDictionary options:0 errorU R 1 ? M:&error];
if (error) {
CMMLog(@"%@", error);
return;
}
CP& A 0 H wUPayloadData = [parsedData copy];* L l
// 3. 数据上报会在 [打造功用强壮、灵敏可装备的数据上报组件](https://github.com/FantasticLBP/knowledge-kit/bln K F ? rob/master/Chapter1%20-%20iOS/1.80.md) 讲
[[PrismClient sharedInstance] sendWithType:CE x B 9 =  E ) `MMonitorCPUType meta:CPUMetaDictionary payload:CPUPayloadData];
}
}
}
}

四、 OOM 问题

1. 根底常识预备I + . / c N y I

6 @ r d盘:也叫做磁盘,用于存储数据。你存储的歌曲、图片、视频都是在硬盘里。

内存:由于硬盘读取速度较慢,假设 CPU 运转程序期间,一切的数据都直接从硬盘中读取,则非常影响功率7 ! Z 3 J。所以 CPU 会将k 0 e程序运转所需求的数据l / 4 y }从硬盘中读取到内存中。然后 CPU 与内存中的数据进行核算、交流。内存是易失性存储器(断电后,数据消失)。内存条区是核算机内部(在主板d I , $ ) 1 V上)的一些存储器,用来保存 CPU 运算的中心数据和成果。Y / o N j d 内存是程序与 CPU 之间的桥梁。从硬盘读取出数据或A ) : n a E x p p许运转程序供给给 CPU。

虚拟内存 是核算机体系内存办理的一种技能。它使得程序认为它拥有连续的可用内存,而实际上,它一般被分割成多个物理内存碎片,或许部分暂时存储在外H 2 ) # [ ?部磁盘(硬盘)存储器上(当需求运用时则用硬N r g盘中数据交流到内存中)。Windowsx S d ] & 体系中称为 “虚拟内存”,Linux/Unix 体系中称为 ”交流空间“。

iOS 不支撑交流空间?不仅仅 iOS 不支撑交流空间,大多数手机体` i | k k系都不支撑。由于移动设备的很多存D # f s # H : 7 d储器是闪存,它的读写速度远远小电脑所运用的硬盘,也便是说手机即便运用了交流空间技能,也由于闪存慢的问题,不能提高功用,所以索性就没有交流空间技能。

2. iOS 内3 6 u _ *存常识

内存(RAM)与 CPU 相同都是体j 0 D D q ! C d系中最稀疏的资源,也很简略L ; O n x l K p !产生竞赛,运用内存与功用直接相关。iOS 没有交流空间作2 b S C %为备选资源,所以内存资源尤为重要。

什么是 OOM?是 oV 8 G y ! 2 T + 0ut-of-memory 的缩写,字面意思是超越了内存约束。分为 FOOM(foreground OOM)和 BOOM(background OOM)。它是由 iOS 的 Jetsam 机制形成的一种非干流 Crash,它不能经过 Signal 这种监控) ; = f 2 C v 2计划所捕获。

什么是 Jetsam 机制?Jetsam 机制能够理解为体系为了操控内存资源过度运用而采用的一种办理机制。Jetsam 机制是运转在一个{ i k独立 Z | m 的进程中,每个进程都有一个内存阈值,一旦超越这个内存阈值,Jet~ Z P ( f U ` Os& + r j + + [ $am 会立即杀掉这个进程。

为什么规划 Jetsn – @ A , `am 机制?由于设备的内存是有限` [ R T的,& 3 P ( r j z R所以内存资源非常重要。体系进程以及其他运用的 App 都会抢占这个资源。9 m E m Z s M由于 iOS 不支撑交流空间,一旦触发低内存工作,Jetsam 就会尽或许多的开释 App 所在内存,这样 iOS 体系上呈现内存不足时,App 就会被体系杀掉,变现为 crashx R u Y

2种状况触发 OOM:体系由于全体内存运用过高,会依据优先级战略杀死优先级较低的 App;当时 App 到达了 “highg water mark” ,体系也会强杀当时 App(超越体系对当时单个 App 的内存约束值)。

读了源码(xnu/bc U ? 7 R . . Wsd/kern/kern_memorystatg K & u : i D [ +us.c)会发现内存被杀也有x S + N 9 u O2p z W C $ $ _ l种机制,如下

highwater 处理 -> 咱们的 App 占用内存不能超越单个约束

  1. 从优先级列表里循环寻觅线程
  2. 判别是否满意 p_memstat_memlimit 的约束| I P D _条件
  3. DiagonoseActive、FREEZE 过滤
  4. 杀进程,成功则 exit,否则循环

memorystatusC 2 O ? # P m_act_aggressive5 7 q | E [ . 处理 -> 内存占用高,依照优先级杀死

  1. 依据 policy 家在 jld_bucket_count,用来判别是否被杀
  2. 从 JETSAM_PRIORITY_ELEVAd r $ b CTED_INACTIVE 开端杀
  3. Old_bucket_count 和 memorystatus_jld_eval_period_msecs 判g Z r 2 r H v :别是否开杀
  4. 依据优先级从低到高开端杀,直到 me( # u 8mory ? l – Aystatus_avail_pages_below_pressure

内存过大的几种状况

  • AppO h ) : 内存耗费较低,一起其他 App 内存办理也很棒,那么即便切换到其他 App,咱们自己的 App 依旧是“活着”的,保留了用户状况。体会好
  • App 内存耗费较低,但其q % _ $ C S =他 App 内存耗费太大(H ~ d u w p ^ / n或许是内存办理糟糕,也或许是自身就耗费资源. : F $ q y,比方游戏),那么除了在前台3 & L % B Z的线程,@ ; + Q ! F t a其他 App 都会被体系杀死,收回内存资源,用来给活跃的进程供给内存。
  • App 内存耗费较大u Z 5 8 t !,切换到其他 App 后,即便其他 App 向体系恳求的内存不大,体系也会由于内存P & ~ ` l C r M资源严重,优先把内存耗费大的 App 杀死。表现为用户将 App 退出到后台,过会儿再次翻开会发现 App 重新加7 a : j e C @ F F载发动。
  • App 内存耗费非常大,在前台运转时就被体系杀死,形成闪退。

App 内存不足时,体系会依照必定战略来腾出更多的空间供运用。比较常见的做法是将一部分优先级低的数据挪到磁盘上,U Y 4 P W % C 3该操作为称为 page out。之后再次访_ p ^ r Z _ } E _问这块数据的时分,体系会担任将它重新搬回到内存中,该操P L | S ] = e作被称为 page in

Memory page** 是内存办理中的最小单位,是体系分配的,或许一个 page 持有多个目标,也或许一个大的目标跨越多个 pagv [ $ 5 r ne。一般它是 16KB 巨细,且有3( x * $ 0品种型的 page。

带你打造一套 APM 监控系统(一)
  • Clean Memory
    Clean memoryr ? o 包含3类:能够 page out 的内存、内存映射文件、App 运用到的 framework(每个 framework 都有 _DATA_CONST 段,一般都是 clean 状况,但运用 runtime swizling,那么变为 dirty)。

    一开端分配的 page 都是洁净的(堆里R 7 v @边的T p k = h目标分配在外),咱们 App 数据写入时分变为 dirty。从硬盘读进内存的文件,也是只读的、clean pa U 3 ! d L )age。

    带你打造一套 APM 监控系统(一)
  • Dirty Memory

    Dirty memory 包含4类:L R f m 6 , Q被 App 写入过数据的内存、一切6 V } n g c B堆区分配的目标、图画解码缓冲区、framewor– = , ( 2k(f5 : U . & s C ) Sramework 都有 _DATA 段和 _DATA_DIRTY 段,它们的内存都是 div r 5rty)f a b ( { D

    在运用 frC y M Z r E U Eamework 的进程中会产生 Dirty memory,运用单例或许e / W Z L E i全局J P ) – f M m初始化办法有助于协助削减 Dirty memory(由于单例一旦创立就不销毁,一直在内存中,体系不认为是 Dirty memor* # } 3 t v C oy)。

    带你打造一套 APM 监控系统(一)
  • Compressed Memory

    由于闪存容量和读写约束,iOS 没有交, m e F l #流空间机制,而是在 iOS7 引进了 memory compressor。它是在内存严重时分能够将最近一段时刻未运用过的内存目标,内存j P $ d – , |紧缩器会q a M ; H a p P把目标紧缩,开释出更多的 page。在需求时内存紧缩器对其解压复用。在节约内存的一起提高了呼应速度。

    比方 App 运用某 Framework,内部有个 NSDictionary 特点存储数据,运用了 3 pages 内存,0 o j 6 7 –在近期未被3 7 – ? 9 } q h访问的时分 memory compressor 将其紧缩为 1 page,再次运用的时分复原为 3 pages。

App 运转内存 = pageNumbers * pageSize。由于. h m n . d v Compressed Memory 归于 Dirty memory。所以 Memory footpr9 S G @ L Nint = dir? c h k : 8tySize + CompressedSize

设备不同,内存占用上限不同,App 上限较高,extension 上限较低,超越上限 crash 到 EXC_RESOURCE+ & q + J 6 i D *_EXCEPTION

带你打造一套 APM 监控系统(一)

接下来谈一下怎么获取内存上限,以及怎么监控 App 由于占用内存| a J j l } x过大而被强杀。

3. 获取内存信息

3.1 经过 JetsamEvent 日志核算内存约束值

当 App 被 Jetsam 机制杀死时,手时机生成体系日志。检查途径:Settings-Privad 3 x I g o _ y zcy-Analytics & Improvements- A2 J . V w L ( j enalytics Data(设置-隐私- 剖析与改善-剖析数据),能够看到 JetsamEvent-2020-03-14-161828.ips 办法的日志,以 JetsamEvent 最初。这些( ) 6 U U JetsamEventa B : 日志都是 iOS 体系内核N H $强杀掉那些优先级不高(idle、frontmost、suspended)且占用内存超越体系内存约束的 App 留下的。

日志包含了 App 的内存信息。能够检查到 日志最顶部有 pageSize 字段,查找到e a g & O Z e | F per-p} , !rocess-limit,该节点所在结构里的 rpages ,将 rpaP R Oges * pageSize 即可得到 OOM 的k n 3阈值。

日志中 largestProcess 字段代表 App 称号;reason 字段代表内存原因;states 字段代表奔溃时 App 的状况( idle、suspenda c E = v ; v #ed、froV ( ! r / x Hntmost…)。

为了测试数据的准确性,! Q , & o Q 1 3 _我将测试2台L , F p a q E设备(iPhone 6s plus/13.3.1,iPhone 11 Pro/13.3.1)的v d B ; l x一切 App 彻底退出,只跑了一个为了测试内存临界值的 Demo App。 循环恳求内存,ViewControllR o b } 1er 代码如下

- (void)viewDidLoad {
[super viewDidLo4 * o h a b t iad]V r e e F V;
NSMug / A : / T M 6tableArray *array = [NSMutableArray array];
for (NSInteger i[ r Kndex = 0; index < 10000000; index++) {
UIImageView^ L % *imageView = [[UII{ j % d K +mageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
UIIma- } 3 q j ? ; . +ge *image = [UIImage imageNamed:@"AppIcon"];P I m j j H G f ]
imageView.image = image;
[array addO5 q R O s Z u 1 |bject:imageView];
}
}

iPhone 6s plus/13.2 9 C ! K3.1 数据如下:

{"bug_type":"298","timestamp":p M , , * O"2020-03-19 17:23:45.94 +0800","os_version":"iPhone OS 13.3.1 (17D50)","incident_id":"DA8AF66D-24E8-458C-8734-981866942168"}
{
"crashReporterKey" : "fc9b659ce486df1edO ( D ] I ? X1b8062d5c7c977a7eb8c851",
"kernel" : "Darwin Kernel Version 19.3.0:F )  w K ( { Thu JaN ` 3 I : y p 9n  9 21:10:44 PST 2020; root:xnu-6153.82.3~1\/RELEASE_ARM64_S8000",
"product" : "iPhone8,2",
"incident= ] " : "DA8AF66D-24E8-458C-8734-981866942168",
"date" : "2020-03-19 17:23:45.93 +0800",
"build| : ~ h H 4 1" : "iPhone OS 13.3.1 (17D50)",
"timeDelta" : 332,
"memoryStatusY W q [ 0 9 d" : {
"compresso5 | x % X , y i PrSize" : 48499,
"compressions" : 7458651,
"decompressions" : 5190200,
"zoneMapCap" : 74: I Q Q d $ H j d4407040,
"largestZone" : "A( k ( Q H * Z ^PFS_4K_OBJS",
"largestZoneSize" : 41402! & H [ ^ T p [368,
"pageSize" : 16384,
"uncompressed" : 104065,
"zoneMapSize" : 141606912,
"memoryPages" : {
"active" : 26214,
"throttled" : 0,
"fileBacked" : 14903,
"wired" : 20019,
"anonymous"A : s W q 5 h : 37140,
"purgeable" : 142,
"inactive" : 23669,
"free" : 2967,
"speculative" : 2160
}
}S : 5 9 U 5 n W,
"largestP& ` t D / - U q @rocess" : "Test",
"genCounter" : 0,
"processes" : [
{
"uuid" : "39c5738b-b321-. ^ - (3865-a7j 4 K & $ l O31-68064c4f7a6f",
"stat# u ; 6 + , jes" : [
"daemon",
"idle"
],? . ( 9 ! | ! n E
"lifetimeMax" : 188,
"age" : 948223699030,
"j k % 9purgeable" : 0,
"fds" : 25,
"coali( q X m ( 3tion" : 422,
"rpages" : 177,
"pid" : 282,
"idleDelta" : 824711280,
"name" : "com.apple.Safari.SafeBrowsing.Se",
"cpuTime" : 10.27- 4 q E #5422000000001
},
// ...
{
"uuid" : "83dbf12I M O 8 ] $ R ,1-7c0c-3ab5-9b66-77ee926e1561",
"states" : [
r $ ( t C h"frontmost"
],
"killDelta" : 2592,
"genCount" : 0,
"age" : 1531004794,
"purgeable" : 0,
"fds" : 50,
"coalition" : 1047,
"rpages" : 92806,
"reason" : "per- e 9 f m 5 Y s S-proI I B 4 c #cess-limit",
"pid" : 2384,
"cpuTime" : 59.4643739f . ] ]99F [ E O 2999999,
"name" : "Test",
"lifetimeMax" : 92806
}i = , h Y % B,
// ...
]
}

iPhone 6s plus/13.3.1 手机 OOM 临界值为:(16384*92806)/(1024*1024)=1450.09375M

iPhone 11 Pro/13.. ~ x n % Q3.1 数据如下:

{"bug_type":"298","timestamp":"202J s e C w X P d0-03-19 17:30:28.39 +0800","os_version":"iPhone OS 13.3.1 (17D50)","inci5 ( z V h o odent_id":"7F111601-BC7A-4BD7-A468-CE33t 2 z - Y V70053057"}
{
4 r % r z j w"crashReporterKey" : "bc2445adc16e g l )4c399b330f812a48248e029e26276",
"kernel" : "Darwin Kernel Version 19.3.0: Thu Jan  9 21:11:10 PST 2020; root:xnu-6153.82.3~1\/RELEASE_ARM64F f & I { w Z {_T8030",
"producg I _t" : "iPhone12,3",
"inci{ % P P f ] x 3 odent" : g J N } Z * G F 1"7F111601-BC7A-4BD7-A468F H & 8-CE3370053057",
"date" : "2020-03-19 17:30:28.39 +0800",
"build" : "iPhone OS 13.3.1 (17D50)",
"timeDelta" : 189,
"memoryStatus" : {
"compressorSize" : 66443,
"compressions/ $ x g e 2 X" : 25498129,
"decompressions" : 15532621,
"zoneMapCap" : 1395015680,
"largestZone" : "APFS_4K_OBJS",
"largestZoneSize" : 41222144,
"pageSize" : 16384,
"uncompressed" : 1279 P F ~ Z027,
"zoneMapSize" : 169639936,
"memoryPages"( R 1 U u ~ ] 5 : {
"active" :z 3  a c x O 58652,
"throttled" : 0,
"fileBaj T b H . [cked" : 20291,
"wired" : 4583= x z a8,
"anonymous" : 96445,
"purgeableF H 1 1 7 R" : 4,
"inacM Y @ xtive" : 54368,
"free" : 5461,
"speculd I  d R - Rative" : 3716
}
},
"largestPr: [ a J 2ocess" : "杭城小刘",
"genCounter" : 0,
"processes" : [
{
"uuid" : "2dd5eb1e-fd3; n { a 0 ! j v1-36c2-99d9-bcbff44efbb7",
"states"S M Z Z & P p i : [
"d= m p _aemon",
"idle"
],
"lifetio G g wmeMax" : 171,
"age" : 5151034269954,
"purgeable" : 0,
"fds" : 50,
"coalition" : 66,
"rpages" : 164,
"pid" : 1127, Z B U *  [6,
"idleDelta" : 3801132318,
"name" : "wcdP Q @",
"cpuTime" : 3.4: 8 ~ - K } . c30787
}y % } 8 @ g f q,
// ...
{
"uuid" : "63158edc-915f-3a2b-975c-0e0ac4ed44c0",
2 + #"states" : [
"frontmosY , s &t"
],
"killD r VDelta" : 4345,
"genCount" : 0,
"age" : 6544807Q C M #78,
"purgeable" : 0,
"fds" : 50,
"coalition" : 1718,
"rpagesC ; + :" : 134278,
"reason" : "per-process-limit",
"pid" : 14206,
"cpuTime" : 23.955463999999999,
"name" : "w $ Q C R v R杭城小刘",
"lifetimeMax" : 1? ` - , ]34278
},
// ...
]
}

iPhone 1Q ` ~ d V K 1 S 81 Pro/13.3.1 手机 OOM 临界F ; = o @ 3 y x值为:(16380 I 8 Z4*134278)/(1024*1024)=2098.09375M

iOS 体| | 4 g 5 ; u系怎么发现 Jetsam ?

MacOS/iOS 是一个 BSD 衍生而F b S N : C x S来的体系,其内核是 Mach,可是关于上层露出的接口一般是依据 BSD 层对 Mach 的包装后的。8 R v 9 o Y @ 6 ;Mach 是一个微内核的架构,真实的虚拟内存办理也是在其间进行的,BSD 对s / H 4 X q内存办理供给了上层接口。Jetsam 工作也是由 BSD 产生的。bsd_init 函数是k } G 7 Z L 5 j入口,其间基本都是在初始化各个子体系,比方虚拟内存办理等。

// 1. Initiali* 1 1  | y K T yze the kernel6 S V v 9 H Z memory allocator, 初始化 BSD 内存 Zone,这个 Zona ; ~e 是依据 Mach 内核的zone 构建
kmG = 1 a , : Feminit();
// 2. Initialise bi W P T t L + wacy Y ) w # X ! Wkground freezing, iOS 上独有的特性,内存和进程的休眠的v s Z z C l & C E常驻监控线程
#if CONFIG_FREE * rZE
#ifndef CONFIG_MEMORYSTATUS
#error "CON/ H C Y _ - 4 / 8FIG_FREEZE defined without matching/ o 5 N n @ ! m X CONFIG_MEMORYSTATUS"
#endif
/* Initialise backgroh _ zund freezing; , ` c D % 7 D */
bsd_init_kprintf("calling memorystatus_freeze_init\n");
memorystatus_freeze_init();
#endif>
// 3. iOS 独有,JetSAM(即低内存工作的常驻监控线程)
#if C$ 2 H D . [ + S kONFIG_MEMORYSTATUS
/* Initialize kernel memory status notifications */
bsd_init_kprintf("calling memorystatus_init\n");
memorystatus_init();
#endif /* CONFIG_MEMORYSTATUS */

首要效果便是敞开了2个优先级最高的线程,来监控整个体系的内存状况。

CONFIG_FREEZE 敞开时,内核对进程进行冷冻而不是杀死。冷冻功用是由内核中发动一个 memorystatug M z 6 ] - Hs_free* - = A 6 h 6 ,ze_thread 进行,这个进程在收到信号后调用 memorystatus_freeze_top_process 进行冷冻。

iOS 体系会敞开优先级最高的线程 vm_pressure_monitor 来监控体系的内存压力状况,并经过一个仓库来保护一切 App 进程] o e。iOS 体系还会保护一个内存快照表,用s 5 s n { 4 –于保存每个进程内存页的耗费状况。有关 Jetsam 也便是 memorystatus 相关的逻辑,能够在 XNU 项目中的 kern_memorystatus.h 和 **kern_memorystatus.c **源码中检查。

iOS 体系因内存占用过高会强杀 App 前,至少有 6秒钟能够用+ ) H : a来做优先级判别,JetsamEvent 日志也是在这6秒内生成的。

上文提到了 iOS 体系没有交流空间,所以引进了 MemoryStatus 机制k 1 E(也称为 Jetsam)。也便是说在X u Z T @ f { C iOS 体系上开释尽或许多的内存供% n ( T s ! n当时 App 运用。这个机制8 D ^ ; l表现在优先级上,便是先强杀后台运用;假设内存e % / |仍是不行多,就强杀掉Y s + 2 Z 当时运用。在 MacOS 中,MemoryStatus 只会强杀掉符号为空闲退出的进程。

MemoryStatus 机制会敞开一个 memorystatus_jetsam_thread 的线程,它担任强杀 App 和记载日志,不会发送音讯,所以内存压力检测线程无法获取到强杀 App 的音讯。

当监控线程发现某 App 有内存压力时,就宣布告诉,此q d / L U J z } &刻有内存的 App 就去履行 didReceiveMem6 o d 3 k ? U H ;oryWarT e @ F _ aning 署理办法。在这个时机,咱们还有时机做一些内存资源开释的逻辑,也许会避免 App 被体系杀死。

源码视点检查问题

iOS 体系内核有一个数组,专门保护线程的优先级。数组的每一项是一个包含进程链表的结构体。结构体如下:h G u * % 8 / O

#define MEMSTAT_BUCKET_COUNT (JETSAM_PRIORITY_MAX + 1)
tyX ~ Tpedef s: e 0 3truct memstat_bucket {
TAILQ_H= d YEAD(, proc) list;
int count;
} memstat_bucket_t;
memstat_bucket_t memstat_bucket[MEMSTAT_BUCKET_COUNT];P K .

在 kern_memorystatus.h 中能够看到进行优先级信息

#define JETSAM_PRIORITY_IDLE_HEAD                -2
/* The value -1 is ano R  I + [ alias to JE_ 5 C o L L | WTSAM_PRV I w 5 8 ^ [ 8 dIORITV n ` vY_DEFAULT */
#define JETSAM_PRIORITY_IDLE                      0
#define JETSAM_PRIORITYm g n o 8 K_IDLE_DEFERRED		  1 /* Keeping this around till all xnu_quick_tests can be ma ! 5 X { 0 / Q $oved away from it.*/
#define JETSAM_PRIORITY_AGING_BAND1		  Ji 6 !ETSV 6 : 6 ]AM_e O mPRIORITY_9 $ W j A d 4 tIDLE_DEFERRED
#define JETSAM_PRIORIT/ ] f L 2 F Y_BACKGROUND_OPPORTUNISTIC  2
#define JETSAM_PRIORITY_AGING_BAND2		  JETS~ & 0 S z _ I 1 :AM[  L R l_PRIORITY_BACKGROUND_OPPORTUNISTIC
#define JETSAM_PRIORITY_BACKGROUND                3
#define JETSw @ g ~AM_PRIORITY_ELEVATED_INACTIVE	  JETSAM_PRIORITY_BACKGROUND
#define JETSAM_PRIORITY_MAIL                      4
#define JETSAM_PRIORITY_PHONE                     5
#define JETSAM_PRIORITY_UI_SUPPORT                8
#define JETSAM_PRIORITY_FOREGROUND_SUPPORT        9
#defR 3 _ m o X 0 zine JETSAM_PRIQ Y 2 . U F I d vORITY_FOREGROUND               10
#define JETSAM_PRIO Y gRITY_AUDIO_AND_AA g M W ZCCT + B PESSORY      12
#define JETSAM_PRIORITY_CONDUCTOR                13
#define JETSAM_PRIORITY_HOME                     16
#define JEF / S | 0 . XTSAM_PRIORITY_EXEP @ yCUTIVE                17
#define JETSAM_PRIORITY_IMPORTANTM 0 {                18
#define JETSAM_PRIORITY_CRITICAL                 19
#define JETSAM_PRIORI# } @ T 4 9TY_MAX                      21

能够显着的看到,后台 App 优先级 JETSAM_PRIORITY_BACKGROUND 为3,前台 App 优先级 JETSAM_PRIORITY_FORE2 0 nGROUND 为10。

优先级规则是:内核线程优先级 > 操作体系优先级 > App 优先级。且前台 App 优先级高于后台运转的 App;当线程的优先级相一起, CPU 占用多的线程的优先级会被下降。

在 kern_memorystatus.c 中能够看到 OOM 或许的原因:

/* For logging clarity */
static const char *memorystatus_kill_cause_name[] = {
""								,= ( w X p x ;		/* kMemor/ * Y 2 L 6 kystatusInvalid							*/
"jetti/ ? q H a Tsoned"					,		/* kMemorystatusKilled							*/
"highwater"						,		/* kMemor] j 4 ; @ T - Q VystatusKilledHiwat						*/
"v* & { | .node-limit"					,		/* kMemorystatusKilledVnodes					*/
"vm-pageshortage"				,		/* kMemo~ b # b . [ L FrystatusKilledVMPageShortage			*/
"proc-thrashing"				,		/* kMemorystatusKilledProcThrast k - z 5 P = c ~hing				*/
"fc-thrashing"					,		/* kMemorystatusKilledFCThrashing				*/
"per-proceF $ + R J :ss-limit"				,		/* kMemorystatusKilledPerProc5 ! H % b R ] - ?essLimit			*/
"disk-space-shortage"			,		/* kMemorystatusKilledDiskSpaceShortage			*/
"idle-exit"						,		/* kMemorystatusKilledIdleExit					*/
"zone-map-exhaustion"			,		/* kMemorystatusKilledZoneMapExhaustion			*/
"vm-cA O y ) Z ! s w ]ompressor-tht ? 0 { X xrashing"		,		/* kMemorystat4 { S n KusKilledVMCompreS % ; Rssor} P Q 2 h i WThrashing		*/
"vm-compressor-space-shortage"	,		/* kMemory? : y E ( ` J U JstatusKilledVMCompressorSpaceShortage	*/
};

检查 memorystatus_init 这个函数中初始化 JetY 5 = @ ( C bsam 线程的要害代码

__private_extern__ void
memorystatus_init(void)
{
// ...
/* Init d t -ialize the jetsam_threads state array */
jetsam_thZ o Preads = kalloc(sizeof(strucP U K = ` t e 1t jetsam_thread_state) * max_jetsam_threads);
/* Initialize all the jetsa e ~ ] d  ! { [am threads */
for (i = 0; i < max_jetsam_threads; i++) {
result = kernel_thread_start_priority(memorystatus_thread, NULL, 95 /* MAXPRI_KERL @ x & | N h K #NEL */, &je. ! v Mtsam_threads[i].thread);
if (result == KERN_SUCCESS) {
jetsam_threads[i].inited = FALSE;
jetsam_threads[i].index = i;
thread_& , d j 2deallocate(jetsam_threads[i].thre6 F = had);
} else {
panic("Could not creh b ^ bate memorystatus_thread %d", i);
}
}
}
/*
*	Hi: M 5 ~ i vgh-levr ; T S Y { r f -el priority assignments
*
**************************************E k Z $*******************5 . r B l z****************
* 127		Re^ # K 6 V qsT _ Q ;erved (reM h { 7 ) c ~ ^al-tX - Fime)
*				A
*				+
*			(32 levels)
*				+
*				V
* 96		RE L [ V (ese2 c e ^ 3 a { S $rved (rJ Y beal-time)
* 95		Kernel mode only
*				A
*				+
*			(16 levels)
*				+
*				V
* 80		Kernel mode only
* 79		System high priority
*				A
*				+
*			(16 levels)
*				+
*				V
* 64		Syst0 + c Wem high priority
* 63		Elevated priori} l | ? C ) G o .ties
*				A
*				+
*			(12 levels)
*				+
*				V
* 52		Elevated priorith & j @ies
* 51		ElevaC + & c u @ 7 ) Tted priorities (incl. BSD +nice)
*				A
*				+
*			(20 levels)
*				+
*				V
* 32		Elevated priorities (incl. BSp i } + ; #D +nice)
* 31		DefY b ) . M e / Tault (default base for threads)
* 30		Lowered priorities (inclm w G + 1 e 0 |. BSD -nice)
*				A
*				+
*			(20 levels)
*				+
*				V
* 11		LowerA O t e ! 4ed priorities (incl. BSD -nice)
* 10		Lowered priorities (aged pri's)
*				A
*				+
*			(11 levels)
*				+
*				V
* 0		Lowered priorities (aged pri's / idle)
****8 Z M + E P l o E******************************************************- { 7 z P _ y**` $ M k ^ O d************F ? : / 4*
*/

能够看出:用户态的运用程序的线程不或许高于操作体系和内核。并且,用户态的运用程序间的线程优先级= K w ( i U r O v分配也有差异,比方处于前台的运用程序优先级高于处于后台的运用程序优先级。iOS 上运用程序优先级最高的是 SpringBoard;此外5 r a + V D W u线程的优先级不是原封不动的。Mach 会依据线程的运用率和体系全体负载动态调整线程优先级~ Q V w ( { j M。假设耗费 CPU 太多就下降线程优先级,假设线程过度挨饿,则会提高线程优先级。可是不管怎么变,程序都不能超越其所在线程的优先级区间规模。

能够看出,体系会依据内核发动参数和设备功用,敞开 max_je, S [tsam_threads 个(一般状况为1,特别状况下或许为, d 3 J _ )3)jetsam 线程,且这些线程的优先级为 95,也便是 MAXPRI_KERNEL(留意这儿的 95 是线程的优先级,XNU 的线程优先级区间为:0~127。上文的宏界说是进程优先级,区间为:-2~19)。

紧接着,剖析下 memorystatus_thread 函数,– S ~ L X w 7首要担任线程发动的初始化

static void
memorystaJ 7 E ^ m W ]tus_thread(void *param __unused, wait_result_t wr __unusY ! | J E d $ed)
{
//...
while (memorystaO _ P o Vtus_action_need- k t o % P ~ Yed()) {
boolean_t killed;
int32_t priority;
uinO W : h Q 8 & C vt32_t cause;
uint64_t jetsam_reason_code = JETSAMU J G 7 0_REASON$ L c v_INVALID;
os_reasoY 8 ! F Rn_t jetsam_reason = OS_REASON_NULL;
cause = kil] % N ( # K r  l_under_pressure_cause;
swit% B 4ch (cause) {
case kMemorystatusKilledFCThrashing:
jetsam_reason_code = JETSAM_REASON_MEMORY_FCTHRASHING;
break;
case kMemorystatusKilledVMCompressorThrashing:
jetsam_reason_code = JETSAM_REASON| i 1_MEMORY_VMCOMPRESSOR_THRASHING;
break;
casq a G 5 7 2 | , 7e kMemorystatusKilledVMCompressorSpaceShortage:
jetsam_reason_code = JETSAM_REASON_MEMORY_V` k DMCOMPRESSOR_SPACE_SHORTAGE;
break;6 c E v U M
case kMemorystatusKilg d $ ; 7 4 0 |ledZoneMapExhaustion:
jetsam_reason_code = JETSAM_RE| o _ d N i 1ASON_ZONE_MAP_EXHAUSTION;
break;
case kMemorystatusKilledVMPageShortage:
/* falls through */
default:
jetsam_reason_code = JETSAM_REASON_MEMORY_VMPAGESHORTAGE;
cause = kMemorystatusKilledVMPag3 x d 3 n 8 o VeShortage;
break;
}
/* Highwater */
boolean_t is_critical = TRg p l Q % r # ^ yUE;
if (memorystatus_act_on_hiwat_processes(&errors, &hwm_kip n D { F m n Q Sll,M 5 k ; &post_snapshot, &is_critical)) {
if (is_X e T Ecritical == FALSE) {
/*
* For nowE 8 0 D, don't kill any other processes.
*/
break;
} else {
goto done;
}
}
jetsam_reason = os_reason_create(OS_REASON_JETX % O S 8 @ l ` gSAM, jetsam_reason_code);
if (jetsam_reason == OS_REASON_NULL) {
printf("memorystatus_thread: fa, A W V U ! ] , #iled to alloca= + N / 2 }te jetsam reason\n");
}
if (memorystatus_act_aggressive(caO P : i y L 2 |use, jetsam_reason, &jl4 k v id_idle_kills, &corpse_list_purged, &post_sn` - C 8 H Tapshot)) {
goto done;
}
/*
* mL ) M k k $ I [emorystatus_kill_top_process() drops a reference,
* so take another one so we can continue to use this exit reason
* even after it returns
*/
os_reason_ref(jetsam_re% g ~ r 4ason);
/* LRU */
killed = memorystatus_kill_top_process(TRUE, sort_flag, cause, jetsam_reason, &priority,F h O &errors);
sort_flag = FALSE;
if (killed) {
if (memorystatus_poj W / i 6 s A ^ st_snapshot(priority, cause) == TRUE) {
post_snapshot = TRUE;
}
/* Jetsam Loop Detection */
if (memorystatus_jld_enabled == TRUE) {
if ((priority == JETSAM_PRIORITY_IDLD ` 9 (E) || (priority == system_procs_aging_ban; * 8d) || (priority == applicatio0 / C t U . ` n &ns_aging_band)) {
jld_idle_kills++;
} elsh Z ;e {
/*
* We've reached into bands beyond idle deferred.
* We make no attempt to monitor them
*/
}
}
if ((priority >= JETSAM_PRIZ X H ( R c f ?ORITY_UI_SUPPORT) && (total_corpseT h o S zs_count() > 0) && (corpse_list_purged d yd == FALSE)) {
/*
* If we have jetsammed a process in or above JETSA+ A I w ` Q j 3M_PRIOC o ~RITY_UI_SUPPORT
* then we attempt to relieve pressure by purging corpse memory.
*/
task_purge_all_corpses();
corpse_list_purged = TRUE;
}
goto done;
}
if (memorystatus_avail_pages_below_critical()) {
/*
* Still under pressure and unable to kill a process - purge corpse memory
*/
if (total_corpses_count() > 0) {
t= - . q M = e U cask_purge_all_corpses();
corpse_list_purS 3 h cged = TRUE;
}
if (memorystatus_avail_pages_below_critd I E q v 5 X hical()) {
/*
* Still u, C } I 3 )nder pressure and unable to kill a process - panip O ; L W P o ! Wc
*/
panic("memorystatus_jetsam_thread: no victim! available pages:%llu\n", (uint64_t)memorystatus_h 2 3 f C  ; l /availas j W 3 ibleX s R d s_pages);
}
}
done:
}

能够看到它敞开了一个 循环,memorystatus_action_needed() 来M J Y o & K X作为循环条件,持续开释内存。

static boolean_t
memorystatus_action_needed(void)
{
#if CONFIG_EMBEDDED
retu5 I ` {rn (is_reason_r ! & Uthrashing(kill_under_pressure_cause) ||
is_reason_zone_map_exhaustion(kill[ t f_under_pressure_cause) ||
memoryZ n J l : f Nstatus_available_pages <= mem2 L _o4 ( 0 J e irystatus_available_pa+ , ) & Pges_pressure);
#else /* CONFIG_EMBEDDED */y : D z . $ v U 
return (is_reason_thrashing(kill_under_pressure_cause) ||
is_reason_zone_map_exhaustion(kill_under_pressure_cause));
#endif /* CONFIG_EMBEDDED */
}

它经过 vm_pagepout 发送的内存压力来判别当时内存资源是否严重。几种状况:频繁的页面换出换进 is_reason_th* f N ) & 5 h Yrashina 9 c % _ Zg, Mach Zone 耗尽了 is_reason_zone_map_exhaustion、以及可用的页低于了 memory status_available_pages 这个门槛。

持续看 memorystatus_thread,会发现内存严重时,将先触发 High-water 类型的 OOM,也便是说假设某个进程运用进程中超越e r V 了其运用内存的最高约束 hight water mark 时会产生 OOM。在 memorystatus_act_on_hiwat_prK i vocesses() 中,经过 memorystatus_kill_hiwat_proc() 在优先级数组 memstat_bucket 中查找优; z h ) ! W E先级最低的进程,假设进程的内存小于阈值(footprint_in_bytes <= memlimit_in_bytes)则持续寻觅次优先级较低的进程,直到找到占用内存超越阈值的进程并杀死。

一般来说单个 App 很难触碰到 high water mark,假设不能完毕任何进程,终究走到 memorystatus_act_aggressive,也便是大多D K = T 0 l 5 3 C数 OOM 产生的当地。

static boolean_t
memorystatus_act_aggr: 3 R . + g Bessive(uint37 % # e C s # V p2_t cause, os_reason+ E ; 5 #_t jetsam_reason, int *jld_idle_kills, booleaU @ % r d ) R ;n_t *corpse_list_purged, boolean_t *post_snapshot)
{
// ...
if ( (jld_bucket_count == 0) ||
(jld_now_msecs > (jld_timestamp_msecs + memoF [ c @ 8 E j | nrystatus_jld_eval_period_msecs))) {
/*
* Refresh evaluation parameters
*/
jld_timestamp_msecs	 = jld_now_mseO S { ( [ d ( Hcs;
jld_idle_kil6 g i = a s [l_candidates = jld] n & i ! e E 0_bucket_count;# 2 }
*jld_idle_kills		 = 0;
jld_eval_aggressive_count = 0;
jld_priority_band_max	= JETSAM_PRIORIy I ~ S )TY_UI_SUPPORT;
}
//...
}

上述代码看到,判别要不要真实履行 kill 是依据必) v [ F 4 S t定的时刻间判别的,条件是 jld_now_m] z s Qsecs > (jld_timestamp_msecs + memoryU 5 . A rstatus_jld_eval_period_msecs。 也便是在 memorystatus_jld_eval_peri~ d 8 y |od_msecs 后才产生条件里边的 kill。

/* Jetsam Loop Detection */
if (max_mem <= (512 * 1024 * 1024)) {
/* 512 MB dev| 8 4 U d N nices */
memorystatus_jld_eval_period_msS m t P Recs = 8000;	/* 8000 msecs == 8 second window */
} else {
/*c 8 a 1GB and larger devices */
memorystatus_jld_eval_peri7 T Aod_msecs = 6000;	/* 6000 msecs == 6 second window */
}

其间 memorystatus_jld_eval_period_msecs 取值最小6秒。所以Q X . m } $ . , 9咱们能够在6秒内做些处理。

3.2 开发者们整理所得

stackoverflow 上有o e ,一份数据,整理了各种设备的 OOM 临界值

device crash amount:MB total amount:MB percentage of total
iPad1 127 256 49%
iPad2 275 512 53%
iPad3 645 1024 62%
iPad4(iOS 8.1) 585 1024 57%
Pa% [ 3d Mini 1st Gene[ f z Mration 297 512 58%
iPad7 W n , ^ ? Miny / pi retina(iH m DOS 7.1) 696 1024e = } ` 2 E | L 68%
iPad Air 697 1024 68%
iPad Air 2(iOS 10.2.1) 1383 2048 68%
iPad Pro 9.7″(iOS 10.0.2 (14A456)) 1395 1971 71%
iPad Pro 10.5”(iOS 11 beta4) 3057 4000 76%
iPad Pro 12.9” (2015)(iOS 11.2.1) 3058 3Z T 9 #999 76%
iPad 10.2(iOS 13.2.3) 1844 2998 62%
iPod touch 4th gen(iOS 6.1.1) 1 G ^ ` R { o =30 256 51%
iPod touch 5th gen 286 512 56%
iPhone4 325 512 63%
iPhone4s 286 512 56%
iPhone5 645 1024 62%
iPhone5s 646 1024 63%
iPhone6(iOS 8.x) 645 1024 62%
iPhone6 Plus(iOS 8.x) 645 1024 62%
iPhone6s(iOS 9.2) 1396 2048 68%
iPh/ C } j b Zon* N e A 7 } n !e6s Plus(iOS 10.2.1) 1396 2048 68%
iPhoneSEk P 8(iOS 9.3) 1395 2048 68%
iPhone7(iOS 10.2) 13? i c , –95 2048 68%
iPhone7 Plus(iOS 10.2.1) 2040 3072 66%
iPhone8(iOS 12.1) 1364 1990 70%M B B C $ F
i2 I dPhoneX(iOS 11.2.1) 1392 2785 50%
iPP $ q ] . EhoneXS(iOS 12.1) 2040 3754 54%
iPhoneXS Max(iOS 12.1) 2039 3735P ? + | F 1 55%
iPhoneXRE 2 u $ 8 a(iOS 12.1) 1792F J K B 2813 63%
iPhoR w K p V Fne11(y + |iOS 13.1.3) 2068 38( Z J C N T U q44 54%
iPhone11 Pro Max(iOS 13.2.9 g w3) 2067 3740 55%

3.3 触发当时 App 的 hY F + / G gigh water mark

咱们能够写守时器,不断的恳求内存,之后再经过 phys_footprint 打印当时占用内存,按道理来说不断恳求内存即可触发 Jetsam 机制,强杀 App,那么终究一次打印的内存占用也便是当时设备的内存上限值

timer = [NSTimer scheduledTimerWit$ 5 * * ( .hTimeInterval:0.L N n * N R 7 N01 target:self selector:@selector(al6 U K V s j E olocateMemory) userInfo:nil repeats:YES];
- (void)allocateMemory {
UIImageView *{ 1 z qiy p [ tmageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
UIImage *$ | ? Q b oimage = [UIImage imageNamed:@"AppIcon"];
imageView.image = image;
[array addObject:imageView];
memoryLimitSizeMB = [self usedSizeOfMemory];
if (me_ P ! WmoryWarningSizeMB && memoryLimitE y t h ) 3 zSizeMB) {
NSLog(@"----- memory warnW ~ % q 9ning:%dMB, memory limit:%dMB", memoryWarningSizeMB, memoryLimitSizeMB);
}
}
- (int)usedSizeOfMemo, t j ~ry {
task_vm_info_data_t taskInfo;V R Z d : _ 
mach_msC D b k tg_type_number_t info, @ jCount = TASK_VM_INFO_COUNT;
kern_return_t kernReturn = task_info(mach_Q 9 U I M Gtask_s( S ( e D | |elf(), TASK_VM_Iw H $ v O y F _ CNFO, (task_info_t)&taskInfo, &infoCount);
if (kernReturn != KERN_SUCCESS) {
retu[ G ! u # ; &rn 0;
}
return (int)(taskInfo.phx 7 F Hys_footprig Q $ M 1 G 7 8nt/1024.0/4 W P P1024.0);
}

3.4 适用于 iOS13 体系的获取办9 u y ) 7 C O q P

iOS13 开端 &lA R j e y nt;os/p # n 1 b x ` H mproc.h> 中 size_t os_proc_available_memory^ P P 1 z - T .(void); 能够检查当时可用K ? C ] T内存。

Return Value

The number of bytes that the app may allb 0 { B 3 _ocate before it? w . ] W f Y hits its memory limit. If the calling process isn’t an app, or if the process has already e6 I ( Mxceeded its memory limit, this function returns 0.

Discussion

Call this function tf l fo determL 5 L 0ine the amount of memory available to youj l f ) [ {r appC ; M & % : n #. The returned value corresponds to the current memory limit minus the memory footprint of your app at the time of the function call{ ; 1 2. Your app’s memory footprint consists of the data thatG a D * 3 6 { 1 you allocated in RAM, ap M W f s 3nd that must stay in RAMS W X H M * (or tx y * # q B Mhe equivalz k u # = x _ &ent) at all times2 Q C Q C O 1 ~. Memory limits can change during the app life cycle and don’t necessarilyL 9 ~ _ correspond to the amount of physical memory available on the device.

Use theN p l B T ret~ ( + M O P eurned value as advisory informat4 ` L ? M c f 9ion only and don’t cache iV b 5 w W vt. The precise value changes when your app does any work that affects memory, which can happee 9 e ~ ( X Y Xn frequently.

Although this function lets you determine the amount of memory your ag [ Hpp may sa/ ) Y w ] ^ #fely consume, don’t useQ ? ? l 1 n 7 L it to maximize your app’s memory usage` $ 6 b 5 1 ? V. Significant memory use, even when under the current i m { u [ N memory lC ^ _ @ 6imit, affects system performance. For examplet = q, when your app consumes all of its available memo: 6 ! M T + / D xry, the system may need to terminate other apps and system processes to accommodate y7 l F [ 0 z 3 eour app’s requests. Instead, always consume the smallest amount of memory you need to be responsive to the user’s needs.

If you need more detailed information about the available~ 7 M w . mE { X = y aemory resource_ K H x 3 U t P !s, you can c5 G e rall task_info. However, be aware that task_info is an expensive call, whereas this function is mua t 3 * zch more efficient.

if (@available(iOS 13.0, *)) {
return os_proc_available_memory() / 10249 L u 6 i.0 / 1024.0;
}

App 内存B } T I 3 Z信息的 API 能够在 Mach 层找到,mach_task_basic_info 结构体存储了 Mach task 的内存运用信息,其间 phys_footB 0 ] . o s t Wprint 便是运用运用的物理内存巨细S q K M ~ O H,virtual_size 是虚拟内存巨细。

#d) 2 ; ` K L 0efine MACH_TASK_BASIC_INFO     20         /* alwas d 6 o   7ys 64-bit basic info */
struct mach_task_basic_info {
mach_vm_size_t  virtual_size;       /* virtual meX = + 6 D %mory size (bytes) */
mach_vm_size_t  resident_size;      /* resident memory size (bytes) */
mach_vm_size_t  resident_size_max;  /* maxi[ 4 y Amum resident memory size (b1 c t )ytes) */
time_value_t    use@ 7 3 p t | Sr_time;          /* total user run time for
terminated threads */
time_value_t    system_time;        /* total system run time for
terminated threads */
policy_t        policy;             /* default policy for new thr7 { 4 6 v X ieads? ( b g M 3 y . */
integer_t       suspend_coun^ C | | Rt;      /* suspend count for task, I - u */
};

所以获取代码为

task_vm_i` ~  8 2 9 W B %nfo_data_t vmInfo;
mach_msg_S B - U jtype_number_t count = TASK_i W w O R g ~ + `VM_INFO_COUNT;
kern_return_t kr = task_info(mach_task_self(), TASK_VM_INFO, (task0 ~ ( F o : F F_i^ R f mnfo_t)&vmInfo, &count);
if (kr != KERN_SUCCESS) {
return ;
}
CGFloat memoryUsed = (CGFloat)(vmInfo.phys_footprint/1024.0/1024.0);

或许有人猎奇不应该是 resident_size 这个字段获取内存的运用状况吗?+ ? m 6一开端测试后发现 resident_size 和 XL 9 s T ! `code 丈量成果差距较大。而运用 phys_footprint 则挨近于 Xcode 给出的成果。且能够从 WebKit 源码中得到印证。

所以在 iOS13 上,咱们能够经过 os_pro7 + b # pc_available_memory 获取到当时能够用内存,经过 phys_fV 1 h Iootprint 获取到当时 App 占用内存,2者的和也便是当时设备的内存上限,超越即触发 Jetsam 机制。

- (CGFloat)limitS7 # 2 8 0 cizq V 5 neOfMemory {
if (@available(iOS 13.0, *)) {
task_vm_info_data_t taskInfo;
mach_msg_tyb ) l  R ipe_number_t infoCount = TASK_VM_INFO_COUNT;
kern_return_t k` d fernReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&taskInfo, &infoCount);
if (kernReturn != KERN_SUCCESS) {
return 0;
}
return (CGFloat)((taskInfo.phys_footprint + os_proz , i %c_avaiE k mlable_memory()) / (1024.0 * 1024.0);
}
return 0;
}

当时能够运用内存:1435.936752MB;当时 ApH u i m E k Ap 已占用内存:14.5MB,临界值:1435.936752MB + 14.5MB= 1450.436MB, 和 3.1 办法中获取到的内存临界值相同「iPhone 6s plus/13.3.1 手机 OOM 临界值为:(16384*928g W O 06)/(1024*1024)=1450.09375M」。

3.5 经过 XNU 获取内存约束值

在 XNU 中,有专门用于获z X G n 2 }取内存Q h M 7上限值的函数和宏,能够经过 memorystatus_priority_ent[ U w nry 这个结构体得到一切进程的优先级和内存约束a o Y M W @值。

typedef struct memorystatus_priority_entry {
pid_t pid;
int32 s ~ # +  Y2_t priority;
uint64_t user_data;
int32_t limit;
u, 8 ; K q W %int32_t state; k m m ) & i ] ~;
} memorystatus_priority_entry_t;

其间,priority 代表进程优先级,limD w ( R Jit 代表进程的内存约束值。可是这种办法需求 root 权限,由于没有越狱设备,我没有尝试过。

相关代码可查阅 kern_memorystatus.h 文件。需求用到函数 int me0 z g ] ~morystatus_cS , #ontrol(uint32_t commz e t o ( { Kand, int32_t pid, uint32_t flags, void *buffer, size_t buffersize);

/* Commands */
#define MEMORYSTATUS_CMD_GET_PRIORITY_LIST            1
#define M- ] S t & TEMORYSTATUS_CMD_SET_PRIORITt | `Y_PROPER0 { T KTIES      2
#define MEMORYSTATUS_CMD_GET_JETSAF A p , ` B M MM_SNAPSHOT          3
#define M, J K / H JEMORYSTATUS_CMD_GET_PRESSUO - E +RE_STATUS          4
#define MEMORYSTATUS_C; ~ / 7 6 ! _ 8 UMD_SET_JETSAM_HIGH_WATER_MARK   5    /* Set active memory lK z % 6 R + % 1 Fimit = inT 1 lactive memory limit, both non-fatal	*/
#define MEMORYSTATUS_CMD_SET_JETSAM_TASK_LIMIz S * 9 C R ; ; yT	      6    /* Set acl 2 Utive memory limit = inactivy } W L ge memory limit, both fatal	*/
#def~  _ hine MEMORYSTATUS_CMD_SET_MEMLIMIT_PROPERTIES      7    /* Set m% 5 nemory limits plus attributes independently			*/
#define MEMORYSTATUS_CMD_GEl Z D 3T_MEMLIMIT_PROPERTIES      8    /* Get m; L J b 9emory limits plus att, b F Rributes					*/
#define MEMORYSTATUS_CMD_PRIVILEGED_LISTENER_ENABLE   9    /* Set the task's statv 8 ^us as a priv` 3 X i { = L Milegedx x B  m i = h listener w.r.t memory notifications  */
#define MEMORYSTATUS_CMD_PRIVILEGED_LISTENER_DISABLE  10   /* Reset the task's status as a privileged listener w.r.t memory notifications  */
#define MEMORYSTATUS_CMD_AGGRESSIVE_JETSAM_LENIENT_MODE_ENABLE  11   /* Enable the 'lenient' mode for aggressive jetsam. See comments in kern_memorystatus.c near the top. */
#define MEMORYSTATUS_CMD_AGGRESSIVE_JETSAM_LENIENT_MODE_DISABLE 12   /* Disable the 'lenient' mode for aggressiv# r S . g S Ve jetsam* + $ | u + ! 0. */
#define MEMORYSTATUS_CMD_GET_MEMLIMIT_EXCESS          13   /* Compute how much a process's phys_footprint exceeds inactive3 ~ } J [ ` e t r memory limit */
#define MEJ , [ q u C G 6 :MORY) / ^ R U P y uSTATUS_CMD_ELEVATED_INAh : 8CTIVEJETSAMPRIORITY_ENABLE 	14 /* Set the inactive jetsam band for a process to JETSAM_PRIORITYG H M W t 0 A_ELEVATED_INACTIVE */
#define MEMORYSTATUS_CMD_ELEVA# H v w ^ } 5 B 0TED_l = @ R f A 4 x ?INACTIVEJETSAMPRIORITY_DISABLE 	15 /* Reset the inactive jetsam band foe u V ! O f | G Mr a process to the default band (0)*/
#define MEMORYSTAR = - Z - 9 : ETUS_CMD_SET_f M 6 - x  ^PROCESS_ISD n e c 3 r_MANAGED       16   /* (Re-)Set state on a process that marks it as (un-)managed by a systeX Q w - u ^ -m entityb $ _ w v n { e.g. assertiond[ ^ . ! r */
#define MEMORK ~ y P K CYSTATUS_CMD_GET_PROCESS_IS_MANAGED       17   /* Return the 'managed' stE 1 matus of a process */
#define MEMORYSTATUS_CMp Z bD_SET_PROC~ E 6 Y X G .ESS_ISj * A 1 i X J ]_FREEZABLE     18   /* Is tha Z 6 e procel L p q f 4ss eligible for freezing? Apps and extensions can pass in FALSE to opt outG W { i U 7 O j of freezing, i.e.,

伪代码

struct memorystatus_priority_entry memStatus~ q I C l 3  # o[NUM_ENTRIES];
siz} - m 1 l  m g e_t count = sizeof(stru@ i D n $ct memorystatus_priority_entry) * NUM_ENTRIES;
int kernResult = memoryI O ? @ E Wstatus_control(MEMORYSTATUS_CMD_GET_PRIORITY_g @ ! N F TLIST, 0, 0, memStatus, count);
if (rc < 0)b $ j D e K % T {
NSLog(@"memorystatus_control");
return ;# 3 u P
}
int entry = 0;
for (; rc > 0; rc -= sizeof(struct memorystatus_priority_entry)){
priY U 7 D 2 wntf ("PID: %5d\tPriority:%2d\tUser Data: %llx\tLimit. l w c 8 0 ! &:%2d( ` B y 4 J _\tState:%s\n",
memsta{ 5 E Z 6tus[ep 2 +ntry].pid,
memstatus[entry].priority,
memstatus[entry].user_data,
memstatus[entry].limit,
state_tq c L ro_text(memstatus[entry].state));
entry++;
}

for 循环打印出每个进程(也便是 App)的 pid、Priority、User Data、Lg M N ~ &imit、State 信息。从 log 中找出优先级为10的进程,即咱们前台运转的 App。为什么是, Q * R10? 由于 #definB A u O 9e JETSAM_PRIORITY_FOREGROU5 r 0 - J nND 10 咱们的意图便是获取前台 App 的内存上限值。

4w ; ;. 怎么断定产生了 OOM

OOM 导致 crash 前,app 必定会收到低内存正告吗?

做2组比照试验:

// 试验1
NSMutableArray *array = [NSMutableArray array];
for (NSIn . T enteger index = 0; index < 10000000; index++) {
NSString *filePatS 8 M : 4 D 1 4h = [[D C 5 m `NSBundle mainBundle] pathForResource:@"Info"b @ ] * { p d ofType:@"plist"];
NSData *data{ x L K Y 3 = [NSDH M B / f r |ata dataWithContentsOfFile:filePath];
[array? & 6 G X s e B addObject:data];
}
// 试验2
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(# ^ A , 1 % + Q0, 0), ^{
NSMutableArray *array = [NSMutabl N #leArray array];
fo+ x D Wr (N, 6 } [ P 9 ;SInteger index = 0; index < 1J Q r0000000; index++) {
NSStS ) % n 2 W O k uring *filePath = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@"plist"];
NSData *data = [NSData dataWithContentsOfFile:filePath]! _ b;
[array addObject:data]U Q - n / N X;
}
});
}
- (void)didReceiveMemoryWarning
{
NSLog(@"2");
}
// AppDelegS  l * / = h c ;ate.m
- (void)applicationDi$ l h gdReceiveMemoryWarning:(UIApplication *)application
{
NSLog(@"1^ l R h");
}

现象:

  1. 在 viewDidLoad 也便是主线程中内存耗费过大,体系并不会宣布低内存正告,直接 Crasm e V J 1 H { ph。由于内存增长过快,主线程很忙。
  2. 多线程的状. % h N况下,App 因内存增长过快,会收到低内存正告,AppDelegate 中的applicationDidReceiveMemoryWarning 先履行,随后是当时 VC 的 df , s fidReceiveMemoryWarning

定论:

收到低内存正告不必定会 Crash,由于有6秒钟的体系判别时刻,6秒内内存下降了则不会 crash。产生 OOM 也不必定会收到低内存正告。

5. 内存信息搜集8 1 D D 6 ) y

要想准确的定位问题,就需求 dump 一切目标及其内存信息。当内存挨近体系内存上限的时分,搜集并记载所需信F – Y息,结合必定的数据上报机制,上传到服务器,剖析并修正。

还需求知道每个目标3 u O n % ^具体是在哪个函数里创立出来的,以便复原“案发现场”S + j 8 :

源代码(libmalloc/malloc),内存1 ^ O g * / 0 k分配函数 malloc 和 calloc 等默认运用 nano_zone,nano_zone 是小于 256B 以下的内存分配,大于 256B 则运用 scalable_zone 来分配。

首要针对大内存的分配监控。mall6 X . . e Noc 函数用的是] ; _ E ? v h } mH [ : j ,alloc_zone_malloU ^ 0 y H 5 P oc, calloc 用的是 malloc_z0 C G n ) 1 { _ sone_calloc。

运用 scalable_zone 分配内存的函数都会调用 malloc_logger 函数,由于体系为了有个当地专门核算并办理内存k B P = j O Q t分配状况。这样的规划也满意「收口准则」。

void *
malloc(size_t size)
{
void *x S t =retval;
retval = malloc_zone_malloc(default_zone, size);
if (retval == NULL) {
erL H e G i Nrno = ENOMEM;
}
return retval;
}
void *
calloc(size_t num_items, size_t size)
{
void *retval;
retval = malloc_zone_calloc(dem + e Ffault_zone, num_items, size1 6 8 E ( m e o);
if (retval == NULL) {
errno = ENOMEM;
}
return ret| y o 5 f j Jval;
}

首要来看看这个 default_zone 是什么东西, 代码如下

typedef struct {
malloc_zone_t malloc_zone;
uint8_t pad[PAGE_MAX_SIZE - sizeof(malloc_zone_t)];
} virtual_default_zone_t;
static virtual_default_zone_t virtual_default_zone
__attribute__((section("__DATA,__v_z2 3 o 3 U `oneU % t z 9 $ a")))
_Y [ G_attribute__((aligned(PAGE_MAX_SIZE))) = {
NULL,
NULL,
default_zone_size,
default_zone_malloc,
def7 u ( ;ault_zon} ( , v m Z A L {e_calloc,
default_zone_valloc,Y 9 ) s 0
default_, I ) ; = 5 t E zone_free,
default_zone_realloc,
default_zone_H . ` 3 . + 6 idestroy,
DEFAULT_MALLOC_ZONE_STRING,
default_zone_batch_malloc,
default_zons 1 = 1e_batch_^ 5 4 W I d ( ~free,
&default_zone_introspect,
10,
default_zone_memalign,
default_zone_free_definite_size,
default_zone_pressure@ A 9 d 8_relief,
dE S b -efault_zone_mall% 9 A % poc_claimed_addJ # ? G 2 {ress,
};
st[  yatic malloc_zone_t *default_zone = &aK ] amp;virtual_d+ g d | @efault_zone.malloc_zone;
static void *
default_zone_malloc(malloc_zone_t *zone, size_t size)
{
zone = rub V ? F Jntime_default_zone();
return zone->malloc(zone, size);
}
MALLOC_ALWAYS_INLINE
static inline malloc_zone_t *
runtime_default_zone() {
return (litY x ( j Ge_zone) ? lite_zE v F rone : inline_malloc_defav O  X } i 7 Qult_zone();
}

能够看到 default_zone 经过这种办法来初始化

static inline malloc_zone_t *
inline_malloc_defau~ 0 ? ` r ylt_zone(void)
{
_malloco z ? C e f Y_initialize_once();
// malloc_report(ASL_LEVEL_INFO, "In inline_malloc_default_zone with %d %d\n", malloc_num_zones, malloc_has_debug_zone);
return malloc_1 - G C 2 v  * .zones[0];
}

随后的调用如下
_malloc_initialize -> ck V A & T Treate_scalable_zone -> create_scalable_szone 终究咱们创立Y / U S D E了 szone_t 类型的目标,2 = i经过类型转换,得到了咱们的 def + W 8 rault_zone。

malloc_zone_t *
create_scalable_zone(size_tr h _ / initial_sT 2 g v 0 ! * {ize, unsigned debug_flags) {
return (malloc_zone_t *) creaG s y q K 5 7 b ftel { Q n r a_scalable_szone(initial_size, debug_flags);
}
void *malloc_zone_malloc(malloc_zone_t *zone, siA ~ ( Mze_t size)
{
MALLOC_TRACE(TRACE_malloc | DBG_FUNC_START, (uintptr_t)zone, size, 0, 0);
void *ptr;
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
if (size > MALLOC_ABSOLUTE_MAX_SIZE) {
return NULL5 : 8 f;
}
ptr = zone->malloc(zone, size);
// 在 zone 分配完内存后就开端运用 malloc_logger 进行进行记载
if (mall@ e $ M E F v q Roc_logger) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE, (uintpn P ytr_t)zone, (uintptr_t)size, 0, (uintptr_t)ptr, 0);
}
MALx = { a y d 2 rLOC_TRACE(TRACE_malln Z R 5 Koc | DBG_J = YFUNC_END, (uintptr_t)zone, size, (uintptr_t)ptr, 0);
retc c Iurn ptr;
}

其分配完结是 zone->malloc 依据之前的剖析,便是szone_t结构体目标中对应的m| u T ` ? a 6 $alloc完结。

; x : J u a创立szone之后,做了一系列如下的初始* ~ $ W S z O M化操作。

// Initia3 - ; P J 0lize the security token.
szone->cookie = (uintptr_tk n 7 5 V 9 w)malloc_( % { V , 8 : d Centropy[0];
szone->basic_zoner ; ) 5 = W E 1.version = 12;
szone->basic_zone.sy [ a Y y M n + Jize = (void *)szone_size;
szone->basic_ * w 2 } 1zone.malloc = (void *)sk ] m I _ J Qzone_malloc;
szone2 # f->basic_zone.calloc = (void *)szone_calloc;
szone->basic_zone.valloc = (void *)szone_valloc;
szone->basic_zone.fd ( k T R dree = (void *)szone_free;
szone->basic_zone.realloc = (v6 7 , - Yoid *)szone_realloc;
szd 0 1 d N $ Aone->basi~ E E k oc_zone.destroy = (void *)szone_destroy;
szone->basic_zone.batch_malloc = (void *)szone_batch_V W { K ; )malloc;
szone->bu 2 5 Q G : S C 3asic_zone.batch_free = (void *)szone_batch_free;
szone->basic_zone.introspect = (s- c l 2 K Ctruct malloc_introspection_t *)&szone_introspect;
szone->basic_zone.memalign = (void *)szone_memalign;
szone->basic_zone.free_defiP W i { nite_size = (void *)szone_free_definite_size;
szone->basic_zone.pressure_relief = (void *)szoJ ? J m [ 0 ! Mne_pressure_relief;
szone->basic_zone.claimed^ n ? h_address = (void *)szone_claimed_add# ! F O - C rress;

其他运用 scalable_zone 分配内1 G :存的函数的办法也类似,所以大内存的分配,不管外部函数怎么封装,终究都会调用到 malloc_logger 函数。所以咱们能够用 fi/ B ( ^ Xshhook 去 hook 这个函数,然后记载内存分配状况,结合必定的数据上报机制,上传到服务器,剖析并修正。

// For logging VM allocation and deallocation, arg1 here
// is the mach_port_name_t of the target task iI Z / 7 J n n which the
// ald q s q (locP h j - or dealloc is occurring. For example, for mma| q V $ i t mp()
// that would be mach_task_self(), but for a cross-task-capablb P h [ $ w [ ke
// call such as mach_vm_map(), it is the target task.
typedef void (malloc_log { p {  t I Sger_t)(uint32_t tyE e 3 n 6 G O kpe, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uG n k . Cintptr_t resz Y H X M ~ult, uint32_t num_hot_frames_to_skip);
extern malloc_logh ] | P + P H U Oger_t *__syscall_logger;

当 malloc_logger 和 __syscaw + Xll_logger 函数指针不为空时,malloc/free、vm_allocate/vm_deallocate 等内存分配/开释经过这两个指针告诉上层,这也是内存调试东西 malloc stack 的完结原理。有了这两个函数指针,咱们很简略记载当时存活目标的内存分配信息(包含分配巨细和分配仓库)。分配仓库能够用 backtrace 函数捕获,但捕获到的地址g $ i p o 是虚拟内存地址,不能从符号表 dsym 解析符号。所以还要记载每个 image 加载时的偏移 slide,这样 符号表地址 = 仓库地址 – slide。

小 tips:

ASLR(Address space layout randomizationR c Y 4 ^ k { U):常见称呼为位址空间随机/ – A % Q h e .载入、位址空间装备随机化、位址空间布局随机化,是一种避免内存损坏漏洞被运用的核算机安全技能,经过随机放置进程要害数据区域的定址空间来放置攻击者能可靠地跳转到内存的特定方位来操作函数。现代作业体系一般都具备该机~ n @ ? ; F 1制。

函数地址 add: 函i T | b P q d + X数真实的完结地址;

函数虚拟地址:vm_add;

ASLR: slide 函数虚拟地址加载到进程内存的随机偏移量,每个 mach-o 的 slide 各不相同。vm_add + slid5 2 g P 2 ne = add。也便是:*(base +offset)= imp

由于P & g w _ n z L腾讯也开源了自W 9 } n b H 5 Y H己的 OOM 定位计划- OOMDeteu * Y 7 4 K = # 3ctor ,有了现成的轮子,那么用好就能够了,所以关于内存的监控思路便是找到体系给 App 的内存上限,然后当挨近内存上限值的时分,dump 内存状况,拼装根底数据信息成一个合格的上报数据,t x ` Q Z O E %经过必定的数据上报战略到服务端,服务端消费数据,剖析产生报表,客户端工程师依据报表剖析问题。不同工程的数据以邮件、短信、企业微信等办法告诉到该项意图 owner、开发者。(状况严重的会直H f E m c B u i W接电话给q m [开发者,并给主管跟进每一步的处理成果)。
问题剖析处理后要么发布新版别,要么 hot fix。

6. 开发阶段针对内存咱们能做些什么

  1. 图片缩放

    WWDC 2018 Session 416 – iOS Memory Deep Dive,处理图片缩放的时分直接运用 UIIP . R u * t 6 xmage 会在解码时r x ( e o g # y读取文件而占用一部分内存,还会生成中心0 | 9 b ` K l J m位图 bitmap 耗费很多内存。而 ImagO ` g 0 j S E G seIO 不存在上述2种坏处,只会占用终究图片巨细的内存

    做了2组比照试验:给 App 显现一张图片

    // 办法1: 19.6M
    UIImage *imageResult = [self scaleImage:[UIImage imageN7 ( h @ D ^ damed:@"test"]                                                  newSize:CGSizeMake(self.view.frame.size.width, se] f ] C P t 0lf.view.frame.size.height)];
    self.imageView.image = imageResult;
    // 办法2: 14M
    NSData *data = UIImagePNGRepresentation([UIImage imageNamed:@"! H H [ +test"]);
    UIImage *imageResult = [self scaledImageWithData:data 				    withSize:CGSizeMake(self.view.frame.size.width, self.view.frame.size.height) scale:3 orientation:UIImageOrientationUp];
    self.imageView.image = imageResult;
    - (UIIP k B 1 E ; A | (mage *, A ~ h B Y Q b 6)scaleImage:t Y $ l {(UIImage *)image newSY v _ize:(CGSize)newSize
    {
    UIGraphicsBeginImageContextWithOptions(newSize, NO, 0);
    [image drawInRect:CGRecta g ? Y % U lMake(0, 0, newSize.wiM d I Hdth, newSize.height)];
    UIImage *newIa 5 B f h emage = UIGraphicsGetImageFromCurrentImageContO n 5 S ! B cext();
    UIGraphicsEndImageCo? 3 g Q ) w u bntext();
    return newImage;
    }
    - (UIImage *)scaledI} } 8 . U B /mageWithData:(NSData *)data withSize:(CGSize)sizU f 4 5 x W p =e scale:(CGFloat)scale orientation:(UIImageOrientation)orientation
    {
    CGFloat maxPixelSize = MAX(: . Q { 5 esize.wids M &th, size.height);
    CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
    NSDictionary *options = @{(__| p Vbridge id)kCGImageSourceg W L l w $ 9CreateThT ! `umbnailFromImageAlways : (__bridge id)kCFBooleanTrue,
    (_7 y $ I o c C_bridge id)kCGImageSourceThumbnaJ 3 X J bilMaxPixel~ ; = } k 9 M Y dSize : [NSNumbw 2 h m ger= g C X | } # U b numberWithFlol W $ : V ) 6at:maxPixelSize]};
    CGImageRef imageRef = CGImageSourceCreateThu$ d nmbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options);
    UIImage *resultImage = [UIImage imageWithCGImage:imageRef scale:scale orientation:orientation];
    CGImageRelease(imageRef);
    CFReleasY G 3 N _ z . ee(sourceRef);
    return resultImage;
    }
    

    能够看出运用 ImageIO 比运用 UIImage 直接缩放占用内存更低。

  2. 合理N @ – M运用 autoreleasepool

咱们知道 autoreleasepool 目标是在 RunLoop 完毕时才开释。在 ARC 下,咱们假设在不断恳求内存,比方各种循环,那么咱们就需求手动增加 autoreleasepool,避免短时刻内内存猛涨产生 OOM。

比照试验

// 试验1
NSMutableD C 5Array *array = [NL p y V 1 - T h #SMuV ] 8 m 2 e l YtableArray array];
for (NSInteger index = 0;O ; ; ! L u 0 index < 10000000; index++) {
NSString *indexStrng = [NSString stringWithV k @Format:@"%zd", index];
NSSt` u ! , ! m k ering *resulc ! E 3  ?tString = [NSStrE j / @ing stringWithFormat:@"%zd-%@", index, indexStrk V Y ! n r x cng];
[array addObject:resultString];
}
// 试验2
NSMutableArray *array = [NSMutableArray array];
for (NSInteger index = 0; index < 10000000; index++) {
@autoreleasepool {
NSStx n E + Q 2 & M fring *indexStrng = [k y , F 4 ?NSString stringWithFormat:@"%zd", index];
NSSt| X D H - q C 8 .ring *resultString = [NSString strinm ! 9 + 0 , +gWithFormat:@"%zd-%@", index, indef + } T A fxStrng];
[ar= ! i $ T T Kray addObject:resultString];
}
}

试验1耗费内存 739.6M,试验2耗费内存 587M。

  1. UIG1 7 ~ Y d j * ;raphicsBeginImageContS z p k V dext 和 UIGraphicsEndImageContext 有必要成双呈现,否则会形成 context 走漏。别的 XCode 的 Analyze 也能扫出这类问题。

  2. 不管是翻开网页,仍是履行 js,都应该运用 WKWebView。UIWebView 会占用很多内存,从而导致 App 产生 OOM 的几率增加,而 WKWebView 是3 6 U一个多进程组件,N6 v 9 # l F ketwork Loading 以及 UI R M 5 lendering 在其它进程中履行,比 UIWebView 占用更低的内存开支。

  3. 在做 SDK 或许Q @ & App,假设场景D l Q g S是缓存相关,尽量运用 NSCache 而不是 NSMutableDictionary。它是体系供给的专门处理缓存的类,NSCa& R O Yche 分配的内存是 Purgeable Memory,能够由体系主动开释。NSCache 与 NSPureableData 的结合运用能够让体系依据状况收回内存,也能够在内存整理时移除目标。

    其他的开发习气就纷歧一描绘了,良0 f B K w R N z好的开发习气和代码认识是需求平时留意修炼的。

五、 App 网络监控

移动网络环境一直很杂乱,WIFI、2G、3G、4G、5G 等,用户运用 App 的进程中或许在这几品种N = R A ] .型之间切换,这也是移动网络和传统网络间的一个差异,被称为「Connection MigratioU J J d X / ;n」。此外还存在 DNS 解析缓慢、失利率高、运营商绑架等问题。用户在运用 App 时由于某些原因导致体会很差,要想针对网络状况进行改善,有必要有清晰的监控手段。

1. App 网络恳求进程

带你打造一套 APM 监控系统(一)

App 发送一次0 ! T – o @网络恳求一般会阅历下面几个要害进程:C f .

  • DNS 解析

    Domain Name system,网络域称号号体系,本质上便是将Z V 9 5域名IP 地址 彼此映射的一个分布式数据库,使人们更便P – s o z V O # j利的访问互联网。首要会查询本地的 DNS 缓存,查找失利就去 DNS 服务器查询,这其间或许会经过非常多的节点,涉及到递归查询和迭代查询| h # ?^ P (进程。运营商或许不干人事:一种状况便是呈现运营商绑架的现象,表现为你在 App 内访问某个网页的时6 G 5 ?分会看到和内容不相关y N m $) 1 N r广G V k ;告;另一种或许的状况便是把你的恳求丢给非常E N R !远的基站去做 DNS 解M f B D析,导致咱们 App 的 DNS 解析时刻较长,App 网络功率低。一般做 HTTPDNS 计划去自行处理 DNS 的问题。

  • TCP 3次握手

    关于 TCP 握手进程中为什么是3次握手而不是2次、4次,能够检查这篇文章。

  • TL! z ?S 握手

    关于 HTTPS 恳求还需求做 TLS 握手,也便是密钥7 } 1 ; ~ K n洽谈的进程。

  • 发送恳求

    衔接树立好之后V N 3 D 4 L { P就能够发送 request,此刻能够记载下 rG ; w U M qequest start 时刻

  • 等候回应

    等候服务器回来呼应。这个时刻首要取决于资源巨细,也是网络恳求进程中最为耗时的一个阶段。

  • 回来呼应

    服务端回来呼应给客户端,依据 HTTP he{ Z T B 5 o L 0ade` j _ 3 ,r 信息中的状况码判别本次恳求是否成功、是否走缓存、是否需求重定向。

2.Z 6 3 ) 监控原理

称号 阐明
NSURLConnection 现已被抛弃。用法简略
NSURLSession iOS7.0 推出,功用更强壮
CFNetwork NSURL 的底层,纯 C 完结

iOS 网络0 k ?结构层级关系如下:

带你打造一套 APM 监控系统(一)

iOS 网络现状是由4层组成的:最底层的 BSD Sockets、SecureTransport;次级底o H J K d +层是 CFNetwork、NSURLSessionN 0 $ :、NSURLConnection、WebView 是用 Obje0 4 g 6ctive-C 完结的,且调用 CFNetwork;运用层结构 AFNetworking 依据 NSURLSession^ F 、NSURLConnecP } : K y Btion 完结。

现在业界关于网络监控首要有2种:一种是经过 NSURLProtoj : * r i B 3 +col 监控、一种是经过 Hook 来监控。下面介绍几种办法来监控网络恳求,各有优缺点。

2.1 计划一:NSURLProtocol 监控 App 网络恳求

NSURLProtocol 作为上层接口,运用较为简略,但 NSURLProtocol 归于 URL Loading Syo o } + c = z uste, R Sm 体系中。运用协议的支撑程度有限,支撑 FTP、HTTP、HTTPS 等几个运用层协议,关于其他的协议则无法监控,存在必定的局限性。假设监控底层网络库 CFNetwork 则没有这个约束。

关于 NSURLProtocol 的具体做法在这4 ` b 7 @ T U x Y篇文章中讲过,承N } o B C继笼统类并完结相应的办法,自界说去主张网络恳求来@ 9 j b 0 c I i完结监@ a f Y & % * y 5控的意图。

iOS 10 之后,NSURLSessionTaskDelegate 中增加了W ? m V Y 一个新的署理办法! ~ } , m

/*p  P
* Sent whk { ) 8 M  , ]en complete statistics information ha% ! ? s bee^ $ b $ Q .n collected for the task.
*/
- (void)URL| T L & [ v Y pSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetr2 F 1 c S H Y K ~ics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));H | _ Y 9 4 [

能够( Y { w V = Y T LNSURLSessionTaskMetrics 中获取^ + 7到网络状况的各项目标。各项参数如下

@interface NSURLSeM - ]ssionTaskMetrics : NSObject
/*
* transactionMetrics array contains the metrics collected for every request/response transaction created during the task execution.
*/
@propertP  Ay (copy, readonly) NSArray<NSURLSessionTaskTransactionMetrics *> *transA l ( S ^ 1 H f sactionMetrics;
/*
* Interval from the task creation time to the task completion time.
* Task creation time is the time when the task was instantiated.
* Task completion time is the time when the task is about to chang| | +e iz % 3 R 6 c u )ts intern: k ] cal state to completed.
*/
@property (copy, readonly) NSDateInterval *taskIntervu q ual;
/*
* redirectCount is the number of redirep % V d M Bcts that w| 2 _ )ere recorded.
*/
@property (assign, reas L ; L Pdonly) NSUInteger redirectCount;
- (instancetype)init API_DEPRECATED(^ @ d K x ] M J F"Not supported", macos(10.12,10.15), ios(10.0,13.0),( H l | wg m 2 x / } _ J oatchos(3.0,6.0), tvos(10.0,13.0)) 1 ! $ H D = :)y r P * e , Q [;
+ (instancetype)new API_DEPRECATED("Not suppor( I o } P jted", macos(10.12,10.15. U r . b [ J), ios} V o P g K(10.0,13.0), watchos(3.0,6.0), tvos(10.0,13.0));. s , / | R p
@end

其间:taskInterval| R l g h J % R h明使命从创立到完结话费的总时刻,使命的创立时刻是使命被实例化时的时刻,使命完结时刻是使命的内部状况将要变为完结的时刻;redirectCount| z L I R : A & 标明被重定向的次数;transactio. ] p @nMetrics 数组包含了使命履行进程中每个恳求/呼应事务中搜集的目标,各项参数如下* G ?

/*
* This class defines the performance metrics collected for a request/response transaction during the task execution.
*/
API_AVAILABLE(macoss v 4 ?x(10.12), ios(10.0), watchos(3.0), tvos(10.0))
@interface NSURLSessionTaskTransacG n f A _tionMetrics : NSObjc a V W + *ect
/3 g y c o*
* Represen8 L k U Y M * F nts the transaction request. 恳求i 3 D W ` q事务
*/
@property (copy, readonly) NSURLRequest *request;
/*[ Z M P r b i v
* Represen _ y A l 4 4 ! uts the transaction response) C Q L c 1 F. Can be nil if error occurred and no respons; z ; e was generated. 呼应事务
*/
@propertl X X 5 ]y (nullable, coT H T z Xpy, readonly) NSURLResponse *response;
/*
* For all NSDate metrics below, if that aspect of the task could not be completed, then the corresponding “EndDate” metric will be nil.
* For example, if a name lookup was started but, M N 9 m the name lookupt B q 6 M a timed out, failed, or the client canceled the task before the name could bef ^ 0 * J resolved -- then while dv n ^omainLookupR 3 m , S e 7 $ !Star9 U v M K w OtDate may be set, domainLookupEndDate wim 8 S ] A -ll be nil along with aln ~ q 3 F 3l later metrics.
*/
// e G G q*
* 客户端开端恳求的时刻,不管是从服务器仍是从本地缓存中获取
* fetchStartDate returns the time when the user agent started fetc] ; { K & *hing the resource, whether or not the resource was retrieved from the ser w j _ Lver or local resources.
*
* The following metrics will be set to nil, if a persistent connection was used or the resource was retrieved from local resources:
*
*   domainLookupStartDate
*   domainLookupEndDate
*   connectSt: b } { Y yartDate
*   connectEndDate
*   secure/ d A m F U 1 S ;ConnectionStartDate
*   secureConnectionEndDate
*/
@property (nullable, copy, readonly) NSDate *fetchStartDate;
/*
* domainLookupSS O 3 m  0 HtartDx g k {ate returns the time immediB 8 K tately before the user agent started the name lookug ] 4p for the resource. DNS 开端解析的时刻
*/
@property (nullable, copy, readonly) NSDate *domainLookupStartDate;
/*
* domainLookupEndDate returns the time after the name lookup was completed. DNS 解析完结的时刻
*/
@property (nullable, copy, readonly) NSDate *domainLookupEndDate;
/*
* conneh g jctStartDate is the time immediately before the user agent started establishing the connection to the server.
*
* For example, tq ! 5 4 W ] G . ;his would correspond to the time immediately before the user agent started trying to estab( B A n d s t Ylish the TCPz w J n z K N 9 n connection. 客户端与服务端开端树立 TCP 衔接的时n : k 3 * $ u e k刻
*/
@property (nullable, copy, readonly) NSDate *connectStartDate;
/*
* If an encrypted connection was used, secureCG 3 U ) gonnectionStartDate is thJ ) & } De time immediately before the user agent started the sb H g Hecuf 1 | {rity handshake to secure the curre9 u | , ynt connection. HT; a eTPS 的 TLS 握手开端的时刻
*
* FoW ] /r example, this would correspond to the time immediately before the user agent started the TLS h) Y 2 Bandshak} 9 * * ^ L # L Ge.
*
* If an encrypted connecP ! + Eti& - Eon waso a U ~ not used, tm l T H C v 9his attribute is set to nil.
*/
@property (nullable, copy, readonly) NSDate *secureConnectionStartDate;
/*
* If an encrypted connection was used, secureConnectionEndDate is the time immediately after the security handshake completed3 [ H ;. HTTPS 的 TLS 握手完毕7 ~ 4 h A ,的时刻
*
* If an encrypted connection was not used, this attribute is set to nil.] D d e 5 ` =
*/
@property (nullable, copy, readonly) NSDate *secureCono @ X P v % fnectionEh P &ndDate;
/*
* connectEndDate is the time immediately after thg 2 G X f : k ` Te user agent finished establishing the connection to tt z X ]he server, including completion of security-related and other handshakes. 客户端与服务器树立+ U C TCP 衔接完结的时刻,包含 TLS 握手时刻
*/
@propO V v N Berty (nullable, copy, readonly) NSDate *0 @ i + ^ j ` connectEndDate;
/*
* requestStartDate is the time immediately before the user agent started requesting the sM v G 3ource, regardless of whether the resource was retrieved from the server or local- R ` y resourc} - . E g J T % ces1 D t ) w H .
客户端恳求开端的时刻,能够理解为开端传输 HTTP 恳求的 header 的榜首个字节时刻
*
* Fc @ e # k W J oH Y 6 0 9 A Cr example, thi{ g q 9s would correspond to the time immediateN 3 k 7ly before the user agent sent an HTv c : P & $ ; ~TP GET request.
*/
@propert$ q ^ s H z M k Zy (nullablA f Pe, copy, readonly) NSDate *requestStar[ ) [ f y 6 x . MtDate;
/*
* requestEnd4 e b z p x L 7Date i6 * ; m 5 P ~s the time immediately after the user agent finished requesting the source, regardless of whether the resource was retrieveZ q d U F I / ! Yd from the ser_ 4 I L - [ mver or local resources.
客户端恳求完毕的时刻,能够理解为 HTTP 恳求的终究一个字节7 j 6 ~ y c传输完结的时刻
*
* For example, this would correspoM b 6 1 ? d A Wnd to the time immediately after the user agent finished sending the last byte of the request.
*/
@property (nullablI | # 2 P o Xe, copy, readonl} | 2 ] G c a 1y) NSDate *requestEndDate;
/*
* responseStartDate is th3 F y  2 5 ~ d `e time imO ? 5 ? 5 pmediately after the user agent received the first byte of the response from the server or from local res . N % R H 3 wources.
客户端从服务端接纳呼应的榜首个字节的时刻
*
* For examps r , Hle, this would correspond to the tS 6 q V $ 7ime immediatelyE U W y ) after the user agent received the first byte of an HTTP response.
*/
@prU 8 , ?operty (nullable, copy, readonly) NSDate *responseSta5 V o / R UrtDb Z z hate;
/*
* responseEndDate is the time immed! y d Ziately after the user agent received the last byte of the resource. 客户端从服-  w x l %务端接纳到终究一个恳( r p C = R 1求的时刻
*/
@property (nulla9 q a D d T M Ible, copy, read2 S B L - konly) NSDate *responseEndDate;
/*
* The network protocol used to fetch the resource, as identified by the ALPN Protocol ID IdentiB y k O & G Z Rfication Sequence [RFC7301].
* E.g., h2, http/1.1, spdy/3.1.
网络协议名0 ? U,比方 http/1.1, spdy/3.1
*
* When a proxq 5 } t n 8 |y is configured AND a tunnell ) L . J | . k connection is established, then this attribute? : % . ! B returns the value for the tunneled protocol.
*
* For example:
* If no proxy were used, and HTTP/2 was negotiated, then h2 would be returned.
* If HTTP/1.1 were used to the proxy, and the tunneled connec7 , ^ D v Z T f rtion was HTTP/2, then h2 would be returned.
* If HTTP/1.F Z T H % 2 .1 were used tor G ~ V the proxy, and there were n* x 1 v & 0o tunnel, then http/1.1 would be returned.m R w W # D y
*
*/
@property (nullable, copy, readonly) NSString *networkProtocolName;
/*
* This property is set to YES if a proxy connection was used to feG ) ttch the resource.
该衔接是否运用了署理
*/
@prop# t - X Q Oer2 # Y O y 6ty (assign, readon9 w yly, getter=isProd w & T a V DxyConnection) BOOL proxyConnection;
/*
* This property is set to YES if a persistent connection was used to fetch the resource.
是否复用了现有衔接
*/
@property (assign, readonly, getter=isReusedConnection) BOOL reusedConnectir L z a [ Q e } )on;
/*
* Indicates whether the r_ l  o m ~esource was loaded, pushedR q e x X p T or retrieved from the local cac9 I 8 Y & t f @ ,he.
获取资源来源
*/
@property (assign, readonly) NSURLSessionTaskMetricsResourceFetchType resourceFetchType;
/*
* countOfRequestHeaderBytesSent is the number of bytes transferred for request header.
恳求头的字节数
*/
@propeM ; e N 6rty (readonly) int64_t countOfRequestHeaderBytesSent API_AVAG F t &ILABLE(macos$ 3 d U(10.15), ios(13.0), watchos(6.0), tvos(13.0));
/*
* countOfReque. 3 J Y  $ BstBodyBytesSent is the number of bytes transferred fof z ?r request body.| c * . X , [
恳求体的字节数
* It includes protocol-specific framing, transfer encoding, and2 E & S ) ? content encoding.
*/
@property (readonly) int64_t countOfRequestBodyByt1 O R c 2 `esSent API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.$ L e ` c - 0 c0));
/V b C U ` *
* countOfRequestBodyBytesBeforeEncodin9 j @g is the size of upload body di : / uata, file, or stream.
上传体数据o 6 h、文件、流的巨细
*/
@property (readonly) int64_t countOfRequestBodyBytesBeforeEncoding API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));
/*
* countOfResponseHeaderByt, S F V w !esReceived is the number of bytes transferred for response header.
呼应头的字节数
*/
@property (readonly) int64_t countOfResponseH* E J ceaderBytesReceived API_AVAILy c ! X m *ABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));
/*
* countOfResponseBodyBytesReceived is the number of bytes transfec a % 9 A & R hrred for response body.
呼应体的字节数
* It includes protocol-specific framing, transfer encoding, andS z v content encoding.
*/
@properJ ^ Qty (readonly) int64_t countOf2 F 2 L E ! - QResponseBody] ` / y pBytesReceived Ax m d ; ! gPI_AVAILABLE(macos(10.15), ios(13.0), w d b hatchos(6.0), tvos(13N o 9 a d ( @ @.0));
/*
* countOfResponseBodyBytesAfterDecoding is the size of data delivered to your delegate or completion ha} ) D b i $ Q L wndler.
给署理办法或许完结后处理的回调的数u m X据巨细
*/
@property (readonly) int64_t countOfRespoa j ) 6 j TnseBodyBytesAfterDecoding API_AVAILABLE(macos(10.15), ios(13.0), w3 y 8 - W ~ - u Zatchos(6.0), tvos(13.0));
/*
* localAddress is the IP address string of the local interface for the connection.
当时衔接下的本地p E G i / D ,接口 IP 地址
*
* For multipath protocols, t5 , [ q . r F 2his is the local address of the} J X W  q initial flow.
*
* If a connection was not used, this. S P attribute is set to nil.
*/
@property (nullable, co% # j L i u r D Tpy, readonly) NSString *localAddress API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));
/*
* localPort is thx T } #e porQ q ] 4  t Xt number ofv 6 } P 0 l the local interh e 5 5 G N Eface for the connection.[ z 
当时衔接下的本地端口号
*
* For multipath protocols, thiN ^ 0s is th? w 4 8e local porr % ) x F = Ct o0 F A ; `f the initial flow.
*
* If a connection was not used, this attribute is set to nil.
*/
@property (nullz F ? | | B $ Gable, cog T 1 d Dp3 E . 0 y }y, readonlj z + $ % !y) NSNumber *localPort API_AVAILABLa k M } - w L -E(macos(10.15), iost @ c g ? o ](13.0), watchos(6.0), tvos(13.0));
/*
* remoteAddress is the IP address st. [ 6 . ] 0 nring of the remote interface for the connecti. s F T c kon.n h 7 S
当时衔接下的远端 IP 地址
*
* For multipath protocols, thib @ c R a ( . & {s is the remote address of the initial flow.
*
* If a cj H F W V Zonnection was not usq 8 S zed, this attribute is set to nilL G y k M h.
*/
@property (nullable, copy, readonly) NSString *remotez -  BAddress API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0),[ F t e V F X tvoV 0 g 0 s(13.0));
/*
* remotb 9 9ePort is the poU } ? l {rt number of the remote interface for the connection.
当时衔接下的远端端口号
*
* For multipath protocols, this is the remote port of the initial flow.
*
* If a connection was not used, this attribute is set to nil.
*/
@propert` 4 - H 6 S p f #y (nullablz l L / o m Ae, copy, readonly) NSNumber *remotePort API_AVAILABLE(macos(10.15)z ! k, ios(13.0), watchos(6.0), tvos(13.0));
/*
* negot{ i biatedTLSProtocolVersion is the TLS protocol version negotiated for the connection.
衔接洽谈用的 TLS 协议版别号
* It is a 2-byte sequence in host byte order.
*
* PleaseN 2 F . [ O S * refer to tls_protocol_v$ & 9 (ersion_t e~ & j u - Z gnum in Security/SecP; 5 H l b p W , rrotocolTypes.h
*
* If an encrypted connection was not used, this attribute i- m m Rs9 s $ m ( @ 5 -  set to nill = X i & c / 2.
*/
@proper} F + ~ty (nullabv n C ? ^ .le, copy, readonly) NSNumber *negotiatedTLSProtocolVersion API_AVAILABLE(macos(1* Z 9 /0.15), ios(13.0), watchos(6.0), tvosj i n %(13.0));
/*
* negotiatedTLSCipherSuite is the TLS cipher suite negotiated for the coc D 7 % .nne3 | u Z Oction.
衔接洽谈用的 TLS 暗码套件
* It is a 2-byte sequej  8 [ S 8 x F ince in host byte order.
*
* Please refer to tls_cipher! ( l N wsuite_t enum in SeJ A )curity/SecProtocolTypes.G 5 r S 8 W e 9 7h
*
* If an encrypted connection was not used, this attribute is set to nil.
*/
@property (nullable, copy, readonly) NSNR } R N n t + ! Lumber *n, o _ . @egotiatedTLSCipherSuite API_d | M + V i ` fAVAILQ A  G X bABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));
/*
* WhetherB l y / L n O Q X the connection is established over a cellular interface.
是否是经过蜂窝网络树立的衔接
*/
@property (readonly, getter=isCellular) BOc a ! . D N N . IOL cellular API_AVAILABLE(macos(1$ W ) m A %0.15), ios(13.0), watchos(6H 6 W.0), tvos(13.0));
/*
* WhetherV + k d 7 r ) D j the connection is est! e Rablished over an expensive8 ^ j interface.
是否经过昂贵的接口树立的衔接
*/
@property (readonly, getter=isExpensive) BOOL expensive API_R T D P ^AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13 F v p.0));
/*
* Whether the connection is est[ : Hablished over a constraiz ^ q | u 8 ned interface.
是否经过受限接口树立的衔接
*/
@property (readonly, getter=isConstrained) BOOL constrained API_AVAU E / FILABLE(macos(10.15), ios(1c ) 1 Z T @ ,3.0), watchos(6.0), tvos(13.0));
/*
* Whether a multipath protocol is successfully negotiated for the connection.
是否为了衔接成功洽x N 1 W *谈了多途径协议
*/
@property (readonly, getter=isMultipath) BOOL multipaZ ; F n { : y B Tth API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));
- (il 4 $ ! u !nstancetype)init API_DEPRECATED("Not supported", macos(10.12,10.15), ios(10.0,13.0), watchos(3.0,6.0), tvos(10.0,13.0));
+ (instancetype)new API_^ t f 6DEPRECATED("Not supported", macos(10.12,10.15), ios(10.0,13.0), watchos(3.0,6.0), th L K y 4 Uvos(10.0,13.0));
@end

网络监控H 5 + y R Y M简略代码

// 监控根底z - & ) ` q 9 W信息
@interface  NetworkMonitorBaseS I T SDataModel4 8 R b Q f # : NSObject
// 恳求的 URL 地址
@propertyE ^ . b B ^ ] ` 7 (nonatomic, strong) NSString *requestUr` I , S z Sl;
//恳求头
@property (nonatomic, strong) NSArray *requesl G u _ MtHeaders;
//呼应头
@property (nonatomic, strong) NSArray *responseHeaders;
//GET办法 的恳求参数
@property (nonatom * U @ :ic, strong) NSString *getRequestParams;
//HTTPG r ( 办法,j - ` 比方 POST
@property (nonatomic, strong) NSString *httpMethod7 ,  D;
/ n b ! O F/协议名,如http1.0 / http1.1 / http2.0
@property (nonatomic, strong) NSString *httpPre ^ R E 2 Dotocol;
//是否运用署理
@property (nonatom* ^ # z 7ic, assign) BOOL useProxy;
//DNS解析后的 IP 地址
@property (nonatomiF ; 0 r  2 3 Qc, strong) NSString *8 h Q v S nip;
@end
// 监控信息v K X  + P t + e模型
@interface  NetworkMonitorDataModel : NetworkMonQ ! : { x /itorBaseDataModel
//客户端主张恳求的时刻
@property (nonatomic, assign) UInt64 requestDate;
//客户端开端恳求到开端dns解析的等候时刻,单位ms
@property (nonatomic, assign) int waitDNSTime;
//DNS 解析耗时
@property (nonatomic, assi7 B U n L Xgn) int dns/  E W A Z 5 dLoo1 o ? H I #kupTime;
//tcp 三次握手耗时,单位ms) H O f p j Z t M
@property (nonatomic, assign) int tcpTime;
//ssl 握手耗时
@property (nonatomic, assign) int sslTime;
//一个完好恳求的耗时,单位ms
@property (nonatomic, assig7 h e g 8 t 1 `n) inC E ; zt requestTime;
//http 呼应码
@property (f , Ononatomiq 1 Ec, assign) NSUInteger httpCode;
//发送的字节数
@proper) q c 1 X E j ,ty (nonatomic, assign) UInt64 sendBytesz = V ~ B [ j [ 2;
//接纳的字节数
@property (nonatomic, assign) UInt64 receiveBytes;
/A @ q a n E/ 过F * X z 1 O C错信息模型
@b  ? } *interface  NetwoN V : P 9 PrkMonitorErrorModelU 7 J # o d 0 ] R : NetworkMonitorBaseDataModel
//过错码
@propertR q C + _ ; uy (nonatomil ` Gc, assign) NSInteger errorCode;
//过错次数
@property (nonatomic, assign) NSUI^ ! U * D # d T Wnteger errCount;
//反常名
@property (nonatomic, strong) NSString *exceptionName;
//反_ M f S常详情
@property (nonatomiQ F k rc, strong) NSString *exceptionDetail;
//反常仓库
@property (nonatomic, strong) NSString *stackTrace;
@end
// 承继自4 [ % v ? [ 9 3 NSURLProtocol: X Q a Z ( p 笼统类,5 ^ y # ( C T完结呼应办法,署理网络恳求
@interf/ o y v O u 3 -ace CustomURLProtocol () <NSURLSessionTaskDel? b U vegate>
@property (nonB p t G R , Uatomic, strong) NSURLSessionDataTask= $ : U R s u y K *dataTask;
@property (nonatomic, stA ^ Kro@ z ` { w X & R +ng) NSOperationQueue *sessionDeleg5 Q K # R - d H 1ateQueue;
@propep ^ y + ^ *rty (nonatomic, strong) Netw* &  : ] orkMonitorDataModel *dataModel;
@property (O W Dnonatomic, strong) NetworkM3 Q ; L $onitorErrorModel *errModel;
@ev . O tnd
//运用NSURL| w C [  I .SessionDataTask恳求网络
- (void)startLoading {
NSURLSessionConfiP ^ - ~ vguration *confi@ } b % jguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
delegate:self
delegateQueue:nil];
NSURLSession *session = [NSURLSession sessionWithConfii ^ . w n 4 X #gu8 L = / ! { $ration:configuration delegat? + o ^e:self delegateQueue:nil];
self.sessionDelegateQueue = [[NSOperatio# B ] N z 4 L ) -nQueue alloc] init];
self.sessionDelegateQueue.m% A j h / o C & *axConcus / } @ w ` + s 8rrentOperatios V 4 a m l ZnCount = 1;
self.sessionDelegateQueue.name = @"com.netG l s x H , +workMonitor.session.queue";
self.dataTask = [session dataTaskWithRequest:self.request];u I * F  D | [ T
[self.dataTask resume];
}
#pragma mark - NSURLSz V e E XessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (} h f { K } | @error) {
[self.client URLProtocol:self didFailWithError:erb E + n a 8 R A _ror];
} else {
[self.client UR+ S 8 LProtocolDidFinishLoading:self];
}
if (e: 6 7  K ) irror) {
NSURLRequest *request = task.currentRequest;
if (request) {
self.errModel.requestUrl  = request.URL.abk * M # &soluteString;
self.errModel.httpMethod = request.HTTPMethod;
self.errModel.requestParams = request.Us E U N ZRL.query;
}
self.errModL T ] m 7el.errorCode = error.code;
self.errModel.exceptionName = error.domain;
self.errModel.exceptionDetail = erroy R 1 A hr.description;
// 上传 Netwo% o $ ork 数据到数据上报组件,数据上报会在 [打造功用强壮、灵敏可装备的数据上报组件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md) 讲
}
self.dataTask = nil;
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionT. O LaskMetrica g _ ls *)metrics {
if (@available(iOS 10.0, *) && [metrics.transactionMetrics count] > 0) {
[metrics.trans$ X e x ] [actionMetrics enumerateObjectsUsingBlock:^(NSURLSessB Q S I + ] u Y nionTaskTransactionMetricsa n  3 Z s *_Nonnd $ q S A }ull obj, NSUInteger idx, BOOL *_Nonnull stop) {
if (obj.resourceFetchType == NSURLSesb 7 : ] = TsionTaskMetricsResourceFetchTypeNetworkLoad) {
if (o_ & W f 1 b & bbj.fetchStartDate) {
self.dataModel.req) x O f #uestDate = [obj.fetchSta0 o c ( Y [ t ) vrtDate timeIntervalSince1970] * 1000;
}
if (obj.doc m K - 8 T G WmainLookupStartDate && obj.domainLookupEndDate) {
self.dataModj * ) [ m Q x Wel. waitDNSTime = ceil([oh G , U Z N b /bj.domainLookupStartDate timeIntervalSinceDate:obj.fetchStartDate] * 1000);
selfO M 9.dataModel. dnsLookupTime = ceil([obj.domainLookupEndDate timeIo k N ~ yntervalSinceDate{ n A # P w + L:obj.domainLookupStartDate] *Y : V n x 1000);
}
if (obj.conn4 q l S  fectStartDate) {
if (obj.secureConnectionStartDate)L H G z i N : @ {
self.dataModel. waitDNSTime = ceil([4 O Yobj.secureConnectionStartDv + = ? l Q } 2 iatp c ) Te timeq j v v ~ J @ AIntervalSinceDate:obj.connX $ YectStaS v nrtDate] * 1000);
} else if (obj.connectEndDate) {
self.datM O waModel.tcpTime = ceil([obj.connectEndDate ti} = rmeIz G gn- h  !tervalSinceDate:obj.connecy Z i 3 L CtStart{ d ? j i 8Date] * 1000);
}
}
if (obj.secureConnectionEndDate && obj.secureConnec} | ] u xtionStartDate) {
self.dataModel.sslTime = ceil([obj.secureConnectionEndDate ti S %meIntervalSinceDate:k l i { c 5obj.secureConnectionStartDate] * 1000);
}
if (obj.fetchStartDate && obj.responseEndDate) {
self.dataModeh 6 O X j v ; `l.requestTime = ceil([obj.responseEndDate timeIntervalSinceDate:obj.fetchStartDate] * 1000);
}
self.dataModel.httpL n Q n k & VProtocol = obj.networkProtocolName;
NSHTTPURLResponse *response = (NSHTTPURLResp) C g h ` ` 5 onse *)obj.response;
iI h  P l /f ([response isKin0 , s } V / YdOfClass:NSHTTPURLResponse.class]) {
self.dath v ` D $ ]aModel.receiveBytes = resP A ^ B Tponse.expectedContentLe9 b ( S gngth;
}
if ([obj respondsToSelector:@seU ` 2 ] $ .lector(_rei  7 U 4 Q V VmoteAddressAndPort)]) {
self.dataModel.ip = [obj valueForKS o } T g f + Dey:@"_remoteAddressAndPoZ $ g $ ` ,rt"];
}
if ([obj respondsToSelector:@selector(_requestR k 6HeaderBytesSent)]) {
self.dataModel.sendBytes = [[obj valueForKey:@"_requestHeaderBytesSent"] unsignedIntegerVaG Z Y Q p k r wlue];
}
if ([obj respondsTo` ; DSelector:@selector(_responseHeF J K T b }aderBytesReceived)]) {
self.daW Y : D TtaModeR M bl.receiveBytes = [[obj vs l (  / f Q W palueForKey:@"_responseHeaderBytesReceived"] unsignedIntegerValue];
}
self.dataModel.requC o ?estUrlD | e Z j J = [obj.request.URL absoluteString];
self.dataModel.httpMethq { J pod = obj.request.HTTPMethod;
self.dataModel.4 , } y U l z `useProxy = obj.isProxyConnection;
}
}];
// 上传 Network 数6 L Y r z据到数据上报组件,数据上报会在 [打造功用强壮、灵+ ( ( g敏可装备的数据上报组件](https://github.co8 z G j  mm/FantasticLBP/knowledge-kit/bloj : s C W C ]b/master/Chapter1%20-%20iOS/1.80.md) 讲
}
}

2.2 计划二:NSURLProtocol 监控 App 网络恳求之黑魔法篇

文章上面 2.1 剖析到了 Nh e P 4SURLSessionW L – x 4 ` jTaskMetrics 由于兼容性问题,关N d , 4 e 于网络监控来说似乎不太完美,可是自后在搜材料的时分看到了一篇文章。文章在剖析 WebView 的网络监控的时分剖析 Webkit 源码的时分发现了下面代码

#if !HAVE(TIMIN? 7 ? q ; 8 c NGDATAOPTIONS)
void setCollectsTimingData()
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[NSURLConnR ! e 2 D l ~ Ue@ c w  L .ction _setCollectsTimingData:YES];
..u X ` .
});
}
#endif

也便是阐明 NSURLConnection 自身有一套 TimingDataK W C 的搜集 API,仅仅没有露出给开发者,苹果自己在用而已。在 rur z @ b wntime header 中找到2 9 [ 9 i了 NSURLConnectp ` v ?ion 的 _setCollectsTimingData:_timingData 2个 apw O ; 4 ^ t z 4i(iOS8 今后能够运用)。

NSURLSession 在 iOS9 之前运H x 1 1 [_setCollectsTimingDat. } p 7 P uaK ; 3 R: 就能够运用 TimingData 了。

留意:

  • 由所以私有 API,所以在运用的时分留意混杂。比方 [[@"_setC" stringByAppendingString:@"ollectsT"] stringByAppendingString:@"iminr E P v Z W / qgData:"]
  • 不引荐私有 API,一般做 APM 的归于公共团队,你想想| 5 # p x D看尽管你做的 SDK 到达p C b j网络监} n q u ; O o W控的意图了,可是万一给事务线的 App 上架形成了问题,那就得不偿失了。一般这种投机取巧,不是百分百确认的工作能够在玩具阶段运用。
@interface _NSURLConnectionProxy : DelegateProxy
@end
@i5 | q 8mplementation _NSURLConnectionProxy
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelecto+ v 1 _r) isEp J . $ T M K ~ CqualToString:@"connectionDidFinishLoading:"]) {
return YES;
}
return [self.target respondsToSelector:aSelector];
}
- (5 ^ S Gvoid)forwardInvoc h : 6 W scation:(NSInvocation *)D @ Z :ip 4 e [ S b & 5 Pnvocation
{
[super forR / ( [  X t 6wardInvocation:invocation];
if ([NSStringFromSelector(invocation.seg L O 9 6lector) iY 4 ] ) WsEqualToString:@"connectionDidFinishLoading:"]) {
__unsafe_unretained NSURLConnecti? v G Q P O !on *conn;
[invocation getArgument:&conn atIndex:2];
SEL selector = NSSelectorFromString([@"_timin" stringByAppendingString:@"gData"]);
NSDictionary *timi= - O : - T t 1ngDataa : l C 4 & , c = [conn per! r _ 1formSelector:selector];
[[8 % 9 m 6 Q x INTDataKeeper shareInstance] trackTimingDataV i H a + Z d:tim9 0 4ingData request1 z 4 k a:conn.currentRequest];
}
}
@end
@implementation NSURLConnection(tracker)
+ (void)load
{
static dispatch_oncU 4 5 h @ - Qe_t oncb C ) ~ F y c /eToken;
dispatch_once(&on5 . 2 h a  f / yceToken, ^{
Class class = [self class];
SEL originalSelector = @selectoC r = & J Q 4 x zr(initWithRequest:delegate:);
SEL swizzledSelector = @selector(s= X n  6 -wizzledInitWithRequest:delegate:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod =Y g [ d class_getIn* , q q ]stanceMethod(class, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
NSString *selectorName = [[@"_setC" stringByAppenc 5 f } j F { .dingString:@"ollectsT"] stringm | ; P I 0ByAppendingString:@"imingData:"];
SEL selector = Nq 4 4SSelectorFrI O c # .omS6 ) ktring(sele* 8 BctorNameS d X  T # q l });
[NSURLConr G 5 Y A 9 L u Tnection performSelector:selector withObject:@(YES)];
});
}
- (instancetype)swizzledInitWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate
{
if (delegate) {
_NSURLConnectionProxy *proxy = [[_NSURLConnectionProxy alloc] initWithTarget:delegate];
objc_setAssociatedObject(delegate ,@"_NSURLConnectionProxy" ,proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIQ l d ]C);
return [self swizzledInitWithRequest:request delegate:(id<NSURLConnectionDelegate>)proxy];
}else{
return [self swizzledInitWithRequest:request delegate:delegate];
}
}
@end

2.3 计划三:Hook

iOS 中 hook 技能有2类,一种是 NSP@ ] Oroxy,一种是 meth{ d x x G b )od swizzling(isa swizzling)

2.3.1 办法h l ` _ | 5 9 #

写 SDK 必定不或许手动侵入事务代码(b j * 5 S你没那个权限提交到线上代码 ),所以不管是 APM 仍是无痕埋点都是经过 Hook 的办法。

面向切面程序规划(Aspect-oriented Programming,AOP)是核算机科学中的一种程序规划范型,将横切关注点与事务主体进一步别离,以提高程序代码的模块化程度。在不修正源代码的状况下给程序动态增加功用。其核心思维是将事务逻辑(核心关注点,体系首要功用)与公共功用(横切关注8 ) E p D点,比方日志体系)进行别离,下降杂乱性,坚持体系模块化程度、可保护性、可重用性。常被用在日志体系、功用核算、安全操控、事务处& , T s 3 T ! y ?理、反常处理等场景下。

在 iOS 中 AOP 的完结是依据 Runtime 机制,现在由3种办u N t k N法:Method Swizzling、NSProxy、f q ~ TFishHook(首要用用于 hook c 代码)。

文章上面 2.1 讨论了满意大多数的需求的场景,NSURLProtocol 监控了 NSURLConnection、NSURLSession 的网络恳求,自身署理后能够主张网络恳V ~ s # G / ? [ _求并得到诸如恳求开端时刻、恳求完毕时刻、header 信息等,可是无法得到非常s G 5详细的网络功用数据,比方 DNS 开U V Q端解析时刻、DNS 解析用了多久、reponse 开端回b D k d Y _来的时刻、回来了多久等。 iOS10 之后 NSURLSessionTaskDelegate 增加了一个署理办法 - (void)URLSession:(NSURLSession *)sessN Y X + ( -ion task:(NSURLSessionTe , + Y / . C .ask *)task didFinishCollectingMetrics:(NSURLSessionO = | B 0 r 3 + ,TaskMetrics *)metrics API_AVAILABLE(macosx(10.12), iosq K . L + W(10.0), watchos(3.0), tvos(10.[ k m h & L0));,能够获取到准确的各项网络数据。可是具有兼容性。文章上面 2.2 讨论了从 Webkit 源码中得到的信息,经过私有办法 _st J G p l $etCollectsTimingData:_timingDatat ; | R * f ^ N z 能够获取到 TimingData。

可是假设需求监悉数的网络恳求就不能满意需求了,查阅材料后发现了阿里百川有 APM 的处理计划,所以有了计划3,关Q k p 5 . O !于网络监控需求做如下的处理

带你打造一套 APM 监控系统(一)

或许关于 CFNetwork 比较陌生,能够看一下 CFNetwork 的层级和简略用法

带你打造一套 APM 监控系统(一)

CFNetwork 的根底是 CFSocket 和 CFStream。r o O 0 $ X

CFSocket:SocZ p 5 O Pket 是网络通信的底层根底,能够让2个 socket 端口互发数据,iOS 中最常用的 sockeh z n x L P qt 笼统是 BSD socket+ j 3 % H w C。而 CFSocket 是 BSD socket 的? 3 d n ^ OC 包装,几乎完结了一切的 BSD 功用,此外参加了 RunLoop。

CFStream:供给了与u @ 1 D S设备无关的读写数据办法,运用它能够为内存、文件、网络(运用 socket)的数据树立流,运用 stQ 9 mream 能够不必将一切数据写入到内存中。CFStream 供给 API 对2种 CFTyJ ! Kpe 目标供给笼统:C* U u ~ HFReadStream、CFWriteStream。一起z V ; a f _ z )也是 CFHTTP、CFFTP 的根底。

简略 Demb C v / ,o

- (void)testCFNetwork
{
CFURLRi A J I = X | G :ef urlRef = CFURLCreateWithStrD & 8 $ Iing(kCFAllocatorDefault, CFSTR("e O $ 8 j Hhttps://httpb& S h ( [ z * kin.org/get"), NULL);
CFHTTPMessageRefn d q q b p 3 httpMessageRef = CFHTTPMessageCreateRequest(kCFAllocatorDefault, CFSTR("GET"), urlRef, kh M { YCFHTTPVersion1_1);
CFRelease(urlA B / z 1 7 ` RRef);
CFReadStreamRef readStrg R * J ueam = CY } z vFReadStreamCreateForHT2 5 #TPRequest(kCB 5 f A u . r 0FAllo c ` t VcatorDefault, httpMessageRef);
CFRelease(httpMessageRef);
CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopComy S f xmonModes);
CFOptionFlags eventFlagX | rs = (kCFStreamEventHasBytesAvailable | kCFStreamEventErrorQ T rOccurred | kCFStreamEventEndEncountered);
CF$ j t  B ; q uStreamClientCo4 s K 3ntext context = {
0,
NULL,
NU6 k c !LL,
NULL,
NULL
} ;
// Assigns a cliend 1 X % w Y pt to a s4 l O P 8 U rtream, which receiv+ ,  : Mes callbacks when certain events occur.
CFReadStreamSetClient(readStream, eventFlags, CFNetw6 9 q ? workRequestCallback, && S &context);
// OpensS t ; 3 s 1 U r y a stream for reading.
CFReadStreamOpen(readStream);
}
// callback
void CFNetworkRequest2 - c S P O lCallback (CFReadStreamRef _Null_unspecified stream, CFStreamEventTm $ d 9 h : N Bype type, void * _Null_unspecified clientCallBackInfo) {
CFMutableDataRef responseByN 7 ctes = CFDataCreatea M b a C ^Mub v W = ] h 0tabl; e i V O F ( .e(kCFAll* d s _ 8 ) X `ocatorDefault, 0);
CFIndex numberOfBytesRead = 0;
do {
UInt8 buffer[2014];
numberOfBytesRead = CFReadStreamRead(stream, buffer, sizeof(buffer));
if (numberOfBytesRead > 0) {
CFData0 W } c _ : h KAppendByt_ V - # x c n 0 yes(responseBytes, buffer, nG O L _ h [umberOfBytesRead);
}
} while (numberOfBytesReadT ^ x > 0);
CFHTTPMessageRef response = (CFHTTPMessageRef)CFReadStreX $ amCopyProperty(stream3 @ % S q O, kCFStreamPropertyHTTPResponseHeader);
if (responseBytes) {
if (response) {
CFHTTPMessageSetBody(response, responsS ; 6eBytes);
}
CFRelea~ g %se(responseBytes);
}
// close and cleanup
CFReadStreamCl. U Q V Tose(stream);
CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCur#  w C % D qrent(), kCFRunLoopCommonModes);
CFRelease(stream);
// print response
if (response) {
CFDataRef reponseBodyData = CFHTTPMessageCopyBody(response);
CFRelease(F * m W S , o ] ~response);
printResponsez d W e %Data(reponseBodyData);
CFRelease(reponseBodyD= Q ; Gata);
}
}
void printRU : W G :esponseData (CFDataRef responseData) {
CFIndex dataLenh 2 S h r b Cgth = CFDataGetLength(responseData);
UInt8 *bytes = (UInt8 *)malloc(dataLength);
CFDataGet8 r z )Bytes(responseData, CFRan d K T 5ngeMake(0, CFDataGetLength(9 T i ^ k GresponseData)), bytes);
CFStringRefN R G l E F responseStrin+ H Yg = CFStringCreateWithBK N & I ] !ytes(kCFAllocatorDefault, bytes, dataLength, kCFS+  q 8 ~ I ^ . @tringEncodingUTF8, TRUE);
CFShow(responseString);
CFRelease(responseString);
free(bytes);
}
// console
{
"args": {},
"headers": {
"HostB P o u I H ) S m": "httpbin.org",
"User-Agentv M M": "Test/1 CFNetworV ? j $k/1125.2 Darwin/19.3.0",
"X-Amzn-TraceN ~ & $ ^ [ / M $-Id": "Root=1-5e8980d0-581f3f44724c7140614c2564"
},
"origin": "183n I C h Z 4.159.122.102",
"url":S i  ! a 5 @ y "https://httpbin.org/get"
}

咱们知道 NSURLSession、NSURLConnection、CFNetwork 的运用都需求调用一堆办法进行设置然& N , @ 2 k后需求设置署理目标,完结署理办法。所以针对这种状况进行监控首要想到的是运用 runtime hoo, e b [ & ( I & xk 掉办法层级。可是t C } x Q _ *针对设置的署理目标的署理办法没办法 hook,由于不知道署理目标是哪个类。所以想办法能够 hook 设置署理目标这个进程,将署理目标替换成咱们规划好的某个类,然后让这个类去完结 NSURLConnection、NSURLSession、CFNetwork 相关的署理办法。然后在这些办法的p H P 2 3 U _ @内部都去调用一下原署理目标的办法完结。所以咱们的需求得以满意,咱们在相应的办法里边能够拿到监控数据,比方恳求开端时& p * e 7 H刻、完毕时刻、状况码、内容巨细等。

NSURLSession、NSURLConnection hook 如下。

带你打造一套 APM 监控系统(一)
带你打造一套 APM 监控系统(一)

业界有 APM 针对 CFNetwork 的计划,整理描绘下:

CFNetwork 是 c 语言完结的,要对 c 代码P X = B进行 hoo7 i r ( Ik 需求运用 Dynamic LZ { W Y G % l E voader Hook! X Q w j Z S K g 库 – fishhook。

Dynamic Loaderl y Rdyld)经过更新 Mach-OL = o J I $件中保存的指针的办法来绑定符号。借用它能够在 Runtime 修正 C 函数调用的函数指针。fishhook 的完结原理:遍历 __DATA segmentN O Q 里边 __nl_symbol_pt= N a h 3 ]rh + 9 a__la_symbol_ptr 两个? 9 z P N P t 3 & section[ g X 里边的符号,经过 Indirect Symbol Table、Symbol Table 和 String Table 的? B c M Q p – M配合,找到自己要替换的函数,到达 hook 的意图。

/* Returns the number of bytes read, oru J B w _ b ; V -1 if an errora Q e occurs preventing any

bytes from being read, or 0 if( q d [ d the stream’s end was encountered.

It is an error to try and read from a stream that hasn’t been opened first.

This call will block until at least one byte is avaiy : :lable; it will NOT block

until the entire buffer can be filled. To avoid blocking, eid A b z s B T e 0ther poll using

CFReadStreamHasBytesAvailable() or use the run loop and listen for the

kCFStreamEventHasBytesAvailaT 0 ,ble eventb p ? ] R e . S for notification of dat ; U y N B aa available. */

CF_EXPORT

CFInl * g % } f { ?dex CFReadStreamRead(CFReadStreamRef _Null_unN y Q Cspecified stream, UInt8 * _Null_unspeci ^ Iified buffer, CFIndex bufferLength);

CFNetwork 运用 CFReadStreamRef 来传递数据,运用回调函数的办法来承受服务器的呼应。当回调函数遭到

具体进程及其要害代码如下,以B @ L + NSURLConnection 举例

  • 由于要 Hook 挺多当地,所以写一个 metho) H 3 / 6 9d swizzling 的东西类

    #J f u 6imc C * b f 6 C { |port <Foundation/Foundation.h: S q e t>
    NS_ASSUME_NONNULL_BEGIN
    @interface NSObj# R X nect (hook)
    /**
    hook目标办法
    @param originalSelector 需求hook的原始目标办法
    @param swizzledSelector 需求替换的目标办法
    */
    + (void)apm_swizzleMeR n y D s Othod:(SEL)originalSelector swizzledSg 6 J : 1 @ A oelector:(SEL)swizzledSelector;
    /**
    hook类办法
    @param originalSelector 需求hook的原始类办法
    @param swizzledSelector 需求替换的类办法
    */
    + (void)+ v qapm_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;
    @end
    NS_ASSUME_NONNULL_END
    + (void)Y $ $ ?apm_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)J K ] i X ! k 4swizzledSelector
    {
    class_swizzleInstanceMeA 0 6 6 o L vthod(self, originalSelector, swizzleh O k ( T n R pdSelector);
    }
    + (voido : % 6 C p j -)apm_swS _ ) !izzleClassMethod:(SEL)originalSelector swizzledSelect. & + | * y  wor:(SEL)swizzledSelector
    {
    //类办法实际上是储存在类目标的类(即元类)中,即类办法相当于元类的实例办法,所以只需求把元类传入,其他逻辑和交互实例办法相同。
    Class class2 = object_g5 0 3 J aetClass(self);
    class_swizzleInstanceMethod(class2, origind M J O p f [ 4 1alSelector, swizzledSelector);
    }f | , ? % ? f / L
    void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL)
    {
    Meth5 K ^  A * pod orc x * 4 4 7 I  1iginMethod1 + D x ; % W = class_getInstanceMethod@ F G } g T x s(class, originalx z c R (  U jSEL);
    Method rep? / s & * . ; M 8laceMethod = cla^ Q p Z _ ) 4ss_ge& 3 =tInstanceMethod(class, replacementSEL);
    if(class_addMethod(class, originalSEL, methd ^ Y = Pod_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod)))
    {
    class_replaceMethod(3  Zclass,replacef A s n f 8mentSEL, method_getImplementation(originMethodf ^ f 0 $ 2), method_getType5 e G / n p - 0 6Encoding(originMethod));
    }else {
    method_exchangeImplementations(Q 7 9 8 C coriginMethod, replaceMethod);
    }
    }
    
  • 树立一个承继自 NSProxy 笼统类的类,完结相应办法。

    #importU { 2 V ] a B F <Foundp 3 O Z @ yation/Foun G jdation.h>
    NS_ASSUME_NONNULL_BEGIN
    // 为 NSURLConnection、NSURLSession、CFNetwork 署理设置署理转发
    @interface NetworkDelegateProxy :K h O NSy ?  a g z Q ! bProxy
    + (instancetype)setProxyForObject:(id)originalTarget withW 2 R UNewDel- f 3 Iegate:(id)newDelegate;
    @end
    NS_ASSUME_NONNULL_END
    // .m
    @interface NetworkDelegateProxy () {
    id _originalTarget;
    id _NewDelegate;
    }
    @end
    @implementation NetworkDelegateProq 7 E b 6 * A ` 1xy
    #pragma mark - life cycle
    + (instancetype)sharedInstance {
    static NetworkDelegateProxy *_sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatA 0 | Pch_once(&onceToken, ^{
    _sharedInstance = [NetworkDelegateProxy alloc];
    });
    return _sharedInstance;
    }
    #pragma mark - public MethU  4od
    + (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate
    {
    NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance];
    instance->_originalTarget =a - U ) V originalTarget;
    instance->_NewDelegate = newDel@ e ) *egate;U ) . q 6
    return instance;
    }
    - (void)forwardI6 9 i { C Ynvocation:(NSInvocation *)invocation
    {
    if ([_originalTarget respondsToSelector:invocation.selee : d z o ! f Hctor]) {
    [invocation invokeWithTarget:_originalTU X @ warget];
    [((N { z ) U i hSURLSessionAndConnectionImplementor *)_NewDelegate,  y * +) invoke:R p binv@ w N ocation];
    }
    }
    - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
    {
    return [_originalTarget metho# m [  D HdSignatureForSelector:sel];
    }
    @p F x { / . pend
    
  • 创立一个目标,完结 NSURLConnection、NSURLSession、NSIuputStream 署理办法

    // NetworkImplementor.m
    #pragma mark-NSURLCY s B T W - d donnectionDelegate
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    N| : o U ` ^ n |SLog(@"%s", __func__);
    }
    - (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response {
    NSLt . G Hog(@"%s",h D x L _ k C __fu ) Z = h f I mnc__);
    return request;
    }
    #pragma mark-NSURLConnectionDataDelegate
    - (void)connection:(NSURLConnection *)connection didReceiveRes{ 0 Lponse:(NSURLResponse *)response {
    NSLog(@"%sd 4 0", __func__);
    }
    - (vL { ~ M j ` b Xoid)connection/ m 7 K:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    NSLog(@"%s",% E  . p j 3 c - __func__);
    }
    - (void)connection:(NSURv w 1 + : # * tLConnection *)connection   didSendBodyData:(NSInteger)bytesWritten
    totalBytesWritten:(NSInteger)totalBytesWritten
    totalBytesExpectedToWrite:(NSInteger)totalBytesExped ! b S R . ccteU R S ` f 5dToWrite {
    NSLog(@"%s", __func__);
    }
    - (void)connec6 R 3 8 u btionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"%sw = Q n", __func__);
    }
    #pragma mark-NSURLConnectionDownloadD! w q P Q %elegate
    - (void)connection:(NSURLConnection *)connection didWriteD~ % U ^ N * p ^ata:(long long)bc O | t rytesWritten totalBytesWrl J J M 5 x 6 vitten:(long long)totalBytesWritten expectedTotal= L ( ? J ? JBy- | k p [ . /tes:(long long) expectedTotm g w R G _ = $alBytesI B L O M 5 + 4 5 {
    NSLog(@"%s", _C T X ]_func__)G ? h N X;
    }
    - (void)connectionDidResumeDownloading:(NSURLConnection *Z C 1 3)connection totalBytesWritten:(long long)totalBytesWrg | q k r 8 n D Gitten expectedTot- h @ }alBytes:(long long) expectedTotalBytes {
    NSLog(@"%s", __func__);
    }
    - (void)connection H ! DidFinishDownloading:(NSURLConnection *)c I N % U 5 8 Yonnection destinationUm g y 9 r aRL:(NSURL *? ] } g z O) destinationURL {
    NSLog(@"%s", __func__);
    }
    // 依据需求自己去写需求监控的数据项
    
  • 给 NSURLConne* % :ction 增加 Category,专门] 6 # _ +设置 hook 署理目标、hook NSURLConnectk 8 u #ion 目标办法

    // NSURLConnection+Monitor.m
    @implementation NSURLCog U *nnectiong s O ; d G (Monitor)
    + (void)load
    {
    static dispatch_once_t onceToken;
    di7 ; | sA w Cpatch_once(&{ c ] v w { @ $ 3;onceToken, ^{
    @autoreleasepool {
    [[self class] apm_sw) q z K P 0 !izzleMethod:s Q % H c _@selector(apm_initWithRequest:delegaM L c ? W ~te:) swizzledSelector:@selector(initWithRequest: delegate:)];
    }
    });
    }
    - (_Nonnull instancetype)apm_initWithRequest:(NSURLRequest *)r8 c _equest delegate:(nullable id)delegate
    {
    /*
    1. 在设置 Delegate 的时分替换 delegatj # V z C ~ Ce。
    2. 由于要在每个署理办法J 5 o 4 T r T P k里边,监控数据,所以需求a { 8 B将署理办法都 hook 下
    3. 在原署理办法履行的时分,让新的署理目标里边,去履行办法| g R ( x 7 u G 7的转发,
    */
    NSString *traceId = @"trac} R a E e U yeId";
    NSMutableURLRequest *rX . $q = [request mutableCopy];
    NSString *preT/ z @ &raceId = [request.allHTTPHeaderFields valueForKey:@"head_kN z  Wey_traceid"];
    if (preTraceId) {
    // 调用 hook 之前的初始化办法,回来 NSURLConnection
    rG M ` Ieturn [self apm_initWithReqo ? 0 5 $ ` f Nuest:rq delegate:delegate];
    } else {
    [rq setValue:traceId fo! J ( 7 r mrHTTPHeaderFI Z m ! tield:@"head_key_traceid"];
    NSURLSessionAndConnectionImplementor *mockDelegate = [NSURLSessionAndConnectionImplementor new];
    [self registk ) N 6 A %erDelegateMeth` o 4 V G aod:@"co. B snnection:didFailWithError:- y $ x U R C J" originalDV { ~ relegate:delegate nW p - 9ewDelegate:mockDelegate flag:"v@:@@"];
    [self registerDelegateMethod:@"connection:didReceiveResponse:" originy 2 ] Y ]alDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"];
    [self registerDelegateMethod:@"connectionp 8 = _ , p:didReceiveData:" originalD= N } ielegate:delegate newDelegate:mockDelegate flag:"v@:@@"( o E : d R  ~ :];
    [self registerDelegateMethod:@"connection:didFailWithError:" origin$ [ ^ ` 3 K L m *alDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"];
    [self registerDelegaz a 9 TteS d ] UMethod:@"connectionDk [ E c 0idFinishLoading3 ^ {  f T @ # q:" originalDelegate:delegatL @ U U A & [ l he newDelegate:mockDelegatv ] * ] 1e flag:"v@:@"];
    [self registerDelegateMethod:@"connection:willSendRequest:redirectResponse:" origin] h x + B Z _alDelegate:delegate newDelegate:mockDelegate flag:"@@:@@"];
    delegate =X @ l [Networa y W kDelegateProxy setProxyForObject:delegate withNewDelegate:mockDelegate];
    // 调用 hook 之前的初N P } G ) i / W Q始化办法,回来 NSURLConnection
    return [self apm_initWithRequest:rq delegate:delegate];
    }
    }
    - (void)registerDelegateMethod:(NSSt` u 9 F X Hring *)methodName originalDelegate:(w J Did<NSURLConnectionDelegate>)originalDelegate newDelegate:(NS[ y M G ` ` 4 c )URLSessionAndConnectionImplementor *)newDeleL # V O x z &gate flag:(const char *)flag
    {
    if ([orig~ n u d # WinalDelegate respondsToSelector:NSSelectorFromString(methodName)]) {
    IMP originalMethodImp = class_getMethodImplementation([originalDelegate class], NSSelectorFro. | a A WmString(meth[ s O RodName));
    IMP newMethodImp = class_getMethc : ] ? % & : z OodImplementation([newDelegate class], NSSelectorFromString(methodName));
    ifu @ X | X r u U (originalMethodImp != newMethodImpN ` t j X e } x) {
    [newDelegate registerSelector: methb _ P # ] z _ V .odName];
    NSLoS 9 B /g(@"");
    }
    } else {
    class_addMethod([originalDelegate class], NSSm r * - X Z h pelectorFromString(methodName), class_getMethodImplementation([newDelegate class], NSSelectorFromString(met~ q @hodName)), flag);
    }
    }
    @end
    

这样下来便是能够监控到网络信息了,然后将数据交给数据上报 SDK,依照下发的数据上报战省略上报数据。

2.3.2 办法二

其实,针对上述的需求还有另一种办法i w , 4 b } t相同能够到达意图,那便是 isa swizzling

顺路说一句,上面针对 NSURLConnection、NSURLSession、NSInput; ] K b ~ FStream 署理目标的 hook 之后,运用 NSProxy 完结署理目标办法的转发,有另一种办法能够完结,那便是 isa swizzling

  • Method swizzling 原理

    struct old_method {
    SEL method_name;
    char *method_types;
    IMP method_imp;
    };
    
    带你打造一套 APM 监控系统(一)

    method swizzling 改善版如下

    Method originalMethod = class_getInstanceMethod(aClass, aSEL);
    IMP originalIMP = method_getImplementation(originalMethod);
    char *cd = methodW ( G E f y * 8 ]_getTyp) 0 eEncoding(originalMethod);
    IMP newIMP = imp_implementationWithBlock(^(id self) {
    void( k 1 (*tmp)(id self, SEL _cmd) = originalIMP;
    tmp(self, aSEL);
    });
    class_r, 8 C H U m ) =eplaceMethod(aClass,2 X l aS& ; _EL, newIMP, cd)i , ] V 9 l;
    
  • isa swizzling

    /// Represents an instance of ab ? b i - @ { class.
    struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILAB % u Q W ^ 6 ` $ILITY;
    };
    /// A pointer to an instance of a class.
    typedef struct objc_object *id;
    
带你打造一套 APM 监控系统(一)

咱们来剖析一下为什么修正 isa 能够完结意图呢?

  1. 写 APM 监控的人没办法确认事务代码
  2. 不或许为了便利监控 APM,写某些类,让事务线开发者别运用体系 NSURLSession、NSURLConnection 类

想想 KVO 的完结原理?结合上面的图

  • 创立监控目标子类
  • 重写子类中特J A G , A } w点的 gettert Z $、seeteW b R nr
  • 将监控目标的 isa 指针指向新创立的子类
  • 在子类的^ Z ~ J j 5 P getter、setter 中阻拦值的改变,告诉监控目标值的改变
  • 监控完之后将监控目标的 isa 复原回去

依照这个思路,咱们也能够对 NSURLConnection、NSURLSession 的 load 办法中动态创立子类,在子类中重写办法,比方 - (**x o A ( _ {nullable** **instancetype**)initWithRequest:(NSURLRequest *)request delegate:(**nullable** **id**)dR 0 ielegate startImmediately:(**BOOL**)startImmediately; ,然后将 NSURLSession、NSURLConnection 的 isa 指向动态创立的子类。在这些办法处理完之后复原自身的 isa 指针。

不过 isa swiz( E czling 针对的仍是 method swizzling,署理目标不确认,仍是需求 NSProxy 进行动态处理。

至于怎么修正 isa, y ( h # $ [,我写一个简略的 Demo 来模拟 KVO

- (void)lbpKVO_addOR Q  I Ubserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(Nl ` jSKeyValueObservingOptions)options contexx { b D 1 4 jt:U = L w % d @(nullable void *)context {
//生成自界说的称号
NSString *className = NSStringFromClass(self.class);
NSString *currentClassName = [@"LBPKVONotifying_" stringByAppendingStrin? + Lg:className];
//1. runtime 生成类
Class myclass = o[ 3 M Abjc_allocateClassPair(selm b | ;f.class, [currentClassName UTF8String], 0);
// 生成后不能马上运用,有必要先注册
objc_registerClassPair(myclass);
//2. 重写 setter 办法
class_addMethod(myclass,@select4 + ` Jor(say) , (IMP)say, "v@0 O } F * 2 d:@"F H p ` D ));
//    class_addMethod(myclass,@selector(setName:)Z q + k Q o B , = , (IMP)setName, "v@:@");
//3. 修正 isa
objj Z lect_setClass(self, myclass);
//4. 将观察者保存到当时目标里边
objc_setAssoa d ` [ { kciatedObject(self, "observer2 W d * G u @ O =", observer, OBJC_ASSOCId ~  }  BATION_ASSIGN);
//5. 将传递的上下文绑定到当时目标里边
objc_setAssociatedObject(self, "context", (__bridge id _Nullable)(context), OBJC_ASSOCIATION_RETAIN);
}
void say(id self, SEL _cmd)
{
// 调用父类办法一
struct objc_super superclass = {self, [selfn n 3 G V superclass]};
((void(*)(struct objc_super *,SEL))objc_msgSendSuper)(&superclass,@selector(say));
NSLog(@"%s", __func_% J M _ m Y -_);
//~ q 6 . % 7 x 调用父类办法二
//    Class class = [self clasc h o Js];
//    object_setClass(self, class_; 1 =getSuperclass(class));
//    objc_msgSend(self, @selector(say));
}
void setName (id self, SEL _cmd, NSString *n4 | ( fame) {
NSLog(@"come here");
//先切换到当时& o E t +类的父类,然后发送音讯 setName,然后切换当时子类
//1. 切换到父类
Class class = [self class];
object_setClass(self, class_getSuperclass(class));
//2. 调用父类的 setName 办法
objc_m$ 9 g 2sgSend(self, @s% 5 P 2elector(setNam6 f Ne:), name);
//3. 调用观察
id observer = objc_getAssociatedObject(sela z b t b xf, "observer");
id context = objc_getAssociatedObject(self, "context");
if (observer) {
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"new": name, @"kind": @1 } , context);
}
//4. 改回子类
object_setc Q )Class(self, class);
}
@end

2.4 计划四:监控 App 常见网络恳求

本着成本的原因,由于现在大多数的K l | J z项意图网络能力都是经过 AFNetworking 完结的,所以本文的网络监控能够快速完结。

AFNetworking 在主张l * 2 = + $ 3 0 0网络的时分会有相应的告诉。AFNetworkingTa3 & T R n RskDidResumeNotificationAFNetworkingTaskDidCompleteNotification。经过监听告诉携带的参数获取网络状况信息。

 self.didResumeObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AFNetworkingTaskDidResu! T b c S 4 # J _meNotificatiE 3 / ~ P q 9 5 #on object:nil queue:selfd C c p.queue- ( w 6 w P ) # usingBlock:^(NSNotification * _Nonnull note) {
// 开端
__stro5 @ ! A  C tng __typ2 ] . 2 z W n meof(weakSelf)strongSelf = weakSelf;
NSURt Q S t i 4 % ! -LSessionTask *task = note.object;
NSString *r5 A = Q [ n { sequestId = [[NSUUID UUID] U- V b r fUIDSs ^ jtring];
task.apm_req5 : [ FuestId = requP w f ;estId;
[strong i U x Q  rSelf.networkRecoder recordStartRe@ F S equestWit} ~ a NhRequestID:reques3 m ` ) X h z v XtId task:task];
}];
self.didCompleteObserver = [[NSNotificationC ; S , 3 ZCenter defaultCenter] addObserverForNa| W B me:AFNetworkinZ ! ; s i E S }gTaskDidCompleteNotification object:nil queue:self.queue usingBlock:^(NSNotification * _Nonnull note) {
__strong __typeof(9 j o {weakSelf)strongSelU ! 6 _ 1  ) ^ Mf = weakSelf;
NSError *N u V F y : x 6 JerrorD m + = note.userInfom Z 2 = } d[AFNetworkingTaskDidCompleteErrorKey];
NSURLSessionTask *task = note.object;
if (!error) {
// 成功
[strongSelf.networkRecoder recordFinishRequestWithRequestID:task.cmn_requestId task:task];
} else {
// 失利
[strongSel/ - K j = F a Uf.nv 3 S v I { NetworkRecoder recordResponseErrorWithRequestID:task.cmn_requ8 4 A v B XestId task:task error:error];
}
}];

在 networkRecoder 的办法里边去拼装数据,交给数据上报组件,比及适宜的时机战省略上报。

由于网络是一个异步的进程,所以当网络恳求开端的时分需求为每个网络设置仅有标识,比及网络恳求完结后再依据每个恳求的标识,判别该网络耗时多久、是否成功等。所以措施是为 NSURLSessionTask 增加分类,经过 runtime 增加一个特点,也便是仅有标识。

这儿插一嘴,为 Category 命名、以及内部的特点和办法命名的时分需求留意下。假设不留意会怎么样呢?假设你要为 NSString 类增加身份证号码中心位数躲藏的功用,那么写代码久了的老司机 A,为 NSString 增加了一个办法名,叫8 T ? M c做 ge/ ) 8 6 8 EtMaskeB L l QdIdCardNumber,可是他的需求是从 [9, 12] 这4位字符串躲藏掉。过了几天同事 B 也遇到了类似的需求,他也是一位老司机,为 NSString 增加了一] . u 1 = w f y e个也叫 getMaskedIdCardNumber 的办法,可是他的需求是从 [8, 11] 这4位字符串躲藏,可是他引进工程后发现输出并不契合预期,为该办法写的单测没经过,他认为自己写错了截取办法,检查了几遍才发现工程f t s ; j _ s v引进了另一个 NSString 分类,里边的办法^ 8 Q +同名 真坑。

下面的比方是 SDK,可是日常开发也是相同。

  • Category 类名:主张依照当时 SDK 称号的简写作为前缀,再加下划线,再加当时分类的功用,也便M 3 m y类名+SDK称号简写_功用称号。比方当时 SDK 叫 JuhuaSuanAPM,那么该 NSURLSessionTask Cp 6 M zategory 称号就叫做 NSURLSessionTask+JuHuaS7 O h h W } E a ruanAPM_NetworkMonitor.h
  • Category 特点名:主张依照当时 SDK 称号的简写作为前缀,再加下划线,再加2 H R特点名,也便是SDK称号简写w } ]_特点称号。比方 JuhuaSu – c R HuanAPM_requestId`
  • CategoryF p , / 办法名:主张依照当时 SDK 称号的简写作为前缀,再加下划线,再加办法名,也便是SDS B N R 0 FK称号简写_办法称号。比方 -(BOO3 / 0 r | 3L)JuhuaSuanAPM__isGzippedData

比方如下:

#import <Foundation/Founm ] P L 7 ] Ldation.h>
@interface NSURLSessionTask (JuhuaSs 0 d &uanAPM_NetworkMonitor)
@property (nonatomic, copy) NSString* JuhuaSuanAPM_requestId;
@end
#i* s u wmport "NSURLSessionTask+JuHuaSuanAPM_NetworkMh [ ! + X  M -onitor.h"
#import <objc/runtime.h>V K  { | j  T
@implementation NSURLSessionTask (Jua L C u l ` C : ZHuaSuanAPM_NetwoQ . D w Z %rkMonitor)
- (NSString*)JuhuaSuanAPM_requestId
{
return objc_getAssociatedy # V P / D nObject(self, _cmd);
}
- (void)setJuhuaSuanAPM_reqQ ? M * |uestId:(NSString*)requestId
{
objc_setAssociatedObject(self, @selector(JuhuaSuanAPM_reques3  WtId), requestId, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

2.5 iOS 流量监控

2.5.1 HTTP 恳求、呼应数据结构

HTTP 恳求报文结构

带你打造一套 APM 监控系统(一)

呼应报文的结构

带你打造一套 APM 监控系统(一)
  1. HTTP 报文是格局化的数A T ? : Q t据块,每条报文由三部分组成:对报文进行描绘的起始行、包含特点的首部块、以及可选的包含数据的主体部分。
  2. 起始行和手部便是由行分隔符的 ASCII 文本,每行都以一个由2! v t d %个字符w ] / e R x组成的行终止u ; 6 N { M序列作为完毕(包含一个回车符、一个换行符)
  3. 实体的主体或许报文的主体是V _ M = ? , A m b一个可选的数据块。与起始行和首部不同的是,主体中能够包含文本或许二进制数– J b q o据,也能够为空。
  4. HTTP 首部(也便是 HeaderQ S X 9 ls)总是应该以一个空行完毕,即便没有实体部分。浏览器发送了一个空白行来告U 6 诉服务器,它现已完毕了该头信息的发送。

恳求报文的格局

<methodB & 7> <reques0 D z yt-URI> <v& j t C E &ersion>
&} 5 Elt;headers>
<entity-body>

呼应报文的格局

<version> <st_ E m S j  s o %atus> <reason-phrase>
<headers>
<entity-body>

下图是翻开n K w x D ^ ` u Chrome 检查极课3 y f ] K B z时刻网页的恳求信息。包含呼应行、呼应头、呼应体等9 5 T 3 O W k 5 A信息。

带你打造一套 APM 监控系统(一)

下图是在终端运用 curl 检查一个完好的恳求和{ , 9 2 T G t E K呼应数据

带你打造一套 APM 监控系统(一)

咱们都知道在 HTTP 通信中,呼应数据会运用 gzip 或其他紧缩办法紧缩,用 NSUR? 6 o 9 9 y ]LProtocol 等计划监听,用 NSData 类型去核算剖析流量等会形成数据的不准确,由于正常一个 HTTP 呼应体的内容是运w | ~ # _ A . t用 gzip 或其他紧缩办法紧缩的,所以运用 NSData 会偏大。

2.5.2 问题
  1. Request 和 Response 不必定成对存在

    比方网络断开、App 突然 Crash 等,所以 Request 和 Response 监控后不应该记载在一条记载里

  2. 恳求流量核算? 4 W ]办法不准确

    首要原因有:

    • 监控技能计划疏忽了恳求头和恳求行部分的数据巨细w x X ( @ C
    • 监控技能计划疏忽了 CoD C [ `okie 部分的数据巨细
    • 监控技能计划在对恳求体巨细核算的时分直接运用 HTTPBoj u 2dy.length,导致不行准确
  3. 呼应流量核算办法不准确

    首要原因有:b 1 L * U P + Q

    • 监控技能计划疏忽了呼应头和呼应行2 e D部分的数据巨细
    • 监控技能计划在对 body 部分的字节巨细核算,因采用 excepteB 1 - v KdContentv = C 0 x ` HLeng6 b ) - + D a 6th 导致不行准确w R . g I O
    • 监控技能计划疏忽了呼应体运用 gzip 紧缩。真实的网络} O O { h j通信进程中,客户端在主张恳求的恳求头中 Accee . 3 9 K U ` 3 fpt-EncodiD v : 1 9 ~ng 字段代表客户端支撑的数据紧缩办法(标明c 0 n h g m ( E ,客户端能够正常运用数据时支撑的紧缩办法),同样服务端依据客户端想要的紧缩办法、服务端当时支撑的紧缩办法,终究处理数据,在呼应头中Content-Encoding 字段标明当时服务器采用了什么紧缩办法。
2.5.3 技能完结

第五部分讲了网络阻拦的各种原理和技能计划,这儿拿 NSURLProtocol 来说完结流量监控(Hook 的办法)。从上述知道了咱们需求什么样的,那么就逐步完结吧。

2.5.3.1 Request 部分
  1. 先运用网络监控计划将 NSURLProtocol 办理 App 的各种网络恳求

  2. 在各个办法内部记载各项所需参数(NSURLPru ` ^otocol 不能剖析恳求握手、挥手等数据巨细和时刻耗费,不过关于正常状况的接口流量剖析足够了,最底层需求 SockeO p * y ; U S bt 层)

    @property(nonatomic, strong) NSURLConnection *internalConnection;
    @property(nonatomic, strong) NSURLRes3 A G T j 0 Z sponse *internalResponse;
    @property(nonatomic, strong) NSMutableData *responseData;
    @property (nonatomic, strong) NSURLRequest *internalRequest;
    
    - (vo{ 5 C [ 6 p ! ,id)startLoading
    {
    NSMutableURLRequest *mutableRequest = [[self request] mutableCopy];
    self.internalConnection = [[NSURLConnection alloc] initWithRequest:mutableRequest delegate:self];
    self.int% r W L m MernalRequest = self.request;
    }
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
    {
    [self.client URLProtocol:sel2 6 Cf didReceiveResponse:response cac. / ; m T u 0 . NheStu G F foragePolicy:NSURLCacheStorageNotAllowed];
    self.iM } 6 p _ {nternalResponse = response;
    }
    - (void)co; z p m x k L ! knnection:(NSURLConnection *)connection didReceivP u B MeData:(NSData *)data9 a 3 )
    {
    [self.responseData appendData:data];
    [self.cN 0 klient URLProtocol:self didLoadData:@ W b T 2 8 H 6 =data];
    }
    
  3. Status Line 部分

NSURLResponse 没有 Status Line 等特点或许接口,HTTP Vt 4 S k w .ersion 信息也没有,所以要想获取 Status Line 想办法转换到 CFNetwork 层试试看。发现有私有 API 能够完结。

b S c % O路:将 NSURLResponse 经过 _W t ^ 4 # i * }CFURLResponse 转换为 CFTypeRK Y 0 5ef,然后再将 CFT6 ( A aypeRef 转换为 CFHTTPMessageRef,再经过 CFG w ) Q P 4 k x XHTTPMessageCopyResponseStatusLine 获取 CFHTTPMessageRef 的 Status Line 信息。

将读取 Stat+ h X ( 3 $us Line 的功用增加一个 NSURLY j } n fResponse 的分类。

// NSURLResponse+cm_FetchStatusLineFromCFNetwork.h
#import <Fouq / O @ Rndation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSURLResponse (cm_FetchStatusLineFromCFNetwork)
- (NSString, f * M 8 R = *)cm_fetchStatusLineFromCFNetwork;
@end
NS_ASSe g 0 { $ 3 J J IUME_NONNULL_END
// NSURLResponse+cm_FetchStatusLineFromCFNetwork.m
#import "NSURLResponse+cm_FetchStatus? f 4 7 2 / wLineFromCFNetwork.h"
#import h 3 $ f 3 dt <dlfcn.h>
#define SuppressPerformSel, D v L  K % s gectorLeakWarning(Stuff) \
do { \
_Pragma(; J _ ( 9 @ f"clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
Stuff; \
_Pragma("clang dir u dagnostic4 = V : G P T ; pop") \
} while (0)
typedefd R 5 s 6 CFHTTPMessageRef (*CMURLResponseFetchHTTPRespW # P k R O y bon+ 6  $ W [ Rse)(CFURLRef responsec a ~ 0);
@impC a 7 b A t S 6 glementation NSURLResponse (cy n 9 c %m_FetchStatusLineFromx k g * - 0CFNetwork)
- (NSString *)cm_fetchStatusLineFromCFNet` ` Q q 2 $wor@ Z g O C *k
{
NSString *statusLine = @"";
NSString *funcName = @"CFURLResponQ u Q h } {seGetHTTPResponse";
CMURLResponseFetchHTTPReH  J O . @ 7 n 9spV 7 i M m g $ ~ {onse originalURLe M p } s } 3Responsen 5 o xFF f d q W ^etchHTTPRes~ n 1ponse = dlsym(RTLD_DEFAULT, [fk S  e ( P V !uncName UTF8String]);
SEL getSelector = NSSelectorFromString(@"_CFURL/ f $ f H q 7Response");
if ([sek 4 D d C % klf rU ! F f  M E ( {espondsToSelector:getSelecto} i Yr] &- = o k 0 5 @ $& NULL != originalURLResponseFetchHTTPResponse) {
CFTypeRef cfResponse;
SuppressPerformSelectorLeakWarning(
cfRT m Kesponse = CFBridgingRetain([self performSelector:getSelector]);
);
if (NULL != cfResponse) {
CFHTTPMessagf w I U qeRef messageRef = originalURLResponW 8 useFetchHTTPResponse(cfResponse);
statusLine =F B 6 d (__bridge_transfer NSString *)CFHTTPMessageCopyRespj z onseStatC f m M V 2 , qusLine(messageRef);
CFRelease(cfResponse);6 C ) = b C 8 F ^
}
}
return statusLinT w ( D g I e Te;
}
@end
  1. 将获取到的 Status Line 转换为 NSData,再核算巨细

    - (NSUIntegeQ : !r)cm_getLineLength {
    NSString *statusLineString = @"";
    if ([self isKindOfClass:[NSHTTPURLResponse class]]) {
    NSHTTPURLResponse *S K y . ~ 3 ~ ~ vhttpResponse = (NSHr 4 E ` lTTPURLResponse *)self;
    statusLineString = [self cm_fetchStatusLineFromCFNetwork];
    }
    NU k U 3 G ^SData *lineData = [statusLineString dataUsingEncoding:NSUTF8StringEncoding];
    return lineData.length;
    }
    
  2. Headel 1 ? ^ j Z W ^ 4r 部分

    allHeaderFields 获取到 NSDic7 G D 5 T l f 1 Ztionary,然后依照 key: value 拼接成字符串,然后转换成 NSData 核算巨细

    N ? ^ M Q m意:key: value ks @ Y L p Vey 后是有空格的,curl 或许 chrome Network 面板能够检查印证下。

    - (NSUInteger)cm_* X z )getHeadersLength
    {
    NSUInteger headersLength = 0;
    if ([self isKindOfClass:[NSHTTPURLResponse class]]) {
    NSI I B u 1 h xHTTPURLReh O 1 y Q Psponse *ho R #ttpResponse = (NSHTTPURLResponse *)s* . X T # #elf;
    NSDictionary *headerFields = httpResponse.allHeaderFieldC ) as;
    NSString *headerString = @") : M K Q ` ; j";
    for (NSString *key in headerFields.allKeys) {
    headerString = [headerStr st4 # f S  ) 1ringByAppendingString:key];
    heah ^ x 9 u 6dheaderStringerStr = [headerString sV T CtringByAppendingString:@N | V B": "];
    if ([headerFieldZ g +s objectForKey:key]) {
    heaV K S ! z - W - SderString = [headerString stringByAppendingString:headerFields[key]];
    }
    headerString = [headerString stringByAppendingStrin| 9 ig:@"\n"];
    }
    NSData *headerData = [heade$ y 8 9 ` ;rString dataUsingEncoding:NSUTF8StringEncoding];
    headersLength = headerData.length;
    }
    return headersLength;
    }
    
  3. Body 部分

    Body 巨细的核V 8 / p P k A w h算不能直接运用 excepectedContentLength,官方m n J I x Y文档阐明晰其不准确性,只能够作为参阅。或许 allHeaderFields 中的 Content-Length 值也是不行准确的。

    /*!

    @abstract Returns the expected content length of the receiver.

    @discussion Some protY c V w 7 B J docol implemF { r [entations report a content length

    as part of delivering load metadata, but not all protocols

    guarantee the amount of data that wT a U v V ~ 3 R ,ill be delivered in an ~ Q ; H . A Zctuality.

    Hence, thi? E k A x – ks method returns an eT 7 @ a ` n E n oxpected amount. Clients should use

    this value as an advisory, and should be prepared to deal with

    either more or less data.

    @result The expected content length of the receiver, or -1 if

    there is no expectati_ 0 W F + 4 L e Fony s T } * – that can be arrived at regarding expected

    content length@ / v V f p.

    */

    @property (readonly) long long expectedContentLength;

    • HTTP 1.1 版别规则,假设存在 Transfer-Encodz & Eing: chunked,则在 header 中不能有 Content-Length,有也会被忽视。
    • 在 HTTP 1) h m 2 ^ g.0及之前版别中,content-length 字段可有可无
    • 在 HTTP 1.1及之后版别。假设是 keep alive,则 Content+ 0 Y [ ? 0 ] O -Lengthchunked 必定是二选一。S @ h B 3 c (若对错keep alive,则和 HTTP 1.0相同。Contep 6 } @nt-Length 可有可无。y & H a .

    什么是 Transfer-Encoding: chunked

    数据以一系列分块的办` . ^ q / 0 : H D法进行发送 Content-Length 首部在这种状况下不被发送.7 ^ v 在每一个分块的最初需求增加当时分块的长度, 以十六进制的办法标明,后边紧跟着 \r\n , 之后是分块自身, 后边也是 \r\n ,终m 0 V y 1 * Q . 7止块是一个惯例的分块, 不同之处在于其长度为0.

    咱们之前拿 NSMutableData 记载了数据,所以咱们能够在 stopLoading办法中核算出 Body 巨细。进程如下:

    • didReceiveData 中不断增a + = ~加 data

      - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
      {
      [self.responseDaU 8 $ q + o P # /ta appendData:data];
      [self.client URLProtocol:self didLoadData:data];
      }
      
    • stopLoading 办法中拿j | * n k *allHeaderFields 字典,获取 Con@ d K # g [ $ v |tent-Encoding key 的值,假设是 gzip,则在 stopLoading 中将 NSData 处理为 gzip 紧缩后的数据,B S W ` L $ D再核算巨细。(gzt X q L 1 $ip 相关功用能够运用这个V H W !东西)

      需求额定核算一个空白行的长度

      - (void)stopLoadi
      {
      [self.internalConnection cancel];
      PCTNetworkTrafficModel *model = [[PCTNetworkTrafficModel alloc] init];
      model.path = self.request.URL.path;
      model.host = self.request.URL.host;
      model.type = DMNetworkTrafficDataTypeResponse;
      model.lio V . O ~ X MneLength = [self.internalResponse cm_gea v v ntStatusLineLeng& 4 : % ? Ath];
      model.headerLeng| ^ }th = [self.internalRM s : t ( ! N +esponse cm_getHeadersLength];
      model.emptyLineLength = [self.internalResponse cm_getEmptyLineLength];
      if ([self.dm_response isKindOfClass:[NSHTTPURLResponse class]]) {
      NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)self.dm_response;
      NSData *data = self.dm_data;
      if ([[httpResponse.allHeaderFields objectForKey:@"Content-Encox a 1 e y * e +ding"] isEqualToString:@"gzipj 5 M w 3 6"]) {
      data = [self.dm_data gzippedDa{ m { Pta];
      }
      mo] @ $ p | [del.bodyLen+ U * Qgth = data.length;
      }
      model.lenw ] 3 N + x 6gth = model.lineLength + model.heaU E m u q ! 7 cderLength + model.bodyLength + model.emptyLineLength;
      NSDictionary *networkTraffU m R x j | ~icDictx Q w @ionaS v Rry = [z 7 { y f Dmodel convertToDictionary];
      [[Pr[ 3 F ; kismClient sharedInstan: | c E d Lce] sendWithTy| ? ~ w 8 { C gpe:CO E @ z 0MMonitor9 ( 5 3 ~ j lNetworkTrafficType meta:networkTrafficDictionary payload:nil];
      }
      
2.5.R ` 43.2 Resquest 部分
  1. 先运用网络监控计划将 NSURLPr: ! I g l ` E g otocol 办理 Ap{ I & E C c 6p 的各种网络恳求

  2. 在各个办法内部记载各项所需参数(NSURLProtocol 不s = s ! Y N ! + ^能剖析恳求握手、挥手等数据巨细和时刻耗费,不过关于正常状况的接口流量剖析足够了,最底层需求 Socket 层)

    @property(nj k Qonatom-  j / oic, strong) NSURLConnection *intl % A Q - q G [ iernalConnection;
    @propeU n ` Q hrtyu _ C 3 q ~(nonatomic, strong) NSURLResponse *internalResponse;
    @property(nonatomic, strong) NSMutableData *responseData;
    @property (nonatomic, strong) NS 1 6SURLRequest *it 9 z / j D $ 3 (nternalRequest;
    
    - (8 z I hvoid)startLoading
    {
    NSMutableURLRequest *mutableRequest = [[self request] mutableCopy]X q O 8 &;
    sev M . ] G O T |lf.internalConnect@ Q v 7 V p C % Hi& [ c /on = [[NSURLConnel Q _ G 7ction alloc] initWithRequest:mutableRequest dL O w s Eelegate:self];
    self.internalRequest = self.request;
    }2 T 9 / 2 F a %
    - (void)connect+ ~ G k  0ion:(NSURLConnection *)connection didRecei, E a . E I j 0 2veResponse:(NSURLResponse *)response
    {
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStora { 0 %  w ^ {agE F A g x Z ~ P leNotAllowed];
    self.internalResponse = response;
    }
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
    {
    [self.resK L - 7 MponseData appendData:data];
    [self.client URLProtocol:self didLoadData:data];
    }^ g $ . 2 k
    
  3. Status Line 部分

    关于 NSURLRequest 没有像 NSURLRea 6 t z ; D d ; Xsponse 相同的= F 7 2 v w办法找到 StatusLine。所以兜底计划是自己依据 Status Line 的结构,自己手动构造一个。结构为:协议版别号+空格+状况码+空格+状况文本+换行

    为 NSURLRequest 增加一个g w D p专门获取 Status Line 的分类。

    // NSURLResquest+cm_FetchStatusLineFromCFC s y ; ! r 0 YNetwork.m
    - (NSU- ? !Integer)cm_fet( B 2 NchStatusLineLength
    {
    NSString *statusLineString = [. v Z % 2 {NSString stringWithFormat:@"%@M 9 ` q p ) a r K %@ %@\n", self.HTTPMethod, se_ u x y N 7lf.URL.path, @"HTTP/1.1"];
    NSData *statusLineData = [statusLinc W h q 0 = * beString dataUsingEncoding:NSUTF8StringEncoding];
    return statusB I a x s tLineData.length;
    }
    
  4. Header 部分

    一个 HTTP 恳求会先构建判别是否存在缓存,然后进行 DNS 域名解析以获取恳求域名的服务器 IP 地址。假设恳求协议是 HTTPS,那么还需求树立 TLS 衔接。接下来便是运用 IP 地址和服务器树立? B q O 4 3 TCP 衔接。衔接树立之后,浏览器端会构建恳求行、恳求头等信息,并把和该域名相关的 Cooki. : D 8 K ke 等N D x S `数据附加到k K Y P D m恳求头中,然后向服务器发送构建的恳求信息。

    所以一个网络监控不考虑 cookie ,借用王多鱼的一句话「那不完犊子了吗」。

    看过一些文章说 NSURLRequest 不能完好获取到恳求头信息。其实问题不大, 几个信息获取不完全也没办法。衡量监控计划自身便是看接口在不同版别或许某些状况下数据耗费是否反常,WebViO b U X ? c s B )ew 资源恳求是否过大,类似于操控变量法的思维。

    所以获取到 NSURLRequest 的 allHeaderFields 后,加上 cookie 信息,核算完好的 Header 巨细

    // NSURLResquest+cm_FetchHeaderWithCookies.m
    - (NSUInteger)cm_fetchHeaderLengthWithCookie
    {
    NSDictionary *headerFields = self.allHTTPHeaderFiP g s  D ; d x 8elds;
    NSDictionary *cookiesHeader = [self cm$ a l ] D R } [ m_fetT g y hchCo9 G Y J E E = SokieI w W 7 t f d 6 Vs];
    if (cookiesHeader.count) {
    NH w z Z l 8 4SMutableDih U fctionary *headerDictionaryWithCookies = [NSMutableDictionary dictionaryWithDictionary:headerFields];
    [R a }headerDis % 4ctionaryWithCookies addEntriesFromDictionary:cookiesHeader];
    headerFields = [headerDic3 A # `tionaryWithCookies copy];
    }
    NSString *headerString = @"";
    for (NSStr2 a B X / 2 } Zing *key in headerFields.allKeys) {
    headerString = [headerString s1 Y % ]tringByAppendingSt2 5 W 4ring:key];
    headerString = [headerString stringByAB 8 Y V v X dppendingString1 { - # i U C U:@": "];
    if ([he = , ` 4 e )aderFields objectForKey:key]) {
    headerString = [headerString stri1 o g = r ; BngByAppendingString:headerFields[kz q 3 q 8ey]];
    }
    headerStrinX M q W 0 0g = [headerString stringByAppendM P Z YingString:@"\n"]Z t B;
    }
    NSData *headerData = [headerString dataUsingEncoding:NSUTFH C 7 {8StringEncoding];
    headersLength =j q u $ headerData.length;
    r@ - ) heturn headerString;
    }
    - (NSDictionary *)cm_fetchCookies
    {
    NSDictionary *cookiesHeaderDictionary;
    NSHTTPCookieStorage *cookieStorage = [NSHO & N & 3 x kTTPCookieStorage sharedHTTPCookieStorage];
    NSArray<NSF B & `HTTPCookie *> *cookies = [coor ` P ckieStorage cookie] D l ,sForURL:self.URL];
    if (cookies.count) {
    cookiesHeaderDictionary& _ N = [NSHTTPCookie requestHeaderFieldsWithCoo1 r v o ( 4 U *kies:cookies];
    }
    return cookiQ 8 A y d desHeaderDictionary;
    }
    
  5. Body 部分

    NSURLConnection 的 HTTPBod` W zy 有或许获取不到,问题类似于 WebViewc f S O 6 t , @ 上 ajax 等状况。所以能够经过 HTTPBodyStream 读取 stream 来核7 Y $ } R k L y C算 bo* 8 K i 1dy 巨细.

    - (NSUInteger)cm_fe{ T . m z 8 P :tchRequestBody
    {
    NSDictionary *headerFields = self.allY m u / ] X eHTTPHeaderFields;
    NSUIu L 0 4nteger bodyLength = [self.HTTPBody length];
    if ([headerFields o| e . ? 9bjectF^ Z C e ^ v horKey:@"Content-Encoding"]) {
    NSData *bodyData;
    if (self.HTTPBody == nil) {
    uint8_t d[1024] = {0};
    NSInputStream *stream = self.HTTPBodyStream;
    NSMutas R Y d  4bleData *dataa Z . _ 5 U : = [[NSMutableData alloc] init];
    [stream open];
    while ([stream hasBytesAvailable]) {
    NSInteger len = [stre4 z { ^am read:d maxLengtC I [ ] ~ h + K rh:1024];
    if (len > 0 && stream.streamError == nil) {
    [data appendBytes4 7 L = / M ] 4 P:(void *)d leng% / ,  U Q Z E ~th:len];
    }
    }
    bodyW ? f G 6 oData = [daQ q E z T Xta copy]S O ~ l;
    [stream close];
    } else {
    bodyData = s` & ~ t : 0elf.HT` 1 &TPBody;
    }
    bodyLb - t w  l l b iength = [[bodyData gzippedData]n / * | x ) - @ r length];
    }
    retur5 y W 8 O h %n bodyLength;
    }
    
  6. - (NS R ) , PURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)requG ] Eest redirectResponse:(NSURLResponse *)response 办法中将数据上报会在 打造功用强壮、灵敏可装备的数据上报组件 讲

    -(NSURLRequest *)connection:(NSURLConnection *)connection willSendReques! : 0 . }  8t:(NSURLRequest *)request redirectResponse:(J = S ^ $ lNSURLResponse *)response
    {
    if (response != nil) {
    self.internalResponse = re; y j _ r = r {sponse;
    [self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
    }
    PCTNetworkTrafficModel *model = [[PCTNetworkTrafficModel alloc] init]B T - 6 j 5 4 D X;
    model.path = request.URL.path;
    model.host = request.URL.host;
    model.type = DMNet7 7 d g D H S 9 dworkTrafficDataTypeRequest;
    model.lineLength = [connecd p Y ;tion.currentRequest dgm_getLineLength];
    model.headerLength = [connection.currentRequest dgm_getHeadersLengthWithCooks X B rie];
    model.bodyLength = [connection.currentRequest dgm_getBodyLength];
    model.emptyLineLength = [self.internalResponse cm_getEmptyLineLength]5 9 d S 8 T g 9 C;
    model.length = model.lineLength + model.headerLength + model.bodyLeq A c w | ,nJ 2 H 1 u E hgth + mode) 2 j $ + L |l.emptyLineL3 k ;ength;
    NSDictionary *ne3 ! / , |tworkTrafficDictionary = [model convertToDictionaC n ^ T ~ry];
    [[PrismClient sharedInstanc( p h V $ G 6 ee] sendWithTypw [ = Q u $e:CMMonitog m + 0 F @ Y z ^rNetworkTrafB ^ M c _ q Z ~ 1ficType meta:networkTrafficDictionary payload:nil];
    return request;
    }