Object-C runtime
Object-C动态运行时特性是用C和汇编实现的,可以使得程序在运行时才去创建,访问,修改类或对象的属性,方法或协议。
对象、类和元类

Method、SEL和IMP
Mehtod是一个函数整体的总称,包括方法名,返回值,参数及实现。
SEL本质上是char *
指针,代表方法的名称。
IMP本质上是函数指针,指向方法的地址。
每一个Object-C对象结构中都维护有一个方法列表,其中方法是以key-value的形式存在的,key就是SEL,而value则是IMP,所以OC中同一对象中不能存在名字相同的方法,即使它们有不同的传入参数。
消息传递
自己类的cacheList -> mehtodList ->
父类们的cacheList -> mehtodList ->
最终查找到共同的父类NSObject,仍未查找到方法->
resolveInstanceMethod(可在此为类临时添加方法) ->
forwardingTargetForSelector(可以指定一个类去接收这个消息) ->
methodSignatureForSelector(可以指定一个NSMethodSignature对象代替该方法) ->
forwardInvocation(可以将该消息发送给指定对象去执行) ->
doesNotRecognizeSelector(可在此记录crash信息)unsignatured selecter crash
JSPath在创建多参数IMP的时候,就是在forwardInvocation方法获取方法的所有参数信息的。
关于JSPath相关介绍参阅这篇文章
更详细的介绍请参阅这篇文章
Category
可以动态地为已经存在的类添加新的方法,这样可以保证类的原始设计规模较小,功能增加时再逐步扩展。
加载时机:
- 打开objc源代码,找到 objc-os.mm, 函数
_objc_init
为runtime的加载入口,由libSystem调用,进行初始化操作。 - 之后调用objc-runtime-new.mm ->
map_images
加载map到内存 - 之后调用objc-runtime-new.mm->
_read_images
初始化内存中的map, 这个时候将会load所有的类,协议还有Category。
更详细的介绍请参阅:深入理解Objective-C:Category
Associate
通过类别和访问器,再结合联合存储技术,我们可以为类增加属性。即在categray文件中声明属性及其访问器,并用objc_setAssociatedObject
和objc_getAssociatedObject
方法去设置和读取属性。
实现原理:
AssociationsManager
通过持有一个自旋锁保证对单例AssociationsHashMap
的操作是线程安全的,即每次只会有一个线程对AssociationsHashMap
进行操作AssociationsHashMap
单例中保存着一个个的ObjectAssociationMap
对象- 每个
ObjectAssociationMap
对象都保有一个从key
到关联对象ObjcAssociation
的映射 - 最关键的
ObjcAssociation
包含了policy
(OBJC_ASSOCIATION_RETAIN_NONATOMIC属性修饰符) 以及value
(存储的值)
更详细的描述参阅:关联对象 AssociatedObject 完全解析
内存释放:
被关联的对象在生命周期内要比对象本身释放的晚很多
对象的内存销毁时间表:https://github.com/ChenYilong
- 调用 -release :引用计数变为零
- 对象正在被销毁,生命周期即将结束.
- 不能再有新的 __weak 弱引用, 否则将指向 nil.
- 调用 [self dealloc]
- 子类 调用 -dealloc
- 继承关系中最底层的子类 在调用 -dealloc
- 如果是 MRC 代码 则会手动释放实例变量们(iVars)
- 继承关系中每一层的父类 都在调用 -dealloc
- NSObject 调 -dealloc
- 只做一件事:调用 Objective-C runtime 中的 object_dispose() 方法
- 调用 object_dispose()
- 为 C++ 的实例变量们(iVars)调用 destructors
- 为 ARC 状态下的 实例变量们(iVars) 调用 -release
- 解除所有使用 runtime Associate方法关联的对象
- 解除所有 __weak 引用
- 调用 free()
KVO
键值观察建立对对象成员变量的观察,当变量值发生改变时会触发相应的观察事件
实现原理:
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。NSNotification的通知回调会在发出post的线程中同步执行
更详细的介绍请参阅详解键值观察(KVO)及其实现机理
内存管理
Object-C是用引用计数来维护和管理内存,遵循谁创建谁释放的原则。
clang规定了四种所有权限定符包括:
- __autoreleasing
- __strong
- __unsafe_unretain
- __weak
clang包含了以下属性声明类型:
- assign implies __unsafe_unretained ownership.
- copy implies __strong ownership, as well as the usual behavior of * copy semantics on the setter.
- retain implies __strong ownership.
- strong implies __strong ownership.
- unsafe_unretained implies __unsafe_unretained ownership.
- weak implies __weak ownership.
arc并非其他语言的垃圾回收器,它只是将MRC中需要手动添加的retian/release/autorelease
方法的过程交给编译去做了相应的处理。@property
属性修饰符:retain/assign/strong/weak/unsafe_unretained/copy
retain/strong:可以持有对象,使得对象的引用计数+1;
copy:会创建对象的副本,防止无意间的修改影响原对象
weak/unsafe_unretained:只是指向对象,不会引起引用计数的变化;对象释放后weak修饰的属性会自动置为nil,unsafe_unretained 则不会
assign:修饰的属性可以不是OC对象,比如CGFloat,int等
1 | // ARC环境下 |
performSelector
的使用:
当使用performSelector
去执行创建或返回对象的方法时需要注意:
1 | SEL selector; |
由于动态特性,编译器不知道即将调用的 selector 是什么,不了解方法签名和返回值,甚至是否有返回值都不懂,所以编译器无法用 ARC 的内存管理规则来判断返回值是否应该释放。因此,ARC 采用了比较谨慎的做法,不添加释放操作,即在方法返回对象时就可能将其持有,从而可能导致内存泄露。
以本段代码为例,前两种情况(newObject, copyObject)都需要再次释放,而第三种情况不需要。这种泄露隐藏得如此之深,以至于使用静态分析都很难检测到。如果把代码的最后一行改成:[object performSelector:selector];
不创建一个返回值变量测试分析,简直难以想象这里居然会出现内存问题。所以如果你使用的 selector 有返回值,一定要处理掉。autorelease
自动释放池:
手动添加autorelease:一般发生在自定义线程中,比如自定义的NSOperation的main函数中需要手动添加@autorelease否则可能造成内存泄露
系统自动添加autorelease:在主线程中每一个运行循环开始的时候,系统会自动创建autorelease,运行循环结束的时候会向池中的每一个对象发送release消息,然后该释放池对象会被添加到下一个运行循环的释放吃中去释放,以此类推。比如:viewController的viewDidLoad到viewDidAppear之间就是一个运行循环
更深入的理解请参阅这篇文章
Block
Block又称匿名函数,他的核心是一段可执行的代码,你可以给它传递参数或获取返回值。
在内存中Block分三种类型:
_NSConcreteGlobalBlock
全局的静态block,不会访问任何外部变量_NSConcreteStackBlock
保存在栈中的block,出栈时会被销毁- 在局部作用域中声明的block需要在作用域之外调用时,需要对该block进行copy操作,将该block拷贝找堆上,并将其类型转换为
_NSConcreteMallocBlock
类型 - 需要对block外的变量进行修改时需要添加
__block
修饰,当系统将block复制到堆上时,该变量的地址也会一同被拷贝到堆上(区别于不加__block
修饰的变量,只会将该变量的值拷贝到堆上,当时栈环境对该变量的修改并不会影响Block执行时使用该变量的值)
- 在局部作用域中声明的block需要在作用域之外调用时,需要对该block进行copy操作,将该block拷贝找堆上,并将其类型转换为
1 | __block int a = 5; |
- MRC情况下,
__block
修饰符可以解除Block循环引用的问题
1 | __block id tmp = self; |
_NSConcreteMallocBlock
保存在堆中的block,当引用计数为0时会被销毁
在ARC环境下,我们常常会使用weak 的修饰符来修饰一个变量,防止其在block中被循环引用,但是有些特殊情况下,我们在block中又使用strong 来修饰这个在block外刚刚用__weak修饰的变量,为什么会有这样奇怪的写法呢?
在block中调用self会引起循环引用,但是在block中需要对weakSelf进行strong,保证代码在执行到block中,self不会被释放,当block执行完后,会自动释放该strongSelf
Runloop
Runloop是iOS中实现的一种事件驱动模型
主线程的Runloop默认是启动的,它主要执行更新用户界面的操作。
子线程的Runloop默认是关闭的,所以在子线程的runloop中添加了一个时间源后,需要手动启动子线程的runloop才能对时间源进行监听和触发。如果想保持子线程长时间运行不退出,可以启动线程的runloop向其中添加一些长时间或周期性的事件源,如:performSelecter
NSTimer
NSURLConection
等
iOS系统中的Runloop可以运行在以下几种模式中:
NSRunLoopDefaultMode
默认模式中几乎包含了所有输入源(NSConnection除外),一般情况下应使用此模式。NSRunLoopCommonModes
这是一个伪模式,其为一组run loop mode的集合,将输入源加入此模式意味着在Common Modes中包含的所有输入源都可以处理。UITrackingRunLoopMode
用户界面拖动操作时处于此种模式下,在此模式下会限制输入事件的处理。例如,当手指按住UITableView拖动时就会处于此模式。
事件响应:
主线程创建的时候Runloop会对source0与source1这两个事件源进行监听,source0用于接收系统事件,如:UIEvent,进而唤醒runloop去进行处理;source1用于接收系统的mach_port事件,然后唤醒runloop去处理。当一个硬件事件(触摸/锁屏/摇晃/加速等)触发后:
- 首先由IOKit.framework生成一个IOHIDEvent事件并由 SpringBoard接收
- 随后由
mach_port
转发给当前的App进程,source1接收mach_port
事件后,在source1的回调函数中转发给source0 - source0接收事件后,在source0的回调函数中调用
_UIApplicationHandleEventQueue()
进行应用内部的分发
,_UIApplicationHandleEventQueue()
会把IOHIDEvent处理并包装成UIEvent进行处理或分发,其中包括识别UIGesture/处理屏幕旋转/发送给UIWindow等; - 当识别到一个guesture手势的时候,会中断当前touchBegin/Move/End系列回调,随后系统将对应的 UIGestureRecognizer标记为待处理,当事件循环的Observer监听到BeforeWaiting(Runloop即将进入休眠)状态时,会在其回调函数
_UIGestureRecognizerUpdateObserver()
中获取所有刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。
当有UIGestureRecognizer的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
Thread
GCD
despatch queue按照任务执行顺序分为:
- 串行队列(serial queue)
串行队列中的任务是顺序执行的(所有任务在一个线程中按顺序执行) - 并行队列(concurrent queue)
并行队列中的任务是并发执行的(同时有多个线程并发执行任务)
dispatch queue分为三种:
- main queue
属于串行队列,运行主循环runloop,一般执行与界面显示有关的操作需要放到主线程队列中去执行。 - global queue
属于并行队列,可以通过dispatch_get_global_queue
函数获取不同优先级(DISPATCH_QUEUE_PRIORITY_HIGH
,DISPATCH_QUEUE_PRIORITY_DEFAULT
,DISPATCH_QUEUE_PRIORITY_LOW
,DISPATCH_QUEUE_PRIORITY_BACKGROUND
)的全局队列 - custom queue
通过dispatch_queue_create
方法创建的队列 ,通过参数可获取指定类型(DISPATCHQUEUESERIAL
,DISPATCHQUEUECONCURRENT
)的队列,用户队列中的任务最终都会被安排到全局队列中去执行。
相关方法:
dispatch_async
:异步执行任务。dispatch_sync
:同步执行任务。dispatch_after
:延迟执行任务。dispatch_apply
:并行执行多次任务。dispatch_group
:用于任务调度,可实现等待放入group中的每个queue中所有任务都执行完之后再继续向下执行。dispatch_barrier_async
:用于任务调度,可实现等待前几个任务并行执行完成后,单独执行下面一条任务,等这条执行完成后,再并行执行后面的任务。dispatch_source
:用于监听系统的源,然后触发相应的处理。
NSOperation
main
与start
默认情况下main
函数不会做任何事情,如果只是执行一些简单的任务只需要重载main
函数即可;但是如果要执行一些复杂的并发操作需要重载start
并手动更新operation的状态(excting,finish等)然后在start
函数中进行一些operation状态的检查,满足条件后再调用main
执行操作。
GCD与NSOperation的比较:
GCD
是底层的C语言构成的API,而NSOperationQueue
及相关对象是Objc的对象。在GCD中,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构;而NSOperation
作为一个对象,为我们提供了更多的选择;- 在
NSOperationQueue
中,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而GCD没法停止已经加入queue的block(其实是有的,但需要许多复杂的代码); NSOperation
能够方便地设置依赖关系,我们可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行;- 我们能将KVO应用在
NSOperation
中,可以监听一个Operation是否完成或取消,这样子能比GCD
更加有效地掌控我们执行的后台任务; - 在
NSOperation
中,我们能够设置NSOperation
的priority优先级,能够使同一个并行队列中的任务区分先后地执行,而在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码; - 我们能够对
NSOperation
进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将block任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。 NSOperationQueue
将任务装载进主线程的队列中,runloop每执行一个循环都会取出队列中的一个任务去执行,这样使得在每两个operation被执行之间可以允许进行更新视图的操作。对于dispatch_async
和performSelectorOnMainThread
,则会将装载进队列中的block或selector一个接一个的执行,而不会插入更新视图或其他类似的操作。
更详细的介绍请参阅这一系列文章
iOS沙盒目录及其用途
下图为出自官方文档

