Crash优化与建议

本文对iOS应用上经常遇到的Crash(常规signal触发的闪退)进行分析,并结合笔者的优化经验,给出Crash分析和建议。

一、Crash与信号

1.1 Crash是如何产生的

在iOS上Crash最终都是通过signal的形式发送给应用,应用可以通过注册signal handler来选择处理或忽略大部分信号。Crash的原因有多种,系统通过不同的signal来告知app这大概是什么原因造成的,常见的信号比如SIGSEGV,SIGTRAP,SIGABRT。

1.1.1 signal

signal是一个4字节的整形数字,在iOS/OSX中定义了31个已知的信号;
crash仅仅是singal触发的一个行为。signal的用途/产生包括但不限于:

  • 显式调用kill,killpg触发signal
  • 改变子进程的状态
  • 致命性中断
  • job控制
  • timer过期
  • 各种通知,如cpu resource limit或file size limit等

signal会导致以下几种行为(action):

  • Terminate 杀死进程
  • Dump core 杀死进程并创建一个core file
  • Stop 暂停进程
  • Continue 恢复进程
  • Ignore 忽略/丢弃该信号

每个signal号都有默认的action,可以通过sigaction() 系统调用来修改signal actions,为SIG_DFL(use the default action),或者修改为SIG_IGN(ignore the signal),或者指定signal handler function(捕获signal)

常见信号与原因如下:

信号 官方注释 可能原因
SIGILL 4 illegal instruction (not reset when caught) ILL_ILLTRP at 0xxxx通常是二进制出错,典型比如app升级前后或者二进制缓存出错
SIGTRAP 5 trace trap (not reset when caught) __builtin_trap()系统调用brk触发软中断结束进程,一般是数据或参数校验异常
SIGABRT 6 abort() 调用abort(),比如典型的NS异常或C++异常
SIGKILL 9 kill (cannot be caught or ignored) 系统升级,app升级,0x8badf00d,Jetsam,XCode调试杀死app,摄像头权限变更等
SIGBUS 10 bus error 总线错误,内存访问未对齐
SIGSEGV 11 segmentation violation 内存访问越界,内存crash或者地址错误,如栈溢出等
SIGPIPE 13 write on a pipe with no one to read it 管道异常,socket通信异常
SIGSTOP 17 sendable stop signal not from tty XCode调试时pause操作可触发

其中SIGKILL,SIGSTOP信号是无法被捕获或忽略等自定义处理的;

1.1.2 signal处理流程

signal的一般流程大概如下:

image.png

我们可以简单的使用kill函数来向应用/线程发送信号:

int kill(pid_t pid, int sig);
int pthread_kill(pthread_t thread, int sig);
1.1.3 signal捕获

一般的signal异常捕获工具会基于上述方式来修改signal的actions或执行signal handler,从而达到异常捕获的目的。我们也可以通过修改signal actions来达到让应用直接忽略某个信号,而不发生异常退出。但是这也有个别例外情况,比如SIGKILL,SIGSTOP 无法被捕获或忽略。如下:

    struct sigaction action = {{0}};
    action.sa_flags = SA_SIGINFO | SA_ONSTACK;
#if KSCRASH_HOST_APPLE && defined(__LP64__)
    action.sa_flags |= SA_64REGSET;
#endif
    sigemptyset(&action.sa_mask);
    action.sa_sigaction = &handleSignal;

    for(int i = 0; i < fatalSignalsCount; i++)
    {
        KSLOG_DEBUG("Assigning handler for signal %d", fatalSignals[i]);
        if(sigaction(fatalSignals[i], &action, &g_previousSignalHandlers[i]) != 0)
        {
            char sigNameBuff[30];
            const char* sigName = kssignal_signalName(fatalSignals[i]);
            if(sigName == NULL)
            {
                snprintf(sigNameBuff, sizeof(sigNameBuff), "%d", fatalSignals[i]);
                sigName = sigNameBuff;
            }
            KSLOG_ERROR("sigaction (%s): %s", sigName, strerror(errno));
            // Try to reverse the damage
            for(i--;i >= 0; i--)
            {
                sigaction(fatalSignals[i], &g_previousSignalHandlers[i], NULL);
            }
            goto failed;
        }
    }
    
    static void handleSignal(int sigNum, siginfo_t* signalInfo, void* userContext)
{
    KSLOG_DEBUG("Trapped signal %d", sigNum);
    //...异常捕获
}

以及

    KSLOG_DEBUG(@"Backing up original handler.");
    g_previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
            
    KSLOG_DEBUG(@"Setting new handler.");
    NSSetUncaughtExceptionHandler(&handleException);

1.2 Crash日志简析

当发生Crash时我们需要尽可能的收集更多的信息,包括crash的堆栈信息以及crash前的一些用户操作记录或者等其它附加信息。
Crash日志就是我们处理Crash时最重要的信息,它记录了一个app是因为什么原因发生了闪退,我们可以根据这些信息去进行分析。

一个Crash日志大概如下:

{"app_name":"mttlite","timestamp":"2019-03-26 13:04:38.95 +0800","app_version":"9.2.0","slice_uuid":"9c96c0a4-9019-39ea-992b-da986ae8cc2d","adam_id":0,"build_version":"9.2.0.8735","bundleID":"com.tencent.mttlite","share_with_app_devs":false,"is_first_party":false,"bug_type":"109","os_version":"iPhone OS 12.0 (16A366)","incident_id":"733801EE-A7D3-44E7-9D12-06BED94B4FAC","name":"mttlite"}
Incident Identifier: 733801EE-A7D3-44E7-9D12-06BED94B4FAC
CrashReporter Key:   f4d479f63324962b4280137a80389c1f3f46b019
Hardware Model:      iPhone11,2
Process:             mttlite [637]
Path:                /private/var/containers/Bundle/Application/CCB9CADB-A07C-46DA-95CE-1B6B18CD033C/mttlite.app/mttlite
Identifier:          com.tencent.mttlite
Version:             9.2.0.8735 (9.2.0)
Code Type:           ARM-64 (Native)
Role:                Non UI
Parent Process:      launchd [1]
Coalition:           com.tencent.mttlite [558]


Date/Time:           2019-03-26 13:04:38.7504 +0800
Launch Time:         2019-03-26 12:37:50.9579 +0800
OS Version:          iPhone OS 12.0 (16A366)
Baseband Version:    1.00.07
Report Version:      104

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY
Triggered by Thread:  0

Application Specific Information:
abort() called

Last Exception Backtrace:
(0x196417f78 0x195610284 0x1963283b0 0x196e2730c 0x1c382c2c8 0x1c382b9f0 0x1c37fb5e4 0x1c3e28450 0x19560b604 0x19aaa1f94 0x19aaa2274 0x19aa06000 0x19aa35518 0x19aa36358 0x1963a3fe0 0x19639eab8 0x19639f03c 0x19639e844 0x19864dbe8 0x1c374d428 0x1020e8b28 0x195e54020)

Thread 0 Crashed:
0   libsystem_kernel.dylib          0x0000000195fa30e4 0x195f80000 + 143588
... thread stack ...

