阅读 96

音视频学习之路--NDK交叉编译解析

前言

在说C/C++项目时必须要涉及编译问题,本章就来系统的说一下这些知识点,包括linux编译、常用linux指令、交叉编译等等。

正文

为了方便使用Linux环境,我这里直接在VMWare中安装了一个ubuntu,具体安装的步骤在网上非常多,安装完就是这样:

image.png

由于好久不玩Linux系统了,这里我也是边搞边学习总结。

编译原理

这里主要说的是一个C/C++文件要经过下面4个步骤菜能变成可执行文件:

  • 预处理(preprocessing)

  • 编译(compilation)

  • 汇编(assembly)

  • 链接(linking)

下面我们分别来说说。

预处理

预处理阶段主要处理include和define等命令,把#include包含进来的.h文件插入到#include所在的位置,把源程序中使用到的#define定义的宏用实际的字符串替代。

首先对Linux基本命令和vi/vim做个总结,后面要经常用:

vim命令行.png

然后我们需要在Linux安装gcc和g++,这个在Linux上就非常简单了,只需要使用: sudo apt install gcc 即可安装,这也是命令行的方便之处,还是看一下预处理命令:

image.png这里有个简单的test.c文件,然后使用gcc -E 命令可以让gcc在预处理后停止编译:

image.png然后看一下.i文件,可以想象它是把stdio.h中的所有东西都复制到了这里:

image.png这里就是前面很多行代码,加上后面我们写的几行代码;

编译

编译阶段,gcc首先检查代码的规范性、是否语法错误等,编译后代码是汇编文件,我们还是来看一下:

image.png同样这里使用 -S 来保证编译结束后生成汇编文件,停止执行,其实这个汇编代码,我们就能看得懂了,不过很难懂。

汇编

通过前面2个步骤,C/C++文件就成了汇编文件,接下来就是把.S文件翻译成二进制机器指令,也就是我们很熟悉的.o文件,看一下指令:

image.png

这里使用 -c 参数来进行汇编,汇编后的代码将是看不懂的代码了:

image.png

连接

什么是链接呢,为什么要有链接这一步骤。说白了链接就是把很多个目标文件链接在一起生成一个可执行文件,比如这里就一个test.o,这里可以链接为可执行文件:

image.png这里就用-C来连接文件,生成的可执行文件直接使用 ./ 即可运行。

好了,到这里我们了解到了C/C++文件到最后的可执行文件需要经历的几个步骤,每个步骤都是必须的,但是指令可以省略,不用4遍,可以直接生成到可执行文件。

交叉编译

什么是交叉编译呢  比如我们上面生成了一个可执行文件test,我们在虚拟机中使用 ./test 便可以运行,但是我们把它拷贝到手机中,手机也是Linux环境,确不可以运行,这就是2个平台的CPU指令集不一样,无法识别指令。

所以交叉编译就是程序的编译环境和运行环境不一样,在一个平台上生成另一个平台可以执行的代码,比如我在Linux环境下编译的代码可以在移动平台上执行,这就是交叉编译。

下面简单了解一下NDK:

NDK概述.png

简单一点说NDK就是可以帮助我们编写C++代码,然后生成文件可以在Android平台上运行,也就是有交叉编译的效果。

配置环境变量

我这里直接在ubuntu上进行操作,从官网下载了一个ndk开发包,放到桌面的文件夹上:

image.png

然后配置环境变量,和Windows系统类似,这里环境变量的配置是在/ect/profile文件中,直接vim打开编辑,在最后加上2行即可:

image.png

然后保存和测试一下:

image.png

显示出版本说明配置成功。

使用NDK的gcc进行编译

在前面我们使用普通的gcc编译出的test是无法在Android系统上运行的,因为电脑的系统架构是X86的,但是Android平台是ARM架构的,所以我们这里使用NDK的交叉编译来进行编译。

首先得找到NDK中的gcc在哪,在下面目录中:

/home/zyh/Desktop/NDK/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc复制代码

然后通过这个gcc来编译上面说的test.c代码:

image.png

这里会提示头文件找不到,所以我们还需要设置头文件,这里头文件也就是NDK开发包自己带的C/C++库,在特定位置,我们只需要安装指令给加上:

/home/zyh/Desktop/NDK/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc 
--sysroot=/home/zyh/Desktop/NDK/android-ndk-r17c/platforms/android-21/arch-arm 
-isystem /home/zyh/Desktop/NDK/android-ndk-r17c/sysroot/usr/include 
-isystem /home/zyh/Desktop/NDK/android-ndk-r17c/sysroot/usr/include/arm-linux-androideabi  test.c -o test复制代码

