- iOS全解1:基础/内存管理/Block/GCD
- iOS全解2:Runloop
- iOS全解3:Runtime
-
iOS全解4:KVC/KVO、通知/推送/信号量、Delegate/Protocol、Singleton
iOS全解5:网络协议 HTTP、Socket
iOS全解6:CoreAnimation/Layer
iOS全解7:音频/视频
iOS全解8:启动优化、性能优化、App后台保活、崩溃检测(当前位置
)
iOS全解9:编程思想、架构、组件化、RAC
iOS全解15: iOS编译原理
一、基础概念
程序编译一般需经几个步骤:预处理
、编译
、汇编
、链接
。
编译
:是将将人类可读的程序代码
文本 --> 翻译成为 --> 计算机可以执行的二进制指令机器码
。(即:源程序 --> 目标程序)
编译器前端 Clang
:
预处理 --> 词法分析 --> 语法分析 --> 生成IR(Clang Code Generator)
编译器后端 LLVM
:
对IR优化 --> 目标代码--> 汇编器 --> 机器码(LLVM Code Generator)--> -Mac-O文件
-> dyId链接
解释器:
是在运行时才去解析代码,获取一段代码后就会将其翻译成目标代码(就是字节码:Bytecode
),然后一句一句地执行目标代码。解释器可以在运行时去执行代码,说明它具有动态性,程序运行后能够随时通过增加和更新代码来改变程序的逻辑。
编译器:
把一种编程语言(原始语言)转换为另一种编程语言(目标语言),编译生成一份完整的机器码再去执行。
编译器 -> 每个文件进行编译 -> Mach-O(可执行文件)
链接器 -> 项目中的多个 Mach-O 文件 -> 合并成一个
可执行文件:
(executable file) 指的是可以由操作系统进行加载执行的文件。在不同的操作系统环境下,可执行程序的呈现方式不一样。在windows操作系统下,可执行程序可以是 .exe文件 .sys文件 .com等类型文件。
可执行程序:
(executable program,EXE File)是可在操作系统存储空间中浮动定位的二进制可执行程序。它可以加载到内存中,由操作系统加载并执行。特定的CPU指令集(如X86指令集)对应的不同平台之间的可执行程序不可直接移植运行。
- GNU:是一个自由的操作系统,其内容软件完全以GPL方式发布。
- GCC: 由 GNU 开发的编程语言编译器。
- LLVM:是一个模块化和可重用的编译器和工具链技术的集合。Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器,目的是提供惊人的快速编译,比 GCC 快3倍。
- Clang:是一个C语言、C++、Objective-C语言的轻量级编译器。
DSYM
• dSYM 文件是什么:具有调试信息的目标文件。 Xcode编译项目后,会有一个项目同名的 dSYM 文件,dSYM 是保存 16 进制函数地址映射信息的中转文件。
• dSYM 文件的作用: release 模式打包或上线后,崩溃错误不直观,这时就需要分析 crash report
文件,iOS 设备中会有日志文件保存我们每个应用出错的函数内存地址,通过 Xcode 的 Organizer
可以将 iOS 设备中的 DeviceLog
导出成 crash 文件
,这个时候我们就可以通过出错的函数地址去查询 dSYM 文件中程序对应的函数名和文件名。大前提是我们需要有软件版本对应的 dSYM 文件,所以要保存每个发布版本的 Archives 文件
。
Xcode 的Organizer ---> iOS设备中DeviceLog ---> 导出 (crash文件:应用出错的函数内存地址)
保存每个发布版本的 Archives 文件:软件版本对应的 dSYM 文件 --> 对应的函数名和文件名
dsym 解析工具地址 :
1、1.0.3版下载
2、Git: dSYMTools
App包文件:
- Executable:可执行文件(Mach_O文件)
- Dylib:动态库
- Bundle:无法被连接的动态库,只能通过dlopen()加载
- Image:指的是Executable,Dylib或者Bundle的一种,文中会多次使用Image这个名词。
- Framework:动态库和对应的头文件和资源文件的集合
1、iOS可执行文件 初探 ipa包
:(iPhone Application)
作为iOS客户端开发者,我们比较熟悉的一种文件是ipa包。但实际上这只是一个变相的zip压缩包
,我们可以把一个ipa文件直接通过unzip命令解压
。
解压之后:会有一个Payload目录
,而Payload里则是一个.app文件
,而这个实际上又是一个目录,或者说是一个完整的App Bundle
。
ipa包(zip压缩包,用unzip命令解压 )
--> Payload文件夹
--> .app文件
--> 显示包内容:
App Bundle: bundle、nib、plist、json、html、图片、视频、音频、mac-O[exec] 等文件。
拆分二进制文件:经常用于整合静态库
瘦身
$ lipo 002--可执行文件 -thin armv7 -output macho_armv7
$ lipo 002--可执行文件 -thin armv64 -output macho_armv64
整合
$ lipo -create macho_armv7 macho_arm64 -output machO_v7_64
查看文件的方式:
方式1:终端查看
来到Mach-O文件所在位置,输入相关命令得到Mach-O文件信息。
方式2: 工具查看(更直观)
首先要下载一个可以查看Mach-O文件格式的工具 MachOView
常见的格式:
1. 可执行文件
2. objcet库文件
◦.o 文件(目标文件:.m -> .o)
◦.a 静态库文件.其实就是N个.o文件的集合
3. DYLIB: 动态库文件
◦ dylib
◦ framework
4. 动态链接器 dyld
5. DSYM(打包上架用于监测崩溃信息)
静态库 和 动态库 的存在形式和使用区别?
存在形式:
• 静态库:以".a"或者“.framework”为文件后缀名 xx.a xx.framework
• 动态库:以".dylib"或者“.framework”为文件后缀名(Xcode7 之后 .tbd 代替了 .dylib)
Mach-O的组成结构包括:(MachOView 汇编分析)
• Mach64 Header (头部)
• Load commands(加载命令): 加载的指令集,库、数据表,描述了下面的数据
• Section64:全是数据
• Section64(__TEXT): 代码段
• Section64(__DATA): 数据段,读写全局变量等 (Data包含多个Segment)
• Section64(__LINKEDIT): linkedit
• Segment中包含多个 Section(节)
从这张图上来看,Mach-O文件的数据主体可分为三大部分分别是:
1、头部(Header)包含可以执行的CPU架构指令集,比如x86,arm64。
2、加载命令(Load commands)
3、数据(Data)数据,包含load commands中需要的各个段(segment)的数据
• CS (Code Segment):代码段寄存器
• DS (Data Segment):数据段寄存器
• SS (Stack Segment):堆栈段寄存器
• ES (Extra Segment):附加段寄存器
• SI: 源变址 寄存器(source)
• DI:目的变址 寄存器(destination)
IP:指令指针寄存器 IP= IP+所读取指令的长度,从而指向下一条指令
内存分区域 特点:
• 代码区: 可读可写可执行
• 栈区域: 放参数和局部变量
• 堆区域: 动态申请 可读可写
• 全局: 可读可写
• 常量区: 只读!
动态加载器/动态链接器 (dyld)
dyld简介:
全称(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。它是开源的,可以通过苹果官网下载它的源码来解系统加载动态库的细节。
苹果的开源项目,dyld下载地址:http://opensource.apple.com/tarballs/dyld。 直接下载dyld
- dynamic linker:动态链接器
- dynamic loadel:动态加载器
App启动的过程的详细解释:
- 启动图标:动态链接绑定 函数符号表
- main():入口函数
- UIApplicationMain():主应用对象
- willFinishLaunchingWithOptions() :即将完成启动
- didFinishLaunchingWithOptions():已经完成启动
- Load main UI():加载主UI (xib、storyboard、tabbar等)
- Final intiallization:完成应用初始化
- applicationDidBecomeActive():应用程序已变为活动状态
——————————————————————————————
加载动态库
dyld会首先读取mach-o文件的
Header
和load commands
。
接着就知道了这个可执行文件「依赖的动态库」。例如加载动态库A到内存,接着检查A所依赖的动态库,就这样的递归加载,直到所有的动态库加载完毕。通常一个App所依赖的动态库在 100~400 个左右,其中大多数都是系统的动态库,它们会被缓存到dyld shared cache(动态缓存库)
,这样读取的效率会很高。
1、加载 Mach-O 文件:dyld 动态加载(DyLib动态库,在动态缓存区,每次启动地址不一样!)
2、ASLR技术:MachO文件加载的时候是随机地址,使用动态共享缓存库(地址空间布局随机化 ASLR
是偏移地址)
3、PIC(Position independence code 代码位置独立)
3.1 如果MachO内部需要调用 系统的库函数时
3.2 先在 Data段中建立一个指针(符号),指向外部函数
3.3 DYLD会动态的进行绑定,将MachO中的Data段中的指针,指向外部函数!
链接器:
在链接多个目标文件的过程中,会创建一个符号表
,用于记录所有已定义的和所有未定义的符号。链接时如果出现相同符号的情况,就会出现“ld: dumplicate symbols
”的错误信息;如果在其他目标文件里没有找到符号,就会提示“Undefined symbols
”的错误信息。
符号表
里面全是指针和地址
offset:地址的偏移量
Data: 符号表的下标
Description:索引的描述
Value:值
动态库:共享缓存
为了提高性能,系统的动态库文件都存在了动态库共享缓存里面!
共享缓存机制
在iOS系统中,每个程序依赖的动态库都需要通过dyld
一个一个加载到内存,然而,很多系统库几乎是每个程序都会用到的,如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能
,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存分别保存着,iPhone6里面就有dyld_shared_cache_armv7s
和dyld_shared_cache_armv64
两个文件,如下图所示。
dyld:位于/usr/lib/dyld
二、启动优化/性能优化
2.1、 App启动的过程:
1、加载 Mach-O 文件 到进程。
2、dyld 动态加载 DyLib动态库,链接多个目标文件,创建一个函数 符号表
(符号地址是随机的)
3、Rebase:链接系统库符号,修正内部(指向当前mach-o文件)的指针指向。
4、Bind:绑定函数符号,修正外部指针指向。根据字符串匹配的方式查找符号表,这个过程比Rebase慢。
然后把上述3、4绑定的函数结果写入缓存。
5、初始化OC Runtime,加载 +load 函数,包括ObjC相关Class的注册、category注册、selector唯一性检查等
6、其它的初始化代码,加载静态构造器:constructor
main阶段:加载所有的依赖库库的lnitializer。
7、首屏渲染
在执行main函数之前,需要把类的信息注册到一个全局的Table中。同时,Objective C支持Category,在初始化的时候,也会把Category中的方法注册到对应的类中,同时会指向唯一Selector,这也是为什么当你的Cagegory实现了类中同名的方法后,类中的方法会被覆盖。
寄存器 > 高速缓存 > 内存 > 磁盘(存放代码)
点开app (磁盘
) --> 代码读入(内存
)里 --> 立刻马上,将有8M的代码 放入(高速缓存
)--> 执行命令(寄存器
)
DyLib动态库:在动态缓存区,每次启动地址不一样!
所有关于多线程的安全操作:
防止资源资源抢夺!都是对内存区域的操作进行保护!
使用dyld2启动应用的过程如图:
2.2、App的 启动时间
总的启动时间 LaunchTime 包括main()
调用之前的pre-main
Time1(T1),加上从main()到applicationDidBecomeActive()
的时间Time2(T2)。
LaunchTime = Time1 + Time2
1、main()之前:加载MacO文件,动态链接函数的符号表
2、main()到 applicationDidBecomeActive()`:入口程序加载(UIApplication对象、开启Runloop、加载infol.plist、加载Main.stroyBoard/ tableBar、通知应用程序代理、和自定义的配置逻辑)
获取启动时间
我们可以通过环境变量的方法来获取pre-main time
。打开Xcode->Product->Scheme->Edit Scheme
或者直接command+shift+<
(在键盘上是逗号,按住shift就是小于号了)。在Edit Scheme
中添加DYLD_PRINT_STATISTICS
这个环境变量,如果要打印详细的时间分布,可以将value设为1。(-FIRAnalyticsDebugEnabled
)
————————————————
运行项目之后就会在控制台会打印出每个阶段都耗时多少。
打印启动时间
Total
pre-main
time: 207.80 milliseconds (100.0%) //总启动时间
dylib
loading time: 72.69 milliseconds (34.9%) //动态库加载时间
rebase/binding
time: 8.99 milliseconds (4.3%) //函数符号绑定,地址修正
ObjC setup
time: 25.89 milliseconds (12.4%) //ObjC类初始化
initializer
time: 100.20 milliseconds (48.2%) //其他初始化
slowest intializers :
libSystem.B.dylib
: 4.27 milliseconds (2.0%) //链接系统库B
libMainThreadChecker
.dylib : 51.87 milliseconds (24.9%) //debug时候检查线程的
ProjectArchitecture : 65.38 milliseconds (31.4%)
main()函数之前耗时的影响因素
- 动态库加载越多,启动越慢。
- ObjC类越多,启动越慢
- C的constructor函数越多,启动越慢
- C++静态对象越多,启动越慢
- ObjC的+load越多,启动越慢
我们已经获取到了main()函数之前的启动时间T1,至于T2,我们可以使用Xcode自带工具Instruments
里面的Time Profiler
来获取,也可以在main()的第一句和applicationDidBecomeActive()
的最后一句加上获取时间的代码CFAbsoluteTimeGetCurrent()
。
点击左上角红色按钮运行,勾选左下角Call Tree
中Separate Thread
和Hide System Libraries
,等到第一个页面显示出来的之后,点击左上角暂停按钮,下面就会统计出每个步骤的耗时情况。这个时候我们就可以很容易得到启动时间 Time2。
然后就可以 根据 Time1 和 Time2 时间段的内容进行优化了。
2.3、冷启动、热启动
启动方式 | 定义 |
---|---|
冷启动 | 手机启动后或者相当长的时间间隔后,某个APP第一次启动 |
热启动 | APP挂到后台,之后点击APP再回来到前台,启动所需要的数据仍然在缓存中 |
启动时间在小于400ms
是最佳的,因为从点击图标到显示Launch Screen
,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。
引用博客解释下
静态构造器 (constructor)
:
constructor 和 +load 都是在 main 函数执行前调用,但 +load 比 constructor 更加早一丢丢,因为dyld
(动态链接器,程序的最初起点)在加载image
(可以理解成Mach-O 文件
)时会先通知 objc runtime 去加载其中所有的类,每加载一个类时,它的 +load 随之调用,全部加载完成后,dyld 才会调用这个 image 中所有的 constructor 方法。
所以 constructor 是一个干坏事的绝佳时机:
1、所有 Class 都已经加载完成
2、main 函数还未执行
3、无需像 +load 还得挂载在一个 Class 中
总体原则无非就是减少启动的时候的步骤,以及每一步骤的时间消耗。
2.4、启动优化 方向:
1、减少启动初始化的流程,使用懒加载,放后台初始化,减少动态库/静态库的加载依赖;
能够延时初始化的就延时,不要卡主线程的启动时间,已经下线的业务直接删掉;
2、优化代码逻辑,去除一些非必要的逻辑和代码,减少每个流程所消耗的时间; 合并或者删减一些OC类和函数。
3、将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数(创建虚函数表有开销)
4、移除不需要用到的类, 减少selector、category的数量,如合并功能类似的类和Category
5、启动阶段使用多线程来进行初始化,把CPU的性能尽量发挥出来;
6、使用纯代码,减少xib或者storyboard来进行UI框架的搭建,尤其是主UI框架比如TabBarController这种,因为xib和storyboard也还是要解析成代码来渲染页面,多了一些步骤。
2.4、性能优化
离屏渲染
在 OpenGL 中,GPU 有两种渲染方式:
On-Screen Rendering
:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作;
Off-Screen Rendering
:离屏渲染,在当前屏幕缓冲区外开辟新的缓冲区进行渲染操作;
离屏渲染消耗性能的原因:
离屏渲染的整个过程,需要多次切换上下文环境
,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),渲染结束后,将离屏缓冲区的渲染结果显示到屏幕上,上下文环境从离屏切换到当前屏幕,这个过程会造成性能的消耗。
哪些操作会触发离屏渲染?
1、光栅化:layer.shouldRasterize = YES
2、遮罩:layer.mask
3、圆角:同时设置 layer.masksToBounds = YES,layer.cornerRadius > 0
;(可以用 CoreGraphics 绘制裁剪圆角,可以用曲线)
4、阴影(如果设置了layer.shadowPath
不会产生离屏渲染)
- 可以考虑使用CALayer取代UIView;
- 不要频繁地调用UIView的相关属性,比如
frame、bounds、transform
等属性,尽量减少不必要的修改,提前计算这些属性值;Autolayout
会比直接设置frame
消耗更多的CPU资源。
2.5、卡顿检测
这里的卡顿检测主要是针对在主线程执行了耗时的操作所造成的,这样可以通过 RunLoop 来检测卡顿:添加 Observer 到主线程 RunLoop 中,通过监听 RunLoop 状态的切换的耗时,达到监控卡顿的目的。
一 检测的方案根据线程是否相关分为两大类:
1、执行「耗时任务」会导致CPU短时间无法响应其他任务,检测任务耗时来判断是否可能导致卡顿
2、由于卡顿直接表现为操作无响应,界面动画迟缓,「检测主线程」是否能响应任务来判断是否卡顿
与主线程相关的检测方案包括:
1、fps(每秒传输帧数 Frames Per Second)默认刷新率都在60Hz(即75帧/秒)以上,要避免动作不流畅的最低是30Hz。
2、ping:网络测试工具
3、runloop:线程循环对象与主线程不相关的检测包括:
1、stack backtrace:回溯调用栈
2、msgSend observe:观察消息转发
1、FPS 监测
通常情况下,屏幕会保持60hz/s
的刷新速度,每次刷新时会发出一个屏幕刷新信号,CADisplayLink
可以注册一个与刷新信号同步的回调处理。可以通过屏幕刷新机制来展示fps
值:
FPS 监测CADisplayLink 核心代码
1.通过打印,我们知道每次刷新时会发出一个屏幕刷新信号,则与刷新信号同步的回调方法fpsDisplayLinkAction:
会调用,然后count
加一。
2.每隔一秒,我们计算一次 fps
值,用一个变量_lastTime记录上一次计算 fps 值的时间,然后将 count 的值除以时间间隔,就得到了 fps 的值,在将 _lastTime 重新赋值,_count 置成零。
3.正常情况下,屏幕会保持60hz/s的刷新速度,所以1秒内fpsDisplayLinkAction:
方法会调用60次。fps 计算的值为0,就不卡顿,流畅。
4.如果1秒内fpsDisplayLinkAction:
只回调了50次,计算出来的fps就是 _count / delta(时间间隔) 。
2.6、耗电优化
耗电的主要来源为:
1、CPU 处理;
2、网络请求;
3、定位;
4、图像渲染;
优化思路
- 尽可能降低 CPU、GPU 功耗;
- 少用定时器;
- 优化 I/O 操作;
- 尽量不要频繁写入小数据,最好一次性批量写入;
- 读写大量重要数据时,可以用 dispatch_io,它提供了基于
- GCD 的异步操作文件的 API,使用该 API 会优化磁盘访问;
- 数据量大时,用数据库管理数据;
- 网络优化;
- 减少、压缩网络数据(JSON 比 XML 文件性能更高);
- 若多次网络请求结果相同,
尽量使用缓存
; - 使用断点续传,否则网络不稳定时可能多次传输相同的内容;
- 网络不可用时,不进行网络请求;
- 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间;
- 批量传输,如下载视频,不要传输很小的数据包,直接下载整个文件或者大块下载,然后慢慢展示;
- 定位优化;
- 如果只是需要快速确定用户位置,用
CLLocationManager
的requestLocation
方法定位,定位完成后,定位硬件会自动断电; - 若不是导航应用,尽量不要实时更新位置,并为
完毕就关掉定位服务
; - 尽量降低定位精度,如不要使用精度最高的 KCLLocationAccuracyBest;
- 需要后台定位时,尽量设置
pausesLocationUpdatesAutomatically 为 YES
,若用户不怎么移动的时候,系统会自暂停位置更新;
- 如果只是需要快速确定用户位置,用
2.5、Tableview的优化
1、提前计算好cell的高度
缓存在相应的数据源模型中
2、 尽可能的降低 storyboard、xib
等使用度
通过Interface知道xib/storyboard
本身就是一个xml 文件
,添加删除控件必然中间多了一个encode/decode
过程,增加了CPU
的计算量。并且 还要避免臃肿的 XIB 文件,因为XIB文件在主线程中进行加载布局;当用到一些自定义XIB文件时,XIB的加载会把所有内容加载进来,如果XIB里面的一些控件并不会用到,这就可能造成一些资源的消耗浪费。
3、滑动过程中尽量减少重新布局
约束最终还是转换成frame
,大量的约束重叠也会增加CPU
的计算量
4、不要阻塞主线程
UIKit的工作基本上都是在主线程上进行,界面绘制,用户输入响应等等.当所有的代码逻辑都放在主线程时,某些耗时任务可能会卡住主线程造成程序无法响应,流畅度降低等问题;在主线程中绘制大量界面图层,网络I/O,磁盘I/O等都可以造成界面卡顿现象.
下面我们通过Xcode自带的调试工具Instruments来看看项目界面的流畅度,及其一些建议,Instruments给我提供了各种各样的调试查看工具,下面简单介绍一下:
1)Blank: 创建一个空的模板,可以从Library库中添加其他模板.
2)Activity Monitor: 监控进程级别的CPU,内存,磁盘,网络使用情况,可以得到你的应用程序在手机运行时总共占用的内存大小.
3)Allocations: 跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史,可以检测每一个堆对象的分配内存情况.
4)Cocoa Layout : 观察NSLayoutConstraint对象的改变,帮助我们判断什么时间什么地点的constraint是否合理.观察约束变化,找出布局代码的问题所在.
5)Core Animation: 这个模块显示程序显卡性能以及CPU使用情况,查看界面流畅度.
6)CoreData: 这个模块跟踪Core Data文件系统活动.
7)Counters : 收集使用时间或基于事件的抽样方法的性能监控计数器(PMC)事件.
8)Energy Log: 耗电量监控.
9)File Activity: 检测文件创建,移动,变化,删除等.
10)Leak: 一般的措施内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录.
11)Metal System Trace: Metal API是apple 2014年在ios平台上推出的高效底层的3D图形API,它通过减少驱动层的API调用CPU的消耗提高渲染效率.
12)Network: 用链接工具分析你的程序如何使用TCP/IP和UDP/IP链接.
13)SceneKit: 3D性能状况分析.
14)System Trace: 系统跟踪,通过显示当前被调度线程提供综合的系统表现,显示从用户到系统的转换代码通过两个系统调用或内存操作.
15)System Usage: 这个模板记录关于文件读写,sockets,I/O系统活动,输入输出.
16)Time Profiler(时间探查): 执行对系统的CPU上运行的进程低负载时间为基础采样.
17)Zombies: 测量一般的内存使用,专注于检测过度释放的野指针对象,也提供对象分配统计,以及主动分配的内存地址历史.
本文主要使用的是Instruments中的第5个工具:Core Animation(图形性能),这个模块显示程序显卡性能以及CPU使用情况,查看界面流畅度.
首先我们必须要把源码安装到测试设备上,1)连接Xcode运行程序;2)然后选择快捷键(Command + Control + i)调出Instruments,选择Core Animation.打开后我们可以看到Debug Options里面有多个调试选项。
三、App后台保活
1、短时间保活的方式有beginBackgroundTaskWithName 和 endBackgroundTask
2、App长时间保活的方式有:播放无声音乐、后台持续定位、后台下载资源、BGTaskScheduler等;
3、唤醒App的方式有:推送、VoIP等;
数据重用/缓存、尽量使用layer绘图 代替View
四、崩溃检测
void uncaughtExceptionHandler(NSException *exception)
{
NSArray *stackArray = [exception callStackSymbols]; //异常的堆栈信息
NSString *reason = [exception reason]; //出现异常的原因
NSString *name = [exception name]; //异常名称
NSString *exceptionInfo = [NSString stringWithFormat:@"================异常崩溃报告================\n name:\n%@\n reason:\n%@\n callStackSymbols:\n%@ \n \n ",name,reason,[stackArray componentsJoinedByString:@"\n"]];
NSLog(@"---> exceptionInfo: %@",exceptionInfo);
// 保存到本地:当app再次打开后,获取本地保存的崩溃信息并上传到服务器即可。
[CatchCrash saveAsText:exceptionInfo];
// 发送邮件
[CatchCrash sendEmail:exceptionInfo];
}