1 NDK
NDK全稱是Native Develop Kit,翻譯作原生開發工具包。它允許你爲Android使用C/C++代碼來實現應用程序的功能。換言之Android的SDK之外,有一個工具叫NDK,用於進行C/C++的開發。一般情況,是用NDK工具把C/C++編譯爲.co文件,然後在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頭文件代碼需要做一下說明
JINEXPORT和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