jni学习笔记:动态链接库与静态链接库的基本使用流程简记

背景

最近做了一段时间的项目中涉及到一些ffmpeg视频编解码的应用和OpenCV算法在Android的使用,其中免不了需要使用jni在java层调用相关算法的内容,尤其当业务逻辑复杂时还需要cpp层调用java层的函数。在此也总结了一些jni使用上的方法以及一些常见的问题。本文我们将总结一些基础知识。

我们知道,Android集成许多第三方库的时候,需要导入许多动态链接库也就是.so文件,而我们只要在java层load一下库的名称,就可以调用其中的jni函数。下面我们将总结一些Linux下库的生成与加载的知识,以便我们更深入地理解jni编写与运作的流程,在编写jni相关代码时减少不必要的弯路。

##静态连接库的生成与使用
我们先来个简单的例子,看一下静态库的生成与使用。
我们先定义一个fun.c的文件,稍后我们会将其生成为一个静态库libfun.a,然后main.c的主函数在链接时会调用这个libfun.a中的fun函数。
###实验1
main.c

#include <stdio.h>

int main(){
	fun();
}

fun.c

#include <stdio.h>
#include "fun.h"

void fun(){
	printf("I am fun!");
}

fun.h

#ifndef FUN_H
#define FUN_H
void fun();
#endif

这就是我们用到的三个最简单的文件,现在我们要将先根据fun.c编译出libfun.a文件,之后再将main.c编译生成main.o文件,最后进行链接,生成最终可执行的文件。下面是我们的Makefile文件。

CC=gcc
AR=ar
LD=ld

.PHONY:clean

all:main

main:main.c libfun
	${CC} -c main.c
	#${CC} -o main.out main.o -L. -lfun
	${LD} -o main.out /usr/lib/crt1.o main.o -L. -lfun -lc
libfun:fun.c
	${CC} -c fun.c
	${AR} cr libfun.a fun.o

clean:
	rm *.o *.a *.out

下面简要解释一下这段Makefile的含义。
完成最终的all标签我们需要依赖main标签,main标签需要依赖于main.c和libfun标签。在libfun中,我们使用 ${CC} -c fun.c命令编译fun.c,最终生成目标文件fun.o,fun.o中包含了不完整的段信息,是没有办法进行直接执行的。接下来我们使用ar命令,将fun.o文件打包成静态链接库libfun.a。

有了libfun.a,我们就可以执行main标签中的命令了。还是通过-c选项,先对main.c文件进行编译,生成目标文件main.o。接下来的两种办法都可以让我们生成最终的可执行文件。

${CC} -o main.out main.o -L. -lfun 这条命令的意思是使用cc提供的工具通过main.o 与libfun.a库进行链接,其中-L用来指定库文件的路径。命令最终生成可以执行文件main.out。

${LD} -o main.out /usr/lib/crt1.o main.o -L. -lfun -lc则是我们手动使用链接命令进行连接,手动链接时我们首先需要手动链接crt1.o这个目标文件,它是Glibc的一个辅助运行库,用来帮助我们找到程序的入口,由于我们调用了printf函数,这个函数是标准c函数,所以我们还要把它也链接进来,也就是libc库链接进来,即-lc。正常情况下我们直接使用cc工具链接即可,而手动使用ld命令进行链接则更有助于我们理解例子程序的生成过程。

运行这段makefile,我们就可以得到 可以独立运行 的main.out,下面是我们的运行结果。

这里写图片描述

运行main.out即可调用链接库中的fun函数打印"I am fun!"。

接下来我们再思考一个问题,假如静态库中有多个函数,而我们只是使用了其中的一些函数,那么整个静态库都会被编译到可执行文件中吗?
###实验2
main.c

#include <stdio.h>
#include "fun.h"

int main(){
	fun();
}

fun.c

#include <stdio.h>
#include "fun.h"

void fun(){
	printf("I am fun!");
}

void fun_extra(){
	printf("I am extra");
}

fun.h

#ifndef FUN_H
#define FUN_H

void fun();
void fun_extra();

#endif