Thread 0 crashed with ARM Thread State (64-bit):
    x0: 0x0000000000000000   x1: 0x0000000000000000   x2: 0x0000000000000000   x3: 0x00000002832872b7
    x4: 0x0000000195607b61   x5: 0x000000016dd1f4b0   x6: 0x000000000000006e   x7: 0x000000002a204838
    x8: 0x0000000000000800   x9: 0x0000000000000303  x10: 0x0000000000000002  x11: 0x0000000000000003
   x12: 0x0000000000000065  x13: 0x0000000000000000  x14: 0x0000000000000010  x15: 0x000000000000004d
   x16: 0x0000000000000148  x17: 0x00000001c6c12888  x18: 0x0000000000000000  x19: 0x0000000000000006
   x20: 0x0000000106b22f40  x21: 0x000000016dd1f4b0  x22: 0x0000000000000303  x23: 0x0000000106b23020
   x24: 0x0000000281e00560  x25: 0x0000000000000000  x26: 0x0000000000000001  x27: 0x0000000000000000
   x28: 0x000000016dd1fb20   fp: 0x000000016dd1f410   lr: 0x000000019601ab68
    sp: 0x000000016dd1f3e0   pc: 0x0000000195fa30e4 cpsr: 0x00000000

Binary Images:
0x1020e0000 - 0x105543fff mttlite arm64  <9c96c0a4901939ea992bda986ae8cc2d> /var/containers/Bundle/Application/CCB9CADB-A07C-46DA-95CE-1B6B18CD033C/mttlite.app/mttlite
0x106938000 - 0x106943fff libobjc-trampolines.dylib arm64e   /usr/lib/libobjc-trampolines.dylib
0x106a84000 - 0x106aebfff dyld arm64e   /usr/lib/dyld
... ...

一个crash日志大概如上,主要关注几点:

  1. 设备和应用信息:Process,Identifier,Version,OS Version
  2. 类型:Exception Type即crash信号值
  3. 地址:Exception Codes是crash发生的地址信息
  4. 问题线程:Triggered by Thread是crash发生的具体线程
  5. 问题线程堆栈
  6. crashed with ARM Thread State 问题线程寄存器信息
  7. Binary Images信息,主要用来堆栈回溯,以及用于定位内存或符号地址范围
  8. Last Exception Backtrace对于抛的异常,这个信息非常有用。

二、Crash优化思路

能复现的crash都是好crash,都是能较容易解决的。但这也不代表很难复现的crash就无从下下手。

2.1 分析问题

当我们得到了如上的crash堆栈后,结合以上信息,我们可以大概得到一个crash是什么原因触发,在什么系统或设备上的什么应用的什么版本发生了crash;接下来重点关注Triggered by Thread的堆栈信息,根据堆栈信息翻译符号表,去分析堆栈,尝试找到原因。
分析和翻译符号时,需要用到Binary Images段的内容,这里描述了当前应用使用的binary images的段加载地址,名称,架构,uuid,路径信息。根据这些信息可以方便去翻译符号表;(当我们要去复现crash时也要使用和这些uuid对应的设备/系统与应用去复现,才能保持二进制一样)

个别情况下,仅仅有当前crash堆栈的信息是不够的,这时候我们就需要重点关注ARM Thread State(Crash线程的寄存器信息)。需要重点关注的有x0,x1,lr,pc这4个寄存器;x1通常如果是发消息的话指向了selector,lr和pc则在尾调用优化或者叶子函数调用时回溯上层调用者时,有很大价值。

2.2 可复现的crash

对于能复现的crash,我们可以通过增加复现概率以及结合一些辅助异常搜集信息来尝试复现问题并去研究问题产生的原因。

比如通过hook viewcontroller切换以及运行时添加部分关键功能或节点的操作记录,当crash时,把这些额外信息随crash堆栈一起上报起来,便可以为后期分析复现该问题提供思路。例如:通过附加信息分析,发现只有触发特定功能或访问特定页面才有问题,那就能更快的缩短问题的范围,帮助更快的去定位问题。

复现前,我们需要确保复现的设备信息要与crash日志里的一致:包括同样的应用版本,系统版本,设备,uuid一致(二进制以及引用的动态库均一致)。

复现需要面对2个问题:

  • 如何操作才能复现?
  • 如何增加复现概率?

如何操作才能复现?

通过数据分析以及堆栈分析,我们是能大概猜出怎么复现的。比如特定系统,特定设备,特定操作路径信息。通过多查看聚类的crash堆栈和附加信息,能帮助我们快速了解到是在什么场景或操作路径下容易遇到问题。

