iOS开发-App应用崩溃卡顿分析
文章目录
- 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
就代表着一种崩溃信息,类似还有SIGSEGV
、SIGBUS
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
你可以使用UIApplication
的beginBackgroundTaskWithName:expirationHandler:
或者beginBackgroundTaskWithExpirationHandler:
方法来申请一些额外的时间。
官方文档
任务最多执行3
分钟,超过时间,你的app将会被杀死,造成崩溃。这也是app退到后台容易产生崩溃的原因。
对于这种崩溃,我们一般是在申请的3
分钟的Task
中,开启一个定时器,当快到3分钟时检测app还在运行就意味着App即将被系统杀死,这个时候进行记录日志上传来达到监控的效果。
Runloop 卡顿
对于Runloop
的卡顿分析,我们可以通过通过添加runloop
的CFRunLoopAddObserver
添加观察者来观察主线程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 // 所有的状态监听
};
我们可以通过添加runloop
的CFRunLoopAddObserver
添加观察者来观察kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
来检测卡顿。
为什么选择这两个时间段呢?系统事件(例如触摸)大多以source1触发,所以在
kCFRunLoopBeforeSources
之后执行,有人说那我们监听kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
的时间不就好啦?但是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;
}
内存分配监听
内存分配函数malloc
和calloc
等默认使用的是nano_zone
,nano_zone
是256B
以下内存的分配,大于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应用崩溃卡顿分析相关推荐
- 移动端测试 APP启动性能分析 WebView性能分析 H5性能分析 卡顿分析 帧分析 CPU统计 网络流量分析 耗电量指标 弱网测试 健壮性测试 兼容性测试 Amdahl
Android官网使用指南性能:https://developer.android.com/topic/performance 一.APP启动性能分析 APP的启动过程 调用起APP.创建一个空白窗口 ...
- iOS中的3种卡顿检测
市面上的iOS卡顿分析方案有三种:监控FPS.监控RunLoop.ping主线程. 前面2个都比较熟悉,第三个是最近才了解到的. 方案一:监控FPS 一般来说,我们约定60FPS即为流畅.那么反过来, ...
- Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
当我们说 流畅度 的时候,我们说的是什么?不同的人对流畅性(卡顿掉帧)有不同的理解,对卡顿阈值也有不同的感知,所以有必要在开始这个系列文章之前,先把涉及到的内容说清楚,防止出现不同的理解,也方便大家带 ...
- bootsrtap h5 移动版页面 在苹果手机ios滑动上下拉动滚动卡顿问题解决方法
bootsrtap h5 移动版页面 在苹果手机ios滑动上下拉动滚动卡顿问题解决方法 bootsrtap框架做的h5页面,在android手机下没有卡顿问题,在苹果手机就一直存在这问题,开始毫无头绪 ...
- Impala-shell卡顿分析——记一次曲折的Debug经历
Impala-shell卡顿分析--记一次曲折的Debug经历 问题发现 最近准备在Impala中增加对UTF-8的支持,以修正跟Hive.Spark等基于Java的系统在UTF-8字符串上的不兼容表 ...
- 安防摄像头Onvif、RTSP、GB28181转web无插件直播卡顿分析
监控摄像头网络直播 越来越多人的开始讲普通安防摄像机接入网络流媒体服务器,进行网络直播.这方面的解决方案比较多,最近测试了一个比较轻巧的解决方案:LiveNVR.这个就是实现接入普通RTSP/Onvi ...
- android 动画卡顿分析工具
android 动画卡顿分析工具 Android应用性能优化之分析工具 上一次记录了解决过度绘制的过程,这一次,想先弄清个概念性的东西,就是如何判断顺不顺畅? 这东西其实最初我自己也觉得有点废话,用起 ...
- 微信小程序 wepy 框架 手势密码 ios 安卓 真正的无卡顿
微信小程序 wepy 框架 手势密码 ios 安卓 真正的无卡顿 https://github.com/t5442107/yj_wepy_gesture_lock 用vscode 打开测试! 下载 h ...
- Android列表滑动卡顿分析与优化
一 目标 尝试解决首页 HomeFragment 在低端机型上滑动存在卡顿的问题. 二 测试设备 华为荣耀 9i Android 8.0 内存 4G CPU 麒麟659 三 数据采样 刚进应用存在 M ...
最新文章
- c语言如何输出整串链表,大神帮我看一下怎么输入输出一个链表,我输入了但是没输出啊...
- VarGFaceNet
- 自己动手制作笔记本SP2系统安装光盘
- 3 左右_3万左右电动迷你小汽车,3万左右电动迷你小汽车车型推荐
- Linux下开启/关闭防火墙命令
- 进阶13 Lambda表达式
- 2020高压电工考试及高压电工复审模拟考试
- 红蓝对抗之win10 权限提升
- 8uftp,8uftp使用教程图解
- 论文写作---matlab符号运算之求解方程组
- DHSNet: Deep Hierarchical Saliency Network for Salient Object Detection阅读总结
- Redis开发运维实践开发者设计规范之延迟考虑
- Java项目如何统一日志框架?
- android qq 邮箱格式,安全邮箱我输入的是qq邮箱为什么说邮箱格式 – 手机爱问
- 新娘JAVA_java新郎新娘结婚的问题
- Oracle11g下载地址
- 3年汽车软件测试工程师的经验总结
- scratch(图形化编程工具)使用画笔绘制雪绒花!真的太美了!
- matlab绘制中国地图
- python获取当天日期
热门文章
- keil5添加c51芯片包
- oracle biee catalog,BIEE 目录迁移(文件夹)方式
- 【速腾聚创混合固态激光雷达RS-M1简介与实物开箱】
- 计算机系统基础 - 为什么要学习计算机基础
- java基础之Compareable和Comparator的区别和使用
- 过于优秀的人,往往不能合群。优点越突出,缺点也越突出。
- SharePoint Framework 1.15.2 发布,包含 Viva 连接、Microsoft Teams 和 SharePoint 的更新
- 18年美亚杯团队赛(第A、B部分)
- aardio名字空间库的扩展方法(四)
- Windows 无法启动xx服务 错误1053:服务没有及时响应启动或控制请求