- MyApp.app
存储应用程序本身的数据,包括资源文件和可执行文件等,并且该目录为只读的。程序启动以后,会根据需要从该目录中动态加载代码或资源到内存,这里用到了lazy loading的思想。
Documents
存储应用程序的数据文件,并且这些数据应该是不可再生的。- Documents/Inbox
存储由外部应用请求当前应用程序打开的文件。
- Documents/Inbox
Library
苹果建议用来存放默认设置或其它状态信息。除Caches子目录外,其他目录会被iTunes同步。- Library/Caches
存储可再生的应用程序的数据文件,如网络请求的数据,并且应用程序负责删除该目录下的文件。 - Library/Preferences
存储应用程序的偏好设置文件,程序中使用NSUserDefaults写的数据会以plist的文件形式存储在该目录下。
- Library/Caches
Temp
保存各种临时文件,即应用再次启动时不需要的文件,该目录下的东西随时(比如系统磁盘空间不足时)有可能被系统清理掉。
UIResponder
Hit-test
通过hit-test确定将touch event传递给谁
其中hit-test通过调用pointTest:withEvent:
方法的返回值确定点击的point是否落在指定视图bounds范围内
Responder-chain
通过responder-chain将touch event逐层传递给父view->window->application
更详细的描述请参阅这篇文章
UIViewController生命周期

