>

第七章

去年今日此门中,人面桃花相映红。人面不知何处去,桃花依旧笑春风。

崩溃和卡顿

本文尝试分析和总结一下崩溃和主线程卡顿(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这个值的创建并不是多线程安全的,多线程野指针的问题基本都可以通过加锁来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
- (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;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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里对线程做了加锁上的优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (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。

支付二维码

ios开发

0

« 《深入解析Mac OS X & iOS操作系统》 iOS知识结构 »

#