运行如下:

image.png

会发现这里成功编译成功生成了可执行文件。同时上面的指令贼长,其实也非常简单就是gcc加上头文件配置即可,话不多说我们把这个test拷贝到Android系统下来看看能否运行:

image.png

到这里我们第一个交叉编译的项目就完成了,我们在Linux下编译的可执行文件成功的在Android的Linux环境上运行起来,这里也就是NDK交叉编译的作用。

上面写的指令有误,具体生成的无法在Android 5.0后的平台上使用,具体怎么改,需要加个 -pie 这个选项,下面我们再来细说一下。

优化系统配置,简化编译

前面的命令实在是太长了,根据最开始配置环境变量思路来说我们可以把这些命令也给配置到系统变量中,具体如下:

export NDK_GCC_x86="/home/zyh/Desktop/NDK/android-ndk-r17c/toolchains/x86-4.9/prebuilt/linux-x86_64/bin/i686-linux-android-gcc"
export NDK_GCC_x64="/home/zyh/Desktop/NDK/android-ndk-r17c/toolchains/x86_64-4.9/prebuilt/linux-x86_64/bin/x86_64-linux-android-gcc"
export NDK_GCC_arm="/home/zyh/Desktop/NDK/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc"
export NDK_GCC_arm_64="/home/zyh/Desktop/NDK/android-ndk-r17c/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-gcc"

export NDK_CFIG_x86="--sysroot=/home/zyh/Desktop/NDK/android-ndk-r17c/platforms/android-21/arch-x86 -isystem /home/zyh/Desktop/NDK/android-ndk-r17c/sysroot/usr/include -isystem /home/zyh/Desktop/NDK/android-ndk-r17c/sysroot/usr/include/i686-linux-android"
export NDK_CFIG_x64="--sysroot=/home/zyh/Desktop/NDK/android-ndk-r17c/platforms/android-21/arch-x86_64 -isystem /home/zyh/Desktop/NDK/android-ndk-r17c/sysroot/usr/include -isystem /home/zyh/Desktop/NDK/android-ndk-r17c/sysroot/usr/include/x86_64-linux-android"
export NDK_CFIG_arm="--sysroot=/home/zyh/Desktop/NDK/android-ndk-r17c/platforms/android-21/arch-arm -isystem /home/zyh/Desktop/NDK/android-ndk-r17c/sysroot/usr/include -isystem /home/zyh/Desktop/NDK/android-ndk-r17c/sysroot/usr/include/arm-linux-androideabi"
export NDK_CFIG_arm_64="--isysroot=/home/zyh/Desktop/NDK/android-ndk-r17c/platforms/android-21/arch-arm64 -isystem /home/zyh/Desktop/NDK/android-ndk-r17c/sysroot/usr/include -isystem -isystem /home/zyh/Desktop/NDK/android-ndk-r17c/sysroot/usr/include/aarch64-linux-android"

export NDK_AR_x86="/home/zyh/Desktop/NDK/android-ndk-r17c/toolchains/x86-4.9/prebuilt/linux-x86_64/bin/i686-linux-android-ar"
export NDK_AR_x64="/home/zyh/Desktop/NDK/android-ndk-r17c/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-ar"
export NDK_AR_arm="/home/zyh/Desktop/NDK/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-ar"
export NDK_AR_arm_64="/home/zyh/Desktop/NDK/android-ndk-r17c/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-ar"复制代码

这里其实也是非常的简单,NDK_GCC_XXX就是需要使用哪个平台的gcc,然后CFIG就是哪个平台的头文件,而且要注意的是如果能生成在Android5.0之后能运行的可执行文件要加上 -pie 编译条件,如下:

image.png

这里就可以少写一大串指令,同时使用 -pie 生成了testandroid1,这个可执行文件可以在Android平台运行:

image.png

这里注意指令因为是NDK的原因,所以编译的包都是跑在Android系统上的,其他暂时还不行。

动态库和静态库

在我们Android开发一般给别人库就是jar包或者aar即可,但是在使用C/C++库时我们会使用so也就是动态链接库,那啥是动态库呢 和静态库有什么区别,这个必须要搞明白。

动态库和静态库.png

这里说了2种的区别,看来还是动态库更使用一些。

这里还是看一下小栗子,看看编译时什么参数可以编译出静态库和动态库,这里明白一点就是gcc -c参数是只编译不链接,也急速参数.o文件,而不是可执行文件,所以下面先使用NDK GCC编译为.o文件:

image.png

这时再用其他工具也就是ar工具吧.o生成为静态库,这里说是生成也可以说是打包,可以把多个.o文件打包为静态库.a,如下:

image.png

到这里这个静态库test.a就生成了,那如何生成一个动态库呢 要给编辑器加额外参数 -shared 即可:

image.png

这样我们关于生成静态、动态库都有了一个简单的了解了。

mk和cmake

既然我们Android平台一般都是用的这so动态库,那肯定不会像上面那么麻烦,需要搞一个脚本来简化这些操作,当然官方人员也想到了,有2种方式mk和camke。

其中mk是早期的开发方式,比较复杂,这里就不说了,差不多已经被淘汰,重点说一下cmake,这个是我们常用的。

cmake

cmake文件在Android studio创建native项目时就生成了,这个脚本看起来内容多,但是要想明白C/C++交叉编译以及编译的过程,就很容易理解,这个脚本肯定是为了让前面那些步骤变简单而设计的,直接看一下默认的CMakeLists文件:

cmake_minimum_required(VERSION 3.10.2)

add_library( # Sets the name of the library.
        jnistudy

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        native-lib.cpp)

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

target_link_libraries( # Specifies the target library.
        jnistudy

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})复制代码

在这里我们只需要2件事,第一就是我Java代码要运行C/C++代码,你这里的C/C++代码通过编译链接有3种形式给我分别是可执行文件、静态库和动态库,第二就是我需要Android平台能运行的C/C++代码,所以这里需要交叉编译,搞明白这2点就清楚了NDK设计的初衷,那上面的CMakeList就是编译C/C++代码用的,比如编译成什么,需要什么库,链接什么库。

CMakeList语法.png

上图就是CMakeList的一些常用语法,说白了就是设置我们这个库需要编译的文件、需要连接的库、需要编译成的类型即可。

构建一个链接静态库项目

既然CMakeList的语法都明白了,那接下来看看如何利用cmake来完成一个简单的demo。

我们使用CMake编译后的库其实都是so库,通过System.loadLibrary("xxxlib")来进行加载,所以我们编写CMakeList的时候最终生成的库肯定是动态库,那假如我们有一些静态库 .a 文件我们如何来加载和链接他们呢,话不多说,现在开干。

  1. 在Linux上编写一个C文件,然后先编译成 .o ,再通过工具链接为 .a 静态库,如下:

image.png

image.png

2、把生成的 .a 静态库拷贝到我们的cpp目录下,待会来使用:

image.png

3、编写AS项目中的cpp代码,来引用test.a静态库中的main函数:

#include <jni.h>
#include <string>
#include <iostream>
#include <android/log.h>
#define TAG "native-lib"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__);

using namespace std;

extern "C"{
    int main();
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_zyh_jnistudy_MainActivity1_testCmake(JNIEnv *env, jobject thiz) {
    const char *hello = "Hello from static test";
    LOGD("从静态库读取main函数值 %d",main())
    return env->NewStringUTF(hello);
}复制代码

4、编写CMakeLists.txt文件来链接库,这里的主要目的就是把我们的test.a静态库在链接的时候给加到生成的库中,让cpp代码能正常执行:

cmake_minimum_required(VERSION 3.10.2)
project("testStaticLib")
# 打印日志
message("当前CMake的路径是: ${CMAKE_SOURCE_DIR}")
message("当前CMake的Android ARCH路径是:${CMAKE_ANDROID_ARCH_ABI}")
#引入文件
file(GLOB allCpp *.cpp)
#加入cpp源文件,也就是native-lib.cpp编译成的动态库
add_library(
        native_static_lib
        SHARED
        ${allCpp})
#导入静态库
add_library(test_a STATIC IMPORTED)
#开始真的导入
set_target_properties(test_a PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/test.a)
#只找系统的log库
find_library(
        log-lib
        log
)
#开始链接指定的库
target_link_libraries(
        native_static_lib
        ${log-lib}
        test_a
)复制代码

5、再编辑gradle中的配置,因为默认情况下的配置是编译4个平台的so,这没必要,我们一般只使用armeabi-v7a这个平台即可,配置一个平台如下:

image.png

image.png

6、我们就可以在Java代码中加载我们需要so来使用了:

class MainActivity1 : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.sampleText.text = testCmake()

    }

    external fun testCmake(): String

    companion object {
        init {
            System.loadLibrary("native_static_lib")
        }
    }
}复制代码

7、最终我们的打印如下:

2021-10-28 10:53:33.208 9198-9198/com.zyh.jnistudy D/native-lib: 从静态库读取main函数值 197复制代码

到这里,我们就捋清楚了如何链接一个静态库,也就是别人给我们静态库我们如何来使用。


作者:元浩875
链接:https://juejin.cn/post/7024033671178895391


文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