app ios 打开闪退原因
2018-04-22 18:02:49 YCM1101743158 阅读数 1229

为保障线上 App 的用户体验,我们一般都会对线上 App 的 crash 率做实时监控,一旦检测到 spike,可以即刻调查原因,但这一切的前提是 crash 日志能够准确上报。

crash 日志上报有两个难点:

  • crash handler 安装之前的代码要绝对稳定
    如果日志采集器还没成功启动就 crash 了,自然什么日志也无法采集到。这一点并没有太多技巧可言,只能严格限制 handler 启动之前可以执行的代码。

  • App 无限循环 crash 时上报
    crash 日志上报时,会发送网络请求,如果请求成功之前 App 又发生 crash 该如何处理?用户甚至会陷入无限循环的 crash 中。

这篇文章介绍下出现第二种情况时,如何准确上报 crash 日志。

首先我们需要一种比较可靠的方式,可以在 app 启动时判断上次是否发生了启动 crash。介绍一个可行的思路。

如何检测连续闪退

连续闪退包含两个元素,闪退和连续。只有这两个元素同时具备时,才会影响我们的日志上传。闪退的定义可以简单为

app crash 时间 -  app 启动时间 <= 5s (或者其他 threshold)

连续的定义为,至少接连出现两次或者以上。一般 2 次就够了,很多时候用户连续经历两次闪退,就会放弃尝试。

我们可以通过记录若干个特殊的时间点 timestamp 来试图还原 App crash 场景下的生命周期。

  • App 启动 timestamp,定义为 launchTs
    App 每次启动时,记录当前时间,写入时间数组。

  • App crash timestamp,定义为 crashTs
    App 每次启动时,通过 crash 采集库,获取上次 crash report 的时间戳,写入时间数组。

  • App 正常退出 timestamp,定义为 terminateTs
    App 在接收到 UIApplicationWillTerminateNotification 通知时,记录当前时间戳,写入时间数组。注意,还有很多种 App 退出行为的时间戳是无法被准确记录的。

之所以要记录 terminateTs,是为了排除一种特殊情况,即用户启动 App 之后立即手动 kill app。如果我们正确记录了上面三个时间戳,那么我们可以得到一个与 App crash 行为相关的时间线。比如:

launchTs => crashTs => launchTs => terminateTs

或者

launchTs => launchTs => launchTs

或者

launchTs => crashTs => launchTs => crashTs => launchTs

请自行脑洞上面三种时间线的行为特征。很明显,第三种时间线看上去是连续 crash 了两次。我们只需要加上时间间隔判断,就能得知是否为连续两次闪退了。注意,如果两个 crashTs 之间如果存在 terminateTs,则不能被认为是连续闪退。检测代码比较简单,我就不贴了。

这个时间线只是记录与 crash 相关的 App 启动和退出行为,还有很多特殊的时间点没有记录,比如 App 在 前台发生 out of memory(FOOM),App 在前台 main thread 卡住被系统 Watch Dog 杀掉,iOS 系统升级时 App 被强杀,App 从 AppStore 升级时被强杀等等,这些特殊的时间点都没有记录,不过这些并不影响我们的 App 连续闪退检测,所以可以忽略。

这里指的注意的是,因为启动时要从 disk 读取时间线记录,涉及磁盘读写,会对 App 的启动时间产生影响,一个优化点是,在每次写入时间点移除掉较老的 timestamp,比如只记录最近 5 个时间戳。或者在没有读取到 crash 日志时,甚至不用启动连续闪退检测的整个流程。

接下来,我们看假设检测到连续闪退,我们如何继续上传日志。

同步等待 Crash 日志上传

最直白的方式,在 App 的代码继续执行之前,先等待日志上传成功。

把网络请求改成同步的?这会卡住 UI 线程,网络差的场景下会被系统 watch dog 强杀,显然不可取。

我们可以依旧保持异步网络请求,但是,暂时中断 UI 线程的流程,让整个 App 处于 UI 线程的 runloop 等待中,一旦网络请求成功,则跳回到 UI 线程的原有代码流程。

