问题 Demo
崩溃原因
崩溃路径
验证方式
其它测试
问题 Demo在多线程下同时给全局变量赋值时会发生崩溃:
static NSObject *_instance;
- (void)foo {
_instance = [[NSObject alloc] init];
}
崩溃原因
如下为源码的汇编代码:
Demo-iOS`-[ViewController foo]:
0x104e4e088 <+0>: stp x29, x30, [sp, #-0x10]!
0x104e4e08c <+4>: mov x29, sp
# newValue = [[NSObject alloc] init]
0x104e4e094 <+12>: ldr x0, #0x7454 ; (void *)0x00000001db209e08: NSObject
0x104e4e098 <+16>: bl 0x104e4e438 ; symbol stub for: objc_alloc_init
# oldValue = _instance
0x104e4e09c <+20>: adrp x9, 7
0x104e4e0a0 <+24>: ldr x8, [x9, #0x788]
# _instance = newValue
0x104e4e0a4 <+28>: str x0, [x9, #0x788]
# objc_release(oldValue)
0x104e4e0a8 <+32>: mov x0, x8
0x104e4e0ac <+36>: ldp x29, x30, [sp], #0x10
0x104e4e0b0 <+40>: b 0x104e4e480 ; symbol stub for: objc_release
对汇编代码进行反汇编,可以看出 ARC 下编译器添加了读取旧值 oldValue = _instance 和释放旧值 objc_release(oldValue) 的操作:
- (void)foo {
NSObject *newValue = [[NSObject alloc] init];
NSObject *oldValue = _instance; //读取旧值
_instance = newValue;
objc_release(oldValue); //释放旧值
}
给全局变量赋值时会读取旧值、释放旧值,旧值是从全局变量读取的,多个线程可以同时读到同一个值,如果一个 线程 在访问旧值时,旧值被其它线程销毁,就会发生崩溃。
即使在代码中添加了判空逻辑,也会有可能多个线程同时进入 if (!_instance) 里,发生错误:
static NSObject *_instance;
- (void)foo {
if (!_instance) {
_instance = [[NSObject alloc] init];
}
}
即使不崩溃,多个线程也会产生不同的实例,是不符合预期的
崩溃路径可以推断出一种复现崩溃的办法:
A B C 线程同时进入 - (NSObject *)foo
方法
A 线程先创建 NSObject 实例,赋值给 _instance (_instance = newValue),_instance 引用计数为 1
B、C 线程再开始执行,执行到 oldValue = _instance 时,会从 _instance 全局变量中读到 A 线程创建的对象,赋值给各自的 oldValue,oldValue 引用计数为 1
B 线程在 objc_release(oldValue) 后会释放 oldValue,oldValue 引用计数为 0,oldValue 被销毁
C 线程在 objc_release(oldValue) 时访问 oldValue,发生崩溃
验证方式lldb 的 thread continue 指令可以控制仅一个线程执行,其它线程保持挂起。
利用该指令,可以复现崩溃路径,按下面步骤可以验证:
准备三个线程执行 [self foo]
,并在 -foo
方法里面打上断点:
可以多次测试让 3 个 线程 同时进入断点,进入断点后可以看到 Thread 2、3、4 是创建的 3 个线程:
不加 asm("nop\n") 的话执行完 objc_release(oldValue) 后,foo 函数会直接结束,不太方便在 objc_release(oldValue) 之后打断点进行调试,添加之后 objc_release 之后会有位置打断点(第 4 5 步用到)
在 Thread 2 中给汇编代码第 10 行打断点,并执行 thread contine
,使 Thread 2 执行完 _instance = newValue :
可以看到 Thread 2 创建的实例为 0x0000000280df8020
使 Thread 3、4 线程 执行完 oldValue = _instance
步骤1:删除断点(每次切换线程都要删掉断点,不然 Xcode 可能会有 bug ...),切换到 Thread 3 ,给第 9 行打断点,并执行 thread continue:
在 Xcode Debug Navitor 中选择线程堆栈可以切换线程
或者使用 lldb,thread select 3
切换线程
步骤2:删除断点,切换到 Thread 4,给第 9 行打断点,并执行 thread continue
:
可以发现 Thread 3、4 读到的旧值都是 Thread 2 创建的 0x0000000280df8020
使Thread 3 执行完 objc_release(oldValue)
步骤:删除断点,切换到 Thread 3,给第 12 行打断点,并执行 thread continue
:
此时 oldValue 引用计数为 0,被销毁
使 Thread 4 执行 objc_release(oldValue), 访问 oldValue
步骤:删除断点,切换到 Thread 4,给第 12 行打断点,并执行 thread continue
:
在 Thread 3 执行 objc_release(oldValue) 后 oldValue 就已经被销毁了,
Thread 4 再次访问时会发生崩溃
其它测试对成员变量赋值时同样有这个问题
@property (nonatomic, strong) NSObject *obj;
- (NSObject *)getInstance {
_obj = [[NSObject alloc] init];
return _obj;
}
局部变量不会有这个问题
局部变量不涉及"将旧值释放"这个操作。
以上就是iOS开发多线程下全局变量赋值崩溃原理详解的详细内容,更多关于多线程全局变量赋值崩溃的资料请关注易知道(ezd.cc)其它相关文章!