内容简介:本文尝试分析和总结一下崩溃和主线程卡顿(ANR)的原理,以及对应的部分解决方案和案例。业界的崩溃率标准:
本文尝试分析和总结一下崩溃和主线程卡顿(ANR)的原理,以及对应的部分解决方案和案例。
一、崩溃
业界的崩溃率标准:
1.崩溃的信号类型
崩溃主要是由于 Mach 异常、Objective-C 异常(NSException)引起的,同时对于 Mach 异常,到了 BSD 层会转换为对应的 Signal 信号,那么我们也可以通过捕获信号,来捕获 Crash 事件。针对 NSException 可以通过注册 NSUncaughtExceptionHandler 捕获异常信息。
OC层的异常通常可以通过崩溃日志去case by case解决,异常信息会很充分,对号入座即可。
EXC_BAD_ACCESS 对应的子类型有SIGSEGV,SIGBUS,SIGILL。
完整的描述通常为 Exception Type: EXC_BAD_ACCESS (SIGSEGV)
SIGSEGV:一般是非法内存访问错误,比如访问了已经释放的野指针,或者C数组越界。是EXC_BAD_ACCESS的子集;
程序无效内存中止信号,比如访问已经释放的内存,或者访问没有权限访问的内存,写入只读的内存等。
SEGV:(Segmentation Violation),代表无效内存地址,比如空指针,未初始化指针,栈溢出等;
SIGABRT:重复释放同一块内存两次会导致,或者手动调用abort()函数;或者iOS内存jetsam的SIGABRT,对应的kill信号。或某些情况下的C数组越界导致的SIGABRT
SIGBUS:非法地址,意味着指针所对应的地址是有效地址,但总线不能正常使用该指针。通常是未对齐的数据访问所致。是EXC_BAD_ACCESS的子集;
SIGILL:非法指令。SIG是信号名的通用前缀。ILL是 illegal instruction(非法指令) 的缩写。SIGILL 是当一个进程尝试执行一个非法指令时发送给它的信号。可执行程序含有非法指令的原因,一般也就是cpu架构不对,编译时指定的march和实际执行的机器的march不同。这种情况,因为 工具 链一样,连接脚 本一样,所以可执行程序可以执行,不会发生exec format error。但是会包含一些不兼容的指令。还有另外一种可能,就是程序的执行权限不够,比如在用户态下运行的程序只能执行非特权指令,一旦CPU遇到特权指 令,将产生illegal instruction错误。
有时候也会因为打印数据的时候参数类型不匹配导致SIGILL,见这个例子 https://blog.csdn.net/Cow_cz/article/details/72930343
非法指令[EXC_BAD_INSTRUCTION // SIGILL] 该进程试图执行非法或未定义的指令。该进程可能试图通过配置错误的函数指针跳转到无效地址。
在Intel处理器上,ud2操作码会导致EXC_BAD_INSTRUCTION异常,但通常用于捕获进程以进行调试。如果在运行时遇到意外情况,则Intel处理器上的Swift代码将以此异常类型终止。有关详细信息,请参阅 https://juejin.im/post/5bdd78bc6fb9a049d2357ca2
SIGTRAP:跟踪陷阱EXC_BREAKPOINT / SIGTRAP
SIGTRAP 由断点指令或其它trap指令产生,由debugger使用。swift的空指针或类型转换失败也会导致此信号。
与异常退出类似,此异常旨在为附加的调试器提供在其执行的特定点中断进程的机会。您可以使用该__builtin_trap()函数从您自己的代码中触发此异常。如果未附加调试器,则终止该过程并生成崩溃报告。
较低级别的库(例如libdispatch)会在遇到致命错误时捕获进程。有关错误的其他信息可以在崩溃报告的“ 其他诊断信息”部分或设备的控制台中找到。
如果在运行时遇到意外情况,则Swift代码将以此异常类型终止,例如:
①具有nil值的非可选类型
②强制类型转换失败
SIGINT 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
SIGPIPE 管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
2.BSD层和Mach层的含义
3.如何拦截崩溃
OC的异常可以针对性做处理,比如不能识别的方法,你可以动态去添加。数组越界或者字典空值则可以method swizzle拦截掉。
其他异常信号的拦截则参考PLC的实现,从BSD层和Mach层都可以处理,不过保险起见,一般从Mach层捕获异常信号。
4.如何分析和解决崩溃
①SIGSEGV野指针,RN的例子;
②SIGTRAP,swift的nats库的例子。
③通用类型的崩溃,比如OC容器的越界和空值导致的,以及unrecognized selector sent to instance这种,可以通过 FFExtension 直接拦截掉。
例子见下方。
野指针大全
这里是调试一个Bad Memory Access的一些小技巧:
如果objc_msgSend或者objc_release在回溯(Backtraces)的顶部附近,这个进程可能是尝试给一个释放的对象发送消息。你应该用Zombies instrument(调试僵尸对象的工具)来更好的理解这个崩溃。
事实上Xcode的这几个工具还是挺有用处的:
关于野指针的类型崩溃在objc_msgSend函数时的拓展阅读见这里 。这篇文章分析了该函数的汇编实现,以及崩溃在不同汇编指令时的情况分析。
二、卡顿
1.监控卡顿的原理
解主线程的卡顿,首先要能够看懂卡顿的堆栈回溯,这里需要了解一下runtime里的消息发送的流程,runloop的流程,以及自动释放池的一些函数调用。
另外就是要理解bugly是如何监控卡顿的,这样子才能知道如何去修复卡顿的问题:
这就是很多SDK会采用的主线程卡顿监控的原理了,只是在execssiveHandler去抓堆栈的具体实现上,可能会有一些不同。PLC则是通过暂停先主线程,然后读取寄存器状态,之后再恢复的方法来实现抓取线程堆栈信息的。不过据说这种策略耗时比较大?有知道其他更好更快策略的小伙伴麻烦告知一下~
2.避免主线程卡顿的通用策略
①不要在主线程做耗时操作,比如解析数据,高度计算,解压缩文件和IO操作;
②耗时的计算结果,应该缓存起来,比如cell的高度;
③尽量少的用同步操作,一定避免死锁;
④能在子线程做的事情,就不要在主线程去做,比如统计打点;
⑤大对象的销毁,可以在子线程去做,参考YYCache;
⑥代码逻辑是否合理,比如我们项目中视频回放处的数据过滤,过滤的大班而不是小班数据;
⑦对于TableView这样的列表,为了提高滑动时的帧率,其cell的层级和数量,应该尽可能的少,离屏渲染一定要控制住,另外就是手动计算frame要比autolayout的性能好的多。
不同布局方式的性能差异见这里 https://lpd-ios.github.io/2017/04/05/FlexBox-Weex/ 和 https://draveness.me/layout-performance 。
三、技术优化的一般流程
通常针对一个技术点做优化的时候,都要先了解清楚这个技术点有哪些流程,优化的方向往往是减少流程的数量,以及减少每个流程的消耗。
比如安装包size的减少,启动时间的降低,滑动帧率的提升,弱网下连接的优化(到达率),耗电量的降低。
例外的是稳定性和主线程卡顿,这两块是从异常日志反推的。
帧率因为Xcode提供的工具比较强大,可以监控整个渲染流程的消耗,所以一般都先用instruments去找问题,然后再去逐个解决。
四、崩溃和ANR的案例及其解决
1.LDNetPing多线程访问property导致野指针,使用串行队列来解决,异步同步皆可;
实际上线程安全除了串行队列之外,并行队列配合barrier也可以实现。当然通用的方案是加锁,比如互斥锁,读写锁等等。
2.SSZAppConfig的getServerConfig函数:
使用AFURLSessionManager要注意,manager和它内部的session循环引用了,原因是NSURLSession的sessionWithConfiguration:delegate:delegateQueue:函数,会对delegate做强引用,除非手动解除,否则就内存泄漏
但是需要注意的一点是,sessionDidBecomeInvalidBlock的回调是在子线程,如果用户反复的切换前后台会导致getServerConfig这个函数被反复调用,同时session失效的概率也会增加,
某些情况下,会导致session失效后,manager来不及设置为nil,从而使用了失效的session去发起网络请求,导致崩溃。后来又修改为如下:
不过遗憾的是,因为业务的原因,我们的用户会频繁切换前后台,所以线上还是会不可避免的有崩溃,故并没有去刻意避开内存泄漏的问题,反而是让其成为一个单例,不做释放的操作。即把block里session的finishTaskAndInvalidate函数调用给注释掉了。
3.RN野指针崩溃
以RCTImageLoader的canHandleRequest:函数为例,videoRegex这个值的创建并不是多线程安全的,多线程野指针的问题基本都可以通过加锁来解决
- (BOOL)canHandleRequest:(NSURLRequest *)request
{
NSURL *requestURL = request.URL;
static NSRegularExpression *videoRegex = nil;
if (!videoRegex) {
NSError *error = nil;
videoRegex = [NSRegularExpression regularExpressionWithPattern:@"(?:&|^)ext=MOV(?:&|$)"
options:NSRegularExpressionCaseInsensitive
error:&error];
if (error) {
RCTLogError(@"%@", error);
}
}
NSString *query = requestURL.query;
if (query != nil && [videoRegex firstMatchInString:query
options:0
range:NSMakeRange(0, query.length)]) {
return NO;
}
for (id<RCTImageURLLoader> loader in _loaders) {
if (![loader conformsToProtocol:@protocol(RCTURLRequestHandler)] &&
[loader canLoadImageURL:requestURL]) {
return YES;
}
}
return NO;
}
@implementation RCTImageLoader (Hook)
- (BOOL)hook_canHandleRequest:(NSURLRequest *)request
{
static pthread_mutex_t mutex;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
pthread_mutex_init(&mutex, NULL);
});
pthread_mutex_lock(&mutex);
BOOL result;
result = [self hook_canHandleRequest:request];
pthread_mutex_unlock(&mutex);
return result;
}
@end
其他的RN野指针崩溃可以通过类似的方式来解决,只不过有时候要用递归锁。
4.主线程卡死问题
有时候子线程会先拿到锁,主线程反而在等待,而子线程可能会同步的丢一个block到主线程,造成死锁。
RCTBatchedBridge+Hook.m里对线程做了加锁上的优化:
- (id)hook_moduleForName:(NSString *)moduleName
{
if (!moduleName || ![moduleName isKindOfClass:[NSString class]]) {
return nil;
}
///< 这个函数会递归调用
static NSRecursiveLock *lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = [[NSRecursiveLock alloc] init];
});
id value = nil;
if (![NSThread isMainThread]) {
[lock lock];
value = [self hook_moduleForName:moduleName];
[lock unlock];
} else {
///< 子线程有时候会同步丢一个block到主线程,避免死锁
if ([lock tryLock]) {
value = [self hook_moduleForName:moduleName];
[lock unlock];
} else {
value = [self hook_moduleForName:moduleName];
}
}
return value;
}
5.页面退出后block继续执行导致的野指针问题
如上图,第605行导致了野指针,看下方的代码,正好处于GCD的执行步骤,猜测是直播间的控制器已经销毁,self为nil导致的,GCD的queue这个值不能传nil,否则crash。
bugly上的日志统计证实了猜测,确实是直播间退出后,block里的代码依然在执行导致崩溃。
解决方案:在block对self进行强引用并判空。
6.swift写的nats库的崩溃问题
以下是源码
因为这里的queue.async是一个异步操作,做成同步会更合适,避免时序问题导致莫名其妙对象被置nil。
另一个问题就是swift下不允许为nil的情况,此处因为是switch-case的语法,case的条件以及后面的指针解引用是不能传nil指针的,所以当inputstream如果是nil值时会造成SIGTRAP类型的崩溃,解决方案也就是对对象做判空,如下图:
事实上Nats的崩溃在做完上述两个操作后(其实最根本的是对inputstream判空的操作),就没有出现SIGTRAP类型的崩溃了,也就是说不会再有值被莫名其妙的置nil导致崩溃。
7.self指针的问题
上图提示为addSubview是野指针。
具体崩溃代码见下图
原因是ARC对self指针为unsafe_unretained,当上图这个函数在调用还未完成时,页面退出或用其他方式把self回收了,此后继续addSubView:的参数传入self则会野指针。
具体原因见 sunny的这篇文章 。
解决方案为,用一个局部strong指针去强引用self。
8.ANR问题
卓越网校的ANR问题,基本是主线程做IO操作,或者解析数据造成的,处理起来也简单,把这些操作放到子线程去做基本就没问题。尤其是对于cell的高度计算,子线程算好高度再返回,是一个通用的做法,用时间换流畅度。
另有小部分ANR是autolayout造成的,而这些ANR分布在相对较旧性能较差的机器上,可以不用修改代码,只需要知道手动计算frame的方式比autolayout的性能要好就可以了。
9.一个解决ANR的骚操作
场景是某个接口数据请求回来后,先解析,然后如果数据命中某个逻辑则发通知去通知业务方做其他的操作,通知是同步的,导致函数调用栈太长执行的时间太久被bugly抓到了ANR。
解决方案是把一个操作拆分成两个步骤,后面的步骤放到下一个runloop去做。原理是这么操作既能让出一个优先级给系统去响应用户的手势操作,还能让bugly检测ANR的代码执行过去。
异步丢到main queue的block是在下一次runloop循环或者从休眠中唤醒后执行的,要注意到runloop的源码里,唤醒后执行操作,会去查看此次唤醒是来自source1的mach port还是来自dispatch_asyn的dispatchPort,这两个的具体执行是两个逻辑分支,是分开的而不是一起执行。对用户手势的响应是来自source1的port。
以上所述就是小编给大家介绍的《《崩溃和卡顿》》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
解构产品经理:互联网产品策划入门宝典
电子工业出版社 / 2018-1 / 65
《解构产品经理:互联网产品策划入门宝典》以作者丰富的职业背景及著名互联网公司的工作经验为基础,从基本概念、方法论和工具的解构入手,配合大量正面或负面的案例,完整、详细、生动地讲述了一个互联网产品经理入门所需的基础知识。同时,在此基础上,将这些知识拓展出互联网产品策划的领域,融入日常工作生活中,以求职、沟通等场景为例,引导读者将知识升华为思维方式。 《解构产品经理:互联网产品策划入门宝典》适合......一起来看看 《解构产品经理:互联网产品策划入门宝典》 这本书的介绍吧!