原稿地址。现在Baymax迎来了她新的法力。

初稿地址

图片 1

前言

正巧以运作的 APP 突然
Crash,是同样件使人不快的从,会烟消云散用户,影响公司发展,所以 APP
运行时有所防 Crash 功能会立竿见影降低 Crash 率,提升 APP 稳定性。但是有时
APP Crash 是理所应当的显现,我们无深受 APPCrash
可能会见招别的逻辑错误,不过我们好抓取到用时的库房信息并臻传至相关的服务器,分析并修复这些
BUG。

用本文介绍的 XXShield 库有个别独至关重要的功能:

  1. 防止Crash
  2. 抓获异常状态下的垮台信息

恍如之系技能分析为有 网易iOS
App运作时Crash自动防护实践

image

时已实现的效力

  1. Unrecoginzed Selector Crash
  2. KVO Crash
  3. Container Crash
  4. NSNotification Crash
  5. NSNull Crash
  6. NSTimer Crash
  7. 野指针 Crash

<section style=”margin: 0px; padding: 0px; max-width: 100%;
box-sizing: border-box; word-wrap: break-word !important; color: rgb(63,
63, 63); font-size: 14px; font-family: Avenir, -apple-system-font,
微软雅黑, sans-serif; text-align: center;”>

1 Unrecoginzed Selector Crash

版权声明

本文转自网易杭州前端技术部公众号,由作者授权发布。

并发由

由于 Objective-C
是动态语言,所有的消息发送都见面放在运行时失去分析,有时候我们管一个信传递给了不当的种,就见面导致这个错误。

<section class=”” style=”margin: 0px; padding: 0px; max-width: 100%;
box-sizing: border-box; word-wrap: break-word !important; font-size:
20px;”>

解决办法

Objective-C 在产出无法解析的点子时发生三部曲来进行信息转发。
详见Objective-C Runtime
运行时之三:方法及信息

  1. 动态方法分析
  2. 备用接收者
  3. 整转发

1 一般适用及 Dynamic 修饰的 Property
2 一般适用同以计转化到其它对象
3 一般适用同信可以转账多独对象,可以实现类似多累或转发中心的定义。

这里选择的是方案二,因为三里面用到了 NSInvocation
对象,此目标性能开销比较充分,而且这种好要出现得频次较高。最可用消息转发到一个备用者对象及。

此间新建一个智能转发类。此目标将在其他对象无法解析数据时,返回一个 0
来防止 Crash。返回 0 是为是通用的智能转发类做的操作看似朝 nil
发送一个信。

代码如下

#import <objc/runtime.h>

/**
 default Implement
 @param target trarget
 @param cmd cmd
 @param ... other param
 @return default Implement is zero
 */
int smartFunction(id target, SEL cmd, ...) {
    return 0;
}

static BOOL __addMethod(Class clazz, SEL sel) {
    NSString *selName = NSStringFromSelector(sel);

    NSMutableString *tmpString = [[NSMutableString alloc] initWithFormat:@"%@", selName];

    int count = (int)[tmpString replaceOccurrencesOfString:@":"
                                                withString:@"_"
                                                   options:NSCaseInsensitiveSearch
                                                     range:NSMakeRange(0, selName.length)];

    NSMutableString *val = [[NSMutableString alloc] initWithString:@"i@:"];

    for (int i = 0; i < count; i++) {
        [val appendString:@"@"];
    }
    const char *funcTypeEncoding = [val UTF8String];
    return class_addMethod(clazz, sel, (IMP)smartFunction, funcTypeEncoding);
}

@implementation XXShieldStubObject

+ (XXShieldStubObject *)shareInstance {
    static XXShieldStubObject *singleton;
    if (!singleton) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            singleton = [XXShieldStubObject new];
        });
    }
    return singleton;
}

- (BOOL)addFunc:(SEL)sel {
    return __addMethod([XXShieldStubObject class], sel);
}

+ (BOOL)addClassFunc:(SEL)sel {
    Class metaClass = objc_getMetaClass(class_getName([XXShieldStubObject class]));
    return __addMethod(metaClass, sel);
}

@end

咱俩这边用 Hook NSObject的
- (id)forwardingTargetForSelector:(SEL)aSelector 方法启动信息转发。
多多人无亮之是如果想如果反发类方法,只需要贯彻一个同名的好像方式即可,虽然当头文件被斯办法无声称。

XXStaticHookClass(NSObject, ProtectFW, id, @selector(forwardingTargetForSelector:), (SEL)aSelector) {
    // 1 如果是NSSNumber 和NSString没找到就是类型不对  切换下类型就好了
    if ([self isKindOfClass:[NSNumber class]] && [NSString instancesRespondToSelector:aSelector]) {
        NSNumber *number = (NSNumber *)self;
        NSString *str = [number stringValue];
        return str;
    } else if ([self isKindOfClass:[NSString class]] && [NSNumber instancesRespondToSelector:aSelector]) {
        NSString *str = (NSString *)self;
        NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
        NSNumber *number = [formatter numberFromString:str];
        return number;
    }

    BOOL aBool = [self respondsToSelector:aSelector];
    NSMethodSignature *signatrue = [self methodSignatureForSelector:aSelector];

    if (aBool || signatrue) {
        return XXHookOrgin(aSelector);
    } else {
        XXShieldStubObject *stub = [XXShieldStubObject shareInstance];
        [stub addFunc:aSelector];

        NSString *reason = [NSString stringWithFormat:@"*****Warning***** logic error.target is %@ method is %@, reason : method forword to SmartFunction Object default implement like send message to nil.",
                            [self class], NSStringFromSelector(aSelector)];
        [XXRecord recordFatalWithReason:reason userinfo:nil errorType:EXXShieldTypeUnrecognizedSelector];

        return stub;
    }
}
XXStaticHookEnd

此间反映了 Crash 信息,出现信息转发一般是一个 logic
错误,为必修复的Bug,上报尤为重要。


前言

2 KVO Crash

</section>

出现因

KVOCrash总结下有以下2那个接近。

  1. 无兼容的移除和增长涉嫌。
  2. 观察者和叫观察者释放的当儿没应声断开观察者关系。

真相大白(Baymax),迪士尼动画《超能陆战队》中的正常化机器人,是一个体型肥胖的充气机器人,因呆萌的表面和善良的实质获得大家之爱,被誉为“萌神”。

解决办法

尼古拉斯赵四说过 :

赵四

比及程序世界就是,程序世界没有什么难以解决的题材还是不得以经抽象层次来化解之,如果发生,那就有数交汇。
综观程序的架构设计,计算机网络协议分层设计,操作系统内核设计等等都是这样。

题材1 : 不成对的丰富观察者和移除观察者会招致 Crash,以往咱们利用
KVO,观察者和叫观察者都是直接互动的。这里的设计方案是咱探寻一个 Proxy
用来做转账, 真正的观察者是 Proxy,被观察者出现了通知消息,由 Proxy
做分发。所以 Proxy 里面如保留一个数据结构 {keypath : [observer1,
observer2,…]} 。

@interface XXKVOProxy : NSObject {
    __unsafe_unretained NSObject *_observed;
}

/**
 {keypath : [ob1,ob2](NSHashTable)}
 */
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSHashTable<NSObject *> *> *kvoInfoMap;

@end

咱俩需要 Hook NSObject的 �KVO 相关办法。

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
  1. 在累加观察者时