看着简单的实现,有几个细节需要注意。首先我们需要增加一个 App 交互,一旦进入 runloop 等待,展示一个 loading 界面,告知用户耐心等待。其次,这个等待时间不能过长,我个人建议不超过 5s,一旦超过 5s,无论 crash 日志上传的 request 是否成功,都恢复 App 原有代码流程。5s 内日志都无法上传成功的情况应该比较小,除非日志文件过大。

这种做法缺陷也很明显,一是改动比较大(修改了原有代码流程),二是需要增加新的 UI 交互,三是延长了用户的等待时间。

我们来看另一种取巧的做法。

启用后台进程上传 Crash 日志

其实最理想的日志上传,是将上传的 request 放到另一个不同的进程,那么即使 App 又发生闪退,也不会影响到另一个进程代码的执行。

问题是,iOS app 都处于 sandbox 环境下,系统不允许代码 fork 一个新进程。

幸运的是,从 iOS 8 开始,系统对 NSURLSession 新增了一个 background session 特性。这个特性允许 NSURLSession 将网络请求放入到一个单独的进程中执行。我个人感觉,这个特性设计,原本是为了增强某些 App 后台下载音视频等资源的体验。我实际测试下来,发现不管下载或者是上传,我们都可以将网络请求放入另一个进程。代码也很简单,比如我写一段如下的测试代码:

NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.mrpeak.background.crashupload"];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue new]];
NSURL *url = [NSURL URLWithString:@"https://images.unsplash.com/photo-1515816949419-7caf0a210607?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=f46b60857b4826e733da34993ec26a2f&auto=format&fit=crop&w=1534&q=80"];
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url];
[task resume];

exit(0);

执行之后,我们可以在 console 中看到如下日志:

可以清楚的看到 nsurlsessiond 进程如何替我们完成网络请求,并试图唤醒已经异常退出的 App。

当然这种最理想的方式,也有一些细节需要处理。比如如何告知 App 某个 crash 日志上传成功,并从本地移除。由于连续闪退的 App 处于极度不稳定的状态,所以任何代码逻辑都无法确保顺利完成。

我个人感觉一种比较理想的方式是,给后台进程上报的日志加上某个特殊的 flag,然后在后台通过 client request ID 和这个 flag 来做去重和整理。

线上 App 连续闪退是一种极其恶劣和可怕的故障,可怕之处在于,发生大面积连续闪退且无法被监控时,你正哼着小曲敲着代码,老板突然发现自己手机上 App 启动不了了,一打开 AppStore,发现一星差评潮水般涌来,如果是主流 App 甚至还会上科技新闻,不难预料一口黑漆漆的大锅正在成形。下次 App 的升级介绍里一定会出现 "fire peter" 了。

2018-09-21 09:48:00 weixin_34185364 阅读数 20

转自 Cocoa开发者社区 微信公众号
https://mp.weixin.qq.com/s/uVRT-rTWZQ0tdYo6CfkLhg

Unrecognized Selector Sent to Instance

由于Objective-c是Message机制,而且对象在转换的时候,会有拿到的对象和预期不一致,所以会有方法找不到的情况,在找不到方法时,查找方法将会进入方法Forward流程,系统给了三次补救的机会,所以我们要解决这个问题,在这三次均可以解决这个问题。

  • resolveInstanceMethod:(SEL)sel 这是实例化方法没有找到方法,最先执行的函数,首先会流转到这里来,返回值是BOOL,没有找到就是NO,找到就返回YES,如果要解决就需要再当前的实例中加入不存在的Selector,并绑定IMP,示例如下:
static void xxxInstanceName(id self, SEL cmd, id value) {    
  NSLog(@"resolveInstanceMethod %@", value);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{    
  NSLog(@"resolveInstanceMethod");        
  NSMethodSignature* sign = [self methodSignatureForSelector:selector];    
  if (!sign) {        
    class_addMethod([self class], sel, (IMP)xxxInstanceName, "v@:@");        return YES;    
}    
return [super resolveInstanceMethod:sel];
  • forwardingTargetForSelector:(SEL)aSelector

如果resolveInstanceMethod没有处理,将进行到forwardingTargetForSelector这步来,这时候你可以返回nil,你也可以用一个Stub对象来接住,把消息流程流转到了你的Stub那边了,然后在你的Stub里添加不存在的Selector,这样就不会crash了,示例如下:

- (id)forwardingTargetForSelectorSwizzled:(SEL)selector{      
  NSMethodSignature* sign = [self methodSignatureForSelector:selector];    
    if (!sign) {        
      id stub = [[UnrecognizedSelectorHandle new] autorelease];        
      class_addMethod([stub class], selector, (IMP)unrecognizedSelector, "v@:");        
      return stub;    
    }    
    return [self forwardingTargetForSelectorSwizzled:selector];
}
  • methodSignatureForSelector:(SEL)aSelector

  • forwardInvocation:(NSInvocation *)anInvocation

这两个方法一起说,因为他们之间有关联,

1.当methodSignatureForSelector返回nil时,会Crash

2.如果methodSignatureForSelector返回一个定义好的NSMethodSignature,但是没有实现forwardInvocation,也会闪退,如果实现了forwardInvocation,会先返回到resolveInstanceMethod然后再才会到forwardInvocation

3.当流转到forwardInvocation,通过以下方法:

[anInvocation invokeWithTarget:xxxtarget1];
[anInvocation invokeWithTarget:xxxtarget2];

还可以流转到多个对象,[anInvocation invokeWithTarget:xxxtarget2]是为了让不存在的方法有着陆点

  • doesNotRecognizeSelector:(SEL)aSelector 执行到这里的时候,两种情况:

1.当methodSignatureForSelector返回一种任意的方法签名的时候,也会进入doesNotRecognizeSelector,但是不会闪退

2.当methodSignatureForSelector返回nil时,进入doesNotRecognizeSelector就会闪退

根据以上流程,最终还是选择流程2,原因如下:

1.resolveInstanceMethod虽然可以解决问题,给不存在的方法增加到示例中去,会污染当前示例

2.forwardInvocation在三步中式最后一步,会导致流转的周期变长,而且会产生NSInvocation,性能不是最好的选择

如何监听实例化对象什么时候释放

先说下这个知识点,因为在接下来的好几个地方都会用到,会有一些异常的情况,所以需要一种知道当前创建者啥时候释放,首先会想到dealloc,这样会Hook的NSObject,在一定程度会影响性能,后面发现一种比较优雅的方法,原理来自于Runtime源码:

/************************************************************************ 
objc_destructInstance* Destroys an instance without freeing memory. * Calls C++ destructors.* Calls ARR ivar cleanup.* Removes associative references.* Returns `obj`. Does nothing if `obj` is nil.* Be warned that GC DOES NOT CALL THIS. If you edit this, also edit finalize.* CoreFoundation and other clients do call this under GC.
**********************************************************************/
void *objc_destructInstance(id obj) {    
  if (obj) {        
    // Read all of the flags at once for performance.        
    bool cxx = obj->hasCxxDtor();        
    bool assoc = !UseGC && obj->hasAssociatedObjects();        
    bool dealloc = !UseGC;        
    // This order is important.        
    if (cxx) object_cxxDestruct(obj);        
    if (assoc) _object_remove_assocations(obj);        
    if (dealloc) obj->clearDeallocating();    
    }    
  return obj;
}

_object_remove_assocations会释放所有的用AssociatedObject数据。

objc_setAssociatedObject给当前对象添加一个中间对象,当前对象释放时,会清理AssociatedObject数据,AssociatedObject的中间对象将被清理释放,中间对象的dealloc方法将被执行。

最终清理被遗漏的监听者,会用在KVO和NSNotification清理没用的监听者,不过这种方式有以下问题需要注意:

  • 清理的时候线程安全问题

  • 清理时机偏晚,是否适合你当前的情况

NSArray,NSMutableArray,NSDictonary,NSMutableDictionary

  • 类族(Class Cluster)

NSDictonary,NSArray,NSString等,都使用了类族,这种模式最大的好处就是,可以隐藏抽象基类背后的复杂细节,使用者只需调用基类简单的方法就可以返回不同的子类实例

  • Swizzle Hook

这里就不赘述Swizzle概念了,Google到处都是讲解的,这里给一个典型的例子:

swizzleInstanceMethod(NSClassFromString(@"__NSArrayI"), 
@selector(objectAtIndex:), 
@selector(hookObjectAtIndex:));
- (id) hookObjectAtIndex:(NSUInteger)index {    
  if (index < self.count) {        
  return [self hookObjectAtIndex:index];    
  }    
  handleCrashException(@"HookObjectAtIndex invalid index");    
  return nil;
}

Zombie Pointer

让野指针不闪退是模仿了XCode debug的Zombie Object,也参考了网易和美团的做法,主要是以下步骤:

1.Hook住dealloc方法

2.如果当前示例在黑名单里,就把当年前示例加入集合,并把当前对象objc_destructInstance清理引用关系,并未真正释放内存,并将object_setClass设置成自己的中间对象

3.Hook中间对象的方法,收到的消息都由中间对象来处理

4.维护的野指针集合,要么根据个数来维护,要么根据总大小来维护,当满了,就需要真正释放对象内存free(obj)

存在的问题:

1.需要单独的内存那些问题对象

2.最后释放内存后,再访问时会闪退,这个方法只是一定程度延迟了闪退时间

3.需要后台维护黑名单机制,来指定那些问题对象

KVO

KVO在以下情况会导致闪退:

  • 添加监听后没有清除会导致闪退

  • 清除不存在的key也会闪退

  • 添加重复的key导致闪退

需要Hook以下方法:

  • addObserver:forKeyPath:options:context:

  • removeObserver:forKeyPath:

主要解决以下问题:

  • 在注册监听后,中间对象需要维护注册的数据集合,当宿主释放时,清除还在集合中的监听者

  • 保护key不存在的情况

  • 保护重复添加的情况

NSTimer

NSTimer存在以下问题:

  • Target是强引用,内存泄漏

  • 在宿主不存在的时候,清理NSTimer

Hook以下方法:

  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats

解决方法: 1.当repeats为NO时,走原始方法 2.当repeats为YES时,新建一个对象,声明一个target属性为weak类型,指向参数的target,当中间对象的target为空时,清理NSTimer

NSNotification

NSNotification的主要问题是:

  • 添加通知后,没有移除导致Crash的问题(不过在iOS9以后没有这个问题,我在真机8.3测试也没有这个问题,不知道iOS8是否有这个问题)

Hook以下方法:

  • addObserver:selector:name:object:

原因和解决办法: 问题就在在于和assign和weak问题,野指针问题,要么置空指针或者删除空指针的集合

MRC

这里单独说下,为什么工程选择了MRC,因为在Hook集合类型的时候,启动的时候就闪退了,Crash的地方在系统类里,Stack里显示在CF这层,这里只能猜测系统底层对ARC的支持不好导致的,后续改成MRC就没有问题,所以这个需要继续研究和追踪,如果有知道的同学记得告知我下.

性能

本来是没有打算注意性能这个问题的,因为从Hook原理的角度来说,只是交换IMP的指向,时间复杂度来说,只是在系统级别上增加了几条逻辑判断指令,所以这个影响是极小的,基本可以忽略,我经过测试,循环1000000次,没有HOOK和HOOK相差0.0x秒的,所以减少Crash,来增加这么点时间复杂度来说,是值得的。

不过最后说一点,就是dealloc确实需要注意,因为这里存在集合的操作,所以要注意时间复杂度,dealloc执行的很频繁的,而且主线程和子线程都会涉及到,尤其是主线程一定注意,否则会影响到UI的体验。

参考资料

https://github.com/opensource-apple/objc4/blob/master/runtime/objc-runtime-new.mm

大白健康系统

最后给出开源代码:https://github.com/jezzmemo/JJException

2018-09-23 19:01:00 olsQ93038o99S 阅读数 176

640?wx_fmt=jpeg

程序员大咖点击右侧关注,免费进阶高级!640?wx_fmt=jpeg


作者:lijie250

链接:https://github.com/jezzmemo/JJException


Unrecognized Selector Sent to Instance


由于Objective-c是Message机制,而且对象在转换的时候,会有拿到的对象和预期不一致,所以会有方法找不到的情况,在找不到方法时,查找方法将会进入方法Forward流程,系统给了三次补救的机会,所以我们要解决这个问题,在这三次均可以解决这个问题。


640?wx_fmt=png

  • resolveInstanceMethod:(SEL)sel 这是实例化方法没有找到方法,最先执行的函数,首先会流转到这里来,返回值是BOOL,没有找到就是NO,找到就返回YES,如果要解决就需要再当前的实例中加入不存在的Selector,并绑定IMP,示例如下:

static void xxxInstanceName(id self, SEL cmd, id value) {    NSLog(@"resolveInstanceMethod %@", value);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{    NSLog(@"resolveInstanceMethod");    
    NSMethodSignature* sign = [self methodSignatureForSelector:selector];    if (!sign) {        class_addMethod([self class], sel, (IMP)xxxInstanceName, "v@:@");        return YES;
    }    return [super resolveInstanceMethod:sel];


  • forwardingTargetForSelector:(SEL)aSelector


如果resolveInstanceMethod没有处理,将进行到forwardingTargetForSelector这步来,这时候你可以返回nil,你也可以用一个Stub对象来接住,把消息流程流转到了你的Stub那边了,然后在你的Stub里添加不存在的Selector,这样就不会crash了,示例如下:


- (id)forwardingTargetForSelectorSwizzled:(SEL)selector{    NSMethodSignature* sign = [self methodSignatureForSelector:selector];    if (!sign) {        id stub = [[UnrecognizedSelectorHandle new] autorelease];        class_addMethod([stub class], selector, (IMP)unrecognizedSelector, "v@:");        return stub;
    }    return [self forwardingTargetForSelectorSwizzled:selector];
}


  • methodSignatureForSelector:(SEL)aSelector

  • forwardInvocation:(NSInvocation *)anInvocation


这两个方法一起说,因为他们之间有关联,


1.当methodSignatureForSelector返回nil时,会Crash


2.如果methodSignatureForSelector返回一个定义好的NSMethodSignature,但是没有实现forwardInvocation,也会闪退,如果实现了forwardInvocation,会先返回到resolveInstanceMethod然后再才会到forwardInvocation


3.当流转到forwardInvocation,通过以下方法:


[anInvocation invokeWithTarget:xxxtarget1];
[anInvocation invokeWithTarget:xxxtarget2];


还可以流转到多个对象,[anInvocation invokeWithTarget:xxxtarget2]是为了让不存在的方法有着陆点


  • doesNotRecognizeSelector:(SEL)aSelector 执行到这里的时候,两种情况:


1.当methodSignatureForSelector返回一种任意的方法签名的时候,也会进入doesNotRecognizeSelector,但是不会闪退


2.当methodSignatureForSelector返回nil时,进入doesNotRecognizeSelector就会闪退


根据以上流程,最终还是选择流程2,原因如下:


1.resolveInstanceMethod虽然可以解决问题,给不存在的方法增加到示例中去,会污染当前示例


2.forwardInvocation在三步中式最后一步,会导致流转的周期变长,而且会产生NSInvocation,性能不是最好的选择


如何监听实例化对象什么时候释放


先说下这个知识点,因为在接下来的好几个地方都会用到,会有一些异常的情况,所以需要一种知道当前创建者啥时候释放,首先会想到dealloc,这样会Hook的NSObject,在一定程度会影响性能,后面发现一种比较优雅的方法,原理来自于Runtime源码:


/***********************************************************************
objc_destructInstance
Destroys 
an instance without freeing memory. 
Calls C++ destructors.
Calls ARR ivar cleanup.
Removes associative references.
Returns `obj`. Does nothing if `obj` is nil.
Be warned that GC DOES NOT CALL THIS. If 
you edit this, also edit finalize.
CoreFoundation and other clients do call this under GC.
**********************************************************************/

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = !UseGC && obj->hasAssociatedObjects();
        bool dealloc = !UseGC;

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        if (dealloc) obj->clearDeallocating();
    }

    return obj;
}


_object_remove_assocations会释放所有的用AssociatedObject数据。

objc_setAssociatedObject给当前对象添加一个中间对象,当前对象释放时,会清理AssociatedObject数据,AssociatedObject的中间对象将被清理释放,中间对象的dealloc方法将被执行。


最终清理被遗漏的监听者,会用在KVO和NSNotification清理没用的监听者,不过这种方式有以下问题需要注意:


  • 清理的时候线程安全问题

  • 清理时机偏晚,是否适合你当前的情况


NSArray,NSMutableArray,NSDictonary,NSMutableDictionary


  • 类族(Class Cluster)


 NSDictonary,NSArray,NSString等,都使用了类族,这种模式最大的好处就是,可以隐藏抽象基类背后的复杂细节,使用者只需调用基类简单的方法就可以返回不同的子类实例


  • Swizzle Hook


这里就不赘述Swizzle概念了,Google到处都是讲解的,这里给一个典型的例子:


swizzleInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:), @selector(hookObjectAtIndex:));

