线程

在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存文件,所以线程之间互相通信不必调用内核

自动释放池

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

自动释放池的实现:

  • 一个线程的自动释放池是一个指针堆栈
  • 每一个指针或者指向被释放的对象,或者是自动释放池的POOL_BOUNDARY,POOL_BOUNDARY 是自动释放池的边界。
  • 一个池子的 token 是指向池子 POOL_BOUNDARY 的指针。当池子被出栈的时候,每一个高于标准的对象都会被释放掉。
  • 堆栈被分成一个页面的双向链表。页面按照需要添加或者删除。
  • 本地线程存放着指向当前页的指针,在这里存放着新创建的自动释放的对象。

自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的,当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中,调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息.
双向链表.png

AutoreleasePoolPage 的结构

AutoreleasePoolPage 是一个 C++ 中的类:
AutoreleasePoolPage.png
它在 NSObject.mm 中的定义是这样的:

  • magic 用于对当前 AutoreleasePoolPage 完整性的校验
  • thread 保存了当前页所在的线程
    每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000):

AutoreleasepoolPage 通过压栈的方式来存储每个需要自动释放的对象:

每一次push,取出来的返回的dest对象一定是EMPTY_POOL_PLACEHOLDER,或者POOL_BOUNDARY,当一个池子push后,里面不含有任何对象,有这个标识时,可以优化内存使用。

RunLoop概念

苹果对run loop 的解释:

A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

RunLoop 实际上是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,函数会一直处于 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息)。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。CFRunLoopRef代码是开源的,https://opensource.apple.com/tarballs/CF/可以下载最新的源码。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

RunLoop与线程的关系

获取 main thread 的方法: pthread_main_thread_np() 或 [NSThread mainThread],获取当前线程的方法:pthread_self() 或 [NSThread currentThread] 。
通过这两个方法CFRunLoopGetMain() 和 CFRunLoopGetCurrent()来获取RunLoop:

从上面的代码可以看出,获取RunLoop时需要传pthread_t t作为参数,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

总结

  • 线程是独立调度和分派的基本单位,RunLoop和自动释放池为线程服务;
  • RunLoop是一个事件循环,让线程休眠和线程保活成为了可能,线程休眠可以节省CPU资源;
  • 自动释放池一定存在于线程之中,解决了资源的释放问题。
分类: iOS

2 条评论

BIN · 二月 6, 2018 12:51 上午

全局线程和子线程如果不获取runloop,他们的逻辑是主线程的runloop处理吗?

    Shawn.Du · 四月 11, 2018 10:37 上午

    子线程如果不主动获取runloop,将不会创建

发表评论

电子邮件地址不会被公开。 必填项已用*标注