![](http://ompeszjl2.bkt.clouddn.com/iOS-APP-%E8%BF%90%E8%A1%8C%E6%97%B6%E9%98%B2Crash%E5%B7%A5%E5%85%B7XXShield%E7%BB%83%E5%B0%B1//KVO-Add.png)
addObserver

  1. 于移除观察者时

removeObserver

题材2: 观察者和为观察者释放的下没有断开观察者关系。
于观察者, 既然我们是投机用 Proxy
做的分发,我们协调就用保留观察者,这里我们大概的运用 NSHashTable
指定指针持有策略也 weak 即可。

对此让观察者,我们利用 iOS
界的毒瘤-MethodSwizzling
一如既往缓遭遇及的法。我们以吃观察者上绑定一个涉嫌对象,在涉及对象的 dealloc
方法被召开连锁操作即可。

- (void)dealloc {
    @autoreleasepool {
        NSDictionary<NSString *, NSHashTable<NSObject *> *> *kvoinfos =  self.kvoInfoMap.copy;
        for (NSString *keyPath in kvoinfos) {
            // call original  IMP
            __xx_hook_orgin_function_removeObserver(_observed,@selector(removeObserver:forKeyPath:),self, keyPath);
        }
    }
}

Baymax项目是为着削减开发人员在出被一些勿专业的代码编写造成的内存泄露,界面卡顿,耗电等题材而来的一个监控网。

3 Container Crash

现Baymax迎来了她新的意义:APP运行时Crash自动防护效果,为app的流水线顺利运转保驾护航!

出现原因

容器在另编程语言中还愈重大,容器是数的载体,很多器皿对容器放空值都召开了容错处理。不幸之凡
Objective-C 并不曾,容器插入了 nil 就见面招致
Crash,容器还有另外一个极爱 Crash 的原由即是下标越界。

下面用详细介绍一下APP运行时Crash自动修复系统开发的目的,设计之法则及以的方。

解决办法

科普的器皿有 NS(Mutable)Array , NS(Mutable)Dictionary, NSCache
等。我们得 hook 常见的艺术在检测功能又捕获堆栈信息反映。

例如

XXStaticHookClass(NSArray, ProtectCont, id, @selector(objectAtIndex:),(NSUInteger)index) {
if (self.count == 0) {

    NSString *reason = [NSString stringWithFormat:@"target is %@ method is %@,reason : index %@ out of count %@ of array ",
                        [self class], XXSEL2Str(@selector(objectAtIndex:)), @(index), @(self.count)];
    [XXRecord recordFatalWithReason:reason userinfo:nil errorType:EXXShieldTypeContainer];
    return nil;
}

if (index >= self.count) {
    NSString *reason = [NSString stringWithFormat:@"target is %@ method is %@,reason : index %@ out of count %@ of array ",
                        [self class], XXSEL2Str(@selector(objectAtIndex:)), @(index), @(self.count)];
    [XXRecord recordFatalWithReason:reason userinfo:nil errorType:EXXShieldTypeContainer];
    return nil;
}

return XXHookOrgin(index);
}
XXStaticHookEnd

可需要专注的是 NSArray 是一个 Class Cluster 的肤浅父类,所以我们需要
Hook 到我们真的的子类。

这里叫出一个增援方法,获取一个近似的有所直接子类:

+ (NSArray *)findAllOf:(Class)defaultClass {

    int count = objc_getClassList(NULL, 0);

    if (count <= 0) {

        @throw@"Couldn't retrieve Obj-C class-list";

        return @[defaultClass];
    }

    NSMutableArray *output = @[].mutableCopy;

    Class *classes = (Class *) malloc(sizeof(Class) * count);

    objc_getClassList(classes, count);

    for (int i = 0; i < count; ++i) {

        if (defaultClass == class_getSuperclass(classes[i]))//子类
        {
            [output addObject:classes[i]];
        }

    }

    free(classes);

    return output.copy;

}

// 对于NSarray :

//[NSarray array] 和 @[] 的类型是__NSArray0
//只有一个元素的数组类型 __NSSingleObjectArrayI,
// 其他的大部分是//__NSArrayI,



// 对于NSMutableArray :
//[NSMutableDictionary dictionary] 和 @[].mutableCopy__NSArrayM



// 对于NSDictionary: :

//[NSDictionary dictionary];。 @{}; __NSDictionary0
// 其他一般是  __NSDictionaryI

// 对于NSMutableDictionary: :
// 一般用到的是 __NSDictionaryM

<section class=”” style=”margin: 0px; padding: 0px; max-width: 100%;
box-sizing: border-box; word-wrap: break-word !important; font-size:
16px;”>

4 NSNotification Crash

Chapter 1 – 开发目的

起因

在 iOS8 及以下的操作系统被丰富的观察者一般要以 dealloc
的上做移除,如果开发者忘记移除,则在殡葬通知的时段会造成 Crash,而于
iOS9 上就是移忘记除也无所谓,猜想可能是 iOS9
之后系统将通报中心具有对象由 assign 变为了weak

</section>

解决办法

因而这边少种植解决办法

  1. 类 KVO 中间加上 Proxy 层,使用 weak 指针来有所对象
  2. 于 dealloc 的上用非让移除的观察者移除

此地我们利用 iOS
界的毒瘤-MethodSwizzling
同等平和被到的计。


是否存在这样的夜,当刚刚躺下准备美美的上床同一醒的下,
突然来同样笔记夺命电话Call,一接起发现凡是若老板!!!“小王啊,刚刚上线的X.X.X版本有问题了呀,怎么样操作会crash啊,导致新职能都心有余而力不足利用了,快定位一下凡是什么原因,抓紧hotpatch修复一下呀!”。心里一万头草泥马呼啸而过,瞬间就满头大汗的而可还要故作镇静地回:“嗯,老板自己立刻去看,一定全力缓解问题!”
急忙打开电脑的卿,知道今夜决定无眠了。

5 NSNull Crash

是不是以存这么的事态,你老板拿大家还聚集起来开了一个年头KPI目标制定会议,说及:“作为一个出名的技能集团,app性能是咱们技术集团首追捕的对象,其中很最使之等同码就是app的崩溃率,去年我们app统计出的崩溃率是千分之五,而我辈的竞争对手的崩溃率只生万分之五,相差了10倍!今年咱们只要迎头赶上他们,最起码也使与他们持平。”
你老是同情,但是你内心却同时有些怀疑,对方的开发资源是我们的一点加倍还要个个都是尽人皆知老车手,我们集团里也差不多还是应届生小鲜肉,这KPI能结束成么?

起因

虽 Objecttive-C 不允开发者将 nil 放上容器内,但是另外一个代表用户态
的类 NSNull
却足以放大上容器,但让人难过的凡其一近乎的实例,并无能够响应任何方法。

容器被冒出 NSNull 一般是 API 接口返回了带有 null 的 JSON �数据,
调用方通常将其知晓为 NSNumber,NSString,NSDictionary 和 NSArray。
这时开发者如果没办好防守 一旦对 NSNull 这个类型调用任何方式都见面现出
unrecongized selector 错误。

真相大白健康系统–APP运行时Crash自动修复系统正是为回应这些现象,解决问题设生之。

解决办法

俺们在 NSNull
的倒车道吃可断定�上面的季种类型是否好分析。如果可以分析一直将该转会给�这几乎种对象,如果不克则调用父类的默认实现。

XXStaticHookClass(NSNull, ProtectNull, id, @selector(forwardingTargetForSelector:), (SEL) aSelector) {
    static NSArray *sTmpOutput = nil;
    if (sTmpOutput == nil) {
        sTmpOutput = @[@"", @0, @[], @{}];
    }

    for (id tmpObj in sTmpOutput) {
        if ([tmpObj respondsToSelector:aSelector]) {
            return tmpObj;
        }
    }
    return XXHookOrgin(aSelector);
}
XXStaticHookEnd

APP运行时Crash自动修复+捕获系统的统筹初衷,就是为了降低app的crash率。利用Objective-C语言的动态特性,采用AOP(Aspect
Oriented Programming)
面向切面编程的计划性思想,做到无痕植入。能够自行在app运行时实时捕获导致app崩溃的破环因子,然后经过特定的技术手段去化解这些破坏因子,使app免于崩溃,照样可以连续健康运转,为app的无休止运行保驾护航。

6. NSTimer Crash

<section class=”” style=”margin: 0px; padding: 0px; max-width: 100%;
box-sizing: border-box; word-wrap: break-word !important; font-size:
16px;”>

并发由

在使用
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
创建定时任务的时候,target� 一般都见面持有 timer,timer又会持有 target
对象,在我们尚无对关闭定时器的早晚,timer 会一直拥有target
导致内存泄漏。

Chapter 2 – 功能简介

解决办法

同 KVO 一样,既然 timer 和 target
直接互动容易出现问题,我们便还找找个代理将 target 和 selctor 等消息保存及
Proxy 里,并且是去世引用 target。
这样避免因为循环引用造成的内存泄漏。然后于接触真正 target 事件的时如果
target 置为 nil 了这手动去关定时器。

XXStaticHookMetaClass(NSTimer, ProtectTimer,  NSTimer * ,@selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:),
                      (NSTimeInterval)ti , (id)aTarget, (SEL)aSelector, (id)userInfo, (BOOL)yesOrNo ) {
    if (yesOrNo) {
        NSTimer *timer =  nil ;
        @autoreleasepool {
            XXTimerProxy *proxy = [XXTimerProxy new];
            proxy.target = aTarget;
            proxy.aSelector = aSelector;
            timer.timerProxy = proxy;
            timer = XXHookOrgin(ti, proxy, @selector(trigger:), userInfo, yesOrNo);
            proxy.sourceTimer = timer;
        }
        return  timer;
    }
    return XXHookOrgin(ti, aTarget, aSelector, userInfo, yesOrNo);
}
XXStaticHookEnd
@implementation XXTimerProxy

- (void)trigger:(id)userinfo  {
    id strongTarget = self.target;
    if (strongTarget && ([strongTarget respondsToSelector:self.aSelector])) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [strongTarget performSelector:self.aSelector withObject:userinfo];
#pragma clang diagnostic pop
    } else {
        NSTimer *sourceTimer = self.sourceTimer;
        if (sourceTimer) {
            [sourceTimer invalidate];
        }
        NSString *reason = [NSString stringWithFormat:@"*****Warning***** logic error target is %@ method is %@, reason : an object dealloc not invalidate Timer.",
                            [self class], NSStringFromSelector(self.aSelector)];

        [XXRecord recordFatalWithReason:reason userinfo:nil errorType:(EXXShieldTypeTimer)];
    }
}

