应用程序的溃散总是最让人头疼的问题。日常开发阶段的溃散,发现后还能够当即处理,可是上线后呈现的溃散,就需求紧急修复bug了并且还需求发版,也对错常严峻的研制事故,那么如何下降程序的溃散率呢?这儿就用到了APP运转时Crash自动捕获和处理,使APP能够持续稳定正常的运转。

常见的Crash有哪些?

  • Container Crash(调集类操作形成的溃散,例如数组越界、刺进nil)
  • NSString Crash(字符串类操作形成的溃散)
  • 找不到方针办法或许类办法
  • KVO和KVC Crash
  • NSNOtification Crash
  • NSTimer Crash
  • Bad Access Crash(野指针)
  • Threading Crash(非主线程刷UI)
  • NSNull Crash
  • iOS16上的Crash

防护原理

Objective-C 语言是一门动态语言,运用 Objective-C 语言的 Runtime 运转时机制,对需求 Hook 的类增加 Category(分类),在各个分类的 +(void)load 中经过 Method Swizzling 阻拦简单形成溃散的体系办法,将体系原有办法与增加的防护办法的 selector(办法挑选器)与 IMP(函数完成指针)进行交流。然后在替换办法中增加防护操作,然后到达防止以及修复溃散的目的。

Method Swizzling 办法的封装

因为这几种常见 Crash 的防护都需求用到 Method Swizzling 技术。所以咱们可认为 NSObject 新建一个分类,将 Method Swizzling 相关的办法封装起来。

@interface NSObject (Safe)
/** 交流两个类办法的完成
 @param originalSelector 原始办法的 SEL
 @param swizzledSelector 交流办法的 SEL
 @param targetClass 类
*/
+ (void)jhDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;
/** 交流两个方针办法的完成
@param originalSelector 原始办法的 SEL
@param swizzledSelector 交流办法的 SEL
@param targetClass 类
*/
+ (void)jhDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;
@end
@implementation NSObject (Safe)
// 交流两个类办法的完成
+ (void)iaskDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
  swizzlingClassMethod(targetClass, originalSelector, swizzledSelector);
}
// 交流两个方针办法的完成
+ (void)iaskDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
  swizzlingInstanceMethod(targetClass, originalSelector, swizzledSelector);
}
// 交流两个类办法的完成 C 函数
void swizzlingClassMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
  Method originalMethod = class_getClassMethod(class, originalSelector);
  Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
  BOOL didAddMethod = class_addMethod(class,
                    originalSelector,
                    method_getImplementation(swizzledMethod);          method_getTypeEncoding(swizzledMethod));
  if (didAddMethod) {
    class_replaceMethod(class,
               swizzledSelector,
              method_getImplementation(originalMethod),
              method_getTypeEncoding(originalMethod));
  } else {
    method_exchangeImplementations(originalMethod, swizzledMethod);
  }
}
// 交流两个方针办法的完成 C 函数
void swizzlingInstanceMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
  Method originalMethod = class_getInstanceMethod(class, originalSelector);
  Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
  BOOL didAddMethod = class_addMethod(class,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
  if (didAddMethod) {
    class_replaceMethod(class,
              swizzledSelector,
              method_getImplementation(originalMethod),
              method_getTypeEncoding(originalMethod));
  } else {
    method_exchangeImplementations(originalMethod, swizzledMethod);
  }
}
@end

下面就来讲解下如何完成代码来防溃散。

常见的Crash及防护措施

1. Container Crash

指的是容器类的crash,常见的有NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的crash。 一些常见的越界,刺进nil,等过错操作均会导致此类crash发生。

处理计划:对于简单形成crash的办法,自界说办法进行交流,并在自界说的办法中参加一些条件约束和判别。

NSArray+Safe

首要创立NSArray的分类(NSArray+Safe),完成代码如下:

#import "NSObject+Swizzling.h"
#import <objc/runtime.h>
+ (void)load {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    //替换 objectAtIndex
    NSString *tmpStr = @"objectAtIndex:";
    NSString *tmpFirstStr = @"safe_ZeroObjectAtIndex:";
    NSString *tmpThreeStr = @"safe_objectAtIndex:";
    NSString *tmpSecondStr = @"safe_singleObjectAtIndex:";
    // 替换 objectAtIndexedSubscript
    NSString *tmpSubscriptStr = @"objectAtIndexedSubscript:";
    NSString *tmpSecondSubscriptStr = @"safe_objectAtIndexedSubscript:";
    [NSObject exchangeInstanceMethodWithSelfClass:NSClassFromString(@"__NSArray0")
                  originalSelector:NSSelectorFromString(tmpStr)                   swizzledSelector:NSSelectorFromString(tmpFirstStr)];
    [NSObject exchangeInstanceMethodWithSelfClass:NSClassFromString(@"__NSSingleObjectArrayI")
                  originalSelector:NSSelectorFromString(tmpStr)                   swizzledSelector:NSSelectorFromString(tmpSecondStr)];
    [NSObject exchangeInstanceMethodWithSelfClass:NSClassFromString(@"__NSArrayI")
                  originalSelector:NSSelectorFromString(tmpStr)                   swizzledSelector:NSSelectorFromString(tmpThreeStr)];
  
    [NSObject exchangeInstanceMethodWithSelfClass:NSClassFromString(@"__NSArrayI")        originalSelector:NSSelectorFromString(tmpSubscriptStr)                   swizzledSelector:NSSelectorFromString(tmpSecondSubscriptStr)];
  });
}
#pragma mark --- implement method
/**
取出NSArray 第index个 值 对应 __NSArrayI
 @param index 索引 index
 @return 回来值
*/
- (id)safe_objectAtIndex:(NSUInteger)index {
  if (index >= self.count){
    return nil;
  }
  return [self safe_objectAtIndex:index];
}
/**
取出NSArray 第index个 值 对应 __NSSingleObjectArrayI
 @param index 索引 index
 @return 回来值
*/
- (id)safe_singleObjectAtIndex:(NSUInteger)index {
  if (index >= self.count){
    return nil;
  }
  return [self safe_singleObjectAtIndex:index];
}
/**
取出NSArray 第index个 值 对应 __NSArray0
 @param index 索引 index
 @return 回来值
*/
- (id)safe_ZeroObjectAtIndex:(NSUInteger)index {
  if (index >= self.count){
    return nil;
  }
  return [self safe_ZeroObjectAtIndex:index];
}
/**
取出NSArray 第index个 值 对应 __NSArrayI
 @param idx 索引 idx
 @return 回来值
*/
- (id)safe_objectAtIndexedSubscript:(NSUInteger)idx {
  if (idx >= self.count){
    return nil;
  }
  return [self safe_objectAtIndexedSubscript:idx];
}

NSMutableArray+Safe