如何增加复现概率?

很多crash问题是随机概率出现的,比如多线程类问题。当我们单次运行无法做到较大概率重现时,就需要通过大量批量操作来达到了。比如放大法,通过循环反复执行某些代码,通过sleep,dispatch_async等提高线程切换和不确定因素。

比如内存访问类问题,大部分情况下内存访问问题都是由于多线程访问导致的。如果我们怀疑一个问题是由于多线程访问导致的,我们可以模拟触发频繁多线程访问来提高复现概率。加for循环多次嵌套异步回调等,来频繁触发多线程访问行为。

再比如某些unrecognized selector类问题,我们可以通过开启zoombie来增加复现的概率。zoombie的本质大概就是在对象释放后,让已使用的内存地址不可被重用/访问,这样我们一定无法再访问一个已经使用过的内存地址,那么下次访问这个内存地址时就一定会出现访问错误,从而触发了crash。也间接达到了提高复现概率的问题。

2.3 难复现的crash

当一个crash很难复现或者我们可以不用复现就能解决时,我们就没必要去纠结到底怎么复现这个问题,而是把精力回归到解决问题的本质上,即这个问题是怎么产生的,从结果倒推原因;相当于是一种更强调逆向分析的过程。这个本质上要求crash分析者,对于应用的运行机制、内存管理,以及XCode调试技巧要求更高。

一些crash分析方式如下:

2.3.1 根据信号分析
  • SIGILL

ILL_ILLTRP at 0xXXX常是二进制出错,典型比如app升级前后或者二进制缓存出错;此类问题无解,只要用户重启手机基本就正常了。

  • SIGTRAP

__builtin_trap()系统调用brk触发软中断结束进程,一般是数据或参数校验异常;

典型例子如下:

1.CF集合类对象接口内部判定数据出错(如mutable CFArray,CFDictionary等CF接口出现内存越界或多线程访问问题);

2.gcd类问题,在当前线程执行dispatch_sync,或者dispatch_once里递归调用了自己;

3.子线程访问WebKit相关接口,如子线程执行[wkwebview evaluateJavaScript:] ;

4.__block多线程并发访问导致可能引用计数出错

  • SIGABRT

调用abort(),比如典型的NS异常或C++异常;重点关注uncaught exception xxx这时可以留意Last Exception Backtrace的信息就能帮助定位问题了。
如果调试遇到此类问题,比如控制台输出如下信息,借助image lookup命令即可快速翻译符号表

2019-06-20 20:06:40.541350+0800 mttlite[15978:906993] *** Terminating app due to uncaught exception 'CALayerInvalidGeometry', reason: 'CALayer bounds contains NaN: [0 0; nan nan]'
*** First throw call stack:
(0x1be52bea0 0x1bd6fda40 0x1be432674 0x1c2b28ca8 0x1c2b19118 0x1eba264d4 0x1eba270b8 0x1eba26d38 0x1eba08608 0x105d55f28 0x105d56614 0x105d58f7c 0x1befde39c 0x1be4bc1cc 0x1be4bc14c 0x1be4bba84 0x1be4b68fc 0x1be4b61cc 0x1c072d584 0x1eb5b1054 0x102115494 0x1bdf76bb4)
libc++abi.dylib: terminating with uncaught exception of type NSException

此时再在lldb控制台里输入image lookup -a 0x1be52bea0就能得到0x1be52bea0的符号信息,以此类推。

场景的NS异常参见NSException.h,关于NSException处理和更多信息参见https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Exceptions/Exceptions.html

/***************    Generic Exception names     ***************/

FOUNDATION_EXPORT NSExceptionName const NSGenericException;
FOUNDATION_EXPORT NSExceptionName const NSRangeException;
FOUNDATION_EXPORT NSExceptionName const NSInvalidArgumentException;
FOUNDATION_EXPORT NSExceptionName const NSInternalInconsistencyException;

