美文网首页
iOS App启动原理解析和启动布局

iOS App启动原理解析和启动布局

作者: 羽裳有涯 | 来源:发表于2018-11-20 11:44 被阅读37次

    一 、IOS开发APP启动原理

    main()函数是整个程序的入口,在程序启动之前,系统会调用exec()函数。在Unix中exec和system的不同在于,system是用shell来调用程序,相当于fork+exec+waitpid,fork函数创建子进程后通常都会调用exec函数来执行一个新程序;而exec是直接让你的程序代替原来的程序运行。

    system是在单独的进程中执行命令,完了还会回到你的程序中。而exec函数是直接在你的进程中执行新的程序,新的程序会把你的程序覆盖,除非调用出错,否则你再也回不到exec后面的代码,也就是当前的程序变成了exec调用的那个程序了。

    • UNIX提供了6种不同的exec函数供我们使用。
    #include(unistd.h)(因识别问题,本行用圆括号替换尖括号)
    
    int execl(const char *pathname,const char *arg0,... /*(char *)0 */);
    
    int execv(const char *pathname,char *const argv[]);
    
    int execle(const char *pathname,const char *arg0,... /*(char *)0,char *const envp[]*/);
    
    int execve(const char *pathname,char *const argv[],char *const envp[]);
    
    int execlp(const char *filename,const char *arg0,... /*(char *)0 */);
    
    int execvp(cosnt char *filename,char *const argv[]);
    
    
    • 通过分析我们发现,含有l和v的exec函数的参数表传递方式是不同的。含有e结尾的exec函数会传递一个环境变量列表。含有p结尾的exec函数取的是新程序的文件名作为参数,而其他exec函数取的是新程序的路径。

    • 如果函数出错则返回-1,若成功则没有返回值。其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

    • exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Unix下可执行的脚本文件。

    二、iOS系统架构

    Mac系统是基于Unix内核的图形化操作系统,Mac OS和iOS系统架构的对比分析发现,Mac OS和iOS的系统架构层次只有最上面一层不同,Mac是Cocoa框架,而iOS是Cocoa Touch框架,其余的架构层次都是一样的。

    6083675-70e2cd5edfad6425.jpg

    Core OS是用FreeBSD和Mach所改写的一个名叫Darwin的开放原始码操作系统,是开源、符合POSIX标准的一个Unix核心。这一层包含并提供了整个iPhone OS的一些基础功能,比如:硬件驱动,内存管理,程序管理,线程管理(POSIX),文件系统,网络(BSD Socket),以及标准输入输出等等,所有这些功能都会通过C语言的API来提供。

    6083675-bb7505ac75992fe1.png

    核心OS层的驱动提供了硬件和系统框架之间的接口。然而,由于安全的考虑,只有有限的系统框架类能访问内核和驱动。iPhone OS提供了许多访问操作系统低层功能的接口集,iPhone应用通过LibSystem库来访问这些功能,这些接口集有线程(POSIX线程)、网络(BSD sockets)、文件系统访问、标准I/O、Bonjour和DNS服务、现场信息(Locale Information)、内存分配和数学计算等。

    Core Services在Core OS基础上提供了更为丰富的功能,它包含了Foundation.Framework和Core Foundation.Framework,之所以叫Foundation,就是因为它提供了一系列处理字符串,排列,组合,日历,时间等等的基本功能。

    Foundation是属于Objective-C的API,Core Fundation是属于C的API。另外Core servieces还提供了如Security(用来处理认证,密码管理,安全性管理等),Core Location,SQLite和Address Book等功能。

    核心基础框架(CoreFoundation.framework)是基于C语言的接口集,提供iPhone应用的基本数据管理和服务功能。该框架支持Collection数据类型(Arrays、Sets等)、Bundles、字符串管理、日期和时间管理、原始数据块管理、首选项管理、URL和Stream操作、线程和运行循环(Run Loops)、端口和Socket通信。

    核心基础框架与基础框架是紧密相关的,它们为相同的基本功能提供了Objective-C接口。如果开发者混合使用Foundation Objects和Core Foundation类型,就能充分利用存在两个框架中的"toll-free bridging"技术(桥接)。toll-free bridging使开发者能使用这两个框架中的任何一个的核心基础和基础类型。

    三、静态链接库与动态链接库

    iOS中的相关文件有如下几种:Dylib,动态链接库(又称DSO或DLL);Bundle,不能被链接的Dylib,只能在运行时使用dlopen()加载,可当做macOS的插件。Framework,包含Dylib以及资源文件和头文件的文件夹。

    动态链接库是一组源代码的模块,每个模块包含一些可供应用程序或者其他动态链接库调用的函数,在应用程序调用一个动态链接库里面的函数的时候,操作系统会将动态链接库的文件映像映射到进程的地址空间中,这样进程中所有的线程就可以调用动态链接库中的函数了。动态链接库加载完成后,这个时候动态链接库对于进程中的线程来说只是一些被放在地址进程空间附加的代码和数据,操作系统为了节省内存空间,同一个动态链接库在内存中只有一个,操作系统也只会加载一次到内存中。

    因为代码段在内存中的权限都是为只读的,所以当多个应用程序加载同一个动态链接库的时候,不用担心应用程序会修改动态链接库的代码段。当线程调用动态链接库的一个函数,函数会在线程栈中取得传递给他的参数,并使用线程栈来存放他需要的变量,动态链接库函数创建的任何对象都为调用线程或者调用进程拥有,动态链接库不会拥有任何对象。如果动态链接库中的一个函数调用了VirtualAlloc,系统会从调用进程的地址空间预定地址,即使撤销了对动态链接库的映射,调用进程的预定地址依然会存在,直到用户取消预定或者进程结束。

    静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都全部被直接包含在最终生成的包文件中了。但是若使用动态链接库,该动态链接库不必被包含在最终包里,包文件执行时可以“动态”地引用和卸载这个与安装包独立的动态链接库文件。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。

    Linux中静态函数库的名字一般是libxxx.a;利用静态函数库编译成的文件比较大,因为整个函数库的所有数据都会被整合进目标代码中。编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。当然这也会成为他的缺点,因为如果静态函数库改变了,那么你的程序必须重新编译。

    动态函数库的名字一般是libxxx.so,相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。

    iOS开发中静态库和动态库是相对编译期和运行期的。静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要载入静态库。而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。

    iOS中静态库可以用.a或.Framework文件表示,动态库的形式有.dylib和.framework。系统的.framework是动态库,一般自己建立的.framework是静态库。

    .a是一个纯二进制文件,.framework中除了有二进制文件之外还有资源文件。.a文件不能直接使用,至少要有.h文件配合。.framework文件可以直接使用,.a + .h + sourceFile = .framework。
    动态库的一个重要特性就是即插即用性,我们可以选择在需要的时候再加载动态库。如果不希望在软件一启动就加载动态库,需要将

    1Targets-->Build Phases-->Link Binary With Libraries

    中*.framework对应的Status由默认的Required改成Optional;或者将xx.framework从Link Binary With Libraries列表中删除。

    可以使用dlopen加载动态库,动态库中真正的可执行代码为xx.framework/xx文件。
    -(IBAction)useDlopenLoad:(id)sender  {
    
        NSString *documentsPath =[NSString stringWithFormat:@"%@/Documents/xx.framework/xx",NSHomeDirectory()];
    
        [self dlopenLoadlib:documentsPath];
    
        }
    
    -(void)dlopenLoadlib:(NSString *)path {
    
        libHandle = NULL;
    
        libHandle = dlopen([path cStringUsingEncoding:NSUTF8StringEncoding],RTLD_NOW);
    
        if(libHandle == NULL){
    
        char *error = dlerror();
    
        NSLog(@"dlopen error: %s",error);
    
        } else {
    
        NSLog(@"dlopen load framework success.");
        }
        }
    
        也可以使用NSBundle来加载动态库,实现代码如下:
    
    -(IBAction)useBundleLoad:(id)sender  {
    
        NSString *documentsPath =[NSString stringWithFormat:@"%@/Documents/xx.framework",NSHomeDirectory()];
    
        [self bundleLoadlib:documentsPath];
    
        }
    
     -(void)bundleLoadlib:(NSString *)path  {
    
        _libPath = path;
    
        NSError *err = nil;
    
        NSBundle *bundle =[NSBundle bundleWithPath:path];
    
        if([bundle loadAndReturnError:&err]){
    
        NSLog(@"bundle load framework success.");
    
        } else {
    
        NSLog(@"bundle load framework err:%@",err);
    
        }
    
    }
    

    可以为动态库的加载和移除添加监听回调,github上有一个完整的示例代码,从中可以发现,一个工程软件启动的时候会加载多达一百二十多个动态库,即使是一个空白的项目。

    但是,需要注意的一点是,不要在初始化方法中调用dlopen(),对性能有影响。因为dyld在App开始前运行,由于此时是单线程运行所以系统会取消加锁,但dlopen()开启了多线程,系统不得不加锁,这就严重影响了性能,还可能会造成死锁以及产生未知的后果。所以也不要在初始化器中创建线程。

    据说,iOS现在可以使用自定义的动态库,低版本的需要手动的使用dlopen()加载。动态库上架会有一些审核的规则,如不要把x86/i386的包和arm架构的包lipo在一起使用。如:

    1lipo –create Release-iphoneos/libiphone.a Debig-iphonesimulator/libiphone.a –output libiphone.a

    如此便将模拟器和设备的静态库文件合并成一个文件输出了。

    上海有家公司有过一个成功上架的案例,但我没有在这方面做过测试,至于能不能过审,还需要验证。

    四、dylib加载调用

    基于上面的分析,在exec()时,系统内核把应用映射到新的地址空间,每次起始位置都是随机的。然后使用dyld加载dylib文件(动态链接库),dyld在应用进程中运行的工作就是加载应用依赖的所有动态链接库,准备好运行所需的一切,它拥有和应用一样的权限。

    加载Dylib时,先从主执行文件的header中获取需要加载的所依赖动态库的列表,从中找到每个dylib,然后打开文件读取文件起始位置,确保它是Mach-O文件(针对不同运行时可执行文件的文件类型)。然后找到代码签名并将其注册到内核。

    应用所依赖的dylib文件可能会再依赖其他dylib,因此动态库列表是一个递归依赖的集合。一般应用会加载100到400个dylib文件,但大部分都是系统dylib,它们会被预先计算和缓存起来,加载速度很快。但加载内嵌(embedded)的dylib文件很占时间,所以尽可能把多个内嵌dylib合并成一个来加载,或者使用static archive。

    在加载所有的动态链接库之后,它们只是处在相互独立的状态,代码签名使得我们不能修改指令,那样就不能让一个dylib调用另一个dylib。通过fix-up可以将它们结合起来,dyld所做的事情就是修正(fix-up)指针和数据。Fix-up有两种类型,rebasing(在镜像内部调整指针的指向)和binding(将指针指向镜像外部的内容)。

    因为dylib之间有依赖关系,所以动态库中的好多操作都是沿着依赖链递归操作的,Rebasing和Binding分别对应着recursiveRebase()和recursiveBind()这两个方法。因为是递归,所以会自底向上地分别调用doRebase()和doBind()方法,这样被依赖的dylib总是先于依赖它的dylib执行Rebasing和Binding。

    Rebaing消耗了大量时间在I/O上,在Rebasing和Binding前会判断是否已经预绑定。如果已经进行过预绑定(Prebinding),那就不需要Rebasing和Binding这些Fix-up流程了,因为已经在预先绑定的地址加载好了。

    Binding处理那些指向dylib外部的指针,它们实际上被符号(symbol)名称绑定,是一个字符串。dyld需要找到symbol对应的实现,在符号表里查找时需要很多计算,找到后会将内容存储起来。Binding看起来计算量比Rebasing更大,但其实需要的I/O操作很少,因为之前Rebasing已经替Binding做过了。Objective-C中有很多数据结构都是靠Rebasing和Binding来修正(fix-up)的,比如Class中指向超类的指针和指向方法的指针。

    参考:iOS APP启动原理及视图~详解

    五、 App启动原理和启动过程

    1、程序启动原理

    • 1.1、main函数中执行了一个UIApplicationMain这个函数UIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName);

    • 1.2、argc、argv:直接传递给UIApplicationMain进行相关处理即可 principalClassName:指定应用程序类名(app的象征),该类必须是UIApplication(或子类)。如果为nil,则用UIApplication类作为默认值
      delegateClassName:指定应用程序的代理类,该类必须遵守UIApplicationDelegate协议

    • 1.3、UIApplicationMain函数会根据principalClassName创建UIApplication对象,根据delegateClassName创建一个delegate对象,并将该delegate对象赋值给UIApplication对象中的delegate属性

    • 1.4、接着会建立应用程序的Main Runloop(事件循环),进行事件的处理(首先会在程序完毕后调用delegate对application:didFinishLaunchingWithOptions:方法)

    • 1.5、程序正常退出时UIApplicationMain函数才返回


      947547-20160429113512048-1429873276.png
    #import <UIKit/UIKit.h>
    #import"AppDelegate.h"int main(int argc, char * argv[])
    {
        @autoreleasepool {
            // return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
            // return UIApplicationMain(argc, argv, @"UIApplication", NSStringFromClass([AppDelegate class]));/*
             argc: 系统或者用户传入的参数个数
             argv: 系统或者用户传入的实际参数
             1.根据传入的第三个参数创建UIApplication对象
             2.根据传入的第四个产生创建UIApplication对象的代理
             3.设置刚刚创建出来的代理对象为UIApplication的代理
             4.开启一个事件循环
             */return UIApplicationMain(argc, argv, @"UIApplication", @"AppDelegate");
        }
    } 
    

    2、程序启动的完整过程

    • 从main文件开始说起程序启动分为两类:1.有storyboard 2.没有storyboard
    (一)有storyboard情况下:
    1.main函数
    
    2.UIApplicationMain
        - 创建UIApplication对象
        - 创建UIApplication的delegate对象
    
    3.根据Info.plist获得最主要storyboard的文件名,加载最主要的storyboard(有storyboard)
        - 创建UIWindow
        - 创建和设置UIWindow的rootViewController
        - 显示窗口
    
    二)没有storyboard情况下:
    1.main函数
    
    2.UIApplicationMain
        - 创建UIApplication对象
        - 创建UIApplication的delegate对象
    
    3.delegate对象开始处理(监听)系统事件(没有storyboard)
        - 程序启动完毕的时候, 就会调用代理的application:didFinishLaunchingWithOptions:方法
        - 在application:didFinishLaunchingWithOptions:中创建UIWindow
        - 创建和设置UIWindow的rootViewController
        - 显示窗口
    
    

    相关文章

      网友评论

          本文标题:iOS App启动原理解析和启动布局

          本文链接:https://www.haomeiwen.com/subject/loptqqtx.html