#import "NSObject+Swizzling.h"
#import <objc/runtime.h>
+ (void)load {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    //替换 objectAtIndex:
    NSString *tmpGetStr = @"objectAtIndex:";
    NSString *tmpSafeGetStr = @"safeMutable_objectAtIndex:";
    [NSObject exchangeInstanceMethodWithSelfClass:NSClassFromString(@"__NSArrayM")
                  originalSelector:NSSelectorFromString(tmpGetStr)                   swizzledSelector:NSSelectorFromString(tmpSafeGetStr)];
    //替换 removeObjectsInRange:
    NSString *tmpRemoveStr = @"removeObjectsInRange:";
    NSString *tmpSafeRemoveStr = @"safeMutable_removeObjectsInRange:";
    [NSObject exchangeInstanceMethodWithSelfClass:NSClassFromString(@"__NSArrayM")
             originalSelector:NSSelectorFromString(tmpRemoveStr)                   swizzledSelector:NSSelectorFromString(tmpSafeRemoveStr)];
    //替换 insertObject:atIndex:
    NSString *tmpInsertStr = @"insertObject:atIndex:";
    NSString *tmpSafeInsertStr = @"safeMutable_insertObject:atIndex:";
    [NSObject exchangeInstanceMethodWithSelfClass:NSClassFromString(@"__NSArrayM")
             originalSelector:NSSelectorFromString(tmpInsertStr)                   swizzledSelector:NSSelectorFromString(tmpSafeInsertStr)];
    //替换 removeObject:inRange:
    NSString *tmpRemoveRangeStr = @"removeObject:inRange:";
    NSString *tmpSafeRemoveRangeStr = @"safeMutable_removeObject:inRange:";
    [NSObject exchangeInstanceMethodWithSelfClass:NSClassFromString(@"__NSArrayM")
              originalSelector:NSSelectorFromString(tmpRemoveRangeStr)               swizzledSelector:NSSelectorFromString(tmpSafeRemoveRangeStr)];
    // 替换 objectAtIndexedSubscript
    NSString *tmpSubscriptStr = @"objectAtIndexedSubscript:";
    NSString *tmpSecondSubscriptStr = @"safeMutable_objectAtIndexedSubscript:";
    [NSObject exchangeInstanceMethodWithSelfClass:NSClassFromString(@"__NSArrayM")
           originalSelector:NSSelectorFromString(tmpSubscriptStr)                   swizzledSelector:NSSelectorFromString(tmpSecondSubscriptStr)];
  });
}
#pragma mark --- implement method
/**
取出NSArray 第index个 值
 @param index 索引 index
 @return 回来值
*/
- (id)safeMutable_objectAtIndex:(NSUInteger)index {
  if (index >= self.count){
    return nil;
  }
  return [self safeMutable_objectAtIndex:index];
}
/**
NSMutableArray 移除索引 index 对应的值
 @param range 移除规模
*/
- (void)safeMutable_removeObjectsInRange:(NSRange)range {
  if ((range.location + range.length) > self.count) {
    return;
  }
  return [self safeMutable_removeObjectsInRange:range];
}
/**
在range规模内,移除掉anObject
 @param anObject 移除的anObject
 @param range 规模
*/
- (void)safeMutable_removeObject:(id)anObject inRange:(NSRange)range {

  if ((range.location + range.length) > self.count) {
    return;
  }
  if (!anObject){
    return;
  }
  return [self safeMutable_removeObject:anObject inRange:range];
}
/**
NSMutableArray 刺进 新值 到 索引index 指定方位
 @param anObject 新值
 @param index 索引 index
*/
- (void)safeMutable_insertObject:(id)anObject atIndex:(NSUInteger)index {
  if (index > self.count) {
    return;
  }
  if (!anObject){
    return;
  }
  [self safeMutable_insertObject:anObject atIndex:index];
}
/**
取出NSArray 第index个 值 对应 __NSArrayI
 @param idx 索引 idx
 @return 回来值
*/
- (id)safeMutable_objectAtIndexedSubscript:(NSUInteger)idx {
  if (idx >= self.count){
    return nil;
  }
  return [self safeMutable_objectAtIndexedSubscript:idx];
}

这儿我仅仅写出了NSArray和NSMutableArray的代码完成,其实NSDictionary和NSString也是相同的办法完成,交流体系的办法,在办法中完成反常情况的处理。

iOS16上的Crash

在项目中为 NSMutableDictionary 增加了空安全的防护,交流了体系的 setObject:forKeyedSubscript: 办法。发现这个会导致在iOS16体系上面,设置 self.navigationItem.rightBarButtonItem = 时呈现内存爆增,终究导致项目因为内存问题Crash。

setObject:forKeyedSubscript:
当Object为nil时,Dictionary会将`key`对应的`obj`移除;
当key为空时,会抛出NSInvalidArgumentException反常;

2. Unrecognized Selector

假如被调用的方针办法没有完成,那么程序在运转中调用该办法时,就会因为找不到对应的办法完成,然后导致 APP 溃散。那么能够在再找不到办法溃散之前,阻拦办法调用。

