注意,例子使用的各版本信息如下:
AS 3.3
Gradle Tool:3.3.0
Gradle Version:4.10.1
1. 概述
最近在阅读Android 源码的过程中发现大量的Native方法,在没有系统掌握JNI与NDK知识的情况下寸步难行,所以有必要系统地了解相关知识。
在学习之前,我常常有有如下几个疑问:
NDK的开发流程?
C/C++ 与 Java如何进行通信的?
如何阅读Android Native 源码?
希望经过一系列的学习和总结,以上的问题能得到解决。 今天首先解决“如何进行NDK开发?”
1.1. JNI:
- JNI :
- Java Native Interface,Java本地编程接口,是一套编程规范,提供了本地语言(C/C++)与Java 语言相互通信的一套API接口;
- Java是跨平台语言,因为其背后依赖Java虚拟机,Java源码编译的.class字节码文件运行在虚拟机上,而虚拟机往往由C/C++编写,那么两者是如何通信的呢?——JNI;
- 通过JNI, Java可以调用C/C++的代码,反过来亦然;
- JNI 是Java 语言的一部分,而非Android 平台特有;
- 作用:
- 提高效率:加密算法、图像处理、机器视觉等需要大量计算的逻辑,可以在C/C++中实现;
- 扩展JVM:扩展JVM的功能;
- 代码复用:复用C/C++平台上的代码,避免重复在Java上重复造轮子;
1.2. NDK:
-
NDK:
- Native Develop Kit,本地开发工具,是Google开发的一套方便开发者在Android 平台上开发Native 代码的工具;
- NDK 是Android 的一部分,与Java无直接关系;
-
作用:
- 方便快速:使用NDK自带的工具,快速对C/C++代码进行构建、编译和打包;
- 交叉编译:快速生成不同CPU平台的动态库;
2. NDK开发流程
在NDK的开发过程中,有两种形式——ndk-build和CMake,其实两种方式最终的目的都是一样,将C 或 C++(“原生代码”)嵌入到 Android 应用。只是CMake使用起来更加方便,同时Android官方也推荐在Android Studio 2.2及以上使用CMake。工具的本质就是方便人们进行日常活动,对比一下两者流程,方便开发过程进行技术选型。
2.1. ndk-build
-
下载和配置NDK:
ndk-build只是NDK工具包里面的一个脚本工具,帮忙调用NDK里面正确的构建脚本,路径在sdk/ndk-bundle目录下。
需要在Studio中下载与配置NDK:Settings–Appearance & Behavior–System Settings–Android SDK–SDK Tools。
-
Java中创建本地方法
在Java类HelloWorldJNI中创建本地方法:public class HelloWorldJNI { // 加载Native动态库(so库),动态库的名称后面在mk文件中会使用到 static { System.loadLibrary("helloworld_jni"); } // 定义Native方法 public native String getString(); public native String setString(String text); }
-
创建JNI文件目录并链接项目
在src\main 目录下创建 jni文件夹,JNI相关的编码都在讲在该文件下进行,有两种创建方式:
a. Android Studio UI下右击main文件下创建:
b. 手动创建jni文件:
在main文件夹手动创建jni文件夹,然后在项目的gradle中配置android { ...... externalNativeBuild { ndkBuild { path 'src/main/jni/Android.mk' } } }
或者
两者效果一样,推荐使用方法a,简单快捷。 -
生成本地方法头文件
在命令终端Terminal中进入到项目的java文件目录中,如本例中的 DemoProjects\JNITest\src\main\java,执行命令:javah -jni -encoding UTF-8 -d <你的工程目录>\DemoProjects\JNITest\src\main\jni com.wuzl.jnitest.HelloWorldJNI
-jni:表示生成jni格式的头文件;
-encoding:编码格式;
-d:头文件的输出路径;javah 命令导出HelloWorldJNI的头文件(com_wuzl_jnitest_HelloWorldJNI.h)至jni文件目录中,重点来了!
头文件的命名规则为:
包名(.变成下划线)
其函数命名遵循如下规则:
Java_包名_类名_方法名/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_wuzl_jnitest_HelloWorldJNI */ #ifndef _Included_com_wuzl_jnitest_HelloWorldJNI #define _Included_com_wuzl_jnitest_HelloWorldJNI #ifdef __cplusplus extern "C" { #endif /* * Class: com_wuzl_jnitest_HelloWorldJNI * Method: getString * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_getString (JNIEnv *, jobject); /* * Class: com_wuzl_jnitest_HelloWorldJNI * Method: setString * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_setString (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
-
创建C/C++ 代码文件:
在jni目录下创建helloworld.cpp,然后导入上面生成的头文件,并实现头文件声明的两个本地方法# include <jni.h> # include <stdio.h> #include <cstring> # include "com_wuzl_jnitest_HelloWorldJNI.h" # ifdef _cplusplus extern "C" { # endif JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_getString(JNIEnv *env, jobject obj){ return env -> NewStringUTF("Get Hello world JNI!"); } JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_setString(JNIEnv *env, jobject obj, jstring str){ char* jnistr = (char *) env->GetStringUTFChars(str, NULL); strcat(jnistr,": I am JNI"); return env -> NewStringUTF(jnistr); } # ifdef _cplusplus } # endif
-
创建配置文件:
在jni文件目录下配置Android.mk和Application.mk配置文件
Android.mk# 当前模块路径,即Android.mk所在的文件目录 LOCAL_PATH := $(call my-dir) # 开始清除LOCAL_XXX变量 include $(CLEAR_VARS) # 动态库模块名称,Java代码中中加载动态库中使用System.loadLibrary("helloworld_jni"); LOCAL_MODULE := helloworld_jni # 编译的C/C++源文件,可多个,以空格隔开 LOCAL_SRC_FILES := helloworld.cpp # 开始动态编译生成动态库 include $(BUILD_SHARED_LIBRARY)
# 需要编译的ABI APP_ABI :=armeabi-v7a x86 x86_64
配置文件的详细选项可查看Google官网:
Android.mk, Application.mk -
直接运行
-
可利用ndk-build命令工具生成so库:
在命令终端Terminal中进入到jni目录的父目录,直接执行ndk-build命令(需要将ndk目录添加到环境变量中)将会在当前目录下生成libs和obj文件目录,其中libs就是我们想要so库
小技巧:
ndk-build NDK开发流程讲得差不多了,但是有个小技巧可以分享给大家,在上述流程中,每次都需要输入命令来生成头文件和so库,而且有时候忘记命令了,其实AS提供了相关技巧来优化开发流程:
Settings–External Tools–“+”
其中 Program:javah所在的路径;
Arguments: 命令参数,此处为-jni -encoding UTF-8 -d
M
o
d
u
l
e
F
i
l
e
D
i
r
ModuleFileDir
ModuleFileDir\src\main\jni
F
i
l
e
C
l
a
s
s
FileClass
FileClass;
Wroking directory: 工作目录,此处为
M
o
d
u
l
e
F
i
l
e
D
i
r
ModuleFileDir
ModuleFileDir\src\main\java;
使用(右击定义Native方法的Java文件,选择External Tools-javah-jni,然后在jni文件下直接生成对应的头文件):
2.2. CMake
CMake并不是什么新鲜的内容,脱离Android来说,CMake是一个跨平台的安装编译工具,通过简单的脚本语句描述不同平台的编译过程,已经运用在UNIX和WindowW系统中。
Google在AS2.2版本上开始支持CMake,并提倡在AS 2.2及以上版本使用CMake进行JNI开发,在上述使用ndk-build开发过程中发现,需要的步骤特别多,比如生成头文件、配置Android.mk, Application.mk文件,并需要了解其中mk各种繁琐的配置项,而使用CMake进行开发可以省略这些步骤。
官网已经给出非常详细的使用过程:向您的项目添加 C 和 C++ 代码
如果是新建项目使用CMake进行JNI开发,非常简单,在创建新项目的时向导面板中选中 Include C++ Support 复选框即可,AS会创建一个简单的DEMO供使用参考。
但是我们大多数都是需要在已存在的项目中进行JNI的开发,下面将以此作为讲解。
-
创建CMakeLists.txt配置文件
在项目根目录下创建CMakeLists.txt配置文件# CMake 最小工具版本要求,可通过SDK Manager 查看CMake的本地版本 # 该版本对Gradle有要求,比如3.10 版本需要Gragle 4.10+version才行 cmake_minimum_required(VERSION 3.6) add_library( # Sets the name of the library. # 动态库的名称,与ndk-build中Android.mk 的模块名一样 # 最终编译出来的动态库名称为:lib+library name helloworld # Sets the library as a shared library. # 编译库的类型,有静态库、动态库、和模块库 SHARED # Provides a relative path to your source file(s). # C/C++ 源文件 src/main/cpp/helloworld.cpp) # Searches for a specified prebuilt library and stores the path as a # variable. Because CMake includes system libraries in the search path by # default, you only need to specify the name of the public NDK library # you want to add. CMake verifies that the library exists before # completing its build. # 找到依赖库,比如下面将系统log-lib链接到helloworld动态库 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 ) # Specifies libraries CMake should link to your target library. You # can link multiple libraries, such as libraries you define in this # build script, prebuilt third-party libraries, or system libraries. # 配置库的链接 target_link_libraries( # Specifies the target library. helloworld # Links the target library to the log library # included in the NDK. ${log-lib} )
这个是最最基本的CMake的配置,CMake还可以做很多强大的配置选项,详情可查看CMake的官方文档(https://cmake.org/documentation/)
-
创建C/C++源文件
在main目录(当然目录可以自己选择)下创建cpp文件夹,然后在该文件夹下创建helloworld.cpp源文件,与ndk-build一致:# include <jni.h> # include <stdio.h> # include <cstring> extern "C" { JNIEXPORT jstring JNICALL Java_com_wuzl_cmake_HelloWorldJNI_getString(JNIEnv *env, jobject obj){ return env -> NewStringUTF("Get Hello world JNI"); } JNIEXPORT jstring JNICALL Java_com_wuzl_cmake_HelloWorldJNI_setString(JNIEnv *env, jobject obj, jstring str){ char* jnistr = (char *) env->GetStringUTFChars(str, NULL); strcat(jnistr,": I am JNI by CMake building"); return env -> NewStringUTF(jnistr); } }
-
链接项目
右击项目,选择Lick C++ Project with Gradle,然后在弹出来的选项里选择CMake并选择上面配置好的CMakeLists.txt
-
运行查看结果
3. 总结
NDK开发的两种流程:
- ndk-build+Android.mk+Application.mk
- CMake+CMakeLists.txt
两者只是构建的脚本和命令不同而已,但是个人感觉CMake更加方便和简洁
Google推荐使用CMake进行JNI的开发,而且CMake的配置更加强大,其官方文档可查阅使用的细节(https://cmake.org/documentation/)。
最后上DEMO:https://github.com/PlepleLiang/JNIDemo
参考:
https://blog.csdn.net/quwei3930921/article/details/78820991
https://juejin.im/post/5a67dcdb518825732c53b338
https://developer.android.com/studio/projects/add-native-code?hl=zh-cn