如何建立iOS crash防護機制

1缘圈、前言

??閃退劣光,指用戶在使用app的過程中異常中斷執(zhí)行,被系統(tǒng)強制結(jié)束應(yīng)用并回到桌面糟把。不僅內(nèi)存信息丟失绢涡,還會阻斷用戶操作流程,對業(yè)務(wù)影響及其嚴重遣疯。
??當然雄可,避免崩潰的最好辦法就是不產(chǎn)生崩潰;在開發(fā)過各中就要盡可能地保證程序的健壯性。但是数苫,人又不是機器聪舒,不可能不犯錯,不可能存在沒有BUG的程序虐急。
??如果能夠利用一些語言機制和系統(tǒng)方法箱残,設(shè)計一套防護系統(tǒng),使之能夠有效的降低APP的崩潰率止吁,那么不僅APP的穩(wěn)定性得到了保障被辑,最重要的是可以減少不必要的加班。
??當然我們不可能強大到把所有類型的crash都處理掉敬惦,但是我們會對一些高頻的crash進行一一的處理盼理,我們的目的就是降低crash率。

2俄删、對RunTime的初識

??C語言作為一門靜態(tài)類語言宏怔,在編譯階段就已經(jīng)確定了所有變量的數(shù)據(jù)類型,同時也確定好了要調(diào)用的函數(shù)抗蠢,以及函數(shù)的實現(xiàn)举哟。
?? 而Objective-C語言是一門動態(tài)語言。在編譯階段并不知道變量的具體數(shù)據(jù)類型迅矛,也不知道所真正調(diào)用的哪個函數(shù)妨猩;在編譯階段,OC可以調(diào)用任何函數(shù)秽褒,即使這個函數(shù)只聲明未實現(xiàn)壶硅,或直接未聲明(performSelector:);只有在運行時間才檢查變量的數(shù)據(jù)類型销斟,同時在運行時才會根據(jù)函數(shù)名查找要調(diào)用的具體函數(shù)庐椒。這樣在程序沒運行的時候,我們并不知道調(diào)用一個方法具體會發(fā)生什么蚂踊。
??實現(xiàn)Objective-C語言運行時機制的一切基礎(chǔ)就是Runtime约谈。Runtime實際上是一個庫,這個庫使我們可以在程序運行時動態(tài)的創(chuàng)建對象犁钟、檢查對象棱诱,修改類和對象的方法。

3涝动、消息機制的基本原理

? ?Objective-C語言中迈勋,方法調(diào)用都是類似[receiver selector];的形式,其本質(zhì)就是讓對象在運行時發(fā)送消息的過程醋粟。
我們來看看方法調(diào)用[receiver selector];『編譯階段』『運行階段』分別做了什么靡菇?

  1. 編譯階段:[receiver selector];方法被編譯器轉(zhuǎn)換為:
    ?1.1. objc_msgSend(receiver重归,selector) (不帶參數(shù))
    ?1.2. objc_msgSend(recevier,selector厦凤,org1鼻吮,org2,…)(帶參數(shù))

OC代碼示例:

 id num = @123;
 NSMutableString *str = [NSMutableString stringWithString:@"Hello"];
 [str appendString:@"World"];

通過clang命令查看OC代碼編譯后的樣子:

//xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
id num = ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 123);
NSMutableString *str = ((NSMutableString * _Nonnull (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("stringWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_zh_hdyn1j_92zn2n74srg5dq88c0000gp_T_main_0eed7d_mi_1);
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_zh_hdyn1j_92zn2n74srg5dq88c0000gp_T_main_0eed7d_mi_2);
  1. 運行時階段:消息接受者recevier尋找對應(yīng)的selector泳唠。
    2.1狈网、通過recevierisa指針找到recevierClass(類)
    2.2笨腥、在Class(類)cache(方法緩存)的散列表中尋找對應(yīng)的IMP(方法實現(xiàn))拓哺;
    2.3、如果在cache(方法緩存)中沒有找到對應(yīng)的IMP(方法實現(xiàn))的話脖母,就繼續(xù)在Class(類)method list(方法列表)中找對應(yīng)的selector士鸥,如果找到,填充到cache(方法緩存)中谆级,并返回selector烤礁;
    2.4、如果在Class(類)中沒有找到這個selector肥照,就繼續(xù)在它的 superClass(父類)中尋找脚仔,以此類推,一直查找到根類舆绎;
    2.5鲤脏、一旦找到對應(yīng)的selector,直接執(zhí)行recevier對應(yīng)selector方法實現(xiàn)的 IMP(方法實現(xiàn))吕朵。
    2.6猎醇、若找不到對應(yīng)的selector,轉(zhuǎn)向攔截調(diào)用努溃,走消息轉(zhuǎn)發(fā);如果沒有重寫攔截調(diào)用的方法,程序就會崩潰硫嘶。

4、防護原理

? ?利用Objective-C語言的動態(tài)特性梧税,采用AOP(Aspect Oriented Programming)面向切面編程的設(shè)計思想沦疾,在不侵入原有項目代碼的基礎(chǔ)之上,通過在 APP 運行時階段對崩潰因素的的攔截和處理第队,使得 APP 能夠持續(xù)穩(wěn)定正常的運行哮塞。
? ?具象的說,就是對需要Hook的類添加Category(分類)斥铺,在各個分類中通過Method Swizzling攔截容易造成崩潰的系統(tǒng)方法彻桃,將系統(tǒng)原有方法與添加的防護方法的selector(方法選擇器)IMP(函數(shù)實現(xiàn)指針)進行對調(diào)坛善。然后在替換方法中添加防護操作晾蜘,從而達到避免以及修復(fù)崩潰的目的邻眷。

5、防護系統(tǒng)可以處理掉哪幾種crash類型剔交?

  • unrecognized selector(找不到對象方法或者類方法的實現(xiàn))
  • KVO Crash
  • KVC Crash
  • NSNotification Crash
  • NSTimer Crash (注冊了沒有主動釋放會內(nèi)存泄露肆饶,甚至在定時任務(wù)觸發(fā)時可能會導(dǎo)致crash)
  • Container / NSString Crash(集合類操作造成的崩潰,例如數(shù)組越界岖常,插入 nil 等)
  • Threading Crash (非主線程刷新UI)

5.1 unrecognized selector類型crash防護

? ?unrecognized selector類型的crash在app眾多的crash類型中占著比較大的成分驯镊,通常是因為一個對象調(diào)用了一個不屬于它方法的方法導(dǎo)致的。
部分復(fù)現(xiàn)代碼:

  //.h中聲明了竭鞍,但是沒有實現(xiàn)此方法
 [self gg];
 //performSelector可以向一個對象傳遞任何消息板惑,而不需要在編譯的時候聲明這些方法
 [self performSelector:@selector(gg1)];
 [self performSelector:NSSelectorFromString(@"gg2:")];
  //id能代表任意類型對象,在編譯期會跳過類型檢查
  id str = @123;
  [str appendString:@"Hello World"];
  //類型不匹配導(dǎo)致崩潰
  [self gg3:@123];
- (void)gg3:(NSString *)str
{
    //沒有對參數(shù)進行類型判斷
    NSInteger length = str.length;
}

? ?在3偎快、消息機制的基本原理 最后一步中有提到:若找不到對應(yīng)的selector冯乘,轉(zhuǎn)向攔截調(diào)用,走消息轉(zhuǎn)發(fā);如果沒有重寫攔截調(diào)用的方法,程序就會崩潰晒夹。
? ?當一個方法找不到的時候裆馒,Runtime 提供了消息動態(tài)解析消息接受者重定向丐怯、消息重定向等三步處理消息喷好,具體流程如下:

圖解:


RunTime消息轉(zhuǎn)發(fā)步驟圖.png

消息動態(tài)解析Objective-C運行時會調(diào)用+resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機會提供一個函數(shù)實現(xiàn)读跷。我們可以通過重寫這兩個方法梗搅,添加其他函數(shù)實現(xiàn),并返回 YES舔亭, 那運行時系統(tǒng)就會重新啟動一次消息發(fā)送的過程些膨。若返回NO或者沒有添加其他函數(shù)實現(xiàn),則進入下一步钦铺。

舉個例子:
#import "ViewController.h"
#include <objc/runtime.h>

@interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // 執(zhí)行 fun 函數(shù)
    [self performSelector:@selector(fun)];
}

// 重寫 resolveInstanceMethod: 添加對象方法實現(xiàn)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(fun)) { // 如果是執(zhí)行 fun 函數(shù)订雾,就動態(tài)解析,指定新的 IMP
        //特殊參數(shù):v@:,具體可參考官方文檔:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
        class_addMethod([self class], sel, (IMP)funMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void funMethod(id obj, SEL _cmd) {
    NSLog(@"funMethod"); //新的 fun 函數(shù)
}

控制臺打印結(jié)果:
2021-03-30 16:50:06.145815+0800 CrashKiller_Example[22017:4869725] funMethod

從上邊例子中可以看出矛洞,雖然我們沒有實現(xiàn) fun 方法沼本,但是通過重寫resolveInstanceMethod: ,
利用 class_addMethod 方法添加對象方法實現(xiàn) funMethod 方法抽兆,并執(zhí)行。
從打印結(jié)果來看辫红,成功調(diào)起了funMethod 方法祝辣。

消息接受者重定向:如果當前對象實現(xiàn)了forwardingTargetForSelector:Runtime就會調(diào)用這個方法切油,允許我們將消息的接受者轉(zhuǎn)發(fā)給其他對象澎胡。如果這一步方法返回 nil攻谁,則進入下一步。

舉個例子:
#import "ViewController.h"
#include "objc/runtime.h"

@interface Person : NSObject

- (void)fun;

@end

@implementation Person

- (void)fun {
    NSLog(@"fun");
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 執(zhí)行 fun 方法
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 為了進行下一步 消息接受者重定向
}

// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(fun)) {
        return [[Person alloc] init];
        // 返回 Person 對象槐瑞,讓 Person 對象接收這個消息
    }
    return [super forwardingTargetForSelector:aSelector];
}
控制臺打印結(jié)果:
2021-03-30 17:10:52.645582+0800 CrashKiller_Example[22427:4885650] fun

可以看到困檩,雖然當前 ViewController 沒有實現(xiàn) fun 方法+resolveInstanceMethod:也沒有添加其他函數(shù)實現(xiàn)悼沿。
但是我們通過 forwardingTargetForSelector 把當前 ViewController 的方法轉(zhuǎn)發(fā)給了 Person 對象去執(zhí)行了糟趾。
打印結(jié)果也證明我們成功實現(xiàn)了轉(zhuǎn)發(fā)义郑。

消息重定向Runtime系統(tǒng)利用methodSignatureForSelector:方法獲取函數(shù)的參數(shù)和返回值類型丈钙。

  • 如果methodSignatureForSelector:返回了一個NSMethodSignature對象(函數(shù)簽名)雏赦,Runtime系統(tǒng)就會創(chuàng)建一個NSInvocation對象星岗,并通過forwardInvocation: 消息通知當前對象俏橘,給予此次消息發(fā)送最后一次尋找IMP的機會。
  • 如果methodSignatureForSelector:返回 nil汉额。則Runtime系統(tǒng)會發(fā)出 doesNotRecognizeSelector:消息榨汤,程序也就崩潰了收壕。
舉個例子:
#import "ViewController.h"
#include "objc/runtime.h"

@interface Person : NSObject

- (void)fun;

@end

@implementation Person

- (void)fun {
    NSLog(@"fun");
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 執(zhí)行 fun 函數(shù)
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 為了進行下一步 消息接受者重定向
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil; // 為了進行下一步 消息重定向
}

// 獲取函數(shù)的參數(shù)和返回值類型蜜宪,返回簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;   // 從 anInvocation 中獲取消息
    Person *p = [[Person alloc] init];
    if([p respondsToSelector:sel]) {   // 判斷 Person 對象方法是否可以響應(yīng) sel
        [anInvocation invokeWithTarget:p];  // 若可以響應(yīng)圃验,則將消息轉(zhuǎn)發(fā)給其他對象處理
    } else {
        [self doesNotRecognizeSelector:sel];  // 若仍然無法響應(yīng)澳窑,則報錯:找不到響應(yīng)方法
    }
}
@end