FOUNDATION_EXPORT NSExceptionName const NSMallocException;

FOUNDATION_EXPORT NSExceptionName const NSObjectInaccessibleException;
FOUNDATION_EXPORT NSExceptionName const NSObjectNotAvailableException;
FOUNDATION_EXPORT NSExceptionName const NSDestinationInvalidException;
    
FOUNDATION_EXPORT NSExceptionName const NSPortTimeoutException;
FOUNDATION_EXPORT NSExceptionName const NSInvalidSendPortException;
FOUNDATION_EXPORT NSExceptionName const NSInvalidReceivePortException;
FOUNDATION_EXPORT NSExceptionName const NSPortSendException;
FOUNDATION_EXPORT NSExceptionName const NSPortReceiveException;

FOUNDATION_EXPORT NSExceptionName const NSOldStyleException;

/***************    Exception object    ***************/
  • SIGKILL
    SIGKILL问题最常见的就是0xbadf00d。此类问题无法被异常捕获工具捕获,因为SIGKILL信号无法被忽略或被捕获。我们只有在系统设置->隐私->分析->分析数据,中找到对应的闪退日志文件。
    顺带提一点:
    应用的所有闪退日志都设置隐私分析中,这里场景的日志文件主要是JetsamEvent和应用闪退日志;
    JetsamEvent是内存闪退日志;应用闪退日志则是以应用进程名开头的一个文件。比如对于进程mtttlite,其crash日志文件名格式为:<进程名>-<年>-<月>-<日>-<时分秒> ,例如: mttlite-2019-05-29-103302.ips

此外,摄像头权限变更,XCode调试terminate app,系统升级,应用升级都会向app发送SIGKILL信号,触发的crash;

  • SIGPIPE

SIGPIPE的主要原因就是socket通信异常,可以参考苹果文档https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/CommonPitfalls/CommonPitfalls.html
处理即可;

  • SIGBUS

SIGBUS和SIGSEGV问题都代表是内存访问错误;
SIGSEGV和SIGBUS的核心区别在于是否code=KERN_INVALID_ADDRESS,原理参加源码https://github.com/apple/darwin-xnu/blob/master/bsd/uxkern/ux_exception.c

SIGBUS意味着bus error,可能原因如下:

内存访问未对齐

访问的内存地址有效但是无权限

一般情况下即便你通过硬编码来尝试让代码走入非对齐内存访问,也基本不会有问题;因为常用的LDR,LDRB,STR等指令自动处理了内存对齐错误的问题;

In ARMv6 and later, except ARMv6-M, unaligned accesses are permitted for LDR, LDRH, STR, STRH, LDRSH, LDRT, STRT, LDRSHT, LDRHT, STRHT, and TBH instructions, where the architecture supports the instruction.

On some ARM processors, you can enable alignment checking. Non word-aligned 32-bit transfers cause an alignment exception if alignment checking is enabled.

只有在一些特殊的如LDRB等未兼容unaligned accessed的指令下,出现未对齐错误则会容易出现该问题。
例如如下ldrb指令

ldrb w0,x20
  • SIGSEGV

SIGSEGV含义为segmentation fault,即内存访问错误;详情可参考https://github.com/apple/darwin-xnu/blob/master/osfmk/vm/vm_map.c

触发SIGSEGV的本质原因如下:

  1. 对应的虚拟地址映射空间无权限访问
  2. 不存在对应的虚拟地址映射空间

举例如下:

  1. 访问/修改无对应读写权限的内存,

如修改__TEXT段(readonly)

  1. 地址超出虚拟地址映射空间范围,

如访问或修改如下地址:进程加载后的[0,0x100000000]范围内的地址都是非法,超出arm64的MACH_VM_MAX_ADDRESS是0x0FC0000000ULL,超出该地址区间也是非法的;

  1. 访问已经释放的内存
  2. stack overflow栈溢出,典型如死递归;
  3. 等等etc...
