iOS面试题iOS开发面试题

runloop、自动释放池、线程、GCD

2019-06-12  本文已影响0人  boy丿log

runloop

runloop是用来处理事件的循环。NSRunloop是CFRunloop的封装,CFRunloop是一套C接口,源码地址
runloop处理消息的流程是“接收消息->恢复活跃->处理消息->进入休眠”。

runloop作用

runloop的构成

它的结构关系如下

struct __CFRunLoop {
     pthread_t _pthread;//线程
    CFMutableSetRef _commonModes;     // commonModes下的两个mode(kCFRunloopDefaultMode和UITrackingMode)
    CFMutableSetRef _commonModeItems; // 在commonModes状态下运行的对象(例如Timer)
    CFMutableSetRef _modes;           // 运行的所有模式(CFRunloopModeRef类)
    CFRunLoopModeRef _currentMode;//在当前loop下运行的mode
    ...
};

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

CFRunLoopModeRef

一个RunLoop包含了多个Mode,每个Mode又包含了若干个Source/Timer/Observer。每次调用 RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换 Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同Mode中的Source/Timer/Observer,让其互不影响。下面是5种Mode。

CommonModes

一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

sources0和_sources1

(1)以前的分法
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources
(2)现在的分法:
Source0 : 触摸事件,PerformSelectors,非基于Port的
Source1 : 基于Port的线程间通信,基于Port的

_timers

定时执行的定时器,底层基于使用mk_timer实现,受RunLoop的Mode影响(GCD的定时器不受RunLoop的Mode影响),当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。如果线程阻塞或者不在这个Mode下,触发点将不会执行,一直等到下一个周期时间点触发。

timer和source1(也就是基于port的source)可以反复使用,比如timer设置为repeat,port可以持续接收消息,而source0在一次触发后就会被runloop移除。

_observers

添加监听的方法:


添加监听

监听返回的状态:

enum CFRunLoopActivity {
    kCFRunLoopEntry              = (1 << 0),    // 即将进入Loop   
    kCFRunLoopBeforeTimers      = (1 << 1),    // 即将处理 Timer        
    kCFRunLoopBeforeSources     = (1 << 2),    // 即将处理 Source  
    kCFRunLoopBeforeWaiting     = (1 << 5),    // 即将进入休眠     
    kCFRunLoopAfterWaiting      = (1 << 6),    // 刚从休眠中唤醒   
    kCFRunLoopExit               = (1 << 7),    // 即将退出Loop  
    kCFRunLoopAllActivities     = 0x0FFFFFFFU  // 包含上面所有状态  
};

runloop流程

runloop流程

runloop与线程

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

runloop与GCD

runloop与自动释放池

苹果在主线程 RunLoop 里注册了两个 Observer:
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入睡眠) 和 Exit(即将退出Loop),
BeforeWaiting(准备进入睡眠)时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

Runloop的作用

NSTimer

方式一

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

方式二

     NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
    [thread start];

- (void)newThread{
    @autoreleasepool{
        //在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode
        timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(incrementCounter:) userInfo: nil repeats:YES];
        //开始执行新线程的Run Loop,如果不启动run loop,timer的事件是不会响应的
        [[NSRunLoop currentRunLoop] run];
    }  
}

自动释放池

PerformSelecter...

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

事件响应

苹果注册了一个 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 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

UI更新

即准备进入睡眠和即将退出 loop 两个时间点,会调用函数更新 UI 界面.

当在操作 UI 时,某个需要变化的 UIView/CALayer 就被标记为待处理,然后被提交到一个全局的容器去,再在上面的回调执行时才会被取出来进行绘制和调整。

GCD

RunLoop 底层会用到 GCD 的东西,GCD 的某些 API 也用到了 RunLoop。如当调用了 dispatch_async(dispatch_get_main_queue(), block)时,主队列会把该 block 放到对应的线程(恰好是主线程)中,主线程的 RunLoop 会被唤醒,从消息中取得这个 block,回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 来执行这个 block

AFNetWorking 3.0以前的线程保活

子线程默认是完成任务后结束。当要经常使用子线程,每次开启子线程比较耗性能。此时可以开启子线程的 RunLoop,保持 RunLoop 运行,则使子线程保持不死。AFNetworking 基于 NSURLConnection 时正是这样做的,希望在后台线程能保持活着,从而能接收到 delegate 的回调。
这一点充分体现了:我们控制了runloop ,就是控制了app 的生死。

           /* 返回一个线程 */
+ (NSThread *)networkRequestThread {
        static NSThread *_networkRequestThread = nil;
        static dispatch_once_t oncePredicate;
        dispatch_once(&oncePredicate, ^{
            // 创建一个线程,并在该线程上执行下一个方法
            _networkRequestThread = [[NSThread alloc] initWithTarget:self
                                                            selector:@selector(networkRequestThreadEntryPoint:)
                                                              object:nil];
            // 开启线程
            [_networkRequestThread start];
        });
        return _networkRequestThread;
    }
/* 在新开的线程中执行的第一个方法 */
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        // 获取当前线程对应的 RunLoop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        // 为 RunLoop 添加 source,模式为 DefaultMode
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // 开始运行 RunLoop
        [runLoop run];
        / /或者
       //[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:4]];
    }
}

因为 RunLoop 启动前必须设置一个 mode,而 mode 要存在则至少需要一个 source / timer。所以上面的做法是为 RunLoop 的 DefaultMode 添加一个 NSMachPort 对象,虽然消息是可以通过 NSMachPort 对象发送到 loop 内,但这里添加的 port 只是为了 RunLoop 一直不退出,而没有发送什么消息。当然我们也可以添加一个超长启动时间的 timer 来既保持 RunLoop 不退出也不占用资源

AsyncDisplayKit

ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

监控系统卡顿

监控主线程状态,在一定时间内没有变化,就可判定为卡顿。这个会在之后的文章讲。

MachPort

MachPort的工作方式其实是将NSMachPort的对象添加到一个线程所对应的RunLoop中,并给NSMachPort对象设置相应的代理。在其他线程中调用该MachPort对象发消息时会在MachPort所关联的线程中执行相关的代理方法。

@interface DPMessageViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) UIAlertAction * ac;
@end

@implementation DPMessageViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSPort *port = [NSMachPort port];
    port.delegate = self;
    [[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];
    
    [NSThread detachNewThreadSelector:@selector(oooooo:) toTarget:[DPMessageViewModel new] withObject:port];
}

- (void)handlePortMessage:(NSPortMessage *)message{
    NSLog(@"子线程的消息%@", message);
    
}
@end
@interface DPMessageViewModel : NSObject<NSMachPortDelegate>
{
    NSPort *remotePort;
    NSPort *myPort;
}
@end

@implementation DPMessageViewModel

- (void)oooooo:(NSMachPort *)port{
    @autoreleasepool {
        remotePort = port;
        [[NSThread currentThread] setName:@"MyWorkerClassThread"];
         [[NSRunLoop currentRunLoop] run];
        myPort = [NSPort port];
        myPort.delegate = self;
        [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
        [self sendPortMessage];
       
      
    }
}
- (void)sendPortMessage{
     NSMutableArray *array  =[[NSMutableArray alloc]initWithArray:@[@"1",@"2"]];
     [remotePort sendBeforeDate:[NSDate date] msgid:100 components:array from:myPort reserved:0];
}
- (void)handlePortMessage:(NSPortMessage *)message{
    NSLog(@"接收到父线程的消息...\n");
}
@end

总结

参考

上一篇下一篇

猜你喜欢

热点阅读