赞
踩
【转载】作者:乳猪啸谷
链接:https://juejin.im/post/6844903688608153614
用户在使用App的过程中,经常遇到闪退的情况,体验不太好,本文尝试探索引发闪退的原因,以及在遇到crash的情况下,尽可能的保持程序运行,并及时上报错误。
KVO Crash常见原因:
-observeValueForKeyPath:ofObject:change:context:
方法对象接收到未知的消息,即下图中消息未能处理的情况。
image
<figcaption></figcaption>
除了OC层面的异常捕获之外,很多内存错误、访问错误的地址产生的crash则需要利用unix标准的signal机制,注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数。该函数中我们可以输出栈信息,版本信息等其他一切我们所想要的。
(1)AppDelegate中添加捕获监听
- - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
- NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
- return YES;
- }
- 复制代码
(2)解析堆栈信息并上报
- void UncaughtExceptionHandler(NSException *exception) {
- /**
- * 获取异常崩溃信息
- */
- NSArray *callStack = [exception callStackSymbols];
- NSString *reason = [exception reason];
- NSString *name = [exception name];
- }
- 复制代码
- void InstallSignalHandler(void)
- {
- signal(SIGHUP, SignalExceptionHandler);
- signal(SIGINT, SignalExceptionHandler);
- signal(SIGQUIT, SignalExceptionHandler);
-
- signal(SIGABRT, SignalExceptionHandler);
- signal(SIGILL, SignalExceptionHandler);
- signal(SIGSEGV, SignalExceptionHandler);
- signal(SIGFPE, SignalExceptionHandler);
- signal(SIGBUS, SignalExceptionHandler);
- signal(SIGPIPE, SignalExceptionHandler);
- }
- 复制代码
- void SignalExceptionHandler(int signal)
- {
-
- NSMutableString *mstr = [[NSMutableString alloc] init];
- [mstr appendString:@"Stack:\n"];
- void* callstack[128];
- int i, frames = backtrace(callstack, 128);
- char** strs = backtrace_symbols(callstack, frames);
- for (i = 0; i <frames; ++i) {
- [mstr appendFormat:@"%s\n", strs[I]];
- }
- [SignalHandler saveCreash:mstr];
-
- }
- 复制代码
hook相关的方法,增加保护机制。 以NSArray越界为例,hook objectAtIndex方法,在方法中捕获越界异常,并在最后返回一个nil对象。
- [self exchangeInstanceMethod:__NSArrayI method1Sel:@selector(objectAtIndex:) method2Sel:@selector(avoidCrashObjectAtIndex:)];
- 复制代码
- - (id)avoidCrashObjectAtIndex:(NSUInteger)index {
- id object = nil;
-
- @try {
- object = [self avoidCrashObjectAtIndex:index];
- }
- @catch (NSException *exception) {
- //捕获异常,根据exception打印出堆栈信息,同时也避免了程序崩溃
- }
- @finally {
- return object;
- }
- }
- 复制代码
注意:使用方法进行捕获异常之后,第三方工具将不会搜集到崩溃信息并上报,需要在catch中手动上报。
新建一个对象,用来记录target,observer,context,keypath等,每添加一个监听,增加一个对象,用一个数组维护。添加和删除的时候做判断,同时hook dealloc函数,dealloc的同时移除我的观察者和我观察的对象。dealloc时遍历数组,数组中不应该存在对象,如果存在对象,应该抛出异常并接收,提示用户KVO的释放存在问题。
-observeValueForKeyPath:ofObject:change:context:
方法:hook observeValueForKeyPath
方法,增加try-catch即可。注意:使用方法进行捕获异常之后,第三方工具将不会搜集到崩溃信息并上报,需要在catch中手动上报。
通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:
- if ([self respondsToSelector:@selector(method)]) {
- [self performSelector:@selector(method)];
- }
- 复制代码
当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃。
image
<figcaption></figcaption>
上图可以看出,在一个函数找不到时,Objective-C提供了三种方式去补救:
1、调用resolveInstanceMethod给个机会让类添加这个实现这个函数
2、调用forwardingTargetForSelector让别的对象去执行这个函数
3、调用methodSignatureForSelector(函数符号制造器)和forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。
如果都不中,调用doesNotRecognizeSelector抛出异常。
- (void)forwardInvocation:(NSInvocation *)anInvocation
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
方法一:hook上述两个方法,在methodSignatureForSelector中返回有效的NSMethodSignature,在forwardInvocation中添加try-catch即可,代码如下:
- [self exchangeInstanceMethod:[self class] method1Sel:@selector(methodSignatureForSelector:) method2Sel:@selector(avoidCrashMethodSignatureForSelector:)];
- [self exchangeInstanceMethod:[self class] method1Sel:@selector(forwardInvocation:) method2Sel:@selector(avoidCrashForwardInvocation:)];
- 复制代码
- - (NSMethodSignature *)avoidCrashMethodSignatureForSelector:(SEL)aSelector
- {
- NSMethodSignature *ms = [self avoidCrashMethodSignatureForSelector:aSelector];
- if ([self respondsToSelector:aSelector] || ms){
- return ms;
- }
- else{
- return [SafeProxy instanceMethodSignatureForSelector:@selector(safe_crashLog)];
- }
- }
-
- - (void)avoidCrashForwardInvocation:(NSInvocation *)anInvocation {
-
- @try {
- [self avoidCrashForwardInvocation:anInvocation];
-
- } @catch (NSException *exception) {
- //捕获异常,根据exception打印出堆栈信息,同时也避免了程序崩溃
- //上报
- } @finally {
-
- }
- }
- 复制代码
方法二:直接hook doesNotRecognizeSelector也可实现,doesNotRecognizeSelector起到抛出异常的作用,自己增加try-catch进行捕获即可,代码如下:
- [self exchangeInstanceMethod:[self class] method1Sel:@selector(doesNotRecognizeSelector:) method2Sel:@selector(avoidCrashDoesNotRecognizeSelector:)];
- 复制代码
- - (void)avoidCrashDoesNotRecognizeSelector:(SEL)aSelector{
- @try {
- [self avoidCrashDoesNotRecognizeSelector:aSelector];
-
- } @catch (NSException *exception) {
- //捕获异常,根据exception打印出堆栈信息,同时也避免了程序崩溃
- //上报
- } @finally {
-
- }
- }
- 复制代码
效果如下:
- NSInvalidArgumentException
- *** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[1]
- Error Place:-[ViewController NSArray_Test_InstanceArray]
- AvoidCrash default is to remove nil object and instance a array.
- 复制代码
打印出了堆栈信息,同时避免了程序崩溃。
注意:使用方法进行捕获异常之后,第三方工具将不会搜集到崩溃信息并上报,需要在catch中手动上报。
模仿Xcode的zombie机制:
1.Swizzle原有allocWithZone方法,添加野指针防护标记。
2.Swizzle原有dealloc方法,如果有野指针防护标记,调用 objc_destructInstance方法,修改实例isa使其指向zombieObject,保存原始 类名,以便上报使用。
3.Swizzle消息转发机制forwardingTargetForSelector方法,处理所 有原始类originObject的方法,收集错误信息并上报。
4.及时释放zombieObject。
注: objc_destructInstance会释放与实例相关联的引用,但是并不释放该实例的内存。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。