我的系列文章地址 RickeyBoy – Github
布景
Crash 图示:
开始剖析
首先咱们根据堆栈信息,定位到 stopSocket 代码方位:
@property (nonatomic, strong, readwrite, nullable) CFSocketRef socket __attribute__ ((NSObject));
- (void)stopSocket {
if (self.socket != NULL) {
CFSocketInvalidate(self.socket);
self.socket = NULL;
}
}
由于这儿的代码比较简略,crash 发生在 CFSocketInvalidate 办法内部中,所以高度怀疑是多线程导致的 crash。不过为了确认这个可能性,咱们需求进一步了解一下什么是 Socket,以及 CFSocket 到底是什么东西。
什么是 Socket
Socket是一种用于在计算机网络中进行通信的编程接口。
它供给了一种机制,使得不同计算机之间能够经过网络传输数据。经过Socket,应用程序能够树立衔接、发送数据、接纳数据和封闭衔接。
咱们一般对 TCP 协议、UDP 协议比较熟悉,而 Socket 是根据这些通信协议的一套接口标准,只有经过 Socket 才干运用 TCP 等协议。
假如想要更清楚地了解什么是 Socket,推荐一个 B 站视频,解说的非常清楚:Socket 到底是什么 – 小白debug
CoreFoundation 中的完成:CFSocket
咱们能够在这儿 CFSocket.c 看到 CoreFoundation 中相关的源码,感兴趣的同学能够仔细地进行阅览。咱们这儿只是摘取它的界说部分,摘取部分内容进行解读。
struct __CFSocket {
CFRuntimeBase _base;
struct {
unsigned client:8; // flags set by client (reenable, CloseOnInvalidate)
unsigned disabled:8; // flags marking disabled callbacks
unsigned connected:1; // Are we connected yet? (also true for connectionless sockets)
unsigned writableHint:1; // Did the polling the socket show it to be writable?
unsigned closeSignaled:1; // Have we seen FD_CLOSE? (only used on Win32)
unsigned unused:13;
} _f;
CFSpinLock_t _lock;
CFSpinLock_t _writeLock;
CFSocketNativeHandle _socket; /* immutable */
SInt32 _socketType;
SInt32 _errorCode;
CFDataRef _address;
CFDataRef _peerAddress;
SInt32 _socketSetCount;
CFRunLoopSourceRef _source0; // v0 RLS, messaged from SocketMgr
CFMutableArrayRef _runLoops;
CFSocketCallBack _callout; /* immutable */
CFSocketContext _context; /* immutable */
CFMutableArrayRef _dataQueue; // queues to pass data from SocketMgr thread
CFMutableArrayRef _addressQueue;
struct timeval _readBufferTimeout;
CFMutableDataRef _readBuffer;
CFIndex _bytesToBuffer; /* is length of _readBuffer */
CFIndex _bytesToBufferPos; /* where the next _CFSocketRead starts from */
CFIndex _bytesToBufferReadPos; /* Where the buffer will next be read into (always after _bytesToBufferPos, but less than _bytesToBuffer) */
Boolean _atEOF;
int _bufferedReadError;
CFMutableDataRef _leftoverBytes;
};
假如仔细阅览,咱们在界说中其实能看出一些端倪。比方 Socket 的效果便是完成两个设备/进程之间通信,因此会有地址的记录 _address
和 _peerAddress
,又比方读写缓冲区的完成 _readBuffer
。
还有一个细节,便是 CFSocket 目标也相关了 runloop 列表,和一个 source0,这是什么效果呢?
CFSocket 为什么要绑定 Runloop
struct __CFSocket {
// ...
CFRunLoopSourceRef _source0; // v0 RLS, messaged from SocketMgr
CFMutableArrayRef _runLoops;
// ...
};
咱们在学习 Runloop 的时候,必定了解过 Source 的概念。Source 是输入源的抽象类(protocol),RunLoop 界说了两种类型 source:
-
Source0:处理 app 内部事情,系统触发:UIEvent、CFSocket 等
- 只包含了一个回调(函数指针),它并不能主动触发事情
- 不行唤醒 RunLoop,只能等候 RunLoop wakeup 之后被处理
-
Source1:RunLoop 内核办理,Mach Port 驱动
- 包含了一个 mach_port 和一个回调(函数指针)
- 用于经过内核和其他线程彼此发送消息
- 可唤醒 RunLoop
这儿是不是就串起来了,当创立一个 CFSocket 目标时,能够将其与 Runloop 相关起来。
CFSocket 就能够将网络事情(如衔接树立、数据抵达)作为 Runloop 的 Source0 输入源,当有事情发生时,Runloop 会主动唤醒并调用相应的回调函数来处理事情。
这样经过 Runloop 事情驱动的方式来处理网络事情,提高了功率并降低了资源耗费。
CFSocketInvalidate 代码详解(可越过)
回到咱们崩溃的办法 CFSocketInvalidate,我进行了非常具体的注释,感兴趣的同学能够仔细阅览以下,对理解 CFSocket 的原理很有帮助。
当然假如没有兴趣,越过这段代码也没有影响。只需求知道,假如是在多线程的状况下,这段代码会有可能导致 crash 就对了。
void CFSocketInvalidate(CFSocketRef s) {
CHECK_FOR_FORK(); // 多进程环境下,查看是否进行了进程分叉(fork)
UInt32 previousSocketManagerIteration;
__CFGenericValidateType(s, __kCFSocketTypeID); // 保证 s 是 CFSocketRef 类型
#if defined(LOG_CFSOCKET)
fprintf(stdout, "invalidating socket %d with flags 0x%x disabled 0x%x connected 0x%xn", s->_socket, s->_f.client, s->_f.disabled, s->_f.connected);
#endif
CFRetain(s); // 增加引用计数,保证履行期间 s 不会被开释
__CFSpinLock(&__CFAllSocketsLock); // 自旋锁,对一切 Socket 列表进行加锁
__CFSocketLock(s); // 对 s 加锁
if (__CFSocketIsValid(s)) {
SInt32 idx;
CFRunLoopSourceRef source0; // 用于保存 Runloop 循环源
void *contextInfo = NULL;
void (*contextRelease)(const void *info) = NULL;
// ↓ 铲除标志位
__CFSocketUnsetValid(s);
__CFSocketUnsetWriteSignalled(s);
__CFSocketUnsetReadSignalled(s);
// ↑ 铲除标志位
__CFSpinLock(&__CFActiveSocketsLock); // 自旋锁,对一切活泼的 Socket 列表进行加锁
// 在 WriteSockets 列表中查找 s 的 idx,找到的话就进行铲除
idx = CFArrayGetFirstIndexOfValue(__CFWriteSockets, CFRangeMake(0, CFArrayGetCount(__CFWriteSockets)), s);
if (0 <= idx) {
CFArrayRemoveValueAtIndex(__CFWriteSockets, idx);
__CFSocketClearFDForWrite(s);
}
// 在 ReadSockets 列表中查找 s 的 idx,找到的话就进行铲除
// No need to clear FD's for V1 sources, since we'll just throw the whole event away
idx = CFArrayGetFirstIndexOfValue(__CFReadSockets, CFRangeMake(0, CFArrayGetCount(__CFReadSockets)), s);
if (0 <= idx) {
CFArrayRemoveValueAtIndex(__CFReadSockets, idx);
__CFSocketClearFDForRead(s);
}
previousSocketManagerIteration = __CFSocketManagerIteration;
__CFSpinUnlock(&__CFActiveSocketsLock); // 自旋锁,开释活泼的 Socket 列表
CFDictionaryRemoveValue(__CFAllSockets, (void *)(uintptr_t)(s->_socket)); // 从大局列表中移除 s
// 封闭 s
if ((s->_f.client & kCFSocketCloseOnInvalidate) != 0) closesocket(s->_socket);
s->_socket = INVALID_SOCKET;
// 开释 s 的地址、数据队列、队列地址
if (NULL != s->_peerAddress) {
CFRelease(s->_peerAddress);
s->_peerAddress = NULL;
}
if (NULL != s->_dataQueue) {
CFRelease(s->_dataQueue);
s->_dataQueue = NULL;
}
if (NULL != s->_addressQueue) {
CFRelease(s->_addressQueue);
s->_addressQueue = NULL;
}
s->_socketSetCount = 0;
// 唤醒 Runloop
for (idx = CFArrayGetCount(s->_runLoops); idx--;) {
// 为了保证在调用 CFSocketInvalidate 后,相关的运行循环能够及时注意到 CFSocketRef 目标的无效状况,并相应地更新事情处理机制
CFRunLoopWakeUp((CFRunLoopRef)CFArrayGetValueAtIndex(s->_runLoops, idx));
}
// 继续铲除 s 的上下文信息等
CFRelease(s->_runLoops);
s->_runLoops = NULL;
source0 = s->_source0;
s->_source0 = NULL;
contextInfo = s->_context.info;
contextRelease = s->_context.release;
s->_context.info = 0;
s->_context.retain = 0;
s->_context.release = 0;
s->_context.copyDescription = 0;
__CFSocketUnlock(s); // 解锁 S
if (NULL != contextRelease) {
contextRelease(contextInfo);
}
if (NULL != source0) {
// 假如 s 绑定过 Runloop,那么需求终止和开释这个 runloop
CFRunLoopSourceInvalidate(source0);
CFRelease(source0);
}
} else {
__CFSocketUnlock(s);
}
__CFSpinUnlock(&__CFAllSocketsLock); // 解锁大局 socket 列表
CFRelease(s);
}
这儿再多说一句,为什么需求履行 CFRunLoopWakeUp 这一步操作呢?
CFRunLoopWakeUp((CFRunLoopRef)CFArrayGetValueAtIndex(s->_runLoops, idx));
因为 CFSocketInvalidate
办法会将 CFSocketRef
目标标记为无效,表明其不再可用,需求将这个状况通知给相关的 Runloop,这样才干能够相应地更新其状况和事情处理机制。
在某些状况下,运行循环可能处于休眠状况,等候事情的发生。假如在这种状况下调用 CFSocketInvalidate
,运行循环可能会继续等候,而不会当即注意到 CFSocketRef
目标的无效状况。为了处理这个问题,需求调用 CFRunLoopWakeUp
函数来唤醒休眠中的运行循环,以便它能够当即注意到 CFSocketRef
目标的无效状况,并相应地更新事情处理机制。
CFSocketInvalidate Crash 原因剖析
首先一个最简略的状况,便是当进入 CFSocketInvalidate 办法时,参数 CFSocketRef s 已经被开释了。在这种状况下下面这段类型查看代码必定就不会经过:
__CFGenericValidateType(s, __kCFSocketTypeID); // 保证 s 是 CFSocketRef 类型
从很多 crash 中,也能找到崩溃在 __CFGetNonObjCTypeID
这儿的,能够和上面这一行代码对应的起来,能够确认便是这个原因。
不过别的一种 crash 在 ___CFCheckCFInfoPACSignature
方位的,没有看到比较清晰的阐明。姑且认为可能是加锁时 CFSocketRef s 已经被开释相关。
处理办法
目前先进行加锁即可,注意两种互斥锁办法,优先选择声明 lock 而不是 @synchronized,因为 stopSocket 的场景可能会高频调用。
- (void)stopSocket {
[self.lock lock];
// CFSocketInvalidate 多线程履行时,有可能导致重复开释
if (self.socket != NULL) {
CFSocketInvalidate(self.socket);
self.socket = NULL;
}
[self.lock unlock];
}