版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
APM是ApplicationPerformanceMonitoring的缩写,监视和管
理软件应用程序的性能和可用性。应用性能管理对一个应用的持续
稳定运行至关重要。所以这篇文章就从一个iOSApp的性能管理
的纬度谈谈如何精确监控以及数据如何上报等技术点
App的性能问题是影响用户体验的重要因素之一。性能问题主要包含:Crash,
网络请求错误或者超时、UI响应速度慢、主线程卡顿、CPU和内存使用率高、
耗电量大等等。大多数的问题原因在于开发者错误地使用了线程锁、系统函数、
编程规范问题、数据结构等等。解决问题的关键在于尽早的发现和定位问题。
本篇文章着重总结了APM的原因以及如何收集数据。APM数据收集后结合数
据上报机制,按照一定策略上传数据到服务端。服务端消费这些信息并产出报告。
请结合姊妹篇,总结了如何打造一款灵活可配置、功能强大的数据上报组件。
一、卡顿监控
卡顿问题,就是在主线程上无法响应用户交互的问题。影响着用户的直接体验,
所以针对App的卡顿监控是APM里面重要的一环。
FPS(framepersecond)每秒钟的帧刷新次数,iPhone手机以60为最佳,iPad
某些型号是120,也是作为卡顿监控的一项参考参数,为什么说是参考参数?
因为它不准确。先说说怎么获取到FPSoCADisplayLink是一个系统定时器,会
以帧刷新频率一*样的速率来刷新视图。[CADisp/agLMkdisplayUi^kVJitkTargetself
。至于为什么不准我们来看看下面的小例代码
_AisplagLi八k=[CADisplayLinkdisplayLiiakWitkTarget^elf
selectOK':^selector(p_displayLik\kTick:')];
[_displayUi^ksetPaased:YES];
匚displagLi八k“ddToR〃八Loop:[NSR〃八Loopc〃“"±Ru八Loop]
f"Modc:NSRu八LoopCoNHAOnModcsJ;
代码所示,CADisplayLink对象是被添加到指定的RunLoop的某个Mode下。
所以还是CPU层面的操作,卡顿的体验是整个图像渲染的结果:CPU+GPUo
请继续往下看
1.屏幕绘制原理
讲讲老式的CRT显示器的原理。CRT电子枪按照上面方式,从上到下一行行
扫描,扫面完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次
扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其
他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进
行扫描时,显示器会发出一个水平同步信号(horizonalsynchronization),简
称HSync;当一帧画面绘制完成后,电子枪恢复到原位,准备画下一帧前,显
示器会发出一个垂直同步信号(Verticalsynchronization),简称VSync。显示
器通常以固定的频率进行刷新,这个固定的刷新频率就是VSync信号产生的频
率。虽然现在的显示器基本都是液晶显示屏,但是原理保持不变。
通常,屏幕上一张画面的显示是由CPU、GPU和显示器是按照上图的方式协同
工作的。CPU根据工程师写的代码计算好需要现实的内容(比如视图创建、布
局计算、图片解码、文本绘制等),然后把计算结果提交到GPU,GPU负责图
层合成、纹理渲染,随后GPU将渲染结果提交到帧缓冲区。随后视频控制器会
按照VSync信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器显示。
在帧缓冲区只有一个的情况下,帧缓冲区的读取和刷新都存在效率问题,为了解
决效率问题,显示系统会引入2个缓冲区,即双缓冲机制。在这种情况下,GPU
会预先渲染好一帧放入帧缓冲区,让视频控制器来读取,当下一帧渲染好后,
GPU直接把视频控制器的指针指向第二个缓冲区。提升了效率。
目前来看,双缓冲区提高了效率,但是带来了新的问题:当视频控制器还未读取
完成时,即屏幕内容显示了部分,GPU将新渲染好的一帧提交到另一个帧缓冲
区并把视频控制器的指针指向新的帧缓冲区,视频控制器就会把新的一帧数据的
下半段显示到屏幕上,造成画面撕裂的情况。
为了解决这个问题,GPU通常有一个机制叫垂直同步信号(V-Sync),当开启
垂直同步信号后,GPU会等到视频控制器发送V-Sync信号后,才进行新的一
帧的渲染和帧缓冲区的更新。这样的几个机制解决了画面撕裂的情况,也增加了
画面流畅度。但需要更多的计算资源
答疑
可能有些人会看到「当开启垂直同步信号后,GPU会等到视频控制器发送
V-Sync信号后,才进行新的一帧的渲染和帧缓冲区的更新」这里会想,GPU收
到V-Sync才进行新的一帧渲染和帧缓冲区的更新,那是不是双缓冲区就失去
意义了?
设想一个显示器显示第一帧图像和第二帧图像的过程。首先在双缓冲区的情况下,
GPU首先渲染好一帧图像存入到帧缓冲区,然后让视频控制器的指针直接直接
这个缓冲区,显示第一帧图像。第一帧图像的内容显示完成后,视频控制器发送
V-Sync信号,GPU收到V-Sync信号后渲染第二帧图像并将视频控制器的指
针指向第二个帧缓冲区。
看上去第二帧图像是在等第一帧显示后的视频控制器发送V-Sync信号。是吗?
真是这样的吗?
揭秘。请看下图
verticalverticalvertical1
retraceretjaceretrace-
」」
FVideodrwd6wdrWpdQW
1.Singleaa①
AB(u①cPD
bufferingImemoryP
2.Double"Buffer
buffering
Video
memory
3.*IHple-Buffer1
buffering
Buffier2
Video
memory
4,DoubleBuffer
buffering
withframe
Video
Bdelayedmemory
d
ar
5.7Hple-Buffer1E
buffering
withframe
BdelayedBuffer2
Videoop/cop/op/
memoryABC
当第一次V-Sync信号到来时,先渲染好一帧图像放到帧缓冲区,但是不展示,
当收到第二个V-Sync信号后读取第一次渲染好的结果(视频控制器的指针指
向第一个帧缓冲区),并同时渲染新的一帧图像并将结果存入第二个帧缓冲区,
等收到第三个V-Sync信号后,读取第二个帧缓冲区的内容(视频控制器的指
针指向第二个帧缓冲区),并开始第三帧图像的渲染并送入第•个帧缓冲区,依
次不断循环往复。
请查看资料,需要梯子:Multiplebuffering
2.卡顿产生的原因
VSync信号到来后,系统图形服务会通过CADisplayLink等机制通知App,
App主线程开始在CPU中计算显示内容(视图创建、布局计算、图片解码、
文本绘制等)。然后将计算的内容提交到GPU,GPU经过图层的变换、合成、
渲染,随后GPU把渲染结果提交到帧缓冲区,等待下一次VSync信号到来再
显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个VSync时间周
期内,CPU或者GPU没有完成内容的提交,就会造成该帧的丢弃,等待下一
次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是CPU、GPU层
面界面卡顿的原因。
目前iOS设备有双缓存机制,也有三缓冲机制,Android现在主流是三缓冲机
制,在早期是单缓冲机制。
iOS三缓冲机制例子
CPU和GPU资源消耗原因很多,比如对象的频繁创建、属性调整、文件读取、
视图层级的调整、布局的计算(AutoLayout视图个数多了就是线性方程求解难
度变大)、图片解码(大图的读取优化)、图像绘制、文本渲染、数据库读取(多
读还是多写乐观锁、悲观锁的场景)、锁的使用(举例:自旋锁使用不当会浪费
CPU)等方面。开发者根据自身经验寻找最优解(这里不是本文重点)。
3.APM如何监控卡顿并上报
CADisplayLink肯定不用了,这个FPS仅作为参考。一般来讲,卡顿的监测有
2种方案:监听RunLoop状态回调、子线程ping主线程
3.1RunLoop状态监听的方式
RunLoop负责监听输入源进行调度处理。比如网络、输入设备、周期性或者延
迟事件、异步回调等。RunLoop会接收2种类型的输入源:一种是来自另一个
线程或者来自不同应用的异步消息(source。事件)、另一种是来自预定或者重
复间隔的事件。
RunLoop状态如下图
RunLoop
1.通知Observer:即将进入LoopObserv
2.通知Observer:将要处理TimerIObserv
3.通知Observer:将要处理SourceOObserv
4.处理SourceO—>1Sourcei
5.如果有Source1,跳到第9步Source
Sourcel(port)6.通知Observer:线程即将休眠Observ
Timer—»7.休眠,等待唤醒
外部手动唤醒8.通知Observer:线程刚被唤醒Observ
9.处理唤醒时收到的消息,之后跳回2Timer
Source
10.通知Observer:即将退出LoopObserv
第一步:通知Observers,RunLoop要开始进入loop,紧接着进入loop
if(curre^tMode->_obse^vcrMask&kCFRu八LoopE八Wg)
//通知Observers:RtmLoop即将进入loop
_CFR〃八curre八tMode,kCFRu八LoopE八trg);//进入loop
result=_CFR〃nLoopRu八currentModejseconds,小仇(mAfte丫SouKceHandlcd,
previousMode);
第二步:开启dowhile循环保活线程,通知Observers,RunLoop触发Timer
回调、SourceO回调,接着执行被加入的block
if(rli^->_obseKV^Mask&kCFR〃八LoopBeforeT7nAed)
//通知Observed:RcmLoop即将触发T/mer回调
_CFR〃八LoopDoObscrvcdCMHkvbk.CFR.u^L.oop8eforeTi^e^if(Hm->_ofeserverM^sk&
kCFRu八LoopBefoscS。乂KCCS)
//通知Observers:RtmLoop即将触发Source回调
_CFR〃八LoopDoObscrvcH(?Lr/m,kCFR〃八LoopBeFo%So〃%es);//执彳了被加入的b/ock
_CFR〃八L0opDoB/ocks(H,r/m);
第三步:RunLoop在触发SourceO回调后,如果Sourcel是ready状态,
就会跳转到handle_msg去处理消息。
//如果有Source(基于port)处于ready状态,直接处理这个Source工然后跳转去处理消息
if(MACH_PORT_NULL!=dispatchPoH&&!didU>ispatchPortLastTii^e)
DEPL。丫MEN口ARGE7LMACOSX||DEPLOYMEN匚TARGE匚EMBEDDED||
DEPLOYMENTLTARGETLEMBEDDED_M/M
MSg=(Kv\ach_^g_header_t
(八
if(_CFRALoopSe"iceMachPoH(dispatchPort,&&sg,sizeof(Msg_b(Affer),&/2ePort,Ot
&evoucherState,NULL)){
gotoha^d(e_^sg;
}#c狂DEPLOYMEN口ARGETLWWDOWS
if(__CFR.u^Loop\A/aitFo^MultipleObjects(NUL-LJ&dispatchPort,O,O,&:livePort,NULL))
(
gotoka^dle_i^sg;
}#。八Aif
)
第四步:回调触发后,通知Observers即将进入休眠状态
Booleanpoll=so〃%eH〃八山cdTkisL。。?||(OUL-L==tii^.eout_co^.text->ter^TSR};//通知
Observers:Ru八Loop的线程即将进入休眠(s/ccp)iF(①。"&&(r/m->_observerMask&
kCFRu八LoopBcfo-eWaitMg))_CFR〃八Lo0pDoObscrvcKS(”_,r/m,kCFR〃八LoopBcforeW川't/'ng);
_CFR〃八LoopSctS/ee磔八g(“);
第五步:进入休眠后,会等待mach_port消息,以便再次唤醒。只有以下4种
情况才可以被再次唤醒。
•基于port的source事件
•Timer时间到
•RunLoop超时
被调用者唤醒
do{
if(kCFUseCollectableAllocator){
//objc_clear_stack(C>);
//<rdar://problet^/163^3cJ5'^>
O,$izeof(i^sg_buffer));
)
Msg=(n/\〃ch_Fv\sgJe〃Wer_t*)ms^_feuffer;
_CFR(xnLoopServiceM〃ckPort(w〃itSet,&nASg,sizeof(MSg_buffeC,&/2ePort,poll?
O:TIMEOUTJNFIMITY,^zvouckerState,&:vouckerCopy);
if(kodcQ〃e〃ePort/=MACH_PORT_NULL&&(ivePort==i^odeQueuePort){
//Pramtheqaeue.IfOMofthecalloutblockssetsthetii^e^Fired
flag,breakoutav\dservicethetiller.
while(_dispatch_r(Aialoop_root_queue_perfori^._4-CF(d^->_queuey);
if("3->_母m0用*4){
//LeavelivePortasthequeueport,ai^dservicet/mersbelow
rim-^tf'merFrre^=false;
break;
}eke(
if(msg&&Msg!=(kv\ach_i^sg_keaderj:F%e(&sg);
)
)eke(
//Goaheada^d(eavethemiaerloop.
break;
}
}while(1);
第六步:唤醒时通知Observer,RunLoop的线程刚刚被唤醒了
//通知Observed:RtmL。。?的线程刚刚被唤醒了if((poll&&&
kCFR〃八LoopAftcrWa浙八g))_CFR〃八LoopD0Observed(K。Hm,kCFR认八LoopAfterWaiti八g);
//处理消息
人〃八山csg:;
__CFRu^LoopSettgMre\A/al<eUps(rl);
第七步:RunLoop唤醒后,处理唤醒时收到的消息
・如果是Timer时间到,则触发Timer的回调
•如果是dispatch,则执行block
如果是sourcel事件,则处理这个事件
#ifUSE_MK_TIMERJTOO
//如果一个Trmer到时间了,触发这个丁加的回调
elseif(rlkv\->_timerPort!=MACH_PORT_NULL&&UvePort==
rl^\->_tikv\eK'Port'){
CFRUNLOOP_WAKEUP_FOR〕IMER。;
//。八W/'八dowSjwehaveobservedav\issuewherethetii^erportisset
beforethetii^ewhichwerequestedittobeset.Forexample,wesetthefiretimeto
beTSR16764676586(9,butitisactuallyobservedfiringatTSR16764676414S,
whichis工7工Sticksearly.Theresultisthat,wk。_CFR,iAn.LoopDoTi^erschecksto
seeifanyoftherunlooptimersshouldbefiHcg,itappearstobe'tooearly1forthe
八extt/mer,av^d.八0timersarehandled.
///八thiscase,the%MCKporthasbeenautomaticallyreset(s/Mceit
wasreturnedfromMsgWaitFoYModtipleObjcctsEx),aiadifwedonotre-^rmit,tkc八
八。timerswilleverbeservicedagaiy\unlesssokctki八gadjuststhetillerlist(e.g.
addingovredos'八gt/mers).Thefixfortheissue,istoresetthet/merMereif
CFR.u^LoopDoTikv\ersdidi^othandleatiMWitself.93O87S4
if(!_CFR〃八LoopD。丁沁cd"。r/m,^ach_absolute_tii^eO),){
//Re-arw^thenextt/mer
^FArmA/extTimeHi^Mo^eCr/m,rf);
)
}#endif
//如果有despatch到m^m_^ueue的block,执行block
elseif(livePort==dispatchPort){
CFRUZLOOP_WAKEUP_FOR_D/SPATCH0;
_CFR〃.LoopModeUMock(”m);
^_CFR,unLoopUnlock(rl);
_CFSetTSD(_CFTSDKeglsMGCDMai八Q,(void*)6,NULL);#1?
DEPLOYMENTLTARGE匚WINDOWS
void.*MSg=O;#e八diF
_CFRUNLOOP」S_SERVIONC_THE_MAIN_DISPATCH_QUEUE_(/);
_CFS况TSD(_CFTSDK09®八GCDM.认Q,(M。%*)0NULL);
_CFR〃nLoopLock(rl);
__CFR.utaLoopModeLock(rlkv\);
sourc6HaiadlcdThisLoop=true;
didDispatckPortLastTime=true;
)
//如果一个So〃ee工(基于port)发出事件了,处理这个事件
else(
CFRUNLOOP_WAKEUP_FOR_SOURCE();
//Ifwcreceivedavoucherfromthistkeiaputacopyofthe
newvoucherintoTSP.CFMachPoKtBoo^twilllookintheTSPforthevoucher.Bg
usingthevaluemtheTSPwetietheCFMackPortl3oosttothisreceived
explicitlywithoutachancefora八9thicgmbetweentketwopiecesofcodetosetthe
voucheragai\^.
voucher_tpreviousVoucher=
_CFSetTSD(_CFTSDKegMachMessageHasVo(Achec(void^voucherCopyjOS_KC/C〃SC);
CFR.u^.L-oopSourceR.ef"s=
_CFR(mLoopModcFindS0〃%cFoKM〃chPort("‘HFVX,livePort);
if(ds){的fP£PLOYMENT_TA^ET_MACOSX||
DEFWYMEAHLTARGE匚EMBEDDED||DEPLOYMEAHYTARCETLEMBEDDED_M/M
i^\ack_msg_keader_t兴忆p®=NULL;
sourceHaiadledTlaisLoop=_CFR〃八LoopDoSo〃rce2(r。rk,msg,
w\sg->ms^h_5/ze,&rep(y)||sourceHai^dledThisLoop;
if(NULL/=reply){
(voidji^ack^sg^eplyjMACH_SEND_MSG>Keplg->MSgk_$izc>O,
MACH_PORT_NULL,OtMACH_PORT_NULL);
CFAllocatorDeallocat^kCFAllocatorSystekvxDefaultjreply);
DEPLOYMENTLTARCE匚WINDOWS
s。〃匕nd/cdTkisL。。?=_CFR〃八LoopDoSoNKcet,",r/m,rk)||
soueeH"八山edThisLoop;#c八dif
第八步:根据当前RunLoop状态判断是否需要进入下一个loop,.当被外部强
制停止或者loop超时,就不继续下一个loop,否则进入下一个loop
if(sourceHa^.dledThisL.oop&&stopAftc/Ha八die){
//进入bop时参数说处理完事件就返回
retVal=kCFfiui^L.oopRu^Ha^dledSource;
}ekeif(tii^cout_coi^text->teri^.TSR<i^,ach_absolute_time()){
//超出传入参数标记的超时时间了
retVal=kCFR.u^L.oopRui^Ti^v\edOut;
}elseif(_^CFRuiaLooplsStopped(rl)){
_CFR〃八LoopUnsetStoppcdm);
//被外部调用者强制停止了
retVal=kCFR.uiaLoopRuk\Stopped;
}ekeif(rlkv\-stopped){
rlw\->^topped.=fake;
retVal=kCFRunLoopRbmStopped;
}ekeif(_CFR认八LoopModcIsEMptgl",r/m,p^eviousMode)){
//soixrce/t/mer一个都没有
retVal=kCFR〃八LoopR〃八Fi八isked;
)
完整且带有注释的RunLoop代码见此处。Sourcel是RunLoop用来处理
Machport传来的系统事件的,Source。是用来处理用户事件的。收到Sourcel
的系统事件后本质还是调用SourceO事件的处理函数。
RunLoop6个状态
typed&fCF_OPTIONS(CFOptio^Flags,CFRunLoopActivity){
kCFRu^.L.0opEntry,//进入loop
kCFRunLoopBeforeTikvxers,//触发Timer回调
kCFRiA^LoopBeforeSources,//触发SourceO回调
kCFRix八LoopBef。丫©Waiting,//等侍i^\ack\_port消息
kCFR认八LoopAfteRWaiting),//接收i^acla_port消息
kCFRuiaLoopExit,//退出loop
kCFRu八LoopAllActivities//loop所有状态改变
)
RunLoop在进入睡眠前的方法执行时间过长而导致无法进入睡眠,或者线程唤
醒后接收消息时间过长而无法进入下一步,都会阻塞线程。如果是主线程,则表
现为卡顿。
一旦发现进入睡眠前的KCFRunLoopBeforeSources状态,或者唤醒后
KCFRunLoopAfterWaiting,在设置的时间阈值内没有变化,则可判断为卡顿,
此时dump堆栈信息,还原案发现场,进而解决卡顿问题。
开启一个子线程,不断进行循环监测是否卡顿了。在n次都超过卡顿阈值后则
认为卡顿了。卡顿之后进行堆栈dump并上报(具有一定的机制,数据处理在
下一part讲)。
WatchDog在不同状态下具有不同的值。
•启动(Launch):20s
•恢复(Resume):10s
•挂起(Suspend):10s
・退出(Quit):6s
•后台(Background):3min(在iOS7之前可以申请lOmin;之后改为
3min;可连续申请,最多到lOmin)
卡顿阈值的设置的依据是WatchDog的机制。APM系统里面的阈值需要小于
WatchDog的值,所以取值范围在[1,6]之间,业界通常选择3秒。
timeout)方法判断是否阻塞主线程,Returnszero。八success,or八。八-zeroifthetimeout
accused,返回非0则代表超时阻塞了主线程。
主或程RunLoop监听线程
1.通如0b“rv.ri即格进入nmLoop
mmunssnuSBi
[2.通知Observer1即将处理tlnerkCIFRunloopBefore
Timers
3.通如Ob“rv”r即将处理,ourceO
I4.处理blocks&SourceO
6.如果有SourceL跳到9#runloop是否跑完
6.通知Obgrv”线程即将休眠
7.然泯等待唤事source的状态
8.通知/“门”施程刚被唉,
9.处・噢■时收到的消息,之后找回2计效器*
■
10.通知ObserverQl将退出runloopH*«S>Ndunp内存堆栈
可能很多人纳闷RunLoop状态那么多,为什么选择
KCFRunLoopBeforeSources和KCFRunLoopAfterWaiting?因为大部分卡顿都
是在KCFRunLoopBeforeSources和KCFRunLoopAfterWaiting之间。比如
SourceO类型的App内部事件等
Runloop检测卡顿流程图如下:
关键代码如下:
//设置Rui^loopobserver的运行环境CFR“八LoopObscrverC。八匕extcontext-(_bridgevoid
*)seENULL,NULL);//创建尺〃八/。叩observer对象
-observer=CFR(An.LoopObser^erCreate(kCFAllocatorDefaiAlt,
kCFRa八LoopAHActivities,
YES,
G
&ru^L-oopObser/erCallBack,
&context);//将活建的oSserver加入到当前
thread.的ru^loopCFR.u^.LoopAddObserver(CFR.u^L-oopGetMai^0J-observer,
kCFR〃八LoopCo30A0hModes);//创建信号
_$e^v\apkore=dispatch_$emapkore_create(0\
_weak_t9peof(self)weakSelf=self;//在了•线程监控时长
dispatc^asyMCdispatck^e^globaLqueueCO,。),A{
—Strong_typeof(\A/eak$elf)str•。ngSe『=weakSelf;
if(fstroiagSelf){
return;
)
while(YES){
if(stKOKgSe/F.kC〃nce。{
return;
)
//N次卡顿超过阈值T记录为一次卡顿
longseMaphoireVJait=d.ispatch_$eMaphore._wait(se.lf->_se,MapkoreJ
di$patch_tiMe(DISPATCH_TIME_NC>WJstrongSdfliMitMiHisecond*NSEC_PER._MSEC));
if(seiMaphoreVJait!=O'){
if(self->_activity==kCFRunLoopBeforeSources||self->_activity==
kCFRuhLoopAfterWaiting){
if(+-f-stroin.gSe-lf.coum.tTiiw.e<stroin.gS&lf.sta^dstillCou^{
co八tinue;
)
//堆栈信息dump并结合数据上报机制,按照一定策略上传数据到服务器。堆
栈会在下面讲解。数据上报会在[打造功能强大、灵活可配置的数据上报组
^](kttps://github.coM/FaAtasticLBP/kMwledge-kit/blob/Master/Chapterl%2.C)-%2.C>iOS/
l.SO.Md)讲
)
)
stroi^.gSelf.coum.tTiiM.e.=6>
1
});
3.2子线程ping主线程监听的方式
开启一个子线程,创建一个初始值为0的信号量、一个初始值为YES的布尔值
类型标志位。将设置标志位为NO的任务派发到主线程中去,子线程休眠阈值
时间,时间到后判断标志位是否被主线程成功(值为NO),如果没成功则认为
主线程发生了卡顿情况,此时dump堆栈信息并结合数据上报机制,按照一定
策略上传数据到服务器。数据上报会在打造功能强大、灵活可配置的数据上报
组件讲
while(self.isCa八ceh,==NO){
^auto^eleasepool{
BOOLisMai八TlwcadNoResp。nd=YES;
dispatck_sekv\apkore_tsemaphore=dispatck_$ekv\aplaore_create(C>');
A
dispatch_asyv\c(dispatch_ge,t_i^.ai^_queueQJ(
kM川'nTkwadZoResp。nd=NO;
dispatch_^ei^aphore_$igi^al(se^\aphore);
1);
[NSThreadsleepForTimel八tcrvahsdf.thKeskold];
if(7sM“in7>\%4dNoResp0八d){
if(^elf.kaiadlerBlock){
sc/f.h“八d/crB/ock();//外部右block内部dump堆栈(下面会讲),
数据上报
)
)
d.ispatcla_$eMaphore_wait(seinn.aphoreJC>ISPATCH_TIME_FOREVER);
)
)
4.堆栈dump
方法堆栈的获取是一个麻烦事。理一下思路。[NSThreaWca〃StackSg3bokJ可以获
取当前线程的调用栈。但是当监控到卡顿发生,需要拿到主线程的堆栈信息就无
能为力了。从任何线程回到主线程这条路走不通。先做个知识回顾。
在计算机科学中,调用堆栈是一种栈类型的数据结构,用于存储有关计算机程序
的线程信息。这种栈也叫做执行堆栈、程序堆栈、控制堆栈、运行时堆栈、机器
堆栈等。调用堆栈用于跟踪每个活动的子例程在完成执行后应该返回控制的点。
维基百科搜索到"CallStack"的一张图和例子,如下
topofstack
StackPointer>
Localsof
DrawLinestackfr
for
ReturnAddressDrawL
FramePointer>subroL
Parametersfor
DrawLine
Localsof
stackframeDrawSquare
forReturnAddress
DrawSquare
subroutineParametersfor
DrawSquare
上图表示为一个栈。分为若干个栈帧(Frame),每个栈帧对应一个函数调用。
下面蓝色部分表示DrawSquare函数,它在执行的过程中调用了DmwL海函数,
用绿色部分表示。
可以看到栈帧由三部分组成:函数参数、返回地址、局部变量。比如在
DrawSquare内部调用了DrawLine函数:第一先把DrawLine函数需要的参
数入栈;第二把返回地址(控制信息。举例:函数A内调用函数B,调用函数B
的下一行代码的地址就是返回地址)入栈;第三函数内部的局部变量也在该栈中
存储。
栈指针StackPointer表示当前栈的顶部,大多部分操作系统都是栈向下生长,
所以栈指针是最小值。帧指针FramePointer指向的地址中,存储了上一次
StackPointer的值,也就是返回地址。
大多数操作系统中,每个栈帧还保存了上一个栈帧的帧指针。因此知道当前栈帧
的StackPointer和FramePointer就可以不断回溯,递归获取栈底的帧。
接下来的步骤就是拿到所有线程的StackPointer和FramePointer。然后不断
回溯,还原案发现场。
5.MachTask知识
Machtask:
App在运行的时候,会对应一个MachTask,而Task下可能有多条线程同时
执行任务。《OSXandiOSKernelProgramming》中描述MachTask为:任
务(Task)是一种容器对象,虚拟内存空间和其他资源都是通过这个容器对象管
理的,这些资源包括设备和其他句柄。简单概括为:Macktask是一个机器无关
的thread的执行环境抽象。
作用:task可以理解为一个进程,包含它的线程列表。
结构体:task_threads,将target_task任务下的所有线程保存在actjist数组
中,数组个数为act_listCnt
task_threads(
task_ttraget_taskJ
thread_act_arrayj:*act」ist,〃线程指针列表
i^ach_i^sg_type_i^u^ber_t*act」is±C八t//线程个数
)
thread_info:
thread」八Fo(
tkread_act_ttarge^act,
thread_flavo^_tHavoc
thread_ii^fo_tthreadjiafo^utj
macM_ms^_type_^umfeer_t^thread_i^.fo_oatCnt
);
如何获取线程的堆栈数据:
系统方法kem_retum_ttask_threads(task_i^pect_tta^getj;ask,tkread_act_array_i
*act」ist,^v\ach_^sg_type_^.u^v\ber_t^actJistCi^t);可以获取到所有的线程,不过这种方
法获取到的线程信息是最底层的mach线程。
对于每个线程,可以用kem_retum_tthread_get_$tate(thread_actj:tav"get_act,
thread_state_flavor_tflavor,thread_state_told^tate,m«ch_m55_type_n(xmfeer_t
为oldjtateC八t);方法获取它的所有信息,信息填充在_STRUCT_MCONTEXT类型的参
数中,这个方法中有2个参数随着CPU架构不同而不同。所以需要定义宏屏蔽
不同CPU之间的区别。
_STRUFMCONTEXT结构体中,存储了当前线程的StackPointer和最顶部栈帧
的Framepointer,进而回溯整个线程调用堆栈。
但是上述方法拿到的是内核线程,我们需要的信息是NSThread,所以需要将内
核线程转换为NSThreado
pthread的p是POSIX的缩写,表示「可移植操作系统接口」(Portable
设计初衷是每个系统都有自己独特的线程模型,
OperatingSystemInterface)o
且不同系统对于线程操作的API都不一样。所以POSIX的目的就是提供抽象
的pthread以及相关APL这些API在不同的操作系统中有不同的实现,但是
完成的功能一致。
Unix系统提供的和操作的都是内核系统,每个内核
线程由threadj类型的id唯一标识。pthread的唯一标识是pthread_t类型。
其中内核线程和pthread的转换(即thread_t和pthread_t)很容易,因为
pthread设计初衷就是「抽象内核线程」。
Me.M.orystatus_action_Mededptkre.ad_create方法创建线程的回调函数
为nsthreadLaunchero
staticvoid〃〃八thread)
(
[八cpostNotificatio八NaMe:NSTkreadr>idStartNotificatioiaobjectsuserliafo:tail];
[t_$etNakv\e:[t八〃k叫;
[tmam];
[NSThseadexit];
returnNULL;
)
NSThreadDidStartNotification其实就是字符串
@"_NSThreadDidStartNotification"o
<NSThread:(9x...>{numfeer=1」八"HC=mafu}
为了NSThread和内核线程对应起来,只能通过name对应。pthread的
API也可获取内核线程名字。np代表notPOSIX,所以不能
跨平台使用。
思路概括为:将NSThread的原始名字存储起来,再将名字改为某个随机数(时
间戳),然后遍历内核线程pthread的名字,名字匹配则NSThread和内核线
程对应了起来。找到后将线程的名字还原成原本的名字。对于主线程,由于不能
使用pthread_ge.t^aMe_iap,所以在当前代码的load方法中获取到thread_t,然
后匹配名字。
staticyv\ach_port_tiia_th^ead_id;
+(void)load{
i^.aiiaj:hreadjd=kv\ach_thread_^elf();
)
二、App启动时间监控
1.App启动时间的监控
应用启动时间是影响用户体验的重要因素之一,所以我们需要量化去衡量一个
App的启动速度到底有多快。启动分为冷启动和热启动。
YourCode
application:
willFinishLaunchingWithOptions:
Variousmethods
application:
didFinishLaunchingWithOptions:
Running
ActivatetheappapplicationDidBecomeActive:
Event
LoopHandleevents
Switchtoadiflerenlapp
冷启动:App尚未运行,必须加载并构建整个应用。完成应用的初始化。冷启
动存在较大优化空间。冷启动时间从application:didF对%L〃〃八ckMgW汨方
法开始计算,App一般在这里进行各种SDK和App的基础初始化工作。
热启动:应用已经在后台运行(常见场景:比如用户使用App过程中点击
Home键,再打开App),由于某些事件将应用唤醒到前台,App会
在applicationWillE八te/Foregirou八4:方法接受应用进入前台的事件
思路比较简单。如下
.在监控类的load方法中先拿到当前的时间值
•监听App启动完成后的通知U1AppIicati。八DidFi八ishLaunchMgNotiFicati。八
•收到通知后拿到当前的时间
•步骤1和3的时间差就是App启动时间。
似枇兀地so/aef沁c是一个CPU/总线依赖函数,返回一个CPU时钟周期数。系
统休眠时不会增加。是一个纳秒级别的数字。获取前后2个纳秒后需要转换到秒。
需要基于系统时间的基准,通过^ach_tikv\eba$e_ik\fo获得。
iM.ebase_iiafo_dataJ:g_api^^StartupMoiaitorTimebaseti^foData=O;
i^ach_tikv\ebase_ik\fo(&:g_api^i^StartupMoiaitorTii^\ebaseli^foData);
〃,八tirelapse-^v\ach_absolute_time()-g_api^\i^LoadTi^e;doubletimeSpa八=
(tirelapse*g_api^i^StartupMo^itorTi^v\ebasel^.foData.i^u^er)/
(g_api^kv\StartupMoK\itorTii^ebasetnfoData.dewi^.*le9);
2.线上监控启动时间就好,但是在开发阶段需要对启动时间做优化。
要优化启动时间,就先得知道在启动阶段到底做了什么事情,针对现状作出方案。
pre-main阶段定义为App开始启动到系统调用main函数这个阶段;main
阶段定义为main函数入口到主UI框架的viewDidAppear。
App启动过程:
・解析Info.plist:加载相关信息例如闪屏;沙盒建立、权限检查;
・Mach-0加载:如果是胖二进制文件,寻找合适当前CPU架构的部
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 淮阴师范学院《室内系统设计》2022-2023学年第一学期期末试卷
- 淮阴师范学院《网球》2022-2023学年第一学期期末试卷
- 淮阴师范学院《国学经典选读》2022-2023学年第一学期期末试卷
- 淮阴工学院《数据通信技术》2022-2023学年第一学期期末试卷
- 淮阴师范学院《播音与主持》2022-2023学年第一学期期末试卷
- DB3707121-2024小麦良种生产技术规程
- 低温仓储与食品冷链物流考核试卷
- 光纤激光器的原理与应用考核试卷
- 初等教育中的科学教育与创新考核试卷
- 电气设备安装安装调试考核试卷
- 砍伐工程方案35963
- 《大医精诚》说课(新)
- 牛羊屠宰管理办法
- 《微观经济学》课程思政教学案例(一等奖)
- DBJ50T-232-2016 建设工程监理工作规程
- 国际人力资源管理课程教学大纲
- 深信服园区级双活数据中心
- T-CSCS 016-2021 钢结构制造技术标准
- DB37∕T 5031-2015 SMC玻璃钢检查井应用技术规程
- 回弹强度对应表
- DB32T 3713-2020 高速公路建设工程施工班组管理规范
评论
0/150
提交评论