当前位置: 首页 > article >正文

一文读懂iOS中的Crash捕获、分析以及防治

Crash系统性总结

  • Crash捕获与分析
    • Crash收集
    • 符号化分析
  • Crash类别以及解法分析
    • 子线程访问UI而导致的崩溃
    • unrecognized selector send to instance xxx
    • KVO crash
    • KVC造成的crash
    • NSTimer导致的Crash
    • 野指针
    • Watch Dog超时造成的crash
    • 其他crash待补充
  • 参考文章:

对于iOS端开发,定位和解决Crash毕竟两个流程,首先是根据线索来分析和定位问题,得到一个大概的猜想,之后按照自己的猜想去提供外部条件,来尝试复现问题,如果问题能够成功复现并复原与线程问题相似的堆栈现场,则基本完成了90%的工作,剩下的10%才是修复此问题。对于crash比例极低的,例如没有版本相关性的,对我们的应用影响极小的,我们可以通过去做AB实验尝试去修复。

大家先思考下以下问题然后阅读文章找到答案

  1. bad_access 的排查途径有哪些 ?
  2. 什么情况下会产生 bad_access ?
  3. 不同的bad_access有什么方案可以完美解决?
    在这里插入图片描述

Crash捕获与分析

Crash收集

收集方式:

  • 利用Xcode获取
    • 将iOS设备连接到Mac电脑。
    • 打开Xcode,选择顶部菜单栏的“Window”。
    • 选中“Organizer”,然后选择“Crashes”标签。
    • 在这里,你可以看到与你的APP关联的所有崩溃日志,选择APP名字以及版本等,就可以查看各种崩溃日志。
  • 友盟、bugly、Sentry(目前我公司使用的就是这个https://sentry.io/for/ios/)等获取。
  • 通过iOS SDK中提供的线程的函数 NSSetUncaughtExceptionHandler用来做异常处理,利用NSSetUncaughtExceptionHandler,当程序退出的时候,可以先进行处理,然后做一些自定义的动作,并通知开发者。(例如:我们把崩溃存在沙盒,等下次用户打开应用的时候,把crash数据上传到我们的服务器)

下面介绍如何自己手动的获取日志(也就是利用NSSetUncaughtExceptionHandler自己实现):

MyUncaughtExceptionHandler.h文件
在这里插入图片描述
MyUncaughtExceptionHandler.m文件
在这里插入图片描述

在这里插入图片描述

AppDelegate.m

在appledelegate导入头文件加上一个异常捕获监听,用来处理程序崩溃时的回调动作 在这里也要判断一下之前有没有崩溃日志 如果有发送给服务器 。

在这里插入图片描述

上方代码就已经 可以获取到 carsh日志了。我们现在来尝试一下,做一个crash代码,然后打开沙盒的log日志。
Carsh代码如下(实现一个kvc中的key为nil的crash):

在这里插入图片描述

取出沙盒的日志如下:

在这里插入图片描述
我们可以通过该表大致的得到 崩溃的原因。

符号化分析

当应用程序在IOS 设备上崩溃(例如,闪退)时,一份“Crash崩溃报告”将在该设备上创建并存储起来。崩溃报告描述了应用程序是在何种条件下崩溃的,大部分情况下包含一份当前正在运行线程的完整堆栈跟踪。
如果设备就在身边,可以连接设备,打开Xcode - Window - Organizer,在左侧面板中选择Device Logs(可以选择具体设备的Device Logs或者Library下所有设备的Device Logs),然后根据时间排序查看设备上的crash日志。这是开发、测试阶段最经常采用的方式。
如果应用程序已经提交到App Store发布,用户已经安装使用了,那么开发者可以 通过iTunes Connect (Manage Your Applications - View Details - Crash Reports)获取用户的crash日志。不过这并不是100%有效的,而且大多数开发者并不依赖于此,因为这需要用户设备同意上传相关信息。然后呢。。。

我们其实在获取到崩溃日志以后,是不知道具体哪行代码崩溃的。这个时候我们就需要获取到dsYM文件,利用dsYM符号化调用栈,找到具体代码行
长话短说就是将运行时信息转换为源码信息,符号化是一种机制,将我们在设备运行时 App 的内存地址和关联的指令信息转换为源码文件中具体文件名、方法名、行数等;可以理解为将运行时机器如何看待处理我们 App 的信息转换成我们开发者如何看待处理我们的 App(源码)。如果缺少这层转换,哪怕只有几行的代码的 App,bug 定位也变得难以进行;一般第三方的crash收集后,我们在集成SDK后,开发者需要在第三方服务的后台配置他们的应用信息,包括应用的标识符、dSYM文件的上传方式等。有些服务允许开发者通过API上传dSYM文件,而有些则要求开发者在构建应用时手动上传。
当应用发生崩溃时,第三方服务会捕获崩溃日志,并使用开发者提供的dSYM文件对日志进行解析。解析后的崩溃报告会包含崩溃发生的文件名、函数名和行号等详细信息,这些信息对于开发者来说是非常有价值的。
下方为博客找到的某个截图示例:
在这里插入图片描述

Crash类别以及解法分析

子线程访问UI而导致的崩溃

Objective-C是一种动态语言,它具有强大的运行时特性。我们可以利用这些特性,设计一套防护系统,以降低应用程序的崩溃率。具体来说,我们可以利用Method Swizzling等技术,对容易造成崩溃的系统方法进行拦截和修改,以达到避免和修复崩溃的目的。
例如,我们可以拦截UIView的setNeedsLayout和setNeedsDisplay方法,确保这些方法只在主线程中被调用。如果它们在子线程中被调用,程序将抛出异常或进行其他错误处理。这样就可以避免因子线程访问UI而导致的崩溃问题。不过我们尽量在做UI操作的时候,转到主线程去做处理。

unrecognized selector send to instance xxx

这样的错误,你可能并不陌生。
这种错误通常是因为调用了某个对象或者某个类里不存在的方法,从而触发了消息转发机制,最终把这个未识别的消息发送给了NSObject的默认实现。

例如调用以下一段代码就会产生crash

//test code
UIButton * testObj = [[UIButton alloc] init];
[testObj performSelector:@selector(someMethod:)];

报错如下:

在这里插入图片描述

runtime中具体的方法调用流程大致如下:

  1. 首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
  2. 如果没找到,在相应操作的对象isa指针指向的类中的方法列表中找调用的方法,如果找到,转向相应实现执行。
  3. 如果没找到,去父类指针所指向的对象中执行1,2.
  4. 以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制。

如果没有重写拦截调用的方法,程序报错。

所以,此类问题解决方案: 拦截调用
在方法调用中说到了,如果没有找到方法就会转向拦截调用。
那么什么是拦截调用呢?
拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理:

+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

在这里插入图片描述

由上图可见,在一个函数找不到时,runtime提供了三种方式去补救:

  1. 调用resolveInstanceMethod给个机会让类动态添加一个该函数的实现。
  2. 调用forwardingTargetForSelector让别的对象去执行这个函数(动态新建类,并给该类创建一个函数实现)
  3. 调用forwardInvocation(函数执行器)灵活的将目标函数分发给其他类来处理。

如果都不中,调用doesNotRecognizeSelector抛出异常。

unrecognized selector crash 防护方案
既然可以补救,我们完全也可以利用消息转发机制来做文章。那么问题来了,在这三个步骤里面,选择哪一步去改造比较合适呢。

这里我们选择了第二步forwardingTargetForSelector来做文章。原因如下:
resolveInstanceMethod 需要在类的本身上动态添加它本身不存在的方法,这些方法对于该类本身来说冗余的
forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写
forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写
选择了forwardingTargetForSelector之后,可以将NSObject的该方法重写,做以下几步的处理:

  1. 动态创建一个桩类
  2. 动态为桩类添加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP
  3. 将消息直接转发到这个桩类对象上。

在这里插入图片描述

下方是一个动态创建类的代码示例:

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 动态创建一个类
        Class dynamicClass = objc_allocateClassPair([NSObject class], "DynamicClass", 0);
        
        if (!dynamicClass) {
            NSLog(@"Failed to allocate class pair");
            return -1;
        }
        
        // 注册这个类
        objc_registerClassPair(dynamicClass);
        
        // 动态创建一个实例
        id instance = [[dynamicClass alloc] init];
        NSLog(@"Instance of DynamicClass: %@", instance);
        
        // 动态添加方法
        class_addMethod(dynamicClass, @selector(sayHello), (IMP)sayHelloIMP, "v@:");
        
        // 调用动态添加的方法
        [instance sayHello];
    }
    return 0;
}