控制臺打印結(jié)果:
2021-03-30 17:26:26.719413+0800 CrashKiller_Example[22715:4896625] fun

從補救三部曲中摊聋,選擇哪一步作為入手點最合適麻裁?
? ?1. resolveInstanceMethod需要在類的本身動態(tài)的添加它本身不存在的方法煎源,這些方法對于該類本身來說是冗余的
? ?2. forwardInvocation可以通過NSInvocation的形式將消息轉(zhuǎn)發(fā)給多個對象手销,但是其開銷比較大图张,需要創(chuàng)建新的NSInvocation對象埂淮,并且forwardInvocation的函數(shù)經(jīng)常被使用者調(diào)用來做消息的轉(zhuǎn)發(fā)選擇機制倔撞,不適合多次重寫痪蝇。
? ?3. forwardingTargetForSelector可以將消息轉(zhuǎn)發(fā)給一個對象,開銷較小耙册,并且被重寫的概率較低毫捣,適合重寫蔓同。
具體步驟如下:
? ?1. 給NSObject添加一個分類斑粱,在分類中實現(xiàn)一個自定義的-crashKiller_forwardingTargetForSelector:方法则北;
? ?2. 利用Method Swizzling-forwardingTargetForSelector:-crashKiller_forwardingTargetForSelector:進行方法交換尚揣。
? ?3. 在自定義的方法中惑艇,先判斷當前對象是否已經(jīng)實現(xiàn)了消息接受者重定向消息重定向滨巴。如果都沒有實現(xiàn)恭取,就動態(tài)創(chuàng)建一個目標類蜈垮,給目標類動態(tài)添加一個方法。
? ?4. 把消息轉(zhuǎn)發(fā)給動態(tài)生成類的實例對象调塌,由目標類動態(tài)創(chuàng)建的方法實現(xiàn)羔砾,這樣 APP 就不會崩潰了姜凄。
實現(xiàn)代碼如下:

#import "NSObject+KillSelector.h"
#import "NSObject+CrashKillerMethodSwizzling.h"

@implementation NSObject (KillSelector)

+ (void)registerKillSelector
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        // 攔截 `+forwardingTargetForSelector:` 方法态秧,替換自定義實現(xiàn)
        [NSObject crashKillerMethodSwizzlingClassMethod:@selector(forwardingTargetForSelector:)
                                             withMethod:@selector(crashKiller_forwardingTargetForSelector:)
                                              withClass:[NSObject class]];

        // 攔截 `-forwardingTargetForSelector:` 方法申鱼,替換自定義實現(xiàn)
        [NSObject crashKillerMethodSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:)
                                                withMethod:@selector(crashKiller_forwardingTargetForSelector:)
                                                 withClass:[NSObject class]];

    });
}


+ (id)crashKiller_forwardingTargetForSelector:(SEL)aSelector {

    if ([[CrashKillerManager shareManager].selectorClassWhiteList containsObject:[self class]]) {
        return [self crashKiller_forwardingTargetForSelector:aSelector];
    }
    SEL forwarding_sel = @selector(forwardingTargetForSelector:);
    // 獲取 NSObject 的消息轉(zhuǎn)發(fā)方法
    Method root_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel);
    // 獲取 當前類 的消息轉(zhuǎn)發(fā)方法
    Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel);
    // 判斷當前類本身是否實現(xiàn)第二步:消息接受者重定向
    BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
    // 如果沒有實現(xiàn)第二步:消息接受者重定向
    if (!realize) {
        // 判斷有沒有實現(xiàn)第三步:消息重定向
        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);

        // 如果沒有實現(xiàn)第三步:消息重定向
        if (!realize) {
            // 創(chuàng)建一個新類
            NSString *errClassName = NSStringFromClass([self class]);
            NSString *errSel = NSStringFromSelector(aSelector);

            NSString *reason = [NSString stringWithFormat:@"-[%@ %@]: unrecognized selector sent to class %p",errClassName,errSel,self];
            [[CrashKillerManager shareManager] throwExceptionWithName:@"NSInvalidArgumentException" reason:reason];

            NSString *className = @"CrachClass";
            Class cls = NSClassFromString(className);

            // 如果類不存在 動態(tài)創(chuàng)建一個類
            if (!cls) {
                Class superClsss = [NSObject class];
                cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
                // 注冊類
                objc_registerClassPair(cls);
            }
            // 如果類沒有對應(yīng)的方法,則動態(tài)添加一個
            if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
                class_addMethod(cls, aSelector, (IMP)CrashIMP, "@@:@");
            }
            // 把消息轉(zhuǎn)發(fā)到當前動態(tài)生成類的實例對象上
            return [[cls alloc] init];
        }
    }
    return [self crashKiller_forwardingTargetForSelector:aSelector];
}

