文章目录

  • App崩溃问题
  • 可捕获的崩溃信息收集
    • PLCrashReporter实现
    • 系统接口
  • 不可捕获的崩溃
    • iOS后台模式
    • Runloop 卡顿
    • Watch Dog
  • 内存达到单个App上限被杀死
    • JetsamEvent 分析内存大小
    • XNU获取内存限值
    • task_info接口
  • 内存分配监听

App崩溃问题

app经常会遇见崩溃问题,比如下

  • 数据越界
  • 多线程操作同一指针,当指针为空时崩溃
  • 野指针问题
  • KVO问题
  • NSNotification线程问题

以及不可捕获的崩溃问题:

  • 后台任务超时
  • App超过系统限制的内存大小被杀死
  • 主线程卡顿被杀死

可捕获的崩溃信息收集

对于可以捕获的崩溃问题,我们可以通过崩溃日志分析,例如PLCrashReporter 和bugly这些三方崩溃分析工具。


PLCrashReporter实现

在崩溃日志中,Exception信息头部经常有
Exception Type: EXC_CRASH (SIGABRT)

常见的崩溃信息

SIGABRT就代表着一种崩溃信息,类似还有SIGSEGVSIGBUS

PLCrashReporter 通过注册这些崩溃signalHandler回调就可以获得崩溃信息。

PLCrashReporter 源码中

/*** Register a new signal @a callback for @a signo.** @param signo The signal for which a signal handler should be registered. Note that multiple callbacks may be registered* for a single signal, with chaining handled appropriately by the receiver. If multiple callbacks are registered, they may* <em>optionally</em> forward the signal to the next callback (and the original signal handler, if any was registered) via PLCrashSignalHandlerForward.* @param callback Callback to be issued upon receipt of a signal. The callback will execute on the crashed thread.* @param context Context to be passed to the callback. May be NULL.* @param outError A pointer to an NSError object variable. If an error occurs, this pointer will contain an error object indicating why* the signal handlers could not be registered. If no error occurs, this parameter will be left unmodified. You may specify* NULL for this parameter, and no error information will be provided.** @warning Once registered, a callback may not be deregistered. This restriction may be removed in a future release.* @warning Callers must ensure that the PLCrashSignalHandler instance is not released and deallocated while callbacks remain active; in* a future release, this may result in the callbacks also being deregistered.*/
- (BOOL) registerHandlerForSignal: (int) signocallback: (PLCrashSignalHandlerCallbackFunc) callbackcontext: (void *) contexterror: (NSError **) outError
{/* Register the actual signal handler, if necessary */if (![self registerHandlerWithSignal: signo error: outError])return NO;/* Add the new callback to the shared state list. */plcrash_signal_user_callback reg = {.callback = callback,.context = context};shared_handler_context.callbacks.nasync_prepend(reg);return YES;
}

可以查看 registerHandlerWithSignal 来查看实现。

PLCrashReporter 将这些崩溃堆栈信息存储后,App就崩溃了,等待下次重启时,就可以取到这些日志,进行上传分析。

系统接口

系统函数的优点在于性能好,但是只能获取简单的信息,无法通过dSYM文件来了解是哪行代码出的问题。