@end

</section>

7. 野指针 Crash

APP运行时Crash自动修复系统
的重点职能,可以用同样句话来大概的连:对作业代码的零侵入性地拿原会造成app崩溃的crash抓取住,消灭掉,保证app继续健康地运转,再以crash的具体信息提取出来,实时回给用户。

并发由

貌似以单线程条件下以 ARC 正确的拍卖引用关系野指针出现的并无亟,
但是多线程下则免尽然,通常在一个线程中释放了对象,�另外一个线程还尚未更新指针状态
后续访问就可能会见招随机性 bug。

据此是随机 bug
是因吃回收的内存不必然就让利用。而且崩溃的职或为同原的逻辑相聚很远,因此收集的堆栈信息也说不定是无规律没有呀价值。
切实的归类请看Bugly整理的脑图。

x

双重多关于野指针的稿子要参考:

  1. 怎样稳定Obj-C野指针随机Crash(一)
  2. 怎么定位Obj-C野指针随机Crash(二)
  3. 怎么样定位Obj-C野指针随机Crash(三)

由此下面的一个聊例子就是足以老直观的体现出系统的意:

解决办法

此地我们好借系统的NSZombies对象的设计。
参考buildNSZombie

釜底抽薪进程

  1. 确立白名单机制,由于系统的切近基本不见面产出野指针,而且 hook
    所有的近乎支出比较充分。所以我们才过滤开发者自定义之类似。

  2. hook dealloc 方法
    这些用维护之好像我们并无让其放出,而是调用objc_desctructInstance
    方法释放实例之中所拥有属性之援和关系对象。

  3. 利用 object_setClass(id,Class) 修改 isa 指针将那个对一个Proxy
    对象(类比较�系统的 KVO 实现),此 Proxy
    实现了一个同眼前所说之智能转发类一样的 return 0的函数。

  4. 以 Proxy 对象内之
    - (void)forwardInvocation:(NSInvocation *)anInvocation 中收集
    Crash 信息。

  5. 缓存的目标是起本的,我们在缓存对象到一定数额上以那释放(object_dispose)。

调用以下的平等段子代码

留存问题

  1. 推放出内存会造成性能浪费,所以默认缓存会导致野指针的Class实例的目标限定是50,超出后会自由,如果这再此点了正要好放掉的野指针,还是会导致Crash的,

  2. 提议使用的上如果近来没有野指针的Crash可以不用被,如果野指针类型的Crash突然增多,可以设想当
    hot Patch 中开启野指针防护,待接到异常信息之后,再关是开关。


图片 2

采集信息

由于要此库没有其余外部依赖,所以没有实现响应的申报逻辑。使用者要用申报信息
只待活动实现 XXRecordProtocol 即可,然后在开 SDK 之前用那注册上
SDK。
当落实方式中会接受到 XXShield 内部定义的错误信息。
开发者无论可以行使诸如 CrashLytics,友盟, bugly等第三库房,或者电动
dump堆栈信息都只是。

@protocol XXRecordProtocol <NSObject>

- (void)recordWithReason:(NSError * )reason userInfo:(NSDictionary *)userInfo;

@end

image

应用方法

结果自然会促成app的夭折,因为testObj是一个UIButton对象,而UIButton并没实现
someMethod:
这个主意,所以往testObj发送someMethod:这个法的上,将会见促成该措施无法在系的主意列表里找到,最终导致app的crash。

演示工程

git clone git@github.com:ValiantCat/XXShield.git
cd Example
pod install 
open XXShield.xcworkspace

而是经过我们的crash防护系统,调用这段代码时app并无见面崩溃,同时XCode的Console如下:

Install

  pod "XXShield"

图片 3

Usage

/**
 注册汇报中心

 @param record 汇报中心
 */
+ (void)registerRecordHandler:(id<XXRecordProtocol>)record;

/**
 注册SDK,默认只要开启就打开防Crash,如果需要DEBUG关闭,请在调用处使用条件编译
 本注册方式不包含EXXShieldTypeDangLingPointer类型
 */
+ (void)registerStabilitySDK;

/**
 本注册方式不包含EXXShieldTypeDangLingPointer类型

 @param ability ability
 */
+ (void)registerStabilityWithAbility:(EXXShieldType)ability;

/**
 ///注册EXXShieldTypeDangLingPointer需要传入存储类名的array,暂时请不要传入系统框架类

 @param ability ability description
 @param classNames 野指针类列表
 */
+ (void)registerStabilityWithAbility:(EXXShieldType)ability withClassNames:(nonnull NSArray<NSString *> *)classNames;

image

ChangeLog

ChangeLog

看得出对应的crash的信息(crash类型,原因,调用栈信息)均可以完整的打印在XCode的Console中。

单元测试

有关的单元测试在演示工程的Test
Target下,有趣味之开发者可以自动查看。并且一度接入
TrivisCI管了�代码质量。

证实我们的大白系统已捕捉到了是crash,将拖欠crash消灭掉并且吐出来该crash的完全信息。

�Bug&Feature

若来有关的 Bug 请提
Issue。

要觉得可以扩大新的严防型,请提
PR
给我。

当目前系的效能并无一往无前到可拿具有的crash都处理掉,不过有些科普的赛频次发生的crash,系统都会指向他们一一处理。目前好处理掉的crash类型具体产生以下几栽:

作者

�ValiantCat,
519224747@qq.com
私家博客
南栀倾寒的简书

  • unrecognized selector crash

  • KVO crash

  • NSNotification crash

  • NSTimer crash

  • Container crash(数组越界,插nil等)

  • NSString crash (字符串操作的crash)

  • Bad Access crash (野指针)

  • UI not on Main Thread Crash (非主线程刷UI(机制亟待改善))

License

XXShield 使用 Apache-2.0 开源协议.

对于每种型的crash,安全系都应用两样之法子,进行了相应的拍卖。

<section class=”” style=”margin: 0px; padding: 0px; max-width: 100%;
box-sizing: border-box; word-wrap: break-word !important; font-size:
16px;”>

Chapter 3 – 实现原理

</section>

前已经提过,目前的安康预防体系可挂到8挨项目的Crash,接下将各个详细介绍就8种类型的Crash的警备的落实之现实性原理:

<section class=”” style=”margin: 30px 0px 0px 8px; padding: 0px;
max-width: 100%; box-sizing: border-box; word-wrap: break-word
!important; font-size: 16px; text-align: left; color: rgb(60, 112,
198);”>

3.1 Unrecognized Selector类型crash防护</section>

3.1.1 crash产生原因

unrecognized
selector类型的crash在app众多的crash类型中据为己有着比异常的分,通常是以一个靶调用了一个无属于其艺术的主意导致的。

如调用以下一截代码就见面生出crash

图片 4

image

切实crash时的见见下图:

图片 5

image

假如解决当下遭到项目的crash,我们要事先了解清楚其产生的切实原因及流程。

3.1.2 方法调用流程

吃咱看一下术调用在运转时的过程。

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

  1. 第一,在相应操作的靶子中之缓存方法列表中寻找调用的章程,如果找到,转向相应实现并实施。

  2. 倘没找到,在对应操作的目标被的计列表中搜寻调用的措施,如果找到,转向相应实现执行

  3. 只要没有找到,去父类指针所指向的对象吃执行1,2.

  4. 盖此类推,如果直白顶根类还尚无找到,转向拦截调用,走消息转发机制。

  5. 假设无重写拦截调用的章程,程序报错。

3.1.3 拦截调用

以方调用中说到了,如果没找到办法就会见转接拦截调用。

这就是说什么是阻碍调用呢?

截留调用就是,在搜索不至调用的方法程序崩溃之前,你发出机会通过重写NSObject的季独办法来处理:

图片 6

image

阻碍调用的整整工艺流程虽Objective-C的音转发机制。其切实流程如下图:

图片 7

image

出于上图可见,在一个函数找不至常,runtime提供了三种艺术去弥补:

  1. 调用resolveInstanceMethod给个机会让类添加这个实现之函数

  2. 调用forwardingTargetForSelector让别的对象去实施之函数

  3. 调用forwardInvocation(函数执行器)灵活的以目标函数以其它形式实行。

假使都未中,调用doesNotRecognizeSelector丢来非常。

3.1.4 unrecognized selector crash 防护方案

既然如此可以挽救,我们了也可以使用信息转发机制来开文章。那么问题来了,在就三个步骤中,选择哪一样步去改造比较适当与否。

这里我们挑选了次步forwardingTargetForSelector来开文章。原因如下:

  1. resolveInstanceMethod需要在类的我上动态增长它本身不在的章程,这些办法对于此类本身来说冗余的

  2. forwardInvocation可以通过NSInvocation的样式拿消息转发给多只目标,但是其开发比较充分,需要创造新的NSInvocation对象,并且forwardInvocation的函数经常为使用者调用,来做多重叠消息转发选择机制,不符合多次重写

  3. forwardingTargetForSelector可以将信息转发给一个对象,开销比较小,并且让再写的几率比逊色,适合重写

选择了forwardingTargetForSelector之后,可以以NSObject的该法重写,做以下几步之拍卖:

  1. 动态创建一个桩类

  2. 动态为桩类添加对应的Selector,用一个通用的返回0的函数来贯彻该SEL的IMP

  3. 拿消息一直转账到者桩类对象及。

流程图如下:

图片 8

image

只顾要目标的接近本事如果更写了forwardInvocation方法吧,就不应有对forwardingTargetForSelector进行重复写了,否则会潜移默化及该类型的对象原本的音讯转发流程。

由此重写NSObject的forwardingTargetForSelector方法,我们就是可以拿无法辨认的计开展阻挠并且将信息转发到安全的桩类对象吃,从而得以使app继续健康运行。

<section class=”” style=”margin: 30px 0px 0px 8px; padding: 0px;
max-width: 100%; box-sizing: border-box; word-wrap: break-word
!important; font-size: 16px; text-align: left; color: rgb(60, 112,
198);”>

3.2 KVO类型crash防护</section>

3.2.1 KVO crash 产生原因

KVO,即:Key-Value
Observing,它提供平等栽体制,当指定的对象的习性让涂改后,则对象就是会接受收到通知。简单的说哪怕是历次指定的给观察的目标的性为涂改后,KVO就见面自动通知相应的观察者了。

KVO机制在iOS的群出状况被还见面吃采取及。不过只要同不小心使用不当的语句,会导致大量之crash问题。所以要是能找到同样栽方法会自行抓取这些由开发者粗心所导致的KVO
Crash问题吧,是起得的价之。

先是我们来探望通过见面招致KVO Crash的蝇头种状况:

KVO的给观察者dealloc时仍注册在KVO导致的crash,见下图

图片 9

image

补加KVO重复添加观察者或更移除观察者(KVO注册观察者和移除观察者不兼容)导致的crash,见下图

图片 10

image

3.2.2 KVO crash 防护方案

一般而言一个对象的KVO关系图如下:

图片 11

image

一个深受考察的对象(Observed
Object)上发生若干只观察者(Observer),每个观察者而着眼若干漫漫KeyPath。

若果观察者和keypath的多少一样多,很易理不亮让观察对象全KVO关系,导致受观察者在dealloc的时光,还留着部分事关没有被撤销。
同时还见面招KVO注册观察者和移除观察者不配合的景况时有发生。

笔者已还遇到了当多线程的气象下,导致KVO重复添加观察者或移除观察者的景象。这好像题目一般多数产生的可比隐蔽,不易于从代码的范畴去排查。