- (id)crashKiller_forwardingTargetForSelector:(SEL)aSelector {

    if ([[CrashKillerManager shareManager].selectorClassWhiteList containsObject:[self class]]) {
        return [self crashKiller_forwardingTargetForSelector:aSelector];
    }
    SEL forwarding_sel = @selector(forwardingTargetForSelector:);
    // 獲取 NSObject 的消息轉(zhuǎn)發(fā)方法
    Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
    // 獲取 當前類 的消息轉(zhuǎn)發(fā)方法
    Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);
    // 判斷當前類本身是否實現(xiàn)第二步:消息接受者重定向
    BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);

    // 如果沒有實現(xiàn)第二步:消息接受者重定向
    if (!realize) {
        // 判斷有沒有實現(xiàn)第三步:消息重定向
        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);

        // 如果沒有實現(xiàn)第三步:消息重定向
        if (!realize) {
            // 創(chuàng)建一個新類
            NSString *errClassName = NSStringFromClass([self class]);
            NSString *errSel = NSStringFromSelector(aSelector);

            NSString *reason = [NSString stringWithFormat:@"-[%@ %@]: unrecognized selector sent to instance %p",errClassName,errSel,self];
            [[CrashKillerManager shareManager] throwExceptionWithName:@"NSInvalidArgumentException" reason:reason];


            NSString *className = @"CrachClass";
            Class cls = NSClassFromString(className);
            // 如果類不存在 動態(tài)創(chuàng)建一個類
            if (!cls) {
                Class superClsss = [NSObject class];
                cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
                // 注冊類
                objc_registerClassPair(cls);
            }
            // 如果類沒有對應(yīng)的方法竿痰,則動態(tài)添加一個
            if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
                class_addMethod(cls, aSelector, (IMP)CrashIMP, "@@:@");
            }
            // 把消息轉(zhuǎn)發(fā)到當前動態(tài)生成類的實例對象上
            return [[cls alloc] init];
        }
    }
    return [self crashKiller_forwardingTargetForSelector:aSelector];
}

// 動態(tài)添加的方法實現(xiàn)
static int CrashIMP(id slf, SEL selector) {
    return 0;
}
@end

5.2 KVO Crash類型crash防護

KVO(Key-Value Observing)影涉,它提供一種機制蟹倾,當指定的對象的屬性被修改后鲜棠,KVO就會自動通知相應(yīng)的觀察者。
通常一個對象的KVO關(guān)系圖如下:

對象的kvo關(guān)系圖.jpg

    [self.wkWebView addObserver:self
                      forKeyPath:@"estimatedProgress"
                         options:NSKeyValueObservingOptionNew
                         context:nil];
    [self.wkWebView addObserver:self
                      forKeyPath:@"title"
                         options:NSKeyValueObservingOptionNew
                         context:nil];

KVO Crash常見原因:
??1. KVO 添加次數(shù)和移除次數(shù)不匹配:
??????移除未注冊的觀察者,導(dǎo)致崩潰
??????重復(fù)移除多次表鳍,移除次數(shù)多于添加次數(shù)譬圣,導(dǎo)致崩潰
??????重復(fù)添加多次胁镐,雖然不會崩潰,但是發(fā)生改變時颇玷,也同時會被觀察多次帖渠。
??2. 被觀察者提前被釋放空郊,被觀察者在 dealloc 時仍然注冊著 KVO狞甚,導(dǎo)致崩潰哼审。 例如:被觀察者是局部變量的情況(iOS 10 及之前會崩潰)涩盾。
??3. 添加了觀察者春霍,但未實現(xiàn) observeValueForKeyPath:ofObject:change:context: 方法址儒,導(dǎo)致崩潰衅疙。
??4. 添加或者移除時 keypath == nil炼蛤,導(dǎo)致崩潰理朋。

解決方案:
??1. 首先為 NSObject 建立一個分類嗽上,利用 Method Swizzling兽愤,實現(xiàn)自定義的 crashKiller_addObserver:forKeyPath:options:context:crashKiller_removeObserver:forKeyPath:
crashKiller_removeObserver:forKeyPath:context:哲思、
crashKiller_dealloc方法棚赔,用來替換系統(tǒng)原生的添加移除觀察者方法的實現(xiàn)靠益。
??2. 然后在觀察者和被觀察者之間建立一個 KVOProxy對象胧后,兩者之間通過KVOProxy對象建立聯(lián)系壳快。然后在添加和移除操作時江醇,將 KVO 的相關(guān)信息例如 observer陶夜、keyPath保存進關(guān)系哈希表kvoInfoMap中条辟。 關(guān)系哈希表的數(shù)據(jù)結(jié)構(gòu):{keypath : observer1, observer2 , ...}
??3. 在添加和移除操作的時候羽嫡,利用KVOProxy對象做轉(zhuǎn)發(fā)杭棵,把真正的觀察者變?yōu)?KVOProxy對象魂爪,而當被觀察者的特定屬性發(fā)生了改變滓侍,再由 KVOProxy對象 分發(fā)到原有的觀察者上撩笆。