SIGABRT--程序中止命令中止信号
SIGALRM--程序超时信号
SIGFPE--程序浮点异常信号
SIGILL--程序非法指令信号
SIGHUP--程序终端中止信号
SIGINT--程序键盘中断信号
SIGKILL--程序结束接收中止信号
SIGTERM--程序kill中止信号
SIGSTOP--程序键盘中止信号
SIGSEGV--程序无效内存中止信号
SIGBUS--程序内存字节未对齐中止信号
SIGPIPE--程序Socket发送失败中止信号
void InstallSignalHandler(void)
{signal(SIGHUP, SignalExceptionHandler);signal(SIGINT, SignalExceptionHandler);signal(SIGQUIT, SignalExceptionHandler);signal(SIGABRT, SignalExceptionHandler);signal(SIGILL, SignalExceptionHandler);signal(SIGSEGV, SignalExceptionHandler);signal(SIGFPE, SignalExceptionHandler);signal(SIGBUS, SignalExceptionHandler);signal(SIGPIPE, SignalExceptionHandler);
}void SignalExceptionHandler(int signal)
{NSMutableString *mstr = [[NSMutableString alloc] init];[mstr appendString:@"Stack:\n"];void* callstack[128];int i, frames = backtrace(callstack, 128);char** strs = backtrace_symbols(callstack, frames);for (i = 0; i <frames; ++i) {[mstr appendFormat:@"%s\n", strs[i]];}//这里写入mstr到你的日志文件}

不可捕获的崩溃

iOS后台模式

  • 后台模式,例如音乐播放VOIP地图类app
  • Background Fetch设置一个间隔来每隔一段时间请求网络数据。由于用户可以在设置中关闭这种模式,导致它的使用场景很少
  • Slience Push 是推送的一种,会在后台唤醒30秒,优先级很低。
  • Push Kit 后台唤醒app后会保活30秒,主要用于VOIP应用提升体验。
  • Background Task 是最常用也是使用最多的模式,会在进入后台时请求3分钟进行额外的操作。

对于 Background Task 你可以使用UIApplicationbeginBackgroundTaskWithName:expirationHandler: 或者beginBackgroundTaskWithExpirationHandler:方法来申请一些额外的时间。

官方文档

任务最多执行3分钟,超过时间,你的app将会被杀死,造成崩溃。这也是app退到后台容易产生崩溃的原因。

对于这种崩溃,我们一般是在申请的3分钟的Task中,开启一个定时器,当快到3分钟时检测app还在运行就意味着App即将被系统杀死,这个时候进行记录日志上传来达到监控的效果。


Runloop 卡顿

对于Runloop的卡顿分析,我们可以通过通过添加runloopCFRunLoopAddObserver添加观察者来观察主线程Runloop的状态,通过回调返回,然后我们开启子线程一个loop循环来检测单位允许时间内(例如 3秒)是否收到了该状态,如果超过了该值,则说明这个状态转变的时间过长了

注意:这个3秒时间,我们可以通过看门狗每个阶段允许的事件来判断,我们可通过下面的Watch Dog各个阶段允许的超过时间来设置,不要大于看门狗杀死app的时间

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry         = (1UL << 0), // 即将进入LoopkCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 TimerkCFRunLoopBeforeSources = (1UL << 2), // 即将处理 SourcekCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒kCFRunLoopExit          = (1UL << 7), // 即将退出LoopkCFRunLoopAllActivities = 0x0FFFFFFFU // 所有的状态监听
};

我们可以通过添加runloopCFRunLoopAddObserver添加观察者来观察kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting来检测卡顿。

为什么选择这两个时间段呢?系统事件(例如触摸)大多以source1触发,所以在kCFRunLoopBeforeSources之后执行,有人说那我们监听kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting的时间不就好啦?但是runloop没有事件时,是会停止在kCFRunLoopBeforeWaiting的,所以我们就监听kCFRunLoopAfterWaiting,当两个状态回调时,将一个flag置为NO(这里是检测3次来实现的),然后每隔几秒查看这个flag,这样监听是否卡顿。


dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保证同步
//创建一个观察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
//将观察者添加到主线程runloop的common模式下的观察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);//回调
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;lagMonitor->runLoopActivity = activity;dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;dispatch_semaphore_signal(semaphore);
}

再进行创建子线程

//创建子线程监控dispatch_async(dispatch_get_global_queue(0, 0), ^{//子线程开启一个持续的loop用来进行监控while (YES) {long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, STUCKMONITORRATE * NSEC_PER_MSEC));if (semaphoreWait != 0) {if (!runLoopObserver) {timeoutCount = 0;dispatchSemaphore = 0;runLoopActivity = 0;return;}//两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {//出现三次出结果if (++timeoutCount < 3) {continue;}// TODO:写入堆栈信息到日志} //end activity}// end semaphore waittimeoutCount = 0;}// end while});

戴明老师的 GCDFetchFeed 中就包含了这个处理

关于Runloop卡顿检测的详细说明,我会在另一篇博文中说明。


Watch Dog

内存打爆主线程卡顿时间超过阈值都会被Watch Dog杀死

Watchdog 在app各个阶段所允许的超时时间

launch(启动) 20s
resume(恢复)    10s
suspend(挂起)   10s
quit(退出)  6s
background(后台) 3min(iOS7之前,申请10min,之后改为每次申请3min,可连续申请直到10min)

看门狗Watch Dog的崩溃异常代码通常是“0x8badf00d”,即“ate bad food”。


一些被系统杀掉的情况,我们可以通过异常编码来分析。你可以在维基百科上,查看完整的异常编码。这里列出了 44 种异常编码,但常见的就是如下三种:

0x8badf00d,表示 App 在一定时间内无响应而被 watchdog 杀掉的情况。
0xdeadfa11,表示 App 被用户强制退出。
0xc00010ff,表示 App 因为运行造成设备温度太高而被杀掉。


内存达到单个App上限被杀死

App达到iOS系统对单个app设置的内存占用上限后,被系统杀死,我们称之为 out of memory(OOM)

我们如何获取app的内存上限大小呢?


JetsamEvent 分析内存大小

对于内存问题杀死的问题,我们可以分析JetsamEvent日志。

我们可以通过手机的设置->隐私->分析->分析数据中 找到JetsamEvent开头的相关日志信息。
先找到per-process-limit内容

{"uuid" : "092ff2cc-0290-3310-a375-cc69c192b94d","states" : ["daemon","idle"],"killDelta" : 6781,"lifetimeMax" : 8241,"age" : 4581135570382,"purgeable" : 485,"fds" : 50,"genCount" : 0,"coalition" : 832,"rpages" : 7736,"reason" : "per-process-limit","pid" : 8326,"idleDelta" : 174877955757,"name" : "photoanalysisd","cpuTime" : 111.697557},

"rpages" : 7736 表示内存页数为 7736 ,再从文件中找到pageSize字段

  "largestZoneSize" : 15192064,"pageSize" : 4096,"uncompressed" : 77779,"zoneMapSize" : 70713344,

则可以计算当前app内存限额 7736*4096/1024/1024 = 30MB

  • iOS是如何发现JetsamEvent
    iOS会开启优先级最高级的线程vm_pressure_monitor来监听系统内存情况,通过一个堆栈来维护所有app的进程。此外iOS还会维护一个内存快照表,来保存每个进程的内存页消耗情况。

    iOS觉得内存有压力,就会发出通知,告诉那些内存有压力的app去释放内存,didReceiveMemoryWarning就是这里产生,在这里释放内存能够可能避免你的app被系统杀死。

  • iOS杀死线程的优先级
    iOS内核有个数组,专门维护线程的优先级。
    内核使用的线程的优先级最高,操作系统其次,App最后。
    App在前台的优先级高于后台,且占用线程多的App优先级将会降低

App在被iOS系统杀死前,系统大概有6s时间来判断优先级,Jetsam日志也是这个时间生成的.


XNU获取内存限值

XNU获取需要Root权限,通过memorystatus_priority_entry结构体获取

// 获取进程的 pid、优先级、状态、内存阈值等信息
typedef struct memorystatus_priority_entry {pid_t pid;int32_t priority;uint64_t user_data;int32_t limit;uint32_t state;
} memorystatus_priority_entry_t;

参考文章 : https://www.jianshu.com/p/2a283df2e839


task_info接口

根据XNU源码,我们可以知道apple获取app使用信息的函数。

源码链接

系统提供了一个函数获取当前任务的信息,我们可以通过phys_footprint获取app的使用内存

#import <mach/mach.h>uint64_t memoryFootprint() {task_vm_info_data_t vmInfo;mach_msg_type_number_t count = TASK_VM_INFO_COUNT;kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);if (result != KERN_SUCCESS)return 0;return vmInfo.phys_footprint;
}

内存分配监听

内存分配函数malloccalloc等默认使用的是nano_zonenano_zone256B以下内存的分配,大于256B 的时候会使用 scalable_zone 来分配。

使用 scalable_zone 分配内存的函数都会调用malloc_logger 函数,因为系统需要有一个地方统计并管理内存的分配情况。

我们可以通过 fishhook 进行hook内存分配的 mallloc_logger ,就可以掌握内存分配的大小。


参考文章 :
https://www.jianshu.com/p/302ed945e9cf
https://www.jianshu.com/p/2a283df2e839
https://www.jianshu.com/p/77ad25620929