是因为达到可见多数由KVO而导致的crash原因是由吃考察对象的KVO关系图混乱导致。那么怎样来管理混乱的KVO关系呢。可以被吃考察对象具备一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行田间管理,delegate通过成立平等摆设map来保安KVO整个涉及。如下图:

图片 12

image

诸如此类做的裨益来半点个:

  1. 如若起KVO重复添加观察者或重新移除观察者(KVO注册观察者和移除观察者不配合)的情形,delegate可以一直堵住这些非正常的操作。

  2. 叫考察对象dealloc之前,可以经过delegate自动将跟团结有关的KVO关系都收回掉,避免了KVO的被观察者dealloc时还是注册在KVO导致的crash。

给swizzle的方法分别是:

图片 13

image

关于

图片 14

image

道改造流程如下图:

图片 15

image

由此地方的流水线,将observerd对象的拥有kvo相关的observer信息全部更换到KVOdelegate上,并且避免了同kvoinfo被再次添加多次之可能。

关于

图片 16

image.gif

主意改造流程如下图:

图片 17

image.gif

移除一个keypath的Observer时,当delegate的kvoInfoMap中找不顶key为该keypath的当儿,说明这delegate并从未有对许keypath的observer,即征移除了一个无配合的观察者,此时只要更持续操作会招app崩溃,所以应该就中断流程,然后统计异常信息。

当keypath对应之KVOInfo列表(infoArray)为空的时刻,说明这delegate已经不再持有另外与keypath相关的observer了。这时该调用原有removeObserver的道将delegate对应之观察者移除。

瞩目到于检讨遍历infoArray的时侯,除了如去相应的info信息,还多了扳平步检查info.observer

nil的进程,是为若observer为nil,那么这若是keypath对应的价值变化吧,也会以找不顶observer而崩溃,所以需要开这无异于步来阻止该种植情形的来。

关于

图片 18

image.gif

艺术改造流程如下图:

图片 19

image.gif

delegate对于observeValueForKeyPath方法的改动最紧要的地方是,在于将相应的响应措施换给真正的KVO
Observer,通过keyInfoMap找到keypath对应之KVOInfo里面预先存储好的observer,然后调用observer原本的应措施。

与此同时以遍历InfoArray的上,发现info.observerw ==
nil的下,需要就以其铲除掉,避免KVO的观察者observer被放后value变化导致的crash.

末段,针对 KVO的受观察者dealloc时仍然注册在KVO导致的crash
的场面,可以用NSObject的dealloc swizzle, 在object
dealloc的时自动将其对应之kvodelegate所有与kvo相关的数码清空,然后以kvodelegate也置空。避免出现KVO的于观察者dealloc时还是注册在KVO而生的crash.

<section class=”” style=”margin: 30px 0px 0px 8px; padding: 0px;
max-width: 100%; box-sizing: border-box; word-wrap: break-word
!important; font-size: 16px; text-align: left; color: rgb(60, 112,
198);”>

3.3 NSNotification类型crash防护</section>

3.3.1 crash 产生原因

当一个靶上加了notification之后,如果dealloc的当儿,仍然具备notification,就会见冒出NSNotification类型的crash。

NSNotification类型的crash多产生让程序员写代码时候犯疏忽,在NSNotificationCenter添加一个靶为observer之后,忘记了以对象dealloc的时段移除它。

所幸的凡,苹果在iOS9其后专门对于这种状态召开了处理,所以当iOS9从此,即使开发者没有移除observer,Notification
crash也非会见更产生了。

可对于iOS9之前的用户,我们还是生必要举行一下NSNotification
Crash的严防的。

3.3.2 NSNotification crash 防护方案

NSNotification Crash的戒备原理非常简短, 利用method swizzling hook
NSObject的dealloc函数,再对象真正dealloc之前先行调用一下

[[NSNotificationCenter defaultCenter] removeObserver:self]

即可。

留意到连无是所有的对象都用举行上述之操作,如果一个目标从没有让NSNotificationCenter
添加为observer的话,在该dealloc之前调用removeObserver完全是多是一举。
所以我们hook了NSNotificationCenter的

addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName
object:(id)anObject

函数,在该加加observer的早晚,对observer动态添加标记flag。这样于observer
dealloc的当儿,就可以通过flag标记来判断其是否发生必要调用removeObserver函数了。

<section class=”” style=”margin: 30px 0px 0px 8px; padding: 0px;
max-width: 100%; box-sizing: border-box; word-wrap: break-word
!important; font-size: 16px; text-align: left; color: rgb(60, 112,
198);”>

3.4 NSTimer类型crash防护</section>

3.4.1 NSTimer crash 产生原因

以程序开发进程被,大家见面时下定时任务,但以NSTimer的
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:接口做重复性的定时任务时有一个题材:NSTimer会强引用target实例,所以需要在适合的会invalidate定时器,否则即会出于定时器timer强引用target的涉及造成target不能够给放飞,造成内存泄露,甚至以定时任务触发时导致crash。
crash的表现形式以及求实的target执行之selector有关。

再者,如果NSTimer是无限重复的实施一个任务以来,也时有发生或致target的selector一直受再调用且处于低效状态,对app的CPU,内存等特性方面全都是绝非必要之荒废。

所以,很有必不可少设计来同样种方案,可以中的防护NSTimer的滥用问题。

3.4.2 NSTimer crash 防护方案

上面的分析可见,NSTimer所发生的题材之要紧因是以那并未再次一个相当的机遇invalidate,同时还有NSTimer对target的高引用导致的内存泄漏问题。

这就是说解决NSTimer的题材的要害点在于以下简单接触:

  1. NSTimer对那个target是否可以免赛引用

  2. 是否找到一个方便的机遇,在确定NSTimer已经失效的状态下,让NSTimer自动invalidate

有关率先只问题,target的强引用问题。 可以为此而下图的方案来解决:

图片 20

image.gif

以NSTimer和target之间在一重合stubTarget,stubTarget主要做吗一个桥接层,负责NSTimer和target之间的通信。

