打造一套客户端功能最全的APM监控系统_第1页
打造一套客户端功能最全的APM监控系统_第2页
打造一套客户端功能最全的APM监控系统_第3页
打造一套客户端功能最全的APM监控系统_第4页
打造一套客户端功能最全的APM监控系统_第5页
已阅读5页,还剩325页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

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.Single­aa①

AB(u①cPD

bufferingImemoryP

2.Double"Buffer

buffering

Video

memory

3.*IHple-Buffer1

buffering

Buffier2

Video

memory

4,Double­Buffer

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. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论