欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

程序员文章站 2022-05-22 19:04:18
目录 "1、基础概念" "├──1.1、JNI" "├──1.2、NDK" "├──1.3、CMake与ndk build" "2、环境搭建" "3、Native C++ 项目(HelloWord案例)" "├── 3.1、项目创建(java、kotlin)" "├── 3.2、CMake的应用(详 ......

目录

├──1.1、jni
├──1.2、ndk
├──1.3、cmake与ndk-build

3、native c++ 项目(helloword案例)

├── 3.2、cmake的应用(详细讲解)

1、基础概念

1.1、jni

jni(java native interface)java本地接口,使得java与c/c++具有交互能力

1.2、ndk

ndk(native development kit) 本地开发工具包,允许使用原生语言(c和c++)来实现应用程序的部分功能

android ndk开发的主要作用:

1、特定场景下,提升应用性能;
2、代码保护,增加反编译难度;
3、生成库文件,库可重复使用,也便于平台、项目间移植;

1.3、cmake与ndk-build

当我们基于ndk开发出native功能后,通常需要编译成库文件,给android项目使用。
目前,有两种主流的编译方式:__cmake__与ndk-build

__cmake__与__ndk-build__是两种不同的编译工具(与android代码和c/c++代码无关)

cmake

cmake是androidstudio2.2之后引入的跨平台编译工具(特点:简单易用,2.2之后是默认的ndk编译工具)

如何配置:
   1、创建cmakelists.txt文件,配置cmake必要参数;
   2、使用gradle配置cmakelists.txt以及native相关参数;

如何编译库文件:
   1、android studio执行build即可;

ndk-build

ndk-build是ndk中包含的脚本工具(可在ndk目录下找到该工具,为了方便使用,通常配置ndk的环境变量)

如何配置:
   1、创建android.mk文件,配置ndk-build必要参数;
   2、可选创建application.mk文件,配置ndk-build参数 (该文件的配置项可使用gradle的配置替代);
   3、使用gradle配置android.mk以及native相关参数;

2、如何编译库文件(两种方式):
   1、android studio执行build即可(执行了:android.mk + gradle配置);
   2、也可在terminal、mac终端、cmd终端中通过ndk-build命令直接构建库文件(执行了:android.mk)

2、环境搭建

jni安装
jni 是jdk里的内容,电脑上正确安装并配置jdk即可 (jdk1.1之后就正式支持了);

ndk安装
可从官网自行下载、解压到本地,也可基于androidstudio下载解压到默认目录;

编译工具安装
cmake 可基于androidstudio下载安装;
ndk-build 是ndk里的脚本工具,ndk安装好即可使用ndk-build;

当前演示,使用的android studio版本如下(当前最新版):

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

启动android studio --> 打开sdk manager --> sdk tools,如下图所示:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

我们选择ndk、cmake、lldb(调试native时才会使用),选择apply进行安装,等安装成功后,ndk开发所依赖的环境也就都齐全了。

3、native c++ 项目(helloword案例)

3.1、项目创建(java / kotlin)

新建项目,选择 native c++,如下图:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

新创建的项目,默认已包含完整的native 示例代码、cmake配置 ,如下图:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

这样,我们就可以自己定义java native方法,并在cpp目录中写native实现了,很方便。

但是,当我们写完native的实现代码,希望运行app,查看jni的交互效果,此时,就需要使用编译工具了,咱们还是先看一下android studio默认的native编译方式吧:cmake

3.2、cmake的应用

在cmake编译之前,咱们应该先做哪些准备工作?

1、ndk环境是否配置正确?
-- 如果未配置正确是无法进行c/c++开发的,更不用说cmake编译了

2、c/c++功能是否实现? 
-- 此次演示主要使用系统默认创建的native-lib.cpp文件,关于具体如何实现:后续文章再详细讲解

3、cmakelists.txt是否创建并正确配置? 
-- 该文件是cmake工具编译的基础,未配置或配置项错误,均会影响编译结果

4、gradle是否正确配置?
-- gradle配置也是cmake工具编译的基础,未配置或配置项错误,均会影响编译结果

除此之外,咱们还应该学习cmake的哪些重要知识?

1、cmake工具编译生成的库文件默认在什么位置?apk中库文件又是在什么位置?
2、cmake工具如何指定编译生成的库文件位置?
3、cmake工具如何指定生成不同cpu平台对应的库文件?

带着这些问题,咱们开始cmake之旅吧:

3.2.1、ndk环境检查

编译前,建议先检查下工程的ndk配置情况(不然容易报一些乱七八糟的错误):
file --> project structure --> sdk location,如下图(我本地的android studio默认没有给配置ndk路径,那么,需要自己手动指定一下):

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

3.2.2、c/c++功能实现

因为本节主讲cmake编译工具,代码就不单独写了,咱们直接使用工程默认生成的native-liv.cpp,简单调整一下native实现方法的代码吧(修改返回文本信息):

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

因native c++工程默认已配置好了cmakelists.txt和gradle,所以咱们可直接运行工程看效果,如下图:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

jni交互效果我们已经看到了,说明cmake编译成功了。那么,这究竟是怎么做到的呢?咱们接着分析一下吧:

3.2.3、cmake生成的库文件与apk中的库文件

安卓工程编译时,会执行cmake编译,在 工程/app/build/.../cmake/ 中会产生对应的so文件,如下图:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

继续编译安卓工程,会根据build中的内容,生成我们的*.apk安装包文件。我们找到、反编译apk安装包文件,查找so库文件。原来在apk安装包中,so库都被存放在lib目录中,如下图:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

3.2.4、cmake是如何编译生成so库的呢?

在前面介绍cmake定义时,提到了cmake是基于cmakelists.txt文件和gradle配置实现编译native类的。那么,咱们先来看一下cmakelists.txt文件吧:

#cmake最低版本要求
cmake_minimum_required(version 3.4.1)

#添加库
add_library(
        # 库名
        native-lib

        # 类型:
        # shared 是指动态库,对应的是.so文件
        # static 是指静态库,对应的是.a文件
        # 其他类型:略
        shared

        # native类路径
        native-lib.cpp)

# 查找依赖库
find_library( 
        # 依赖库别名
        log-lib

        # 希望加到本地的ndk库名称,log指ndk的日志库
        log)


# 链接库,建立关系( 此处就是指把log-lib 链接给 native-lib使用 )
target_link_libraries( 
        # 目标库名称(native-lib 是咱们要生成的so库)
        native-lib

        # 要链接的库(log-lib 是上面查找的log库)
        ${log-lib})

实际上,cmakelist.txt可配置的内容远不止这些,如:so输出目录,生成规则等等,有需要的同学可查下官网。

接着,咱们再看一下app的gradle又是如何配置cmake的呢?

apply plugin: 'com.android.application'

android {
    compilesdkversion 29
    buildtoolsversion "29.0.1"
    defaultconfig {
        applicationid "com.qxc.testnativec"
        minsdkversion 21
        targetsdkversion 29
        versioncode 1
        versionname "1.0"
        testinstrumentationrunner "androidx.test.runner.androidjunitrunner"
        //定义cmake默认配置属性
        externalnativebuild {
            cmake {
                cppflags ""
            }
        }
    }
    
    //定义cmake对应的cmakelist.txt路径(重要)
    externalnativebuild {
        cmake {
            path "src/main/cpp/cmakelists.txt"
        }
    }
}

dependencies {
    implementation filetree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testimplementation 'junit:junit:4.12'
    androidtestimplementation 'androidx.test.ext:junit:1.1.1'
    androidtestimplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

实际上,gradle可配置的cmake内容也远不止这些,如:abi、cppflags、arguments等,有需要的同学可查下官网。

3.2.5、如何指定库文件的输出目录?

如果希望将so库生成到特定目录,并让项目直接使用该目录下的so,应该如何配置呢?
比较简单:需要在cmakelist.txt中配置库的输出路径信息,即:

cmake_library_output_directory

# cmake最低版本要求
cmake_minimum_required(version 3.4.1)

# 配置库生成路径
# cmake_current_source_dir是指 cmake库的源路径,通常是build/.../cmake/
# /../jnilibs/是指与cmakelist.txt所在目录的同级目录:jnilibs (如果没有会新建)
# android_abi 生成库文件时,采用gradle配置的abi策略(即:生成哪些平台对应的库文件)
set(cmake_library_output_directory ${cmake_current_source_dir}/../jnilibs/${android_abi})

# 添加库
add_library( # 库名
        native-lib

        # 类型:
        # shared 是指动态库,对应的是.so文件
        # static 是指静态库,对应的是.a文件
        # 其他类型:略
        shared

        # native类路径
        native-lib.cpp)

# 查找依赖库
find_library(
        # 依赖库别名
        log-lib

        # 希望加到本地的ndk库名称,log指ndk的日志库
        log)


# 链接库,建立关系( 此处就是指把log-lib 链接给native-lib使用 )
target_link_libraries(
        # 目标库名称(native-lib就是咱们要生成的so库)
        native-lib

        # 要链接的库(上面查找的log库)
        ${log-lib})

还需要在gradle中配置 jnilibs.srcdirs 属性(即:指定了lib库目录):

sourcesets {
        main {
            jnilibs.srcdirs = ['jnilibs']//指定lib库目录
        }
    }

接着,重新build就会在cpp相同目录级别位置生成jnilibs目录,so库也在其中了:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

注意事项:
1、配置了cmake_current_source_dir,并非表示编译时直接将so生成在该目录中,实际编译时,so文件仍然是
先生成在build/.../cmake/中,然后再拷贝到目标目录中的

2、如果只配置了cmake_current_source_dir,并未在gradle中配置 jnilibs.srcdirs,也会有问题,如下:
more than one file was found with os independent path 'lib/arm64-v8a/libnative-lib.so'

此问题是指:在编译生成apk时,发现了多个so目录,android studio不知道使用哪一个了,所以需要咱们
告诉android studio当前工程使用的是jnilibs目录,而非build/.../cmake/目录
3.2.5、如何生成指定cpu平台对应的库文件呢?

我们可以在cmake中设置abifilters,也可在ndk中设置abifilters,效果是一样的:

defaultconfig {
        applicationid "com.qxc.testnativec"
        minsdkversion 21
        targetsdkversion 29
        versioncode 1
        versionname "1.0"
        testinstrumentationrunner "androidx.test.runner.androidjunitrunner"
        externalnativebuild {
            cmake {
                cppflags ""
                abifilters "arm64-v8a"
            }
        }
    }
defaultconfig {
        applicationid "com.qxc.testnativec"
        minsdkversion 21
        targetsdkversion 29
        versioncode 1
        versionname "1.0"
        testinstrumentationrunner "androidx.test.runner.androidjunitrunner"
        externalnativebuild {
            cmake {
                cppflags ""
            }
        }
        ndk {
            abifilters "arm64-v8a"
        }
    }

按照新的配置,我们重新运行工程,如下图:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

再反编译看下工程,果然只有arm64-v8a的so库了,不过库文件在apk中仍然是放在lib目录,而非jnilibs(其实也很好理解,jnilibs只是我们本地的目录,便于我们管理库文件,真正生成apk时,仍然会按照lib目录放置库文件),如下图:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

至此,cmake的主要技术点都讲完了,接下来咱们看下ndk-build吧~

3.3、ndk-build的应用

在ndk-build编译之前,咱们又应该先做哪些准备工作?

1、ndk-build环境变量是否正确配置?
-- 如果未配置,是无法在cmd、mac终端、terminal中使用ndk-build命令的(会报错:找不到命令)

2、ndk环境是否配置正确?
-- 如果未配置正确是无法进行c/c++开发的,更不用说ndk-build编译了

3、c/c++功能是否实现?
-- 此次演示主要使用系统默认创建的native-lib.cpp文件,关于具体如何实现:后续文章再详细讲解
-- 注意:为了与cmake区分,咱们新建一个“jni”目录存放c/c++文件、配置文件吧

4、android.mk是否创建并正确配置? 
-- 该文件是ndk-build工具编译的基础,未配置或配置项错误,均会影响编译结果

5、gradle是否正确配置?(可选项,如果通过cmd、mac终端、terminal执行ndk-build,可忽略)
-- gradle配置也是ndk-build工具编译的基础,未配置或配置项错误,均会影响编译结果

6、application.mk是否创建并正确配置?(可选项,一般配abi、版本,这些项均可在gradle中配置)
-- 该文件也是ndk-build工具编译的基础,未配置或配置项错误,均会影响编译结果

除此之外,咱们还应该学习ndk-build的哪些重要知识?

1、ndk-build工具如何指定编译生成的库文件位置?
2、ndk-build工具如何指定生成不同cpu平台对应的库文件?

带着这些问题,咱们继续ndk-build之旅吧:

3.3.1、环境变量配置

介绍ndk-build定义时,提到了其实它是ndk的脚本工具。那么,咱们还是先进ndk目录找一下吧,ndk-build工具的位置如下图:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

如果我们希望任意情况下都能便捷的使用这种脚本工具,通常做法是配置其环境变量,否则我们在cmd、mac终端、terminal中执行 ndk-build 命令时,会报错:“未找到命令”

配置ndk的环境变量,也很简单,以mac电脑举例(如果是windows电脑,网上也有很多关于配置环境变量的文章,如果有需要可自行查下):

1、打开命令终端,输入命令: open -e .bash_profile,打开bash_profile配置文件

2、写入如下内容(ndk_home指向 ndk-build 所在路径):
export ndk_home=/users/xc/sdk/android-sdk-macosx/ndk/20.1.5948944
export path=$path:$ndk_home

3、生效.bash_profile配置
source .bash_profile

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

当我们在cmd、mac终端、terminal中执行 ndk-build 命令时,如果出现下图所示内容,则代表配置成功了:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

3.3.2、c/c++功能实现

咱们使用比较常用的一种ndk-build方式吧:ndk-build + android.mk + gradle配置

项目中新建jni目录,拷贝一份cmake的代码实现吧:

1、新建jni目录
2、拷贝cpp/native-lib.cpp 至 jni目录下
3、重命名为haha.cpp (与cmake区分)
4、调整一下native实现方法的文本(与cmake运行效果区分)
5、新建android.mk文件

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

接着,编写android.mk文件内容:

#表示android.mk所在目录
local_path := $(call my-dir)

#clear_vars变量指向特殊 gnu makefile,用于清除部分local_变量
include $(clear_vars)

#模块名称
local_module    := haha
#构建系统用于生成模块的源文件列表
local_src_files := haha.cpp

#build_shared_library 表示.so动态库
#build_static_library 表示.a静态库
include $(build_shared_library)

配置gradle:

apply plugin: 'com.android.application'
android {
    compilesdkversion 28
    defaultconfig {
        applicationid "com.aaa.testnative"
        minsdkversion 16
        targetsdkversion 28
        versioncode 1
        versionname "1.0"
        testinstrumentationrunner "android.support.test.runner.androidjunitrunner"

        //定义ndkbuild默认配置属性
        externalnativebuild {
            ndkbuild {
                cppflags ""
            }
        }
    }
   
    //定义ndkbuild对应的android.mk路径(重要)
    externalnativebuild {
        ndkbuild{
            path "src/main/jni/android.mk"
        }
    }
}

dependencies {
    implementation filetree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testimplementation 'junit:junit:4.12'
    androidtestimplementation 'com.android.support.test:runner:1.0.2'
    androidtestimplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

现在,native代码、ndk-build配置都完成了,咱们运行看一下效果吧,如下图:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

3.3.4、如何指定库文件的输出目录?

通常,可在android.mk文件中配置ndk_app_dst_dir
指定源目录与输出目录(与cmake类似)

#表示android.mk所在目录
local_path := $(call my-dir)

#设置库文件的输入目录
#输出目录 ../jnilibs/
#源目录 $(target_arch_abi)
ndk_app_dst_dir=../jnilibs/$(target_arch_abi)

#clear_vars变量指向特殊 gnu makefile,用于清除部分local_变量
include $(clear_vars)

#模块名称
local_module    := haha
#构建系统用于生成模块的源文件列表
local_src_files := haha.cpp

#build_shared_library 表示.so动态库
#build_static_library 表示.a静态库
include $(build_shared_library)
3.3.5、如何生成指定cpu平台对应的库文件呢?

可在gradle中配置abifilters(与cmake类似)

externalnativebuild {
            ndkbuild {
                cppflags ""
                abifilters "arm64-v8a"
            }
        }
externalnativebuild {
            ndkbuild {
                cppflags ""
            }
        }
  ndk {
            abifilters "arm64-v8a"
        }
3.3.6、如何在terminal中直接通过ndk-build命令构建库文件呢?

除了执行androidstudio的build命令,基于gradle配置 + android.mk编译生成库文件,我们还可以在cmd、mac 终端、terminal中直接通过ndk-build命令构建库文件,此处以terminal为例进行演示吧:

先进入包含android.mk文件的jni目录(android studio中可直接选中jni目录并拖拽到terminal中,会自动跳转到该目录),再执行ndk-build命令,如下图:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

同样,编译也成功了,如下图:

安卓JNI精细化讲解,让你彻底了解JNI(一):环境搭建与HelloWord

因是直接在terminal中执行了ndk-build命令,所以只会根据android.mk进行编译(不包含gradle配置内容,也就不会执行abifilters过滤),生成了所有默认cpu平台的so库文件。

ndk-build命令其实也可以配上一些参数使用,此处就不再详解了。日常开发时,还是建议选择cmake作为native编译工具,因为是安卓主推的,而且更简单一些。