程序猿阵线联盟-汇总各类技术干货

iOS面试题总结(二)

2017-11-07  本文已影响0人  沧州宁少

iOS面试题(二)

消息发送和转发机制,SEL和IMP

消息发送转载自黄龙辉消息发送和消息转发机制

尾部调用优化:一般情况下。方法内部调用另外一个方法,就会把方法的内部变量,返回地址等信息压人栈中,以便另外一个方法调用结束后,直接调用,比如A->B->C->D这样的话,就需要往栈里面压入很多信息。这样有可能发生栈溢出。为了一定程度避免这种情况,当方法是最后一步执行另外一个方法的时候。编译器会进行尾部调用优化。即不保留外层方法的信息(因为返回地址、内部变量等信息都不会再用到了),直接用内层方法的调用记录,取代外层方法的调用记录。

谈一下对RunLoop的理解

苹果公开提供的 Mode 有两个:

NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
NSRunLoopCommonModes(kCFRunLoopCommonModes)

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

  while(AppIsRunning){
  
      //睡眠状态等待唤醒事件
      
       let whoWakesMe = AwakeUpEvent()  
        
       id  event = GetEvent(whoWakesMe);
       
       AppIsRunning = HandEvnet(event);
                
  }  
} 

下面的RunLoop理解摘自一只魔法师的工坊

function loop(){

initialize();

do{

 var message = get_next_message();
 process_message(message);
}while(message != quit)        

}

这种模型称为Event Loop.Event Loop在许多系统中均有体现。比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种model的关键是如何管理消息/唤醒消息。使得线程在没有处理消息的时候休眠,避免消耗资源。有消息处理的时候唤醒。

所以,RunLoop实际就是一个对象,这个对象管理了其要处理的事件和消息。并且提供了一个入口函数来处理上面的Event Loop。线程执行了这个函数之后,当前线程就会一直处于 接收 + 等待+ 处理。直到传入quit 函数返回。OSX/IOS系统中提供了两个这样的对象。NSRunLoop/CFRunLoopRef。CFRunLoopRef是基于C语言Api下的。是线程安全的。NSRunLoop不是线程安全的。

线程和RunLoop之间的关系是一一对应的,其关系保存在全局的Dictionary里。线程刚创建出来的时候没有RunLoop.如果你不主动获取,那它一直都不会有,换句话就是RunLoop对象是懒加载的。RunLoop的创建是发生在第一次获取时。RunLoop的销毁发生在线程结束时。你只能在一个线程的内部获取其RunLoop(主线程外)。

在CoreFoundation里面关于RunLoop有五大类:

CFRunLoopModeRef类并没有对外面暴露。只是通过CFRunLoopRef进行了封装。

一个RunLoop包含若干个Model,每个Model又包含若干个Time/Source/Observe。每次调用RunLoop的主函数时,只能指定一个Model。因此当进行model切换的时候,必须先退出当前的Loop,再重新指定一个Model进入。这样是为了分割不同组的Source/Timer/Observer

CFRunLoopSoureRef 是事件产品的地方。Source分为两个版本。Source0和Source1。

Source0包含一个回调(函数指针),他不能主动触发事件。使用时候要使用CFRunLoopSoureSignal(source)对这个Source进行标记待处理。然后手动调用CFRunLoopWeakUp(runLoop)来唤醒RunLoop.让其处理source.

Source1包含一个match_port和一个回调。使用通过内核和其他线程相互发送消息。它可以主动唤醒RunLoop的线程。

CFRunLoopTimerRef 是基于时间的触发器。它和NSTimer是 toll-free bridged的,可以混用。其包含一个时间长度和一个回调。当其加入到RunLoop时,RunLoop会注册对应的时间点。时间点到的时候,RunLoop会被唤醒以执行那个回调。

CFRunLoopObserveRef是观察者。每个Observer都包含一个回调。当RunLoop的状态发生变化时。观察者能通过回调接收这个变化。可以观测到的时间点

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit          = (1UL << 7), // 即将退出Loop

};

上面的Source/Timer/Observer被统称为model,item 一个item可以被加入到多个Model当中。一个item重复加入到一个model没有用。如果一个Model中一个item都没有则直接退出。

RunLoop的Mode

CFRunLoopMode和CFRunLoop的结构大体如下

struct __CFRunLoopMode{

