手把手教你查看和分析iOS的crash崩溃异常

作者 : 开心源码 本文共11617个字,预计阅读时间需要30分钟 发布时间: 2022-05-12 共237人阅读

要学会看crash崩溃和报告

一个应使用程序并不总会一直运行的很好,它总会有出现crash崩溃的情况。假如在应使用程序中接入了少量第三方的crash收集工具或者者自建crash收集报告平台的话将会很好的帮助开发者去分析和处理应使用程序在线上运行的问题,当出现的崩溃问题能得到及时的处理和快速的修复时必将会大大的提升应使用程序的使用户体验。

当前比较流行的crash收集分析工具很多都是基于开源的KSCrash代码来进行封装和改进的。苹果自身也构建了一套crash采集和分析的机制,你可以从真机的联机日志或者者从开发者账号中去查看对应的crash信息。网络上也有很多关于crash分析的文章,以及crash堆栈符号化解决的文章。这里假定你已经理解了少量查看crash报告的方法和技巧以及少量简单的crash分析技巧,由于这些是作为开发者需要具有的技能之一。

一个objc_msgSend+16崩溃栈

应使用程序出现的crash崩溃异常有少量能够简单的被分析和处理,往往这些crash崩溃异常都会带有明确的上下文信息和函数调使用层级堆栈。但并不是所有的crash崩溃异常都能被简单的处理,尤其是那些没有明确上下文信息的函数调使用堆栈或者者那些调使用堆栈中没有一个函数或者者方法能够被直接定位到源代码的场景,就如下面这个崩溃的函数调使用栈(部分信息):

Incident Identifier: 85BE3461-D7FD-4043-A4B9-1C0D9A33F63DCrashReporter Key:   9ec5a1d3b8d5190024476c7068faa58d8db0371fHardware Model:      iPhone7,2Code Type:       ARM-64Parent Process:  ? [1]Date/Time:       2018-08-06 16:36:58.000 +0800OS Version:      iOS 10.3.3 (14G60)Report Version:  104Exception Type:  EXC_BAD_ACCESS (SIGBUS)Exception Codes: 0x00000000 at 0x00000005710bbeb8Crashed Thread:  2Thread 2 name:  WebThread Thread 2 Crashed:0   libobjc.A.dylib                 objc_msgSend + 161   UIKit                           -[UIWebDocumentView _updateSubviewCaches] + 402   UIKit                           -[UIWebDocumentView subviews] + 923   UIKit                           -[UIView(CALayerDelegate) _wantsReapplicationOfAutoLayoutWithLayoutDirtyOnEntry:] + 724   UIKit                           -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 12565   QuartzCore                      -[CALayer layoutSublayers] + 1486   QuartzCore                      CA::Layer::layout_if_needed(CA::Transaction*) + 2927   QuartzCore                      CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 328   QuartzCore                      CA::Context::commit_transaction(CA::Transaction*) + 2529   QuartzCore                      CA::Transaction::commit() + 50410  QuartzCore                      CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 12011  CoreFoundation                  __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 3212  CoreFoundation                  __CFRunLoopDoObservers + 37213  CoreFoundation                  CFRunLoopRunSpecific + 45614  WebCore                         RunWebThread(void*) + 45615  libsystem_pthread.dylib         _pthread_body + 24016  libsystem_pthread.dylib         _pthread_body + 0Thread 2 crashed with ARM-64 Thread State:  cpsr: 0x0000000020000000     fp: 0x000000016e18d7c0     lr: 0x000000018e2765fc     pc: 0x0000000186990150     sp: 0x000000016e18d7b0     x0: 0x0000000174859740     x1: 0x000000018eb89b7b    x10: 0x0000000102ffc000    x11: 0x00000198000003ff    x12: 0x0000000102ffc290    x13: 0xbadd8a65710bbead    x14: 0x0000000000000000    x15: 0x000000018caeb48c    x16: 0x00000005710bbea8    x17: 0x000000018e2765d4    x18: 0x0000000000000000    x19: 0x0000000103a52800     x2: 0x0000000000000000    x20: 0x00000000000002a0    x21: 0x0000000000000000    x22: 0x0000000000000000    x23: 0x0000000000000000    x24: 0x0000000000000098    x25: 0x0000000000000000    x26: 0x000000018ebade52    x27: 0x00000001ad018624    x28: 0x0000000000000000    x29: 0x000000016e18d7c0     x3: 0x000000017463db60     x4: 0x0000000000000000     x5: 0x0000000000000000     x6: 0x0000000000000000     x7: 0x0000000000000000     x8: 0x00000001acfb9000     x9: 0x000000018ebf8829 Binary Images:       0x100030000 -        0x1022cbfff +xxxx arm64  <6b98f446542b3de5818256a8f2dc9ebf> /var/containers/Bundle/Application/441619EF-BD56-4738-B6CF-854492CDFAC9/xxxx.app/xxxx       0x1063f8000 -        0x106507fff  MacinTalk arm64  <0890ce05452130bb9af06c0a04633cbb> /System/Library/TTSPlugins/MacinTalk.speechbundle/MacinTalk       0x107000000 -        0x1072e3fff  TTSSpeechBundle arm64  <d583808dd4b9361b99a911b40688ffd0> /System/Library/TTSPlugins/TTSSpeechBundle.speechbundle/TTSSpeechBundle...       0x18e03d000 -        0x18ede3fff  UIKit arm64  <314063bdf85f321d88d6e24a0de464a2> /System/Library/Frameworks/UIKit.framework/UIKit       0x18ede4000 -        0x18ee0cfff  CoreBluetooth arm64  <ced176702d7c37e6a9027eeb3fbf7f66> /System/Library/Frameworks/CoreBluetooth.framework/CoreBluetooth

