Swizzle Foundation容器的正确姿势

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
2
3
4
5
6
7
8
9
10
11
12
13
14
//从数据库读取
NSString* item;
const char* name = sqlite3_column_text(statement, idx);
if (name != NULL){
item = [NSString stringWithUTF8String:name];
}

//长长的逻辑
...

NSMutableArray* items = [NSMutableArray array];
if (item.length){
[items addObject:item]
}

但很快你就会发现,如果项目中要加保护的地方数不胜数,而且很容易在开发阶段把问题隐藏起来。
但不加的话,如果修复不及时,很容易把crash带到线上版本,严重影响产品的质量。
这个时候最好的办法就是Hook掉 addObject:和 stringWithUTF8String:这两个函数,
然后在Debug模式下Assert,Release模式就打Log 帮助定位。

Objective-c Runtime Swizzle

Objective-c Swizzle 是 AOP (面向切面编程) 在iOS开发中一个典型的应用,有兴趣的同学可以先读读
Method Swizzling 和 AOP 实践 这篇文章。
读完记得回来啊!!!我还没讲完呢囧
言归正传,普通类的swizzle 其实很简单,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)swizzleClass:(Class)classs method:(SEL)origSelector withMethod:(SEL)newSelector
{
Method originalMethod = class_getInstanceMethod(class, origSelector);
Method swizzledMethod = class_getInstanceMethod(class, newSelector);

if (class_addMethod(class,
origSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod)) ) {
/*swizzing super class instance method, added if not exist */
class_replaceMethod(class,
newSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));

} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- (void) safeAddObject:(id)anObject {
if (anObject) {
[self safeAddObject:anObject];//调用系统的addObject:
} else {
NSAssert1(NO, @"NSMutableArray invalid args safeAddObject:[%@]", anObject);
}
}

至于这里为什么要先调用 class_addMethod,Method Swizzling 和 AOP 实践 这篇文章也有提到,其实就是为了防止swizzle掉父类的方法导致后面的swizzle出现隐藏的bug。(有可能子类A 替换到方法func:,然后子类B 又把func替换回来)
那直接用上面的函数对NSArray进行swizzle,会怎么样呢?

1
2
3
4
5
6
NSMutableArray* items = [NSMutableArray array];

[items swizzleInstance:[NSMutableArray class] method:@selector(addObject:)
withMethod:@selector(safeAddObject:)];

[items addObject:nil];

尼玛!还是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:,首先还是调用到子类__NSArrayMaddObject: 方法,因此不会进到父类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的继承关系:

class-cluster

在实际编码中,调用不同的API生成的对象会对应不同的子类:

1
2
3
4
5
6
7
8
9
[NSArray alloc]; //--> __PlaceholderArray

[[NSArray alloc] init]; //--> __NSArray0 这里比较特殊,空的NSArray其实没有实际意义,因为没有数据也不能被骗修改

[[NSArray alloc] initWithObject:@0]; //--> __NSArrayI

[NSMutableArray alloc]; //--> __PlaceholderArray

[[NSMutableArray alloc] init];//--> __NSArrayM

每次hook的时候都要去查代对象的方法到底是属于子类还是父类太麻烦了,有没有更方便的办法?
其实在hook的时候遵守 谁调用hook谁 的原则,直接生成相应的对象,再调用swizzClass:就可以了。
hook容器类对象一般可分为下面三种类型:

  • 普通方法
1
2
3
4
5
6
7
8
9
10
11
12
13
/** 
* 数组有内容obj类型才是__NSArrayI,没内容类型是__NSArray0
* objectAtIndex:在__NSArray0,__NSArrayI 和 __NSArrayM都有实现
* 所以都要进行swizzle
*/
NSArray* obj = [[NSArray alloc] init];
[obj swizzleInstanceMethod:@selector(objectAtIndex:) withMethod:@selector(safeObjectAtIndex0:)];

obj = [[NSArray alloc] initWithObjects:@0, nil];
[obj swizzleInstanceMethod:@selector(objectAtIndex:) withMethod:@selector(safeObjectAtIndex:)];

NSMutableArray* obj = [[NSMutableArray alloc] init];
[obj swizzleInstanceMethod:@selector(objectAtIndex:) withMethod:@selector(safeObjectAtIndex:)];
  • init方法
1
2
3
4
5
/**
* init方法要取到NSPlaceholderString对象
*/
NSString* obj = [NSString alloc];//NSPlaceholderString
[obj swizzleInstanceMethod:@selector(initWithCString:encoding:) withMethod:@selector(safeInitWithCString:encoding:)];
  • 类方法
1
2
/* 这里为什么不用NSArray 的实例对象呢? */
[NSArray swizzleClassMethod:@selector(arrayWithObject:) withMethod:@selector(safeArrayWithObject:)];

这里重点说一下如何hook静态方法:
我们知道Objective-c的每个类都有元类,而类的静态方法就是放在这个元类里面,子类的元类继承自父类的元类。
下面这张图描述了类、元类间的继承关系(又要无耻地进行盗图了-,-):

class-diagram

这里遵守的原则也一样,都是要首先找到静态方法所在的元类,然后再调用swizzClass
实验发现,像NSArray这样的类簇,它的静态方法基本都是放在最顶层的NSArray的元类里,直接取 [NSArray class] 就可以swizzle得到。

NSObjectSafe

针对上面那些场景,已经整理出一个简单开源框架NSObjectSafe,有需要的童鞋可移步前往。
这里贴出其中swizzle的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
+ (void)swizzleClassMethod:(SEL)origSelector withMethod:(SEL)newSelector
{
Class class = [self class];

Method originalMethod = class_getClassMethod(class, origSelector);
Method swizzledMethod = class_getClassMethod(class, newSelector);

Class metacls = objc_getMetaClass(NSStringFromClass(class).UTF8String);
if (class_addMethod(metacls,
origSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod)) ) {
/* swizzing super class method, added if not exist */
class_replaceMethod(metacls,
newSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));

} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- (void)swizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector
{
Class class = [self class];

Method originalMethod = class_getInstanceMethod(class, origSelector);
Method swizzledMethod = class_getInstanceMethod(class, newSelector);

if (class_addMethod(class,
origSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod)) ) {
/*swizzing super class instance method, added if not exist */
class_replaceMethod(class,
newSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));

} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

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