添加觀察者時: 通過關(guān)系哈希表判斷是否重復(fù)添加夕冲,只添加一次耘擂。
移除觀察者時: 通過關(guān)系哈希表是否已經(jīng)進行過移除操作醉冤,避免多次移除蚁阳。
觀察鍵值改變時: 同樣通過關(guān)系哈希表判斷螺捐,將改變操作分發(fā)到原有的觀察者上定血。
??另外澜沟,為了避免被觀察者提前被釋放茫虽,被觀察者在 dealloc 時仍然注冊著 KVO 導(dǎo)致崩潰濒析。防護系統(tǒng)還利用 Method Swizzling 實現(xiàn)了自定義的 dealloc号杏,在系統(tǒng) dealloc 調(diào)用之前盾致,將多余的觀察者移除掉绰上。

KVO崩潰測試代碼:

 /**
 1.1 移除了未注冊的觀察者蜈块,導(dǎo)致崩潰
 */
- (void)testKVOCrash11 {
    // 崩潰日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
    [self.objc removeObserver:self forKeyPath:@"name"];
}

/**
 1.2 重復(fù)移除多次爽哎,移除次數(shù)多于添加次數(shù)课锌,導(dǎo)致崩潰
 */
- (void)testKVOCrash12 {
    // 崩潰日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
    [self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    self.objc.name = @"0";
    [self.objc removeObserver:self forKeyPath:@"name"];
    [self.objc removeObserver:self forKeyPath:@"name"];
}

/**
 1.3 重復(fù)添加多次渺贤,雖然不會崩潰志鞍,但是發(fā)生改變時固棚,也同時會被觀察多次此洲。
 */
- (void)testKVOCrash13 {
    [self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    [self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    self.objc.name = @"0";
}

/**
 2. 被觀察者 dealloc 時仍然注冊著 KVO呜师,導(dǎo)致崩潰
 */
- (void)testKVOCrash2 {
    // 崩潰日志:An instance xxx of class xxx was deallocated while key value observers were still registered with it.
    // iOS 10 及以下會導(dǎo)致崩潰,iOS 11 之后就不會崩潰了
    CrashObject *obj = [[CrashObject alloc] init];
    [obj addObserver: self
          forKeyPath: @"name"
             options: NSKeyValueObservingOptionNew
             context: nil];
    obj = nil;
}

/**
 3. 觀察者沒有實現(xiàn) -observeValueForKeyPath:ofObject:change:context:導(dǎo)致崩潰
 */
- (void)testKVOCrash3 {
    // 崩潰日志:An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
    CrashObject *obj = [[CrashObject alloc] init];

    [self addObserver: obj
           forKeyPath: @"title"
              options: NSKeyValueObservingOptionNew
              context: nil];
    self.title = @"111";
}

/**
 4. 添加或者移除時 keypath == nil碰酝,導(dǎo)致崩潰送爸。
 */
- (void)testKVOCrash4 {
    // 崩潰日志: -[__NSCFConstantString characterAtIndex:]: Range or index out of bounds
    CrashObject *obj = [[CrashObject alloc] init];
    [self addObserver: obj
           forKeyPath: @""
              options: NSKeyValueObservingOptionNew
              context: nil];

        [self removeObserver:obj forKeyPath:@""];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context {

    NSLog(@"object = %@, keyPath = %@", object, keyPath);
}

5.3 KVC Crash類型crash防護

? ?KVC(Key Value Coding),即鍵值編碼纹磺,提供一種機制來間接訪問對象的屬性橄杨。而不是通過調(diào)用Setter式矫、Getter方法進行訪問采转。
KVC 日常使用造成崩潰的原因通常有以下幾個:
??1. key 不是對象的屬性故慈,造成崩潰察绷。
??2. keyPath 不正確克婶,造成崩潰情萤。
??3. key 為 nil筋岛,造成崩潰睁宰。
??4. value 為 nil柒傻,為非對象設(shè)值红符,造成崩潰致开。
常見的使用 KVC 造成崩潰代碼:

/********************* CrashObject.h 文件 *********************/
#import <Foundation/Foundation.h>
@interface CrashObject : NSObject
@property (nonatomic, copy) NSString *name;
@end

/********************* ViewController.m 文件 *********************/
#import "ViewController.h"
#import "CrashObject.h"
@interface ViewController ()
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
//   1. key 不是對象的屬性双戳,造成崩潰
//    [self testKVCCrash1];
//    2. keyPath 不正確飒货,造成崩潰
//    [self testKVCCrash2];
//    3. key 為 nil膏斤,造成崩潰
//    [self testKVCCrash4];
//    4. value 為 nil傲茄,為非對象設(shè)值盘榨,造成崩潰
//    [self testKVCCrash4];
}
/**
 1. key 不是對象的屬性草巡,造成崩潰
 */
- (void)testKVCCrash1 {
    // 崩潰日志:[<KVCCrashObject 0x600000d48ee0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key XXX.;

    CrashObject *objc = [[CrashObject alloc] init];
    [objc setValue:@"value" forKey:@"address"];
}

/**
 2. keyPath 不正確山憨,造成崩潰
 */
- (void)testKVCCrash2 {
    // 崩潰日志:[<KVCCrashObject 0x60000289afb0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key XXX.
    CrashObject *objc = [[CrashObject alloc] init];
    [objc setValue:@"后廠村路" forKeyPath:@"address.street"];
}
/**
 3. key 為 nil,造成崩潰
 */
- (void)testKVCCrash3 {
    // 崩潰日志:'-[KVCCrashObject setValue:forKey:]: attempt to set a value for a nil key
    NSString *keyName;
    // key 為 nil 會崩潰棚亩,如果傳 nil 會提示警告讥蟆,傳空變量則不會提示警告
    CrashObject *objc = [[CrashObject alloc] init];
    [objc setValue:@"value" forKey:keyName];
}
/**
 4. value 為 nil瘸彤,造成崩潰
 */
- (void)testKVCCrash4 {
    // 崩潰日志:[<KVCCrashObject 0x6000028a6780> setNilValueForKey]: could not set nil as the value for the key XXX.
    // value 為 nil 會崩潰
    CrashObject *objc = [[CrashObject alloc] init];
    [objc setValue:nil forKey:@"name"];
}
@end

KVC的setter和getter方法
Setter方法
??系統(tǒng)在執(zhí)行setValue:forKey:方法時低零,會把keyvalue作為輸入?yún)?shù)掏婶,并嘗試在接收調(diào)用對象的內(nèi)部雄妥,給屬性key設(shè)置value值。
通過以下幾個步驟:

`官方說明請查看 NSKeyValueCoding.h 文件`
1. 按順序查找名為  `set<Key>: `黎炉、`_set<Key>:`、 `setIs<Key>:`方法庆械。如果找到方法缭乘,則執(zhí)行該方法堕绩,使用輸入?yún)?shù)設(shè)置變量逛尚,則`setValue:forKey:`完成執(zhí)行刁愿。如果沒找到方法绰寞,則執(zhí)行下一步。
2. 訪問類的`accessInstanceVariablesDirectly`屬性铣口。如果    `accessInstanceVariablesDirectly`屬性返回**YES**滤钱,就按順序查找名為   `_<key>`、`_is<Key>`脑题、`<key>`、`is<Key>`的實例變量叔遂,如果找到了對應(yīng)的實例變量他炊,則使用輸入?yún)?shù)設(shè)置變量。則`setValue:forKey:`完成執(zhí)行已艰。如果未找到對應(yīng)的實例變量痊末,或者`accessInstanceVariablesDirectly`屬性返回**NO**則執(zhí)行下一步。
3. 調(diào)用`setValue: forUndefinedKey:`方法哩掺,并引發(fā)崩潰凿叠。
  • 相關(guān)代碼:
CrashObject *objc = [[CrashObject alloc] init];
[objc setValue:@"value" forKey:@"name"];

Getter方法
??系統(tǒng)在執(zhí)行valueForKey:方法時,會將給定的key作為輸入?yún)?shù),在調(diào)用對象的內(nèi)部進行以下幾個步驟:
??1. 按順序查找名為get<Key>盒件、<key>蹬碧、is<Key>_<key>的訪問方法炒刁。如果找到恩沽,調(diào)用該方法,并繼續(xù)執(zhí)行步驟 5翔始。否則繼續(xù)向下執(zhí)行步驟 2飒筑。
??2. 搜索形如countOf<Key>objectIn<Key>AtIndex:绽昏、<key>AtIndexes:的方法协屡。
??3. 如果實現(xiàn)了countOf<Key>方法,并且實現(xiàn)了objectIn<Key>AtIndex:<key>AtIndexes:這兩個方法的任意一個方法全谤,系統(tǒng)就會以NSArray為父類肤晓,動態(tài)生成一個類型為NSKeyValueArray的集合類對象,并調(diào)用上邊的實現(xiàn)方法认然,將結(jié)果直接返回补憾。
??? - 如果對象還實現(xiàn)了形如get<Key>:range:的方法,系統(tǒng)也會在必要的時候自動調(diào)用卷员。
??? - 如果上述操作不成功則繼續(xù)向下執(zhí)行步驟 3盈匾。
??? - 如果上邊兩步失敗,系統(tǒng)就會查找形如countOf<Key>毕骡、 enumeratorOf<Key>削饵、 memberOf<Key>:的方法。系統(tǒng)會自動生成一個 NSSet類型的集合類對象未巫,該對象響應(yīng)所有NSSet方法并將結(jié)果返回窿撬。如果查找失敗,則執(zhí)行步驟 4叙凡。
??4. 如果上邊三步失敗劈伴,系統(tǒng)就會訪問類的accessInstanceVariablesDirectly方法。
???? - 如果返回YES握爷,就按順序查找名為_<key>跛璧、 _is<Key><key>新啼、 is<Key>的實例變量追城。如果找到了對應(yīng)的實例變量,則直接獲取實例變量的值师抄。并繼續(xù)執(zhí)行步驟 5漓柑。
???? - 如果返回NO,或者未找到對應(yīng)的實例變量叨吮,則繼續(xù)執(zhí)行步驟 6辆布。
??5. 分為三種情況:
??? - 如果檢索到的屬性值是對象指針,則直接返回結(jié)果茶鉴。
??? - 如果檢索到的屬性值是NSNumber支持的基礎(chǔ)數(shù)據(jù)類型锋玲,則將其存儲在 NSNumber 實例中并返回該值。
??? - 如果檢索到的屬性值是NSNumber不支持的數(shù)據(jù)類型涵叮,則轉(zhuǎn)換為 NSValue 對象并返回該對象惭蹂。
??6. 如果一切都失敗了,調(diào)用valueForUndefinedKey:割粮,并引發(fā)崩潰盾碗。