我们增加了一个额外的函数void fun_extra(),但是在main函数中我们并没有对其进行调用,makefile不变,编译,链接,生成可执行文件。现在我们来猜测一下,生成的文件与实验1有什么区别呢,直观上讲生成的库文件肯定变大了,因为有了新的函数他需要包含更多的信息,那么最终的可执行文件呢?也会随着它所链接的库的增大而增大吗?下面我们来看一下文件信息。
这里写图片描述
最终的main.out会大于上面产生的main.out。但是我们仍然无法确定main.out中是否有libfun.a的全部内容。

我们在做这样一个实验,这一次我们不去重新生成libfun.a,而是使用我们当前生成的libfun.a进行链接,但是我们在main.c中调用fun_extra(),看其是否能调用成功。
这里写图片描述
如图,这里main.out中调用过fun_extra()与没有调用过fun_extra()生成文件的大小是不变的, 但是main.o文件是变大的,顺着这个思路可以推测最终生成文件中整个.a文件的功能是都被包涵进.out中的。

下面我们使用mac的nm工具看看生成文件的符号表,看看这些文件里到底包含了哪些函数。

我们先看第一种情况,也就是只调用了动态链接库中fun函数而没有调用fun_extra函数。
先看一下main.o,我们执行这条操作:nm -px main.o
这里写图片描述
可见.o文件中只有它调用过的函数的符号。
再看看最终生成的main.out ,输入 nm -px main.out
这里写图片描述
可见,最终经过链接生成的文件确实包括了.a文件中的所有所有函数,即便是没有调用函数。

###去掉没有调用的代码
由于我们的.a文件生成时没有做其他处理,最终得到函数会被分配到同一个段内,这样就导致了链接的时候把没有用到的函数也一并带入到最终生成的文件中。也就是说我们在编译.a文件的时候,需要将每个函数编译到独立的段中,下面我们把每个函数放到独立的代码段中,然后进行链接,看看效果。实验中发现直接使用ld链接最终生成的文件反而大了,此处还需要继续研究,下面我们直接使用gcc进行链接。

我们先对makefile进行修改,改动部分如下

main:main.c libfun
        ${CC} -c main.c
        ${CC} -dead_strip -o main.out main.o -L. -lfun
        #${LD} -dead_strip -o main.out /usr/lib/crt1.o main.o -L. -lfun -lc
libfun:fun.c
        ${CC} -ffunction-sections -c fun.c
        ${AR} cr libfun.a fun.o

编译libfun.a的时候,我们加入了-ffunction-sections ,将fun.c中函数中每个函数代码段分开,链接时我们使用${CC} -dead_strip -o main.out main.o -L. -lfun, 去除无用的代码。现在我们再编译一下新的项目,看看时啥个情况。
这里写图片描述
可见main.out大小确实小了,现在我们看看main.out中有啥。执行nm -px main.out
这里写图片描述
可见,现在没有用到函数都没有了。

##动态连接库的生成与使用
不同于静态库直接链接到最终的可执行文件,动态库是在程序运行时动态分配地址。所以动态库必须与我们最终要执行的文件一起打包才能使用。ndk最终调用c/cpp也是通过调用其生成的动态链接库,也就是我们常见的.so文件。集成过一些推送或者地图等sdk时,我们都需要将.so文件拷贝到libs目录中,这些就是动态链接库。

###实验1
这里我们简要看一下动态链接库的生成。
下面这个动态库中,我们将会使用上一小节中生成的静态库的文件,也就是说我们要把静态库libfun.a中的内容编译进动态库libfunshare.so。然后我们在运行main.out时动态调用libfunshare.so。在实际ndk开发中这种情况也是经常发生的,比如opencv的android官方库就是为我们提供了一堆.a文件,我们调用其中的接口生成我们自己的.so文件,ffmpeg中添加编解码器也可以通过调用编解码器的.a文件来生成我们最终在jni中调用的.so文件。

fun_share.c文件是我们的动态链接库的c文件,代码如下

#include <stdio.h>
#include "fun_share.h"
#include "fun.h"
void fun_share(){
	printf("I am from fun_share");
	fun();
}

这里的fun函数是上一节生成静态库libfun.a中的那个fun函数。最后我们在main函数中调用这个fun_share函数。
main.c

#include <stdio.h>

int main(){
	fun_share();
}

这里调用动态链接库的fun_share函数。

下面是makefile的代码,简要看一下这个demo的生成。

CC=gcc
AR=ar

.PHONY:clean

all:main

main:main.c libfunshare
        ${CC} -c main.c
        #(ldconfig 'pwd')
        ${CC} -o main.out main.o -L. -lfunshare

libfunshare:fun_share.c libfun
        ${CC} -c fun_share.c
        ${CC} -shared -fPIC -o libfunshare.so fun_share.o -L. -lfun

libfun:fun.c
        ${CC} -c fun.c
        ${AR} cr libfun.a fun.o

clean:
        rm *.o *.so *.a *.out

这里我们着重看一下生成这个.so文件和调用.so文件的地方。

${CC} -shared -fPIC -o libfunshare.so fun_share.o -L. -lfun

这条命令用于动态链接库的生成,-share是告诉gcc生成文件当做动态链接库。-fPIC命了也经常在编译.so文件时用到,它的意思是说生成不带绝对地址而带相对地址的文件(position independent code)。由于动态库不是在编译时直接链接到位,所以如果加载时需要对其中各段的内容进行重定位,这样如果有多个进程同时调用这个.so文件,由于每次重定为前都要重新计算地址,所以这些.so文件的代码是没办法复用的。当然,在编译动态库时也可以不加这个选项。

再往后看-L. -lfun是我们上一节中了解过的指令,-L指定静态链接库的位置,-lfun链接libfun.a文件。这样,我们的libfunshare.so文件就生成完毕了。关于.so文件的调用,一般有两种方式,这里我们先介绍第一种。

${CC} -o main.out main.o -L. -lfunshare

与链接静态库一样,直接指定就可以了,这样就完成了main.out的生成。执行一下,我们发现动态库本身的内容和被调用的静态库的内容都可以被执行。

###实验二
下面来看另外一种.so文件的调用方式,这种方式中我们可以在代码中动态指定调用.so中的哪些方法。
下面先看一下我们的main函数,这里发生了较大的变化。
main.c

#include <stdio.h>
#include <dlfcn.h>

int main(){
	
	void *handle = dlopen("./libfunshare.so", RTLD_LAZY);
	const char* error = dlerror();
	if(error != NULL){
		printf("load err\n");
		exit(1);
	}	

	void (*fun)();
	fun = dlsym(handle, "fun_share");
	fun();
}

dlfcn.h中的api可以帮助我们动态调用.so中的内容。我们声明了一个函数指针fun,然后用这个函数指针接受dlsym的返回值,这样就可以fun就称为了一个指向.so中fun_share函数的函数指针,执行fun()就可以调用fun_share了。

makefile文件也需要做少许的改动。实验一我们使用了静态链接。这里我们将

${CC} -o main.out main.o -L. -lfunshare

替换为

${CC} -o main.out main.o -rdynamic -ldl

-rdynamic是将符号添加到动态符号表,-ldl说明最后生成的文件需要使用共享库。以前一直当做固定的写法这么写,但是现在发现去掉这两个选项生成的文件依然可以正常运行,此处存疑。

##总结
本文通过几个实验,总结了静态链接库(.a)和动态链接库(.so)文件的基本生成与使用的方法。

.a文件我们可以看作是ar文件将很多.o文件压缩生成的库,链接过程相当于是把其中的各个段链接到我们最终生成的文件。我们在编译.a文件的.o文件时可以指定将函数编译到单独的段中,这样我们在链接完毕后可以strip掉没有调用的函数。

.so文件是运行时动态加载的文件,通过-share选项生成,可以通过选择-fPIC编译为使用相对地址。使用相对地址在加载时可以不改变代码,使得多个进程可以加载同一个.so文件,但是在有些情况下页可以不加入这个选项。在调用.so文件的代码时我们可以在编译时使用静态链接,也可以在代码中通过dlfcn.h提供的api直接调用.so文件中的函数,无论使用哪种方法,.so文件在主程序运行时必须存在。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值