【Android】Android NDK开发扫盲及最新CMake的编译使用 so

前言:

本篇文章旨在简介 Android 中 NDK 是什么以及重点讲解最新 Android Studio 编译工具 CMake 的使用

1、NDK简介

在介绍 NDK 之前还是首推 Android 官方 NDK 文档。传送门

官方文档分别从以下几个方面介绍了 NDK

  1. NDK 的基础概念
  2. 如何编译 NDK 项目
  3. ABI 是什么以及不同 CPU 指令集支持哪些 ABI
  4. 如何使用您自己及其他预建的库

本节将会对文档进行总结和补充。所以建议先浏览一遍文档,或者看完本篇文章再回头看一遍文档。

1.1NDK基础概念

JNI(Java Native Interface)Java本地接口,为了方便Java调用C、C++等本地代码所封装的一层接口(一个标准)

Java跨平台导致本地交互能力不强,一些和操作系统相关的特性Java无法完成,于是提供了jni用于和本地代码交互

上述部分文字摘自任玉刚的 Java JNI 介绍

NDK(Native Development Kit)原生开发工具包,帮助开发原生代码的工具,包括编译工具、一些公用库、开发IDE

     NDK:提供了一套将c/c++编程成静态、动态库的工具,而Android.mk和application.mk(描述编译参数和一些配置文件)如指定使用c++11还是14编译,引用哪些共享库并描述关系,指定编译的abi,有了这些NDK中的编译工具才能准确的编译c/c++

    ndk-build:Android NDK r4引入的shell脚本,调用正确的NDK构建脚本,最终还是会去调用NDK自己的编译工具

    CMake:跨平台的编译工具,据自定义语音规则CMakeLists.txt生成对应makefile或project文件,调用底层编译;

           c/c++编译文件再不同平台不一样:unix下使用makefile文件编译,windows使用project编译

            Android studio2.2后工具增加CMake支持,在Android Studio2.2后有2种选择编译c/c++代码,一个是ndk-build+android.mk+application.mk组合,一个是cmake+cmakelists.txt组合,这两与android与c/c++代码无关,只是不同的构建脚本和命令

1.2ABI应用程序二进制接口

Application binary interface,不同的CPU与指令的组合都有定义的ABI

程序只有遵循了这个接口规范才能在该CPU上运行,为了兼容多个CPU,需为不同CPU构建不同的库文件,对CPU来说,不同的架构不一定不兼容:

  • armeabi设备只兼容armeabi;
  • armeabi-v7a设备兼容armeabi-v7a、armeabi;
  • arm64-v8a设备兼容arm64-v8a、armeabi-v7a、armeabi;
  • X86设备兼容X86、armeabi;
  • X86_64设备兼容X86_64、X86、armeabi;
  • mips64设备兼容mips64、mips;
  • mips只兼容mips;

规律:

  • armeabi的SO文件基本上可以说是万金油,它能运行在除了mips和mips64的设备上,但在非armeabi设备上运行性能还是有所损耗;
  • 64位的CPU架构总能向下兼容其对应的32位指令集,如:x86_64兼容X86,arm64-v8a兼容armeabi-v7a,mips64兼容mips

具体的兼容问题可以参见这篇文章。Android SO文件的兼容和适配

2 CMake 的使用

介绍CMake 的规则和使用,以及如何使用 CMake 编译自己及其他预建的库。

首先创建一个新的包含原生代码的项目。在 New Project 时,勾选 Include C++ support:

项目创建好以后我们可以看到和普通Android项目有以下4个不同。

  1. main 下面增加了 cpp 目录,即放置 c/c++ 代码的地方
  2.  
  3. module-level 的 build.gradle 有修改
  4.  
  5. 增加了 CMakeLists.txt 文件
  6.  
  7. 多了一个 .externalNativeBuild 目录
android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
        //CMake 的命令集成在externalNativeBuild 
            cmake {
                cppFlags "-frtti -fexceptions"
                //CMake 的命令参数
                arguments "-DANDROID_ARM_NEON=TRUE"
            }
        }
    }
    buildTypes {
        ...
    }
    externalNativeBuild {
        cmake {
//指明了 CMakeList.txt 的路径
            path "CMakeLists.txt"
        }
    }
}
...

    更多的可以填写的命令参数和含义可以参见Android NDK-CMake文档

CMakeLists.txt

主要定义了哪些文件需要编译,以及和其他库的关系等

cmake_minimum_required(VERSION 3.4.1)

# 编译出一个动态库 native-lib,源文件只有 src/main/cpp/native-lib.cpp
add_library( # Sets the name of the library.
             native-lib
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )

# 找到预编译库 log_lib 并link到我们的动态库 native-lib中
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.
                       native-lib
                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

一个最基本的 CMakeLists.txt ,其实 CMakeLists.txt 里面可以非常强大,比如自定义命令、查找文件、头文件包含、设置变量等等。建议结合 CMake 的官方文档使用。同时在这推荐一个中文翻译的简易的CMake手册

2.2 CMake 使用自己及其他预建的库

当你需要引入已有的静态库/动态库(FFMpeg)或者自己编译核心部分并提供出去时就需要考虑如何在 CMake 中使用自己及其他预建的库

Android NDK 官网的使用现有库的文档中还是使用 ndk-build + Android.mk + Application.mk 组合的说明文档。(其实官方文档中大部分都是的,并没有使用 CMake

Github上的官方示例 里面有个项目 hello-libs 实现了如何创建出静态库/动态库,并引用它。

  • app - 从 $project/distribution/ 中使用一个静态库和一个动态库;
  •  
  • gen-libs - 生成一个动态库和一个静态库并复制到 $project/distribution/ 目录,你不需要再编译这个库,二进制文件已经保存在了项目中。当然,如果有需要你也可以编译自己的源码,只需要去掉 setting.gradleapp/build.gradle 中的注释,然后执行一次,接着注释回去,防止在 build 的过程中不受影响。


 

我们采用自底向上的方式分析模块,先看下 gen-libs 模块。

gen-libs/build.gradle

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                //-DANDROID_PLATFORM 代表编译的 android 平台,直接设置 minSdkVersion 就ok,这个参数可忽略
                arguments '-DANDROID_PLATFORM=android-9',
                          '-DANDROID_TOOLCHAIN=clang'
                //2种编译工具链 - clang(默认) 和 gcc(废弃)                
                 // explicitly build libs,编译哪些项目,默认都编译
                targets 'gmath', 'gperf'
            }
        }
    }
    ...
}
...

cpp/CMakeLists.txt

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)

set(lib_src_DIR ${CMAKE_CURRENT_SOURCE_DIR})

set(lib_build_DIR $ENV{HOME}/tmp)
file(MAKE_DIRECTORY ${lib_build_DIR})
//为构建添加一个子路径,子路径中的 CMakeLists.txt 也会被执行
add_subdirectory(${lib_src_DIR}/gmath ${lib_build_DIR}/gmath)
add_subdirectory(${lib_src_DIR}/gperf ${lib_build_DIR}/gperf)

cpp/gmath/CMakeLists.txt:静态库的

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_VERBOSE_MAKEFILE on)
//编译出一个静态库,源文件是 src/gmath.c
add_library(gmath STATIC src/gmath.c)

# copy out the lib binary... need to leave the static lib around to pass gradle check
set(distribution_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../distribution)
//是设置目标的一些属性来改变它们构建的方式
//gmath 的 ARCHIVE_OUTPUT_DIRECTORY 属性,改变了输出路径
set_target_properties(gmath
                      PROPERTIES
                      ARCHIVE_OUTPUT_DIRECTORY
                      "${distribution_DIR}/gmath/lib/${ANDROID_ABI}")
//自定义命令
# copy out lib header file...
add_custom_command(TARGET gmath POST_BUILD
                   COMMAND "${CMAKE_COMMAND}" -E
                   copy "${CMAKE_CURRENT_SOURCE_DIR}/src/gmath.h"
                   "${distribution_DIR}/gmath/include/gmath.h"
#                   **** the following 2 lines are for potential future debug purpose ****
#                   COMMAND "${CMAKE_COMMAND}" -E
#                   remove_directory "${CMAKE_CURRENT_BINARY_DIR}"
                   COMMENT "Copying gmath to output directory")

以上就是一个静态库/动态库的编译过程。总结以下3点

  1. 编译静态库/动态库
  2. 修改输出路径
  3. 复制暴露的头文件

app 模块是如何使用预建好的静态库/动态库的

app/src/main/cpp/CMakeLists.txt

cmake_minimum_required(VERSION 3.4.1)

# configure import libs
set(distribution_DIR ${CMAKE_SOURCE_DIR}/../../../../distribution)

# 创建一个静态库 lib_gmath 直接引用libgmath.a
add_library(lib_gmath STATIC IMPORTED)
set_target_properties(lib_gmath PROPERTIES IMPORTED_LOCATION
    ${distribution_DIR}/gmath/lib/${ANDROID_ABI}/libgmath.a)

