每一年一次的iOS升级,都会给开发者带来一些适配工做,一些本来工做正常的代码可能就会发生崩溃。 本文讲到了一种 CoreFoundation 对象的内存管理方式在iOS13上遇到的问题。安全
iOS 13 Beta 版本上,手淘出现了一个必现的崩溃:app
Thread 0 name: Dispatch queue: com.apple.main-thread Thread 0 Crashed: 0 libobjc.A.dylib 0x00000001d6f9af20 objc_retain + 16 1 CFNetwork 0x00000001d7843f60 0x1d77b0000 + 606048 2 CFNetwork 0x00000001d780cec8 0x1d77b0000 + 380616 3 CFNetwork 0x00000001d77dff24 _CFSocketStreamCreatePair + 56 4 xxxxxxxxxxxxxxxxx 0x000000010c2a44b4 0x10b46c000 + 14910644 5 xxxxxxxxxxxxxxxxx 0x000000010c2a6238 0x10b46c000 + 14918200 6 xxxxxxxxxxxxxxxxx 0x000000010c2a661c 0x10b46c000 + 14919196
崩溃在了 _CFSocketStreamCreatePair
方法里面, 而后崩溃在了 objc_retain
里面,推测是传入的某个ObjC的对象野指针了致使的。优化
经过追溯源码,发现调用的是 CFStreamCreatePairWithSocketToHost
这个方法,而后找到这个方法的定义:ui
void CFStreamCreatePairWithSocketToHost( CFAllocatorRef _Null_unspecified alloc, CFStringRef _Null_unspecified host, UInt32 port, CFReadStreamRef _Null_unspecified * _Null_unspecified readStream, CFWriteStreamRef _Null_unspecified * _Null_unspecified writeStream );
根据上下文判断,是第二个参数 CFStringRef _Null_unspecified host
野指针了。url
而后找到这个 host
对象的初始化:spa
NSURL *serverUrl = [NSURL URLWithString:@"xxxxx"]; CFStringRef hostRef = (__bridge CFStringRef)serverUrl.host;
这段代码看起来好像并无问题,怎么会致使野指针,而后Crash呢?线程
这要从iOS的内存管理上找答案。3d
咱们都知道苹果使用 “引用计数” 技术来管理内存, 使用 “自动释放池AutoreleasePool” 技术来解决方法返回值的内存管理问题。 相关技术原理网上都有不少文章。可是本文中遇到的Crash是由苹果对使用 ARC 代码进行的编译优化从而引起的。因此先讲一下这个优化是什么。指针
考虑一个内存管理的最简单的case:调试
在最初的 ARC 机制下,上图中的左边代码会编译成右边这样的代码,从而保证了对象 b
的生命周期完整。
可是咱们再详细分析下这个代码,是否是去掉 [b autorelease]
和 [b retain]
这两步操做的话,代码也是能够正常执行的呢? 答案是确定的, 那么这个操做其实就是能够优化掉的。苹果考虑到了这一点。
那么要怎么样作到这个优化呢? 由于这个优化是须要同时考虑 被调用方: funcB
和 调用方: funcA
这两个方法配合来完成,由于须要根据调用方的内存管理代码才能决定我被调用方要不要真的去掉autorelease操做。 并且还要在ABI上向下适配。 苹果是这样作的:
代码:
// Prepare a value at +1 for return through a +0 autoreleasing convention. id objc_autoreleaseReturnValue(id obj) { // 判断是否须要优化, 若是能够,就直接return,不作autorelease if (prepareOptimizedReturn(ReturnAtPlus1)) return obj; return objc_autorelease(obj); } id objc_retainAutoreleasedReturnValue(id obj) { // 判断是否走了优化逻辑,若是走了就不用retain if (acceptOptimizedReturn() == ReturnAtPlus1) return obj; return objc_retain(obj); } static ALWAYS_INLINE bool prepareOptimizedReturn(ReturnDisposition disposition) { assert(getReturnDisposition() == ReturnAtPlus0); // 判断方法返回地址是否是某个值,是的话就认为能够优化 if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) { // 能够优化就把ReturnAtPlus1 存起来,存到了tls里面 if (disposition) setReturnDisposition(disposition); return true; } return false; } static ALWAYS_INLINE bool callerAcceptsOptimizedReturn(const void *ra) { // fd 03 1d aa mov fp, fp // arm64 instructions are well-aligned // 判断return address是否是 0xaa1d03fd, 在arm64上就是 `mov fp, fp` 指令 if (*(uint32_t *)ra == 0xaa1d03fd) { return true; } return false; } static ALWAYS_INLINE ReturnDisposition acceptOptimizedReturn() { ReturnDisposition disposition = getReturnDisposition(); setReturnDisposition(ReturnAtPlus0); // reset to the unoptimized state return disposition; } // 存在当 tls中,当前线程相关的 static ALWAYS_INLINE ReturnDisposition getReturnDisposition() { return (ReturnDisposition)(uintptr_t)tls_get_direct(RETURN_DISPOSITION_KEY); } static ALWAYS_INLINE void setReturnDisposition(ReturnDisposition disposition) { tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition); }
从上面的分析中,咱们能够得出,只要看到调用 objc_msgSend
以后的一条指令是 mov x29, x29
, 那么确定就是开启了这个优化。
因此,你们汇编调试的时候看到这样一行指令,不要以为奇怪 mov x29,x29
不是啥都没作么?实际上是用于这里的优化。
了解了 ObjC的 autorelease优化以后,再回到咱们遇到的crash问题。有理由怀疑 [NSURL host]
这个方法在旧版本系统上不会走这个优化,所以返回值被放入了 AutoreleasePool
因此后面继续使用是正常的。可是iOS13 上走到了这个优化逻辑,实际上返回的 host
是没有加入 AutoreleasePool
的。 而这个时候刚好又没有 objc 对象接收,直接用 __bridge
转移到了 CF对象上。致使这个 host
直接释放了。
经过查看 对 [NSURL host]
的调用代码证实了这个猜测:
[NSURL host]
获取host.mov x29, x29
因此若是[NSURL host]
里的实现是相似上述 funcB
则会走到autorelease优化。也就是返回的 host 没有加入autoreleasePool还须要证实的就是 [NSURL host]
自己的实现了。因而对比了iOS12 和 iOS13 上的实现:
iOS12 上内部经过调用了 [NSURL _cfurl]
获取,已经加入了autoreleasePool。
在iOS13上,就是正常的取值作autorelease, 所以会走到优化逻辑:
慎用 __bridge
来进行 OC对象和 CF对象直接的强转。 由于Autorelease优化的存在,这种用法可能让你的代码不安全,所以尽量使用 CFBridgeRetain
__bridge_retained
来转换管理CF对象,避免由于做用域不一致的状况致使对象呗提早释放的问题。
原文连接 本文为云栖社区原创内容,未经容许不得转载。