KVC Crash 防護方案
??從 Setter 方法 和 Getter 方法 可以看出:
???? - setValue:forKey:執(zhí)行失敗會調(diào)用setValue: forUndefinedKey:方法,并引發(fā)崩潰舀瓢。
???? - valueForKey:執(zhí)行失敗會調(diào)用valueForUndefinedKey:方法廷雅,并引發(fā)崩潰。
??所以京髓,為了進行 KVC Crash 防護航缀,我們就需要重寫setValue: forUndefinedKey:方法和valueForUndefinedKey:方法。重寫這兩個方法之后堰怨,就可以防護1. key不是對象的屬性2. keyPath不正確這兩種崩潰情況了芥玉。
??那么3. key為nil,造成崩潰的情況备图,該怎么防護呢灿巧?
??我們可以利用 Method Swizzling方法,在NSObject的分類中將 setValue:forKey:crashKiller_setValue:forKey:進行方法交換揽涮。然后在自定義的方法中砸烦,添加對keynil這種類型的判斷。
??還有最后一種4. value為nil绞吁,為非對象設(shè)值幢痘,造成崩潰 的情況。
??在NSKeyValueCoding.h文件中家破,有一個setNilValueForKey:方法颜说。上邊的官方注釋給了我們答案。
??在調(diào)用setValue:forKey:方法時汰聋,系統(tǒng)如果查找到名為set<Key>:方法的時候门粪,會去檢測value的參數(shù)類型,如果參數(shù)類型為NSNmber的標量類型或者是 NSValue的結(jié)構(gòu)類型烹困,但是valuenil時玄妈,會自動調(diào)用 setNilValueForKey:方法。這個方法的默認實現(xiàn)會引發(fā)崩潰。
??所以為了防止這種情況導(dǎo)致的崩潰拟蜻,我們可以通過重寫setNilValueForKey:來解決绎签。
??至此,上文提到的 KVC 使用不當造成的四種類型崩潰就都解決了酝锅。
下面我們來看下具體實現(xiàn)代碼:

+ (void)registerKillKVC
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        [NSObject crashKillerMethodSwizzlingInstanceMethod:@selector(setValue:forKey:)
                                                withMethod:@selector(crashKiller_setValue:forKey:)
                                                 withClass:[NSObject class]];
    });
}

- (void)crashKiller_setValue:(id)value forKey:(NSString *)key {

    @try {
        [self crashKiller_setValue:value forKey:key];
    } @catch (NSException *exception) {
        [[CrashKillerManager shareManager] printLogWithException:exception];
    }
}