iOS开发-App应用崩溃卡顿分析相关推荐

  1. 移动端测试 APP启动性能分析 WebView性能分析 H5性能分析 卡顿分析 帧分析 CPU统计 网络流量分析 耗电量指标 弱网测试 健壮性测试 兼容性测试 Amdahl

    Android官网使用指南性能:https://developer.android.com/topic/performance 一.APP启动性能分析 APP的启动过程 调用起APP.创建一个空白窗口 ...

  2. iOS中的3种卡顿检测

    市面上的iOS卡顿分析方案有三种:监控FPS.监控RunLoop.ping主线程. 前面2个都比较熟悉,第三个是最近才了解到的. 方案一:监控FPS 一般来说,我们约定60FPS即为流畅.那么反过来, ...

  3. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析

    当我们说 流畅度 的时候,我们说的是什么?不同的人对流畅性(卡顿掉帧)有不同的理解,对卡顿阈值也有不同的感知,所以有必要在开始这个系列文章之前,先把涉及到的内容说清楚,防止出现不同的理解,也方便大家带 ...

  4. bootsrtap h5 移动版页面 在苹果手机ios滑动上下拉动滚动卡顿问题解决方法

    bootsrtap h5 移动版页面 在苹果手机ios滑动上下拉动滚动卡顿问题解决方法 bootsrtap框架做的h5页面,在android手机下没有卡顿问题,在苹果手机就一直存在这问题,开始毫无头绪 ...

  5. Impala-shell卡顿分析——记一次曲折的Debug经历

    Impala-shell卡顿分析--记一次曲折的Debug经历 问题发现 最近准备在Impala中增加对UTF-8的支持,以修正跟Hive.Spark等基于Java的系统在UTF-8字符串上的不兼容表 ...

  6. 安防摄像头Onvif、RTSP、GB28181转web无插件直播卡顿分析

    监控摄像头网络直播 越来越多人的开始讲普通安防摄像机接入网络流媒体服务器,进行网络直播.这方面的解决方案比较多,最近测试了一个比较轻巧的解决方案:LiveNVR.这个就是实现接入普通RTSP/Onvi ...

  7. android 动画卡顿分析工具

    android 动画卡顿分析工具 Android应用性能优化之分析工具 上一次记录了解决过度绘制的过程,这一次,想先弄清个概念性的东西,就是如何判断顺不顺畅? 这东西其实最初我自己也觉得有点废话,用起 ...

  8. 微信小程序 wepy 框架 手势密码 ios 安卓 真正的无卡顿

    微信小程序 wepy 框架 手势密码 ios 安卓 真正的无卡顿 https://github.com/t5442107/yj_wepy_gesture_lock 用vscode 打开测试! 下载 h ...

  9. Android列表滑动卡顿分析与优化

    一 目标 尝试解决首页 HomeFragment 在低端机型上滑动存在卡顿的问题. 二 测试设备 华为荣耀 9i Android 8.0 内存 4G CPU 麒麟659 三 数据采样 刚进应用存在 M ...

最新文章

  1. c语言如何输出整串链表,大神帮我看一下怎么输入输出一个链表,我输入了但是没输出啊...
  2. VarGFaceNet
  3. 自己动手制作笔记本SP2系统安装光盘
  4. 3 左右_3万左右电动迷你小汽车,3万左右电动迷你小汽车车型推荐
  5. Linux下开启/关闭防火墙命令
  6. 进阶13 Lambda表达式
  7. 2020高压电工考试及高压电工复审模拟考试
  8. 红蓝对抗之win10 权限提升
  9. 8uftp,8uftp使用教程图解
  10. 论文写作---matlab符号运算之求解方程组
  11. DHSNet: Deep Hierarchical Saliency Network for Salient Object Detection阅读总结
  12. Redis开发运维实践开发者设计规范之延迟考虑
  13. Java项目如何统一日志框架?
  14. android qq 邮箱格式,安全邮箱我输入的是qq邮箱为什么说邮箱格式 – 手机爱问
  15. 新娘JAVA_java新郎新娘结婚的问题
  16. Oracle11g下载地址
  17. 3年汽车软件测试工程师的经验总结
  18. scratch(图形化编程工具)使用画笔绘制雪绒花!真的太美了!
  19. matlab绘制中国地图
  20. python获取当天日期

热门文章

  1. keil5添加c51芯片包
  2. oracle biee catalog,BIEE 目录迁移(文件夹)方式
  3. 【速腾聚创混合固态激光雷达RS-M1简介与实物开箱】
  4. 计算机系统基础 - 为什么要学习计算机基础
  5. java基础之Compareable和Comparator的区别和使用
  6. 过于优秀的人,往往不能合群。优点越突出,缺点也越突出。
  7. SharePoint Framework 1.15.2 发布,包含 Viva 连接、Microsoft Teams 和 SharePoint 的更新
  8. 18年美亚杯团队赛(第A、B部分)
  9. aardio名字空间库的扩展方法(四)
  10. Windows 无法启动xx服务 错误1053:服务没有及时响应启动或控制请求