iOS面试题总结(二)

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_1.png

可以看到,实际上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:

  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 该方法也会失效

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • iOS 面试题总结(一) 101. 修改下面的代码: typedef enum{ UserSex_Man,User...
    AlaricMurray阅读 1,665评论 0 6
  • Runloop是iOS和OSX开发中非常基础的一个概念,从概念开始学习。 RunLoop的概念 -般说,一个线程一...
    小猫仔阅读 978评论 0 1
  • 转载:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling阅读 1,434评论 0 13
  • http://www.cocoachina.com/ios/20150601/11970.html RunLoop...
    紫色冰雨阅读 828评论 0 3
  • 没有人会直接给你荣华富贵,只有送你机会和平台,现在这个时代什么都不缺,缺的只有像鹰一样的眼光,像狼一样的精神,像熊...
    狼之恋阅读 225评论 0 0