// 方法的实现
void sayHelloIMP(id self, SEL _cmd) {
    NSLog(@"Hello from DynamicClass!");
}

在这个示例中,我们首先使用objc_allocateClassPair创建一个新的类,然后使用objc_registerClassPair注册这个类。接着,我们动态添加了一个名为sayHello的方法,并调用它。
解释参数
objc_allocateClassPair:用于分配一个新的类对,第一个参数是父类,第二个参数是新类的名称,第三个参数是额外的内存大小(通常为0)。
objc_registerClassPair:用于注册这个类,使其可以被使用。
class_addMethod:用于向类添加一个新的方法。第一个参数是目标类,第二个参数是选择器(SEL),第三个参数是方法的实现(IMP),第四个参数是方法的签名(type encoding)。

KVO crash

KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。
苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。

1、注册观察
在这里插入图片描述
2、实现回调方法
在这里插入图片描述

3、移除观察
在这里插入图片描述

KVO举例以及注意事项

//被观察者 StockData.m
#import "StockData.h"
@interface StockData()
@property(nonatomic, strong)NSString *stockName;
@property(nonatomic, strong)NSString *price;
@end

//观察者 SLVKVOController.m
#import "SLVKVOController.h"
#import "StockData.h"

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.stockData setValue:@"searph" forKey:@"stockName"];
    [self.stockData setValue:@"10.0" forKey:@"price"];
    [self.stockData addObserver:self forKeyPath:@"price"  options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:SLVKVOContext];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if(context == SLVKVOContext && object == self.stockData && [keyPath isEqualToString:@"price"]) {
        NSString * oldValue = [change objectForKey:NSKeyValueChangeOldKey];
        NSString * newValue = [change objectForKey:NSKeyValueChangeNewKey];
        self.myLabel.text = [NSString stringWithFormat:@"oldValue:%@ , newValue:%@",oldValue,newValue];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

-(void)dealloc {
    [self.stockData removeObserver:self forKeyPath:@"price" context:SLVKVOContext];
}

KVO常见crash及防护方案

KVO常见crash类型:

1.不能对不存在的属性进行kvo观测,否则会报crash:uncaught exception 'NSUnknownKeyException', reason: '[<StockData 0x600000203d50> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key stockName.'
2. 订阅者必须写observeValueForKeyPath:ofObject:change:context:方法,否则crash。
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<SLVKVOController: 0x7f811372ff70>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
3.移除观察,超过addObserver的次数就会 crash:Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <SLVKVOController 0x7ff8e8703100> for the key path "price" from <StockData 0x60800003d000> because it is not registered as an observer.'

KVO crash解决方案:
首先为 NSObject 建立一个分类,利用 Method Swizzling,实现自定义的 BMP_addObserver:forKeyPath:options:context:BMP_removeObserver:forKeyPath:BMP_removeObserver:forKeyPath:context:BMPKVO_dealloc 方法,用来替换系统原生的添加移除观察者方法的实现。
然后在观察者和被观察者之间建立一个 KVODelegate 对象,两者之间通过 KVODelegate 对象 建立联系。然后在添加和移除操作时,在自定义的方法交换内部将 KVO 的相关信息例如 observer、keyPath、options、context 保存为 KVOInfo 对象,并添加到 KVODelegate 对象 中对应 的 关系哈希表 中,对应原有的添加观察者。

关系哈希表的数据结构:{keypath : [KVOInfo 对象1, KVOInfo 对象2, … ]}
在添加和移除操作的时候,利用 KVODelegate 对象 做转发,把真正的观察者变为 KVODelegate 对象,而当被观察者的特定属性发生了改变,再由 KVODelegate 对象 分发到原有的观察者上。
那么,BayMax 系统是如何避免 KVO 崩溃的呢?
添加观察者时:通过关系哈希表判断是否重复添加,只添加一次。
移除观察者时:通过关系哈希表是否已经进行过移除操作,避免多次移除。
观察键值改变时:同样通过关系哈希表判断,将改变操作分发到原有的观察者上。
另外,为了避免被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO 导致崩溃。BayMax 系统还利用 Method Swizzling 实现了自定义的 dealloc,在系统 dealloc 调用之前,将多余的观察者移除掉。

KVC造成的crash

场景1:key 不存在
在这里插入图片描述
在这里插入图片描述
防护方法:进行 KVC Crash 防护,我们就需要重写 setValue: forUndefinedKey: 方法和 valueForUndefinedKey: 方法。重写这两个方法之后,就可以防护key不存在的情况了。

场景2:key为nil
在这里插入图片描述
在这里插入图片描述

**防护方法:**可以利用 Method Swizzling 方法,在 NSObject 的分类中将setValue:forKey:和自定义的 ysc_setValue:forKey: 进行方法交换。然后在自定义的方法中,添加对 key 为 nil 这种类型的判断。

Person * person = [[Person alloc] init];
[person setValue:nil forKey:@“name”];
当value为nil的时候不会Crash.

NSTimer导致的Crash

NSTimer、CADisplayLink会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用。

先来看看timer最常用的写法

@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TimerViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- (void)timerRun {
    NSLog(@"%s", __func__);
}
- (void)dealloc {
    [self.timer invalidate];
    NSLog(@"%s", __func__);
}
@end

循环引用了
在这里插入图片描述

解决方案1:使用weakSelf (这里使用的是timer的block方法)

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerRun];
    }];
}

在这里插入图片描述

解决方案2:加入了一个中间代理对象LJProxy,timer的target不直接是TimerViewController,而是持有LJProxy实例,让LJProxy实例来弱引用TimerViewController,timer强引用LJProxy实例. 而且代理类里面要重写消息转发方法去处理一下,要不然消息传递会找不到方法导致崩溃。

LJProxy可以继承自NSObject,也可以继承自NSProxy,但是内部代码处理会有所不同。
如果继承自NSProxy,会实现下面的方法,消息转发方法需要实现这两个。

在这里插入图片描述
在这里插入图片描述

如果继承自NSObject,会实现下面的方法,消息转发方法需要实现这一个即可。

@interface LJProxy : NSObject
+ (instancetype) proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation LJProxy
+ (instancetype) proxyWithTarget:(id)target
{
    LJProxy *proxy = [[LJProxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
    //如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。
}
@end
- (void)viewDidLoad {
    [super viewDidLoad];
    // 这里的target发生了变化
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LJProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

由于NSProxy专门用来做消息转发的,效率高,因为这个内部直接去消息转发,调用methodSignature…,如果继承自NSObject,里面调用会先去父类里面搜索,没有的话才会去做消息转发。
所以这里建议做代理类的时候直接继承自NSProxy的就可以,这样子是最好的。

解决方案3: 及时的把timer销毁,即:调用 [self.timer inbalidate]; 例如 识别到当前控制器返回按钮点击的时候 等等。 (该方法不推荐,代码混乱,不具有统一性。)

解决方案4:「出自 高性能iOS开发 一书」。
在这里插入图片描述

这里的间接层和 方案2类似,但是 只不过是在间接层里边进行 timer的创建以及timer的销毁。

控制器直接调用间接层并传入self「后边会赋给delegate<weak引用>」和selector。 间接层会创建timer并倒计时处理事件。 处理事件之后会 通过delegate 调用selector. [self.delegate performSelector:@selector(self.selector) withObject:想要传的值];

这里我觉得方案4是最容易理解的。也是最容易实施的。

方案4代码:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

野指针

  • 野指针就是指向一个被释放或者被回收的对象,但是指向该对象的指针没有任何修改,以致于该指针让指向已经回收后的内存地址。
  • 其中访问野指针是没有问题的,使用野指针的时候会出现Crash,样例如下:

在这里插入图片描述

这是网友总结的,有兴趣的可以看下:www.jianshu.com/p/9fd4dc046… 本人,也就是看看乐呵,其原理啥的,见仁见智吧。开发行业太j8难了!

Watch Dog超时造成的crash

这种崩溃通常比较容易分辨,因为错误码是固定的0x8badf00d。(程序员也有幽默的一面,他们把它读作Ate Bad Food。)在iOS上,它经常出现在执行一个同步网络调用而阻塞主线程的情况。因此,永远不要进行同步网络调用。

其他crash待补充



参考文章:

crash收集:https://sentry.io/for/ios/
crash日志分析:https://developer.volcengine.com/articles/7062608853434630152
方法找不到解决方案:https://neyoufan.github.io/2017/01/13/ios/BayMax_HTSafetyGuard/
消息转发机制以及避免崩溃方案:https://blog.csdn.net/mumubumaopao/article/details/108113405
kvo crash解决方案:https://juejin.cn/post/6844903927469588488
https://github.com/itcharge/YSC-Avoid-Crash
kvc 防护:https://blog.csdn.net/lianai911/article/details/103400862
NSTimer循环引用问题处理:https://www.jianshu.com/p/d4589134358a
野指针定位:https://www.jianshu.com/p/9fd4dc046046?utm_source=oschina-app


http://www.kler.cn/a/509542.html

相关文章:

  • 某国际大型超市电商销售数据分析和可视化
  • Nginx三种不同类型的虚拟主机(基于域名、IP 和端口)
  • 【论文阅读】基于空间相关性与Stacking集成学习的风电功率预测方法
  • VS Code--常用的插件
  • Spring框架 了解
  • Jupyter notebook中运行dos指令运行方法
  • 高斯数据库 Shell 脚本:批量执行 SQL 文件
  • C++ 成员初始化列表
  • 二、点灯基础实验
  • Unreal Engine 5 C++ Advanced Action RPG 九章笔记
  • 迅为RK3568开发板篇OpenHarmony实操HDF驱动控制LED-编写内核 LED HDF 驱动程序
  • 搜维尔科技提供完整的人形机器人解决方案以及训练系统
  • 机器学习加州房价预测模型报告
  • 华为数据中心CE系列交换机级联M-LAG配置示例
  • 13-1类与对象
  • 【21】Word:德国旅游业务❗
  • 游戏引擎学习第81天
  • 探索 Transformer²:大语言模型自适应的新突破
  • wow-agent---task2使用llama-index创建Agent
  • Ubuntu 空闲硬盘挂载到 文件管理器的 other locations
  • Apache 如何设置 Upgrade-Insecure-Requests 报头 ?
  • 用于零镜头视频对象分割的深度感知测试时训练
  • PyTorch基本功能与实现代码
  • 浙江安吉成新照明电器:Acrel-1000DP 分布式光伏监控系统应用探索
  • vector迭代器的使用以及迭代器失效
  • 非科班转码第5年零241天