音讯转发的流程

  • 动态办法解析:方针/类 在接收到无法解读的音讯后,首要会调用+resolveInstanceMethod:或许+resolveClassMethod:,表明该类是否会增加一个办法完成。假如没有增加完成,进入下一步;

  • 备援接收者(receiver):假如当时方针完成了 forwardingTargetForSelector:,Runtime 就会调用这个办法,答应咱们将音讯的接受者转发给其他方针。假如回来为nil,则进入下一步转发流程;

  • 完整的音讯转发:假如methodSignatureForSelector:回来为nil,则会报错找不到办法。假如该办法回来了一个函数签名,那么体系会创立一个NSInvocation方针,把未处理的那条音讯有关的细节都封于其中,改变调用方针,使音讯在新方针上得到调用;

iOS-各种Crash防护

这儿咱们挑选第二步(音讯接受者重定向)来进行阻拦。因为 -forwardingTargetForSelector 办法能够将音讯转发给一个方针,开销较小,并且被重写的概率较低,合适重写。

具体完成过程如下:

  • 给 NSObject 增加一个分类,在分类中完成一个自界说的 -jh_forwardingTargetForSelector: 办法;

  • 运用 Method Swizzling 将 -forwardingTargetForSelector: 和 -jh_forwardingTargetForSelector: 进行办法交流;

  • 在自界说的办法中,先判别当时方针是否现已完成了音讯接受者重定向和音讯重定向。假如都没有完成,就动态创立一个方针类,给方针类动态增加一个办法;

  • 把音讯转发给动态生成类的实例方针,由方针类动态创立的办法完成,这样就不存在找不到办法了;

完成代码如下:

+ (void)load {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    // 阻拦 `-forwardingTargetForSelector:` 办法,替换自界说完成
    [NSObject iaskDefenderSwizzlingInstanceMethod: @selector(forwardingTargetForSelector:)withMethod: @selector(jh_forwardingTargetForSelector:)withClass:[NSObject class]];
    [NSObject iaskDefenderSwizzlingClassMethod: @selector(forwardingTargetForSelector:)withMethod: @selector(jh_forwardingTargetForSelector:) withClass:[NSObject class]];
  });
}
// 实例办法
- (id)jh_forwardingTargetForSelector:(SEL)aSelector {
  SEL forwarding_sel = @selector(forwardingTargetForSelector:);
  // 获取 NSObject 的音讯转发办法
  Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
  // 获取 当时类 的音讯转发办法
  Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);
  // 判别当时类自身是否完成第二步:音讯接受者重定向
  BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
  // 假如没有完成第二步:音讯接受者重定向
  if (!realize) {
    // 判别有没有完成第三步:音讯重定向
    SEL methodSignature_sel = @selector(methodSignatureForSelector:);
    Method root_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);
    Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
    realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
    // 假如没有完成第三步:音讯重定向
    if (!realize) {
      // 创立一个新类
      NSString *errClassName = NSStringFromClass([self class]);
      NSString *errSel = NSStringFromSelector(aSelector);
      NSLog(@"出问题的类,出问题的方针办法 == %@ %@", errClassName, errSel);
      NSString *className = @"CrachClass";
      Class cls = NSClassFromString(className);
     
      // 假如类不存在 动态创立一个类
      if (!cls) {
        Class superClsss = [NSObject class];
        cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
        // 注册类
        objc_registerClassPair(cls);
      }
      // 假如类没有对应的办法,则动态增加一个
      if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
        class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
      }
      // 把音讯转发到当时动态生成类的实例方针上
      return [[cls alloc] init];
    }
  }
  return [self jh_forwardingTargetForSelector:aSelector];
}
// 类办法
+ (id)jh_forwardingTargetForSelector:(SEL)aSelector {
  SEL forwarding_sel = @selector(forwardingTargetForSelector:);
  // 获取 NSObject 的音讯转发办法
  Method root_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel);
  // 获取 当时类 的音讯转发办法
  Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel);
  // 判别当时类自身是否完成第二步:音讯接受者重定向
  BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
  // 假如没有完成第二步:音讯接受者重定向
  if (!realize) {
    // 判别有没有完成第三步:音讯重定向
    SEL methodSignature_sel = @selector(methodSignatureForSelector:);
    Method root_methodSignature_method = class_getClassMethod([NSObject class], methodSignature_sel);
   
    Method current_methodSignature_method = class_getClassMethod([self class], methodSignature_sel);
    realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
    // 假如没有完成第三步:音讯重定向
    if (!realize) {
      // 创立一个新类
      NSString *errClassName = NSStringFromClass([self class]);
      NSString *errSel = NSStringFromSelector(aSelector);
      NSLog(@"出问题的类,出问题的类办法 == %@ %@", errClassName, errSel);
      NSString *className = @"CrachClass";
      Class cls = NSClassFromString(className);
      // 假如类不存在 动态创立一个类
      if (!cls) {
        Class superClsss = [NSObject class];
        cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
        // 注册类
        objc_registerClassPair(cls);
      }
      // 假如类没有对应的办法,则动态增加一个
      if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
        class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
      }
      // 把音讯转发到当时动态生成类的实例方针上
      return [[cls alloc] init];
    }
  }
  return [self jh_forwardingTargetForSelector:aSelector];
}
// 动态增加的办法完成
static int Crash(id slf, SEL selector) {
  return 0;
}

