动态库的使用

作者: wangzzzzz | 来源:发表于2018-06-05 16:26 被阅读130次

    前言

    说到动态库,就不得不提静态库。静态库可以看做是一个具有特定功能的代码块,如果app中引用了静态库,则在编译时会将静态库直接复制到app的可执行文件(也就是mach-o)中。
    使用静态库会导致mach-o文件过大,而mach-o文件直接影响app的启动时间和执行时占用的内存大小。

    为了减少mach-o文件的大小,需要用到动态库。当app中引用了动态库时,动态库并不会被复制到app的mach-o文件中,只有当动态库真正被用到时,才会去加载(加载到内存中)和链接(动态库可能引用了其他库)动态库,可能是在app启动时或者是运行时。

    目录
    1. 动态库和静态库的区别
    2. 创建动态库
    3. 使用动态库
      3.1. 添加为依赖库-启动时加载
      3.2. 运行时加载
    4. 注入动态库
    5. yololib

    1. 动态库和静态库的区别

    静态库的后缀名是以.a结尾,动态库的后缀名可以是.dylib.framework结尾,所有的系统库都属于动态库,在iOS中一般使用framework作为动态库。

    下面是apple官方的两张图,表示app启动后内存的使用情况,很形象的说明了静态库和动态库的区别

    使用静态库的app



    使用动态库的app


    在使用static linker链接app时,静态库会被完整的加载到app的mach-o文件(上图中的Application file)中,作为mach-o文件的一部分,而动态库不会被添加到mach-o文件中,这可以有效减少mach-o文件的大小。
    如果app将动态库作为它的依赖库,则在mach-o文件中会添加了一个动态库的引用;如果app在运行时动态加载动态库,则在mach-o文件中不会添加动态库的引用。

    在使用app时,静态库和动态库都会被加载到内存中。当多个app使用同一个库时,如果这个库是动态库,由于动态库是可以被多个app的进程共用的,所以在内存中只会存在一份;如果是静态库,由于每个app的mach-o文件中都会存在一份,则会存在多份。相对静态库,使用动态库可以减少app占用的内存大小。

    另外,使用动态库可以缩短app的启动时间。原因是,使用动态库时,app的mach-o文件都会比较小;app依赖的动态库可能已经存在于内存中了(其他已启动的app也依赖了这个动态库),所以不需要重复加载。

    2. 创建动态库

    上文提到过,动态库一般有两种,分别以.framework.dylib后缀结尾,通常把它们叫做Framework和Shared Library。Framework本质上是由Shared Library加上头文件header和其他资源文件打包得来的。

    下面以创建LibPersonFramework为例

    1. 创建一个新工程,选择iOS -> Cocoa Touch Framework

    2. 实现framework,并指定对外的头文件

    定义头文件LibPerson.h

    #import <Foundation/Foundation.h>
    
    @interface LibPerson : NSObject
    
    @property (nonatomic, copy) NSString *name ;
    
    - (void)watch;
    
    - (void)eat;
    
    @end
    

    指定LibPersonFramework.hLibPerson.h为对外的头文件

    指定framework的架构模式,这里选择了Generic iOS Device机型,然后build一下,就会创建一个通用mach-o文件,包含了arm64和arm_v7两种架构。如果选择了模拟器,会创建一个x86_64架构的mach-o文件。

    需要注意的是,App和它依赖的framework的架构必须兼容,也就是说,在创建可执行文件时,要么都是真机,要么都是模拟器。当然,也可以分别在真机和模拟器两种模式下创建framwork,然后使用lipo命令来将两个framework内部的同名mach-o文件合并成一个通用mach-o文件,这样,不管App是什么架构模式,都能正确使用这个framework了。

    3. 使用动态库

    使用动态库有两种方式,一种是将动态库添加为依赖库,这样会在工程启动时加载动态库,一种是使用dlopen在运行时加载动态库,这两种方式的区别在于加载动态库的时机。

    在iOS中一般使用第一种方法,第二种方式一般在mac开发中使用,如果在iOS中使用了这种方式,是不能上架到App Store的。

    3.1. 添加为依赖库-启动时加载

    创建一个新的工程DylibDemo,并引入LibPersonFramework.framework,在main.m文件中调用这个framework中的方法

    这个时候,app工程已经对LibPersonFramework.framework产生了依赖,对于系统framework,到这一步就可以了,因为系统framework已经被预先安装在iphone上了。对于自定义的framework,还需要通过下面一步来将framework复制到app的安装包中。

    最后运行一下,调用成功!

    2018-06-04 16:32:09.076551+0800 DylibDemo[1790:700462] wang is watching TV!
    2018-06-04 16:32:09.078597+0800 DylibDemo[1790:700462] wang is eating!
    
    3.2. 运行时加载

    在运行时加载动态库,是指不需要在工程中引入动态库,作为替代,在代码中使用dlopen()这个函数来加载动态库,在调用完成之后,需要调用相同次数的dlclose()函数来关闭动态库。
    除了dlopen()dlclose()以外,另外还有一个dlsym()函数来根据传入的symbol获取对应数据或函数的地址。在本例中,会使用runtime机制来代替dlsym()函数。(dlsym()一般是在c或c++中使用)

    1. 创建新工程DylibDemo-Runtime,添加被调用库的头文件LibPerson.h(这里不需要添加LibPersonFramework.framework)

    2. 在main.h文件中加载和调用LibPersonFramework.framework

    void loadWhenRunTime(){
        
    
        // Open the library.
        NSString *bundlePath = [[NSBundle mainBundle]pathForResource:@"LibPersonFramework" ofType:nil];
        void* lib_handle = dlopen([bundlePath UTF8String], RTLD_LOCAL);
        
        if (!lib_handle) {
            
            NSLog(@"[%s] main: Unable to open library: %s\n",
                  
                  __FILE__, dlerror());
            exit(EXIT_FAILURE);
        }
        
    
        Class class_person = objc_getClass("LibPerson");
        LibPerson *person = [class_person new];
        person.name = @"wang";
        [person watch];
        [person eat];
    
        // Close the library.
        if (dlclose(lib_handle) != 0) {
            
            NSLog(@"[%s] Unable to close library: %s\n",
                  __FILE__, dlerror());
            exit(EXIT_FAILURE);
            
        }
    }
    

    dlopen()函数需要传入两个参数path和mode,path表示动态库的mach-o文件的路径,mode中可以包含多个标识符,比如RTLD_LAZYRTLD_NOW表示动态库中的symbol什么时候被加载,RTLD_GLOBALRTLD_LOCAL表示symbol的可见性。(详情可通过终端命令man dlopen查看)

    上述代码中,path指定动态库是在生成的app包中,文件名为LibPersonFramework;mode的值是RTLD_LOCAL,表示在使用dlsym()函数时,只能通过dlopen()函数返回的handle来获取传入的symbol的地址,由于在例中并不会使用dlsym()函数,所以大可不必关注这个值。

    另外,在上述代码中还有一点需要注意的,在创建LibPerson类的对象时,不能直接使用LibPerson *person = [LibPerson new],如果这样做,程序会报如下编译错误:

    Undefined symbols for architecture arm64:
      "_OBJC_CLASS_$_LibPerson", referenced from:
          objc-class-ref in main.o
    ld: symbol(s) not found for architecture arm64
    

    这是因为在编译时,如果调用了[LibPerson new],编译器会去验证app的mach-o文件以及它依赖的动态库的mach-o文件中是否有这个类的定义。
    由于在编译时,程序还没有加载动态库LibPersonFramework,而程序只包含了LIbPerson类的头文件,并没有它对应的.m文件(编译器只会将.m文件编译到最终的mach-o文件中),所以编译器在app的mach-o文件以及它依赖的动态库中找不到LibPerson类的定义,然后编译器就报错了。

    从上述代码可以看出,在创建LibPerson类的对象时,程序中其实已经加载了LibPersonFramework,也就是说,在那个时候程序中已经有这个类的定义了。所以,上述代码中使用了下列代码来”欺骗“编译器。

      Class class_person = objc_getClass("LibPerson");
      LibPerson *person = [class_person new];
    
    3. 添加动态库LibPersonFramework文件

    首先build一下,生成app的包文件


    这个时候可能会报编译错误,说找不到LibPersonFramework,所以接下来就需要添加LibPersonFramework。
    在之前创建的LibPersonFramework.framework中,找到动态库LibPersonFramework


    找到app的包文件,鼠标右键点击显示包内容,然后将这个LibPersonFramework文件复制到这里


    4. 给动态库重签名

    这个时候运行一下,dlopen()函数会报错,它不能加载LibPersonFramework,这个是签名出错了。虽然生成framework和运行app使用的是同一个证书,但是这里使用的并不是整个framework,所以这里需要使用codesign强制重签名一下。

    添加一个脚本


    /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$BUILT_PRODUCTS_DIR/$TARGET_NAME.app/LibPersonFramework"
    
    

    到这里就做完了,运行一下,应该是成功的!

    4. 注入动态库

    注入动态库是指,给一个现有的mach-o添加一个动态库,这样可以在一个现有的app中执行动态库的代码。在给现有app注入动态库时,这个动态库只能作为一个依赖库被注入,这是因为在注入之前,不能在现有app中执行代码,所以也就不能使用dlopen()函数来加载动态库了。

    首先,观察一下,当一个app添加了一个依赖库之后,会有哪些变化。在上文中,DylibDemo添加了一个依赖库LibPersonFramework.framework,下面就以这个项目作为例子。

    1. 项目生成的app包中增加了Frameworks文件,如果是系统动态库,则不会被添加到app包中。


    2. mach-o文件中增加了一条Load Commands数据,这条记录表示了app对指定的动态库的依赖。

    使用MachOView打开app包中的mach-o文件

    在app启动时,会自动根据Load Commands指定的路径去加载动态库,所以必须保证路径下存在对应的动态库。

    下面举个例子

    新建一个动态库LibInjectFramework,下面会将这个动态库注入到一个现有app中,如果注入成功,则图中的+[load]方法会被执行。

    新建一个项目DylibDemo-Inject,这个项目什么代码都没有,只是一个空项目,下面需要将动态库LibInjectFramework注入到这个项目中。

    1. 将动态库LibInjectFramework复制到这个项目的app包中


    2. 添加动态库依赖

    这一步需要修改被注入app的mach-o文件,这里使用yololib来完成。将yololib下载后,然后编译,将生产的命令复制到/usr/local/bin$PATH中的其他路径,这样就可以在终端使用这个命令了。
    yololib需要两个参数,第一个参数指定被修改的mach-o文件的路径,第二个参数指定动态库的路径。

    在项目中,添加两个脚本命令,分别用来重签名动态库和修改mach-o文件


    /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$BUILT_PRODUCTS_DIR/$TARGET_NAME.app/Frameworks/LibInjectFramework"
    yololib "$BUILT_PRODUCTS_DIR/$TARGET_NAME.app/$TARGET_NAME" "Frameworks/LibInjectFramework"
    

    执行,控制台应该会输出下面这句

    Inject success😊😊😊😊😊😊😊😊😊😊
    

    需要注意的是,这个项目只有在第一次运行时会成功,因为多次运行,会在mach-o文件中增加多个相同的Load Command。解决方法是保存一个原始的mach-o文件,然后每次运行前替换。

    5. yololib

    在使用yololib去添加动态库依赖时,会修改mach-o文件的两个地方

    1. 修改mach-o文件的头文件

    mach header的定义

    struct mach_header_64 {
        uint32_t    magic;      /* mach magic number identifier */
        cpu_type_t  cputype;    /* cpu specifier */
        cpu_subtype_t   cpusubtype; /* machine specifier */
        uint32_t    filetype;   /* type of file */
        uint32_t    ncmds;      /* number of load commands */
        uint32_t    sizeofcmds; /* the size of all the load commands */
        uint32_t    flags;      /* flags */
        uint32_t    reserved;   /* reserved */
    };
    

    由于增加了一条Load Command,所以需要修改的是ncmdssizeofcmds这两个字段,它们分别表示Load Command的总数目和总大小。

    1. 添加一个dylib_command结构体

    动态库的信息是以dylib_command结构体的形式被存储,dylib_command的定义

    struct dylib_command {
        uint32_t    cmd;        /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
                           LC_REEXPORT_DYLIB */
        uint32_t    cmdsize;    /* includes pathname string */
        struct dylib    dylib;      /* the library identification */
    };
    
    struct dylib {
        union lc_str  name;         /* library's path name */
        uint32_t timestamp;         /* library's build time stamp */
        uint32_t current_version;       /* library's current version number */
        uint32_t compatibility_version; /* library's compatibility vers number*/
    };
    

    创建一个dylib_command结构体,并添加到所有Load Command之后,

           fseek(newFile, sizeofcmds, SEEK_CUR);
            
            struct dylib_command dyld;
            fread(&dyld, sizeof(struct dylib_command), 1, newFile);
            
            NSLog(@"Attaching dylib..\n\n");
            
            dyld.cmd = LC_LOAD_DYLIB;
            //cmd的大小是dylib_command结构体的大小加上path的大小。
            dyld.cmdsize = (uint32_t) dylib_size;
            dyld.dylib.compatibility_version = DYLIB_COMPATIBILITY_VERSION;
            dyld.dylib.current_version = DYLIB_CURRENT_VER;
            dyld.dylib.timestamp = 2;
            //指定从哪里开始是name
            dyld.dylib.name.offset = sizeof(struct dylib_command);
            fseek(newFile, -sizeof(struct dylib_command), SEEK_CUR);
            fwrite(&dyld, sizeof(struct dylib_command), 1, newFile);
    

    紧跟着被添加的Load_Command,添加动态库的path字符串。

    fwrite([data bytes], [data length], 1, newFile);
    

    在添加新的Load_Command时,是直接使用新数据来覆盖就数据的,因为Load_CommandSection之间还预留了一部分空间,所以直接覆盖不会影响Section的数据。

    相关文章

      网友评论

      • 棕枝与芦苇:请问, 使用动态库上传appstore, 有没有被拒的风险?

      本文标题:动态库的使用

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