标签:常量池 ice 内容 tst Opens zhang nbsp product 调试
阶段1
事情的起因是同事写了这样一段代码。
1 2 3
| @synchronized(@"test synchronized"){ NSLog(@"do something"); }
|
于是我指出这样应该是锁不住的,因为 synchronized 锁的是对象,而每次创建的字符串都是新对象,所以锁不住。
同事跟我说,“no,no,no”,你太天真了,编译器会优化字符串,像这种写在代码里的字符串,会被放在ios包的常量字符串里,终生只有一个地址。还给我祭出了ipa包内容截图。
![技术图片](https://zgzczzw-blog-image.oss-cn-beijing.aliyuncs.com/synchronized%E7%8C%8E%E5%A5%87/20180719101458.png)
于是我自己写了段测试代码
1 2 3 4 5 6 7 8 9 10 11
| dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @synchronized(@"test synchronized"){ [NSThread sleepForTimeInterval:3]; NSLog(@"1"); } }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @synchronized(@"test synchronized"){ NSLog(@"2"); } });
|
1 2
| 2018-07-19 10:19:43.029043+0800 TestJsPatch[4988:1322177] 1 2018-07-19 10:19:43.029133+0800 TestJsPatch[4988:1322179] 2
|
看来真的是这样。
阶段2
针对上面的问题,我想着写死在代码里的纯字符串会被编译器优化,那如果新创建的 NSString 对象,是不是就锁不住了呢。于是我测试了下面的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| NSString *string1 = [[NSString alloc] initWithString:@"test synchronized"]; NSString *string2 = [[NSString alloc] initWithString:@"test synchronized"]; NSLog(@"%p", &string1); NSLog(@"%p", &string2); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @synchronized(string1){ [NSThread sleepForTimeInterval:3]; NSLog(@"1"); } }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @synchronized(string2){ NSLog(@"2"); } });
|
1 2 3 4
| 2018-07-19 10:22:03.083801+0800 TestJsPatch[4994:1323439] 0x16f2b9868 2018-07-19 10:22:03.083857+0800 TestJsPatch[4994:1323439] 0x16f2b9860 2018-07-19 10:22:06.089249+0800 TestJsPatch[4994:1323509] 1 2018-07-19 10:22:06.089360+0800 TestJsPatch[4994:1323510] 2
|
神奇的事情发生了,string1 和 string2 的地址明显是不一样的,为什么还是能锁住呢。有了阶段1的经验,在这里,有一个猜想是,虽然 string1 和 string2 对象的地址不一样,但是他们指向的内容地址是一样的,还是 “test synchronized” 的地址。
后来又加了两句这样的打印。
1 2 3 4 5
| NSLog(@"%p", string1); NSLog(@"%p", string2); 2018-07-19 10:25:24.797033+0800 TestJsPatch[5000:1325160] 0x102b30a80 2018-07-19 10:25:24.797042+0800 TestJsPatch[5000:1325160] 0x102b30a80
|
发现他们指向的内容地址果然是一样的。那么这里就存在两个问题
- 这个指向的内容的地址是否就是 “test synchronized” 常量的地址呢
- synchronized 锁的是内容地址而非对象地址,这个可否从代码里找到根据
问题1
接下来我们去看 “test synchronized” 常量的地址是什么呢,通过Hopper可以看到
![技术图片](https://zgzczzw-blog-image.oss-cn-beijing.aliyuncs.com/synchronized%E7%8C%8E%E5%A5%87/20180719103012.png)
字符串在包里的地址是
再看看程序打印出来string1和string2的地址
不一样,有点纳闷。这时候又请教了组里的一位大神,大神给解释说,ios程序的安装就好像是把安装包的内容搬到了内存里。安装包里的地址和内存里的地址肯定是不一样的,但他们相对于起始位置的偏移应该是一样的。于是下面开始找安装包和内存各自的起始位置。
安装包的起始位置也可以从Hopper中看到,在Hopper中将位置拉到安装包的起始处,可以看到如下地址。
![技术图片](https://zgzczzw-blog-image.oss-cn-beijing.aliyuncs.com/synchronized%E7%8C%8E%E5%A5%87/20180719103511.png)
可以看到安装包的起始位置是。
那内存的起始位置怎么看到,可以在Xcode中使用命令image list
能列出整个程序image的内容。
1 2 3 4 5 6 7
| (lldb) image list [ 0] 294BD955-9C66-3433-AFBC-DA4A79560B66 0x0000000102b0c000 /tmp/xcode/TestJsPatch-cvdefnzlhcjogbdlyqafsspkkxzw/Build/Products/Release-iphoneos/TestJsPatch.app/TestJsPatch /tmp/xcode/TestJsPatch-cvdefnzlhcjogbdlyqafsspkkxzw/Build/Products/Release-iphoneos/TestJsPatch.app.dSYM/Contents/Resources/DWARF/TestJsPatch [ 1] B15E536A-7107-32DA-BFAF-ECE44C5685E4 0x0000000102ccc000 /Users/eric.zhang/Library/Developer/Xcode/iOS DeviceSupport/11.4 (15F79)/Symbols/usr/lib/dyld [ 2] BBB23B9E-FD65-3AB5-B873-85910ABE5B95 0x00000001929a7000 /Users/eric.zhang/Library/Developer/Xcode/iOS DeviceSupport/11.4 (15F79)/Symbols/System/Library/Frameworks/Photos.framework/Photos [ 3] CC396CA7-A9D1-33D4-898E-573CC46EC982 0x0000000183983000 /Users/eric.zhang/Library/Developer/Xcode/iOS DeviceSupport/11.4 (15F79)/Symbols/usr/lib/libz.1.dylib [ 4] E53F9393-BFC8-3EF5-8520-B0FE6B193183 0x000000018440d000 /Users/eric.zhang/Library/Developer/Xcode/iOS DeviceSupport/11.4 (15F79)/Symbols/System/Library/Frameworks/Foundation.framework/Foundation
|
可以看出起始位置是
那么我看减一下,看看偏移是否一致呢
1
| 0x102b30a80 - 0x0000000102b0c000 = 0000000100024a80 - 0000000100000000
|
可以看到,是一样的,也就是说,即使是用静态字符串初始化的NSString,他们指向的内容依然是一样的。
问题2
对于问题2,synchronized 锁的是内容地址而非对象地址,这个可否从代码里找到根据。就需要去翻阅ios的代码了,首先我们需要搞清楚@synchronized这个语法糖,到底调用的是什么方法,从Xcode中打开Debug -> Debug Workflow -> Always Show Disassembly,在断点调试的时候可以可以看到汇编代码。
可以看到@synchronized编成汇编后如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| -> 0x10067a838 <+12>: ldr x0, [x0, #0x20] 0x10067a83c <+16>: bl 0x1006941b0 ; symbol stub for: objc_retain 0x10067a840 <+20>: mov x19, x0 0x10067a844 <+24>: bl 0x100694210 ; symbol stub for: objc_sync_enter 0x10067a848 <+28>: nop 0x10067a84c <+32>: ldr x0, #0x2118c ; (void *)0x00000001b6025858: NSThread 0x10067a850 <+36>: nop 0x10067a854 <+40>: ldr x1, #0x20ae4 ; "sleepForTimeInterval:" 0x10067a858 <+44>: fmov d0, #3.00000000 0x10067a85c <+48>: bl 0x100694174 ; symbol stub for: objc_msgSend 0x10067a860 <+52>: adr x0, #0x1e280 ; @"'1'" 0x10067a864 <+56>: nop 0x10067a868 <+60>: bl 0x100693f40 ; symbol stub for: NSLog 0x10067a86c <+64>: mov x0, x19 0x10067a870 <+68>: bl 0x10069421c ; symbol stub for: objc_sync_exit 0x10067a874 <+72>: mov x0, x19 0x10067a878 <+76>: ldp x29, x30, [sp, #0x10] 0x10067a87c <+80>: ldp x20, x19, [sp], #0x20 0x10067a880 <+84>: b 0x1006941a4 ; symbol stub for: objc_release 0x10067a884 <+88>: mov x20, x0 0x10067a888 <+92>: mov x0, x19 0x10067a88c <+96>: bl 0x10069421c ; symbol stub for: objc_sync_exit 0x10067a890 <+100>: mov x0, x20 0x10067a894 <+104>: bl 0x100693fd0 ; symbol stub for: _Unwind_Resume
|
@synchronized对应的代码就是objc_sync_enter和objc_sync_exit,接下来我们去ios runtime的源码里找对应的实现,源码可以从https://opensource.apple.com/source/objc4/中下载,下载之后搜索objc_sync_enter,代码是在objc-sync.mm中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int objc_sync_enter(id obj) { int result = OBJC_SYNC_SUCCESS; if (obj) { SyncData* data = id2data(obj, ACQUIRE); assert(data); data->mutex.lock(); } else { // @synchronized(nil) does nothing if (DebugNilSync) { _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug"); } objc_sync_nil(); } return result; }
|
这个方法其实比较简单,通过 id2data 方法返回一个 SyncData 对象,然后调用 SyncData的 mutex 锁,如果传进来的 obj 是 nil 的话,这个锁就没有效果。看来重点在id2data 方法中。
id2data 主要是生成一个 SyncData 对象,关于 id2data 仿佛,这篇文章解释的很清楚剖析@synchronizd底层实现原理。简单来说,就是两层cache机制,能保证synchronized对同一个对象只会锁一次,并且还能适当加快效率。
其实对于问题2,我们只要看生成的 SyncData 存的是什么东西就行了。
1 2 3 4
| result = (SyncData*)calloc(sizeof(SyncData), 1); result->object = (objc_object *)object; result->threadCount = 1; new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
|
从以上代码就可以看出,SyncData 存的是 object 指向的地址,而非 object 本地的地址。
阶段3
后来我又好奇了,在阶段2是一层对象,指针指向常量池里的字符串,那如果我用两层对象呢,比如如下这。
1 2 3 4 5 6 7 8 9 10 11 12
| NSString *string1 = [[NSString alloc] initWithString:@"test synchronized"]; NSString *string2 = [[NSString alloc] initWithString:@"test synchronized"]; NSLog(@"%p", &string1); NSLog(@"%p", &string2); NSLog(@"%p", string1); NSLog(@"%p", string2); NSString *string3 = [[NSString alloc] initWithString:string1]; NSString *string4 = [[NSString alloc] initWithString:string2]; NSLog(@"%p", &string3); NSLog(@"%p", &string4); NSLog(@"%p", string3); NSLog(@"%p", string4);
|
string3 和 string4 分别指向了 string1 和 string2,然后又指向了常量字符串,打印出的内容如下。
1 2 3 4 5 6 7 8
| 2018-07-19 10:46:38.288084+0800 TestJsPatch[5030:1333710] 0x16d79d868 2018-07-19 10:46:38.288128+0800 TestJsPatch[5030:1333710] 0x16d79d860 2018-07-19 10:46:38.288135+0800 TestJsPatch[5030:1333710] 0x102684a80 2018-07-19 10:46:40.813209+0800 TestJsPatch[5030:1333710] 0x102684a80 2018-07-19 10:46:40.813298+0800 TestJsPatch[5030:1333710] 0x16d79d858 2018-07-19 10:46:40.813309+0800 TestJsPatch[5030:1333710] 0x16d79d850 2018-07-19 10:46:40.813318+0800 TestJsPatch[5030:1333710] 0x102684a80 2018-07-19 10:46:40.813326+0800 TestJsPatch[5030:1333710] 0x102684a80
|
如下可以看出,虽然经过了两层指针转换,但他们指向的内容地址依然一样,所以对 synchronized 的效果也是一样的。
阶段4
上面的例子都是用常量字符串直接初始化 NSString,所以可能编译器有优化,那么如果我用 initWithFormat 来初始化会怎么样呢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| NSString *string3 = [[NSString alloc] initWithFormat:@"%@", @"test synchronized"]; NSString *string4 = [[NSString alloc] initWithFormat:@"%@", @"test synchronized"]; NSLog(@"%p", &string3); NSLog(@"%p", &string4); NSLog(@"%p", string3); NSLog(@"%p", string4); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @synchronized(string3){ [NSThread sleepForTimeInterval:3]; NSLog(@"1"); } }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @synchronized(string4){ NSLog(@"2"); } });
|
1 2 3 4 5 6
| 2018-07-19 10:53:34.314633+0800 TestJsPatch[5048:1338079] 0x16ba39858 2018-07-19 10:53:34.314677+0800 TestJsPatch[5048:1338079] 0x16ba39850 2018-07-19 10:53:34.314684+0800 TestJsPatch[5048:1338079] 0x1c4453980 2018-07-19 10:53:34.314691+0800 TestJsPatch[5048:1338079] 0x1c44539e0 2018-07-19 10:53:34.315006+0800 TestJsPatch[5048:1338159] 2 2018-07-19 10:53:37.319756+0800 TestJsPatch[5048:1338155] 1
|
可以看出 initWithFormat 并没有进行常量字符串的优化,而是新创建了一个对象。 @synchronized 也就失效了。
结论
常量字符串在编译时会被放在常量池里,也就是 Section __cfstring
中,如果是用 initString 方式初始化 NSString,则NSString的内容还是指向这块地址的。但是如果用 initWithFormat 的方式初始化 NSString,则会创建一个新的对象。所以在日常使用中,如果用常量字符串初始化 NSString,应该优先考虑 initString 方法。同时也应该注意 @synchronized 的使用范围。
原文:大专栏 synchronized猎奇
synchronized猎奇
标签:常量池 ice 内容 tst Opens zhang nbsp product 调试
原文地址:https://www.cnblogs.com/petewell/p/11601774.html