美文网首页SDK
iOS Crash处理方法(二):自己编写代码定位Crash

iOS Crash处理方法(二):自己编写代码定位Crash

作者: 晨寂 | 来源:发表于2017-09-06 15:18 被阅读942次

    这篇接着iOS Crash处理方法(一):MethodSwizzle 继续描述Crash的问题。
    这篇跟上篇不同,上篇是讲述如何避免Crash的问题,而这篇则是把Crash暴露出来,定位Crash位置。


    本篇文章完整代码的github地址:https://github.com/WalkingToTheDistant/ErrorManager


    上篇讲述了替换系统库API的IMP ,变成自定义API的IMP,这种方式在测试的时候好像是完美地解决了多数内存越界的问题,怎么测试就怎么爽,可是真正上线了,突然Crash数直飚,而且在iOS8 的Crash率奇高,而且还无法定位问题代码!!!
    报错的信息大致是这样的:


    屏幕快照 2017-08-24 15.25.02.png

    好吧,任我怎么测试也没有出现类似的Crash,看来不能再用这种投机取巧、无底线浪的方法了,踏踏实实改BUG吧。

    改BUG就要收集Crash报告,然后才能找到在哪报错了呀,而目前收集Crash报告的方法一般有这几种方法:

    • Xcode 的Crash收集:这个是iOS 系统自带的Crash报告上传,优点在于可以直接在Xcode里面定位代码,缺点就是需要用户同意,而且流程时间一般要好几天:从用户的手机上传到苹果服务器,服务器汇总再转发给我们的Xcode。
    • 第三方Crash收集SDK:目前比较常用的有友盟和腾讯bugly,他们会汇集Crash的详细信息,比如iOS版本号以及Crash log,优点是能够很快获取到Crash log,不过有一小部分的log还是无法定位出BUG代码。
    • 第三种方法是导出Iphone本地存储的Crash log,一般APP发生Crash的时候,iOS会存储相关Crash的信息到本地,这时候需要使用Xcode连接该iphone,然后使用XCode->windows->Devices->view Device Logs,或者用Itunes同步手机获取信息,就可以看到相关APP的Crash的信息。(这个需要符号化Crash信息)
    • 额,Xcode连接手机运行项目,等待Crash的方法就不扯了…

    通过CrashAddress 定位问题的方式也有几种,不过目前比较常用的是atos命令从Symbol(Symbol文件是Xcode打包之后自动生成的符号表,记录着每行代码的位置信息。),在最后分析环节再说明如何使用atos定位问题。

    现在我们来说说第三方一般都是怎么捕获异常信息,然后也就解释了友盟上很多Crash log定位的代码最终都指向了UmengSignalhandler。

    捕获异常

    (1)Mach异常 和 Unix信号

    关于Mach和Unix的具体信息,念茜大神这篇讲解的很详细:漫谈iOS Crash收集框架。简单来说,Mach异常是指最底层的内核级异常,而为了更直观友好的展示异常,就把Mach异常转换成了Unix信号,所以也就存在这种情况:Mach异常处理会先于Unix信号处理发生,如果Mach异常的handler让程序exit了,那么Unix信号就永远不会到达这个进程了。例如,访问野指针导致的EXC_BAD_ACCESS问题,Mach会转换成 SIGSEGV 的Unix信号。
    这篇文章暂时只处理Unix信号,捕获Unix信号的方法:

    /* 第一个参数是需要捕获的信号值,第二个参数是处理函数的函数指针,也就是你写的捕获方法 */
    void (*signal(int, void (*)(int)))(int);
    
    #import <sys/signal.h>
    /** 信号处理函数 */
    void handleSignalHandler(int signalValue){
    
    }
    signal(SIGSEGV, handleSignalHandler); // 监听 SIGSEGV 信号
    
    /* 常用的信号值说明
    * SIGABRT :由于abort()函数调用发生的程序中止信号
    * SIGILL:由于非法指令产生的程序中止信号
    * SIGSEGV:由于无效内存的引用导致的程序中止信号
    * SIGFPE:由于浮点数异常导致的程序中止信号
    * SIGBUS:由于内存地址未对齐导致的程序中止信号
    * SIGPIPE:程序通过端口发送消息失败导致的程序中止信号(这个信号比较特殊,后面会提到)
    */
    
    (2)NSException 抛出

    单单依靠捕获Unix信号是不够的,因为还有一种情况会导致Crash,那就是抛出的Objective-C异常报告(NSException)。这种Crash就不会触发上面说的Unix信号,所以还要单独处理这种情况。
    捕获 NSException 的方法:

    /* 参数同样为异常处理方法的函数指针 */
    void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);
    
    #import <Foundation/NSException.h>
    void uncaughtExceptionHandler (NSException *exception){
    
    }
    NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler); // 使用方法
    

    获取异常信息

    (1)Unix 信号

    当我们设置监听的信号发出来的时候(这时候一般是Crash了),我们的回调函数handleSignalHandler 会被触发,但是handleSignalHandler方法只有一个int参数,那就是信号值,我们想获取更多Crash信息的话,需要从堆栈中取出信息。

    #include <execinfo.h>
    void handleSignalHandler(int signalValue){
        void* callstack[128];
        int frames = backtrace(callstack, 128);  // Linux下,该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针列表。参数size 用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小。
    
        char** strs = backtrace_symbols(callstack, frames); // backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组;参数buffer应该是从backtrace函数获取的指针数组,size是该数组中的元素个数(backtrace的返回值)。
    
        // 转换好了,这样strs是一个指向字符串数组的指针,它的大小同buffer相同, 每个字符串包含了一个相对于buffer中对应元素的可打印信息.它包括函数  名,函数的偏移地址,和实际的返回地址
        for (int i = 0; i <frames; i+=1) {
            NSString *strTemp = [NSString stringWithUTF8String:strs[i]];
            NSLog(@"%@", strTemp); // 可以直接输出信息
        }
        free(strs); // 用完了要记得释放
        kill(getpid(), signalValue); // 最后要杀掉进程这句话不能少,不然你会看到你的APP卡死在那里
    }
    
    (2)NSExceoption

    这个就获取就比较容易了,NSExceoption对象本身就存储了异常信息(callStackSymbols),我们要做的就是从callStackSymbols中输出信息就可以了。

    /** 这是 NSException 的声明 */
    @interface NSException : NSObject <NSCopying, NSCoding> 
    @property (readonly, copy) NSExceptionName name;
    @property (nullable, readonly, copy) NSString *reason;
    @property (nullable, readonly, copy) NSDictionary *userInfo;
    
    @property (readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses NS_AVAILABLE(10_5, 2_0);
    @property (readonly, copy) NSArray<NSString *> *callStackSymbols NS_AVAILABLE(10_6, 4_0);
    
    #import <Foundation/NSException.h>
    
    void uncaughtExceptionHandler (NSException *exception){
        NSLog(@"%@", exception.callStackSymbols.description);
    
        NSSetUncaughtExceptionHandler(NULL); // 最后取消监听,用完就回收嘛
    }
    

    异常信息分析

    墨迹了那么久,终于开始进入重点啦。



    我先写个常见的错误代码,然后看看输出的异常信息是什么样的:

    - (void) clickBtn:(UIButton*)btn
    {
        NSArray *ary = [NSArray arrayWithObjects:@"dsfs", @"dsfsd", nil];
        NSLog(@"%@", ary[4]);  // 会触发 uncaughtExceptionHandler 方法
    }
    

    输出结果:

    callStackSymbols:
    (
        0   CoreFoundation    0x0000000103c12b0b __exceptionPreprocess + 171
        1   libobjc.A.dylib   0x00000001035cf141 objc_exception_throw + 48
        2   CoreFoundation    0x0000000103b5038b -[__NSArrayI objectAtIndex:] + 155
        3   TempPro           0x0000000102ff1f94 -[ViewController clickBtn:] + 242
        4   UIKit             0x0000000104037d82 -[UIApplication sendAction:to:from:forEvent:] + 83
        5   UIKit             0x00000001041bc5ac -[UIControl sendAction:to:forEvent:] + 67
        6   UIKit             0x00000001041bc8c7 -[UIControl _sendActionsForEvents:withEvent:] + 450
        7   UIKit             0x00000001041bb802 -[UIControl touchesEnded:withEvent:] + 618
        8   UIKit             0x00000001040a57ea -[UIWindow _sendTouchesForEvent:] + 2707
        9   UIKit             0x00000001040a6f00 -[UIWindow sendEvent:] + 4114
        10  UIKit             0x0000000104053a84 -[UIApplication sendEvent:] + 352
        11  UIKit             0x00000001048375d4 __dispatchPreprocessedEventFromEventQueue + 2926
        12  UIKit             0x000000010482f532 __handleEventQueue + 1122
        13  CoreFoundation    0x0000000103bb8c01 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
        14  CoreFoundation    0x0000000103b9e0cf __CFRunLoopDoSources0 + 527
        15  CoreFoundation    0x0000000103b9d5ff __CFRunLoopRun + 911
        16  CoreFoundation    0x0000000103b9d016 CFRunLoopRunSpecific + 406
        17  GraphicsServices  0x000000010793fa24 GSEventRunModal + 62
        18  UIKit             0x0000000104036134 UIApplicationMain + 159
        19  TempPro           0x0000000102ff28ff main + 80
        20  libdyld.dylib     0x00000001069d465d start + 1
    ) 
    

    前5行(0 ~ 4)一般都是最贴近你代码里面的错误信息,这个异常比较容易,一看就大概知道什么原因(2 :[__NSArrayI objectAtIndex:]+ 155),以及什么位置了(3:[ViewController clickBtn:] + 242)。那如果想要更精确地确定是哪一行代码出现了问题呢,那就需要使用atos命令进行解析(后面的+ 242 可不是代码行数,不要那么耿直…)。
    首先我们要从这里提取重要信息,这里哪一行信息才是我们需要的呢?首先前面标有CoreFoundation、 libobjc.A.dylib这些信息我们就不用看了,都是系统库问题,你也找不到,我们要找的是 标有我们工程名的那一行,上面的例子中就是 TempPro 这一行,一般在0~5行,越往后定位越远…

    3   TempPro  0x0000000102ff1f94 -[ViewController clickBtn:] + 242
    

    我们要的是这个地址:0x0000000102ff1f94,下面开始说明如何用这个地址。
    (1)拿到dSYM文件
    首先我们要拿到dSYM文件,dSYM文件是每次打包安装包的时候,会自动生成的,里面类似一张列表,对应存储着我们代码的位置。获取方法:Xcode -> Window -> Organrizer -> Archives -> 你的项目名(TempPro) -> 右键"Show in Finder" -> 右键TempPro. xcarchive 文件 -> 显示包内容 -> dSYMs -> TempPro.app.dSYM -> 右键显示包内容 -> Contents -> Resources -> DWARF -> TempPro(就是这个啦)(注:TempPro都是你的工程Targets名)把TempPro拷贝出来放在一个文件夹下。

    (2)获取dSYM内存地址
    因为iOS采用了ASLR(Address space layout randomization),ASDL机制会在app加载时根据load address动态加一个偏移地址slide address。所以在捕获错误地址stack address后,需要减去偏移地址才能得到正确的符号地址symbol address,上面我们从异常信息提取到的地址0x0000000102ff1f94 是stack address。额,简单说,dSYM里面存储的是一张连续的内存地址表(0~10),但是iOS 运行时从中拿了一部分(3~5),然后加载到一个起始内存地址为20的内存块中,那么我们从异常信息中提取到的内存地址是 (23 ~ 25),这样是无法映射到dSYM表(0~10)中,就需要减去一个偏移量(就是起始内存地址20),之后才可以在dSYM表中找到该代码的位置了。
    下面是获取偏移量(偏移地址slide address)的代码:

    /** 获取加载偏移地址 */
    long long getSlide()
    {
        long long slide = 0;
        for (uint32_t i = 0; i < _dyld_image_count(); i++) {
            if (_dyld_get_image_header(i)->filetype == MH_EXECUTE) {
                slide = _dyld_get_image_vmaddr_slide(i);
                break;
            }
        }
        return slide;
    }
    

    我们假设获取到的地址slide address = 0x00f2d33
    那么最终符号地址symbol address = 0x0000000102ff1f94 - 0x00f2d33 = 0x102EFF261

    (3)atos命令分析
    先看下atos命令

    atos -arch arm64 -o /temp/TempPro 0x102EFF261
    

    其中arm64是指CPU的型号,这个就需要根据APP是在哪个手机上运行决定的,这里有个型号对应表

    armv6:iPhone、iPhone2、iPhone3G
    armv7:iPhone4、iPhone4S
    armv7s:iPhone5、iPhone5C
    arm64:iPhone5S
    

    而 /temp/TempPro 这个是刚才我们第一步提取到的dSYM文件的存放位置(我放在一个temp文件夹下)
    而 0x102EFF261 这个就是我们要输入的symbol address啦(第二步获取到)
    那么根据上面的步骤,我们最终要执行的atos命令如下:

    atos -arch arm64 -o /temp/TempPro 0x102EFF261
    

    执行之后,输出结果(上面都是示例地址,下面这个是我实际测试用的)


    屏幕快照 2017-09-06 15.17.52.png
    -[ViewController clickBtn:] (in TempPro) (ViewController.m:267)
    

    这句话就很清晰啦,ViewController.m 是位置文件,m:267就是代码行数。
    纳尼!你不知道怎么显示行数?!………………
    Xcode -> Preferencs -> Text Editing -> Line numbers 勾选

    (4)注意事项
    有时候在获取异常信息 callStackSymbols 的时候,是无法获取到具体问题代码位置的信息,是因为某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架,或者删除框架指针,这些原因都会导致无法正确解析堆栈内容。(如果用友盟方法,经常定位到UmengSignalHandler就是这种情况(如果是上面的代码,就是定位到handleSignalHandler方法))。
    另外关于 SIGPIPE 信号,是关于iOS Socket长链接,APP处于前台然后锁屏,再重新解锁打开APP,会Crash的问题,可以查看这篇文章
    如何在 iOS 上避免 SIGPIPE 信号导致的 crash (Avoiding SIGPIPE signal crash in iOS)
    解决办法,在APP刚启动时执行这句代码

    signal(SIGPIPE, SIG_IGN);
    

    相关文章

      网友评论

        本文标题:iOS Crash处理方法(二):自己编写代码定位Crash

        本文链接:https://www.haomeiwen.com/subject/qzvwdxtx.html