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

六、 电量耗费

移动设备上电量一直是比较灵敏的问题,假如用户在某款 App 的时分发现耗电量严峻、手机发热严峻,那么用户很大或许会p x 7 ~ Y ~ {立刻卸载这款 App。所以需求在开发阶段关心耗电量问题。

一般来说遇到耗电量较大,咱们立马会想到是不是运用了定位、是不是运用了频_ 8 5 w繁网络恳求、是不是不断循环做某件作业?

2 @ l发阶段基本没啥问题,咱们o , x k 4 p A z b能够结合 Instrucments 里的 Energy Log 东西来定位问题。但是线上问题就需求代码去监控耗电量,能够作为 APM 的才干之一。

1. 怎么获取电量

在 iOS 中,IOKit 是一个私有结构,用来获取硬件和设备的详细信息,也是硬件和内核服务通讯的底层结构。所以咱们能够经过 IOKit来获取硬件信息,然后获取到电r ? d量信息。进程如下:

  • 首要在苹果开放源代码 openso^ q I Turce 中找到 IOPo; % ? : J 7werSources.h、IOPSKeys.h。在 Xcode 的 Package Contents 里边找到 IOKit.framework。 途径为 /ApE + Bplications/Xcode.app/Contents/Def 9 { { , [veloper/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/IOKit.$ ! F V y F 9 $ *framework
  • 然后将 IOPowerSources.h、IOPSKeys.h、IOKit.framewd 2 ] K P Oork 导入项目工程
  • 设置 UIDevice 的 batteryMonio f atoringEnabled 为 true
  • 获取到的耗电量精确度为 1%

2. 定位问题

通常咱们经过 Instrucments 里的 Energy Log 处理了许v A ! i #多问题后,App 上线了,线上的耗电e & @ 6 8 8 1量处理就需求运用 APM 来处理了。耗电当地或许是二方库、三方库,也或许是某个同事的代码。

思路是:在检测到耗电后,先找到有问题的线程,然后仓库 dump,复原案发现场。

在上面部分咱们知道了线程信息的结构, thread_basiF y & i ^ w &c_in` q ! | ! 8 7 l yfo 中有个记载 CPU 运用率百分比的字段 cp~ g q ,u_usage。所以咱们能够经过遍历当时线程,判别哪个线程的 CPU 运用率较高,然后找出有问题的线程。然后再 dump 仓库,然后定位到发生耗电量的代码。详细请看 3.2 部分。

- (double)fetchBatteryCostUsage
{$ i ) } 6
// returns a blob of powo C S Ger source information in an opaque CFTypeRef
CFTypeRef blob = IOPSCopyPowerSourcesInfo();
// returns a CFArray of power source hax r , b H H zndles, each of type CFTypeRef
CFAr;  A 8 d orayRef sources = IOPSCopyPowerSourcesList(blob);
CFDictionaryRef pSource = NULL;
constx s 1 void *psValue;
// returns the num@ 6 sber of values currently in an array
int numOfSources = CFArrayGetCount(sources);
// error in CFArrayGetCount
if (nuL R _mOfSources == 0) {
NSLog(@"Error in CFArrayGetCount");
return -1.0f;
}
// calculating the remaininD m /g eneo ` 8 # v N A Orgy
for (int i=0; i<numOfl 0 % $ =Sources; i++) {
// returns a CFD^ ? [ E 7 q 9 iictionam `  xry with readable information about the specific power source
pSource, ~ l N H # i C = IOPSGetPowerSourceDescription(blob, CFArrayGeR p _ ? rtValueAtIndex(sources, i));
if (!pSource) {
NSLog(@"Error in IOPSGetPowerSourceDescription");
return -1.0f;
}
psValue = (CFStri: M q Q x 6 n |ngRef) CFDictm q I 9 - M b A OionaryGetValue(e q R ) f | spSource, CFSTR(kIOPSNameKey));
int curCapacity = 0;
int maxCapacity = 0;
double percentage;
psValue = CFDictionaryGetValue(pSource, CFSTR(kIOPSCurrentCapacityKey));
CFNumberq { MGetValue((CFNumberRef)psValue, kF # c m s ~ qCFNumbe7 c { H | _ . erSInt32Type, &curCapacity);
psValue = CFDictionaryGetValue(pSource, CFSTR(kIOPSMaxCapacityKey));
CFNumberGetValue((CFNumv O S 2 o K I Q fberRef)psValue, kCFNumberSInt32Type, &maxCapacity);
percentage = ((dN & s o Kouble) curCa* ; o ) } b T Z Xpacity / (douq $  x K O p eble) maxCapacity * 100.0f);
NSLog(@"curCapacity : %d / maxCapacity: %d , percentage: %.1f ", curCapacity,Z a = { , maxCapacity, percentage);
r( 4 E Y 8 E veturn percentage;
}
return -1.0f;
}

3. 开发阶段针对电量耗费咱们能做什么

CPU 密集运算是耗O a h ; $ S m电量首要原因。所以咱们对 CPU 的运用需求克勤克俭。尽量防止让 CPU 做无用功。关于很多数据的杂乱运算,能够借助服务器的才干、GPU 的才干。假t h ? U如计划规划有必要S + n V W是在 CPUp h w G Y $ 上完结数据的运算,则能够运用 GCD 技能,运用 dispatch_blocK S x Pk_create_with_qos_cla^ $ w h / _ss(<b e 2 E i;#d, % : c ^ 4 q 9ispatch_block_flags_t flags#>,) * a d W M a dispatch_qos_class_t qos_class, <#int relative_priorN R _ L _ 6 K Mity#>, <#^(void)block#>)() 并指定 队列的 qos 为 QOS_CLASS_UTILITY。将使命提交到这个队列的r ] % block 中,在 QOS_CLASS_UTILITY 形式下,L W 体系针对很多数据的核算,做了E I j电量优化

除了 CPU 很多运算,I/O 操作也是耗电首要原因。业界常见计划都是将「碎片化的数据写入磁盘存储」这个操作拖延,先在内存中聚合吗,然后再进行磁盘存储。碎片化数据先聚合,在内存中进行存储的机制,iOS 供给 NSCache 这个目标。

NSCache 是线程安全的,NSCache 会在] g `达抵达预设的缓存空间的条件时收拾缓存,此时会触发 - (**void**)cache:(NSCache *)cache willEvictObject:(**id**)2 x V n ^ v p e Xobj; 办法回调,在该办法内部对数据进行 I/O 操作! t 8 D T ~,达到将聚合的数据 I/O 拖延的意图。I/O 次数少了,对电量的耗费也就减少了。

NSCache 的运用能够检查 SDWebImage 这个图片加载结构。在图片读取缓存处理时,没直接读取硬盘文件(I/O),而是运用体系的 NSCache。

-! _ P U y { Q ; (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [selfg - f ) B ?.meu c _ 9 # b l CmoryCache objectForKey:key];
}
- (nullableI * ( S UIImage *)imageFromDiskp q W 1CacheForKey:(nullable NSString *)key {
UIImage *diskImage = [self diskM * q } IImageForKey:ke. z 5 F T 9y];
if (diskImage &&G U g { [amp; self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage f` l G Z F G x BorKey:key cost:cost];
}
return diskImage;
}

能够看到首要逻辑是先从磁盘中读取图片,假如配置答应敞开内存缓存,则将图片保存到 NSCache 中,运用的时分也是从 NSCache 中读取图片。NSCache 的 totalCos7 { 3 3 O RtLimit、coun% T P V [ ) D YtLiT 4 5 q ` Tmit 特色,

- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUIntegU $ l 7 ^er)g; 办法用来设置缓存条件。所以咱们写磁盘、内存的文件操作时能够借鉴该战略,以优化耗电量。

七、 Crash 监控

1. 反常相关常识回忆

1.1 Mach 层对反常的处理

Mach 在音讯传递根底上完结了一套独特的反常处理办法。Mach 反常处理在规划时考虑到:

  • 带有一致的语义的单一反常处理设施:Mach 只供给一个反常处理机制用于处理一切类型的反常(包括用户界说的反常、渠道无关的反常以及渠道特定的反常)。) B l M X依据反常类型进行分组,详细的渠道能够界说详细的子类型。
  • 明晰和简练:反常处理的q S @ 9接口依赖于 Mach 已有的具有杰出界说的音讯和端口架构,因此十分高雅(不会影响功率)。这就答应调试器和外部处理程序的拓展-甚至在理论上还支撑拓展依据网络的反常处理。

在 Mach 中,反常是经过内核中的根底设施-音– % ] u J e k ^ 9讯传递机制处理的。一个反常并不比一条音讯杂乱多少,反常由犯错的线程或许使命(经过 msg_send()) 抛出,然后由一个处理程序经过 mg a * [ 2 *sg_recvp Z w _())捕捉。处理程序能够处理反常,也能够清楚反常(将反常标记为已完结并继续)O B m ` ] y AA u 0 ^还能够C _ D t T u S决议终止线程。

Mach 的反常处理模型和其他的反常处理模型不同,其y 9 0 o & t X h他模型的反常处理程序. ^ v运转在犯错的线程上下文中,而 Mach 的反常处理程序在不同的上下文中运转反常处理程序,犯错的线程向预先h E z * _ @指定好的反常端口发送E u # X { 7 C &音讯,然后等候应对。每一个使命都能够注册一个反常处理端口,这个反常处理端口会对该使命中的一切线程收效。此外,每2 c v . $ P个线程都能够经过 thread_set_eL T { R W | K xxception_por/ J H ; g K Ats(<#thread_act_tD . 7 z ! ; ! thread#>, <#exception_mask_t exception_mask#>, <e ] P , ? h J o;#mach_port_t new_port#>, <#exception_behavior_t behavior#>, <#thread_state_flavor_t new_flavor#>) 注册自己的反常处理端口L H 8 m R S W。通常状况下,使命和线程的反常端口都q E y ! k o } 7是 NULL,也便是反常不会被处理,而一旦创立反常端口,这些端口就像体系中的其他端口相同,6 _ 2 :能够转! q C J b交给其他使命或许其他主机。(有了端口,就能够运用 UDP 协7 h q Y d议,经过网络才干让其他的主机上运用程序处理反常)。

发生反常时,首要测, R v 验将反常抛给线程的反常端口,然? v D Z后测验抛给使命的反常端口,最终_ c q再抛给主机的反常端口(即主机注册的默许端h $ $ 1口)。假l f ^ | } M .如没有一个端口回来 KERN_SUCCESS,那么整个使命将被终止。也便是 Mach 不供给反常处理逻辑,只供给传递反常奉告的结构。

反常首要是由处理器圈套引发的。为了处理圈套,每一个现代的内核都会安插圈套处理程序。这些底层函数是由内核的汇编部分安插的。