2.3.2 根据exception信息提示

比如基于Last Exception Backtrace:以及crash收集组件收集到的exception codes信息;

Exception Type: NSInvalidArgumentException(SIGABRT)
Exception Codes: *** -[__NSCFString stringByAppendingPathExtension:]: nil argument at 0x000000021879e0dc

比如这里我们知道是什么异常类型,那就直接根据异常类型和提示去在对应堆栈找问题该问题即可。
这里常用于抛NSException类问题的处理;

2.3.3 根据异常地址

此类问题主要是SIGSEGV类内存访问错误问题;
比如如果所有的异常地址都指向同一个地址,那肯定说明这里有一个很特别或奇怪的逻辑。
例如:可能是指针箭头访问weak指针的成员变量

__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(0,0),^{
    weakSelf->count = 100; //此时如果weakSelf提前释放,则就存在内存访问错误了.
});

典型的一个问题是苹果在iOS12发布后对SKStoreViewController的异步释放就造成了一个问题,这里我们分析发现所有的crash地址都是0x368一个固定的值,于是就去调查为什么存在这个值。具体的问题分析参照https://www.jianshu.com/p/d4028baeb915

再典型地址如0x0,访问空指针并尝试写内存;例如

NSError *e = nil;
//....
- (void)open:(NSString*)path error:(NSError**)e
{
    //...
    *e = [NSError errorWithDomain:xxx];//此时就crash了.
}
2.3.4 根据寄存器信息

重点关注x1,lr,pc寄存器;
关注x1寄存器是为了看下x1寄存器的值是否能在符号表中查找得到。根据arm64函数调用阅读,x0~x7一般为函数的前8个参数,函数如果有返回值一般就存在x0寄存器内。因此objc_msgSend中,x0指向当前对象,x1指向selector。知道selector也有利于进一步确定上下文或可能的具体问题。

关注pc寄存器,pc寄存器顾名思义指向了下一条执行指令,所以也可以用来缩小堆栈范围。

关注lr寄存器是为了解决leaf function优化和tail call优化的问题;编译器会对函数调用进行优化,从而导致对于leaf function和tail call会尽可能的不适用bl调用,从而减少了对栈的操作,但导致lr的值为当前上层的lr,而非原本代码内函数调用后的lr。

具体如下:

@implementation MttTailCallTest

+ (void)asyncTest
{
    dispatch_async(dispatch_get_main_queue(), ^{
        [self call];
    });
}

+ (BOOL)call
{
    int x=2;
    int *p = NULL;
    *p = 10;
    return x;
}
@end

假如入存在如下代码调用[MttTailCallTest asyncTest]
则当call发生crash时,我们是不知道是call发生了crash,堆栈能搜集到的信息只有dispatch_async,但仅仅根据一个dispatch_async我们什么信息也查不了。

tail call优化和leaf function优化会导致我们丢失原本的调用栈信息,所以进行堆栈回溯时,回溯的堆栈信息不够全面,典型的例子是gcd调用时;
此时我们就需要利用lr寄存器的信息,在符号表里查找当前lr指向的代码范围,由此来缩小堆栈搜索范围来解决问题。
由上我们根据lr寄存器就能查到lr的指向此时应该是在[MttTailCallTest asyncTest],然后再缩小范围,搜索对应有dispatch_async的代码进行排查,就八九不离十了。

查找方式如下:

macho中单架构地址 = (lr寄存器的值-当前二进制加载首地址) + 0x0100000000

将上述得到的地址在Hopper中打开对应二进制查找即可。

关于leaf function(叶子函数?)和tail call optimization(尾调用优化),可以自行搜索;此处不做详解;

2.3.5 根据附加信息

一个合格的异常收集组件,肯定需要对一些view路径,跳转路径做记录;也最好能允许应用开发者写一些关键信息。当crash时,把这些信息一并记录,这样我们就能利用这些信息去定位问题。

