使用->访问实例对象成员变量Crash问题

前言

最近线上发生了一个Crash。根据栈信息定位到如下代码
1
2
EXC_BAD_ACCESS (SIGBUS)
Attempted to dereference garbage pointer 0x10. Originated at or in a subcall of WebViewJavascriptBridge_js
1
2
3
if (instance->ivar) {
...
}
就是访问成员变量时Crash。
难道instance可能为空,但是为空应该也不会Crash吧?因为在我的固有的思维里面,对nil发送消息并不会crash。
但是又仔细想了想,访问成员变量时,是通过当前类的实例对象偏移量再加上此变量在类中的偏移量获取的。与发送消息无关(不是属性,没有写get方法)。如果当前实例对象如果为空(obj的offet=0),为什么还会继续呢?百思不得解。
写了一个demo,使用clang命令转换为cpp文件后,才知道缘由。
大体代码如下:
1
2
3
4
5
6
7
8

//MyObject.h
@interface MyObject : NSObject {
@public
NSString *_strObject;
NSArray *_arrList;
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
//Test.h
@implementation Test

- (void)helloworld {
MyObject *obj = nil;
if (condition) {
obj = [MyObject new];
}
NSString *str = obj->_strObject;
NSLog(@"%@", str);
}

@end
1
clang -rewrite-objc Test.m
通过clang命令获取Test.cpp文件(10万行左右)
在后面看到如下片段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma clang assume_nonnull end
// @implementation Test


static void _I_Test_helloworld(Test * self, SEL _cmd) {
MyObject *obj = __null;
if (condition) {
obj = ((MyObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyObject"), sel_registerName("new"));
}
NSString *str = (*(NSString **)((char *)obj + OBJC_IVAR_$_MyObject$_strObject));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hs_pl2pwmlj27g9xt0wlb9mbdbh0000gn_T_Test_2546e4_mi_0, str);
}

// @end
主要下这行代码
1
NSString *str = (*(NSString **)((char *)obj + OBJC_IVAR_$_MyObject$_strObject));
这段代码就是获取_strObject偏移量,OBJC_IVAR_$_MyObject$_strObject是一个在编译器已经决定了的固定的偏移量。obj就是实例对象的首地址。通过2个值获取到_strObject地址。如果obj==nil;这个偏移地址就是一个固定的值0,加上变量固定偏移量,值就类似于0x10(这也和crash信息里相匹配)。

结论

通过->访问成员变量要判断当前变量是否为空。通过offset寻址成员变量时并不会多一层为空判断逻辑。