# 创建一个动态库 lib_gperf 直接引用libgperf.so
add_library(lib_gperf SHARED IMPORTED)
set_target_properties(lib_gperf PROPERTIES IMPORTED_LOCATION
    ${distribution_DIR}/gperf/lib/${ANDROID_ABI}/libgperf.so)

# build application's shared lib
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")

# 创建库 hello-libs
add_library(hello-libs SHARED
            hello-libs.cpp)

# 加入头文件
target_include_directories(hello-libs PRIVATE
                           ${distribution_DIR}/gmath/include
                           ${distribution_DIR}/gperf/include)

# hello-libs库链接上 lib_gmath 和 lib_gperf
target_link_libraries(hello-libs
                      android
                      lib_gmath
                      lib_gperf
                      log)

可以看下基本上分成了4个步骤引入:

  1. 分别创建静态库/动态库,直接引用已经有的 .a 文件 或者 .so 文件
  2. 创建自己应用的库 hello-libs
  3. 加入之前暴露头文件
  4. 链接上静态库/动态库

编辑好并 Sync 后,你就可以发现 hello-libs 中的c/c++代码可以引用暴露的头文件调用内部方法了

3 资料文献

首推 Android NDK 官方文档,虽然很多都不完整,但是绝对是必须看一遍的东西。

当初次接触 NDK 开发又觉得新建的 Hello World 项目过于简单时。建议把 googlesamples - android-ndk 项目拉下来。里面有多个实例参考,比官方文档完整很多。

当你发现示例里的一些NDK配置满足不了你的需求后,你就需要到 CMake 官方文档 去查询完整的支持的函数,同时这里也提供一个中文翻译的简易的CMake手册

以上文档资料仅为了解决 NDK 开发过程中编译配置问题,具体 c/c++ 的逻辑编写、jni等不在此范畴。

彩蛋

文末献上一组彩蛋,将 CMake 或者 NDK 开发过程中遇到的坑和小技巧以 Q&A 的方式列出。持续更新https://www.jianshu.com/p/6332418b12b1

Q1:怎么指定 C++标准?

A:在 build_gradle 中,配置 cppFlags -std

externalNativeBuild {
  cmake {
    cppFlags "-frtti -fexceptions -std=c++14"
    arguments '-DANDROID_STL=c++_shared'
  }
}

Q2:add_library 如何编译一个目录中所有源文件

A: 使用 aux_source_directory 方法将路径列表全部放到一个变量中。

# 查找所有源码 并拼接到路径列表
aux_source_directory(${CMAKE_HOME_DIRECTORY}/src/api SRC_LIST)
aux_source_directory(${CMAKE_HOME_DIRECTORY}/src/core CORE_SRC_LIST)
list(APPEND SRC_LIST ${CORE_SRC_LIST})
add_library(native-lib SHARED ${SRC_LIST})

Q3:怎么调试 CMakeLists.txt 中的代码?

A:使用 message 方法

cmake_minimum_required(VERSION 3.4.1)
message(STATUS "execute CMakeLists")
...

运行后在

 .externalNativeBuild/cmake/debug/{abi}/cmake_build_output.txt 中查看 log

Q4:什么时候 CMakeLists.txt 里面会执行?

A:测试了下,好像在 sync 的时候会执行。执行一次后会生成 makefile 的文件缓存之类的东西放在 externalNativeBuild 中。所以如果 CMakeLists.txt 中没有修改的话再次同步好像是不会重新执行的。(或者删除 .externalNativeBuild 目录)

真正编译的时候好像只是读取.externalNativeBuild 目录中已经解析好的 makefile 去编译。不会再去执行 CMakeLists.txt



作者:Tsy远
链接:https://www.jianshu.com/p/6332418b12b1
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

https://www.jianshu.com/p/6332418b12b1

在命令行下用cmake交叉编译可在android中运行的so包https://blog.csdn.net/minghuang2017/article/details/78938852

so动态链接库[源】

so(shared object,共享库)是机器可以直接运行的二进制代码,是Android上的动态链接库,类似于Windows上的dll。每一个Android应用所支持的ABI是由其APK提供的.so文件决定的,这些so文件被打包在apk文件的lib/目录下,其中abi可以是上面表格中的一个或者多个。

为什么使用so

  • so机制让开发者最大化利用已有的C和C++代码,达到重用的效果,利用软件世界积累了几十年的优秀代码;
  • so是二进制,没有解释编译的开消,用so实现的功能比纯java实现的功能要快;
  • so内存分配不受Dalivik/ART的单个应用限制,减少OOM;
  • 相对于java代码,二进制代码的反编译难度更大,一些核心代码可以考虑放在so中。

猜你喜欢

转载自blog.csdn.net/ma15732625261/article/details/81779594
今日推荐