iOS面试题(二)
消息发送和转发机制,SEL和IMP
消息发送转载自黄龙辉消息发送和消息转发机制
- 在Objective-C中,使用对象进行方法调用是一个消息发送的过程(Objective-C采用动态绑定机制,所以调用的方法直到运行期才能确定),例如:
id returnValue = [someObject messageName:parameter]; 其中,someObject是消息的接受者。messageName为选择子,选择子与参数合起来叫做消息。当编译器看到消息后,会将其转换成一条标准的C函数调用objc_msgSend。void objc_msgSend(id self,SEL cmd,...)。先去cache列表。cache列表没有的话。去method_list方法调度列表中去查找。以sel为key去查找对应的实现。如果仍然没有的话,则通过super指针去它的父类去寻找
尾部调用优化:一般情况下。方法内部调用另外一个方法,就会把方法的内部变量,返回地址等信息压人栈中,以便另外一个方法调用结束后,直接调用,比如A->B->C->D这样的话,就需要往栈里面压入很多信息。这样有可能发生栈溢出。为了一定程度避免这种情况,当方法是最后一步执行另外一个方法的时候。编译器会进行尾部调用优化。即不保留外层方法的信息(因为返回地址、内部变量等信息都不会再用到了),直接用内层方法的调用记录,取代外层方法的调用记录。
-
消息转发
- resolveInstanceMethod或者+ (BOOL)resolveClassMethod:(SEL)sel 如果返回YES则可以相应方法。
- 如果上面返回NO,则继续进行下一层。
- (id)forwardingTargetForSelector:(SEL)aSelector。
返回一个可以处理这个消息的对象。 - 若第二步返回nil,则进入消息转发的第三步。调用
- (void)forwardInvocation:(NSInvocation *)anInvocation
这个方法选择一个可以相应这个SEL的对象。anInvocation invocationWithTarget:id. - 如果最后仍然没法处理该方法,则调用doesNotRecognizeSelector抛出异常。
-
SEL类型
Objective-C在编译的时候 会根据方法的名字。生成一个用来区别这个方法的唯一的一个ID。这个ID就是SEL类型。我们要注意,只要方法的名字和参数相同,那么他们的ID就是相同的。也就是说不管超类还是子类。不管有没有超类和子类的关系,只要名字相同那么ID就是一样的。
从效率上来看,执行的时候不是通过方法的名字而是方法ID也就是一个整数去查找和匹配字符串要快得多。所以这样可以在某种程度上提高效率。
-
IMP类型
说白了就是方法的实现。我们获取到这个函数指针之后,就意味着我们取得了执行的时候的这段方法的代码的入口,这样我们就可以想普通的C函数一样使用这个函数指针。我们也可以把这个IMP指针做为参数传递到其他的方法中。或者实例变量里面。从而获得极大的动态性。我们获得了动态性,但是付出 的代价就是编译器不知道我们要执行哪一个方法所以在编译的时候不会替我们找出错误,我们只 有执行的时候才知道,我们写的函数指针是否是正确的
获取当前方法的IMP
IMP imp = class_getMethodImplementation([self class], _cmd);
谈一下对RunLoop的理解
-
model的主要作用是什么
model主要用来指定事件在运行循环中的优先级的,分为
- NSDefaultRunLoop(kCFRunLoopDefaultMode):默认,空闲状态
- NSTrackingRunLoopMode: ScrollView滚动时的
- UIInitializationRunLoopMode 启动时的
- NSRunLoopCommonModes Model的集合可以理解为Default和Tracking的集合。
苹果公开提供的 Mode 有两个:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
NSRunLoopCommonModes(kCFRunLoopCommonModes)
- 猜想runloop内部是如何实现的
int main(int argc,char*argc[]){
while(AppIsRunning){
//睡眠状态等待唤醒事件
let whoWakesMe = AwakeUpEvent()
id event = GetEvent(whoWakesMe);
AppIsRunning = HandEvnet(event);
}
}
下面的RunLoop理解摘自一只魔法师的工坊
-
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与线程的关系
首先iOS开发遇到两个线程对象。pthread_t 和NSThread对象。NSThread是基于pthread_t的封装。pthread_t 与NSThread肯定是一一对应的。CFRunLoop是基于pthread来管理的。苹果不支持直接创建RunLoop,他提供了2个自动获取的函数。CFRunLoopGetMain(),CFRunLoopGetCurrent()。
线程和RunLoop之间的关系是一一对应的,其关系保存在全局的Dictionary里。线程刚创建出来的时候没有RunLoop.如果你不主动获取,那它一直都不会有,换句话就是RunLoop对象是懒加载的。RunLoop的创建是发生在第一次获取时。RunLoop的销毁发生在线程结束时。你只能在一个线程的内部获取其RunLoop(主线程外)。
- RunLoop对外的接口
在CoreFoundation里面关于RunLoop有五大类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
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就是一个函数。其内部是一个do-while循环,你调用CFRunLoopRun()时。线程会一直停留在这个循环里。直到超时或者被手动停止,该函数才会返回。
RunLoop的底层实现
RunLoop的核心是基于mach port的,其休眠时调用的函数是mach_msg()。我们先了解下OSX/iOS的系统
苹果官方将整个系统分为4个层次。
- 应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。
- 应用框架层即开发人员接触到的 Cocoa 等框架。
- 核心框架层包括各种核心框架、OpenGL 等内容。
- Darwin 即操作系统的核心
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:
kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
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 该方法也会失效