这是一个在iOS10.3.3版本的64位设施上的一条crash异常报告的片段信息,要记住这些信息,它对定位crash崩溃异常有很大的帮助。从崩溃的函数调使用栈中可以看出异常是出现在最顶层的函数调使用objc_msgSend+16处,也就是在objc_msgSend函数的第5条指令处(通常情况下arm体系结构中每条指令占使用4个字节,上述的信息表明是崩溃在函数的第16个字节的偏移地址处,也就是函数的第5条指令处)。崩溃异常类型显示为EXC_BAD_ACCESS表明是产生了无效的地址的读写访问,整个崩溃函数调使用栈中没应使用程序中的任何上下文信息。objc_msgSend函数是runtime方法执行的核心引擎而且调使用如此的频繁,函数内部是不可能有BUG的。 那么为什么会崩溃在这呢?

当异常出现在没有源代码的函数内部时,唯一的方法就是去看它内部的“源代码”实现

既然出现问题是在objc_msgSend函数的第5条指令处,可以来看看这个函数实现的汇编代码指令开头片段:

;iOS10以后的objc_msgSend的部分实现代码。_objc_msgSend:00000001800bc140<+0>    cmp     x0, #0x0     ;判断对象receiver和0进行比较00000001800bc144<+4>    b.le    0x1800bc1ac    ;假如对象指针为0或者者高位为1则执行特殊解决跳转。00000001800bc148<+8>    ldr     x13, [x0]           ;取出对象的isa指针赋值给x1300000001800bc14c<+12>   and x16, x13, #0xffffffff8   ;得到对象的Class对象指针赋值给x1600000001800bc150<+16>   ldp x10, x11, [x16, #0x10]    ;取出Class对象的cache成员分别保存到x10,x11寄存器中 -----------------------------------------------------------上面的指令就是代码崩溃处。00000001800bc154<+20>   and     w12, w1, w11

无论是真机还是模拟器,XCODE都支持在运行时来查看任何调使用的函数的汇编代码实现,你可以通过设置符号断点或者者进入汇编调试模式以及单指令跳转的方式来查看函数的汇编代码实现。

从代码中可以看出是在读取对象的Class对象指针的数据成员cache时出现了无效的地址访问异常。但是对象的Class对象这部分定义数据是存储在进程内存的数据区段中,并且伴随着整个应使用的生命周期而存在,是不可能被释放和销毁的,因而正常情况下是不可能存在非法内存地址访问异常的。会出现这种问题的起因就是调使用方法的OC对象被销毁了,再说具体一点就是对一个已经被释放掉的OC对象继续调使用了实例方法而导致的。因而当出现这种类型的崩溃时,不论能否有明确上下文,其起因都是一致的。下面这张图就能很清楚的说明其中的起因了:

