我的系列文章地址 RickeyBoy – Github

布景

Crash 图示:

疑难杂症:CFSocketInvalidate 多线程导致的 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 等协议。

疑难杂症:CFSocketInvalidate 多线程导致的 Crash 问题

假如想要更清楚地了解什么是 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 这儿的,能够和上面这一行代码对应的起来,能够确认便是这个原因。

疑难杂症:CFSocketInvalidate 多线程导致的 Crash 问题

不过别的一种 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];
}

参考资料: