iOS优化实战

这是一个很宏大的课题,有UI卡顿的优化,网络请求的优化,降低Crash概率的优化,技术方案的优化等等。本文将会重点关注降低Crash概率的优化。

一、知己知彼,百战不殆

首先我们来了解一下Crash,Crash的原因有很多种,不同的技术所导致的Crash也会不同。

1、内存非法地址访问,常说的野指针,段错误

ObjC不是强类型的,在强制类型转换或者强制写内存等操作时,很容易Crash。

2、访问了不存在的方法

ObjC的消息传递机制会在无法解读消息时抛出异常,并让程序Crash。

3、访问数组等对象越界或插入了空对象

一个固定数组有一块连续内存,数组指针指向内存首地址,靠下标来计算元素地址,如果下标越界则指针偏移出这块内存,会访问到野数据,ObjC 为了安全就直接让程序 Crash 了。

4、循环引用导致内存泄漏

ObjC使用的内存管理机制ARC(自动引用计数),当对象的引用计数为0,执行RunLoop时会自动回收其内存,但是如果出现对象循环引用,引用计数无法减为0,则出现了内存泄漏。

既然已经知道了原因,该如何进行优化呢?

二、工欲善其事必先利其器

我们再来了解一下ObjC的基础知识。

1、ARC(Automatic Reference Counting)

其实在ObjC中内存的管理是依赖对象引用计数器来进行的:在ObjC中每个对象内部都有一个与之对应的整数(retainCount),叫“引用计数器”,当一个对象在创建之后它的引用计数器为1,当调用这个对象的alloc、retain、new、copy方法之后引用计数器自动在原来的基础上加1(ObjC中调用一个对象的方法就是给这个对象发送一个消息),当调用这个对象的release方法之后它的引用计数器减1,如果一个对象的引用计数器为0,则系统会自动调用这个对象的dealloc方法来销毁这个对象。遵循谁创建,谁释放原则。

在ObjC中没有GC机制,但提供了一种半自动化的机制(ARC),在程序编译阶段编译器会自动为我们添加retain,release,处于autoreleaespool中的对象都会自动release一次。当弱引用对象被释放时,运行时自动将其置为nil

2、消息传递

众所周知,ObjC是从C发展而来的一门面向对象开发语言,不同于C++的静态性,ObjC是真正意义上的动态语言(虽然C++也能通过virtual来实现有限的动态性)。观察objc_class的定义,如下:

struct objc_class {
    Class isa;
  
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif
  
} OBJC2_UNAVAILABLE;

这里的isa指向一个“类对象”(类的实例是一个对象,类本身也是对象,此时isa指向其元类,不单单表示一个数据类型)。

对象的类不仅描述了对象的数据:对象占用的内存大小、成员变量的类型和布局等,而且也描述了对象的行为:对象能够响应的消息、实现的实例方法等。因此,当我们调用实例方法

 [receiver message]

给一个对象发送消息时,这个对象能否响应这个消息就需要通过 isa
找到它所属的类,然后遍历methodLists查找字符串匹配的实例方法,再通过super_class遍历继承树继续查找字符串匹配的实例方法,直至超级父类NSObject,若还是无法找到匹配的方法,则最终会执行

// 该方法会抛出异常,并abort程序
- (void)doesNotRecognizeSelector:(SEL)aSelector

当我们调用类方法,比如[NSObject new],给类对象发送消息。同样的,类对象能否响应这个消息也要通过 isa 找到类对象所属的类(元类)才能知道。也就是说,实例方法是保存在类中的,而类方法是保存在元类中的
说了这么多,大家可能已经有点绕迷糊了,下面我们看一张图,一切自会明了。

object_model.png

3、消息转发

ObjC的消息转发机制分为两大阶段。第一阶段先征询接收对象所属的类,看其能否动态添加方法。第二阶段运行时系统会请求接收对象看看有没有其他对象能处理这条消息,若有则会转发给那个对象继续消息传递;若没有则会启动完整的消息转发机制。
1)、动态方法解析
对象在收到无法解读的消息后,首先会调用其类方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel

动态的为对象添加新方法,前提是相关方法的实现代码已经写好,只等着运行时调用即可。
2)、备援接收者
运行时会征询当前接收对象,能不能转给其他接收者来处理,与之相对应的处理方法如下:

- (id)forwardingTargetForSelector:(SEL)aSelector

若可以转给其他对象,则执行其他对象的消息传递机制
3)、完整的消息转发
转发算法来到这一步,首先会把消息有关的全部细节都封装在NSInvocation对象中,并调用下列方法来转发消息:

- (void)forwardingInvocation:(NSInvocation *)invocation;

如果还不能处理消息,同消息传递一样程序很自然地会抛异常,abort。整个消息转发流程如下图:

message.png

4、Runtime

基于ObjC的对象模型,消息传递、转发机制,Apple提供了一系列底层可操作它们的API。比如methodLists本质上是一个链表,使用下列API即可动态地控制消息的实现。

// 添加实例方法
BOOL class_addMethod(Class cls, SEL name, IMP imp,  const char *types);
// 替换实例方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
// 交换实例方法
void method_exchangeImplementations(Method m1, Method m2);
// 获取类方法
Method class_getClassMethod(Class cls, SEL name);
// 获取实例方法
IMP class_getMethodImplementation(Class cls, SEL name);
......

所有的ObjC代码都会被转化成runtime的C代码执行,例如[receiver message];会被转化成objc_msgSend(target, @selector(doSomething));我们可以把@selector(doSomething)替换成任意指定的方法实现,从而达到Hook(钩子)的目的。这就是大名鼎鼎"Method Swizzling 黑魔法"的基本原理。

5、RunLoop

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。对于iOS之类的GUI(图形用户界面系统)需要一个机制,让线程能随时处理各种事件但并不退出,通常的代码逻辑是这样的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

我们常说的程序启动了,某种意义上来说可理解成RunLoop运行起来了。一旦该RunLoop结束了(很多异常Crash都会导致RunLoop停止运行),程序也就终止了。

三、分而治之,各个击破

1、内存非法地址访问,常说的野指针,段错误

解决方案:遵循良好的编码规范,变量使用前要判断非空,释放后要置空。类型不确定时,尽量进行类型判断,少用类型强制转换。必要位置可使用@try @catch捕捉异常。

2、访问了不存在的方法

解决方案:
(1)在消息传递,转发关键位置指定相应的异常处理逻辑(或异常处理对象)。
(2)使用Method Swizzling去Hook- (void)doesNotRecognizeSelector:(SEL)aSelector ,添加异常处理逻辑。

3、访问数组对象越界或插入了空对象

解决方案:
(1)访问数组前判长度,插入数组前判空对象
(2)使用Method Swizzling去Hook- (id)objectAtIndex:(NSUInteger)index 等方法,添加异常处理逻辑。

4、循环引用导致内存泄漏

解决方案:
(1)搭配使用libextobjc中的@weakify@strongify宏来避免循环引用

@weakify(self);
[self.context performBlock:^{
    @strongify(self);
    [self doSomething];
}];

(2)使用Method Swizzling去Hook- (void)dealloc; 方法,添加检测内存泄漏逻辑,代码如下:

- (void)custom_dealloc {
    [self custom_dealloc];
    @weakify(self);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        @strongify(self);
        if ( self != nil ) { // 利用ARC下weak变量会自动置为nil的特性
            NSLog(@"%@ leaked!", NSStringFromClass(self.class));
        }
    });
}
5、让程序回光返照

当程序因Crash导致RunLoop终止,我们截获相应的异常处理,同时再次重启当前所有的RunLoop,让程序回光返照继续运行。

至此我们差不多已经解决了绝大多数的Crash,当然Crash率也会如期而降。

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    SOI阅读 21,776评论 3 63
  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    made_China阅读 1,200评论 0 7
  • 428里除了我当然还有庆庆是正常人以外 其余四个都是长期关在污人院的一群傻子 说她们是傻子已经很客气...
    1123gx阅读 231评论 0 0
  • 近来中年危机成了炙手可热的电影题材,如《港囧》《夏洛特烦恼》这类电影,很不幸的是我竟然都看了,好在悬崖勒马表...
    钟白庸阅读 1,159评论 0 9