1 NDK
NDK全称是Native Develop Kit,翻译作原生开发工具包。它允许你为Android使用C/C++代码来实现应用程序的功能。换言之Android的SDK之外,有一个工具叫NDK,用于进行C/C++的开发。一般情况,是用NDK工具把C/C++编译为.so文件,然后在Java中调用。NDK 可能不适合大多数 Android 编程初学者,这些初学者只需使用 Java 代码和框架 API 来开发应用。然而,如果你需要完成一或多个以下事项,那么 NDK 就能派上用场:
提高代码的安全性,因为Java层代码比较容易被反编译,而so库反编译比较困难。
重复使用目前已有的 C/C++ 库。
通过C/C++实现的动态库可以很方便地在其他平台间移植使用
进一步提升设备性能,以实现低延迟时间,或运行计算密集型应用,如游戏或物理模拟。
2 JNI
JNI全称是Java Native Interface,即Java本地接口。JNI是Java调用Native 语言的一种特性,它是为了方便Java和C/C++等本地代码相互调用所封装的一层接口。
3 配置NDK环境
第一步,下载NDK
在Android Studio的”Default Preferences”中勾选NDK项进行下载NDK,待下载成功后,便可在”Project Structure”中查看到NDK的目录,操作如下面图:
第二步,配置环境变量
打开“终端”,并输入命令:echo $PATH 查看当前环境变量
输入:sudo vi ~/.bash_profile,按回车输入密码后用vi打开用户目录下的bash_profile文件。一定要用sudo,否则没权限保存文件
按i键,开始编辑,并将你电脑里NDK路径填写到后面
编辑完之后,按ESC键,输入:wq,就可以保存退出了,如果不想保存就输入:q就可以了
4 Hello world
准备工作完成后,现在我们就来开始创建一个JNI的Demo,该Demo很简单,就是使Java能正常调用到C++代码而已。创建工程后,正常情况下会看到local.properties文件中有指定了ndk的目录,如果没有则要手动加上:
ndk.dir=/Users/liyizhi/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/liyizhi/Library/Android/sdk
第一步,创建JNIUtils类,在类中将执行加载so库文件和定义一个native方法getInfo,并在MainActivity中调用getInfo方法:
JNIUtils.java
public class JNIUtils {
static {
System.loadLibrary("jni-demo");
}
public static native String getInfo();
}
MainActivity.java
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String info = JNIUtils.getInfo();
TextView tv = findViewById(R.id.tv1);
tv.setText(info);
}
}
第二步,执行命令,生成.h头文件。如下图,执行了两条命令,首先通过命令:cd app/src/main/java跳转到工程中java文件夹中,然后使用命令:javac -h . com/zyx/jnidemo/JNIUtils.java 生成了对应的.h文件。这里笔者使用的是jdk1.8,如果是jdk较前的版本中,应该要使用命令:javah -jni com.zyx.jnidemo.JNIUtils。
上面.h头文件代码需要做一下说明
JNIEXPORT和JNICALL: 是JNI中定义的宏,可以在jni.h这个头文件中查找到
Jstring: 是代表的是getInfo方法的String返回类型
Java_com_zyx_jnidemo_JNIUtils_getInfo: 是函数名,格式遵循如下规则:Java_包名_类名_方法名
JNIEnv*: 表示一个指向JNI环境的指针,可以通过它来访问JNI提供的接口方法
jclass: 表示Java对象中的this
第三步,创建JNI Folder,并将com_zyx_jnidemo_JNIUtils.h头文件移至该文件夹中
第四步,在刚创建的jni文件夹中创建2个文件:JNIUtils.cpp和Android.mk,它们的实现如下所示:
JNIUtils.cpp
#include "com_zyx_jnidemo_JNIUtils.h"
#include <stdio.h>
#include <android/log.h>
JNIEXPORT jstring JNICALL Java_com_zyx_jnidemo_JNIUtils_getInfo(JNIEnv * env, jclass thiz) {
__android_log_print(ANDROID_LOG_DEBUG, "zyx", "Hello world from JNI !");
return env->NewStringUTF("Hello world from JNI !");
}
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jni-demo
LOCAL_SRC_FILES := JNIUtils.cpp
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
上面代码中,LOCAL_MODULE表示模块的名称,LOCAL_SRC_FILES表示需要参与编译的源文件,LOCAL_LDLIBS表示在C++代码中支持log日志输出。
第五步,修改app目录下的build.gradle,如下代码,其中说明请看注释:
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.zyx.jnidemo"
minSdkVersion 22
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk {
moduleName "jni-demo" // so库的名称
abiFilters "armeabi-v7a", "x86" // 支持的cpu构架平台类型,all表示编译所有cpu平台
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk' // 指向Android.mk文件
}
}
}
}
第六步,执行编译运行,这时就会在app/build/intermediates/ndkBuild/debug/obj/local目录下生成对应两类so文件,这里两类是对应着gradle中配置的cpu构架平台类型。然后将程序运行到手机上便会看到Java成功调用了C++代码返回了结果。
5 引用外部so库
在实际开发过程中,往往C++工程是跟Android工程分离,或者Android工程中直接引用外部提供现成的so库文件。现在我们就来模拟一下这种情况的发生。
首先在jni文件夹中再添加一下Application.mk文件,代码如下:
Application.mk
APP_ABI := armeabi-v7a,x86
接着切换到jni目录的父目录,然后通过命令:ndk-build来手动编译产生so库,这时候NDK会创建一个和jni目录平级的目录libs,libs下面存放着就是so库。
然后修改Gradle文件:
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.zyx.jnidemo"
minSdkVersion 22
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// ndk {
// moduleName "jni-demo" // so库的名称
// abiFilters "armeabi-v7a", "x86" // 支持的cpu构架平台类型,all表示编译所有cpu平台
// }
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
// externalNativeBuild {
// ndkBuild {
// path 'src/main/jni/Android.mk' // 指向Android.mk文件
// }
// }
sourceSets.main {
jni.srcDirs = [] // 禁用自动NDK生成调用
jniLibs.srcDirs = ['src/main/libs'] // so库存放目录
}
}
}
这里要说明一下,因为JNI的代码是必须放在上面创建的jni文件夹里,如果不想采用jni这个名称,可以在Gradle通过jin.srcDirs指定JNI代码的路径。因为我们上例中JNI的代码是默认放在了创建的jni文件夹里,就必须得加上jni.srcDirs=[],否则会编译不通过。
最后就是再次执行编译运行,可以再次看到程序运行到手机上也能展示同样的结果
另外说明一下,如果在实际开发过程中存在旧版本兼容的问题,可以尝试在gradle.properties文件中加上下面的一行代码:
Android.useDeprecatedNdk=true