- (id) hookObjectAtIndex:(NSUInteger)index {
    if (index < self.count) {
        return [self hookObjectAtIndex:index];
    }
    handleCrashException(@"HookObjectAtIndex invalid index");
    return nil;
}


Zombie Pointer


让野指针不闪退是模仿了XCode debug的Zombie Object,也参考了网易和美团的做法,主要是以下步骤:


1.Hook住dealloc方法


2.如果当前示例在黑名单里,就把当年前示例加入集合,并把当前对象objc_destructInstance清理引用关系,并未真正释放内存,并将object_setClass设置成自己的中间对象


3.Hook中间对象的方法,收到的消息都由中间对象来处理


4.维护的野指针集合,要么根据个数来维护,要么根据总大小来维护,当满了,就需要真正释放对象内存free(obj)


存在的问题:

1.需要单独的内存那些问题对象


2.最后释放内存后,再访问时会闪退,这个方法只是一定程度延迟了闪退时间


3.需要后台维护黑名单机制,来指定那些问题对象


KVO


KVO在以下情况会导致闪退:


  • 添加监听后没有清除会导致闪退

  • 清除不存在的key也会闪退

  • 添加重复的key导致闪退


需要Hook以下方法:


  • addObserver:forKeyPath:options:context:

  • removeObserver:forKeyPath:


主要解决以下问题:

  • 在注册监听后,中间对象需要维护注册的数据集合,当宿主释放时,清除还在集合中的监听者

  • 保护key不存在的情况

  • 保护重复添加的情况


NSTimer


NSTimer存在以下问题:


  • Target是强引用,内存泄漏

  • 在宿主不存在的时候,清理NSTimer


Hook以下方法:


  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats


解决方法: 1.当repeats为NO时,走原始方法 2.当repeats为YES时,新建一个对象,声明一个target属性为weak类型,指向参数的target,当中间对象的target为空时,清理NSTimer


NSNotification


NSNotification的主要问题是:


  • 添加通知后,没有移除导致Crash的问题(不过在iOS9以后没有这个问题,我在真机8.3测试也没有这个问题,不知道iOS8是否有这个问题)


Hook以下方法:


  • addObserver:selector:name:object:


原因和解决办法: 问题就在在于和assign和weak问题,野指针问题,要么置空指针或者删除空指针的集合


MRC


这里单独说下,为什么工程选择了MRC,因为在Hook集合类型的时候,启动的时候就闪退了,Crash的地方在系统类里,Stack里显示在CF这层,这里只能猜测系统底层对ARC的支持不好导致的,后续改成MRC就没有问题,所以这个需要继续研究和追踪,如果有知道的同学记得告知我下.


