synchronized獵奇
階段1
事情的起因是同事寫了這樣一段程式碼。
@synchronized(@"test synchronized"){
NSLog(@"do something");
}
於是我指出這樣應該是鎖不住的,因為 synchronized 鎖的是物件,而每次建立的字串都是新物件,所以鎖不住。
同事跟我說,“no,no,no”,你太天真了,編譯器會優化字串,像這種寫在程式碼裡的字串,會被放在ios包的常量字串裡,終生只有一個地址。還給我祭出了ipa包內容截圖。
於是我自己寫了段測試程式碼
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");
}
});
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 物件,是不是就鎖不住了呢。於是我測試了下面的程式碼。
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");
}
});
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” 的地址。
後來又加了兩句這樣的列印。
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可以看到
字串在包裡的地址是
0000000100024a80
再看看程式列印出來string1和string2的地址
0x102b30a80
不一樣,有點納悶。這時候又請教了組裡的一位大神,大神給解釋說,ios程式的安裝就好像是把安裝包的內容搬到了記憶體裡。安裝包裡的地址和記憶體裡的地址肯定是不一樣的,但他們相對於起始位置的偏移應該是一樣的。於是下面開始找安裝包和記憶體各自的起始位置。
安裝包的起始位置也可以從Hopper中看到,在Hopper中將位置拉到安裝包的起始處,可以看到如下地址。
可以看到安裝包的起始位置是。
0000000100000000
那記憶體的起始位置怎麼看到,可以在Xcode中使用命令image list
能列出整個程式image的內容。
(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
可以看出起始位置是
0x0000000102b0c000
那麼我看減一下,看看偏移是否一致呢
0x102b30a80 - 0x0000000102b0c000 = 0000000100024a80 - 0000000100000000
可以看到,是一樣的,也就是說,即使是用靜態字串初始化的NSString,他們指向的內容依然是一樣的。
問題2
對於問題2,synchronized 鎖的是內容地址而非物件地址,這個可否從程式碼裡找到根據。就需要去翻閱ios的程式碼了,首先我們需要搞清楚@synchronized這個語法糖,到底呼叫的是什麼方法,從Xcode中開啟Debug -> Debug Workflow -> Always Show Disassembly,在斷點除錯的時候可以可以看到彙編程式碼。
可以看到@synchronized編成彙編後如下
-> 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中。
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 存的是什麼東西就行了。
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是一層物件,指標指向常量池裡的字串,那如果我用兩層物件呢,比如如下這。
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,然後又指向了常量字串,列印出的內容如下。
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 來初始化會怎麼樣呢。
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");
}
});
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 失效。
相關文章
- 獵奇:Mac系統預設桌布拍攝Mac
- 前端獵奇系列之探索Python來反補JavaScript——上篇前端PythonJavaScript
- synchronizedsynchronized
- synchronized 原理synchronized
- Synchronized bnsynchronized
- synchronized探究synchronized
- synchronized原理synchronized
- synchronized學習synchronized
- Synchronized詳解synchronized
- Synchronized 精講synchronized
- Synchronized同步鎖synchronized
- synchronized的使用(一)synchronized
- synchronized實現原理synchronized
- synchronized 鎖的原理synchronized
- Synchronized 實現原理synchronized
- Synchronized的那些事synchronized
- java中的synchronizedJavasynchronized
- synchronized 關鍵字synchronized
- 徹底理解synchronizedsynchronized
- synchronized底層原理synchronized
- 深入學習synchronizedsynchronized
- 出一款爆一款,這類超休閒遊戲的獵奇之路正在越走越遠遊戲
- 併發程式設計之synchronized(二)------jvm對synchronized的優化程式設計synchronizedJVM優化
- transient和synchronized的使用synchronized
- 四、Synchronized與Lock原理synchronized
- java Synchronized的優化Javasynchronized優化
- SpringMvc的Controller singleton synchronizedSpringMVCControllersynchronized
- 使用 Synchronized 關鍵字synchronized
- 構建一個 @synchronizedsynchronized
- Java基礎-Synchronized原理Javasynchronized
- java併發之synchronizedJavasynchronized
- synchronized鎖重入問題synchronized
- 從此不怕Synchronized鎖synchronized
- synchronized 的實現原理synchronized
- 深入瞭解Synchronized原理synchronized
- synchronized升級過程synchronized
- Java synchronized那點事Javasynchronized
- 你真的懂synchronized鎖?synchronized