1. 如何追踪app崩溃率,如何解决线上闪退
当iOS设备上的App应用闪退时,操作系统会生成一个crash日志,保存在设备上。crash日志上有很多有用的信息,比如每个正在执行线程的完整堆栈跟踪信息和内存映像,这样就能够通过解析这些信息进而定位crash发生时的代码逻辑,从而找到App闪退的原因。通常来说,crash产生来源于两种问题:违反iOS系统规则导致的crash和App代码逻辑BUG导致的crash,下面分别对他们进行分析。
违反iOS系统规则产生crash的三种类型
- 内存报警闪退当iOS检测到内存过低时,它的VM系统会发出低内存警告通知,尝试回收一些内存;如果情况没有得到足够的改善,iOS会终止后台应用以回收更多内存;最后,如果内存还是不足,那么正在运行的应用可能会被终止掉。在Debug模式下,可以主动将客户端执行的动作逻辑写入一个log文件中,这样程序童鞋可以将内存预警的逻辑写入该log文件,当发生如下截图中的内存报警时,就是提醒当前客户端性能内存吃紧,可以通过Instruments工具中的Allocations 和 Leaks模块库来发现内存分配问题和内存泄漏问题。
- 响应超时当应用程序对一些特定的事件(比如启动、挂起、恢复、结束)响应不及时,苹果的Watchdog机制会把应用程序干掉,并生成一份相应的crash日志。这些事件与下列UIApplicationDelegate方法相对应,当遇到Watchdog日志时,可以检查上图中的几个方法是否有比较重的阻塞UI的动作。
application:didFinishLaunchingWithOptions:
applicationWillResignActive:
applicationDidEnterBackground:
applicationWillEnterForeground:
applicationDidBecomeActive:
applicationWillTerminate:
- 用户强制退出
一看到“用户强制退出”,首先可能想到的双击Home键,然后关闭应用程序。不过这种场景一般是不会产生crash日志的,因为双击Home键后,所有的应用程序都处于后台状态,而iOS随时都有可能关闭后台进程,当应用阻塞界面并停止响应时这种场景才会产生crash日志。这里指的“用户强制退出”场景,是稍微比较复杂点的操作:先按住电源键,直到出现“滑动关机”的界面时,再按住Home键,这时候当前应用程序会被终止掉,并且产生一份相应事件的crash日志。
应用逻辑的Bug
大多数闪退崩溃日志的产生都是因为应用中的Bug,这种Bug的错误种类有很多,比如SEGV:(Segmentation Violation,段违例),无效内存地址,比如空指针,未初始化指针,栈溢出等; SIGABRT:收到Abort信号,可能自身调用abort()或者收到外部发送过来的信号; SIGBUS:总线错误。与SIGSEGV不同的是,SIGSEGV访问的是无效地址(比如虚存映射不到物理内存),而SIGBUS访问的是有效地址,但总线访问异常(比如地址对齐问题); SIGILL:尝试执行非法的指令,可能不被识别或者没有权限; SIGFPE:Floating Point Error,数学计算相关问题(可能不限于浮点计算),比如除零操作; SIGPIPE:管道另一端没有进程接手数据;
常见的崩溃原因基本都是代码逻辑问题或资源问题,比如数组越界,访问野指针或者资源不存在,或资源大小写错误等。
crash的收集
如果是在windows上你可以通过itools或pp助手等辅助工具查看系统产生的历史crash日志,然后再根据app来查看。如果是在Mac 系统上,只需要打开xcode->windows->devices,选择device logs进行查看,如下图,这些crash文件都可以导出来,然后再单独对这个crash文件做处理分析。
市场上已有的商业软件提供crash收集服务,这些软件基本都提供了日志存储,日志符号化解析和服务端可视化管理等服务:
- Crashlytics (www.crashlytics.com)
- Crittercism (www.crittercism.com)
- Bugsense (www.bugsense.com)
- HockeyApp (www.hockeyapp.net)
- Flurry(www.flurry.com)
开源的软件也可以拿来收集crash日志,比如Razor,QuincyKit(git链接)等,这些软件收集crash的原理其实大同小异,都是根据系统产生的crash日志进行了一次提取或封装,然后将封装后的crash文件上传到对应的服务端进行解析处理。很多商业软件都采用了Plcrashreporter这个开源工具来上传和解析crash,比如HockeyApp,Flurry和crittercism等。
由于自己的crash信息太长,找了一张示例:
- crash标识是应用进程产生crash时的一些标识信息,它描述了该crash的唯一标识(E838FEFB-ECF6-498C-8B35-D40F0F9FEAE4),所发生的硬件设备类型(iphone3,1代表iphone4),以及App进程相关的信息等;
- 基本信息描述的是crash发生的时间和系统版本;
- 异常类型描述的是crash发生时抛出的异常类型和错误码;
- 线程回溯描述了crash发生时所有线程的回溯信息,每个线程在每一帧对应的函数调用信息(这里由于空间限制没有全部列出);
- 二进制映像是指crash发生时已加载的二进制文件。以上就是一份crash日志包含的所有信息,接下来就需要根据这些信息去解析定位导致crash发生的代码逻辑, 这就需要用到符号化解析的过程(洋名叫:symbolication)。
解决线上闪退
首先保证,发布前充分测试。发布后依然有闪退现象,查看崩溃日志,及时修复并发布。
2. iOS应用生命周期
应用程序的状态
Not running未运行:程序没启动。
Inactive未激活:程序在前台运行,不过没有接收到事件。在没有事件处理情况下程序通常停留在这个状态。
Active激活:程序在前台运行而且接收到了事件。这也是前台的一个正常的模式。
Backgroud后台:程序在后台而且能执行代码,大多数程序进入这个状态后会在在这个状态上停留一会。时间到之后会进入挂起状态(Suspended)。有的程序经过特殊的请求后可以长期处于Backgroud状态。
Suspended挂起:程序在后台不能执行代码。系统会自动把程序变成这个状态而且不会发出通知。当挂起时,程序还是停留在内存中的,当系统内存低时,系统就把挂起的程序清除掉,为前台程序提供更多的内存。
下面看一下AppDelegate.m文件,这个关乎着应用程序的生命周期:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 当应用程序启动时执行,应用程序启动入口,只在应用程序启动时执行一次。若用户直接启动,lauchOptions内无数据,若通过其他方式启动应用,lauchOptions包含对应方式的内容。
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
//当程序从active转为inactive时会调用这个方法。例如:来电话、信息或者当用户退出程序,程序从前台专为后台的过场。
//用这个方法可以暂定正在执行的任务,暂停计时器以及降低OpenGL的帧率。游戏可以用这个方法来暂停游戏。
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
//用这个方法可以释放资源、保存用户数据、取消计时器以及储存用来恢复程序的状态信息,以免程序被终止。
//如果你的程序支持后台模式,当用户退出程序时,这个方法可以用来替代applicationWillTerminate:
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
//从后台到inactive时回调用这个方法,用这个方法你可以取消之前进入后台很多操作
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
//程序已经进入前台并且处于active状态这个方法可以重启之前在inactive状态下被暂停的任务;如果程序之前处于后台,可以选择在这个方法里刷新UI界面
}
- (void)applicationWillTerminate:(UIApplication *)application {
//当程序即将被终止时调用,如果需要的话可以再次保存数据,可以参见applicationDidEnterBackground:
}
初次启动:
iOS_didFinishLaunchingWithOptions
iOS_applicationDidBecomeActive
按下home键:
iOS_applicationWillResignActive
iOS_applicationDidEnterBackground
点击程序图标进入:
iOS_applicationWillEnterForeground
iOS_applicationDidBecomeActive
当应用程序进入后台时,应该保存用户数据或状态信息,所有没写到磁盘的文件或信息,在进入后台时,最后都写到磁盘去,因为程序可能在后台被杀死。释放尽可能释放的内存。
- (void)applicationDidEnterBackground:(UIApplication *)application
方法有大概5秒的时间让你完成这些任务。如果超过时间还有未完成的任务,你的程序就会被终止而且从内存中清除。
如果还需要长时间的运行任务,可以在该方法中调用
[application beginBackgroundTaskWithExpirationHandler:^{
//....此处执行你的代码....
}];
程序终止
程序只要符合以下情况之一,只要进入后台或挂起状态就会终止:
- iOS4.0以前的系统
- app是基于iOS4.0之前系统开发的。
- 设备不支持多任务
- 在Info.plist文件中,程序包含了 UIApplicationExitsOnSuspend 键。
系统常常是为其他app启动时由于内存不足而回收内存最后需要终止应用程序,但有时也会是由于app很长时间才响应而终止。如果app当时运行在后台并且没有暂停,系统会在应用程序终止之前调用app的代理的方法
- (void)applicationWillTerminate:(UIApplication *)application
,这样可以让你可以做一些清理工作。你可以保存一些数据或app的状态。这个方法也有5秒钟的限制。超时后方法会返回程序从内存中清除。
3.Runtime
Objective-C 是面相运行时的语言(runtime oriented language),就是说它会尽可能的把编译和链接时要执行的逻辑延迟到运行时。这就给了你很大的灵活性,你可以按需要把消息重定向给合适的对象,你甚 至可以交换方法的实现,等等。
RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。
以下面的代码为例:
[obj makeText];
其中obj是一个对象,makeText是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成
objc_msgSend(obj,@selector(makeText));
首先,编译器将代码[obj makeText];转化为objc_msgSend(obj, @selector (makeText));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中 通过SEL查找对应函数method(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若 cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
Objective-C Runtime 是什么?Objective-C 的 Runtime 是一个运行时库(Runtime Library),它是一个主要使用 C 和汇编写的库,为 C 添加了面相对象的能力并创造了 Objective-C。这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体,让 Objective-C 的面相对象编程变为可能。
具体还可以参考这篇文章
Method Swizzling 原理
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。
我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,我们可以利用 class_replaceMethod 来修改类,我们可以利用 method_setImplementation 来直接设置某个方法的IMP,……归根结底,都是偷换了selector的IMP。