iOS开发-简单分析防线上crash

我们开发APP,虽然在极力避免出现线上crash,但是某些情况还是没法把控,比如和后端约定好的数据格式,突然哪天给你换了,很容易导致crash。但是如果我们在任何地方都做防御性判断,代码会写得特别难受。

之前看到有人开源了防止crash的代码,所以分析了下。这些方案主要利用runtime的方法交换和消息转发来实现,对那些容易引起crash的方法,添加判断,或者在crash之后走消息转发

之前项目用到这个,NSObjectSafe就是这么一个开源库,只需要拖进工程就可以起作用。
NSObjectSafe代码地址:github链接

比如,向NSArray插入一个nil、获取NSArray长度之外的元素等等,从而导致APP奔溃。NSObjectSafe能有效避免这些奔溃。这些方案,基本都在load中交换这些容易引起crash的方法,并且添加判断,以此来避免异常。简化版就是下面这样:

@implementation NSArray (Safe)
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        /* 没内容类型是__NSArray0 */
        swizzleInstanceMethod(NSClassFromString(@"__NSArray0"),  @selector(objectAtIndex:),  @selector(hookObjectAtIndex:));
        /* 有内容obj类型才是__NSArrayI */
        swizzleInstanceMethod(NSClassFromString(@"__NSArrayI"),  @selector(objectAtIndex:),  @selector(hookObjectAtIndex:));
        .
        .//还有很多
        .
    });
}

- (id) hookObjectAtIndex:(NSUInteger)index {
    @synchronized (self) {
        if (index < self.count) {
            return [self hookObjectAtIndex:index];
        }
        SFAssert(NO, @"NSArray invalid index:[%@]", @(index));
        return nil;
    }
}

可以看到,这里面用了个+load 方法、runtime的方法交换。

那么 +load 到底是如何被调用的呢?

在 iOS 开发中,我们经常会使用 +load 方法来做一些在 main 函数之前的操作,比如方法交换(Method Swizzle)等。

+load()方法的调用时机是这样的:
1、当类或分类被添加到 Obj-C 运行时的时候被调用;可以实现该方法用来在加载时刻执行特定类的操作。
2、动态加载和静态链接都能将 load 消息发送到类和分类,但前提是新加载类或分类实现了要响应的方法。
3、初始化的顺序如下:
链接的所有框架(Framework)中全部的构造器。
镜像(Image)中所有的 +load 方法。
镜像中所有的 C++ 静态构造器,以及 C/C++ 的 attribute(constructor) 函数。
框架中链接的所有构造器。
4、类的 +load 方法在其所有父类的 +load 方法调用之后调用。
5、分类的 +load 方法在其主类的 +load 方法调用之后调用。

了解到 +load 在运行时初始化加载镜像时就会被调用,使得可以有机会预先做很多事情。但正是因为其加载的时机非常靠前,如果在 +load 方法中做比较复杂且在主线程的操作,将会影响 App 启动时间,降低用户体验。所以APP启动优化可以尽量把放到这个方法的任务往后放,比如+(void)initialize中,他会在每个类初始化的时候调用一次。

runtime会在运行时调用工程中的类的+load方法,并且不需要类在代码中被显示import,所以有些第三方可拖进工程就会起作用,而不需要显示import。比如:IQKeyboad、NSObjectSafe...
从这里可以解开我之前的一个误解:一直以为垃圾代码(尤其是第三方库)留在工程里面不管他,以为它不会起作用,如果他里面实现了 +load 方法,还是可能会起作用的,一旦出bug了,那叫一个难找

接着,看看runtime的方法交换:

+ (void)load{
    NSLog(@"UIViewController Hook load");
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oriMethod = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc"));
        Method repMethod = class_getInstanceMethod([self class], @selector(xx_dealloc));
        method_exchangeImplementations(oriMethod, repMethod);
    });
}

- (void)xx_dealloc{
    NSLog(@"%@ dealloc",NSStringFromClass([self class]));
}

或者像这样(根据系统版本修改字号):

@implementation UIFont (hook)
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleClassMethod:@selector(systemFontOfSize:) withMethod:@selector(xx_systemFontOfSize:)];
    });
}
+ (UIFont *)xx_systemFontOfSize:(CGFloat)fontSize{
    NSString *version = [UIDevice currentDevice].systemVersion;
    if (version.doubleValue >= 11.0) {
        return [UIFont xx_systemFontOfSize:25.0f];
    } else {
        return [UIFont xx_systemFontOfSize:14.0f];
    }
}
@end

为什么xx_systemFontOfSize里面又调用了xx_systemFontOfSize?不会死循环么?
嗯,这里不会,因为交换了方法的IMP,这里不细说。

看NSObjectSafe源码,hook那么多不常见的类,为什么呢?比如__NSArray0、__NSArrayI、__NSSingleObjectArrayI等等。

这些都是NSArray在某些情况下的实际类,NSArray在这里使用了类簇的模式。

类簇是一种设计模式,它包含了一组私有的具体的类,这些类继承一个公开的抽象类,也即是基类,基类负责提供对外接口供调用者使用,具体类负责方法的真正实现, 我们只需要调用基类提供的接口来实现相关功能,而无需关心背后的具体实现细节。

在Cocoa中,许多类实际上是以类簇的方式实现的,即它们是一群隐藏在通用接口之下的与实现相关的类。例如创建NSString对象时,实际上获得的可能是NSLiteralString、NSCFString、NSSimpleCString、NSBallOfString或者其他未写入文档的与实现相关的对象。

NSNumber就是最常用的类簇实现的类之一。


NSNumber类簇

这种模式的好处就是,可以隐藏私有类,调用者只能使用基类暴露的API,而无需关心里面是什么类,以及背后的具体实现细节。即使将来添加更多的私有类,对外的基类API也没什么变化。

技术分享记录~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • 面向对象的三大特性:封装、继承、多态 OC内存管理 _strong 引用计数器来控制对象的生命周期。 _weak...
    运气不够技术凑阅读 1,093评论 0 10
  • 1.设计模式是什么? 你知道哪些设计模式,并简要叙述?设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型...
    龍飝阅读 2,142评论 0 12
  • 一、你在项目中用过 runtime 吗?举个例子。 a、Method Swizzling动态交换方法实现,实则交换...
    写代码的小农民阅读 1,367评论 0 4
  • 2019年2月9日 星期六 多云 又睡过头儿了,不知道为什么,一觉醒来就到了现在!当然,不排...
    佳依我心阅读 157评论 0 2