3. KVC Crash

什么是KVC?

KVC表明键值编码,供给一种机制来间接访问方针的特点。

KVC常见的溃散原因:

  • key 不是方针的特点,形成溃散;
  • keyPath 不正确,形成溃散;
  • key 为 nil,形成溃散;
  • value 为 nil,为非方针设值,形成溃散;

KVC的Setter 搜索形式

iOS-各种Crash防护

KVC的Getter 搜索形式

iOS-各种Crash防护

从上面流程能够看出,setValue:forKey: 履行失利会调用 setValue: forUndefinedKey: 办法,并引发溃散;valueForKey: 履行失利会调用 valueForUndefinedKey: 办法,并引发溃散;

对应上面的溃散给出了对应的处理计划:

  • 重写setValue: forUndefinedKey:办法和valueForUndefinedKey: 办法;
  • key为nil导致的问题,只需求交流体系的setValue:forKey:办法;
  • value 为 nil导致的问题,需求重写体系的setNilValueForKey:办法

可参阅『Crash 防护体系』(三)KVC 防护

4. KVO Crash

什么是KVO?

KVO是键值对调查,是 iOS 调查者形式的一种完成。KVO 答应一个方针监听另一个方针特定特点的改变,并在改变时接收到事情。

KVO常见的溃散原因:

  • KVO 增加次数和移除次数不匹配;
  • 被调查者提早被开释,被调查者在 dealloc 时依然注册着 KVO;
  • 增加或许移除时 keypath == nil;
  • 增加了调查者,但未完成 observeValueForKeyPath:ofObject:change:context: 办法

处理计划:

  • 创立一个KVOProxy方针,在方针中运用{keypath : observer1, observer2 , …} 结构的关系哈希表进行 observer、keyPath 之间的保护;
  • 然后运用 KVOProxy 方针对增加、移除、调查办法进行分发处理;
  • 在分类中自界说了 dealloc 的完成,移除了剩余的调查者;

可参阅『Crash 防护体系』(二)KVO 防护

5. NSNotification Crash

发生的原因: 当一个方针增加了notification之后,假如dealloc的时分,依然持有notification,就会呈现NSNotification类型的crash。NSNotification类型的crash多发生于程序员写代码时分犯忽略,在NSNotificationCenter增加一个方针为observer之后,忘记了在方针dealloc的时分移除它。

iOS9之前会crash,iOS9之后苹果体系已优化。在iOS9之后,即使开发者没有移除observer,Notification crash也不会再发生了。

处理计划: 交流体系的dealloc办法,在方针真实dealloc之前调用下[[NSNotificationCenter defaultCenter] removeObserver:self]

6. NSTimer Crash

发生的原因:因为定时器 timer 强引证 target 的关系导致 target 不能被开释,形成内存泄露。与此一起,假如 NSTimer 是无限重复的履行一个任务的话,也有可能导致 target 的 selector 一向被重复调用且处于无效状态,对 app 的 CPU ,内存等性能方面均是没有必要的浪费。