- (void)setNilValueForKey:(NSString *)key {

    NSString *reason = [NSString stringWithFormat:@"[<%@ %p> setValue:forUndefinedKey:]: could not set nil as the value for the key %@. ***",NSStringFromClass([self class]),self,key];
    [[CrashKillerManager shareManager] throwExceptionWithName:@"NSUnknownKeyException" reason:reason];
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {

    NSString *reason = [NSString stringWithFormat:@"[<%@ %p> valueForUndefinedKey:]: this class is not key value coding-compliant for the key %@.",NSStringFromClass([self class]),self,key];
    [[CrashKillerManager shareManager] throwExceptionWithName:@"NSUnknownKeyException" reason:reason];
}

- (nullable id)valueForUndefinedKey:(NSString *)key {

    NSString *reason = [NSString stringWithFormat:@"[<%@ %p> valueForUndefinedKey:]: this class is not key value coding-compliant for the key %@.",NSStringFromClass([self class]),self,key];
    [[CrashKillerManager shareManager] throwExceptionWithName:@"NSUnknownKeyException" reason:reason];
    return self;
}

5.4 NSNotification Crash類型crash防護

? ?當一個對象添加了notification之后诡必,如果dealloc的時候,仍然持有notification搔扁,就會出現(xiàn)NSNotification類型的crash爸舒。
? ?NSNotification類型的crash多產(chǎn)生于程序員寫代碼時候犯疏忽,在NSNotificationCenter添加一個對象為observer之后稿蹲,忘記了在對象dealloc的時候移除它扭勉。
? ?所幸的是,蘋果在iOS9之后專門針對于這種情況做了處理苛聘,所以在iOS9之后涂炎,即使開發(fā)者沒有移除observer,Notification crash也不會再產(chǎn)生了焰盗。

5.5 NSTimer Crash類型crash防護

NSTimer crash 產(chǎn)生原因:
? ?在程序開發(fā)過程中璧尸,大家會經(jīng)常使用定時任務(wù),但使用NSTimerscheduledTimerWithTimeInterval:target:selector:userInfo:repeats:接口做重復(fù)性的定時任務(wù)時存在一個問題:NSTimer會強引用target實例熬拒,所以需要在合適的時機invalidate定時器爷光,否則就會由于定時器timer強引用target的關(guān)系導(dǎo)致target不能被釋放,造成內(nèi)存泄露澎粟,甚至在定時任務(wù)觸發(fā)時導(dǎo)致crash蛀序。 crash的展現(xiàn)形式和具體的target執(zhí)行的selector有關(guān)。
? ?與此同時活烙,如果NSTimer是無限重復(fù)的執(zhí)行一個任務(wù)的話徐裸,也有可能導(dǎo)致targetselector一直被重復(fù)調(diào)用且處于無效狀態(tài),對app的CPU啸盏,內(nèi)存等性能方面均是沒有必要的浪費重贺。
? ?所以,很有必要設(shè)計出一種方案回懦,可以有效的防護NSTimer的濫用問題气笙。

NSTimer crash 防護方案:
? ?上面的分析可見,NSTimer所產(chǎn)生的問題的主要原因是因為其沒有再一個合適的時機invalidate怯晕,同時還有NSTimer對target的強引用導(dǎo)致的內(nèi)存泄漏問題潜圃。
? ?那么解決NSTimer的問題的關(guān)鍵點在于以下兩點:
? ?? ?1. NSTimer對其target是否可以不強引用。
? ?? ?2. 是否找到一個合適的時機舟茶,在確定NSTimer已經(jīng)失效的情況下谭期,讓NSTimer自動invalidate堵第。
? ?關(guān)于第一個問題,target的強引用問題,可以用如下圖的方案來解決:

image.png

? ?在NSTimer和target之間加入一層stubTarget隧出,stubTarget主要做為一個橋接層踏志,負責(zé)NSTimer和target之間的通信。同時NSTimer強引用stubTarget鸳劳,而stubTarget弱引用target狰贯,這樣target和NSTimer之間的關(guān)系也就是弱引用了也搓,意味著target可以自由的釋放赏廓,從而解決了循環(huán)引用的問題。
? ?上文提到了stubTarget負責(zé)NSTimer和target的通信傍妒,其具體的實現(xiàn)過程又細分為兩大步:
? ?1. swizzle NSTimer中scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:相關(guān)的方法幔摸,在新方法中動態(tài)創(chuàng)建stubTarget對象,stubTarget對象弱引用持有原有的target颤练,selector既忆,timer,targetClass等properties嗦玖。然后將原target分發(fā)stubTarget上患雇,selector回調(diào)函數(shù)為stubTarget的fireProxyTimer;
? ?2. 通過stubTarget的fireProxyTimer:來具體處理回調(diào)函數(shù)selector的處理和分發(fā),當NSTimer的回調(diào)函數(shù)fireProxyTimer:被執(zhí)行的時候宇挫,會自動判斷原target是否已經(jīng)被釋放苛吱,如果釋放了,意味著NSTimer已經(jīng)無效器瘪,此時如果還繼續(xù)調(diào)用原有target的selector很有可能會導(dǎo)致crash翠储,而且是沒有必要的。所以此時需要將NSTimer invalidate橡疼。
? ?如此一來就做到了NSTimer在合適的時機自動invalidate援所。

5.6 Container / NSString 類型crash防護

? ?這個問題比較簡單,就是對一些常用類中會導(dǎo)致崩潰的API進行method swizzling,然后在swizzle的新方法中加入一些條件限制和判斷欣除,從而讓這些API變的安全住拭。
? ?目前對以下類進行防范,類中可能導(dǎo)致crash的方法历帚,逐步進行增量擴充滔岳。
? ?NSArray/NSMutableArray
? ?NSDictionary/NSMutableDictionary
? ?NSString / NSMutableString

5.7 非主線程刷UI類型crash防護(UI not on Main Thread)

初步處理方案就是swizzle UIView類的以下三個方法:

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

在這三個方法調(diào)用的時候判斷 一下當前的線程,如果不是主線程的話抹缕,直接利用dispatch_async(dispatch_get_main_queue(), ^{ //調(diào)用原本方法 });
來將對應(yīng)的刷新UI的操作轉(zhuǎn)移到主線程澈蟆。
考慮到此三個方法調(diào)用頻率過高,且在開發(fā)階段xcode就會給出提示卓研;故不考慮維護此類異常crah趴俘。

image.png

6 CrashKiller防護系統(tǒng)設(shè)計文檔:

gitlab.mypaas.com.cn_appcloud_cocoapods_CrashKiller.png

7 CrashKiller崩潰測試用例

Simulator Screen Recording - iPhone 11 - 2021-09-28 at 11.39.41.gif
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末睹簇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子寥闪,更是在濱河造成了極大的恐慌太惠,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疲憋,死亡現(xiàn)場離奇詭異凿渊,居然都是意外死亡,警方通過查閱死者的電腦和手機缚柳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門埃脏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秋忙,你說我怎么就攤上這事彩掐。” “怎么了灰追?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵堵幽,是天一觀的道長。 經(jīng)常有香客問我弹澎,道長朴下,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任苦蒿,我火速辦了婚禮殴胧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘刽肠。我一直安慰自己溃肪,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布音五。 她就那樣靜靜地躺著惫撰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪躺涝。 梳的紋絲不亂的頭發(fā)上厨钻,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音坚嗜,去河邊找鬼夯膀。 笑死,一個胖子當著我的面吹牛苍蔬,可吹牛的內(nèi)容都是我干的诱建。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼碟绑,長吁一口氣:“原來是場噩夢啊……” “哼俺猿!你這毒婦竟也來了茎匠?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤押袍,失蹤者是張志新(化名)和其女友劉穎诵冒,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谊惭,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡汽馋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了圈盔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片豹芯。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖药磺,靈堂內(nèi)的尸體忽然破棺而出告组,到底是詐尸還是另有隱情煤伟,我是刑警寧澤癌佩,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站便锨,受9級特大地震影響围辙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜放案,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一姚建、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吱殉,春花似錦掸冤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至押赊,卻和暖如春饺藤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背流礁。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工涕俗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人神帅。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓再姑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親找御。 傳聞我的和親對象是個殘疾皇子元镀,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容