synchronized猎奇
阶段1事情的起因是同事写了这样一段代码。
于是我指出这样应该是锁不住的,因为 synchronized 锁的是对象,而每次创建的字符串都是新对象,所以锁不住。 同事跟我说,“no,no,no”,你太天真了,编译器会优化字符串,像这种写在代码里的字符串,会被放在ios包的常量字符串里,终生只有一个地址。还给我祭出了ipa包内容截图。 于是我自己写了段测试代码
看来真的是这样。 阶段2针对上面的问题,我想着写死在代码里的纯字符串会被编译器优化,那如果新创建的 NSString 对象,是不是就锁不住了呢。于是我测试了下面的代码。
神奇的事情发生了,string1 和 string2 的地址明显是不一样的,为什么还是能锁住呢。有了阶段1的经验,在这里,有一个猜想是,虽然 string1 和 string2 对象的地址不一样,但是他们指向的内容地址是一样的,还是 “test synchronized” 的地址。 后来又加了两句这样的打印。
发现他们指向的内容地址果然是一样的。那么这里就存在两个问题
问题1接下来我们去看 “test synchronized” 常量的地址是什么呢,通过Hopper可以看到 字符串在包里的地址是
再看看程序打印出来string1和string2的地址
不一样,有点纳闷。这时候又请教了组里的一位大神,大神给解释说,ios程序的安装就好像是把安装包的内容搬到了内存里。安装包里的地址和内存里的地址肯定是不一样的,但他们相对于起始位置的偏移应该是一样的。于是下面开始找安装包和内存各自的起始位置。 安装包的起始位置也可以从Hopper中看到,在Hopper中将位置拉到安装包的起始处,可以看到如下地址。 可以看到安装包的起始位置是。
那内存的起始位置怎么看到,可以在Xcode中使用命令
可以看出起始位置是
那么我看减一下,看看偏移是否一致呢
可以看到,是一样的,也就是说,即使是用静态字符串初始化的NSString,他们指向的内容依然是一样的。 问题2对于问题2,synchronized 锁的是内容地址而非对象地址,这个可否从代码里找到根据。就需要去翻阅ios的代码了,首先我们需要搞清楚@synchronized这个语法糖,到底调用的是什么方法,从Xcode中打开Debug -> Debug Workflow -> Always Show Disassembly,在断点调试的时候可以可以看到汇编代码。 可以看到@synchronized编成汇编后如下
@synchronized对应的代码就是objc_sync_enter和objc_sync_exit,接下来我们去ios runtime的源码里找对应的实现,源码可以从https://opensource.apple.com/source/objc4/中下载,下载之后搜索objc_sync_enter,代码是在objc-sync.mm中。
这个方法其实比较简单,通过 id2data 方法返回一个 SyncData 对象,然后调用 SyncData的 mutex 锁,如果传进来的 obj 是 nil 的话,这个锁就没有效果。看来重点在id2data 方法中。 id2data 主要是生成一个 SyncData 对象,关于 id2data 仿佛,这篇文章解释的很清楚剖析@synchronizd底层实现原理。简单来说,就是两层cache机制,能保证synchronized对同一个对象只会锁一次,并且还能适当加快效率。 其实对于问题2,我们只要看生成的 SyncData 存的是什么东西就行了。
从以上代码就可以看出,SyncData 存的是 object 指向的地址,而非 object 本地的地址。 阶段3后来我又好奇了,在阶段2是一层对象,指针指向常量池里的字符串,那如果我用两层对象呢,比如如下这。
string3 和 string4 分别指向了 string1 和 string2,然后又指向了常量字符串,打印出的内容如下。
如下可以看出,虽然经过了两层指针转换,但他们指向的内容地址依然一样,所以对 synchronized 的效果也是一样的。 阶段4上面的例子都是用常量字符串直接初始化 NSString,所以可能编译器有优化,那么如果我用 initWithFormat 来初始化会怎么样呢。
可以看出 initWithFormat 并没有进行常量字符串的优化,而是新创建了一个对象。 @synchronized 也就失效了。 结论常量字符串在编译时会被放在常量池里,也就是 原文:大专栏 ?synchronized猎奇 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |