Nil Crash
这事还得从一个crash 讲起。。。
在开发iOS App的时候,很多时候会遇到下面这种场景:1
2
3
4
5
6
7
8//从数据库读取
NSString* item = [NSString stringWithUTF8String:sqlite3_column_text(statement, idx)];
//长长的逻辑
...
NSMutableArray* items = [NSMutableArray array];
[items addObject:item]
上面的代码存在两个地方可能引发crash,没办法,项目进度紧张,先保护一下再说:
1 | //从数据库读取 |
但很快你就会发现,如果项目中要加保护的地方数不胜数,而且很容易在开发阶段把问题隐藏起来。
但不加的话,如果修复不及时,很容易把crash带到线上版本,严重影响产品的质量。
这个时候最好的办法就是Hook掉 addObject:和 stringWithUTF8String:这两个函数,
然后在Debug模式下Assert,Release模式就打Log 帮助定位。
Objective-c Runtime Swizzle
Objective-c Swizzle 是 AOP (面向切面编程) 在iOS开发中一个典型的应用,有兴趣的同学可以先读读
Method Swizzling 和 AOP 实践 这篇文章。
读完记得回来啊!!!我还没讲完呢囧
言归正传,普通类的swizzle 其实很简单,直接上代码
1 | - (void)swizzleClass:(Class)classs method:(SEL)origSelector withMethod:(SEL)newSelector |
至于这里为什么要先调用 class_addMethod,Method Swizzling 和 AOP 实践 这篇文章也有提到,其实就是为了防止swizzle掉父类的方法导致后面的swizzle出现隐藏的bug。(有可能子类A 替换到方法func:,然后子类B 又把func替换回来)
那直接用上面的函数对NSArray进行swizzle,会怎么样呢?
1 | NSMutableArray* items = [NSMutableArray array]; |
尼玛!还是crash!而且并没有进到我们的safeAddObject:方法里面。
这又是为什么?
别慌别慌,那是因为NSMutableArray内部实现不同于普通的类,实际上apple官方文档有提到过,包括NSNumber, NSArray, NSDictionary, NSSet等这一类容器都属于类簇,[NSMutableArray array] 生成的对象的class并不是 NSMutableArray,而是 __NSArrayM 这样的私有类,而 addObject:
方法是在 __NSArrayM 这个子类里实现的。
问题就出在这里:
按照前面 swizzClass:
的实现, hook的时候我们会先检查 NSMutableArray 这个类有没有addObject:
方法,如果没有则添加进去,很显然这里会对NSMutableArray添加addObject:
,同时addObject:
和safeAddObject:
交换函数实现。这个时候如果用[NSMutableArray array] 生成的对象去调用addObject:
,首先还是调用到子类__NSArrayM 的addObject:
方法,因此不会进到父类NSMutableArray,我们的safeAddObject:
也就没有机会被调用到。
说了这么多,很明显想要正确的hook到类簇NSArray这样的容器类方法,要先找到它隐藏的子类才行。
神马? 还没搞明白 类簇
是个什么鬼?
Class Cluster(类簇)
类簇 是什么鬼?我们先看看这两篇文章再说:
Class Cluster
从NSArray看类簇
大概意思就是说:
类似 NSNumber, NSArray, NSDictionary这样一类的Foundation自带的容器类,实际上是类似c++虚基类的东东,它把NSArray的具体实现隐藏在__NSArrayI, __NSArrayM这样的私有子类中,然后对api的使用者只暴露出简单的接口。
one abstract public class declares the interface for multiple private subclasses
下面我们通过一张图看看NSArray,NSMutableArray的继承关系:
在实际编码中,调用不同的API生成的对象会对应不同的子类:
1 | [NSArray alloc]; //--> __PlaceholderArray |
每次hook的时候都要去查代对象的方法到底是属于子类还是父类太麻烦了,有没有更方便的办法?
其实在hook的时候遵守 谁调用hook谁 的原则,直接生成相应的对象,再调用swizzClass:
就可以了。
hook容器类对象一般可分为下面三种类型:
- 普通方法
1 | /** |
- init方法
1 | /** |
- 类方法
1 | /* 这里为什么不用NSArray 的实例对象呢? */ |
这里重点说一下如何hook静态方法:
我们知道Objective-c的每个类都有元类,而类的静态方法就是放在这个元类里面,子类的元类继承自父类的元类。
下面这张图描述了类、元类间的继承关系(又要无耻地进行盗图了-,-):
这里遵守的原则也一样,都是要首先找到静态方法所在的元类,然后再调用swizzClass
。
实验发现,像NSArray这样的类簇,它的静态方法基本都是放在最顶层的NSArray的元类里,直接取 [NSArray class] 就可以swizzle得到。
NSObjectSafe
针对上面那些场景,已经整理出一个简单开源框架NSObjectSafe,有需要的童鞋可移步前往。
这里贴出其中swizzle的代码:
1 | + (void)swizzleClassMethod:(SEL)origSelector withMethod:(SEL)newSelector |
Desciption
Swizzle commonly used function of Foundation container to prevent nil crash
Assert when in Debug and log when in Release
Usage:
Involve NSObjectSafe.h/NSObjectSafe.m as build phases