iOS App启动原理

序言:

如果让我说在哪家公司待过最开心?肯定是跟炽哥一起共事的公司里最快乐。关键在于炽哥,我们经常会在下班的时候讨论iOS技术上的问题,包括一些原理方面,当时特么想从App的启动原理开始说起,然后分享出来,可惜好景不长,创业公司总是活不久~那么现在我又重拾初心,让我一个无名小卒来从App启动原理开始讲起吧!🤡

一、易忽视的main.m文件

    相信仍然有部分开发者从来未进main.m文件瞧瞧,也不知道这个文件干嘛用的,其实啊main.m文件的作用意义非凡,那就让我们进来看看吧!

创建一个新工程,进入main.m文件上,然后按住option点击 UIApplicationMain
这里告诉我们,UIApplicationMain 会有如下几个步骤执行:

  1. 创建一个application 对象
  2. 设置application的代理(appDelegate)
  3. 创建一个事件循环(runloop)
  4. 读info.plist 文件

《iOS App启动原理》

创建一个application 对象,这是毫无疑问的。但是设置application的代理,我们可能不明白,此时进入AppDelegate.m看看

因为一个新的工程没有删除Main.storyboard, 所以AppDelegate.m又像如下代码顶替:
#import "AppDelegate.h"
#import "ViewController.h"

@interface AppDelegate ()

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    
    self.window.rootViewController = [[ViewController alloc] init];
    [self.window makeKeyAndVisible];
    self.window.backgroundColor = [UIColor whiteColor];
    
    return YES;
}

这里需要拓展下+ (void)load;方法:在一个程序开始运行之前(在main函数开始执行之前),load函数就会开始被执行。
做个小实验,在AppDelegate.m 和 ViewController.m分别都添加这三行代码:

+ (void)load {
    NSLog(@"当前的类: %@", [self class]);
}

并且在main.m添加:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"我是main.m");
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

程序运行,结果输出:

《iOS App启动原理》

为什么执行顺序是这个样子的呢?

《iOS App启动原理》 一张图解释

我们都知道load函数先于main执行,所以main.m在上图Complie Sources的位置暂时无关紧要,但是此时我们把AppDelegate.m拖拽下:

《iOS App启动原理》

执行:

《iOS App启动原理》

实验效果再次证明了,main函数晚于load函数执行。并且load的执行顺序由Compile Sources的顺序决定!

说到main函数会读取info.plist文件,它首先读的就是LaunchScreen文件,其次读到Main.storyboard,如果没有storyboard,会在AppDelegate找windows的rootViewController

二、维持App生命的RunLoop
RunLoop 的概念

基本的概念可以耐心的看YYKit的作者写的文章 《深入理解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。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

所以,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。(没有特殊说明,基础知识采摘大神ibireme)

RunLoop其内部代码整理删减如下

一张图先了解下

《iOS App启动原理》

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

/// 用DefaultMode启动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    /// 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode里没有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
    /// 1. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
 
            /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
            /// 4. RunLoop 触发 Source0 (非port) 回调。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
 
            /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
            /// • 一个基于 port 的Source 的事件。
            /// • 一个 Timer 到时间了
            /// • RunLoop 自身的超时时间到了
            /// • 被其他什么调用者手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
 
            /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
            /// 收到消息,处理消息。
            handle_msg:
 
            /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            /// 9.2 如果有dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            /// 执行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
 
            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 进入loop时参数说处理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出传入参数标记的超时时间了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部调用者强制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一个都没有了
                retVal = kCFRunLoopRunFinished;
            }
            
            /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
        } while (retVal == 0);
    }
    
    /// 10. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
简单说下RunLoop的Mode

Mode有若干个, 但是系统默认注册了5个Mode:

ModeNamedescription
kCFRunLoopDefaultModeApp的默认 Mode,通常主线程是在这个 Mode 下运行的。
UITrackingRunLoopMode界面跟踪 Mode(也叫UI Mode),用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
UIInitializationRunLoopMode在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
GSEventReceiveRunLoopMode接受系统事件的内部 Mode,通常用不到。
kCFRunLoopCommonModes这是一个占位的 Mode。

一张图说下:

《iOS App启动原理》

Mode其实大有拳脚施展,比如点击验证码获取倒计时 并滑动了屏幕,此时验证码倒计时还会继续倒计时吗?再比如我们刷新UITableView,里面控件都是需要在主线程使用,为了不造成卡顿,此时observer也有文章可用。。。等等许多。。。

NameDescription
CFRunLoopSourceRefCFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
• Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
• Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
CFRunLoopTimerRefCFRunLoopTimerRef是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRefCFRunLoopObserverRef是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。

CFRunLoopObserverRef可以观测的时间点有以下几个:

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
};
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
  1. 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

  2. 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

  3. 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

好比如我们在ViewController.m添加如下代码

- (void)test {
        char autoreleaseActive = 0xa0;
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), autoreleaseActive, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    
            if (activity == kCFRunLoopEntry ) {
                NSLog(@"kCFRunLoopEntry");
            }
            if (activity == kCFRunLoopBeforeTimers ) {
                NSLog(@"kCFRunLoopBeforeTimers");
            }
            if (activity == kCFRunLoopBeforeSources ) {
                NSLog(@"kCFRunLoopBeforeSources");
            }
            if (activity == kCFRunLoopBeforeWaiting ) {
                NSLog(@"kCFRunLoopBeforeWaiting");
            }
            if (activity == kCFRunLoopAfterWaiting ) {
                NSLog(@"kCFRunLoopAfterWaiting");
            }
    
        });
    
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
}

然后打断点调试

《iOS App启动原理》


_wrapRunLoopWithAutoreleasePoolHandler添加

《iOS App启动原理》

然后执行,运行

《iOS App启动原理》

RunLoop在没事情做的时候会进入睡眠状态,并且会不断询问状态,从而提高执行效率

三、Autoreleasepool了解下

我们前面所看到的main.m的@autoreleasePool有什么意义呢?
现在我们在ViewController.m敲:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    for (int i = 0; i < 1024*1024*100; i++) {
           NSString *str = [NSString stringWithFormat:@"超人打怪兽啊啊啊啊啊啊"];
    }
}

运行后,查看内存状况让我们吓一跳:

《iOS App启动原理》

但是我们添加了autoreleasepool情况之后,内存合理没有出现飙升状况

- (void)viewDidLoad {
    [super viewDidLoad];
    
    for (int i = 0; i < 1024*1024*100; i++) {
        @autoreleasepool {
            NSString *str = [NSString stringWithFormat:@"超人打怪兽啊啊啊啊啊啊"];
        };
    }
}

《iOS App启动原理》

更多的RunLoop介绍、原理解析、源代码等,请参考文献
参考文献:

《深入理解RunLoop》 ———- ibireme
《苹果开源的CFRunLoopRef》
《CoreFoundation 源码下载》

    原文作者:超人猿
    原文地址: https://www.jianshu.com/p/fad1130b1444
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