性能


本来是没有打算注意性能这个问题的,因为从Hook原理的角度来说,只是交换IMP的指向,时间复杂度来说,只是在系统级别上增加了几条逻辑判断指令,所以这个影响是极小的,基本可以忽略,我经过测试,循环1000000次,没有HOOK和HOOK相差0.0x秒的,所以减少Crash,来增加这么点时间复杂度来说,是值得的。


不过最后说一点,就是dealloc确实需要注意,因为这里存在集合的操作,所以要注意时间复杂度,dealloc执行的很频繁的,而且主线程和子线程都会涉及到,尤其是主线程一定注意,否则会影响到UI的体验。


参考资料

https://github.com/opensource-apple/objc4/blob/master/runtime/objc-runtime-new.mm

大白健康系统

最后给出开源代码:https://github.com/jezzmemo/JJException


640?【点击成为源码大神】

2018-09-21 18:18:26 ios8988 阅读数 198

Unrecognized Selector Sent to Instance

由于Objective-c是Message机制,而且对象在转换的时候,会有拿到的对象和预期不一致,所以会有方法找不到的情况,在找不到方法时,查找方法将会进入方法Forward流程,系统给了三次补救的机会,所以我们要解决这个问题,在这三次均可以解决这个问题

forward

  • resolveInstanceMethod:(SEL)sel 这是实例化方法没有找到方法,最先执行的函数,首先会流转到这里来,返回值是BOOL,没有找到就是NO,找到就返回YES,如果要解决就需要再当前的实例中加入不存在的Selector,并绑定IMP,示例如下:

static void xxxInstanceName(id self, SEL cmd, id value) {    NSLog(@"resolveInstanceMethod %@", value);}+ (BOOL)resolveInstanceMethod:(SEL)sel{    NSLog(@"resolveInstanceMethod");        NSMethodSignature* sign = [self methodSignatureForSelector:selector];    if (!sign) {        class_addMethod([self class], sel, (IMP)xxxInstanceName, "v@:@");        return YES;    }    return [super resolveInstanceMethod:sel];}
  • forwardingTargetForSelector:(SEL)aSelector

如果resolveInstanceMethod没有处理,将进行到forwardingTargetForSelector这步来,这时候你可以返回nil,你也可以用一个Stub对象来接住,把消息流程流转到了你的Stub那边了,然后在你的Stub里添加不存在的Selector,这样就不会crash了,示例如下:

- (id)forwardingTargetForSelectorSwizzled:(SEL)selector{    NSMethodSignature* sign = [self methodSignatureForSelector:selector];    if (!sign) {        id stub = [[UnrecognizedSelectorHandle new] autorelease];        class_addMethod([stub class], selector, (IMP)unrecognizedSelector, "v@:");        return stub;    }    return [self forwardingTargetForSelectorSwizzled:selector];}
  • methodSignatureForSelector:(SEL)aSelector

  • forwardInvocation:(NSInvocation *)anInvocation

这两个方法一起说,因为他们之间有关联,

  1. 当methodSignatureForSelector返回nil时,会Crash

  2. 如果methodSignatureForSelector返回一个定义好的NSMethodSignature,但是没有实现forwardInvocation,也会闪退,如果实现了forwardInvocation,会先返回到resolveInstanceMethod然后再才会到forwardInvocation

  3. 当流转到forwardInvocation,通过以下方法:

[anInvocation invokeWithTarget:xxxtarget1];[anInvocation invokeWithTarget:xxxtarget2];

还可以流转到多个对象,[anInvocation invokeWithTarget:xxxtarget2]是为了让不存在的方法有着陆点

  • doesNotRecognizeSelector:(SEL)aSelector 执行到这里的时候,两种情况:

  1. 当methodSignatureForSelector返回一种任意的方法签名的时候,也会进入doesNotRecognizeSelector,但是不会闪退

  2. 当methodSignatureForSelector返回nil时,进入doesNotRecognizeSelector就会闪退

根据以上流程,最终还是选择流程2,原因如下:

  1. resolveInstanceMethod虽然可以解决问题,给不存在的方法增加到示例中去,会污染当前示例

  2. forwardInvocation在三步中式最后一步,会导致流转的周期变长,而且会产生NSInvocation,性能不是最好的选择

如何监听实例化对象什么时候释放

先说下这个知识点,因为在接下来的好几个地方都会用到,会有一些异常的情况,所以需要一种知道当前创建者啥时候释放,首先会想到dealloc,这样会Hook的NSObject,在一定程度会影响性能,后面发现一种比较优雅的方法,原理来自于Runtime源码:

/************************************************************************ objc_destructInstance* Destroys an instance without freeing memory. * Calls C++ destructors.* Calls ARR ivar cleanup.* Removes associative references.* Returns `obj`. Does nothing if `obj` is nil.* Be warned that GC DOES NOT CALL THIS. If you edit this, also edit finalize.* CoreFoundation and other clients do call this under GC.**********************************************************************/void *objc_destructInstance(id obj) {    if (obj) {        // Read all of the flags at once for performance.        bool cxx = obj->hasCxxDtor();        bool assoc = !UseGC && obj->hasAssociatedObjects();        bool dealloc = !UseGC;        // This order is important.        if (cxx) object_cxxDestruct(obj);        if (assoc) _object_remove_assocations(obj);        if (dealloc) obj->clearDeallocating();    }    return obj;}

_object_remove_assocations会释放所有的用AssociatedObject数据。

objc_setAssociatedObject给当前对象添加一个中间对象,当前对象释放时,会清理AssociatedObject数据,AssociatedObject的中间对象将被清理释放,中间对象的dealloc方法将被执行。

最终清理被遗漏的监听者,会用在KVO和NSNotification清理没用的监听者,不过这种方式有以下问题需要注意:

  • 清理的时候线程安全问题

  • 清理时机偏晚,是否适合你当前的情况

NSArray,NSMutableArray,NSDictonary,NSMutableDictionary

  • 类族(Class Cluster)

NSDictonary,NSArray,NSString等,都使用了类族,这种模式最大的好处就是,可以隐藏抽象基类背后的复杂细节,使用者只需调用基类简单的方法就可以返回不同的子类实例

  • Swizzle Hook

这里就不赘述Swizzle概念了,Google到处都是讲解的,这里给一个典型的例子:

swizzleInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:), @selector(hookObjectAtIndex:));- (id) hookObjectAtIndex:(NSUInteger)index {    if (index < self.count) {        return [self hookObjectAtIndex:index];    }    handleCrashException(@"HookObjectAtIndex invalid index");    return nil;}