比如我们的应用就曾遇到过用户访问某个页面时发现高概率闪退,我们根据这些附加信息,直接就知道了页面地址,就利于我们去定位解决问题了。

2.3.6 根据静态/动态分析

如果以上的招都不灵了,那就老老实实的不要投机取巧了,找台对应的设备装上对应的包或运行代码,在对应符号打断点,然后一步步分析当crash的时候到底发生了什么。通过单独调试观察每个寄存器的值的变化,由此确定到底是什么值的改变可能会造成crash。

这个方式对于任何crash都不在话下,但基本也是最繁琐,最无聊的方式了。需要分析者熟悉简单的lldb调试技巧,以及熟悉基础arm64汇编。

一些常用有帮助的lldb命令如下:

//0x000000015bd0ff80等是某个虚拟地址
po 0x000000015bd0ff80
mem read 0x000000015bd0ff80 ;用来取内存查看内存
watchpoint set expression -w read_write -- 0x00000002812cdb80 ;用来设置watchpoint,来观察某个内存的读写情况;
watchpoint set expression -w write -- 0x0000000281281e00
dis -a 0x000000015bd0ff80; //反汇编某个地址,如果该地址是个函数符号,则可以看到对应汇编代码;
x 0x000000015bd0ff80 ; //这个命令x/n比较有意思,查看对应地址往后n字节的内存
breakpoint set -a 0x00000001ca9abc0c ;命令行的方式设置断点地址,特别有利于设置非符号断点,可直接根据crash地址计算得到当前的运行地址然后直接设置断点.
im li ;这个命令把当前所有加载的binary images的地址给出来,可用来结合进行动态分析;
thread return 0;直接当前函数调用return或return返回值0
image lookup -a 0x202952620; 符号查找

静态分析常用的工具主要用Hopper,比较简单方便,当然也可以用强大的IDA。进行静态/动态分析能帮我们稳定的找到的crash的根源,但是能不能解还是不一定的。比如你最后追根溯源得到这是个系统bug,且crash的符号和内存都和objc没有半点关系,连hook也没有思路,又是处于只有汇编代码的前提下,难度还是很大的,问题基本无解了。可能就需要从其它地方入手解决了。但无论如何,我们能定位到原因后,就可以有更多的思路去寻找解决办法了。

2.3.7 根据内存管理原则

对于大部分SIGSEGV类问题而言,问题基本都是访问非法内存访问导致的。比如访问了一个已经释放的内存;
如果不能通过hook的方式来规避访问或者杜绝非法访问的发生,那么我们可以绕过这个问题。
即:

对于我们无法完全控制或管理的内存对象,要么就延长对象声明周期(strong),要么就不关心对象生命周期(weak)顺其自然;

  • 延长生命周期

在对象的init或其它函数内间接获取到该对象,并延长其生命周期(strong)。例如retain自己持有;

  • 不关心对象生命周期

设法让对象或引用变为weak,这样即便出现内存已经释放,也不会触发访问已释放内存的问题(weak);

其中weak的例子很常见,例如我们解决UIControl类target action类问题,因为target是assign使用的,所以当target已经释放,而我们没有提前移除target时,就会可能遇到像已经释放的target发消息的问题,例如 :

