美文网首页
《基础知识整理温习二》

《基础知识整理温习二》

作者: canlanrensheng | 来源:发表于2019-01-14 19:33 被阅读0次

    一、Runtime

    runtime 概念:又叫运行时,是一套底层的 C 语言 API,它将很多静态语言在编译和链接时期做的事放到了 runtime 运行时来处理,可以说 runtime 是我们 Objective-C 幕后工作者。


    SEL:类成员方法的指针,但和C的函数指针还不一样,函数指针直接保存了方法的地址,但是SEL只是“方法编号”。

    IMP:函数指针,保存了“方法地址”。

    通过取得 IMP,我们可以跳过 runtime 的消息传递机制,直接执行 IMP指向的函数实现,这样省去了 runtime 消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些,当然必须说明的是,这种方式只适用于极特殊的优化场景,如效率敏感的场景下大量循环的调用某方法。

    runtime消息机制:OC调用方法底层调用objc_msgSend发送消息,如果是对象的话会先从自身方法列表查找方法,没有再去父类中查找方法。如果是类调用方法,先从本类查找,没有再去元类中查找方法。如果一直查找到 NSObject 还不响应,则会进入消息转发处理流程。

    消息转发是在运行时进行的,大致分为两个阶段:第一阶段是先检查接收者,看是否能通过runtime动态添加一个方法,来处理这个unknown selector的消息;第二阶段就是完整的消息转发机制,首先会先查看有没有其它对象能够处理该消息,如果没有,就把该消息的全部信息封装到NSInvocation对象中,看那个对象能否处理,如果还无法处理,就查看继承树中的类是否能够处理该消息,如果到NSObject之前都无法处理该消息,那么最后就会调用NSObject类的doesNotRecognizeSelector方法来抛出异常,表明调用的方法不存在。

    消息转发机制基本分为三个步骤:

    1.动态方法解析

    对象在收到无法处理的消息时,会调用下面的方法,前者是调用类方法时会调用,后者是调用对象方法时会调用

    // 类方法专用+ (BOOL)resolveClassMethod:(SEL)sel

    // 对象方法专用+ (BOOL)resolveInstanceMethod:(SEL)sel

    在该方法中,需要给对象所属类动态的添加一个方法,并返回YES,表明可以处理

    在这个方法中我们可以利用runtime的特性动态添加方法来处理

    2、备用接受者

    经历了第一步后,如果该消息还是无法处理,那么就会调用下面的方法,查询是否有其它对象能够处理该消息

    -(id)forwardingTargetForSelector:(SEL)aSelector

    在这个方法里,我们需要返回一个能够处理该消息的对象

    3、完整转发

    经历了前两步,还是无法处理消息,那么就会做最后的尝试,先调用methodSignatureForSelector:获取方法签名,然后再调用forwardInvocation:进行处理,这一步的处理可以直接转发给其它对象,即和第二步的效果等效,但是很少有人这么干,因为消息处理越靠后,就表示处理消息的成本越大,性能的开销就越大。所以,在这种方式下,会改变消息内容,比如增加参数,改变选择子等等。

    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector-(void)forwardInvocation:(NSInvocation*)anInvocation

    Runtime应用场景:

    1、动态添加属性

    2、动态添加方法

    3、动态交换方法

    4、字典转模型

    5、归档和解档

    二、Runloop

    run loop 就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。其实内部就是do-while循环,这个循环内部不断地处理各种任务(比 如Source,Timer,Observer)。使用run loop的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态,这可以大大节省系统资源。

    RunLoop基本作用:

    保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行

    处理App中的各种事件(比如:触摸事件,定时器事件,Selector事件等)

    节省CPU资源,提高程序性能,程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CUP,现在没有事情做,我要去休息,这时CUP就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情

    我们先通过API内一张图片来简单看一下RunLoop内部运行原理


    RunLoop 有五种运行模式,其中常见的有1.2两种

    1.kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行

    2.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响3.UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode

    4.GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到

    5.kCFRunLoopCommonModes:这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode

    一、线程与run loop

    1.1 线程任务的类型

    再来说说线程。有些线程执行的任务是一条直线,起点到终点;而另一些线程要干的活则是一个圆,不断循环,直到通过某种方式将它终止。直线线程如简单的Hello World,运行打印完,它的生命周期便结束了,像昙花一现那样;圆类型的如操作系统,一直运行直到你关机。在IOS中,圆型的线程就是通过run loop不停的循环实现的。

    1.2 线程与run loop的关系

    Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop(以下都已Cocoa为例)。每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象。

    1.2.1 主线程的run loop默认是启动的。

    iOS的应用程序里面,程序启动后会有一个如下的main() 函数:

         int main(int argc, char *argv[])

         {

                @autoreleasepool {

                  return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class]));

               }

      }

    重点是UIApplicationMain() 函数,这个方法会为main thread 设置一个NSRunLoop 对象,这就解释了本文开始说的为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

    1.2.2 对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。

    2.runloop应用场景?

    2.1 、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 了。

    2.2定时器

    NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

    如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

    CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,这个稍后我会再单独写一页博客来分析。

    2.3PerformSelecter

    当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

    当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

    2.4事件响应

    苹果注册了一个 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 事件都是在这个回调中完成的。

    2.5手势识别

    当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

    苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

    当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

    2.6界面更新

    当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

    苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

    _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

    2.7关于GCD

    实际上 RunLoop 底层也会用到 GCD 的东西。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

    当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

    2.8关于网络请求

    iOS 中,关于网络请求的接口自下至上有如下几层:

    • CFSocket 是最底层的接口,只负责 socket 通信。

    • CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。

    • NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。

    • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。

    下面主要介绍下 NSURLConnection 的工作过程。

    通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

    当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

    NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。

    深入理解Runloop

    三、main()之前的过程有哪些?

    main之前的加载过程:

    1、dyld开始将程序二进制文件初始化

    2、交有ImageLoader读取image,其中包含了我们的类、方法等各种符号

    3、由于runtime 向dyld 绑定了回调,当image加载到内存后,dyld会通知runtime进行处理

    4、runtime 接手后调用map_images做解析和处理

    5、接下来load_images 中调用call_load_methods方法,遍历所有加载进来的Class,按继承层次依次调用Class的+load和其他Category的+load方法

    6、至此 所有的信息都被加载到内存中

    7、最后dyld调用真正的main函数

    注意:dyld会缓存上一次把信息加载内存的缓存,所以第二次比第一次启动快一点

    RunLoop和线程间的关系

    每条线程都有唯一的一个与之对应的RunLoop对象

    RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value

    主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建

    RunLoop在第一次获取时创建,在线程结束时销毁

    四、NSTimer为什么不准?

    1、NSTimer是被添加到runloop当中,受mode的影响,mode切换的时候定时器就不工作了

    2、NSTimer被添加到runloop当中,因为runloop要处理各种事件,比如timer, observer ,source,会出现等待的情况。

    五、为什么AFN2.0用了线程保活AFN3.0不再用了?

    1、2.0的时候使用的NSUrlconnection进行网络请求,必须在同一个线程中请求,然后处理回调

    2、3.0的时候NSUrlSession取代了NSUrlconnection进行网络请求,进行并行请求,并设置回调delegateQuene为1,串行回调

    相关文章

      网友评论

          本文标题:《基础知识整理温习二》

          本文链接:https://www.haomeiwen.com/subject/wktbrqtx.html