4、iOS强化 --- 链接与符号(Symbol)

首先我们来认识一下什么是链接:

  • 链接的本质就是把一个或多个目标文件和需要的库(静态库/动态库,如果需要的话)组合成一个文件(Mach-O可执行文件)
    通常.o文件被我们称之为目标文件。
    下面我们来看一下目标文件的生成过程:
image.png
  • 这里大家要注意一下:
    在生成目标文件的过程中
    1、链接器 (llvm-ld) 并没有被执行。
    2、目标文件不会包含Unix程序在被装载执行时所必须的包含信息。
    那么上面这句话究竟是什么意思呢?
    接下来我们来探索一下:
    首先我们在Mach-O 文件这篇文章里面已经简单了解了Mach-o文件的格式,这里我们再来加深一下印象,这里我们将通过终端打印一下Mach-o的一些相关信息。
    1、首相我们建立一个命令行工程test,在工程中引入一个脚本脚本地址,配置如下:
    image.png

2、创建我们自己的xcconfig文件(这一步有不明白的同学可以阅读Xcode 多环境的配置这一批文章)
3、在xcconfig文件中输入脚本要用的一些参数(注意:1、这里不是shell指令,是Key-Value的形式,2、此时我们还没有写任何代码)

脚本中需要输入两个变量:
CMD : 指令

TTY:指定的终端(可以终端输入tty,会打印当前终端的信息)

同时我们还需要知道当前Mach-O的地址:

MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
  • MACH_PATH:我们自己定义的变量,用来存储Mach-O的地址
  • ${BUILD_DIR}:当前编译的路径
  • $(CONFIGURATION):构建产品的目录
  • $(EFFECTIVE_PLATFORM_NAME)Mach-O所在的目录
  • ${PRODUCT_NAME}:项目名称,也就是Mach-O的名称

① 首先我们打印一下Mach header

// 查看mach-header
CMD = objdump --macho -private-header ${MACH_PATH}

image.png

这里大家可以看到Mach header的一些原始的信息。
② 接下来我们查看一下Mach-o里面的__TEXT段(因为:main函数被编译之后会放到__TEXT段的)
image.png

可以看到显示的__TEXT段、__text section的机器指令(如:55),后面跟着的汇编代码是给开发者看的。
在底层会有一个类似与字典的东西,会将汇编指令机器码一一对应起来
下面我们在main.m文件中添加一些代码,在来看一下__TEXT段(代码段):

/// 全局变量
int global_uninit_value;

int global_init_value = 10;
//__attribute__关键字主要是用来在函数或数据声明中设置其属性。给函数赋予属性的主要目的在于让编译器进行优化。
double defaule_x __attribute__((visibility("hidden")));
/// 静态变量 -》本地符号
static int static_uninit_value;

int main(int argc, const char * argv[]) {
    static_uninit_value = 10;
    NSLog(@"%d", static_uninit_value);
    return 0;
}

image.png

我们会发现__TEXT段多了很多东西。
可以看到NSLog直接就变成了callq 0x100003f90指令,一个有明确地址的指令。

  • 其实在编译生成.o文件的过程中是做了这样一件事情:
    1、能变成汇编的,尽量变成机器码
    2、符号归类(比如:数据放到数据段,等等);再比如NSLog,在生成目标文件的时候,我们并不知道它的地址,这个时候就要将它临时发到一个地方。将符号归类之后,放到重定位符号表里面。

  • 为什么要放到重定位符号表里面呢?
    1、因为在生成.o文件的时候,整个的地址并没有虚拟化(虚拟内存的地址)
    2、在生成.o的过程中,我们链接的符号,一些我们知道它的位置(如:global_init_value,因为在同一个Mach-o中,我们可以通过偏移地址直接取到符号);但是有一些导入符号我们是不知道的(如:NSLog
    也就是说:重定位符号表里面放的就是.o文件里面用到的API,没有用到的就不在这里面。
    既然重定位符号表是这样的,那么我们也就可以通过检查.o文件里面的重定位符号表来查看文件里面对某种API的使用情况。

  • 接下来.o会进入链接过程,处理我们编译的情况;会把生成的多个.o文件合并成一个,这也就意味着,大家的符号表(包括重定位符号表)都会被合并到一张表中。

  • 最后生成可执行文件(exec)

  • 因此我们说的链接,就是在处理.o文件中符号的过程

全局符号与本地符号

结合上面的代码:

  • 全局符号:int global_uninit_value;
    全局符号对整个项目可见,对使用它的项目也是可见的
  • 本地符号:static int static_uninit_value;
    本地符号只对定义它的文件可见

接下来我们来查看一下符号表:

// 查看符号表
CMD = objdump --syms ${MACH_PATH}
image.png

上图中有一点不同,不知道大家注意到没有:
defaule_x这个符号我们定义的是一个全局符号,但是最终它是一个本地符号,这是为什么呢?
因为我们是这样写的:

double defaule_x __attribute__((visibility("hidden")));

这里我们就要引入visibility属性:

// visibility属性,控制文件导出符号,限制符号可见性
/**
    -fvisibility:clang参数
    default:用它定义的符号将被导出。
    hidden:用它定义的符号将不被导出。
 */
// 隐藏 -> 本地
int hidden_y __attribute__((visibility("hidden"))) = 99;
// 符号
double default_y __attribute__((visibility("default"))) = 100;

这里说明一下,我们在开发中经常会遇到被Apple遗弃的方法,会有一条黄线的警告,其实也是用了这个属性。

导入符号 & 导出符号

还是上面的代码,我们用到了NSLog(它存在于Foundation动态库中);那么对于我们自己的可执行文件NSLog就是导入符号;对于Foundation动态库NSLog就是导出符号
这也就意味着导出符号全局符号
下面我们打印一下导出符号

// 查看导出符号
CMD = objdump --macho --exports-trie ${MACH_PATH}

image.png

可以看到正好对应的是我们设置的全局变量。
在日常开发中我们要注意:当我们把变量设置成全局变量的时候,也就意味着会被默认设置成导出符号

  • 这里我们补充一下,间接符号表里面保存着,我们当前可执行文件使用的其他的动态库里面的导出符号
    下面我们来打印一下间接符号表
// 查看间接符号表
CMD = objdump --macho --indirect-symbols ${MACH_PATH}
image.png
  • 这里就有一个问题了,在我们在strip符号剥离的时候,间接符号表里面的面的符号是我们不能剥离的,那我们反过来向上推,也就意味着我们在写自己的OC的动态库的时候没办法剥离我们没有暴露的符号。这里跟大家讲一下,OC默认的符号都是全局符号,而全局符号又是导出符号,我们来验证一下:
    image.png

    image.png

    接着我们来打印一下导出符号
// 查看导出符号
CMD = objdump --macho --exports-trie ${MACH_PATH}
image.png
  • 不导出符号
OTHER_LDFLAGS=$(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_METACLASS_$_YSOneObject
image .png

我们成功的将符号_OBJC_METACLASS_$_YSOneObject设置成非导出符号
这样我们就可以对齐进行符号剥离,进一步减少动态库的体积;同时外界也就看不到我们的符号了。
⚠️ 开发中我们也不必一个一个的去将符号设置成不导出,我们可以指定一个文件来设置符号的导出属性,使用-unexported_symbol_list这个指令

  • 我们还可以查看我们使用的所有的符号,并写入到相应的文件中:
OTHER_LDFLAGS=$(inherited) -Xlinker -S -Xlinker -map -Xlinker /Users/aaron/Desktop/test-02/Source.text
image.png
弱符号(Weak Symbol)

弱符号分为:

  • Weak Reference Symbol(弱引用符号):表示此未定义符号是弱引用。如果动态连接器找不到该符号的定义,则将其设置为0。链接器会将此符号设置弱链接标志。
  • Weak defintion Symbol(弱定义符号):表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义。

那么怎么理解上面的两句话呢?
首先我们来看在代码中怎么写:

// 弱引用
void weak_import_function(void) __attribute__((weak_import));

// 弱定义
// weak def
void weak_function(void)  __attribute__((weak));
// weak 本地符号
void weak_hidden_function(void) __attribute__((weak, visibility("hidden")));

首先我们来看一下:

  • Weak defintion Symbol(弱定义符号)
    其中weak_function为全局弱定义符号;weak_hidden_function为本地弱定义符号。
    接着我们查看一下到处符号表:
CMD = objdump --macho --exports-trie ${MACH_PATH}

image.png

可以看到弱定义并不影响其作为导出符号
接着我们在其他地方在定义一个名字相同的全军符号,如下:
image.png

我们会发现,工程并不会报错。运行起来也没有问题,这正式我们上面说的:如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。

接下来我们再看一下:

  • Weak Reference Symbol(弱引用符号)
    首先我们只声明,不实现:
void weak_import_function(void) __attribute__((weak_import));

同样的查看导出符号表

image.png

我们会发现导出符号表里面并没有该符号。
接下来我们实现以下该函数:

void weak_import_function(void) {
    NSLog(@"weak_import_function");
}

然后再查看一下导出符号表:

image.png

这也就是当动态链接器找不到它的定义,则将其定义为0,也就不会出现在导出符号表里面了。

  • 既然是这样,那我们只声明,不定义该符号。通过判断的符号是否为0,去做一些事情呢?
if (weak_import_function) {
        weak_import_function();
        
        /**
         一些其他的业务
         */
    }

我们cmd + B会发现,工程报错:

image.png

也就是说说在链接的过程中,链接器找不到这个符号(不知道符号具体在哪个地方)。

这个时候我们可以告诉链接器,不要管这个符号,即使是未定义的也不要管,我这个符号是动态链接的,到时候会自己找到这个符号。那么我们可以通过下面的指令来达到这个目的:

OTHER_LDFLAGS=$(inherited) -Xlinker -U -Xlinker _weak_import_function

重新导出符号

举一个例子:
我们上面的代码在mian.m文件中用到的NSLog这个函数。然而NSLog对于当前工程来说是一个未定义符号

image.png

那么此时,我们如果想要使用我们这个库的工程,也能够使用NSLog,此时我们就需要将NSLog以别名的形式从新导出。
⚠️ 这里要注意:只能给间接符号表中的符号起别名。

OTHER_LDFLAGS=$(inherited) -Xlinker -alias -Xlinker _NSLog -Xlinker YS_NSLog

我们再来查看一下导出符号表:

image.png

可以看得到YS_NSLog作为导出符号,并且被标记为re-export

Swift 符号

首先我们在swift文件中定义一些结构体方法

struct YSSwiftStructSymbol {
    func testSwiftStructSymbol(o: Int) {}
}

private protocol YSSwiftProtocolSymbol: class {
    func testSwiftProtocolSymbol()
}

private class YSSwiftClassSymbol {
    func testSwiftSymbol() {}
}

接着我们查看一下现在的符号表:

// 查看符号表
CMD = objdump --syms ${MACH_PATH}

image.png

会发现这里面多了很多的符号。
那么我们来定向查找swift产生的符号:

CMD = objdump --syms ${MACH_PATH} | grep "SwiftClass"

image.png

这样就少了很多,并且全部都是本地符号。下面我们修改一下swift中方法的权限:

public class YSSwiftClassSymbol {
    func testSwiftSymbol() {}
}

再来查看一下swift符号:

image.png

可以看到有一些符号已经变成了全局符号

总结:Swift是一门静态语言,跟OC不一样。Swift在编译的时候就能确定符号的类别。

你可能感兴趣的:(4、iOS强化 --- 链接与符号(Symbol))