   CFStringRef _name;  //Mode Name
   CFMutableSetRef _sources0;
   CFMutableSetRef _source1;
   CFMutableArrayRef _observers;
   CFMutableArrayRef _timers;    // Array 
}

struct __CFRunLoop {

    CFMutableSetRef _commonModes;  //Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    
    CFRunLoopModeRef _currentMode;    //当前的RunLoopMode
    CFRunLoopSetRef _modes;           //Set
}

这里面有一个概念叫做CommonModes: 一个Mode可以将自己标记为Common属性。通过mode自己的name添加到RunLoop的commondModes中。当RunLoop的内容发生变化的时候,RunLoop的_commonModeItems会自动同步到具有Common标记的Mode中去。

有时候你需要一个Time。有两个Mode中都能回调。一种方法是分别加入这两个Mode。还有一种是加入到顶级的_commonModeItems里面。这样的话RunLoop状态变化,_commonModeItems里面的Time.Observe.Source便会自动同步到被标记为Common的Model中。

RunLoop_1.png

可以看到,实际上RunLoop就是一个函数。其内部是一个do-while循环,你调用CFRunLoopRun()时。线程会一直停留在这个循环里。直到超时或者被手动停止,该函数才会返回。

RunLoop的底层实现

RunLoop的核心是基于mach port的,其休眠时调用的函数是mach_msg()。我们先了解下OSX/iOS的系统

苹果官方将整个系统分为4个层次。

BSD,Mach,IOKit共同XNU内核。XNU的内环为Mach。其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。

BSD 层可以看做是Mach层的一个外环,提供了进程管理,文件系统和网络等功能。

IOKit为设备提供了一个面向对象的框架

Mach 本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的 API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对象”。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。消息是Mach中最基础的概念。 消息在两个端口之间传递。是Mach的进程间通信的核心。

为了实现消息的发送和接收,mach_msg()函数实际是调用一个Mach陷阱。即mach_mag_tap().陷阱的概念在Mach中等同于系统调用。当用户在外部执行mach_msg_tap()的时候触发陷阱机制。切换到内核;内核态中内核实现的mach_msg()函数会完成相应的工作。

可以看到,系统默认注册了5个Mode:

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

  2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

  3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。

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

  5. kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

AutoreleasePool

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

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

第二个Observer监听两个事件: BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush() 释放旧池并创建新池子。Exit(即将退出Loop)的时候调用_objc_autoreleasePoolPop()。这个Observe的order最低。保证其释放池子在其他的回调之后。

在主线程执行的代码,通常是写在诸如事件回调,Timer回调内的。这些回调会被RunLoop创建好的AutoreleasePool环绕着。所以不需要显示的去创建AutoreleasePool。

事件响应

苹果注册了一个Source1(基于match port的)用来接收系统事件的,其回调函数是__IOHIDEventSystemClientQueueCallback()

当一个硬件事件(触摸/锁屏/摇晃)产生后,首先由IOKit.framework产生一个IOHIDEvent事件并由SpringBoard接收。SpringBoard只接收锁屏,触摸。加速,接近传感器等几种Event。随后用mach port转发给需要的App进程。刚才的回调会触发,并调用_UIApplicationHandleEventQueue()进行内部的分发。

_UIApplicationHandleEventQueue()会把IOHIDEvent处理并包装成UIEvent进行处理和分发。其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

手势识别

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

苹果注册了一个Observer监听beforeWaiting(Loop即将进入睡眠),其对应着一个回调。其内部会找到所有标记的手势,并执行对应的SEL。当有UIGestureGecognizer的变化(创建/销毁/状态改变) 这个回调都会进行相应的处理。

界面更新

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

苹果注册了一个Observe监听beforeWaiting(即将进入休眠)和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数.然后处理里面所有被标记的UIView/CALayer。

定时器

NSTimer其实就是CFRunLoopRef.他们之间是toll-free bridged 的。一个Timer注册到RunLoop之后。RunLoop会为其重复时间注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会非常准确的时间点回调这个Time。Timer有个宽容度。标记了时间点到后,存在多大的误差。

PerformSelector

当调用NSObject的performSelector:afterDelay:后,实际会在当前线程创建一个Timer 添加到当前线程的RunLoop中,如果当前线程没有RunLoop则会执行失败。

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

上一篇下一篇

猜你喜欢

热点阅读