做个小网站 虚拟空间 买服务器,济宁seo营销,诚聘php网站开发师,做搜索引擎优化网站费用Crash系统性总结 Crash捕获与分析Crash收集符号化分析 Crash类别以及解法分析子线程访问UI而导致的崩溃unrecognized selector send to instance xxxKVO crashKVC造成的crashNSTimer导致的Crash野指针Watch Dog超时造成的crash其他crash待补充 参考文章#xff1a; 对于iOS端开… Crash系统性总结 Crash捕获与分析Crash收集符号化分析 Crash类别以及解法分析子线程访问UI而导致的崩溃unrecognized selector send to instance xxxKVO crashKVC造成的crashNSTimer导致的Crash野指针Watch Dog超时造成的crash其他crash待补充 参考文章 对于iOS端开发定位和解决Crash毕竟两个流程首先是根据线索来分析和定位问题得到一个大概的猜想之后按照自己的猜想去提供外部条件来尝试复现问题如果问题能够成功复现并复原与线程问题相似的堆栈现场则基本完成了90%的工作剩下的10%才是修复此问题。对于crash比例极低的例如没有版本相关性的对我们的应用影响极小的我们可以通过去做AB实验尝试去修复。 大家先思考下以下问题然后阅读文章找到答案
bad_access 的排查途径有哪些 什么情况下会产生 bad_access 不同的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源码。如果缺少这层转换哪怕只有几行的代码的 Appbug 定位也变得难以进行一般第三方的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中具体的方法调用流程大致如下
首先在相应操作的对象中的缓存方法列表中找调用的方法如果找到转向相应实现并执行。如果没找到在相应操作的对象isa指针指向的类中的方法列表中找调用的方法如果找到转向相应实现执行。如果没找到去父类指针所指向的对象中执行12.以此类推如果一直到根类还没找到转向拦截调用走消息转发机制。
如果没有重写拦截调用的方法程序报错。
所以此类问题解决方案 拦截调用 在方法调用中说到了如果没有找到方法就会转向拦截调用。 那么什么是拦截调用呢 拦截调用就是在找不到调用的方法程序崩溃之前你有机会通过重写NSObject的四个方法来处理: (BOOL)resolveClassMethod:(SEL)sel;(BOOL)resolveInstanceMethod:(SEL)sel;
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;由上图可见在一个函数找不到时runtime提供了三种方式去补救
调用resolveInstanceMethod给个机会让类动态添加一个该函数的实现。调用forwardingTargetForSelector让别的对象去执行这个函数动态新建类并给该类创建一个函数实现调用forwardInvocation函数执行器灵活的将目标函数分发给其他类来处理。
如果都不中调用doesNotRecognizeSelector抛出异常。
unrecognized selector crash 防护方案 既然可以补救我们完全也可以利用消息转发机制来做文章。那么问题来了在这三个步骤里面选择哪一步去改造比较合适呢。
这里我们选择了第二步forwardingTargetForSelector来做文章。原因如下 resolveInstanceMethod 需要在类的本身上动态添加它本身不存在的方法这些方法对于该类本身来说冗余的 forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象但是其开销较大需要创建新的NSInvocation对象并且forwardInvocation的函数经常被使用者调用来做多层消息转发选择机制不适合多次重写 forwardingTargetForSelector可以将消息转发给一个对象开销较小并且被重写的概率较低适合重写 选择了forwardingTargetForSelector之后可以将NSObject的该方法重写做以下几步的处理
动态创建一个桩类动态为桩类添加对应的Selector用一个通用的返回0的函数来实现该SEL的IMP将消息直接转发到这个桩类对象上。 下方是一个动态创建类的代码示例
#import objc/runtime.h
#import Foundation/Foundation.hint 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:(NSDictionaryNSKeyValueChangeKey,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观测否则会报crashuncaught 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的次数就会 crashTerminating 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
场景1key 不存在 防护方法进行 KVC Crash 防护我们就需要重写 setValue: forUndefinedKey: 方法和 valueForUndefinedKey: 方法。重写这两个方法之后就可以防护key不存在的情况了。
场景2key为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加入了一个中间代理对象LJProxytimer的target不直接是TimerViewController而是持有LJProxy实例让LJProxy实例来弱引用TimerViewControllertimer强引用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「后边会赋给delegateweak引用」和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_sourceoschina-app