Zombie Pointer

让野指针不闪退是模仿了XCode debug的Zombie Object,也参考了网易和美团的做法,主要是以下步骤:

  1. Hook住dealloc方法

  2. 如果当前示例在黑名单里,就把当年前示例加入集合,并把当前对象objc_destructInstance清理引用关系,并未真正释放内存,并将object_setClass设置成自己的中间对象

  3. Hook中间对象的方法,收到的消息都由中间对象来处理

  4. 维护的野指针集合,要么根据个数来维护,要么根据总大小来维护,当满了,就需要真正释放对象内存free(obj)

存在的问题:

  1. 需要单独的内存那些问题对象

  2. 最后释放内存后,再访问时会闪退,这个方法只是一定程度延迟了闪退时间

  3. 需要后台维护黑名单机制,来指定那些问题对象

KVO

KVO在以下情况会导致闪退:

  • 添加监听后没有清除会导致闪退

  • 清除不存在的key也会闪退

  • 添加重复的key导致闪退

需要Hook以下方法:

  • addObserver:forKeyPath:options:context:

  • removeObserver:forKeyPath:

主要解决以下问题:

  • 在注册监听后,中间对象需要维护注册的数据集合,当宿主释放时,清除还在集合中的监听者

  • 保护key不存在的情况

  • 保护重复添加的情况

NSTimer

NSTimer存在以下问题:

  • Target是强引用,内存泄漏

  • 在宿主不存在的时候,清理NSTimer

Hook以下方法:

  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats

解决方法: 1.当repeats为NO时,走原始方法 2.当repeats为YES时,新建一个对象,声明一个target属性为weak类型,指向参数的target,当中间对象的target为空时,清理NSTimer

NSNotification

NSNotification的主要问题是:

  • 添加通知后,没有移除导致Crash的问题(不过在iOS9以后没有这个问题,我在真机8.3测试也没有这个问题,不知道iOS8是否有这个问题)

Hook以下方法:

  • addObserver:selector:name:object:

原因和解决办法: 问题就在在于和assign和weak问题,野指针问题,要么置空指针或者删除空指针的集合

MRC

这里单独说下,为什么工程选择了MRC,因为在Hook集合类型的时候,启动的时候就闪退了,Crash的地方在系统类里,Stack里显示在CF这层,这里只能猜测系统底层对ARC的支持不好导致的,后续改成MRC就没有问题,所以这个需要继续研究和追踪,如果有知道的同学记得告知我下

性能

本来是没有打算注意性能这个问题的,因为从Hook原理的角度来说,只是交换IMP的指向,时间复杂度来说,只是在系统级别上增加了几条逻辑判断指令,所以这个影响是极小的,基本可以忽略,我经过测试,循环1000000次,没有HOOK和HOOK相差0.0x秒的,所以减少Crash,来增加这么点时间复杂度来说,是值得的。

不过最后说一点,就是dealloc确实需要注意,因为这里存在集合的操作,所以要注意时间复杂度,dealloc执行的很频繁的,而且主线程和子线程都会涉及到,尤其是主线程一定注意,否则会影响到UI的体验。

参考资料

App崩溃的原因(iOS)

阅读数 639

iOS 打开第三方app

阅读数 60

没有更多推荐了,返回首页