Unit 4

来源:互联网 发布:mac为什么会全灭 编辑:程序博客网 时间:2024/06/07 13:02

Runloop与线程有什么关系?

Runloop的概念

一般来讲,线程只能线性地执行任务,执行完成后线程就会退出。如果我们需要某种机制来构成一个闭环,让线程能随时处理事件但并不退出,通常的代码逻辑就会是这样的:

function loop() {    initialize();    do {        var message = get_next_message();        process_message(message);    } while (message != quit);}

这种模型通常被称为Event Loop,在macOSiOS中就是Runloop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

Runloop实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

它们之间的关系

在iOS中,每个线程(包括主线程)都有一个Runloop对象与之对应,在主线程中Runloop是默认启动的,因为所有关于UI的更新必须放在主线程完成,它是在主函数中被创建启动的:

int main(int argc, char * argv[]) {   @autoreleasepool {       return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));   }}

这就解释了为什么应用程序能够随时响应用户的交互(故障状态除外)。对于其他线程来说,Runloop默认是没有启动的。因为通常多线程任务是确定的,不需要用户交互的,当然你也可以手动配置和启动来实现线程更加复杂的功能。

Runloop准备休眠之前,会释放旧的自动释放池并创建新的。当然在Runloop即将退出时,就会释放所有的自动释放池而不会在创建新的。

以上林林总总,Runloop与线程的关系可归结为一句话:Runloop是主线程任务执行的基石,是其他线程扩展功能必要条件。

以+ scheduledTimerWithTimeInterval…的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

原因

Runloop只能在一种模式(Mode)下运行,模式主要用来指定事件在运行循环中的优先级,共有如下几种:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态;
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合;
  • UITrackingRunLoopMode:ScrollView滑动时;
  • UIInitializationRunLoopMode:启动时。

只有前两种模式是公开提供的,如果要更改Runloop的模式,必须退出当前的Runloop,更改模式后重新启动。基于这样的一个机制,为了保证UIScrollView的滑动顺畅,在UIScrollView实例滚动时(滚动结束后,自动回置原态),Runloop将更改模式为UITrackingRunLoopMode。而用+ scheduledTimerWithTimeInterval:target:selector: userInfo:repeats:方法触发的定时器将被添加到NSDefaultRunLoopMode模式下的Runloop,不同模式下的Runloop分别管理着自己的若干个Source/Timer/Observer,这便导致了定时器的暂停回调。

解决办法

换一种方式初始化定时器并指定加入Runloop的模式,如下:

_timer = [NSTimer timerWithTimeInterval:3.0f target:self selector:@selector(changeViewBackgroundColor:) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];

以下是Apple官方文档对这个模式的说明:

This is a configurable group of commonly used modes. Associating an input source with this mode also associates it with each of the modes in the group. For Cocoa applications, this set includes the default, modal, and event tracking modes by default. Core Foundation includes just the default mode initially.

这是一个可配置的常用模式组。将输入源与此模式联系起来也可以将其与该组中的每个模式联系起来。对于Cocoa applications,默认情况下,此集合包括默认、模态和事件跟踪模式。Core Foundation仅仅包含了默认模式的初始化。

Apple是如何实现autoreleasepool?

autoreleasepool是以一个栈的形式实现的,主要通过以下三个函数完成:

  • objc_autoreleasepoolPush
  • objc_autoreleasepoolPop
  • objc_autorelease

从函数名不难看出对应的逻辑:当一个对象加入到自动释放池时,调用objc_autoreleasepoolPush()压入;当一个对象要被销毁时,调用objc_autorelease释放对象并调用objc_autoreleasepoolPop()将该对象弹出自动释放池。

在block内如何修改block外部变量?为什么如此就可修改?

问题的解决是很简单的,即在外部变量声明前加__block(双下划线)关键字修饰。然而当你遇到这个问题时,你要小心,因为面试官想要的答案不可能这么浅显,你需要给出理由。

block从本质上可以归入函数范畴,其内部的代码是通过函数指针来指向的。block有三种类型:NSGlobalBlockNSStackBlockNSMallocBlock,更详尽的信息可以在这篇文章里找到。对于有访问外部变量且能通过block名就能调用的block属于第二类(勘误:第三类),代码段存储在堆内存中。而局部变量(__block只能修饰局部变量,但block内部除了修改外部的局部变量,还可以修改类的成员变量和属性,因为它们是存放在堆中的。)的存储空间是在栈里的。所以说当局部变量进入到block中时,就涉及到了作用域变换问题。以下是一系列测试代码:

//以这种方式获取地址,最终结果是字符串在堆内存常量区中的地址。无疑,它们是相同的。__block NSString *str = @"str";NSLog(@"%p", str);void (^test)(void) = ^() {    NSLog(@"%p", str);    str = @"";};test();//将获取地址换成下面的形式,此时获取的就是真正意义上局部变量str的地址NSLog(@"%p", &str);//打印结果依次是:// 0x7fff5022b8f8  栈区地址// 0x608000055238  堆区地址//对于类来说,对象的内存是分配在堆中的,它从类获得的属性和成员变量自然也是如此,所以在block中可以读取或修改//...@property (strong, nonatomic) NSNumber *age;//.._age = @0;NSLog(@"%p", _age);void (^test)(void) = ^() {    NSLog(@"%p", _age);    _age = 1;};//打印结果://0xb000000000000002//0xb000000000000002//换成如下语句获取地址,结果依然相同NSLog(@"%p", &_age);//打印结果://0x7f805dd07168//0x7f805dd07168

现在就可以总结__block关键字产生了什么效果:__block修饰的局部变量进入到block内部时,系统会将局部变量复制到堆区,且以后对它的访问都是对堆区的这个局部变量进行操作。

从两者的内存地址也可以得出相同的结论:iOS的一个进程栈区内存只有1M,Mac也只有8M,而前后的内存地址偏移量远远大于这个值,故block内部访问的被__block修饰的局部变量是位于堆区的。

其实,在堆区内读取外部局部变量是读取该变量所指向堆内存中实际存放的值,而上面说论述的这种block自然可以合法读取此变量的值,因为它们位于同一作用域。而想要修改该变量的值,就必须通过该局部变量在栈区的内存地址以指针指向的方式来修改,__block修饰就是为了让堆区内有这样的一个指针指向从而合法修改变量值。这就好比你引用别人的论文需要贴出原文的出处,而你想要修改别人的论文你就必须通过和原作者协商的方式来达成目的,最后由原作者发表声明并修改该论文,但如果你有原作者修改论文的授权(姑且认为这样可行)就另当别论了。

GCD的队列(dispatch_queue_t)分哪两种类型?

  • 串行队列: Serial Dispatch Queue
  • 发队列: Concurrent Dispatch Queue

参考并整理自:

@微博@iOS程序犭袁:iOSInterviewQuestions
sunnyxx:招聘一个靠谱的iOS
CocoaChina:深入理解Runloop

0 0
原创粉丝点击