处理办法: 界说一个抽象类,NSTimer 实例强引证抽象类,而在抽象类中,弱引证 target ,这样 target 和 NSTimer 之间的关系也便是弱引证了,意味着 target 能够自由的开释,然后处理了循环引证的问题。

7. Bad Access Crash

野指针便是指向一个已删除的方针或许受限内存区域的指针。比较常见的便是这个指针指向的内存,在别处被回收了,可是这个指针不知道,依然还指向这块内存。

处理野指针导致的crash往往是一件扎手的事情,因为发生crash 的场景不好复现,再一个crash之后console(操控台)的信息供给的帮助有限。XCode自身为了便于敞开调试时发现野指针问题,供给了Zombie机制,能够在发生野指针时提示呈现野指针的类,然后处理了开发阶段呈现野指针的问题。但是针对于线上发生的野指针问题,仍旧没有一个比较好的办法来定位问题。

野指针crash 防护计划:

野指针问题的处理思路方向其实很简单确认,XCode供给了Zombie的机制来排查野指针的问题,那么咱们这边能够完成一个类似于Zombie的机制,加上对zombie实例的全部办法阻拦机制 和 音讯转发机制,那么就能够做到在野指针访问时不Crash而仅仅crash时相关的信息。

一起还需求注意一点:因为zombie的机制需求在方针开释时保存其指针和相关内存占用,跟着app的进行,越来越多的方针被创立和开释,这会导致内存占用越来越大,这样明显对于一个正常运转的app的性能有影响。所以需求一个合适的zombie方针开释机制,确认zombie机制对内存的影响是有极限的。

zombie机制的完成首要分为以下四个环节:

第一步:method swizzling替换NSObject的allocWithZone办法,在新的办法中判别该类型方针是否需求参加野指针防护,假如需求,则经过objc_setAssociatedObject为该方针设置flag符号,被符号的方针后续会进入zombie流程

iOS-各种Crash防护

做flag符号是因为很多体系类,比如NSString,UIView等创立,开释非常频频,而这些实例发生野指针概率非常低。基本都是咱们自己写的类才会有野指针的相关问题,所以经过在创立时 设置一个符号用来过滤不必要做野指针防护的实例,提高计划的功率。

一起做判别是否要参加符号的条件里边,咱们参加了黑名单机制,是因为一些特定的类是不适用于增加到zombie机制的,会发生溃散(例如:NSBundle),并且所以和zombie机制相关的类也不能参加符号,否则会在开释过程中循环引证和调用,导致内存走漏甚至栈溢出。

第二步:method swizzling替换NSObject的dealloc办法,对flag符号的方针实例调用objc_destructInstance,开释该实例引证的相关特点,然后将实例的isa修改为HTZombieObject。经过objc_setAssociatedObject 保存将原始类名保存在该实例中。

iOS-各种Crash防护

调用objc_destructInstance的原因:

这儿参阅了体系在Object-C Runtime 中NSZombies完成,dealloc最终会调到objectdispose函数,在这个函数里边 其实也做了三件事情,

  • 调用objc_destructInstance开释该实例引证的相关实例
  • 开释该内存

官方文档对objc_destructInstance的解释为:

Destroys an instance of a class without freeing memory and removes any associated references this instance might have had.

说明objc_destructInstance会开释与实例相关联的引证,可是并不开释该实例等内存。

第三步:在HTZombieObject 经过音讯转发机制forwardingTargetForSelector处理所有阻拦的办法,依据selector动态增加能够处理办法的呼应者HTStubObject 实例,然后经过 objc_getAssociatedObject 获取之前保存该实例对应的原始类名,计算过错数据。

iOS-各种Crash防护

HTZombieObject的处理和unrecognized selector crash的处理是一样,首要的目的便是阻拦所有传给HTZombieObject的函数,用一个回来为空的函数来替换,然后到达程序不溃散的目的。

第四步:当退到后台或许到达未开释实例的上限时,则在ht_freeSomeMemory办法中调用原有dealloc办法开释所有被zombie化的实例

