这是一个很宏大的课题,有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 找到类对象所属的类(元类)才能知道。也就是说,实例方法是保存在类中的,而类方法是保存在元类中的。
说了这么多,大家可能已经有点绕迷糊了,下面我们看一张图,一切自会明了。
3、消息转发
ObjC的消息转发机制分为两大阶段。第一阶段先征询接收对象所属的类,看其能否动态添加方法。第二阶段运行时系统会请求接收对象看看有没有其他对象能处理这条消息,若有则会转发给那个对象继续消息传递;若没有则会启动完整的消息转发机制。
1)、动态方法解析
对象在收到无法解读的消息后,首先会调用其类方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel
动态的为对象添加新方法,前提是相关方法的实现代码已经写好,只等着运行时调用即可。
2)、备援接收者
运行时会征询当前接收对象,能不能转给其他接收者来处理,与之相对应的处理方法如下:
- (id)forwardingTargetForSelector:(SEL)aSelector
若可以转给其他对象,则执行其他对象的消息传递机制
3)、完整的消息转发
转发算法来到这一步,首先会把消息有关的全部细节都封装在NSInvocation
对象中,并调用下列方法来转发消息:
- (void)forwardingInvocation:(NSInvocation *)invocation;
如果还不能处理消息,同消息传递一样程序很自然地会抛异常,abort。整个消息转发流程如下图:
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率也会如期而降。
网友评论