1.2 BSD 层对E – , ` ? B反常的处理

BSD 层是用户$ W – 6态首要运_ K C V G ]用的 XUN 接口,这一层展现了一个契合 POSIX8 3 | R _ ; Z , 规范的接口。开发者能够运用 UNIX 体系的全部功用,但不需求了解 Mach 层的细节完结。

Mach 现已经过反常机制供给了底层的陷进处理,而 BSD 则在反常机制之上构建了信号处理机制。硬件发生的信号被 Mach 层捕捉,然后转化为对应的 UNIX 信号,为了维护一个一致的机制,操作体系和用户发生的信号V m w H & d 8 F 1首要被转化为 Mach 反常,然后再转化为信号。

Mach 反常都在 host 层被 ux_exception 转化为相应的 unix 信号,并经过 threadsignal 将信号投递到犯错的线程。

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

2. Crash 搜集办法

iOS 体系自带的 Apples`L g A ! *s Crash Reporterx K [ o 在设置中记载 Crash 日志,咱们先观察下 Crash 日志

Incident Identifier: 7FA6736D-09E8-47A1-95EC-76C4522BDE1A
CrashReporter Key:   4e2d36419259fJ B ~ { ( N14413c3229e8b7235bcc74847f3
Hardware Model:      iPhone7,1
Processd ! s %:         CMMonitorExample [3608]
Path:            /var/containers/Bundle/ApplicatioR 3 n/9518A4F4-59B7-44E9-BDDA-9FBEE8CA18y H * s j q & XE5/CMMonitorExample.app/CMMonitorExample
Identifier:      com.Wacai.CMMonitorExam, 1 K g ] i 8 &ple
Version:         1.0 (1)
Code Ty 3 upe:       ARM-64
Parent2 K { Process:  ? [1]
Date/Time:       2017-01-03 1S M # M A u 6 d1:43:03.000 +0800
OS Version:      iOS 10.2 (14C92)
Report Version:  104
Exception Type:  EXC_CW ( - k RRASH (SIGABRT)
Exception Codes: 0x00000000 at 0x0000000000000000
Crashed Thread:  0
Application Specific Information:
*** Terminating app due t E N z Po uncaught exception 'NSInvalidArgumentException', reason: '-[__NSSingleObjec7 & ~ _ QtArrayI objectForKey:x L 3 w r K e]: unrecognized selector sent to instance 0x174015060'
Thread 0 Crashed:
0   CoreFoundation                  0x0000000188f291b8 0x188df9000 + 1245624 (<redacted> + 124)
1   libobjc.A.dylib                 0x000000018796055c 0x187958000 + 34140 (objc_exception_throw + 56)
2   CoreFoun= C Ldation                  0x0000000188f30268 0x188df9000 + 1274472 (<redD ! ] H w ( K 3 +acted> + 140)
3   CoreFoundation                  0x0000000188f2d270 0x188df9000 + 1262192 (<redacted> + 916)
4   CoreFoundation                  0x0000000188e2680c 0x188df9000 + 186380 (_CF_forwarding_prep_0 + 92)
5   CMMonitorExample                0x000000010004c618 0x1000446 i & u u i ^ (000 + 34328 (-[MakeCrashHandX } _ I B /ler t9 I ( , I 1 B ,hro?  ` }wUncaughtNSException] + 80)

会发现,Crash 日志中 Ek S !xception Type 项由2部分组成:Mach 反常 + Unix 信号。

所以 Exception Type: EXC_CRASH (SIGd O 4 eABRT) 表明:Mach 层发生了 EXC_CRASH 反常,在 host 层被转化v 9 ` ! $ [O x m s a : , SIGABRT 信号投递到犯错的线程。

问题: 捕获 Mach 层反常、注册 Unix 信号处理都能够捕获 CrD u w 1 o ( jash,这两种办法怎么挑选?

答: 优选 Mach 层反常阻拦。依据上面 1.2 中H m I 7 y W的描绘咱们知道 Mach 层反常处理机遇更早些,假如 Mach 层反常处理程序让进程退出,这样 Unix 信号永久不会发生了。

业界关于溃散日志的搜集开源N Y V $ ) r项目许多,闻名的有: KSCrash、plcrashreporter,供给一条龙服务的 Bugly、友盟等。咱们一般运用= c W B开源项目在此根底上开发成2 $ z u C契合公司内部需求的 buN B N 8 K (g 搜集东西。一番比照后挑选 KSCrash。为什么挑选 KSCrash 不在本文重点。

KSCra: Z j = a S L jsh 功用完全,能D % h u F t够捕获如下类型的 Crash

  • Mach kernel exceptions
  • Fatal signals
  • C++ exceptions
  • Objective-C exceptions
  • Main th? 2 n |read deadlock (experimental)
  • Custom crashes (e.g.6 $ c g g j [ from scripting languages)

所以剖析 iOS 端的 Crash 搜集计划也便是剖析 KSCras: R 6 Oh 的 Crash 监控完结原理。

2.1. Mach 层反常处理

大体思路是:先创立一个反常处理端口,为该端口申请权限,再设置反常端口、新建一个内核线程,在该线程内循环等候反常。但是为了5 * E K 9 y ,防止自己注册的 Mach 层反常处理抢占了其他 SDK、或许业务线开发者设置的逻辑,咱们需求在最开端q – g v j r 保存其他的反常处理端口,等逻辑履行完后将反常处理交给其他的端口内的逻辑处理。搜集到 Crash 信息后拼装数据,写入 json_ S 3 % N 文件r Z @ X A

流程图如下:

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

关于 Mach 反常捕获,能够注册一个反常端口,该端口负责对当时使O ) l v 4命的一切线程进行监听。

下面来看看关键代码:

注册 Mach 层e g u X [ c反常 M | #监听代码

static bool installExceptionHandler()
{
KSLOG_^ B 0 Q y g U 3 rDEBUG("Installing mach exception handler.");
bool attributes_created = false;
pthread_attr_t attr;
kern_return_t kr;
int error;
// 拿k 5 C w 5到当时进程
const task_t thisTask = mach_task_self();
exception_mask_t mask = EXC_MASK_BAD_ACCESS |
EXC_MASK_BAD_INSTRUCTION |
EXC_MASK_ARITHMETIC |
EXC_MASK_SOFTWARE |
EXCf u  M ~ n o_MASK_BREAKPOINT;
K/ 8 i P E R + SLOG_DEBUG("Backing up original exception ports.");
// 获取该 Task 上的注册好的反常端口
kr = task_get_exception_ports(thisTask,
mask,
g_previousExceptionPorts.masks,
&g/ I R z ; D g $ ,_previousExceptionPorts.count,_ c w
g_previousExceptionPorts.ports,
g_previousExceptionPorts.: W 5be/  5 r m U )haviors,
g_previT t 2 ! CousExceptionPorts.flavors);
// 获取失利走 failed 逻辑
if(kr != KERN_SUCCESS)
{
KSLOG_ERROR("task_get_exception_pors B T gts: %s", mach_error_string(kr));
g) 1 7 a W , f - coto failed;
}
// KSCrash 的反常为空则走履行逻辑
if(g_exceptionPort == MACH_PORT_NULL)
{
KSLOG_DEBUG("Allocat5 N : c ? ) a Ming newY f x port with receive rights.");
// 申请反常处理端口
kr = mach_port_allocate(thisTask,
MACH_PORT6 ) G G_RIGHT_RECEIVE,
&g_exceptionPort);
if(kr != KERN_SUCCESS)
{
KSLOG_ERROR("mach_port_allocate: %s", mach_error_string(kr));
goto failed;
}
KSLOG_DEBUG("Adding send rights h g to port.");
// 为反常处理端口申请权限:MACH_MSG_TYPE_MA; p Z l ` k ~ H AKE_SEND
kr = mach_port_insert_right(thisTask,
g_exceptionPort,
g_exceptionPort,
MACH] ? /_MSG_TYPE_MAKE_SEND);
ifO h Y e(kz B W M @ Br != KERN_SUCCESS)
{k O ! O ] F s $ V
KSLOG_ERROR("mach_port{ ( G Y ) H J g #_insert_r1 n Q k qight: %s", mach_error_string(kr));
goto failed;
}
}
KSLOG_DEBUG("Installing port as exception handler.");
// 为该 Task 设置反常处理端口
kr = task_set_exception_ports(thisTask,
mask,
g_exceptionPort,
EG D @XCEPTION_DEFA} I o SULT,
THREAD_STATE_NONE);
if(kr != KERN_SUCCES@ L N xS)
{
KSLOG_ERROR("task_set_exception_ports: %s", mach_error_string(kr));
goto failed;
}
KSLOG_DEBUG("Creating secondary exception` I N M W ? ? x P thread (suspended).");
pthread_attr_init(&attr);
attributes_created = true? - % T F;
pthread_attr_setdetachw ! : Z istate(&attr, PTHREAD_6 S M [CREATE_DETACHED);
// 设置监控线程7 E % X O
error = pthread_create(&g_secondaryPT; R j $ / hread,
&attr,
&handleExceptions,
kThreadSecondary);
if(err- 2 ` N A D dor != 0)
{
KSLOG_ERROR("pthread_create_suspex : w r V 0nded_np: %s", strerror(error));
goto failed;
}
// 转化为 Mach 内核线程
g_secondaryMachThread = pthread_macy 1 s v ]h_thread_np(g_secondaryPThread);
ksmcW S 9 2 i b Z @_addReservedThread(g_secoC ^ = e q NndaryMac9 ! r A w L E :h* N p &  }Thr. M /ead);
KSLOG_DEBUG("Creating primary ex% A q ] 9cP q { z 0 w ) 1 +eption threj = Z w _ 4 ` Pad.");
error = pthread_create(&g_primaryPThread,
&attr,
&handleExceptions,
kThreadPrimary);
if(error != 0)
{
KSLOG_ERROR(v E K ! 1 M"pthread_create: %s", strerror(error));
goto failed;
}
pthreadb 8 I 8 ( 9 W_attr_dO w $ I 3 Iestroy(&attr);
g_primaryMachThread = pthread_mach_thread_np(g_primaryPThread);
ksmc_a$ J W g Q X E }ddq 5 [ReservedTQ S 5 0 ^ & X ` ~hread(g_primary5 6 2 W 9 7 2 8 ZMachThread);
KSLOG_DEBUG(n 5 9 k ~ Y d C"Mach exceptioM Q {n handler installed.");
return true;
failed:
KSLOG_DEBUGx V z 1("Failed to install mach exception handler.");
if(attribut9 W o j X ) es_created)
{
pthread_attr_destroy(&attr);
}
// 复原之前的反常注册f  $端口,将控制权复原
uninstallExceptionHandler();
return false;
}

处理反常的逻辑、拼装溃散信息

/** Our exception handler thread routine.
* Wait for an exception me$ c Wssage, uninstall our exception port, record the
* exception inform P h ( J S J 0 }ation, and write a report.
*/
static void* handleExceptions(void* const userData)
{
MachExceptionMessageQ , 3 @ exceptionMessage = {{0}};
MachReplyMessage replyMessage = {{0}};
char*t o X A : ~ eventID = g_primaryEventID;
const char* threadName = (coy 0 { D ] a k | Rnst char*) userJ [ C | f h r  ZData;
pthread_setname_np(threadName);
if(] a GthreadName == kThreadSecondary)
{
KSLOG_DEBUG("This is the secondary thread. Suspending.");
th& = ~ a w r + Lreads O M U b W O I {_suspend((thread_; c  Ct)ksthread_self());
eventID = g_secondaryEventID;
}
// 循环读取注册好的反常端口信息
for(;;)
{
KSLOG_DEBUG("Waiting for mach exception");
// Wait for a mess. + `   y ( xage.
kern_return_t kr = machG } f t ( A_msg(&exceptionMessage.header,
MACH_RCV_MSG,
0,
sizeof(exceptionMessage),
g_exceptionPort,
MACH_MSG_TIMEOUT_NOY H L z ` y JNE,
MACH_PORT_NULL);
// 获取到信息后则代表发生了 Mach 层反常,跳出 for 循环,拼装数据
if(kr == KERN_SUCCESS)
{
break;
}
// Loop and try again on failur] L Z u p re.
KSLOG_ERRORl G H K L S &("mach_msg: %s", mach_error_striD b # e k W 8 f Kng(kr));
}
KSLOG_DEBUG("Trapped mac2 - b ` u  Kh exception code 0x%x, subco~ Y g - a Qde 0x%x",
exceptiony p } = y 3Message.code[0], exceptionMessage.code[1]);
if(g_isEnabled)
{
// 挂起一切线程
ksmc_so Q 5 L H n j J :uspendEnvironment();
g_isHandlingCrash = true;
// 奉告发生r E f V i f了反常
k5 ? J O : D x Wscm_notifyFatalExceptionCaptu5 . 7 .red(tr% d ? {ue);
KSLOG_DEBUG(% A ) N D"Exception han6 - jdler is installed. Continuing exception handling.");
// Switch to the secondary thread if necessary, or uninstall the handler
// to avoid a death loop.
if(ksthread_self() == g_H S ` V q l 8 d PprimaryMachThread)
{
KSLOG_DEBUG("This is the primary exception t/ * J i l 9hread. Activating secondary tM t p D ) g A / (hread.");
// TODO: This was put here to avoid a freeze. Does secondary thread ever fire?
restoreExceptionPorts();
if(thread_resume(g_secondaryMachThread) != KERN_SUCCEk b - p n Q z j DSS)/ F a g ^ X 5 q
{
KSG [ i o f * 4 5 vLOG_DEBUG("Could noY [ et activate secondary thread. Restoring original exception? $ ( poB  d c f { N ~rts.");
}
}
else
{
KSLOx o f Y E V cG_DEBUG("This is the secondary exception thread. Restoru O x ` Zing original exception ports.");
//            restoreExceptionPorts();
}
// FilT 8 Z dl ouF N M { 5 E r D }t crash informatio$ J * _ 4n
// 拼装反常所需求的计划现场信息
KSLOG_DEBUG("Fetching machine state.");
KSMC_NEW_C6 d } A J u vONTEXT(machineContext);
KSCrash_MonitorContext*[ 5 R % ! i crashContL I wext = &g_monitor6 Q . y y @ r zContext;
crashContext->offendingMachineContext = machineContext;
kssj t a q M 2 .c_initCursor(&g_stackCursor, NULL, NULL);
if( Q i E z V Oksmc_getContextForThread(exceptionMessage.thread.name, machineContext, true))
{
ksS b 3 6 C 7sc_ini7 7 + f U ^tb n D e pWithMachineContext(&g_stacO K Y } B I z #kCursor, 100, mac+ [  @ b QhineContext);
KSLOG_TRACE("Fault address 0x%x, instruction address 0x%xh f I L", kscpu_faultAddress(machineContF 0 s | Lext), kscpu_instructionAddress(machi/ [ j p % A 8 YneContext));
if(exceptionMessage.exception == EXC_BAD_b d LACCESS)
{
crashContext->faultAddress = kscp: N K V , 6 ) {u_faultAddress(machineContext);
}
else
{
crashContext->faultAddress = kscpu_insg ^ & 4 w x MtructionAddress(machineContext);
}
}
KSLOG} m O K_DEBUG("Filling out cm M d A d _ 1 Gonteq B r 4 + d Z zxt.");
crashContext->crashType = KSCk 8 SrashMonitorTypeMachException;
cc I s [ M j G ( krashContext->eventID = eventID;
crashContext->registe{ % n C , : V zrsAreValid = true;
crash} e p U 1 R &Context->mach.type = exceptionMessage.exception;
crashContext-&C - } m L - 9 i zgt;mach.code = exceptionMessage.code[0];
crashContext->mach.subcode = exceptionMessage.code[1];
if(crashContext->mach.code == KERN_PRO] c B l 9 i _TECTION_FAILURE &&6 x iamp; crashContw ] + b o q xext->isStackOverflowk n z G d z - b)
{
// A stack overflow should return KERq A r K w J o & ;N_INVALID_ADDRESS, but
// when a stack blasts through the8 1 X 0 guard pages at the top of the stM 6 S | @ g H 7ack,
// it gen7 ( ( ^ / l - /erati ^ @ I 4 & * res KERN_PROTECTION_FAILURE. Correct for this.
crashContext->mach.code = KERN_INVALID_ADDRESS;
}
crashContext->signal.signum = signalF) a I XorMachException(cr ~ a q Z : F CashContI 7 0 Dext->mach.type, crashContw f Jext->mad o F Y | z Dch.code);
crashContext->$ q 5 ] d ` g;stackCursor = &g_stackCursor;
kscm_handleExcepta V I q G Tio% S v  4 M 9 @ on(crashContext);
KSLOG_DEBUG("Crash^ 6 D  handling complete. Restoring original handlers.");e K 0 g g p V 2
g_isHandlingCrash = false;N * p
ksmc_resumeEnvironment();
}
KSLOG_DEBUG("Replying to mach exception message.");
// Send a reply sayB ) a f &ing "I didn't handle this exception".
replyMessage.header = exceptionMessage.header;
replyMessage.NDR = exceptionMessage.NDR;
replyMessage.returnCode = KERN_FAILURE;
mach_msg(&replyMessage.header,
MACH_SEND_MSG,
sizeof(replyMessage),
0,
MA5 F l 6 r ( 9CH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
return NULL;
}

复原反常处理端口,转移控制权

/** Restore the original mach exception ports.
*/
static void resto$ w # v O { / +reExcept4 q K . N 6 ? rionPorts(void)
{
KSLOG_DEBUG("Restoring original exception ports.");
if(g_previousExceptionPorts.count == 0)
{
KSLOG_DEBUG("Original exception ports were already restored.");
retu4 K k v |rn;
}
const task_p q / M +t thisTask = mach_task_self();
kern_return_t kr;
// Rein_ / ` 7 Q estall olc  Fd exception ports.q @ =
// for 循环去除保存好的在 KSCr; Q ) A ( B ) + ~ash 之前注册好的反常端口,将每个端口注册回去
for(mach_msg_type_numb. W P d m C ter_t i = 0; i < g_previousExceptionPorts.count; i++)
{{ j s
KSLOG_TRACE("Restoring port index %d", i)B A S . 5;
kr = task_set_exception_po] w V p # d O ;rts(thisTask,
g_previous~ - A # x % ^ExceptionPorts.masks[i],
g_previousEY ) C 9 D $xceptionPorts.ports[i],
g_previousExceptionPorts.behaviors[i],
g_previousExcV # H | I L -eptionPorts.flavors[i]);
if(kr != KERN_SI R c l S $ UCCESS)
{
KSLOG_ERROR("task_set_exception_ports: %s",
mach_error_string(kr))d s N s m T x;
}
}
KSLOG_DEBUG("Exception ports restored.");
g_previousExceptionPorts.count = 0;
}

2.2. Signal 反常处理

关于 Mach 反常,操作体系会将其转化为对应的 Unix 信号,所以开发者能够经过注册 signanHandler 的办法来处理。

KSCrash 在这儿的处理逻辑如下图:

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

看一下关键代码:

设置信号处理函数

static bool installSi_ % - O V h tgnalHandler()
{
KSLOG_DEB* ! E ; C 6 4 K 1UG("Installinga ~ 5 S S ) signal handler.");
#if KSCRASH_HAS_SIGNAL_S[ u # a & T i g ~TACK
// 在堆上分配一块8 J 7 C l u : n内存,
if(g_signalStackp w % l p . ( = T.ss_size == 0)
{
KSLOG_DEBUG("Allocating signal stack area.");
g_signalStack.ss_size = SIGSTKSZ;
g_signalStack.ss_sp = malloc(g_signalStack.ss_siz4 O K R ( ~ D . He);
}
// 信号处理函数的栈挪到堆中,而不和进程共用一块栈区
// sigaltstack() 函数,该函数的第9 1 M 4 H ( K 1 个参数 sigstack 是一个 stack_t 结构的指针,该结构存储了一个“可替换信号栈u 0 ( v ! 5 C H” 的方位及特色信息。第 2 个参数 old_sN p g n kigstack 也是一个 stack_tm [ e 8 ~  3 g 类型指针,它用来回来上一次树立的“可替换信号栈”的信息(假如有的话)
KSLOG_DEBUG("Setting signal stack area.");
// sigaltstack 第一个参数为创立的新的可替换信号栈,1 0 7第二个参数能够设置为NULL,假如不为NULL的话,将会将旧的可替换信号栈的信息保存在里边。函数成功回来0,失利回来-1.
if(sigaltstack(&g_signalStack, NULL) != 0)
{
KSLOG_ERROR("signalstack: %s", strerror(eG Y r q [ [ 8 7 (rrno));
goto failed;
}
#endif
const int* fatalSignals = kssignal_fatalSignals();
int fatalSignalsCount = kssignal_numFatalSignals();
if(g_previousSignalHt q E kandlers == NULL)
{
KSLOG_DEBUG("Allocating memory to store prem V ]vious signal handlers.");
g_previousSignalHandlers = me q L r ? N ! ]alloc(sizeo0 % X z 8 K =f(*g_previousSignalHandlers)
* (unsigned)fatalSignalsCount);
}
// 设置信号处理函数 sigaction 的第二个参数,类型为 sigaction 结构体
struct sigaction action = {{0} d L F -};
// s[ _ 4 Wa_flags 成员树立 SA_ONSTACK 标志,该标志奉告内L z y ` ]核信号处理函数的栈帧就在“可替换信号栈”上树立。
action.sa+ Y T 2 8_flags = SA_SIGINFO | SA_ONSTACK;
#if KSCRASH_HOST_APPLE && defined(__LP64__)
action.sa_flags |= SAB j ~ h | r_64REGSET;
#endif
sigemptyset(&action.sa_mas9 G 4k);
action.sa_sigaction = &handleSignal;
// 遍历需求处理的信号数组
for(int i = 0; i < fatalSignalsCount; i++)
{
// 将每个信号的处理$ & ` 7 b Z函数绑定到上面声明的 action 去,另外用 g_previousSignalHandlers 保存当时信号的处理函数
Ks l oSLOG_DEBUG("A( - - 5 4ssigning handler for signal %d", fa9 O ? u C b | &talSignals[i]);
if(sigaction(f% % r 7 -atalSignals[i], &action, &g_previousSignalHandlers[i6 K ^ : ` K 7 H 2]) != 0)
{
char sigNameBuff[30];
conr z Fst cl  ^har* sigNa~ H y G N cme = kssignal_signalName(fatalSign& 1 ~ Fals[i]);
if(sigName == NULL)
{
snprintf(sigNameBuff, sizeof(sigNameBuff), "%d", faL { J j OtalSignals[i]);
sigNaq 2 * l ~me = sigNameBuff;
}
KSLOG_ERROR("sigaction (%s): %s", sigName, strerror(errno));
// Try to reverse the damage
for(i--P e . V;i >= 0; i--)
{
sigaf _ : @ o ZctionV g v e ^ ^ F(fatalSignals[i], &g_previousSignalHandlers[i], NULL/ F H L);
}
goto failed;
}
}
KSLC  eOG_DEBUG("Signal handlers installed.");
return true;n z V B 9 X 4 N {
failed:
KSLOG_DEBUG("Failed to install signal handlers.");
return false;
}

信号处理时记载线程b 4 # v ? .等上下文信息

static void handleSignal(int sigNum, siginfo_R m j ut* signalInfo& _ j d, void* userContext)
{
KSLOG_DEBUG("Trapped signal %d", sigNum);
if(g_isEnabled)
{
ksmc_suspendEnvironment(s c 1 M  | J);
kscm_notifyFatalExceptionCaptured(false);
KSLOG_Dz g l K a L 9 y wEBUG("Filling out context.");
KSMC_NEW_CONTEXT(machineContext);
ksmc_getContextForSignal(userConte4 V y | # 4 7 a Oxt, machineContext);
kssc_initWithMachineContext(&g_stackCursor, 100, machineContext);
// 记载信号处理时的上下文信息m } 9 = ? P
KSCrash_MonitorConteM w r A E b & U jxt* crashContext = &g_monitorContext;
memset(crashContext, 0, sizeof(*crashContext));
crashContext-&q d u ~ &gt;crashType =L N O e ( O KSCrashMonitorTypeSignal;
crashContext->eventID =L c y G E g_eventID;
crashContext->offendingMachineContext = machineContext;
crashContext->regis1 d 2 g p t StersAreValid = true;
crashContext->faultAddress = (uintptr_t)sig8 K I K = A N vnalInS o B Xfo->si_addr;
crashContext->signal.userContext = userContext;
crashCo} 0 g z L _ dntext->signal.signuH Y } #m =. q X t ) signalInfo->si  9 &i_signo;
crashContext->si0 t x  o }gnal.sigcode = signalInfo->V W ) K s w 1 z !si_code;
craB  o 8 m NshContext->stackCursor = &g_stackCursor;
kscm_handleExcF G X b L ! Xeption(crashCont5 A 6 I H *ext);
ksmc_resumeEnvironment();
}
KSLOG_DEBUG("Re-raising signal for regular handlers to catch.");
// This is technically not allowed, butW I 1 it works in OSX aV B q * 4 r w hnd iOS.
raise(sigNum);
}

KSCrash 信号处理后复原之前的信号处理权限

static void uninstallSignalHandler(void)
{
KSLOG_DEBUG("Uninstalling signal handlers.");
const int* fatalSignals = kssignal_fatalSignals();
int fatalSignalsCx 4 v ] ? : }ount = kssignal_numFatalSignals();
// 遍历需求处理信号数组,将之前的信号处理函数复原
for(int i = 0; i < fatalSignalsC{ P g fount; i++)
{
KSLOG_DEBUG("Restoring original handler for signal %d", fatalSignals[i]);
sigaction(fatalSignals[i], &g_previousSignalHandlers[i], NULL);
}
KSLOG_DEBUG("Signal handlers uninstalledd q Y 6 X.");
}

阐明:

  1. 先从堆上分配一块内存区域,被称为“可替换信号栈”,意图是将信号处理函数的栈干掉,用堆上的内存区域替代,而不和进程共用一块栈区。

    为什么这么做?一个进程或许有 n 个线程,每个线程都有自己的使命,假如某个线程履行犯错,这样就) 2 + : 6 h| q – n S y导致整个进程的溃散。所以为了信号处理函数正常运转,需求为信号处理函数设置独自的运转空间。另一种状况是递归函数将体系默许的栈空间用尽了,但是信号处理函数运用的栈e 3 @是它完结在堆中分配的空间,而不是体系默许的( P w V + W b d栈,所以它仍旧能够正常作业。

  2. inv J H v } Ot sigaltstack(const stack_t * __restrict, stack_t * __restrict)7 s [ j _ %数的二个参数都是 sta4 D d 0 ? d 7 0ck_t 结构的指针,存储了可替换信号栈的信息(栈的开端地址、栈的长度、状况)。第1个参数该结构存储了一个X K = r ( . N o“可替换信号F V I 1 ` Z 8 O栈” 的方位; m f [ P g A M及特色信息。第 2c ] _ I G p P B 个参数用来B _ . i : + ~回来上一次树立的“可替换信号栈”的信息(假如有的话)。

    _STRUC: a g } pT_SIGAW ~ S 5 x D / {LTSTACK
    {
    vo6 F = { T K /id            *ss_sp;         /* signal stack base */
    __darwin_size_t ss_size;        /* signal stack length */
    int             ss_flags;       /* SA_DISABLE and/o; ( = K h Pr SA_ONSTACK */
    };
    tN x T _ |ypedef _STRUa t L 7 R ` } _ 8CT_ 0 | N ] @SIGALTSTACK     stack_t; /* [??- Y Q?] signal stack */
    

    新创立的可替换信号栈,ss_flags 有必要设置为 0。体系界说了 SIGSTKSZ; / $ C量,可满意绝大多可替换信号栈的需求。

    /*
    * Structure used in sigaltstack call.
    */
    #define SS_ONSTACK      0x0001  /* take signal on signal stack */
    #define SS_DISABLE      0x0004  /* disable taking signals on a& F ) | R d D r )lternate stack */
    #define MINSIGSTKSZ     32768   /* (32K)mq ;  ] C `inimum allowable stack */
    #defi? ` 0 & #ne SIGSTKSZ        131072  /* (128K)recommended stack s: e j + ! eize */
    

    sigaltstack 体系调用奉告内核“可替换信号栈”现已树立。

    ss_flagsSSG k & . Z_ONSTACKa { z s L h P 时,表明进程当时正在“可替换信号栈”中履行,假如此时a X } O 9 企图去树立一个新的“可替换信号栈”,那么会遇到 EPERM (制止该动作)S – C _ / u 的过错;为 SS_DISABLE 阐明当时没有已树立的“可替换信号栈”,制止树立“可替换信号栈”。

  3. int sigaction(int, const struct sigaction * __restrict, struct sigaction * __restrict[ N 2);

    第一个函数表明需求处理的信号值,但不能是 SIGKILLSIGSTOP ,这两个信号的处理函数不答运用户重写,由于它们给超级用户d B W u # ` @供给了终止程序的办法( SIGKILL and SIG9 J + u T 1 U _ ,STOP cannot be caught, blocked, or ignored);

    第二个和第三个参数是一个 sigaction 结构体。假如第二个参数不为空则代表将其指向信号处理函数,第三个参数不为空,则将之前的信号处理函数保存到该指针中。假如第二个参数为空,第三个参数不为空,则能够获取当时的信号处理函数。

    /Y G 1 ~ 5 : $ % j*
    * SigL v ~ ! r H unal vector "template" used in sigaction call.
    */
    struct  sigaction {
    union __sig% ; % - R H k h 3action_u __sigaction_u;  /* signal handler */
    sigset_t sa_mask;               /* signal mask to apply */
    int     sa_flags;               /* seeS C W , | 0 9 signal options below */
    };
    

    sigaction 函数的 sa_flags 参数需求设置 SA_ONSTACK 标志,奉告内核信号处理函数的栈帧就在“可替换信号栈”上树立。

2.3. C++ 反常处理

c++ 反常处理的完结是依靠了规范A T g | m库的 std::see d w _ D / x Ut_term} 0 C u K ( { ,inate(CPPExceptionTerminate) 函数。

iOS 工程中某些功用的完结或许运用了C、C++等。假如抛出 C++ 反常,假如该反常能够被转化为 NSException,则走 OC 反常捕获机制,假如不能e d , o转化,则继续走 C++ 反常流程,也便是 default_t) j 7 5 %erminate_han( D b jdler。这个; O / g = C++ 反常的默许 ter = = 7minate 函数内部调t S ]abort_message 函数,最终触发p b S 8 + h @了一个 abort 调用,体系发生一个 SIGABRT 信号。

在体4 g $ C 6系抛出 C++ 反常后,加一层 try...catch... 来判别该反常是否能够转化为 NSExceptiI g h R E K ] + Pon,再从头抛出的C++反常。此时反常的现场仓库现已消失,所以# n 4 ! J上层经过捕获 SIGABRT 信号是无法复原发生反常时的场景,即反常仓库缺失。

为什么?tr- R C e :y...catch... 句子内部会调用 __cxa_rethrow() 抛出反常,__cxa_rethrow() 内部又? % i w X V # 9会调用 unwind ` L t bunwind 能够简略了解为函数调用的逆调用,首要用来收拾函数调用进程中每个函数生成的部分变量,一直到最外层的 catch 句子地点的函数,并把控制移交给 catchG } r ; U W y ! k 句子,这便是C++反常的仓库消失原因。

static void setEnabled(bool isEnabled)
{
if(isEnabled != g_isEnabled)
{
g_isEnabled = isEnabled;
if(isEnabled)
{
initialize();
ksid_generaG j J Ite(g_eventID);n B _ o P 5 ? P 9
g_originalTerminateHandler = std::set_termin! ] ` E s ,ate(CPPExceptionTerminate);
}
else
{
std::set_terminate(g_originalTerminateHandle6 n 5 ) Y Jr);
}
g_captureNextStackTrace = isEnabled;
}
}
static void inH ] )itv # i , m ; 1 } Qialize()
{
static bool isInitL 0 . i L o R C Yialized = false;
if(!isInitializedL  & ; t ()
{
isInitialized = true;
kssc_initCursor(H 9 # - K&g_stackCursor, NULL, NULL);
}
}
void kssc_initCursor(KSStackCursor *cursor,
void (*resetCursor)(KSStackCursor*),
bool (*advanceCursorX / K H /)(KSStackCursor*))
{
cursor->symbolicate = kssymbolicator_symz x @ z R bolicate;
cursor->advanceCursor = advanceCursor != NULL ? advanceCursor : g_advanceCursor;
cursor->resetCursor = resetCursor != NULL ? resetCursor : kssc_resetCursor;
cursor->resetCursory l p E .  v(cursor);
}
static void CPP0 ^ [ExceptionTerminate(void)
{
ksmc_suspendEnvironment();
KSLOG_DEBUG("Trapped c++ exception");
const char* name = NULL;
std::type_ b e w Q i ? T_info* tif I w e y  N k .nfo = __cxxabiv1::__cxa_current_exception_type();
if(tinfo != NULL)
{
name = tinfo->name();
}
if(name == NULL || strcmp(name, "NSExceptioG + ! F 2n") != 0)
{
kscm_notifyFatalExceptionCaptured(false);
KSCrash_MonitorContext* craK |  N 8 a Y JshContext = &g_monitorContext;
memset(crashContext,w $ D 4 5 0, sizeof(*crn [ U )ashContext));
char4 + I Q M H P z k descriptionBuff[DESCRIPTION_BUFFER_LENGTH]# & !;
const cl l *har* description = descriptionBuff;
descriptionBuff[0] = 0;
KSLOG_DEBUG("Discovering what kind of exceptioj s # Z j A ` n was thrown.");
g_captureNextStackTrace = false;
try
{
th) z S a erow;
}
catch(std::exception& exc)
{
strncpy(descriptionBuff, exc.what(K y }  ; w ; | i), sizeof(descriptionBuff));
}
#define CATCH_VA` j +LUE(TYPE, PRINTFTYPE) 
catch(TYPE value)
{ 
snprintf(descriptiony m c $ g (Buff, sizeof(description Q c w T  l o FBuff), "%" #PRINTL U X 4 q g ! K JFTYPE, value); 
}
CATCH_VALUE(char,                 d)
CATCH_VALUE(short,                d)
CATCH_VALUE(int,                  dn l U 7  x)
CATCH_VALUE(loC + $ ; ; 0ng,                ld)
CATCH_VALUE(long long,          lld)
CATCH_VALUE(u[ | 3nsigned char,        u)
CATCH_VALUE(unsigned short,       u)
CATCH_VALUE(unsiH X ! h h U =gned int,         u)
CATCH_VALUE(unsigned long,       lu)
CATCH_VALUE(unsigned long long, llu)
CATCH_VALUE(float,                f)
CATCH_VALUE(dk ? } M y souble,               f)
CATCH_VALUE(long double,         Lf)
CATCH_VALUE(char*,                s)
catch(L S V 0 3...)
{
description = NULL;
}
g_caR , 1  L , K 7ptureNextStackTrace = g_isEnabled;
// TODO: Should this be done here? Maybe better in the exception handler?
KSMC_NEW_CONTEXT(machineContext);
ksmc_getContextFo) b Z C % m HrThreag d 5 Rd(ksthread_self| i & R ](), mach& ` K o 6 R } 2 wineContext, truey @ ^ %  R);
KSLOG_DEBUG("Filling out context.");
crashCoK % a J M Dntext->crO M ! aashType = KSCrashMonitorTypeCPPExceT V N 4 U + 5ption;
crashContext->eventID = g_eventIDQ Z ! } 0 -;
crashContext->registersAreValid = false;
crashContext->stackCursor = &g_stackCurs8 5 S f D bor;
crashContext->CPPException.name = name;
crashContext->exceptionName = name;
crao 9 S J u _ 2shContext->crashReason = description;
crashContext-e T U =>offendingMachineContext = mach q ! {hh j W & 5 8 e LineContext;
kscm_handleException(crash; V R u ZContext);
}
else
{
KSLOG_DEBUG("Detected NSException. Letting the current NSException handler deal with it.");
}
ksmc_resumeEnvironment();
KSLOG_DEBUG("CalJ S *ling original terminate handler.");
g_original& h J T - 5 P ! aTerminateHandler();
}

2.4. Objective-C 反常处理

关于 OC 层面的 NSException 反常处理较为简略,能够经过注册 NSUncaughtExcep/ a Z # , 1tionHandler 来捕获反常信息,经过 NSException 参数来做 Crash 信息的搜集,交给数据上报组件。

statid [ :c void setEnabled(bool isEnabled)
{
if(isEnabled != g_isEnabled)
{
g_isEnabled = isEnabled;
if(isEnabled)
{
KSLOG_DEBUG(@"Backing up original8 A ^ y ` T 7 K handler.");
// 记载之前的 OC 反常处理函数
g_previousUn? I Q pcaughtExch x . )eptionHandler = NSGetUnc4 J $ x z . Lau= $ # 7ghtExceptionHandler();
KSLOG_DEBUG(@"Setting new handler."( I Z);
// 设置新的 OC 反常处理函数
NSSetUncaughtExceptionHandler(&handleException);
KSCrash.sharedInstance.uncaughtExceptionHandler = &handleExceptiE @ n O z T l ?on;
}
else
{
KSLOG_DEBUG(@"Restoring original handler.");
NSSetUncaughtExceptionHandler(g_previousUncaughtExceptionHandler);
}
}
}

2.5. 主线程死锁

主线程, z = U D C g死锁的检测和 ANR 的检测有些类似

  • 创立一个线程,在线程{ H P G运转办法中用 do...while... 循环处理逻辑,加了 autorelease 防止内存过高

  • 有一个 awaitingResponse 特色和 watchdogPulse 办法。watchdogPulse 首要逻辑为设置S J I H b awaitingResponse 为 YES,切换到主线程中,设置 awaitingResponse 为 NO,

    - (voidv 5 + w) watchdogPulse
    {
    __block id blockSelf = self;
    self.awaitingResponse = YES;
    dispatch_async(dispatch_get_main_queue(), ^
    {
    [blockSelf watchdogAnswer];
    });
    }
    
  • 线程的履行办法里边不断循环,等候设置的 g_watchdogInterval 后判别 awaitingResponse 的特色值是不是初始状] Y f H况的值,否则判别为死锁

    - (void) r) s U H |unMonitor
    {
    BOOL cancelled = NO;
    do
    {
    // Only do a watchdog check if the watchdog interval is &gK Z K h Zt; 0.
    // If the interval is <= 0, just idle until the usA Y Ser changes it.
    @autoreleasepool {
    NSTimeInterval sleepInterval = g_watchdogInterval;
    BOOL runWatchdogCheck = sleepInterval > 0;
    if(!runWatchd= U Y D p AogCheck)
    {
    sleep: M Q , =Interval = kIdleInterval;
    }
    [NSThread sleepForTimeInterval:sleepInterval];
    cancelled = sel_ z  J k }f.monito, : A N e a i ?rThread.isCancelled;
    if(!cancelled &&a* M ? 8 F } ? H Ump; runWA g 4 atchdogCheck)
    {
    io w e Q d ; # C xf(self.awaitingResponse)
    {
    [self handleDeadlock];
    }
    else
    {
    [self watchdogPulse];
    }
    }
    }
    } while (!cancelled);
    }
    

2.6? W a | , ! Crash 的生成与保存

2[ e Y K L % ?.6.1 Crash 日志的生成逻辑

上面的部分讲过了 iOS 运用开发中的各种 crash 监控逻辑,接下来就应该剖析下 crash 捕获后# q ~ QW G ( K #么将 crash 信息记载下来C ) % L u ; Y 7,也便是保存到运用沙盒中。

拿主线程死锁这种 crash 举比方,看看 KSCrash 是怎么记载 crash 信息的。

// KSCrashMonitor_Deadlock.m
- (void) handleDeadlock
{
ksmc_suspendEnvironment();
kscm_notifyFatalExceptionCaptured(false);
KSMC_NEW_CONTEXT(machineContext);
ksmc_getContext] J *ForThread(g_mainQueue b $ G b IThread, mz O h , 3 ! 6achineContext, false);
KN h b s ` _ {SStackCursor stackCursor;
kssc_initWithMachineContext(&stackCursor, 100, machineContext);
char eventID[37];
ksid_generate(evem N _ XntID);
KSLOG_DEBUG(@"Filling out context.");
KSCrash_MonitorContext* crashContext = &i ( U o n Q;g_monito! Q g - X . 7 ErContext;
memset(crashContext, 0, sizeof(*craB F ] = 5 U a ` PshContext));
cra. c e K @ n u +shContext->crashType = KSCrashMonitorTypeMainThreadDeadlock;
crashContext->e6 e * M SventID = eventID;
cras] 8 { I 9 s F a ghConteB C ` h  J { Z vxt->registersAreValid = false;
crashContext->offendingMachineContext = machineContexd O Lt;
crashContext->stackCursor = &stackCursor;
kscmH I 8 ! + 8_handleException(crashCon? % # m E 0 L otext);
ksmc_resumeEnvironmeno H v 6 at();
KSLOG_DEBUG(@"Calling abort()");
aborg  T Y f f J 6 /t();
}

其他几个 crash 也是相同,反常信息经过包装交给 kscm_handleExceptioh , * $ an() 函数处理。能够看到这个函数被其他几种 crash3 v N 捕获后所调用。

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

/** Start general exception processing.
*
* @oaram context Contextual information about the exception.
*/
void kscm_handleExy 2 } ` d &ception(struct KSCrash_MoniE G X WtorContext* context)
{
context-&1 ^ 3 - # C w O *gt;requiresAsyncSafeI ! & ( i m q Xty = g_requiresAsyncSafety;
if(g_crashedDuringExceptionHandling)
{
context->crashedDuringCrashHandling = true;
}
for(int i = 0; i < g_monitorsCount; i++)
{
Monitor| a @ ( ] _ M W* monitor = &af ! Y C d ( 1mp;g_monitors[i];
// 判别r T C * [当时的 crash 监控是敞开状况
if(isMonitorEnabled(monitor))
{
// 针对每种 crash 类型做一些额外的补充信息
addContextualInfoToEvent(monitor, contc t R W }ext);
}
}
// 真正处理 crashQ f w : ^ R y A ( 信息,保存 json 格局的 crash 信息
g_onExceptionEvent(context);
if(g_handlingFatalExceptionf ~ S && !g_crashedDuringExceptionHandling)
{
KSLOG_DEBUG("Exception is fatal. Restoring original handlers.");
kscm_setActiveMonitors(KSCrashMonitorTypeNone);
}
}

g_onExceptionEvent 是一个 block,声明为 static vw [ 0 b $ %oid (*g_onExceptionEvent)(struct KSCrX o 0ash_MonitorContext* monitorContext);KSCrashMonitor.c 中被赋值

void kscm_setEvent* . J H } (Callback(void (*onEvent)(sU 4 ~ d D 8 #truct KSCr1 9 ~ G 8ash_MonitorContext9 4 #* monitorContext))
{
g_onExceptionEvent = onEvent;
}

kscm_setEventCallback() 函数在 KSCrashC.c 文件中被调用

KSCrashMonQ + v i 3itorType kscrash_install(const char* appNa? B $ q 6 ^ cme, const c^ P ! phar* coT q ; S | Dnst installPath)
{
KSLOG_DEBUG("Installing crash repors a h y r !ter.");
if(g_installed)
{
KSLOG_DEBU3  @ t 0 k 3 1 FG("Crash reporter already installed.");
return g_monitoring;
}
g_installed = 1;
char path[KSFU_MAX_PATH_LENGTH];
snprintf(path, sizeof(path), "%s/Reports", installPath);
ksfu_makePath(path);
kscrs_initialize(appd 0 X ?Name, path);
snprintf(path, sizeof(path), "%s/Data", inst# / ( |allPath);
ksfu_makePath(pE 7 J )ath);
snprintf(path, sizeof(path), "%s% 7 ~ 0 0 w 7 p C/Data/CrashState.json", installPath);
kscrashstate_initialz 9 M [ nize(path);
snprintf(g_consoleLogPath, sizeof(g_consoleLogPath), "%s/Data/Co[ 6 Q F p ;nsoleLog.txt", installPath);
if(g1 4 E E_shouldPrintPreviousLog)
{
printPreviousLog(g_consol2 q a K ueLogPath);
}
kslog^ / W ( ) ` b_setLogFilenamh s Y & e o oe(g_consoleLoy O  D @gPath, true);
k} B r % vsccA ` 9 Ad_init(60)y - i * + j . n g;
// 设置 crash 发生时的 callback 函数
kscm_setEventCallback(onCrash);
KSCrashMonitorType monitors = kscrash_setMoni4 O ` : )  R w _toring(g_monitoring);
KSLOG_DEBUG("Installation ci s Bomplete.");
return monitors;
}
/** Called when a crash occurs.
*
* This function gets passed as a callback to a crash handler.
*/
static void onCrash(o _ + ] ^ 0struct KSCrash_MonitorContext* monitorConV e W Z 2 F z Atext)
{
KSLOG_DEBUG("Updating applic* ~ ( Z 7 p ,ation state to note crash.");
kscrashstate_notifyAppCrash();
monZ 7 n 6itorContextW k r B H G 5-!  4 0>consoleLogPathX t D E ^ l w = g_shouldAddConsoleLogToReport ? g_consoleLogPath : NULL;
// 正在处理 c* _ c H 4 #rash 的时分,发生了再次 crash
if(monitorContext->crashedDuringCrashHandling)
{
kscrashreport_writeB j %RecrashReport(monitorContext, g_lastCrashReportFilePath);
}
e! ] H { L : 6lse
{
// 1. 先依据当时时刻创立新的 crash 的/ G 8 . K ? Q文件途径
char crashReportFilePath[KSFU_MAX_PATH_LENGTH];
kscrs_getNextCrashReportPath(crashReportFilePath);
//X i $ q e l ? / 2. 将新生成的文件途径保存到 g_lastCrashReportFilePath
strncpy(g_lastC^ $ u I [rashRepo7 k H { ]rtFilePath, crashRept ( $ H @ r SortFilePath, sizeof(g_lastCrashReportFili [ : i X Q n UePath));
// 3. 将新生成的文件途% ] T K N { j 1径传入函数进行 crash 写入
kscrx 2 0 &ashreport_writeStandardReport(monitorContext, crashReportFilePath);
}
}

Y J M 6 ; v 6下来的函数便是详细的日志写入文件的完结。2个函数做的作业类似,都是格局化为 json 形式并写入文件。差异在于 crash 写入时假如再次发生 crash, 则走简易版的写入逻辑 kscrash` l _ 7 | Q dreport_writeRecrashReport(),否则走规范的x | z ` } # : J写入逻辑 kscrashreport_writeStandardReport()

bool ksfu_openBuffere6 n * t } _ H A |dWriter(KSBufferedWriter* writer, const char* const path, char* writeBuffer, int writeBuffeT 9 $ L n M /rLength)
{
writer->buffer = writeBuffW n q C Cer;
writer->bufferLength = writ^ I MeBufu A ` ^ y 5 /ferLength;
writer->position = 0;
/*
open() 的第二个参数描绘的是文件操作的权限
#define O_RDONLY        0x0000         open for reading only
#! o X  + bdefine O_WRONLY        0x0001         opb 4 D R s W 1 P ven~ c 0 ^ g for writing only
#define O_RDWR          0x0002         open for reQ a 0ading and writing
#defv + ine O_ACCMODE       0x0003         mask for a= b m r d 6 5 Tbove mode
#define O_C! u | | * ( C !REAT         0x0200         cr( ( 7 s %eate if noT 1 U T / K nexistant
#define O_TRUNC         0x0400         truncate to zero length
#define O_EXCL          0x0800         error if alr. k 3 2 p ! R e_ A Jady exists
0755:即用户具有读/写/履行权限i ) / 9 b B,组用户和其它用户具z S G s t 9 C L有读写权限;
06R H C Y ?44:即用户具有读写权限,组用户和其它用户具有只读权限;
成功则回来文件描绘符,若出现则回来 -1
*/
writer->fd = open(path, O_RDWRG { . 2 A s @ 1 * | O_CRE% k w AT | O_EXCL, 0644);
if(writer->fd < 0)
{
KSLOG_ERROR("Could not open crash report file %s: %s",& ] 4 M _ I 8 B path, strerror(errno));
return false;
}
return t2 S ( D i .rue;
}
/**
* Write a standard crash report to a file.
*
*  @param monitorContext Contextual information about the crash and environment.
*                      The caller must fill this out before passing it in.
*
*  @param path The file to wr( x ^ r 2 m A Rite to.
*/
voidS N 0 P kscrashrepJ . B sort_writeStandardReport(const struct KSCrash_MonL 3 8itorContext* const monitorContext,
const char* path)
{
KSLOG_INFO("Writi) d n _ng crash report to %s", path);
chart I F o l ~ Z q writeBuffer[1024];
KSBufferedWriter bufferedWriter;
if(!ks 3 = Q ; ^ esfu_openBufferedWriter(&bufferedWriter, path, writeBuffer, sizeof(writeBuffer)))
{
return;
}
ksccd_freeze();
KSJSONEncodeContext jsonContext;
jsonContext.userData = &ba 7 5 b |u6 | M 5 8ffereO F ? [ =dWriter;
KSCrashReportWriter concreteWriter;
KSCrashReportWriter*b x m I H writer = &concreteWriter;
prepareReportWriter(writer, &jsonContext);
ks3 d * 8 ; 4 g [ #jsQ : j | Y 0 F g .on_beginEncode(getJsonContexP v x ~ 2 @ v Xt(writer), true,g ? , f W . L s = addJSONData, &bufferedWriter);
writer->beginO? z q Z e U *bject(writer, KSCrashField_Report);
{
writeRef 7 4 N /portInb * C 4 z gfo(writer,
KSCrashField_Report,
KSCrashReportType_Standard,
monitorContext->eventID,
monitorCono i F Itext->System.processName);
ksfu_flushBufferedWriter(&bufferedWriter);
writeBinaryImages(writer, KSCrashFieldg J g * j m N . v_BinaryImages);
ksfu_flushB8 | CufferedWriter(&bufferedWriX R i 4 a , U h 2ter);
writeProcessStatN | E / S @e(wri6 X P ` { W Iter, KSCrashField_ProcessState, monitorZ ( t 8 7 T q OContext);
ksfu_H p c N  Q o ; =flushBufferedWriter(&bufferedWriter)k ~ H C ` A W @ y;
writeSystemInfo(writer, K: m 8 K T [ pSCrashField_System, mo* g unitorContext);
ksfu_flushBufferedWriter(&buffereG * z x @dWriter);
wrS ? ( L _iter->beginObje[ 3 n ; v rct(writer, KSCrashField_3 ] D a N ( g . JCrash);
{
writeErr& 0 C * e v Q m por(writer, KSCrashField_Error, monitorContext);
ksfu_flush_ u 7 - $ j C nBuf% 5 j K q & r G nferedWriter(&bufferedWriter);
writeAllThreads(writer,
KSCrashField_Threads,
monitoB ; ? drContext,
g_introspectionRules.enabled_ : l k L O T u);
ksfu_flushBufferedWriter(&bufferedWriter);
}
writer->endContainer(wrir a |ter);
if(g_use} o D ` i 2rInfoJSON != NULL)
{u x t m r - a = R
addJSONElement(writem y e p nr, KSCrashField_User, g_userInfoJSON, false);
ksfu_flushBufferedWriter(&buffer| W g k y w hedWriter);
}
else
{
write@ L ~ I G M .r->beginObject(writ$ O 4 #er, KSCrashField_User);
}
if(g_userSectionWriteCallback != NULLz m 3 @)
{
ksfu_flushBufferedWriE q jter(&e ? };bufferedWriter);
g_userSectionWriteCallback(writer);
}
writer->endContainer(? j M 9 4 % T @ :writer);
ksfu_flushBufferedWriter(&bufferedWriter);
writeDe~ k * 6 p U .bugInfo(writer, KSCrashFiZ $ y l ^ o ; 1eld_Debug, monitorContext);
}
writer->endContainer(writer);
ksjson_endEncode(getJsonContext(writer));7 6 7 % @
ksfu_z 1 )closeBufferedWriter(&/ t ) G f o  D;bufferedWe _ [ 2 * h M Jriter);
ksccd_unfreeze();
}
/** Write a minimal crash report to a file.
*
* @param monitorContext Contextual information about the crass ] Eh and environment.
*                       The caller must fill this out before passing it2 b u 7 5 a I F 7 in.
*
* @param path The file to write to.C b `
*/
void kscrashreport_writeRecrashReport(cons_ ] C ( : =t struct KSCrash_MonitorContext. J u Z &* const monitorContext,
constj X : char* path)
{
char writeBuE p + b Y J cffer[1024];
K` Z P L c [ ISBufferedWriter bufferedWriter;
static char tempPath[KSFU_MAX_PATH~ V + 3 i  O $_LENGTH];
// 将传递过来的上份 crash report 文件名途径(/var/mobile q w ! N * L s 6e/Containers/Data/Application/******/Library/Caches/KSCrash/Test/Reports/Test-report-******.json)修正为去掉 .json ,加上 .old 成为新的文件途径 /var/mobile/Containers/Data/Application/******/Library/Caches/KSCrashJ s e 0 s , $/Test/Reports/Test-report-******.old
strncpy(tempPath, path, sizeof(tempPath) - 10);
strncpy(tempPath + strlen(tempPath) - 5D 8 C * e L, ".old", 5);
KSLOa _ 3G_INFO("Writing recrash report to %s", path);
if(rename(path, tempPath) < 0)
{
KSLOG_ERROR("Could not rename %s to %s: %s", path, tempPath, st@ 7 0 ~ x 9 Z W Urerror(errno));
}
// 依据传入途= ! j径来翻开内存写入需求的文件
if(!Z R ( ^ O jksfu_{ p KopenBufferedWriter(&bufferedWriter, path, writeBuffer, sizeof(writeBuffer)))
{
return;
}
ksccd_freeze();
// json 解析的 c 代码
KSJSONEncodeContext jsonContext;
jsonContext.userData = &; ! Q;bufferedWriter;
KSCrashReportWriter concreteWriter;
KSCrashReportWriter* wK A i q q 7 v Mriter = &concreteWriter;
prepareReportWriter(writer, &jsonContext);
ksjson_beginEe O E ! 3 G  o Wncode(getA a + i d R QJsonContext(writer), true, addJSONData, &bufferedWriter);
writer->beg% j 5 r yinObject(writer, KSCrashField_Report);
{
writeRecraf z _ F m M r ) qsh(writer, KSCrashField_RecrashReporC v 7 H Ft, tempPath);
ksfu_flushBufferedWriter(B : ^&buffeQ & v o 2 D 7  PredWriter);
if(remove(tempPath) < 0)0 z |
{
KSL/ 6 ( f 6 & |OG_ERROR("Could not remo v N * q T T N fove %s: %s"` 3 9 ) I $ s, tempPath, strerror(errno));u ] ! @ / v
}
writeReportIn3 [ ifo(writer,
KSCrashField_Report,
KSCrashReportType_Minimal,
monitorContext->eventID,
monitorContext->System.processNS 5 # ] I V b Eame);
ksfu_flushBufferedWriter(&bufferedWriter);
writer->beginObject(writer, KSCrashField_Crash);
{
writeError(writer, KSCrashField_Error, monitorContext);
ksfu_flushBufferedWriter(&buff^ & 3 ` ? yeredWriter);
int threadIndex = ksmc_indexOfThread(moni$ [ DtorContext->E K _offendingMachineContext,
ksmc_getThW ; K  J 8 *readFromContext(monitorContext->offendingMachineContext));
writeThread(writer,
KSCrashField_CrashedThread,
monitorContext,
monitorCont% H  k +ext->offendingMach7 /  x b w mineContext,
thread~ Z S Q ( 0 j q WInde` 0 / ( jx,
false);
ksfu_flushBufferedWriter(&bufferedW@ g griter);
}
writer->endContainer(writer);
}
writer->endContainer(writer);
ksjson_endEncode(getJsonContext(writer));
ksfu_closeBufferedWriter, E V ; - s v(&buff4 $ k | n k $ f ,eredWriter);
kk [ j / ; ssccd_uJ g e y q a P 0 5nfreeze();
}
2.6.2 Crash 日志的读取逻辑

当时 Ae : Y m Wpp 在 Crash 之后,KSCrash 将数据/ + t )保存到 A # { w | Zpp 沙盒目录下,App 下次发动后咱们读取存储的 crash 文件,然后处理数据并上传。

App 发动后函数调用:

[KSCrashInstallatioN U 5 g J Ln sendAllReporw U a , $ * T NtsWithCompletion:] -> [KSCrash sendAll: - , @ B } _ReportsWithCompletion:] -&t ` agt; [KSCrash allReports] -> [KSCrash reportWithIntID:] ->[KSCrash loadCrashReportJSONU k ? y 4 g %WithID:] -> kscrs_readRepoc b ~ srt

sendAllReportsWithCompletion 里读取沙盒里的Crash 数据。

// 先经过读取文件夹,遍历文件夹内的文件数量来判别 crash 陈述的个数
static int getReportCount()
{
int count = 0;
DIR* dir = openk H 2 ~ m l G I ddir(g_reportsPath);
if(dir == NULL)
{
KSLOG_ERROR("Could not open directory %s! Z O / 2", g_reportsPath);
goto done;
}
struct dirent* ent;
while((ent =; p ]  j w N @ readdir(dir)) != NULL)
{
if(getReportIDFromFilename(eng 4  g 6t->d_name) > 0)
{
count++;
}
}
done:
if(dir != NULL)
{
closedir(dir);
}
return count4 ] B . /;
}
/t 3 / P a/ 经过 crash 文件个数、文件夹信息去遍历,一次获取到文件名(文件名的最终一部分便是 reportID),拿到8 m  W ? u s L reportID 再去读取 crash 陈述内的文件内容,写入数组
- (NSArray*) allRepU r 6orts
{
int repF [ W ] l u X ^ortCount = kscrash_getRe} u T L wportCount();
int64_t reportIDs[reportCount];
reportCount = k- Y 1 D z f 3scrash_getReportIDs(reportIDs, reportCount);
NSMutableArray* reports = [NSMutableArray arrayWithCapacity:(NSUInteger)reportCount];
for(int i = 0; i &lX ^ 9 ! i St; reportCount; i++)
{
NSDictionaO E Z [ u Y % U }ry_ R Q p D A M 1 P* reporE h 7 Z o f 9t = [self reportWithIntID:reportIDs[i]];
if(report != nil)M a A s * A -
{
[reports ads @ . e j q LdObject:report];
}
}
return reports;
}
//  依据 reportID 找到L H : S , crash 信息
- (NSDictionary*) reportWithI2 $ nntID:(int64_t) reportID
{
NSDatad + * Y R R 0* jsonData = [self loadCk U M U e &rashReportJSONWithID:reportID];
if(jsonData == nil)
{
return nil;
}
NSError* error = nil;
NSMutableDictionary* c$ d ; 6 9 `rashReport = [KSJSONCodec decode:jsonData
options:KSJSONDecodeOptionIgno7 [ ! $ U VreNullInArray |
KSJSONDecodeOptionIgnoreNullInObject |
KSJSOV ) 8 y # R n ?NDecodeOptionKeepPartialObject
error:&error];
if(error !E ] 1 [ s + P ;= nil)
{
KSLOG_ERROR(@"Encountered error loading crash report %" PRIx64 ": %@S c # 5", reportID, error);
}
if(crashReport == nil)
{
KSLOG_ERROR(@"Could noE j - !t load crash report");
return nil;
}
[self doctorReX L n b b oport:crashReport];
return crashReport;
}
//  reportID 读取 crash 内容并转化为 NSData 类型
- (NSData*) loadCrashReportJSONWithID:(int64_t) reportID
{
char* report = kscrash_readRepor^ p Y ht(reportID);
if(report != NULL)
{
return [t A YNSData dataWithBytesNoCopy:report lengtF 7 a H ch:strlen(report) freeWhenDone:YES];
}
return nil;
}
// repoj - _ { p * IrtID 读取 crashC [ u a [ : 数据到 cha. r e g e _ w Dr 类型
char* kscrash_readReport(int64_t reportID)
{
if(reportID <= 0)
{
KSLOG_ERROR("Report ID was %" PRIx64, reportID);
return NULL;
}
char* rawReport = kscrs_readReport(reportID);
if(rawReport == NULL)
{
KSLOG_ERROR("F1 * R w Q Oailed to load reportk & Q S ID %" PRIx64,5 8 w X L 7 reportID);
return NULL;
}
chay _ Cr* fixedReport = kscrf_fixupCrashU t wReport(rawReport);
if(fixedReport == NULL)
{
KSLOG_ERROR("Failed to fixup report IDc + F C - i & , %" PRIx64, reportID);
}
free(rawReport);
return fixedReps h ? ^ort;
}
// 多线程加锁,经过 rm  F h U %eportID 履行 c 函数 getCrashRepo; O N 0 N w T hrtPathBy8 # 6 Z r KID,将途径设置到 path 上。然后履行 ksfu_readEntireFile 读取 crash 信息到 result
char* kscrs_readReport(int64_t reportID)
{
pthread_m) Y | M W n Z T 5utex_locg H S O Hk(&S 3 w - :;g_mutex);
char path[KSCRS_MAX_. X 2 Z A E PPATH_LENGTH];
getCrashReportPathByID(reportID, path);
char* result;
ksfu_readEntireFq K 3 P } P s J ile(path, &amc t @ dp;result, NULL, 2000000);
pthread_mutex_unlock(&g_mutex);
return result;
}
int kscrash_getReportIDs(M H Yint64_t* reportIDs, int count)
{
return kscrs_getReportID- 8 % . a . 3s(reportIDs, count);
}
int kscrs_getReportIDs(inS _ I n A x Ft64_t* reportIDs, int count)
{
pthre- J { 2  W _ad_mutex_lock(&g_mutex);
count = getReportIDs(reportIDs, count);
pthread_mutex_unlock(&g_muy % T r ) v atex);
reM o q H I y 7 {turn count;
}
// 循环读取文件夹内容,依据 ent->d_name 调用 getReportIDFromFilename 函数,来获取 reportID,O P ? v A 8 S q .循环内部填充数组
static int getReportIDs(int64_t* reportIDs, int count)
{
int index = 0;
DIR* dir = opendir(g_reportsPath);
if(dir == NULL)
{
KSLOG_ERRO= i | e { a $ n R("Could not` * % & { Y 0 ! open directory %s", g_reportsPathW k J l K);
goto done;
}
struct% + k dirent* ent;
while((ent = readdir(dir)) != NULL && index < count)
{
int64_t reportID = getReportIDFromFilename(ent->d_name);
if(rep|  `ortID > 0)
{
reportIDs[index++] = reportID;
}
}
qsort(re 7 T w I } 1eportIDs, (unsigned)count, sizeo3 : # P 8 I !f(repw % _ x ;ortIDs[0]), compareInt64);
done:
if(dir != NULL)
{
closedir(dir);
}
return index;
}
// sprint4 Q ` d P & ; / Af(参数1, 格局2) 函数将格局2的值回来到参数1上,然后履行 sscanf(参数1, 参数2, 参数3),函数将字符串参数1的内容,依照参数2的格局,写入到参数3上。crash 文件命名为 "App称号-report-reportID.json"
stm ] W x m 9 S b gatic int64_t getReportIDFromFilename(const* i P char* filename)
{) q r 8 ( t e 1
char scanFormat[100];
sprintfw z 1 5 -(scanFormat, "%s-report-%%" PRIx64 ".json", g_appName);
int64_t reportID = 0;
sscanf(filename, scanFormat, &reporS b g m 8 xtID);
return reportID;
}
带你打造一套 APM 监控系统(二)

2.7 前端 js 相关的 Crash 的监控

2.7.1 JavascriO L b @ z x rptCore 反常监控

这部分简略粗暴,直接经过 JSContext 目标的 exceptionHand] L Y _ Z 5 g s Iler 特色来监控,比方下面的代码

jsConti e ) Y x y N X Gext.exceptionHandler = ^(N - t x v 5 X 1 ]JSContext *context, JSValue *exception) {
// 处理 jscore 相关的反常信息
};
2.7.2 h5 页面反常监控

当 h5 页面内的 Javascript 运转反[ { $ ` S常时会 window 目标会触发 E4 y lrrC 9 *orEvent 接口的 error 工作,并履行 window.onerror()

window.onerrors 0 : t C = function (msg, url, lineNumber, columnNumber, error) {
// 处理反常信息
};
带你打造一套 APM 监控系统(二)
2.7.3 React Native 反o ; ` T [ ) ; r #常监控

小试验:下图是写了一J U O t N ] _个 RN Demo 工程,在 Debug Text 控件上加了工作监听代码,内部人为触发 crash

<Text style={styles.sectionTitle} onPress={()=>{1+qw;}T 2 + # } V *}>Debug</Text>

比照组1:

条件:_ K % iOS 项目 debug 形式。在 RN 端增加了反常处理的代码。j W / c f @

模仿器点击 command + d 调出面板,挑选 Debug,翻开 Chrome 浏览器, Mac 下快捷键 Command + Option + J 翻开q R o 7 X ~ 5 R =调试面板,就能够像调@ [ y试 React 相同调试 RN 代码了。

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

检查到 crash stack 后点击能够跳转到 sourceMap 的当地。

Tipx z X X Ws:RN 项目打 Release 包

  • 在项目根目录下创立文件夹( release_iOS),作为资源的输出文件夹

  • 在终端切换到工程目录,然后履行下面的代码

    react-native bundle --l E #entry-file index.js --platform ioY G i w L } ; [ Ps --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_iOS --sourcemap-output release_ios/index.ios.map;
    
  • 将 release_iOS 文件夹内的 .jsbundleassets 文件夹内容拖入到 iOS 工程中即可

比照组2:

条件:iOS 项目 release 形式。在 RN 端不增加反常处理代码

操作:运转 iOS 工程,点击按钮模仿 crash

现象:iOS 项目奔溃。截图以及日志如下

带你打造一套 APM 监控系统(二)
2020-06-22 22:26:03.318 [info][tid:main][RCTRootView.m:294] Running application todos ({
initialPro/ ~ x l Z :ps^ 7 m x L F - ] 7 =     {
};
rootTag = 1;
})
2020-06-22 22:26:03.490 [info][tid:com.facebook.react.JavaV | 1 &Script] Running "todos" with {"roa D G D s O 4otTag":1,"initialProps":{}}
2020-06-22 22:27:38.673 [error][tid:com.fac. 7 l c A `ebook.react.JavaScript] ReferenceError: Can't find variable: qw
2020-06-22 22:27:38.675 [fI h 4 i fatal][tid:com.facebook.react.ExceptionsMaR Z n w Q : HnageV K 6 t CrQueue] Unhandled JS Exception: ReferenceEl k  ` ]rror: Can't find variable: qw
2020-06-22 22:27:38.69S l K 1300+f t c v d 8 c O0800 todos[16790:314161] *** Te[ x !  0rminatingn  ] u Q app due to uncaught exception 'RCTFatalED B 0 R {xceptioW # q k + ] % ?n: Unhandled JS Exception: ReferenceError: Can'F A V p 1t find variable: qw', reason: 'Unhandled JS ExJ a O # Jce$ v E x W T p / HpO j Q Vtion: ReferenceError: Can't find variable: qw, stack:
onPress@397:1821
<unknown>@203:w 0 L 1 O3896
_performSideEffectsForTransition@21v , S @ O /0:9689
_performSideEO l % k q c I :ffectsForT2 v y V G @ransition@(null):(null)
_receiveSignal@210:8425
_receiveSigq / [ B 0 hnal@(null):(null)
tou[ q x / 7 0 ~ T vchableHandlew u y - S |ResponderRelease@210:5671
touchableHandleRespond` 4 { s K O C serRel5 , s T a ? 3ease@(null):(null)
onResponderRelease@203:3006
b@97:1125
S@97:1268
w@97:1322
R@97:1617
M@97:2401
forEach@(null):(null)
U@97:2201
<unknown>@97:13818
Pe@97:90199
Re@97:13478
Ie@97:13664
receiveTouches@97:14448
value@27:354j | ) p . ) o K4
<unks N $ D 7 a * & Bnown>@27:840
value@27:2798
value@27:812
value@(null):(null)
'
*** First throB Y T 8 ! [w call stack:
(
0   CoreFoundai h L ] r & U O Qtion                      0x00007fff23e3cf0e _$ * D V n G % & u_exceptionPreproI H { s / e r ! =cess + 3: x 250
1   libobjc.A.dylib                     0x00007fff50ba89b2 objc_exception_throw + 48
2   todod N % 4 ? ? ps                               0x00000001017b0510 RCTFormatError + 0
3   todos                               0x000000010182d8ca -[RCTExF G U  | | r F {ceptionsA e 5 ] ) y r ,Manager reportFatal:stack:exceptionId:suppressRedBox:] + 503
4   t= # X z - ] z y uodos                               0x000000010182e34e -[RCTExceptionsManager reportException:] + 1658
5   CoreFoundation                      0x00007fff23e43e8c __invok; r Ving___# x L @ g W 4 + 140
6   CoreFoundation                      0x00007fff23e41071 -[NSInvocation invoke] + 321
7   CoreFoundation                      0x00007fff23e41344 -[NSInvocation inv M w fokeWithTarget:] + 68
8   todos                               0x00000001017e07fa -[RCTModuleMethod invokeWithBridge:module:a` E GrgumeH E Xnts:] + 578
9   todos                               0x00000001017l s b n +e2a84 _ZN8facebook5reactL11invokeInnerEP9RCTBridgeP13RCTModuleDatajRKN5folly7dyE r H : L q rnamicE + 246
10  todos                               0x00000001017e280c ___ZN8facebook5react15Rc ; ] $ G g ^ Cp G E  A n ( V oTNativeModulA i me6invokeEjON5folly7dynamicEi_block_invoke + 78
11  libdispatch.dylib                   0x00000001025b5f11 _dispatch_call_block_and_release + 12
12  lM T  E M ~ N Libdispatch.dylib                   0x00000001025b6e8e _dispatch_client_callout + 8
13  libdispatch.dylib                   0x00000001025bd6fd _dispatch_lane_s% o -  % M 5erial_drain + 788
14  libdispatch.dylib                   0x00000001025be28f _dispatch_w * &lane_invoke + 422
15  libdispatch.5 : , f S *dylib                   0x00000001025c9b65 _dispatch_workloop_worker_thread + 719
16  libsystem_pthread.dylib             0x00007fff51c08a3d _pthread_wqthread + 290
17  libsyst# : - * p 9 Zem_pthread.dylib             0x00007ff/ { ; I C [ }f51c07b77 start_wqthread +s [ - 1 ~  15
)
libc++abi.dylib: terminating with un1 x O v Zcaught exception of type NSException
(lldb)

Tips:怎么0 b 9 b F _在 RN release 形式下调试(看到 js 侧的 console 信息)

  • AppDelegate.m 中引入 #iA # [mport <Reactz N { D y x L/RCTLog.h>
  • - (BOOL)applica^ N ( 8 ` H | l Ttion:(UIApplication *)application didFinishLaunchingWithOptiI - e ( E 5 qons:(NSDictionary *)launchOptions 中加入 RCTSetLogThreshold(RCTLogLA 0 q 3 d n oevelTrace);

比照组3:

条件:iOS 项目 release 形式。在 RN 端增加反常处理代码。

global.ErrorUtils.setGlobalW D c 5 ) ~ p O mHT h h % 1 V bandler((e) => {
console.log(e);
let message = { name: e.name,
message: e.message,
stack: e.stack
};
axios.get(F ) y'http://192.168.1.100:8888/test.php'. 2 { -, {
params: { 'messageO & M K ;  h ~': JSON.stringify(m{ P ! ! W z aessa7 R # ) hge) }
}).then(function (response) {
console.log(response)
}).caG r J }tch(fh @ @ 4 G nuncc  f / . ? $ J .tb x bion (error) {
console.log(error)
});
}, true)

操作:运转W r z F H iOS 工程,点击按钮模J ; # 6 O @ 2仿 crash。

现象:[ = F = *iOS 项目不奔溃。日志信息如下,比照 bundle 包中? { } C ~ v q的 js。

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

定论:

在 RN 项目中,假如发生% T M ? J ; 7了 crash 则会在 NaG Q U Gtive 侧有相应体现。假如 RN 侧写了 crash 捕获的代码,则 Native 侧不会奔溃。假如 RN 侧的 crash 没有捕获,则 Native 直接奔溃Y v _

RN 项目写了 crash 监控,监控后将仓库信息打印出来发现对应的 js 信息是经过 webpack 处理的,crash 剖C V 8 0 *析难度很大。所以咱们针对 RN 的 crash 需求在 RN 侧写监控代码,监控后需求上报,此外针对监控后的信息需求写专门的 crash 信息复原给你,也便是 sourceMap 解析。

2] ; ?.7.! n D l _3.1 js 逻辑过错

3 Z H K K g过 RN 的人都知道在 DEBUG 形式下 js 代码有问题则会发生红屏,在 RELEASE 形式下则会白屏或许闪退,为? W 1 q了体会和质量把控需求做反常监控。

在看 RN 源码时分发现了 Errou { l 8rUtils,看代码能够设置处理过错信息。

/*| D y i D G*
* Copyright (c) Facebook, In% * [c. anN ^ ( u 6 I J & ed its affiliates.
*
* This sourcf | e ( 7 d xe code is license] r A H C K Dd under the MIT licensK : { 5 1 { ? )e found in the
* LICENSE file in tJ o t Q W Ahe root! ) * directory of this source tree.
*
* @format
* @flow stricZ  y p - T @t
* @polyfill
*/
let _inGuard = 0;
type ErrorHandler = (error: mixed, isFatal: boolean) => void;
type Fn<Args, Return> = (...Args) => Return;
/**
* This is the error handler that is calle4 ?  R - # .d when we encounter an exception
* when loading a module. This will report any e! X F 3 3 @rrors encountered before
* ExceptionsManC M & P K T 3 xager is configured.
*/
let _glob6 R E . / b xalHanl Z ` h *dler: ErrorHandler = function onError(
e: mixed,
iz  9sFatal: boolean,
) {
throw e;
};
/**
* The particular require runtime that we are using looks for a global
* `ErrorUtils` object and if it exists, then it requires modules with the
* error handler specified via ErrorUtils.setGlobalHa1 T ( _ Z Vndler by calling the
* reque y * lire function with applyWithGuard. Since the require module is loaded
* before any of the modules, this Error+ H ? E w * y H /Utils3 8 9 Z t _ 4 z l must be defined (and the handler
* set) globally before rv 7 j 6 r ( 3 1equiring anything.
*/
const ErrorUtils = {
se1 6 [ R { H m c NtGlobalHandler(fun: ErrorHandler): void {
_globalHandler = fun;
},
gL z retGlobalHandler(): ErrorHa! . _ ( jndler {
return _globalHandler;
},
r` ~ QeportError(o 9 0 oerror: mixedd ( U E Q $): void {
_globalHandler &&R ^ : 6 k , I S :; _globalHandler(error, false);
},
reportFatalError(error: mixed): voiQ ? * ~ L ,d {
// NOTE: Tx p I  8his has ano ; [ 0 h untyped call site- h C 7 # X U ~ in Metro.
_globalHandler && _globalHandler(error, true);
},
applyWithGuard<TArgQ + m Bs: $ReadOnlyArray<mixed>, TOut>(
fun: Fn<TArgs, TOut>,
cono 5 n 3 G # ttexV s = = :t?: ?mixed,
args?: ?TArgs,
// Unused, but s( : }ome code synced from www sets it to null.
unused_onError?: null,
// Some callers pass a name herA _ =e, which we igno5  5 }re.
unused_naC j 8 [ ;me?: ?string,
): ?TOut {
try {
_inGuard++;
// $FlowFixMe: TODO T48204745 (1) apply(contex@ . g ~t, null) is fiC q + Y l T U ! :ne. (2) array -> rest array should work
return fun.apply(context, args);
} catch (ez % * ~ ( % @ x) {^ u ` t ^ c g
ErrorUtils.reportError(e);
} finally {
_inGuard--;
}
return null;
},
applyWithGuardIfNeeded<TArgs: $ReadOnlyArray<mixeV t N 7 t V ,d>, TOut>(
fun: Fn<TArgs, TOut>,
context?: ?mixed,
args?: ?TArgs,
): ?Ts v O , _Out {
if (ErrorUtils.inGuard()) {
// $FlowFixMeq f c _ t: TODO T48204745 (1) apply(context, nG _ 5 O ? Oull) is fine. (2) array -> rest array_ | 0 ) shouf T 0ld work
return fun.apply(context, args);
} else {
ErrorUtils.applyWithGuard(fun, context, args);
}
return null;
},
inGuard(): boolean {
return !!_iO [ v R nnGuard;
},
gua_ [ V w $ e 7 Drd<TArgs: $Readm r C hOnlyArray<mixed>, TOut>(
fun: Fn<TArgs, TOut G Q t  0>,
name?: ?string,
context?: ?mixed,
): ?(...TArgs) => ?TOut {
// TODO: (moti) T48204753 Make sure this warning5 V E is never hit am X Y M $nd remove it - types
// should be sufficient.
if (typeof fun !== 'function') {
console.warn('A function must be paP ; f ~ G N u )ssed to ErrorUtils.guard, got ', fun);
return null;
}
const guardName = name ?? fun.name ?? '<generated g y V { a a uuard>';
function guarded(.H Y j ; O { w Z..args: TArgs): ?TOut {
return ErrorUtils.applyWithGuard(
fun,
context ?? this,
argsQ G {,
nula O f 1 , v T wl,
guardName,
);
}
return guarded;
},
};
global.ErrorUtils = ErrorUtils;
export type ErrorUt; K T 8 ) y  L 9ilsT = typeof ErrorUtils;

所以 RN 的反常能够运用 global.ErrorUtils 来设置过错处理。举个比方

global.ErrorUtilZ ? H vs.setGlobalHandler(e => {
// e b [ _.name e.message e.stack
}, true);
2.7.3.2 组件问题

其实关Q Q 0 ~ [ x `于 RN 的 crash 处理还有个需求注意的便是 React Error Boundaries。详细资料

曩昔,组件内的 JavaScript 过错会导致 React 的内# y 5部状况被损坏,而且在下一次烘托时 发生 或许无法追踪的 过错。这些过错基本上是由较早的其他代码(非 React 组件代码)过错引起的,但 Reac= { 3 { $ k _t 并没有供给一种在组件中高雅处理这些过错的办法,也无法从过错中恢复。

部分 UI 的 ^ O JavaScript 过错不应该导致整个运用溃散,为了处理这个问题,React 16 引入了一个1 ? R o h Y e新的概念 —— 过错鸿沟。p b @ o 7 3 m

过错鸿沟是一种 React 组件,这种组件能够捕获并打印发生在其子组件树任何方位的 JavaScT v – ) ) ( l Xript 过错,而且,它会烘托出备用 UI,而不是烘托那些溃散} , t了的子组件树。过错鸿沟在烘托期间、生命周期办法和整个组件树的结构函数中捕获过错。

它能捕获子组件生命周期函数中的反常,包括结构函数(constructor)和 render 函数

而不能捕获以下反常:

  • Event handlers(工作处理函数)
  • Asynchronous code(异步代码,如setTimeout、promise@ U L等)
  • Server side rendering(服务端烘托)
  • ErrP 1 ) t e z 5ors thrownO _ H in the error boundary itself (rather than its children)(反常鸿沟组件自身抛出的反常)

2 ` z e f I @以能够经过反常鸿沟组件捕获组件生命周期内的一切反常然后烘托L I 1 K 7 % B兜底组件 ,防止 App crash,提高用户体会。也可引导用户反馈g X X ) w F ^问题,便利问题的排查和修复

至此 RN 的 crash 分为2种,分别是 js 逻辑过错、组件 js 过错,都现已被监控处理了。接下来就看看怎么从工e d ? |程化层面处理这些问题

2.7.4 RN Crash 复原

SourceMap 文件关于前端日志的解析至关重要,SourceMap 文件中各个参数和怎么核算的进程都在里边有写,能够检查这篇文章。

有了 SourceMap 文件,借助于 mozilla 的 soV X T 2 T lurce-map 项目,能够很好的复原 RN 的 crash 日志。

我写了个 NodeJy s !S 脚本,代码如下v ` s / + A

var fs = requireY ) v('fs');
var soH 6 { urceMap = require('source-map');
var argumen_ $ S + 3 [ U dts = process.argv.splice(K F b O $ 3 U2);
function parseJSError(aLine, aColumn) {
fs.readFile] a F ` Y('./index.ios.map', 'utf8', function (err, data) {
const whatever =  sourceMap.SourceMapConsumer.with(data, null, consumer => {
// 读取 crashW f & W 1 t 日志的行号、g I E $列号
let parseData = consumer.originalPositionFor({
line: parseInt(aLine),
column: parseInt(aColumn)
})o # I ( f;
// 输出到控制台
console.M E + tlog(parseData);
//e | 3 T L 输出到文件中
fs.writeFileSync('./parsed.txt', JSON.stringify(parseData) + 'n', 'utf8', function(err) {
if(err) {
console.log(err);
}
});
});
});
}
var line = arguments[0];
var column = arguments[1s ^ E G]q ] N ` / K g };
parseJSError(line, column);

接下来做个试验,还是上述的 todos 项目。

  1. 在 TeS B V S ( `xQ P C U Y Kt 的点击工作上模仿 crash

    <Tex{ I 2t style={stylesG K W v k * c ~.sectionTitle} onPress={()=>{1+qw;}}>Debug</Text>
    
  2. 将 RN 项目打 bundle 包、产出 sourceMap 文件。履行指令,

    react-native bundle --entry-file index.js --platform android --dev false -H ) C !-bundle-output relq { h 7 sease_ios/main.jsbundle --assets-dest release_Q B B qiOS --sourcemap/ k w ^-output rek 1 _lease_ios/index.andrU v 0 Q % ( 3oid.map;
    

    由于高频运用,所以给U K ) @ + iterm2 增加 alias 别名设置,修正 .zshrc 文件

    alias RNRelease='react-native bundle --entry-file index.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-des5 j ^ 7 T n x V jt release_iOS --sourcemap-output release_ios/index.ios.maw S mp;' # RN 打 Rm m B @elease 包
    
  3. 将 js bundle 和图片资源拷贝到 Xcode 工程中

  4. 点击模仿 crash,将日志下面的行号和列号拷贝q H Q N 7 ] +,在 Node 项目下,履行下面指令

    node index.js 397 1822
    
  5. 拿脚本解析好的行号、列号、文件信息去和源代码文件比较,成果很正确。

带你打造一套 APM 监控系统(二)
2.7.5 SourceMap 解析体系规划

意图:经过渠道能够将 RN 项目线上 crash 能够复原到详细的文件、代码行数、代码列数。能够看到详细的代码,能够看到 RN stack trace、供给源文件下载功用。

  1. 打包体系下办理的服务器:
    • 出产环境下打包才生成 source map 文件
    • 存储打包前的一切文件(install)
  2. 开发产品侧 RN 剖析界面。点击搜集到的 RN crash,在详情页能够看到详细的文件、代码行数、代码列数。能够看到详细的代码,能够看到 RN stack trace、Native stacka 7 w m ^ A E : trace。(详细t z b s % o b S技能完结上面讲过了)
  3. 由于 souece map 文件较= : ; . 1 n大,RN 解析过长尽管不久,但是是对核算资Z K 8 B U ? 源的耗费,所以需求规划高效读取办法
  4. SourceMap 在 iOS、Android 形式下不相同,所K 1 G V以 SoureceMap 存储需求区分 os。

3. KSCrash 的运用包装

然后再封装自己的 Crash 处理逻辑。比方要做的作业便是X q F % S W K

  • 承继自0 2 K KSCrashInstallation 这个抽象类,设置初始化作业(抽象类比方 NSURa 5 ( p / G v x mLProtocol 有必要承继后运用),完结抽象类中的 sink 办法。

    /**
    * Crash system installation which handles9 @ V n x # backend-specific details.
    *
    * Only one installation can be installed at a time.
    *
    * This is an abstract5 G U ; class.
    */
    @interface KSCrashInstallation : NSObject
    
    #import "CMCrashInstallation.h"
    #import <KSCrash/KSCrashInstallation+Private.h>
    #import "CMCrashReporterSink.h"
    @implementation CMCrashInstallation
    + (instancetype)sharedInstance {
    static CMCrashInstallation *sharedInstance = nil;
    static dispatch_once) j H 8 p i b $_t onceToki ~ 1 S ken;
    dispatch_once(&onceToken, ^{
    sharedInstance = [[CMCrashInstallation alloc] init];
    });
    return sharedInstance;
    }
    - (id)init {
    return [super initWithRequ{ ] 5 9 t $ + |irj f b IedPropertie g % ? t 2 _ E ees: nil];
    }
    - (id<KSCrashRepol * % W . X a prtFilter>)sink {
    CMCrass } , X x M j hhReporterSink *sink = [[CMCrashReporterSink alloc] init];
    return [sink defaultCr/ Y z ~ D D EashReportFilterS^ Q { ] q & b G 2etAppleFmt];
    }
    @b = E kend
    
  • sink 办法内部的 CMCrashReporterSink 类,遵从了 KSCrashReportFilter 协议,声明晰公有@ A M %办法 defaultCrashR; X @ v # X E oeportFilte% l R m GrSetAppleFmt

    // .h
    #import <Foundation/Foundation.h>
    #import <KSCrash/KSCx , & l i c e ~ $rashReportFilter.h>
    @interface CMCrasx ; 4 X khReporterSink : NSObject<KS k * Y tCrashReportFilter>
    - (id <KSCrashReportFilter&gk 7 q 6 h 7 j 4t;) defaultCrashReportFilterSetAppleFmt;
    @end
    // .m
    #pragma mark - public Method
    - (id <KSCrashReportFilter>) defaultCras# 4  !hReportFilterSetAppleFmt
    {
    return [KSCrashReportFilter+ B 3 L Y ePipeline filterWithFilters:
    [CMCrashReportFilterAppleFmt filterWithReportStyle:KSAppleReportStyleSymbolicatedSideBySide],
    self,
    nil];
    }
    

    其间 defaultCrashReportF? n 2 n W Z ; I zilterSetAppleFmt 办法内部回来了一个 KSCrS , 1 J } washX M . * } 7 y L 3Rep} / x H | 1ortFil$ { z o I rterPipeline 类办法 filt2 2 k ? }erWithFilters 的成果。

    CMCrashReportFilterAppleFmt 是一个承继自 KSCras- l U ohReportFilterAppleFmt 的类,遵从了 KSCrashReportFd A C ~ ^ 8ilter 协议。协议办法答应开发者处理7 H E : 6 ^ C Crash 的数据格局。

    /** Filter the specified reports.
    *
    * @paramZ S 2 v reports The reports to process.
    * @param onCompletion Block to call when procesO z I l ~ 5 o i ysing is completR R # % { X Ue.
    */
    - (void) filterReports:(NSArray*) re5 , (ports
    onCompletion:(KSCrashReportFilterCoZ 8 / k r 8 + M nmpletion) onCompletion;
    
    #import <KSCrash/KSCrashF 6 I ~ jReportFilterAppleFmt.h>
    @interface CMCrashReportFilterAppleFmt : KSm 4 = L ) BCrashRepc = LortFilterAppleFmt<KSCrashReportFilter>
    @end
    // .m
    - (void) filterReports:(NSArray*)reports onCompletion:(KSCrashReportFilterCompletion)onCompletion
    {
    NSMutableArray* filteredP z | j b F Y :Reports = [NSMutableArray arrayWithCapacity:[reports count]];
    for(NSDictV X z D ~ )ionary *report in reports){
    if([self majorVersion:report] == kExpectedMajorVersion){
    id monitorInfo = [T 7 & Rs; J i qelf generateMonitorInfoFromCrashReport:report];
    if(monitorInfo != nil){
    [filV y r p u . ~ jteredReports addObject:monitorInfo];
    }
    }
    }
    kscrash_callCompletion(onCe ~ 1ompletion, filteredReports, YES, nil);
    }
    /**
    @brief 获取Crash JSON中的U O #crash时刻、m} h Y S b Vach name、signal name和apple report
    */
    - (NSDictionary *)generateMonitorInfoFromCrashReport:(NSDictionary *)crashReport
    {
    NSDi v /ctionary *infoReport = [crashRepD & :ort objectForKey:@"report"];
    // ...
    id appleReport = [self toAppleFormat:crashReport];
    NSMutableDictionaryD I n F c *info = [NSMutableDictionary dictionary];
    [info setValue:crashTime forKey:@"crashTime"];
    [info setValue:appleReport forKey:@"appleReport"];
    [info setValue:6 ; ? 7 H ? j 2 kuserExcex _ e ( 5 | Q Xption forKey:@"userExcD 7 7 O A & { seption"];
    [info setValue:userInfo fM f f 7 F O i x ForKey:@"custom"];
    return [info copy];
    }
    
    /**
    * A pipeline of filters. Report2 $ I . } W ? hs get passed througV 4 zh each subfilter in order.
    *
    * Input: Depends on what's in the pipeline.
    * Ou/ J 6 .tput: Depends on what's in the pipeline.
    */
    @ink C Y t p - 3 ? Bterface KSCrashReportFilterPipeline : NSObject <KSCrashReportFilter>
    
  • APM 才干中为 Crash 模块设置一个发动器。发动器内部设置 KSCrash 的初始C B 3 i V化作业,以及触发 Craso ! / X ph 时分监控所需数据的拼装。比方:SESSION_ID、App 发动时刻、Ag 7 * Y p `pp 称号、2 X k & 2 q溃散时刻、App 版别号、当时页面信息等根底信息。

    /** C Function to call during a crash report to give the callZ I H v ; & ~ eee an opportunity to
    * add to the report. NULL = ignore.
    *
    * WARNING: Only call async-safe functions from this function!q J h t z ( 9 6 j DO NOT call
    * Objective-C methods!!_ & J v Q m!
    */
    @property(atomic,readwrite,assign) KSReportW : + h WriteCU X 8 W % + _  Aallback onCrash;
    
    + (instancetype)sharedInstance
    {
    static CMCrashMonitor *_sharedManager = nil;
    static dispatch_once_t onceToken;
    dispa)  3 _ s 7tchc J  o s d p C 1_once(&onceToken, ^{
    _shK % O = $ J _ -ars d  y s / XedManager = [[CMCrashMon, X d ` i t 7 8itor aK . ulloc] init];
    });
    return _sharedManager;
    }
    #pragma mark - public Method
    - (void)startMonitor
    {
    CMMLog(@"crash monitor started");
    #ib . * m H sfdef DEBUG
    BOOL _tD v ~ | ^rackingCrasho  D z s + { T (OnDebug = [CMMonitorCoP g ( L z r N T nfig sharedInstance].track5 p h U tingCrashOnDeq # I Kbug;
    if (_trackingCrashOnDebug) {
    [self installKSCrash];
    }
    #else
    [self installKSCrash];
    #e8 s G : ~ /ndif
    }
    #pragma mark - pr* ; F zivate method
    static void o* H WnCrash(const KSCrashReportWriter* writer)
    {
    NSString *sessionId = [NSString stringWithFormat:@""%@"", ***]];
    writer->addJSOI o hNElement(writer, "SESSION_ID", [sessionId UTF8String], true);
    NSString *appLaunchTime = ***;
    writer->addJSONElement(writer, "USER_APP_S/ 2 M YTART_DATE", [[NSString stringWithFormat:@""%@"", appLaunchTime] UTF8String], true);
    // ...
    }
    - (void)installKSCrR u [ 8ash
    {
    [[CMCrashInstallation sharedInstance] install];
    [[CMCrashInstallatf  _ Q | / iion sharedInstance] sendAllRepp 1 f r 4 Gortsi V - 8Wit[ c T KhCompletion:n% 3 [ A  $ | lil];
    [CMCrashInstall+ s ` k 5 7 yation sharedInstance].onCraso 2 { x K F p Yh = onCrash;
    dispatch_after(dispat` @ = `ch_time(DISPATCH_TIME_NOW, (int64_t)(5.f * NSEC_h v , B s Z cPER_SEC)), dispatch_get_main_queu7 * A l | N e(), ^{
    _isCanAddCrashCount = NO;
    });f u G = Z $ A 5
    }
    

    installKSCrash 办法中调用了 [[CMCrashInstallation sharedInstance] sendAllReportsWithCompletion: nil],内部完结如下

    - (void) sendAllReportsWithCompletion:(KSCrashReportFilterCompletion) oC + Y 2nCompletion
    {
    NSError*R 9 , q ` + t V erro/ O @  T o Wr = [self validateProperties];
    if(erD u S 6 . % aror != nil)
    {
    if(onCo= 6 J ! O .mpletion != nil)
    {
    onCompletion(nil, NO, error);
    }h . P
    return;
    }
    id<Kr I f 3 F H R 5SCrashReport# t ; sFilter> sink = [self sink];
    if(sink == nil)
    {
    onCompletion(niT o v R . ^ Nl, NO, [NSError]  +  P o : U F errorWithDomain:[[sr T Delf class] description]
    code:0
    des] D } H } XcriptiA y / W p ) [ _ot ? D 2 % rn:@"Sink was nil (subclasses must implement method "si_ ? 0 nk")"]);
    return;
    }
    sink = [KSCr8 5 , S H L  qashReportFiltery 5 y % [ h F APipeline filterWithFilters:self.9 . ^ o o p l 4 0prependedFilters, sink, nil];
    KSCrash* handler = [KSCrash sharedInstanc^ d  O # ` g  se];
    handler.sink = sink;
    [handler sendAllReportsWithComplet5 v K H  . ; ; +ion:onCompletion];
    }
    

    办法内部将 KSCrashQ L X a 4 ! nInstallationsink 赋值给 KSCrash 目标。 内部还是调用了 KSCrashsen= j % 2 3 u 7 $ NdAllReportsWithCompletion 办法,完结如下

    - (void) sendAllReportsWithg Y # G t z FCompletion:(KSCrashReportFilterCompletion) onCompletion
    {
    NSArray* reports = [self allReports];
    KSLOG_INFO(@"Sending %d crash reporg X T  I d Q ; wts", [reports count]);
    [self sendRe: @ K ports:reports
    onCompletion:^(NSAr= . e E _ xray* filteredReports, BOOL completed, NSError* error)
    {
    KSLOG_DEBUG(@"Pro# D $ e ^ R L } )cess finished with completio@ n Jn: %d", completedU w x e &);
    i^ 0 hf(error != nil)
    {
    KSLOG_ERROR(@"Failed to send reports: %@", error);
    }
    if((self.dg 0 c  A t n z 2eleteBehaviorAfterSendAll == KS: + w oCDeleteOnSucess && completed) ||
    self.deleteB5 a L ?eN Y ` a % V vhaviorAfterSendAll == KSCDeleteAlways)
    {
    kscrash_deleteAllReports(); # G t
    }
    kscrashX - ~ F r i_callCompletion(onCompletix ( 9 B ( D + m _on, filtered! } : gReports, completed, error);
    }];
    }
    

    该办法内部调用了目标办法 sendReports: onCompletion:,如下所示

    - (void) sendReports:(NSArrayP L b*E + D) reports onCompletion:(KSCrashRepor[ t ~ o 0tFilterCompletion) onCompletion
    {
    iw ! l P  8 | Ff([reports count] == 0)
    {
    kscraR B c % w ? , ? ush_callCompletion(onCompletion, reports, YES, nil);
    return;
    }
    if(self.sink == nil)
    {
    kscrashO e q M_callCompletion(onCompletion, reports, NO,
    [NSError errorWithDomain:[[self class] descriE ~ Aption]
    code:0
    description:@"No sink set. Crash reports not sent."]);
    return;
    }
    [self.sink filterReports:reports
    onCompletion:^(NSArray* filteredReport[ + X Ts, BOOL completed, NSError* error)
    {
    kscrash_callCompletion(onCompletion, filteredReports, completed, error);
    }];
    }
    

    办法内部的 [self.sink filterReports: onCompletC ! ( V 5 4 R R -ion: ] 完结其实便是 CMCrashInstallation 中设置的 sg y A &ink getter 办法,内部回来了 CMCrashReporterSink 目标的 defaultCrashRes l i h % vportFilterSetAppleFmt 办法的回来值。内部完结如下

    - (id <KSCrashReportFilter>) defaultCrashReportFil% ? Z bterSetAppleFmt
    {
    return [KSCrashReportFilterPipeline filterWithFilters:
    [CMCrashReportFilterAppleFmt filterWithReportSd W w Mtyle:KSAppleReportStyleSymbolicatedSideBySide],
    self,
    nil];
    }
    

    能够看到这个函数内部设置了L 5 d $多个 filters,其间一个W k ? p便是 self,也便是 CMCrashReporteL C p o 7 1 h JrSink 目标,所以上面的 [self.sink filterRepo0 g o Z W lrts: onCompletion:] ,也Q ? k k Y d S z便是调用 CMCrashRep^ X 4 A 3 b $orterSink 内的数据处理办法。完1 ] N了之后经过 kscrash_callCompletion(onCompletion, reports, YES, nil); 奉告 KSCrash 本地保存的w V T 7 0 y 5 Crash 日志现已处理完毕,能够删去了。

    - (void)filterReports:(NSArray *)reports onCompletiov E C X ; mn:(KSCrashReportFilterCompletion)onCompletf ; , b G Qion
    {
    for (NSDictionaryC C ? k n [ f G *report in reports) {
    // 处理 Crash 数据,将J l p 3 f 3 D m S数据交给一致的数据上报组件处理.1 ~ . 6 t B K R..
    }
    k~ f S : Lscrash_callCompletion(onCompletion, reports, YES, nil);
    }
    

    至此,归纳下 KSCrash 做的作业,供给各种 crash 的监控才干,在 crash 后将进程信息f h % G h ?、基本信息、反常信息、线^ s @ K @ [程信息等用 c 高效转化为 json4 y U 1 写入文件,App 下次发动后读取本地– @ ! Y的 crash 文件夹中的 crash 日H v Q W j u志,让开发者能够自界说 key、value 然后去上报日志到 APM 体系,然后删去本地 crash 文件夹中的日志。

4. 符号化

运用 crash 之后,体系会生成一份溃散日志,存储在设置中,运用的运转状况、调用仓库、所处线程等信息会记载在日志中。但是这些日志是地址,并不可读,所以需求进行符号化复原。

4.1 .dSYM 文件

.dSYM (debugging syZ 2 & J E ) ! Nmbol)文件是保存十六进制函数地址映射信息的中转文件,调试信息(symbols)都包括在该文件中。Xcode 工程每次编E D d h r译运转都会生成新的 .dSYM 文件。默许状况下 debug 形式时不生成 .dSYM ,能够) S : Z % w v h Y在 Build Settings -> Build Options –L R B . Q 8> Debug Information Format 后将值 DWARFZ – p正为 DWARF with dSYM File,这样再次编译运转就能够生成 .dSYM 文件。

所以每次 App 打包的时分都需求保存每个版别的 .dSYM 文件。

.dSYM 文件中包括 DWARF 信息,翻开文件的包p [ C P K * O内容 Test.app.dSb H aYM/Contents/Resource! = | 3s/DWARF/Test 保存的便是 DWAp @ V N x { YRF 文件。

.dSYM 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,发布的时分为了安全,会把调试信息存储在独自的文件,.dSYM 其实是一个文件目录,结构如下:

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

4.2 DWARF 文件

DWARF is a debugging file format used by many compilers and debuggers to support source level debugging. IN v % Dt addresses the requirements of a number of procedural languages, such as C, C++, and Fortran, and is deL i C msigned to be extensible to other languages. DWARF% Q is architecture independent and applicable to any! / Q ^ ) – G 5 processor or operating system. It is widely used on Unix, Linux a4 U [ : ^ Fnd other operating syn L + stems, as well as in stI v d T n P Jand-alone environments.

DWARF 是一种调试文件格局,它U Z ^被许多编译器和调试器所广泛运用以支撑源代码等级的调试。它满意许多进程语言(C、C++、Fortran)/ % V q F ( G K的需求,它被规划为支撑拓展到其他语言。DWARF 是架构独立` t d [ Z i ` G R的,适用于其他任何6 6 1 1 I # r %的处理器和操作体系。被广泛运用在 Unix、Linux 和其他的操作体系上,以及独立环境上。

DWARF 全称是 Debugging With Arbitrary Record Formats,是一种运用特色化记载格局的调试文件。

DWARF 是可履行程序与源代码关系的一个紧凑表明。

大多数现代编程语言都是块结构:每个实体(一个类、一个函数)被包括在另一个实体中。一个 c 程序,每个文件或许包括多个数据界说、多个变量、多个函数,所以 DWARF 遵从这个模型W T +,也是块结构。DWARF 里基本的描绘项是调试信息项 DIE(Debugging Information Entry)。一个 DIE 有一个标签,表明这个 DIE 描绘了什v , 8 @ : 2 _ ]么以及一个填入了细节并进一[ r _ N } 步描绘该项的, O # b U U :特色列表(类比 hH e 6 R a @tml、xml 结构)。一个 DIE(除了最顶层的)被一个父 DIE 包括,或许存在兄弟 DIE 或许子 DIE,Z ` & H V @ Q Y l特色或许包括各种值:常量(比方一个函数名),变量(比方一个函数的开端地址),或对另一个DIE的引证(比方一个函数的回3 6 U G L来值类型)。

DWARF 文件中的数据如下:

数据列 信息阐明
.debug_loc 在 DWc ) Q 2_AT_location 特色中运用的方位列表
.debug_macinfo 宏信息
.debug_pubnam% ? 6 S M es 大局目标和函数的查找表
.debug_pubA ~ {types 大局类型的查找表
.debug_ranges 在 DW_? 4 x 5 P 0 B QAT_ranges 特色中运用的地址规模
.debug_l # @ W 6 , ? T /str 在 .debug_info 中运用的字符串N 6 I l /
.dn ! |ebug_types 类型描绘

常用的标记K y ` G 2 ^ P r a与特色如下:

数据列 信息阐明
DW_TAG_class_type 表明类称号和类型信息
DW_TAG_structf ! & g n Y y 6 7ure_type 表明结构称号和类型信息
DW_TAG_union_type 表明联合称号和F o A w w O B类型信息
DW_TAG_enum; W i 2 & ! |eration_type 表明枚举称号和类型信息
DW_TAG_typedef 表明 type7 q * T C W *def 的_ – H K 3 J称号和类型信息
DW_TAG_arr3 = 7 + W }ay_type 表明数组称号和类型信息
DW_TAG_subrange_type 表明数组的巨细信息
DW_/ s STAG_inheritance 表明承继的类称号和类型A U D信息
DW_TAG_member 表明b f B K 5 j H类的成员. _ 0
DW_d @ r { 8 g |TAG_subprogram 表明函数的称号信息
DW_TAG_formal_parameter 表明函数的参数信{ p – n ) 2 p % u
DW_TAG_name 表明称号字符串
DW_TAG_type 表明类型信息
DW_TAG_art0 a P Wifical 在创立时由编译程序设置
DW_TAGv u p y ! V_sibling 表明兄弟方位信息
DW_TAG_data_memver_location N % W 3 J 7明方位信息
DW_TAG_virtuality 在虚拟时设置

简略看一个 DWARF 的比方:将测试工程的 .dSYX L ,M 文件夹下的 DWARF 文件用下面指令解析

dwarfdump -F --debug-info Tej k $ O . k % ` Ost.app.dSYM/Conk t Jtents/B 8 X XResources/DWARF/Tes& ( 1 K ) Q 8t > debug-info.txt

翻开如下

Test.app.dSYM/Contents/Resourcu ~ g ` U . 2es/DWARF/Test:	file fO g - cormat Mach-O arm64
.debug_info contents:
0x00000000: Compile Unit: length = 0x0000004f version = 0x0004 abbr_offset = 0x0000 addr_size = 0x08 (next unit at 0x00000053)
0x0000000b: DW_TAG_compile_unit
DW_ATM r i 0 ,_producer [DW_FORM_strp]	("A) L A v ; d tpple clang version 11.0.3 (clang-1103.0.32.62)")
DW_AT_language [DF J 6 P }W_FORM_data2]	(DW_LANG_ObjC)
DW_AT_nu L ~ ) 5 / = . xame [DW_FORM_strp]	("_Builtin_stddef_max_align_t")
DW_AT_stmt_list [DW_FORM_sec_offset]	(0x00000000)
DW_AT_comp_dir [DW_FORM_strp]	("/Users/lbp/DesktoT y t tp/Test")
DW_AT_APPLE2 l F s O_major_runtime_vers [DW_FORM_t ^ | m Odata1]	(0x02)
DW_AT_GNU_dwo_id [DW_FORM! p b z T_data8]	(0x392b5344d415340c)
0x00000027:   DW_TAG_m? # # ] 8 ( c Godule
DW_AT_name [DW_FORM_strp]	("_Builtin_stddef_max_align_t")
DW_AT_LLVM_config_macros [DW_FORM G v_strp]~ ^ G & = : X = y	(""-DDEBUG=1" "-DOBJC_OLD_DISPATCH_PROTOTYP# y j T = `ES=1"")
DW_AT_LLVM_include_path [DW_FORM_strp]	("/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.3/include")
DW_AT_LLVM_isysroot [DW_FORM_strp]	("/Applications/Xcode.app/Contents/Developer/Platform[ d 2 c 7 e * 9s/iPhoneOS# i ? d G.platform/Developer/S3 G N G D 9 ] ; YDKs/iPhoneOS13.5.sdk")
0x0000003~ Y K l ?8:     DW_TAG_typedef
DW_AT_type [DW_FORM_ref4]	(0x0000004b "long double")
DW_AT_name [DW_FORM_strp]	("max_align_t")
DW_AT_decl_file [DW_FORM_data1]	("/Applications/Xc( : i x zode.] W ) Papp/Contents/Developer/Toolchain& t 4 o @ ? {s/XcodeDefault.x% d * w ; _ K & Vctoo( ^ c { = { S Mlchain/usr/lib/clang/11.D z P # O H0.3/G 6 P }include/__stddef_max_align_t.h")
DW_AT_4 u j I Odecl_line [DW_FORM_data1]	(16)
0x00000043:     DW_( w = | + m ?TAG_imported_declaration
DW_AT_decM @ S f H n r S Pl_file [DW_FORM_data1]	("/Applicationsu 4 { o 3/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.3/include/__I b 3 . @ B P Rstddef_max_align_t.h")
DW_AT_decl_line [DW_FORM_data1]	(27)
DW_AT_importr G , h g w w [DW_FORM_ref9 % o_addr]	(L 6 v0x0000000000000027)
0x0000004a:     NULL
0x0000004b:   DW_TAG_base_type
DW_AT_name [DW_FORM_strp]	("long double")
DW_AT_encoding [DW_FORM_data1]	(DW_ATE_float)
DW_AT_byte_size [DW_FORM_data1]	(0x08)
0x00000052:   NULL
0x00000053: Compile Unit: length = 0x000183dc version = 0x0004 abb} 6 ;r_offset = 0x0000 addr_size = 0x08 (next unit at 0x00018433)
0x0000005e: DW_TAG_compile_unit
DW_U ]  3AT_producer [DW_FORM_str9 5 cp]	("[ H [ ^ V 2 z F NA$ C ! g * ` apple clang version 11.0.3 (clang-1103.0.32.62)")
DW_AT_language [DW_FORM_data2]	(DW_LANGt T 3_ObjC)
DW_AT_name [DWI 1 $ ! p_FORM_strp]	("Darwin")
DW_AT_stmt_list [DW_FORM_sec_offset]	(W ~ : 30x000000a7)
DW_AT_comp_dir [DW_FORM_strp]	("/Users/lbp/Desktop/Test")
DW_AT_APPLE_major_runtime_vers [DW_FORM_data1]	(0x02)
DW1 ? u w x 5 r u |_AT_GNUS 0 D - p ~ @_dwo_id [DW_FORM_data8]	(0xd / c Ba4a1d3393l @ ) 4 x @ v79e18a5)
0x0000007a:   DW_TAG_modn , H 6 J uule
DW_AT_name [DW_FORM_strp]	("Darwin")
DW_$ M f e m [ E 4AT_LLVM_config_macros [DW_FORM_strp]	(""-DDEBUG=1" "-DOBJC_OLD_DISPATCH_PROTOTYPES=1"")
DW_AT_LLVM_include_path [DW_q b - ( T T YFORM_strp]	("/Applicati( 0 ` ^ Q } F r lons/Xcod| & @ ) B -  F 2e.app/Contents/_ e ! 4Deves q @ g - * ploper/Platforms/iPhoneOS.platform/Developer/SB ! 1 3DKs4 8  v/iPhoneOS13.5.sdk/usr/include")
DW_AT_LLVM_isysroot [DW_FORM_strp]	(@ 5 ~"/Appli& / j w p D m 2cations/Xcode.app/Contents/DevelopA 1 d 6 r ( 9 h ;er/Platforms/iPhoneOS.platformv ! $ f 4 t/Developer/SDKs/iPhoneOS13.5.sdk")
0x0000008b:     DW_TAG_module
DW_AT_name [DW_FORM_strp]	("C")
DW_AT_LLVM_config_macros [DW_FORM_strp]	(""-DDEBUG=1" "-DOBJC_OLD_DISPATCH_PROTOTYPES=1"")
DW_AT_LLVM_include_path [DW_FORM_strp]	("/Applications/Xcop s = ) 0 8de.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/includ# T m ge")
DW_AT_LLVM_isysroot [DW_FORM_strp]	("/Applications/Xcode.app/CoF i x . ontents/Developer/Platforms/iPhoneOS.platform/Devel| i b G * Zoper/SDKs/iPhoneOS13.5.sdk")
0x0000009c:       DW_TAG_mo$ X _ 7dule
DW_AT_name [DW_v d w L JFORM_strp]	("fenv")
DW_AT_LLVM_con% J Sfig_macros [DW_FORM~ 9 m % U P_strp]	(= M L 0 ` # K"f $ C ) G M ~ ) &"-DDEBUG=1" "-DOBJC_OLD_DISA l NPATCH_PROTOTYPES=1"")
DW_AT_LLVM_include_path [DW_FORM_strp]	("/Applications/Xcode.app/Contents/Developer/Platforms/iPA X  B u m ; = 5honeOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include")
DW_AT_LLVM_isysroot [DW_FORM_strp]	(7 T N"/Applications/Xcode.app/Contents/Developer/P+ T O )latforms/iPhoneOS.platform/DeE - H r wveloper/SDKs/iPhoneOS13 q n3.5.sdk")
0x000000ad:         DW_TAG_enumeration_type
DW_AT_type [DW_FORM_ref4]	(0x0001/ o R N7276 "unsigned int")
DW_AT_byte_size [DW_FORM_data1]	(0x04)
DW_AT_decl_file [DW_FORM_data1]	("/ApplicL y Y , Pations/Xcode.app/Contents/Developer/m : F 1 XPlatforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/N q b @ + x F y &usr/include/fenv.h")
DW_AT_decl_line [DW_FORM_data1]	(154)
0x000000b5:           DW_TAG_enumeraw 9 V ) jtor
DW_AT_name [DW_FORM_stro } ep]	("__fpcr_trap_invaB U plid")
DW_AT_const_value [DW_A o O f t q W ( pFORM_udata]	(256)
0x000000bc:           DW_TAG_enumerator
DW_AT_name [DW_FORM_strp]	("__fpcr_tr6 ] % ~ap_divbyzero")
DW_AT_const_value [DW_FORM_udata]	(512)
0x000000c3:           DW_TAG_enumeratT i Y Yor
DW_AT_name [DW_FORM_strp]	("__fpcr_trap_overflow")
DW_AT_const_value [DW_FORM_udata]	(1024)
0x000000ca:           DW_TAG_enumerator
DW_AT_name [D ! YW_FORM_stu  5 R Lrp]	("__fpcr_trap_underflow")
// ......
0x000466ee:   DW_TAG_subprogram
DW_AT_name [DW_FORM_strp]	("CFBridgingRetain")
DW_AT_decl_file [DW_FORM_data1]	("/Applications/Xcode.app/Cont/ j ? W 8 z 6 )ents/Developer/Platforms/iPhoneOS.platform/Developer/SDKsY n ` ( 8 D/iPz : ! $ : C b RhoneOS13{ @ { N ! = {.5.sdk/System/m t OLibrary/Frameworks/Foundation.framework/Headers/NSObject.h"u d ( M)
DW_AT_decl_line [DW_FORM_data1]	(s [ J : 4 H g K105)
DW_AT_prototyped [DW_F, 6 R h ~ iORM_flag_present]	(true)
DW_AT_type [DW_FORM_ref_addr]	(0x0000000000019155 "CFTypeRef")
DW_AT_in= N Uline7 O = * 5 [DW_FORM_data1]	(DW_INL_inlined)
0x000466fa:     DW_TAG_for7 1 + { F S E rmal_parav f - ; g Ameter
DW_AT_name [DW_FORM_strp]	(B g G b $ I + _ %"X")
DW_AT_decl_file [DW_FORM_data1) $ * % H]	("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Develop9 a 0 ] ? i q 5er/SDKs/iPhoneOS13.5.sdk/System/Library/Frameworks W W W * A .s/Foundation.framework/Headersh 3 5 l/NSObject.h")
DW_AT_decl_line [DW_FORM_, = l # | : Zdata1]j _ I K n b i	(105)
DW_AT_type [DW_FORo | C O 9 A / ( 7M_ref4]	(0x00046706 "id")
0x00046705:     NULL
0x00046706:   DW_TAG_typedef
DW_AT_type [DW_FORM_ref4]	(0x00046711 "objc_Z y Mobject*")
DW_AT_name [DW_FORM_strp]	("id")
DW_AT_decl_file [DW_FORM_de B w L oata1]	("/Users/lbpU 4 m n P P G C P/Desktop/Test/Test/NetworkAPM/NSURLResponse+cm_FetchStatusLineFromCFN$ q Y h Ketwork.m")
DW_AT_decl_line [DW_FORM_data1]	(44)
0x0004671Z t 8 81:   DW_TAG_pointer_type
DW_AT_type [DW_FORM_ref4]Y w f &	(0x00046716 "objc_object")
0x00046716:   DW_TAG_str~ Y R ! x i cucture_type
DW_AT_name [DW_FORM_strp]	("objc_object")
DW_AT_b% %  ! yte_size [DW_FORM_data1]	(0x00)
0x0004671c:     DW_TAG_member
DW_AT_name [DW_FORM_strp]	("isa")
D2 X g f w g l T KW_AT_type [DW_FORM_ref4]	(0x00046727 "objc_class*")
DW_AT_data_member_location [DW_FORM_+ y ( c f b e /data1]	(0x00)V f h
// ......

这儿就不张贴全部内8 f l D容了(太7 [ 4 $ 7 *长了)。能够看到 DIE 包括了Y [ . 5 U 6 – @函数开端地址、完毕地址、函数名、文件名、所内行数,关于给定的地址,找到函数开端地址、完毕地址之间包括该抵抗的 DIE,则能够复原函数名和文件名信息。

debug_line 能够复原文件行数等信息

dwarfdump -F --debug-line Test.app.dSYM/Contents/Resources/Y C x i /DWARF/Test > debug-inline.txt: . ] `

贴部分信息

Test.app.dSYM/Contents/Resources/DWARF/Test:	file format Mach-O) X  Y , arm* j A K u A B ) l64
.debug_line contents:
debug_line[0x00000000]
Line table prologue:
total_length: 0x000000a3
version: 4
prolo# y _ ; @ } Mgue_length: 0x0000009a
min_inst_length: 1
max_ops_per_inst: 1
deff C {ault_isz W @_stmt: 1
line_base0 N x W m Q P ~ L: -5
line_range: 14
opcode_basi h Q We: 13
standard_opcode_le5 * P ^ngths[DW_LNS_copy] = 0
standard_opcode_lengths[Dz X 3 BW_LNS_advance_pc] = 1
standard_opcode_lengths[DW_LNS_advance_line] = 1
standard_opcode_lengths[DW_LNS_set_file] = 1
standard_opcode_lengths[DW_LNS_set_column] = 1
standard_opcode_lengths[DA r DW_LN6 H 7 . 4 R Z _S_negate_stmt] = 0
standard_opcode_lengths[DW_LNS_set_basic_bloc+ C / O J , 5 b }k] = 0
standard_opcode_lengths[DW_LNS_const_add_pc] = 0
standard_opcode_lengths[DW_LNS_fixed_advance_pc] = 1
standard_oB Q @pcode_lengths[DW_LNS_set_prologue_end] = 0
standar! [ w 1d_opcode_lengths[DW_L{ P n t [NS_set_epilogue_begin] = 0
standard_opcode_lengths[DW_LNS_set_isa] = 1
include_directories[  1] = "/Applications/Xcode.app/Contents/DeveloperK $ { B/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.3/include"
file_names[  1]:
name: "__stddef_max_align_t.h"A P K P  y
dir_index: 1
mod_time: 0x00000000
length: 0x00000000
Address            Li, Y _ T = k 0 e 9ne   Column File   ISA DiscriminatorI % q H P U E Flags
------------------ ------ ------ ------ --- ------------- -------------
0x0000000000000000      1      0      1   0             0  is_stmt end_sequence
debug_line[0x000000a7]
Line table prologue:
total_length: 0x0000230a
version: 4
prologue_length: 0x00002301
min_inst_lenG r Igthw $ N & P { 4: 1
max_ops_per_inst: 1
default_is_stmt: 1
line_base: -5
line_range: 14
opcode_base: 13
standard_opcode_lengths[DW_LNS_copy] = 0
standard_opcode_lengths[DW_LNS_advance_pc] = 1
standard_opcode_lengths[DW_LNS_advance_line] = 1
standard_opcode_lengF 7 ^ y U ` Y EtE { -hs[DW_LNS_set_file] = 1
standard_opcode_lengths[DW_LNS_set_column] = 1
standard_opco` z . z d ] W 1de_lengths[DW_LNS_negate_stmt] = 0
standard_oe K 8 k ? 2 k bpcod Q Q de_lengths[D 8 , I P i d { W_LNS_set_basic_blocS : % y F h Y  ikH Y F C o k J z +] = 0
stand[ ( #ard_opcode_lengths[DW_LNS_const_add_pc] = 0
standard_opcode_lengths[DW_LNS_fixed_advance_pc] = 1
standa{ 3 c + j 1 c J erd_opcode_lengths[DW_LNS_set_prologue_end]r i j j z ? G = 0
stanj C V G W W Ddard_opcode_lengths[DW_LNS_set_epilogue_begin] = 0
standa_ B & N ] ! Brd_opcode_lengths[DW_` h %LNS_setG Q [ C s t_isa] = 1
include_directories[  1] = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platX h : Pform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include"
include_directories[  2] = "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctooH : elchain/usr/lib/clangI 9 [/11.0.3/include"
includ) G 4 b 7 ` a , de_directorz $ ~ies[  3] = "/Applicat2 / S X Lions/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneO# H [  9 H KS13.5.sdk/usr/include/sy) 0 r ( _s"
include& 7 ) ] O } k ? j_directories[H C *  4] = "/Applt u A # j v . q ^ications/Xcode.appj 8 -/Contents/Developer/Platforms/iPhoneR m # - -OS.platform/Developer/SDKs/iPhoneOS1n ` e T n 4 B3.5.sdk/usr/include/machv } @ w N"
include_M ? $ t / s vdirectories[  5] = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platformE O G c ; B 9 p/DM ! - G A % s 4 3evelope% } A s 9 j / |r/SDKs/iPhoneOS13.5.sdk/usr/include/libkern"J Z t f c z K 4
include_directories[  6] = ") | u a ? I P/Applications/Xcode.app/ContA k D l T oents/Developer2 T ? T v/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/us4 0 ! 8 * % 4 U vr/include/N ( 4 Aarchitecture"
include_directories[  7] = "/Applications/Xcode.app/ConR q U { G ( # w Vtents/= ^ w e 8 yDeveloper/Platforms/iPhoneOS.platform/Dev. $ i ! u / neloper/SDKs/iPhoneOS13.5.sdk/usr/include/sys/_types"
include_directo# C m v dries[  8] = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/includer . t 4 h x ,/_types"
include_directories[  9] = "/Applicat& & l tions/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include/8 N 9arm"
include_directories[ 10] = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneV $  @ dOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include/sys/_pthread"
i` T i i 8 2 B t unclude_directories[ 11] = "/Applica h c , Ctions/Xcode.app/Contents/Developerm R P h c 6/Platforms/iPhoneOS.platform/Developer/SDKs/iPhone, D 8 n d j V ?OS13.5.sdk/usr/incX Z H i B a [lude/mach/arm"
include_8 d pdirectories[ 12] = "/Applications/Xcode.app/Contn - h (ents/Developer/Platforms/iPhoneOS.platforg d v Fm/Developer/SDD @ Z nKs/iPhoneOS13.5.sdk/usr/include/? Y ,libkern/arm"
include_M z S 5 { 5 Y 4 rdirectories[ 13] = "/Applications/Xcode.aY { , } 1 / opp/ContentA [ ; I = a Ws/Developer/Platforms/iPhoneOS.platf$ W p %  P corm/Developer/SDKs/iPhoneOS13.5.sdk/usr/include/uue d c _ V ! l cid"
include_directorik t K 5 p s pes[ 14] = "/Applications/Xcode.app/Contents/Developer/Platformf d # ss/iPhoneOS.platfo, A - 0 orm/D- 8 Feveloper/SDKs/iPhoneOS13.5.sdk/usr/include/netinet"
include_directories[ 15] = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include/netinet6"
include_directories[ 16] = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include/net"
include_directorim c T ` qes[ 17] = "/Ap* _ tplicaG f v 2 a b btions/Xcode.app/Contents/W d _ 0 a 9Developer/Platfo~ ` T A jrms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include/pthread"
include_directoriesa P ) v + y Z s[ 18] = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDA T X 3 g 1 aKs/w @ iiPhoneOS13.5.sdk/usr/incluL J : Ide/mach_de# y n O P bbug"
include_directories[ 19] = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usrG K w K H/include/os"
include_directories[ 20] = "/Applications/Xcode.app/Contents/Develo` j l I P g & ~per/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.1 + 9 I5.sdk/usr/include/malloc"
include_directories[ 21] = "/Applx : [ W ; / a nications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/includes z | 1 1 ~/bsm"
includen T R q_directories[ 22] = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhone( ! N J q ! )OS.` # ^ `platformI V * w } / E z/Developer/SDKs/iPhoneOS13.5.sdk/usr/include/machine"
include_directoriQ @ 8 ; 0 x | Jes[ 23] = "/Applications/Xcode.app/Contents/Developer/Platfor( $ 1ms. F R v / 5 G/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/uq R : I E = L jsr/include/mach/machine"
include_direo c p Ectories[ 24] = "/Applications/Xcode.H - . 9 A sapp/Contents/Developer/Platforms/iPhoneOS.platform/Develope1 A , 7 B x  Or/SDKs/iPhoneOS13.5.sdd O F 9 4 S M @k/usr/include/secure"
include_directories[ 25] = "/Applicationsw 9 0 5/Xcodeg 3 ; u s a.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include/xlocale"
include_C C 0 J z } E Wdirectories[ 26V J w R } t O] = "/Applications/Xcode.app/Contents/Developer/Platforms) _ 2/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.5.sdk/usr/include/arpa"
file_names[  1]:
name: "fenv.h"
dir_index: 1
mod_time: 0x00000000
lengW v jth: 0x00000000
file_names[  2]:
name: "stdatomic.h[ % ] O = _ G"
dir_index: 2
mod_time: 0x00000000
length: 0x00000000
file_names[  3]:
name: "wait.h"
dir_index: 3
mod_time: 0x00000000
length: 0x00000000
// ......
Address            Line   Column File   ISA Discriminator Flags
------------------ -----! R A- ------ ------ --- ------------- -------------
0x000000010000b588i s $ n % )     14      0      2   0             0  is_stmt
0x000000010000b5b4     16      5      2   0             0  is_stmt prologue_end
0x000000010000b5d0     17     11      2   0             0  is_stmt
0x000000010000b5d4      0      0      2   0             0
0x000000010000b5d8     17      5      2   0             0
0x000000010000b5dc     17     11      2   0             0
0x000000010000b5e8     18      1      2   0             0  is_stmt
0x000000010000b608     20      0      2   0             0  is_stmt
0x000000010000b61c     22      5      2   0             0  is_stmt prologue_eny g D   W Id
0x000000010000b628     23      5      2   0             0  is_stmt
0x000000010000b644     24      1      2   0             0  is_stmt
0x000000010000b650     15      0      1   0             0  is_stmt
0x000000010000b65c     15     41      1   0             0  is_stmt prologue_end
0x00000001N b +0000b66c     11      0      2   0             0  is_stmt
0x000000010000b680     11     17      2   0             0  is_stmt prologue_end
0x000000010000b6a4     11     17      2   0             0  is_stmV y y B ! xt end_sequence
debug_line[0x0000def9]
Line table prologue:
total_length: 0x0000015a
veri 6 B 8 ; x W V )sio[ H )n: 4
prologue_ln # W ( ; f _ A eength: 0x000000eb
mi& I ; k Z * j i Zn_inst_length: 1
max_ops_per~ ! | : o 5 W V B_ins o g  k t 4t: 1
defaulv K } 1 a } . Xt_i. Q V X l @ 5s_stmt: 1
line_base: -5
line_range: 14
opcode_base: 13
standard_` E ropcode_lengths[DW_Q 3 ^ - I % _ :LNS_copy] = 0
standard_opcode_lengths[DW_LNS_advance_pc] = 1
sP + ] = 2 : 2tandard_opcode_lengthsN % | N ! 9 I | X[DW_LNS_advance_line] = 1
standard_opcod# 6 E C & 5 x 0e_lengths[DW_LNS_set_file] = 1
standa* Y Y d | r B K rd_opcode_lengths[DW_LNS_set_column] = 1
standard_opco: ( ede_lengths[DW_LNS_negate_stmt5 E s] = 0
stan: 4 V 6 6 &dard_opcod4 h k S me_lengths[DW_LNS_set_basic_block] = 0
standard_opcode_lengths[DW_LNS_const_add_pc] = 0
sL H Ltandard_opcode_lengths[DW_LNS_fixed_advance_pcd J Z e] = 1
standard_opcode_lengths[DW_LNS_s( Z ` U M H d Ret_prologue_end] = 0
standard_opcode_lengths[DW_LNS_set_epilogue_begin] = 0
standard_opcode_lengths[DW_LNS_set_isa] = 1
include_directories[  1] = "Test"
include_directories[  2] = "Test/NetworkAPM"
include_directories[  3] = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.plat( a W U # Y Nform/Develd J  eoper/SDKs/iPhoneOS13.5.sdk/usr/include/obj+ e t  T 1 y u qc"
file_names[  1]:
name: "AppDelegate.h"
dir_index: 1
mod_time: 0x00000000
length: 0x00000000
file_names[  2]:
name: "JMWebS b W d - pResourceURg - A 4 +LProtocol.h"
dir_index: 2
mod_time: 0x00000000
length: 0x00000000
file_names[  3]:x C  N U } i 8 V
name: "AppDelegate.m"
dir_index: 1
mod_time: 0x00000000
length: 0x00000000
file_names[  4]:
name: "ob1 K j q [jc.h"
dir_iH = vndex: 3
mod_time: 0x00000000
length: 0x00000000
// ......

能够看到 debug_li) l F r n g % 1ne 里包括了每个代码地址对应的行数。上面贴了 AppDelegate 的部分。

4.: ? o = k v L3 symbols

在链接中,咱们将函数和变量总# l a f称为契合(Symbol),函数5 T G o K O : :名或变量名便是符号名(Symb# ; P ] ol Na` w . u ?me),咱们能够将符号看成是链接中的粘合剂,整个链接进程正是依据符号才干正确完结的。

上述文字来自《程序员的自我修养@ , I J》。所以符号便是函数、变量、类的总称。

依照类型区分,符号能够分为三类:

  • 大局符号:目标文件外可见的符号,能够被其他目标文件所引证,或许需求其他目标文件界说
  • 部分符号:只在目标文件内可见的符号,指只在目标文件内可见的函数和变量
  • 调试符号:包括行号信息的调试符号信3 p a 2 :息,行号信息记载了函数和变量对应的文件和文件行号。

符号表(Symbol Table):是内存地址与函数名、文件名、行号的1 n R i + { ] a Y映射表。/ + e每个界说的符号都有一个对应的值得,叫做符号值(SymX r J a }bol Value),关于变量和函数来说,符号值便是地址,符号表组成如下

<开端地址> <完毕地址> <函数> [<文件名:行号>]

4.4 怎么获取地址?

image 加载的时分r U v I ) m P会进行相对基地址进行~ I k z r重定位,而且每次加载的基地址都不. 6 +相同,函数栈 frame 的地址是重定位后的绝对地址,咱们要的是重定位前的相对地址。

Binary Images

拿测试工程的 crash 日志举比k F 5 0 q B ] p 1方,翻开u ( X C贴部分 Binary Images 内容

// ...
Binary Images:
0x102fe0000 -K j ; 0 Z w I & c 0x102ff3fff Test arma H #64  <37eaa57df2523d95969e47a9a1d69ce5> /var/containers/Bundle/Application/643F0DFE-A710-4136-A278-A89D780B7208/Test.app/Test
0x1j Z O n , ? # R 1030e0000 - 0x1030ebfff libobjcV 4 B 8 o (-trampolines.dylib arm64  <A n q :181f3aa866d93165ac54344385ac6e1dj t # h {> /usr/lib/libobj0 ] v - { Zc-trampolines.dylib
0x103204000 - 0x103267fff dyld arm64  <6f1c86b640a3352a8529bca213946dK 9 O ; D l / L hd5> /usr/lib/dyld
0x189a78000 - 0x189a8efff libsystem_trace.dylib b m arm64  <b7477df8f6ab3b2b9275ad23c6cc0b75> /usr/lib/system/libsystem_trace.d[ a & Q ? W 8 . tylib
// ...

能够看到 Crash 日志的 Binary Images 包括每个 Image 的加载开端地址、完毕地址、image 称号、arm 架构、uuid、image– A 8 + F B 7 ( 2 途径。

crash 日志中的信息

Last Exception Backtrace:
// ...
5   Test                          	0x102fe592c -[ViewController testMonitorCrG . A z kash] + 22828 (ViewController.mm:58)
Binary Images:
0x102fe0000 - 0x102ff3fff Test arm64  <37eaa57df2523d95969e47a9a1d69ce5> /var/containers/a A o 8 GBundle/Application/6= m C O + E A43g e & N 0 H  :F0DFE-E | ! r ) 2 RA710-4136-A278-A89D780B7208/Test.app/T{ p D ; ? dest

所以 frn l P K `ame 5 的相对地址为 0x102fe592c - 0x102fe0000。再运用 指令能够复原` p ~符号信息。

运用 atos 来解析,0x102fe0000 为 image 加载的开{ X j C端地址,0x102fe592c 为 frame 需求复原的地址。

atos -o Test.a$ 2 K 6 Hpp.dSYM/Contents/Resources/DWARF/Test-arch arm64 -lU = i z / c d 0x102- u  Y [ 5fe0000 0x102fe592c

4.5 UU@ , gID

  • crash 文件的 UUF Q zID

    grep --after-context=2 "Binary Images:" *.craG l ! / _ ;sh
    
    Test  5-28-20, 7-47 PM.w 4 M $ 4 F ScZ ~ # 6rasr + ` X D { + |h:Binary Images:
    Test  5-28s J J D ! #-20, 7-47 PM.crash-0x102fe0000 - 0xO ? 6 | J -102ff3fff Test arm6^ ! D c  8 O & B4  <% 1 2 % A O d _;37eaa57df2523d95969e47a9a1d691 h % zce5> /var/co7 X Bntainers/Bundle/Application/643F0DFE-A710-4136-A27/ G g 4 f8-A89D780B7208/Test.app/Test
    Test  5-28; v u-20, 7-47 PM.crash-0x1030e0000 - 0x1030ebfff libobjc-trampolines.dylib arm64  <181f3aa866d96 S ! d3165ac54344385ac6e1d> /usr/lib/libobjc-trh y N e g r Yampolines.dylib
    --
    Test.cras] n / N Y ^ f 6 Eh:Binary Imag^ 6 wes:
    T( n V 6est.crash-0x102fe0000 - 0x102ff3fff Test arm64  <R + e W ?;37eaa57df2523d95969e47a9a1d69ce5> /var/con{ T S F : E Rtainers/Bun} X Vdle/Application/643F0DFE-A710-4136-A278-A89D780B7208/Test.app/Test
    Test.I 9 E q ! 9 mcrash-0x1030e0000 - 0x1030ebfff libobjc-tray p S } 6mpolines.dylib arm64  <181f3aa866d93165ac54344385ac6e1d>? Z / R k; /usr/lib/libobjc-trampolines.dz . 0 D (y( U G O E = Ylib
    

    Test App 的 UUID 为 37eaa57X 7 R vdf252i j ! f :3d2 $ t b 9 f95969e47a9a1d69ce5.

  • .dSYM 文件的 UUID

    dwarfdumc x H q } ip --uuid Test.app.dSYM
    

    成果为

    UUID: 37EAA57D-F252-3D95-969E-47A9A1D69CE5 (arm64) Test.app.dSYM/Cont@ s !ents/Resources/DWARF/Test
    
  • app 的3 / M Y UUID

    dwarfdump --uuid Test.app/Test
    

    成果为g . B h q 3

    UUID: 37EAA57D-+ % b lF252-3D95-969E-47A9A1D69CE5 (arm64: + @  I @ C I I) Tev p % = g 8 (st.app/Test
    

4.V g p H K G6 符号化(解析 Crash 日志)

上述篇幅剖析了怎么捕获各种类型的 crash,Aq 7 } Z V P 0pp 在用户手中咱们经过技能手法能够获取 crash 案发现场信息并结合一定的机制去上报,但是这种仓库是十六进制的地址,无法定位问题,所I m n 6 c * G以需求做符号化处理。

上面也阐明晰.dSYM 文) V ) ,件 的效果,W | c c A A .过符号地址结合 dS2 0 } UYM 文件来复原文件名、所内行、函数名,这个进程叫符号化。但是 .dSYM 文件有必要和 crash log 文件的 bundle id、version 严格对应。

获取v ! ~ E * Crash 日志能够经过 Xcode -> Window -> Devices and Simulators 挑选对应设备,找到 Crash 日志文件,依据时刻和 App 称号定位。

app 和 .dSYM 文件能够经过打包的产物得到,途径为 ~/Library/Developer/Xcode/Archives

解析办法一般有2种:

  • 运用 symbolicatecrash

    symbolicatecrash 是 Xcode 自带的 crash 日志剖析东西,先确定地点途径,在终端履行下面的指令

    find /Applications/Xcode.app -name symbolicatecrash -type f
    

    会回来几个途径,找到 iu e A - = d X j RPhoneSimulator.platform 地点那一行

    /Ap: { Hplicat( } k $ z S $ . 1ions/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Privat- $ ! h 9 B _ S VeFrameH @ j , L & R `w^ U Yorks/DVTFoundati4 | &on.framework/symbolicatecrash
    

    将 sym3 S ( = u . e T :bolicatecrash 拷贝到指定文件夹下(保存了 app、dSYM、crash 文件的文件夹)

    履行指令

    ./symbolicatecrash Test.crash Test.dSYM > Test.crash
    

    第一次做这事儿应该8 d a 1 a K B +会报错 Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 69.,处理计划:在终端履行下面指令

    export DEVELOPER_DIR=/AF w _ B , Bpplications/Xcode.app/Contents/Developer
    
  • 运用 atos

    差异于 symboliE / A ! K ) * +catecra4 P b ( 9 . D ,sh,atosw X 7 较为灵活,只要 .crash.dSYM 或许 .crash} y { .app 文件对应即可。

    用法如下,-l 最终跟得是符号地址

    xcrun atos -o Test.app.dSYM/Contents/Resources/DWARF/Test -ark h H Ych armv7 -l 0x1023c592c
    

    也能够解析 .app 文件(不存在 .j ) G 7 K `dSYM 文件),其间xxx为段地址,xx为偏移地址

    atos -arch architecture -o binary -lo C | 8 k  xxx xx
    

由于咱们的 App 或许有许多,每个 App 在用S Z P W v 4 ,户手中或许是不同的版别,所以在 APM 阻拦之后需求符号化的时分需求将 crash 文件和 .dSYM 文件一一对应,才干正确符号化,对应的原则便是 UUp j H V w m , fID 一致。

4.7 体= $ + * q N系库符号化解析

咱们每次真机衔接 Xcode 运转程序,会提示等候,其实体系为了仓库解析o R m n i F g l u,都会把当时版别的体系符号库主动导入( { T [ @ `/Users/你自己的用户名/Library/Developer/Xcode/iOS DeviceSupport 目录下安装了一大堆体系库的符号化文件。你能够访问下面目录看看

/Users/你自己的用户名/Library/Developer/Xcode~ 2 2 j/iOS DeviceSupport/
带你打造一套 APM 监控系统(二)

5. 服务端处理

5.1 ELK 日志体系

业界规划日志监控体9 6 V系一般会选用依据 ELK 技能。ELK 是 Elasticsearch、Logstash、Kibana 三个开源结构缩写。Elasticsearch 是一个分布式、经过 Ru j ` u . [estful 办法进行交互的近实时查找的渠道结构。Logstash 是一个中心数据流引擎,用于从不同目标(文件/数据存储/MQ)搜集不同格局的数据,经过过滤后支撑输出到不同意图地(文件/MQ/Redis/El/ Q hasticsSearch/Kafka)。Kibana 能够将 Elasticserarch 的数据经过友爱的页面展现出来,供给可视化剖析功用。所以 ELK 能够建立一个高效、企业级的日志剖析体系。

前期单体运用年代,简直运用的一切功用都在一台机器上运转,出了问题,运维人员翻开终端输入指令直接检查体系日志,进而定位问题、处理问题。跟着体系的功用越来越杂乱,用户体量越来越大,单体运用简直很难满意需求,所以技t t J # C i t u能架构迭代了,经过水平拓展来支撑巨大的用户量9 y ~ ; = 2 % e J,将单体运用进行拆分为多个运用,每个运用选用集群办法布置,负载均衡控制调度,假如某个子模块发生问题,去找这台服务器上终端找日志剖析吗?显然台落后,所以日志办理渠道便应运而生。经过 Logstash 去搜集剖析每台服务器的日志文件,然后依照界说的正则模版过滤后传输到 Kafka 或 Redis,然后由另一个 Logstash 从 Kafka 或 Redis 上读取日志存储到 ES 中创立索引,最终经过 Kibana 进行可视化剖析。此外能够= _ t { I将搜集到的数据进行数据剖析,做更进一步的维护和决议计划。

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

上图展现了一个 ELK 的日志架构图。简略阐明下:

  • Logstash 和 ES 之前存在一个 Kafka 层,由于 Logstash 是架起在数据资源服务器上,将搜集到的数据进行实时过滤,过滤需求耗费时刻和内存,所以存在 Kafka,起到了数据缓冲存储效果,由于 K– ` w A w k m , 0afka 具有十分超卓的读写性能。
  • 再一步便是 Logstash 从 Kafka 里边进行读取数据,将% @ h S , [ Y数据过c F w a p滤、处理,将成果传输到 ES
  • 这个规划不光性能好、耦合低,还具有可拓展性。比方能够从 n 个不同的 Logstash 上读取传输到 n 个 Kafka 上,再由 n 个 Logstash 过滤处理。日志来源能够是 m9 O | 个,比方 App 日志、Tomcat 日志、Nginx 日志等等

下图贴一@ p | 3个 Elasticsearch 社区分享h o 3的一个 “Elastic APM 着手实战”主题的内容截图。

带你打造一套 APM 监控系统(二)
5.2 服务侧

Crash log 一致入库 Kibana 时是没有符号化的,所以需求符号化处理,以便利定位问题、crash 发生报表和后续处理。

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

所以整个流程便是:客户@ ( 5 y Z ` +端 APM SDK 搜集 crash log -> Kafka 存储 -> Mac 机履行定时使命符号化 -&gta f r } A; 数据回传 Kafka -> 产品侧(显示端)对数据进A h U G A行分类、报表、报警等操作k n l i ] f d V %

由于公司的产品线有多条,相应的 Ap^ u ~p 有多个,用户运用t w @ u l | , M的 App 版别也各不相同,所以 crash 日志剖析有必要要有正确的 .dSYM 文件,那么多 App 的不同版别,主动4 [ T H K ] h化就变得十分重要了。

主动化有2种手法,规模小一点的公司或许图省劲,能够在 Xcode中 增加 runScri8 K – F b 9pt 脚本代码来主动在 release 形式下上传dSYM)。

由于咱们公司有自己的一套体系,wax-cli,能够一起办理 iOS SDK、iOS App、Android SDK、Android App、Node、React、React Native 工程项意图初始化、依赖办理、构建(持续集e k ` k / d U成、Unit Test、Lint、统跳检测)、测试、打包、布置、动态才干(热更新、统跳路由下发)等才干于一身。能够依据各个阶段做才干的刺进,所以能够在调用打包后在打包机上传 .dSYM 文件到七牛云存储(规则能够是以 AppName + Version 为 key,value 为 .dSYM~ o F ! O C 文件)。

现在许多架构规划都是微服务,至于为什么选微服务,不在本文领域。所以 crash 日志的符号化被规划为3 U { W ^ % $ ~一个微服务。E e w r D = M架构图如下

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

阐明:

  • Symbolication Service 作为整个监控体系 Prism 的一个组成部分,是专心于 crash report 符号化的微服务。

  • 接纳来自 mass 的包括预处理过的 crash report 和 dsym index 的恳求,从七牛拉取对应的 dsym,对 crash red ; e O E S _port 做符号^ d 3 = r b Z !化解析,核算 hash,并将 hash 响应给 mv k r j P M Iass。

  • 接纳来自 Prism 办理体系的包括原始 crash report 和 dsym index 的恳求,从七牛拉取对应的 dsym,! 6 0 C ) Q对crash report 做符号化解析,并将符号化的 crash report 响应给 Prism 办理体系。

  • Mass 是一个通用的数据处理( G j : – 4 J 4 O(流式/批式)和使命调度结构

  • candle 是一个打包体系,上面说的 wax-cli 有个才干便是打包,其实便是调用的 candle 体系的打包构建才干。会依据项意图特色,挑选合适的打包机(打包渠s O J z ^ ] j道是维护了多个打包使命,不同使命依据特色被派发到不同的打包机上,使命详情页能够看到依赖# . q @ K的下载_ [ W ; 、编译、运转进程v o 6 A Z 1 P )等,打包好的产物包括二进制包、下载二维码等等)

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

其间符号化服务是大前端背景下大前端团? , 9 t u队的产物,所以是 NodeJS 完结的。iOS 的符号化机器是 双核的 Mac minn Q q % g , ^ l i,这就需求做试k N } D @ b验测评究竟需b T c ;求敞开几个 wor` ( O W 2 c J u )ker 进程做符号化服务。成果是双进程处理 crash log,比单进程功率高近一倍,而四进程比双进程功率提高不明显,契合双核A H 9 X S p I R mac mini 的特色。所以敞开两个 worker 进程做符号化处理。

下图是完好规划图

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

简略 G @ C ; W阐明下,符号化流程是一个主从形式,一台 masto s ; ] –er 机,多个 slaveW _ M P J z b q 机,m_ c = { k d 7aster 机读取 .dSYM 和 crash 成果的 cache。mass 调度符号化服务(内部2个 symbolocate worker)一起从七牛云上获取 .dSYM 文件。

体系架构图如下

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

八、 APM 小结

  1. 通常来说各个端的监控才干是不太一致的,技能完结细节也不一致。所以在技能计划评定的时分需求将监控才干对齐一致。每个才干在各个端的数据字段有必要对齐(字段个数、称号、数据类型和精度),由于 APM 自身是4 w G I )一个闭环,监控了之后需} ! M q : +符号化解析、数据收拾,进行产品化开发I V : C G /、最终需求监控大盘展现等

  2. 一些 crash 或许 ANR 等依据等级需求邮件、短信、企业内容通讯东西奉告干系人,之后快速发布版别、hot fix 等。

  3. 监控的各个才干需求做成可配置,灵活敞开关闭。

  4. 监控数据需求做内存T | 1到文件的写入处理,6 D v – @需求注意战略。监控数据需求存储数据库,o G l s V p F数据库巨细、规划规则等。存入数据库后怎么& ! g . Z上报,上6 g ( f ( X e B报机制等会在另一篇文章讲:打造一个通用、可配置的M J W %数据上报 SDK

  5. 尽量在技能评定后,将各端的技能完结写进文档中,同步[ # : / e给相关人员。比方 ANR 的完结

    /*` 0 F 4 s S
    android 端
    依据设备分级,一般超越 300ms 视为一次卡顿g ! %
    hook 体系 loop,在音讯处理前后插桩,用以核算每条W { ~ u音讯的时长
    敞, 0 c X Q开另外线程 dump 仓库,处理完毕后关闭
    */
    new Excepr t g ^ C N _ btionPu p . Y I nrocessor().init(this, new Runnable() {
    @Override
    public void run() {
    //监测卡顿
    try {
    ProxyPrinter proxyPrinter = new ProxyPrinter(PerformanceMonitor.this);
    Looper.getMainLooper().setMessageLogging(proxyPv 4 7 Frinter);
    mWeakPrinter = new WeakReferenceq 0 l y j - @<PE 8 j . jroxyPrinter>(proxyPrinter);
    } catch (F3 { b H _ ` t } kileNotFoundException e) {
    }
    }
    })
    /*
    iOS 端
    子线程经过 ping 主线程来确认主线程当时是否卡顿。
    卡顿阈值设置为 300ms,超越阈值时以为卡Q d C q N o x ;顿。
    卡顿时获取主线程的仓库* S H,并存储上传。
    */
    - (void) main() {
    while (self.cancle == NO) {
    self.isMainThreadBlocked = YES;
    dispatch_async(disp$ ] i y k U 1 Z &atch_get_main_queue(), ^{
    selC t g - )f.isMainThreadBlocked = YES;
    [sD % = l G ^ X o Xelf.semaphore singal];
    });
    [Thread sleep:300];
    if (self.isMan W g ^ j W ; J 7in{ + 0 e % pThreadBlocked) {
    [self handleMainThreadBlock];
    }
    [self.semaphore wait];
    }
    }
    
  6. 整个 APM 的架构图如下

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

    阐明:

    • 埋点 SDK,经过 sessionId 来相关日志数据
    • wax 上面V / R n a S A ? i介绍过了o ( F J v,是一种多端项目办理形式,每个 wax 项目都具有根底信G 4 ] } c o M
  7. APM 技能计划自身是跟着技能手法、剖析需求O L o P x W不断调整晋级的。上图的几个结构示意图是前期几个版别的,目前运用的是在此根底上进行了晋级和结构调整,( t X n t d ~提几Z 2 7个关键词:Hermes、Flink SQL、InfluxDB。

参考资料

  • iOS 保持界面流通的技巧
  • Call Stack
  • 关于函数调用栈(call stack)的个人了解
  • 获取任意线程调用栈的那些事
  • iOS发i & 7动时刻优化
  • WWDC2019之发动时刻与Dyld3
  • Apple-libmalloc
  • Apple-XNU
  • OOMC c ~探究:XNU 内存状况办理
  • Reducing FOOMs in the Facebook iOS app
  • iOS内存abort(Jetsam) 原理探究
  • iOS微信内存监控
  • iOS仓库信息解析(函数地址与符号相关)
  • Apple-CFNetwork Programming Guide
  • MDN-HTTP Messages
  • DWARF 和符号化