对象被销毁前后内存布局比照图

实际上在arm64位系统中isa中保存的并不是对象的Class对象地址,上面的图目的是为了更加直观的显示问题起因。

一个OC对象obj在被销毁前,其中的isa指针会指向正确的Class对象所在的内存地址。因而调使用objc_msgSend方法将会正常的运行,而一旦obj对象被销毁后,为其分配的堆内存将被回收使用作其余使用途,因而有可能这部分内存区域的数据会被覆写。当对一个已经释放了的OC对象继续调使用实例方法时,在objc_msgSend函数内部读取到obj的isa指针得到的将是一个未知或者者有可能无效的指针值。所以当对这个未知地址指向的内存进行访问时就出现了上面的EXC_BAD_ACCESS的异常崩溃了。

CPU指令中操作寄存器和常数的指令一般不会产生崩溃异常,比方上面的第1,2,4,6条指令;而一般产生访问异常的指令是发生在那些访问内存地址的指令当中,比方第3条和5条。

也许你会好奇既然obj对象已经被释放了,为什么崩溃会出现在objc_msgSend函数的第5条指令,其中的第3条指令是访问对象的isa数据的,为什么不崩溃在这呢? 其实答案很简单,由于几乎所有的OC对象都是从堆内存区域中分配内存的,所以当某个OC对象被销毁后,其所占使用的内存依然会放回堆内存区域中进行管理,而堆内存区域的地址是可以进行任意的读写访问的,所以即便对象被销毁释放,依然是可以访问对象所指向的内存区域的数据的。

应使用程序出现崩溃异常时除了函数调使用栈可提供分析参考外,还可以从寄存器中的值来进行一步分析。根据上述的函数指令实现中可以看出:

x0 寄存器中的保存的就是那个被销毁了的对象指针。
x1 寄存器中保存的就是产生崩溃的对象的方法名称的地址。
x13 寄存器中保存的就是对象的isa指针值。
x16 寄存器中保存的就是对象的Class指针对象。

函数崩溃处指令为:

ldp x10, x11, [x16, #0x10] 

这时候由于x16中其实保存的是一个非法的Class对象指针地址了,所以当执行ldp指令来从x16所指向地址的偏移0x10处读取内存数据时就产生了崩溃,而崩溃的异常代码:

  Exception Codes: 0x00000000 at 0x00000005710bbeb8

中的地址值也恰好和x16寄存器中的值是一致的。也就是表明x16中所保存的Class对象指针就是一个非法和无效的内存地址。

在所有的OC方法中假如你设置了符号断点那么在方法开始执行时x0中保存的总是执行方法的对象,也是第一个方法的参数;x1中总是保存的执行的方法的名称字符串,也是第二个方法的参数;而后x2到x15有可能依次是方法的其余参数。因而通常情况下你可以在调试控制台中输入: po $x0 来显示对象信息, p (char*)$x1 来显示方法名称。 具体的详细详情可以参考我的另外一篇文章:寄存器详情

上面的崩溃调使用栈中,所有的函数和方法都是系统函数并没有程序自身的源代码,因而很难跟踪或者者发现问题产生的起因,由于此时是无法知道是哪个类的对象执行方法调使用而产生的crash了,唯一的线索就是x1寄存器中的值了。这个寄存器中的值保存的是调使用的方法名, 它是一个SEL类型的数据,因而可以根据x1中保存的方法名来进行反推,也就是从方法名来反推出产生崩溃的对象的类名。

x1寄存器中保存的方法的内存地址是存在于某个加载的库Image的代码段中,因而可以在崩溃日志的Binary Images列表中找到定义方法名的库Image信息,Binary Images列表中的每个库Image都有这个库加载的开始和结束地址以及路径名称,可以很容易就从这些区间列表中找到x1寄存器所指的方法名究竟属于哪个库。就上面的例子来说可以很明确的看到方法地址0x18eb89b7b是属于:

 0x18e03d000 -  0x18ede3fff  UIKit arm64  <314063bdf85f321d88d6e24a0de464a2> /System/Library/Frameworks/UIKit.framework/UIKit

也就是UIKit库中定义的某个对象在执行x1所指的方法而产生了崩溃。有了这个更进一步的信息后即可以在源代码中进行检查看看哪部分代码调使用到了产生崩溃的库中所定义的对象了(当然UIKit这里不具有代表性,实际中崩溃时方法名也许会在其余的库中)。这样就从肯定程度上能够缩小排查问题的范围。

常见的崩溃异常分析定位方法

当出现了没有上下文的崩溃异常调使用栈时,并不是对它束手无策。除了可以根据异常类型(signal的类型)分析外,还可以借助搜索引擎以及少量常见的问题解答站点来寻觅答案,当然还可以借助下面列出几种定位和分析的方法:

1.开源代码法

这个方法其实很简单,苹果其实开源了非常多的基础库的源代码,因而当程序崩溃在这些开源的基础库上时即可以去下载对应的基础库的源代码进行阅读。而后从源代码上进行问题的分析,从而找到产生异常崩溃的起因。你可以从https://opensource.apple.com处去下载开源的最新的源代码。这种方法的缺点是并不是所有的代码都是开源的,而且开源的代码并不肯定是你真机设施上运行的iOS版本。因而这种方法只能是一种辅助方法。

2.方法符号断点法

采使用这种方法时,确保你手头上要有一台和产生崩溃异常问题的操作系统版本相同的真机设施,以方便联机调试和运行。你可以在崩溃异常报告的:

OS Version:      iOS 10.3.3 (14G60)

部分看到产生异常的操作系统版本号,就如本文的例子里面产生异常的操作系统版本号为iOS 10.3.3。由于相同的操作系统版本号中所有库中代码实现的都是一样的。假如实在没有对应的版本号的设施则可以试图找一台版本号最相近的设施。明确了操作系统版本和真机设施后再从代码仓库中检出和你线上相同版本的应使用程序的源代码(如果崩溃调使用栈中没有任何我们编写的函数代码则这个条件要求不必那么严格)。并打开项目工程,而后为产生崩溃的函数调使用栈的栈顶函数或者者方法名增加一个符号断点。假如你不知道如何增加符号断点请参考文章:https://blog.csdn.net/xuhen/article/details/77747456, 或者者查找关键字:“XCODE 符号断点”。

设置符号断点的方法或者者函数名时可以有如下的选择:

  1. 假如产生崩溃的栈顶是一个OC对象的方法则可以直接使用这个类名和方法名来设置符号断点。
  2. 假如产生崩溃的栈顶是一个通使用的C函数比方objc_msgSend、free、objc_release则考虑使用函数调使用栈的第二层函数和方法名来设置符号断点。比方文本例子中的-[UIWebDocumentView _updateSubviewCaches]方法。
  3. 假如产生崩溃的函数调使用栈顶是一个没有对外暴露的C函数,由于这种函数设置符号断点的难度比交大,所以往往考虑采使用函数调使用栈的第二层函数或者者方法名来做为符号断点。

设置符号断点的目的是为了在崩溃函数调使用堆栈重现时,能在运行时的断点处进行动态分析。当你设置了符号断点后,假如程序逻辑运行到这个函数或者者方法时,系统就会在设置的方法或者者函数的第一条指令处中止下来。这时候即可以查看此时的函数调使用栈能否和产生崩溃时的调使用栈相符,假如相符合那么表明能够重现可能发生问题的逻辑了,假如断点处的调使用栈和产生崩溃的调使用栈不相同,则可能需要让程序继续运行,以便下次在同样断点处时进行调使用栈的比较,由于设置断点的方法名并不肯定只在一处被调使用。

符号断点的设置

当程序停在了设置符号断点的函数或者者方法的开始地址后,接下来就需要在这个方法内进行第二个断点的设置,设置的地方就是崩溃函数调使用栈中函数调使用上层函数的偏移处,这个可以在崩溃的报告中看到:

0   libobjc.A.dylib                 objc_msgSend + 161   UIKit                          -[UIWebDocumentView _updateSubviewCaches] + 40

也就是需要在_updateSubviewCaches函数的第11条指令或者者函数的第40个偏移字节周围处增加一个断点。这样当程序运动到断点处时即可以在函数调使用上层函数前查看各寄存器的值从而进行问题的定位和分析。

运行到产生崩溃异常的指令

一般情况下崩溃函数栈报告中除栈顶函数外的每一层函数名后 + 的数字表明是在当前函数的对应的地址偏移处周围进行了上层函数的调使用,也就是对应的地址偏移周围一般都会存在一条bl指令或者者blr这两条指令,这两条指令的作使用就是执行函数的调使用。

通过二次断点的设置,程序运行到断点时的指令是:

0x18c0248fc <+36>: bl     0x1893042dc   ;0x1893042dc 这个地址就是objc_msgSend的函数地址

本例子的异常崩溃的起因是对一个已经释放的对象继续调使用方法而产生的崩溃。所以当断点停在指令处时,我们可以在右下角的lldb控制台中打印指令:

(lldb)po $x0<__NSArrayM 0x1c044c2a0>(<UIWebOverflowScrollView: 0x1281d7e00; frame = (0 0; 375 603); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x1c0851190>; layer = <WebLayer: 0x1c4426ba0>; contentOffset: {0, 0}; contentSize: {375, 12810}; adjustedContentInset: {0, 0, 0, 0}>)(lldb) p (char*)$x1(char *) $6 = 0x000000018cb9dd70 "release"(lldb) 

可以看出x0是一个数组对象,而x1中则是release方法。这样就进一步明确了是对一个已经释放了的数组对象调使用了release方法而导致异常崩溃了。至于x0是一个什么数组以及保存在哪里,则可以通过汇编指令中的x0寄存器的用进行回溯往上查找指令来进一步分析了。其实这个问题假如进一步观察即可以看出:崩溃的线程并不是出现在主线程,而是在一个工作线程中。而视图的操作基本都应该放在主线程进行,因而当主线程的某些子视图数组对象被释放后,这里又在辅助线程中进行读取访问,就出现了上面的异常崩溃问题了。

在函数调使用bl或者者blr指令处设置断点后,由于根据ABI规则所有非浮点数的参数分别依次保存在x0,x1,….这些寄存器中。所以可以在断点处分别打印出这些寄存器的值即可以知道函数调使用前所传递的参数值了。这个方法非常有助于进行问题的定位和分析。

3.手动重现法

有时候即便你设置了符号断点,场景仍然无法重现,这时候就需要采使用少量特殊的手段,那就是手动的执行方法调使用。实现方式很简单就是在某个演示代码中人为的进行崩溃栈顶函数的调使用。就比方上面的例子当[UIWebDocumentView _updateSubviewCaches]方法一直不被执行时,即可以自己手动的去创立一个UIWebDocumentView对象,并手动的调使用对应的方法_updateSubviewCaches就可。这里存在的两个问题是有可能这个类并没有对外进行公告,或者者我们并不知道方法的参数类型或者者需要传递的值。对于第一个问题处理的方法可以采使用NSClassFromString来得到类信息并进行对象创立。而第二个问题则可以借助少量工具比方class-dump或者者少量其余的手段来确认方法的参数个数和参数类型。总之,目的就是为了能够进入函数的断点,甚至都可以在不知道如何传递参数时将所有的参数都传值为0或者者nil来临时处理问题。下面就是模拟崩溃函数的调使用实现代码:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    // Override point for customization after application launch.        //由于类名和方法名都未对外公开,我们可以借助少量技术手段来让某个特定的方法执行,目的是为了能够进入到方法的内部实现。    Class cls = NSClassFromString(@"UIWebDocumentView");    id obj = [[cls alloc] init];    SEL sel = sel_registerName("_updateSubviewCaches");    [obj performSelector:sel];   //...}

测试代码可以写在任何一个地方,这里为了方便就在程序启动处加上测试代码。等代码编写完毕后,即可以为方法设置符号断点。这样当程序一运行时就肯定能够进入到这个函数的内部去。一旦函数被执行后出现了断点,即可以按照第2种方法中的详情进行崩溃分析了。

其实第3种方法的准则就是只需能让产生崩溃异常的方法被调使用,这其中可以尝试着采使用各种手段将对象和方法run起来。

4.第三方工具静态分析法

前面两种详情的都是动态分析法, 有时候还可以借助少量反编译的工具来对程序代码进行静态分析。比方像Hopper或者者IDA之类的工具。缺点就是这些工具是收费的,而且效果没有动态分析那么的好。在用上个人觉得IDA分析工具更加友好和强大少量。

采使用第三方工具时需要找到产生崩溃的函数所在的库,函数所在的库在崩溃的函数调使用栈列表中就能找到了。假如崩溃函数是在应使用程序本身中被定义,那么需要将上传到appstore的ipa文件解压缩并提取出其中的可执行程序使用工具打开就可。假如崩溃函数是在某个系统库中被定义,那么可从如下的路径:

~/Library/Developer/Xcode/iOS DeviceSupport/

iOS DeviceSupport这个文件夹下的内容将展现你所有曾经联机调试过的各种操作系统版本的库的一份拷贝,假如你没有真机调试过出现崩溃的操作系统版本,请找一个安装了这个操作系统版本的真机设施,并联机,这样你的文件夹中就会有对应的操作系统版本下的系统库的拷贝信息了。

UIKit库的路径

中找到对应的产生崩溃的手机操作系统版本号的库文件:10.3.3(14G60)/Symbols/System/Library/Frameworks/UIKit.framework/UIKit

当使用IDA工具打开对应的库文件或者者可执行文件时你看到的将是这个库文件的所有汇编形式的代码和数据。因而你可以通过搜索菜单来查找产生崩溃的函数或者者方法名。这时候你即可以进一步对产生问题的函数的汇编代码进行分析了。采使用IDA工具进行汇编代码分析的缺点是静态分析无法看到运行时的各个寄存器的真实的值,因而采使用这种方法可能更需要考虑你对汇编代码的了解能力。下面就是本文例子中的[UIWebDocumentView _updateSubviewCaches]方法的实现汇编代码:

IDA工具查看_updateSubviewCaches的实现

采使用IDA工具进行分析时,需要理解少量比方库基地址和代码数据偏移地址以及地址重定向相关的知识。苹果系统为安全对每个库的加载都采使用了ASLR的方式,也就是库所加载的基地址每次运行时都是随机的,这样当某次崩溃发生时需要将产生崩溃时的地址转化为我们通过IDA工具打开的地址。 转换公式为:

   转换后的地址 = 崩溃时寄存器中保存的原始地址值 -  崩溃时地址所在的库的基地址值 + 工具打开库时所设定的基地址。

就以上面崩溃异常为例,当我们使用IDA工具看看x1寄存器中的值究竟是一个什么方法名,那么只要要把x1的值(0x018eb89b7b),减去其所在的库UIKit的基地址值(0x18e03d000),在加上IDA工具打开库时的基地址(要想看基地址则滚动到IDA视图的最开始部分,本次打开的基地址为:0x187769000)。所以x1寄存器中的地址值被转化后应该为:

0x018eb89b7b -  0x18e03d000 + 0x187769000 = 0x1882B5B7B

在IDA工具中将地址跳转到0x1882B5B7B即可以看到本例子中产生崩溃的方法名是叫release:

导致崩溃异常的方法名

当然IDA工具是可以手动进行基地址的自己设置设置的,这样就不需要进行计算以便和线上崩溃的基地址对齐。

假如你手头上没有第三方工具,其实系统内置的otools工具也可以帮我们进行问题的定位以及汇编代码的查看和分析了,具体的方法大家就去查找相关的对otools用的教程就可,这里就不开展了。

总结

上面列出的所有分析方法中有静态分析的也有动态分析。当出现了崩溃时除了从崩溃函数调使用栈去分析问题,还可以从寄存器,以及加载的镜像列表,以及崩溃栈顶部的函数的汇编代码等等进行综合的分析和判断。当然即便这样也不能保证所有问题就肯定能够得到处理,本文中列举的例子只是在实际中的一种非常常见的崩溃异常,希望通过这个示例来起到一个抛砖引玉的效果,毕竟不同的崩溃异常的差异是比较大的。遇到问题需要具体分析,走进函数的内部实现就肯定能够找到产生问题的根源。


欢迎大家访问我的github地址和简书地址

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 手把手教你查看和分析iOS的crash崩溃异常

发表回复