前言
文章所讲述主要来源于一个视频教学,算是一个笔记。不同的是,视频在讲述NDK开发时采用了传统的开发模式和C语言,在这里将讲述使用CMake来构建项目,以及使用C++实现本地方法,内容相对简单。
参考的资料
- Android Studio对NDK开发的支持
- 视频下载地址
- 谷歌官方文档(已翻译成中文)
- JNI/NDK开发指南
- 《Java核心技术 卷II》的第十二章
- 《Android开发艺术探索》的第十四章
概念
JNI
Java平台有一个用于和本地C/C++代码进行交互操作的API,称为Java本地接口(JNI)。可以把它理解成协议,JNI指导你如何实现Java与C/C++进行交互。
NDK
NDK是谷歌为了方便在Android应用中使用C/C++而发布的一款工具库。Android Studio2.2或更高版本对NDK提供了支持,默认的编译工具是CMake。
Java数据类型和C数据类型
Java编程语言 | C编程语言 | 字节 |
---|---|---|
boolean | jbolean | 1 |
byte | jbyte | 1 |
char | jchar | 2 |
short | jshort | 2 |
int | jint | 4 |
long | jlong | 8 |
float | jfloat | 4 |
double | jdouble | 8 |
boolean[] | jbooleanArray | / |
byte[] | jbyteArray | / |
char[] | jcharArray | / |
int[] | jintArray | / |
short[] | jshortArray | / |
long[] | jlongArray | / |
float[] | jfloatArray | / |
double[] | jdoubleArray | / |
Object[] | jobjectArray | / |
Class | jclass | / |
String | jstring | / |
环境搭建
我的环境:win10,Android Studio 3.2.0,ndk的版本r18b,CMake的版本3.6。
NDK配置
1、方法一,直接在其官网下载(NDK下载)
2、方法二,通过as来下载,打开settings界面,如下图所示:
3、配置环境变量(方便使用ndk命令),复制ndk目录下的build目录路径:我的电脑—>属性(右键)—>高级系统设置—>环境变量—>Path(系统变量)—>新建,然后粘贴保存,如下如图所示:
如果是win7、win8的记得用英文状态下的
;
进行分隔。4、打开cmd命令行窗口,输入
ndk-build
,然后回车,如果出现下图所示表示配置成功:CMake配置
打开as的Settings窗口,如下图所示:
CMake建议选择3.6的,3.10不知道是什么原因,在定义好本地方法后Alt+Enter的方式帮你生成对应的C/C++代码。而LLDB是C/C++的调试工具,选择最新版本就好。
项目创建步骤
1、创建一个新的项目,且勾选上include C++ support
选项,如下图所示:
2、步骤1后一直保持默认,一路
Next
下去,直到这一步为止,如下图所示:接着Finish就好,上图画圈的内容可以参考这篇博客。
3、如果你是选择从官网下载的NDK,那么项目会报一个错误——
NDK not configured.
,就是不知道NDK在那,如下图所示:解决方法如图所示:
然后等待项目构建完成。
4、项目构建完成后其运行的效果如下图所示:
分析
下图是项目的部分结构:
先来看
MainActivity
下的代码,如下:
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
// 固定写法,加载动态库,要在调用本地方法之前调用,可以写在其它类里
static {
// 库的名称是native-lib
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
// 本地方法的声明方式,该方法的主体实现在cpp目录下的native-lib.cpp,
// 而且是用C/C++来实现的
// 该方法的目的是从C/C++哪里得到一个字符串
public native String stringFromJNI();
}
在上述代码中的静态代码块中,我们怎么知道要加载的动态库的名称是native-lib
呢,请看app
目录下的CMakeLists.txt
:
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
# 编译后动态库的名称,可以改,但注意要和System.loadLibrary同步
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
# 编译生成的动态库所依赖的源文件,可以依赖多个源文件,也就是cpp目录下的cpp或c文件
# 比如cpp目下还有一个test.cpp文件,则书写如下,此处只是举个例子
src/main/cpp/native-lib.cpp
# src/main/cpp/test.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.
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.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
经过编译后,会生成一个名称为libnative-lib.so
的动态库,在名称前面的lib
是编译后加上的,因为我们在CMakeLists.txt
的配置中库的名称是native-lib
,另外,在使用System.loadLibrary
加载库时,库的名称写的时native-lib
,而不是libnative-lib
。其编译后的位置如下图所示(官网上介绍是如果未指定目标 ABI,则 CMake 默认使用 armeabi-v7a,我也不知道为啥我的demo第一次编译运行后是x86的,这个不影响后续的介绍):
最后看
cpp
目录下native-lib.cpp
,也就是对本地方法stringFromJNI
的实现:
// jni.h这个头文件必须要导进来
#include <jni.h>
#include <string>
// 你可以使用C++实现本地方法。然而,那样你必须将实现本地方法的函数声明为extern"C"
// (这可以阻止C++编译器混编方法名)
// 方法命名的格式:
// JNIEXPORT 返回类型 JNICALL Java_+方法的全类名(.用_替代)
extern "C" JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
// 标准模板库(STL)提供了一个std::string类
std::string hello = "Hello from C++";
// c_str(),一个将string转换为 const* char的函数
return env->NewStringUTF(hello.c_str());
}
关于字符串(摘自《Java核心技术 卷II》),Java编程语言中的字符串是UTF-16编码点的序列,而C的字符串则是以null结尾的字节序列,所以在这两种语言中的的字符串是很不一样的。Java本地接口有两组操作字符串的函数,一组把Java字符串转换成“改良的UTF-8”
字节序列,另一组将它们转换成UTF-16
数值的数组,也就是说转换成jchar
数组。
如果你的C代码已经使用了Unicode,你可以使用第二组转换函数。另一方面,如果你的字符串都仅限于使用ASCII字符,你可以使用“改良的UTF-8”转换函数。
NewStringUTF
函数可以用来构造一个新的jstring
,而读取现有jstring
对象的内容,需要使用GetStringUTFChars
函数。
接着是JNIEnv* env
,可以通过ctrl+鼠标左键
的方式,点击JNIEnv*
,定位到其具体的实现,可以参考这篇文章。可以得知,env
是一个二级指针,它是指向函数指针表的指针,封装了JNI开发所需的函数,如NewStringUTF
。
在C中,必须在每个JNI调用前面加上(*env)->
,以便实际上解析对函数指针的引用。在C++中,JNIEnv
类的C++版本有一个内联成员函数,它负责帮你查找函数指针,所以你可以这样使用:jstr = env->NewStringUTF(greeting)
。
jobject
和this
引用等价。静态方法得到的是类的引用,而非静态方法得到的是对隐式的this
参数对象的引用。
那么,接下来的内容就是以jni为主了。
Java调C++
新建类JavaCallC,如下:
public class JavaCallC {
// 让c代码做加法运算,把结果返回
public native int add(int x, int y);
// 从Java传入字符,c代码进行拼接
public native String stringConcatenation(String s);
// 让c代码给每个元素都加上10
public native int[] increaseArrayElements(int[] intArry);
}
此时,as会提示这些方法有错,那是因为在native-lib.cpp
并没有创建与之对应的函数,通过alter+enter
可以快速创建。C++的实现如下:
// jni.h这个头文件必须要导进来
#include <jni.h>
#include <string>
// 你可以使用C++实现本地方法。然而,那样你必须将实现本地方法的函数声明为extern"C"
// (这可以阻止C++编译器混编方法名)
// 方法命名的格式:
// JNIEXPORT 返回类型 JNICALL Java_+方法的全类名(.用_替代)
extern "C" JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
// 标准模板库(STL)提供了一个std::string类
std::string hello = "Hello from C++";
// c_str(),一个将string转换为 const* char的函数
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_add(JNIEnv *env, jobject instance, jint x, jint y) {
int result = x + y;
return result;
}
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_increaseArrayElements(JNIEnv *env, jobject instance,
jintArray intArry_) {
jint *intArry = env->GetIntArrayElements(intArry_, NULL); // 访问数组元素
jsize len = env->GetArrayLength(intArry_); // 数组长度
// 遍历数组,给每个元素加10
for (int i = 0; i < len; ++i) {
*(intArry + i) += 10;
}
env->ReleaseIntArrayElements(intArry_, intArry, 0);
return intArry_; // 注意这里返回的是intArry_,而不是intArry,原因在于指针
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_stringConcatenation(JNIEnv *env, jobject instance, jstring s_) {
const char *s = env->GetStringUTFChars(s_, 0); // 得到java传递的string,固定写法
// 使用C语言的方式来拼接字符串
char *fromC = ",I am C++.";
int len = strlen(s) + strlen(fromC);
char returnValue[len];
strcpy(returnValue, s); // 把s复制到returnValue
strcat(returnValue, fromC); // 把fromC所指向的字符串追加到returnValue所指向的字符串的结尾
// 虚拟机必须知道你何时使用完字符串,这样它就能进行垃圾回收(垃圾回收器是在一个独立线程中运行的,它能够中
// 断本地方法的执行),基于这个原因,你必须调用ReleaseStringUTFChars函数
env->ReleaseStringUTFChars(s_, s);
return env->NewStringUTF(returnValue); // 生成字符串并返回
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_greeting(JNIEnv *env, jclass type) { // 注意第二个参数
std::string returnValue = "Hello,I am static method.";
return env->NewStringUTF(returnValue.c_str());
}
C++调用Java
其大概流程如下:
1、在Java端定义一个native
方法并调用它
2、在C++端实现该native
方法,并实现C++调用Java的功能
编码签名
为了访问实例域和调用用Java编程语言定义的方法,你必须学习将数据类型的名称和方法名进行“混编”的规则(方法签名描述了参数和该方法返回值的类型)。下面混编方案:
代表 | 类型 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
Lclassname | 类的类型 |
S | short |
V | void |
Z | boolean |
为了描述数组类型,要使用[
。例如,一个字符串数组如下:
[Ljava/lang/String;
一个float[][]
可以描述为:
[[F
注意:在L
表达式结尾处的分号是类型表达式的终止符,而不是参数之间的分隔符。另外,在这个编码方案中,必须用/
代替.
分隔包和类名。
要建立一个方法的完整签名,需要把括号内的参数类型都列出来,然后列出返回值类型。例如,一个接收两个整形参数并返回一个整数的方法编码为:
(II)I
查看方法签名
方法一
先编译项目,然后在下图的所示的classes
的目录右键,选择Show in Explorer
:
注意是
classes
目录下!打开cmd
命令行窗口,cd
到此目录下,执行如下命令:
javap -s 全类名
// 如果要查看private的域
javap -s private 全类名
如下图所示:
方法二
假设,桌面上有一个Test.java
,内容如下:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Calendar;
public class Test
{
private int age;
public static void main(String[] args) {
Calendar calendar = Calendar.getInstance();
int currentYear = calendar.get(Calendar.YEAR);
int currentMonth = calendar.get(Calendar.MONTH);
int currentDay = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println("currentDay:" + currentDay);
}
}
查看其方法签名的大致步骤如下:
1、打开cmd
命令行窗口,cd
到Test.java
文件所在额目录
2、编译Test.java
——javac Test.java
3、执行命令——javap -s Test
效果如下图所示:
方法三
通过生成的头文件来查看。举个例子,如下图所示:
至于哪些报错,只要不影响头文件的生成就好。然后将生成的头文件剪切粘贴到
cpp
目录下,打开就可以查看方法签名了。
注意:我们在前面的步骤中生成了头文件且剪切粘贴到cpp目录下(如果目录下有其它的cpp或c文件,as自动生成的代码时也不一定生成在你想要放的文件里),新建的native方法通过alt+enter的方式生成的对应函数不一定生成在你想要的文件里,比如native-lib.cpp(什么原因造成还不知道)。你可以手动将自动生成的代码剪切粘贴到你想放的地方,可以先生成对应的函数再生成头文件,或者在cpp目录下新建一个includes目录专门存放头文件。所以,还是手动将它粘贴过来吧。
新建类CCallJava.java
,代码如下:
public class CCallJava {
/**
* 当执行这个方法的时候,让C代码调用
* public int add(int x, int y)
*/
public native void callbackAdd();
/**
* 当执行这个方法的时候,让C代码调用
* public void helloFromJava()
*/
public native void callbackHelloFromJava();
/**
* 当执行这个方法的时候,让C代码调用void printString(String s)
*/
public native void callbackPrintString();
/**
* 当执行这个方法的时候,让C代码静态方法 static void sayHello(String s)
*/
public native void callbackSayHello();
public int add(int x, int y) {
Log.e("TAG", "add() x=" + x + " y=" + y);
return x + y;
}
public void helloFromJava() {
Log.e("TAG", "helloFromJava()");
}
public void printString(String s) {
Log.e("TAG","C中输入的:" + s);
}
public static void sayHello(String s){
Log.e("TAG", "我是java代码中的JNI."
+ "java中的sayHello(String s)静态方法,我被C调用了:"+ s);
}
}
native-lib.cpp
更改如下:
// jni.h这个头文件必须要导进来
#include <jni.h>
#include <string>
// 你可以使用C++实现本地方法。然而,那样你必须将实现本地方法的函数声明为extern"C"
// (这可以阻止C++编译器混编方法名)
// 方法命名的格式:
// JNIEXPORT 返回类型 JNICALL Java_+方法的全类名(.用_替代)
extern "C" JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
// 标准模板库(STL)提供了一个std::string类
std::string hello = "Hello from C++";
// c_str(),一个将string转换为 const* char的函数
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_add(JNIEnv *env, jobject instance, jint x, jint y) {
int result = x + y;
return result;
}
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_increaseArrayElements(JNIEnv *env, jobject instance,
jintArray intArry_) {
jint *intArry = env->GetIntArrayElements(intArry_, NULL); // 访问数组元素
jsize len = env->GetArrayLength(intArry_); // 数组长度
// 遍历数组,给每个元素加10
for (int i = 0; i < len; ++i) {
*(intArry + i) += 10;
}
env->ReleaseIntArrayElements(intArry_, intArry, 0);
return intArry_; // 注意这里返回的是intArry_,而不是intArry,原因在于指针
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_stringConcatenation(JNIEnv *env, jobject instance, jstring s_) {
const char *s = env->GetStringUTFChars(s_, 0); // 得到java传递的string,固定写法
// 使用C语言的方式来拼接字符串
char *fromC = ",I am C++.";
int len = strlen(s) + strlen(fromC);
char returnValue[len];
strcpy(returnValue, s); // 把s复制到returnValue
strcat(returnValue, fromC); // 把fromC所指向的字符串追加到returnValue所指向的字符串的结尾
// 虚拟机必须知道你何时使用完字符串,这样它就能进行垃圾回收(垃圾回收器是在一个独立线程中运行的,它能够中
// 断本地方法的执行),基于这个原因,你必须调用ReleaseStringUTFChars函数
env->ReleaseStringUTFChars(s_, s);
return env->NewStringUTF(returnValue); // 生成字符串并返回
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_greeting(JNIEnv *env, jclass type) { // 注意第二个参数
std::string returnValue = "Hello,I am static method.";
return env->NewStringUTF(returnValue.c_str());
}
extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackAdd(JNIEnv *env, jobject instance) {
// 得到字节码,使用/代替.
jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");
// 得到方法,最后一个参数的方法签名
jmethodID jmethodIDs = env->GetMethodID(jclazz, "add", "(II)I");
// 实例化该类
jobject jobject1 = env->AllocObject(jclazz);
// 调用方法,方法的返回值是int
jint value = env->CallIntMethod(jobject1, jmethodIDs, 99, 1);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackHelloFromJava(JNIEnv *env, jobject instance) {
// 得到字节码,使用/代替.
jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");
// 得到方法,最后一个参数的方法签名
jmethodID jmethodIDs = env->GetMethodID(jclazz, "helloFromJava", "()V");
// 实例化该类
jobject jobject1 = env->AllocObject(jclazz);
// 调用方法,方法的返回值是void
env->CallVoidMethod(jobject1, jmethodIDs);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackPrintString(JNIEnv *env, jobject instance) {
// 得到字节码,使用/代替.
jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");
// 得到方法,最后一个参数的方法签名
jmethodID jmethodIDs = env->GetMethodID(jclazz, "printString", "(Ljava/lang/String;)V");
// 实例化该类
jobject jobject1 = env->AllocObject(jclazz);
// 调用方法
jstring jst = env->NewStringUTF("I am afu!!!(*env)->");
// 方法的返回值是void
env->CallVoidMethod(jobject1, jmethodIDs, jst);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackSayHello(JNIEnv *env, jobject instance) {
// 得到字节码,使用/代替.
jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");
// 得到方法,最后一个参数的方法签名
jmethodID jmethodIDs = env->GetStaticMethodID(jclazz, "sayHello", "(Ljava/lang/String;)V");
// 实例化该类
jstring jst = env->NewStringUTF("I am 123456android");
// 注意这个调用的是static方法
env->CallStaticVoidMethod(jclazz, jmethodIDs, jst);
}
打印日志
在C++
里打印Log
日志。
在app
目录下的build.gradle
文件添加如下代码:
ndk {
ldLibs 'log'
}
具体位置:
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.jwstudio.ndkdemo"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
// 生成.so库的目标平台,如果未指定目标 ABI,则 CMake 默认使用 armeabi-v7a,其配置如下
// abiFilters 'armeabi', 'armeabi-v7a'
}
}
ndk {
ldLibs 'log'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
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'
}
在类JavaCallC.java
中添加一个新的本地方法:
// 通过C++来打日志
public native void myLog();
在native-lib.cpp
中的顶部添加如下代码:
#include <android/log.h>
// 定义Log的tag
#define LOG_TAG "System.out"
// 不同等级的Log
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
myLog()
方法在native-lib.cpp
的实现如下:
extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_myLog(JNIEnv *env, jobject instance) {
// 打印不同等级的log
LOGD("result=%s", "result1");
LOGI("result=%s", "result2");
LOGE("result=%s", "result2");
}
总结
NDK开发的入门内容大致就这些,更详细的说明还请查看推荐的参考资料。除了一些必要的配置外,其实大部分内容都是与JNI相关,另外,可能还需要补一补C/C++的知识。