再者NSTimer强引用stubTarget,而stubTarget弱引用target,这样target和NSTimer之间的干也就是去世引用了,意味着target可以自由的自由,从而化解了巡回引用的题目。

上文提到了stubTarget负责NSTimer和target的通信,其实际的兑现过程还要细分为简单良步:

Step 1. swizzle
NSTimer中scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
相关的法,在初方式吃动态创建stubTarget对象,stubTarget对象弱引用持有原有的target,selector,timer,targetClass等properties。然后将原target分发stubTarget上,selector回调函数为stubTarget的fireProxyTimer,流程如下图:

[图片上传中…(image-2ca561-1512360000925-6)]

Step 2.
经过stubTarget的fireProxyTimer:来具体处理回调函数selector的处理与分发,流程如下图:

图片 21

image.gif

因stubTarget的厕,原有的target已经可以免吃NSTimer强引用的掣肘,而即兴的放飞。

由上图流程可知,当NSTimer的回调函数fireProxyTimer:让执行之上,会自动判断原target是否已给放飞,如果释放了,意味着NSTimer已经不算,此时要还继续调用原有target的selector很有或会见招致crash,而且是未曾必要的。所以这时需要用NSTimer
invalidate,然后统计上报错误数据。如此一来就好了NSTimer在适龄的机自动invalidate。

<section class=”” style=”margin: 30px 0px 0px 8px; padding: 0px;
max-width: 100%; box-sizing: border-box; word-wrap: break-word
!important; font-size: 16px; text-align: left; color: rgb(60, 112,
198);”>

3.5 Container类型crash防护</section>

3.5.1 Container crash 产生原因

Container 类型的crash
指的凡容器类的crash,常见的来NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的crash。
一些周边的越界、插入nil等错误操作均会招致该类crash发生。
由于起的原由比较简单,就无开展来讲述了。

此类crash虽然比较易于排查,但是那于app
crash概率总比还是特别高,所以有必不可少对该展开防。

3.5.2 Container crash 防护方案

Container crash
类型的备方案也比较简单,针对于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些常用的会晤招崩溃的API进行method
swizzling,然后以swizzle的新方式吃参加一些规则限制与判,从而给这些API变的安全,这里虽不进行来具体描述了。

<section class=”” style=”margin: 30px 0px 0px 8px; padding: 0px;
max-width: 100%; box-sizing: border-box; word-wrap: break-word
!important; font-size: 16px; text-align: left; color: rgb(60, 112,
198);”>

3.6 NSString类型crash防护</section>

NSString/NSMutableString 类型的crash的发原因以及防方案和Container
crash很相如,这里呢非开展来讲述了。

<section class=”” style=”margin: 30px 0px 0px 8px; padding: 0px;
max-width: 100%; box-sizing: border-box; word-wrap: break-word
!important; font-size: 16px; text-align: left; color: rgb(60, 112,
198);”>

3.7 野指针类型crash防护</section>

3.7.1 野指针crash 产生原因

在App的拥有Crash中,访问野指针导致的Crash占了深特别一部分,野指针类型crash的显现吧:Exception
Type:SIGSEGV,Exception Codes: SEGV_ACCERR 或者 如下图:

图片 22

image.gif

缓解野指针导致的crash往往是平等码吃力的业务,一来产生crash
的景不好复现,二来crash之后console的消息提供的鼎力相助有限。
XCode本身为方便开放调试时发现野指针问题,提供了Zombie机制,能够以来野指针时提醒出现野指针的切近,从而化解了开发阶段出现野指针的题目。然而对被线及发出的野指针问题,依旧没有一个较好的方来定位问题。

之所以,因为野指针出现概率高而难以定位问题,非常有必不可少对于野指针专门做同样叠防护方法。

3.7.2 野指针crash 防护方案

野指针问题之化解思路方向其实很轻确定,XCode提供了Zombie的体制来祛除查野指针的题材,那么我们当下边可以实现一个看似于Zombie的机制,加上对zombie实例的成套方法拦截机制

消息转发机制,那么即便得形成在野指针访问时未Crash而只是是crash时相关的信息。

又还亟需专注一点:因为zombie的体制亟待在靶释放时保留其指针和相关内存占用,随着app的拓展,越来越多之靶子吃创造及刑释解教,这会促成内存占用越来越深,这样显然对一个常规运行的app的特性有影响。所以待一个相当的zombie对象释放机制,确定zombie机制对内存的熏陶是发度的。

improve版的zombie机制的贯彻重大分为以下四个环节:

step 1. method
swizzling替换NSObject的allocWithZone方法,在新的不二法门被判断该型对象是否用在野指针防护,如果需要,则通过objc_setAssociatedObject为该对象设置flag标记,被标记的对象后续会进去zombie流程

流程图如下:

图片 23

image.gif

做flag标记是以过剩系统类,比如NSString,UIView等创建,释放非常累,而这些实例发生野指针概率非常低。基本还是咱们好写的好像才见面产生野指针的相关题材,所以经过当创造时
设置一个记用来过滤不必要做野指针防护的实例,提高方案的效率。

再就是做判定是否如加盟标记的原则里,我们进入了私名单机制,是以有的特定的类是无适用于上加至zombie机制的,会发出崩溃(例如:NSBundle),而且用与zombie机制相关的近乎为无可知在标记,否则会当放过程被循环引用和调用,导致内存泄漏甚至栈溢出。

step 2. method
swizzling替换NSObject的dealloc方法,对flag标记的目标实例调用objc_destructInstance,释放该实例引用的连带属性,然后用实例的isa修改为HTZombieObject。通过objc_setAssociatedObject
保存将原始类名保存在该实例中。

流程图如下:

图片 24

image.gif

调用objc_destructInstance的原因:

此地参考了系统在Object-C Runtime
中NSZombies实现,dealloc最后见面调动到objectdispose函数,在这函数里面
其实也召开了三件工作,

  1. 调用objc_destructInstance释放该实例引用的相干实例

  2. 以拖欠实例的isa修改也stubClass,接受任意方法调用

  3. 放活该内存