综上所述,能够用下图总结一下bad access类型crash的防护流程:

iOS-各种Crash防护

因为做了延时开释若干实例,对体系总内存会发生必定影响,现在将内存的缓冲区开到2M左右,所以应该没有很大的影响,但仍是可能潜在一些危险。

延时开释实例是依据相关功能代码会聚焦在某一个时间段调用的假设前提下,所以野指针的zombie保护机制只能在其实例方针依然缓存在zombie的缓存机制时才有用,若在实例真实开释之后,再调用野指针仍是会呈现Crash。

8. 非主线程刷UI(Crash)

为什么只在主线程改写UI?

UIKit并不是一个线程安全的类,UI操作规划到渲染访问各种View方针的特点,假如异步操作下会存在读写的问题,而为其加锁则会耗费大量的资源并拖慢运转速度。另一方面因为整个程序的起点UIApplication是在主线程进行初始化,所以的用户事情都是在主线程上进行传递,所以View只能在主线程上才干对事情进行呼应。而在渲染方面因为图画的渲染需求以60帧的改写率在屏幕上一起更新,在非主线程异步话的情况下,无法确认这个处理过程能够完成同步更新。

现在开始的处理计划是swizzle UIView类的以下三个办法:

-(void)setNeedsLayout;

-(void)setNeedsDisplay;

-(void)setNeedsDisplayInRect:(CGRect)rect;

在这三个办法调用的时分判别一下当时的线程,假如不是主线程的话,直接运用dispatch_async(dispatch_get_main_queue(), ^{ //调用原本办法 });可是真实实施了之后,发现这三个办法并不能彻底覆盖UIView相关的所有刷UI到操作。现在我也就找到这样的办法来处理。

9. NSNull Crash

在解析后端Json数据时,有时会存在莫名其妙的回来了NSNull(完成现已约定不能回来NUll),导致了App闪退,真坑爹啊。咱们知道是不能给NSNull类型发送音讯的。

第一种办法:AFN自带的特点,能够从呼应JSON中移除值为NULL的键

AFJSONResponseSerializer *responseSerializer = [AFJSONResponseSerializer serializer];
responseSerializer.removesKeysWithNullValues = YES;

NullSafe剖析完成:这儿首要仍是用到OC运转时机制,要害点仍是在音讯转发上。新建一个Null的分类NullSafe,重写动态转发的办法。首要查找办法签名,假如能获取不到,就对常用的Foundation框架类遍历验证,假如能呼应这个办法,就生成新的办法签名,然后进行下一步的转发。最终一步对办法进行转发,设置target方针为nil,像nil发送音讯是不会发生溃散的。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
  //look up method signature
  NSMethodSignature *signature = [super methodSignatureForSelector:selector];
  if (!signature)
  {
    for (Class someClass in @[
      [NSMutableArray class],
      [NSMutableDictionary class],
      [NSMutableString class],
      [NSNumber class],
      [NSDate class],
      [NSData class]
    ])
    {
      @try
      {
        if ([someClass instancesRespondToSelector:selector])
        {
          signature = [someClass instanceMethodSignatureForSelector:selector];
          break;
        }
      }
      @catch (__unused NSException *unused) {}
    }
  }
  return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
  invocation.target = nil;
  [invocation invoke];
}

总结:

在项目中,咱们能够界说一个类办理这些Crash防护,需不需求敞开。因为在咱们开发的过程中仍是想能及时发现问题,然后及时修复问题。

咱们也要合理权衡敞开的防护类型,仅默许敞开线上反馈的常见类型,而不是敞开所有类型,其他类型能够配置为动态敞开,依据用户设备的闪退日志敞开防护。其次各种Hook带来的未知性,Crash 自身对错正常情况下才发生的,假如一味地躲避这种反常,可能会发生更多的反常情况,特别是业务逻辑上会呈现不可操控的流程。所以咱们往常也要注意代码质量,严格防止呈现那些低级的过错,应该去防止呈现过错,而不是去防护过错的呈现。

参阅文章:

/post/684490…

大白健康体系