《崩溃和卡顿》

栏目: IOS · 发布时间: 5年前

内容简介:本文尝试分析和总结一下崩溃和主线程卡顿(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。

《崩溃和卡顿》


以上所述就是小编给大家介绍的《《崩溃和卡顿》》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Bandit Algorithms for Website Optimization

Bandit Algorithms for Website Optimization

John Myles White / O'Reilly Media / 2013-1-3 / USD 19.99

This book shows you how to run experiments on your website using A/B testing - and then takes you a huge step further by introducing you to bandit algorithms for website optimization. Author John Myle......一起来看看 《Bandit Algorithms for Website Optimization》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

html转js在线工具
html转js在线工具

html转js在线工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具