Thread 0 Crashed: 
0  libobjc.A.dylib                0x00000002053d3530 objc_msgSend + 16
1  UIKitCore                      0x0000000231ddbf14 ___45-[_UIButtonBarTargetAction _invoke:forEvent:]_block_invoke +  156
2  UIKitCore                      0x0000000231ddbe44 -[_UIButtonBarTargetAction _invoke:forEvent:] +  172
3  UIKitCore                      0x00000002325e1230 -[UIApplication sendAction:to:from:forEvent:] +  96
4  UIKitCore                      0x000000023208aaf8 -[UIControl sendAction:to:forEvent:] +  80
5  UIKitCore                      0x000000023208ae18 -[UIControl _sendActionsForEvents:withEvent:] +  440
6  UIKitCore                      0x0000000232089e84 -[UIControl touchesEnded:withEvent:] +  568
7  UIKitCore                      0x000000023261829c -[UIWindow _sendTouchesForEvent:] +  2108
8  UIKitCore                      0x00000002326194c4 -[UIWindow sendEvent:] +  3140
9  UIKitCore                      0x00000002325f9534 -[UIApplication sendEvent:] +  340
10 UIKitCore                      0x00000002326bf7c0 ___dispatchPreprocessedEventFromEventQueue +  1768
11 UIKitCore                      0x00000002326c1eec ___handleEventQueueInternal +  4828
12 UIKitCore                      0x00000002326bb11c ___handleHIDEventFetcherDrain +  152
13 CoreFoundation                 0x00000002061722bc ___CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ +  24
 +  24
14 CoreFoundation                 0x000000020617223c ___CFRunLoopDoSource0 +  88
15 CoreFoundation                 0x0000000206171b24 ___CFRunLoopDoSources0 +  176
16 CoreFoundation                 0x000000020616ca60 ___CFRunLoopRun +  1004
17 CoreFoundation                 0x000000020616c354 CFRunLoopRunSpecific + 432
18 GraphicsServices               0x000000020836c79c GSEventRunModal + 92
19 UIKitCore                      0x00000002325dfb68 UIApplicationMain + 204
20 microvision                    0x000000010183d4ec main (main.m:40)
21 libdyld.dylib                  0x0000000205c328e0 _start +  4

此类问题解决也很简单,hook后,将所有对该对象的访问改为weak-proxy的方式转发给weak引用即可;
很多assign delegate的老问题也是这个解决思路;

2.3.8 根据源码分析

有些问题,如果我们始终无法得到一个很合理的解释或者一直停留在某些猜想但又不确定是否一定正常时。借助源码去了解就能事半功倍了。
比如我们熟悉的objc,其实它的内部设计里也留下了不少坑,导致个别情况下由于objc设计的缺陷而发生程序crash;
典型的几个例子:

__block非线程安全

KVO 非线程安全

三、结语

由于篇幅限制,本文并未针对每个问题都给出详细的问题分析和案例分析;本文主要是对iOS的crash优化的一些分析思路进行归纳总结。问题的解决基本都不外乎如上方式,但更多是我们要去研究每一个问题是怎么产生,从逆向去倒推,根据结果去追溯问题的本质,最终把问题给解决。如果对一些具体问题案例感兴趣还可以留意下本人博客,里面有对一些具体问题分析过程。

Crash优化析说简单也简单,很多常见的异常和问题已经很多人踩过坑了,可能随便google一下就能找到答案了;但是说难也很难。Crash优化需要开发者对iOS开发的各个方方面面都有较为扎实的了解,例如dyld加载与启动,mach-o文件结构,objc底层,系统内存管理,xnu内核,以及最基础的arm汇编分析等待都有部分了解。不然当面对一个新的问题新的挑战时,很可能就无从下手了。无论如何,对于任何程序异常,从异常的发生的原理去探寻问题的原因,我们总能找到一个合理的解释与原因,但能不能确保有办法或插入代码去修复这个问题,又是另一回事了。

Crash优化的过程很多时候既痛苦,也甜蜜。苦的是版本发布节奏摆在那里,时间催的又紧,稍有不慎又有人写出新的crash;甜的是当你费尽周折找到问题的本源并用来一个很hack的方式或很简单的方式解决了问题的时候,那种满足感很让人回味。

四、参考

Understanding and Analyzing Application Crash Reports

man3/signal.3

Exception Programming Topics

ARM64 Function Calling Conventions

ux_exception.c

vm_map.c

What is a leaf function

What Is Tail Call Optimization

__block不适合多线程并发

dyld与ObjC

你可能感兴趣的:(Crash优化与建议)