官方文档对objc_destructInstance的诠释吗:

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

说明objc_destructInstance会释放以及实例相关联的援,但是并无自由该实例等内存。

Step 3. 每当HTZombieObject
通过信息转发机制forwardingTargetForSelector处理所有拦截的计,根据selector动态增长能够处理方法的响应者HTStubObject
实例,然后通过 objc_getAssociatedObject
获得之前封存该实例对应之原始类名,统计错误数据。

流程图如下:

图片 25

image.gif

HTZombieObject的拍卖与unrecognized selector
crash的拍卖是同等,主要的目的就是是阻挡所有污染被HTZombieObject的函数,用一个回来为空的函数来替换,从而达到程序不倒的目的。

Step 4.
当下降至后台或者上不释放实例的上限时,则当ht_freeSomeMemory方法中调用原有dealloc方法释放具有被zombie化的实例

综合,可以为此生图总结一下bad access类型crash的防止流程:

图片 26

image.gif

3.7.3 相关的风险

  1. 开了野指针防护,通过动态插入一个拖欠实现之主意来防止出现Crash,但是工作范围的表现难以确定,可能会见进入业务特别的状态。需要拟定一下怎么样表现该问题吃用户之方案。

  2. 鉴于做了延时获释若干实例,对系统总内存会产生一定影响,目前以内存的缓冲区开至2M左右,所以当无那个特别的影响,但还是可能潜在一些高风险。

  3. 延时释放实例是冲相关职能代码会聚焦于有一个时空段调用的设前提下,所以野指针的zombie保护机制只能于那实例对象仍缓存在zombie的缓存机制时才使得,若以实例真正释放之后,再调用野指针还是碰头产出Crash。

<section class=”” style=”margin: 30px 0px 0px 8px; padding: 0px;
max-width: 100%; box-sizing: border-box; word-wrap: break-word
!important; font-size: 16px; text-align: left; color: rgb(60, 112,
198);”>

3.8 非主线程刷UI类型crash防护(UI not on Main Thread)</section>

每当非主线程刷UI将见面造成app运行crash,有必不可少对那开展拍卖。

现阶段初始的处理方案是swizzle UIView类的以下三单艺术:

- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;

在就三独方法调用的时节判断一下脚下的线程,如果不是主线程的话,直接采用
dispatch_async(dispatch_get_main_queue(), ^{ //调用原本方法 });

来拿相应的刷UI的操作转移至主线程上,同时统计错误信息。

但诚执行了然后,发现这三个方法并无能够完全覆盖UIView相关的持有刷UI到操作,但是要是如拿一切暨UIView的刷UI的点子统计起来而swizzle,感觉稍笨拙而不便捷。

因此作者还以搜索,看是不是发生重新好之方案来化解拖欠问题。

<section class=”” style=”margin: 0px; padding: 0px; max-width: 100%;
box-sizing: border-box; word-wrap: break-word !important; font-size:
16px;”>

Chapter 4 – 使用手册

</section>

时sdk实现了以下的机能以及配备:

1. 配备需要防的crash类型

好根据自身需要,选择得之crash防护配置,通过以下的接口进行布置:

- (void)configSafetyGuardService:(HTSafetyGuardType)SafetyGuardType;

内部可安排的SafetyGuardType有:

  • HTSafetyGuardType_None

  • HTSafetyGuardType_All

  • HTSafetyGuardType_UnrecognizedSelector

  • HTSafetyGuardType_KVO

  • HTSafetyGuardType_BadAccess

  • HTSafetyGuardType_Notification

  • HTSafetyGuardType_Timer

  • HTSafetyGuardType_Container

  • HTSafetyGuardType_String

  • HTSafetyGuardType_UI

得因自己种之急需自动选择要防止的型。

2. 实时时 开启/暂停 安全防范效果

布置了后,需要调用-
(void)start;来开启备,防护的开关是实时的(无需更启app),可以当随意的时刻选择
开启/关闭 防护功能。

通过- (BOOL)isWorking接口可以获当前戒效果的状态。

通过- (void)start接口实时开启备功能

通过- (void)stop接口实时关闭防护效果

3. 配置白名单和黑名单,指定相应的怀念 加上/去掉
安全防范效果的切近和对象

出于未同类实现之特殊性,考虑到或某些类并不需要开启备效果。
所以提供了伪名单的法力。
在黑名单里的近乎本身及其子类,都未见面跻身戒备的范围。

白名单的起是因作者在支付的时节发现有些网自带的类是没有必要进入戒备范围之,所以将整体防护的克调动及独具用户从定义之近乎中。
但是后来还要发现多数底crash和部分常用之系的类似(例如NSString,NSDictionary,UIView等等)有十分强的维系,针对于这些常用的系统类还是殊有必不可少开启备的。所以本着这些需要防的系统类,专门供了白名单的作用。

小心:野指针类型的预防,由于其特殊性,不适用于即套白名单和黑名单。
其本身会维护一拟新的是非曲直名单,详见:3.7 野指针类型Crash防护

4.
设置非常处理handler,指定出现crash被抓取情况之后,用户想打定义的操作

并发了crash,并且给我们的体系捕捉到加以处理后,用户可能还亟需进一步的拍卖,例如上传埋点等。这时可以经过设置一个handler来实现,
HTExceptionHandler会将crash的信经过HTCrashInfo的款式来回到。

HTCrashInfo内包含了:

  • 导致crash的类型:crashType

  • crash线程的调用栈:callStackSymbols

  • crash的具体讲述信息:crashDescription

  • 扩展信息:userinfo

如上接口具体详细的音通通好于(HTSafetyGuardService.h)中找到。(注意HTSafetyGuardService是单例)

出于当下sdk还免通过完整的机能测试与总体性测试,故暂不开放对应的sdk。等作者认为项目质量上了肯定的正规化后,会将品种sdk开放出来。如果对该档感兴趣,可以联系
taozeyu890217@126.com
,欢迎并研究。

</section>

相关文章