苹果文档中对于loadView的解释:
你不应该直接调用该方法,视图控制器会在需要视图但是它的视图属性为nil时调用该方法。
该方法用于加载或创建一个新的视图对象并将它与视图控制器的视图属性建立关联。
当视图控制器与一个nib文件相关联时,该方法会自动去加载该nib文件。
当视图控制器实例化自一个storyboard,
或通过initWithNibName:bundle:方法为其指定一个与其相关联的nib文件;
或者iOS查找到一个与视图控制器的名称相匹配的nib文件时,
该视图控制器的nibName属性都会返回一个非空的值。
通过以上几步视图控制器还是没有找到一个与它相关联的nib文件时,
该方法回去创建一个普通的视图取代它们对视图控制器的视图属性进行初始化。
如果你是通过Interface Builder去创建你的视图并初始化视图控制器的,你不可以重载该方法。
当你想要手动创建自定义视图的时候需要调用该方法。这样的话你就必须指定视图层的跟视图。
你创建的视图必须是唯一的实例并且不应该共享给任何其它的视图控制器对象。
你的自定义实现中不应该调用[super loadView]
如果你想要实现有关自定义视图的其它初始化工作应该在viewDidLoad方法中去完成。
UIApplication
启动流程
程序启动->
libDispatch、Runtime初始化->
main->UIApplicationMain->
初始化UIApplication对象,确定ApplicationDelegate->
启动主线程及Runloop->
监听处理系统或用户事件->
手动或系统强制结束程序
UIApplication Delegate Protrol
FinishLaunchingWithOptions程序启动完成
WillResignActive即将失去活动状态
DidBecomeActive已经重新获取活动状态
DidEnterBackground已经进入后台
WillEnterForeground即将进入前台
WillTerminate即将被销毁
DidReceiveMemoryWarning接收到内存警告
NSProxy
众所周知,NSObject类是Objective-C中大部分类的基类。但不是很多人知道除了NSObject之外的另一个基类——NSProxy,NSProxy实现被根类要求的基础方法,包括定义NSObject协议。然而,作为抽象类,它不实现初始化方法,并且会在收到任何它不响应的消息时引发异常。因此,具体子类必须实现一个初始化或者创建方法,并且重写已下两个方法,来转发它没实现的方法。
1 | - (void)forwardInvocation:(NSInvocation *)invocation |
这也是NSProxy的主要功能,负责把消息转发给真正的target的代理类,NSProxy正是代理的意思。
load 方法的调用情况至此已经全部清晰。思路梳理如下三大流程:
load方法调用流程
- Load Images: 通过 dyld 载入 image 文件,引入 Class。
- Prepare Load Methods: 准备 load 方法。过滤无效类、无效方法,将 load 方法指针和所属 Class 指针收集至全局 Class 存储线性表 loadable_classes 中,其中会涉及到自动扩展空间和父类优先的递归调用问题。
- Call Load Methods: 根据收集到的函数指针,对 load 方法进行动态调用。进一步过滤无效方法,并记录 log 日志。
###runLoop都能做什么
####AutoReleasePool:
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 了。
####CAAnimation
我们知道CAAniamtion为我们提供的是补间动画,开发者只要给出始末状态后中间状态有系统自动生成。那么动画是怎么出现的呢,是开发者给出始末状态后,系统计算出每一个中间态的各项参数,然后启一个定时器不断去回调并改变属性。
####事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
####手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
####定时器
不多说了这个就。
####PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。