Android中NDK開發基礎
簡介
Android NDK 是在SDK前面又加上了“原生”二字,即Native Development Kit,因此又被Google稱為“NDK”。
眾所周知,Android程式執行在Dalvik虛擬機器中,NDK允許使用者使用類似C / C++之類的原生程式碼語言執行部分程式。
NDK包括了:
從C / C++生成原生程式碼庫所需要的工具和build files。
將一致的原生庫嵌入可以在Android裝置上部署的應用程式包檔案(application packages files ,即.apk檔案)中。
支援所有未來Android平臺的一系列原生系統標頭檔案和庫
為何要用到NDK?
概括來說主要分為以下幾種情況:
1. 程式碼的保護,由於apk的java層程式碼很容易被反編譯,而C/C++庫反匯難度較大。
2. 在NDK中呼叫第三方C/C++庫,因為大部分的開源庫都是用C/C++程式碼編寫的。
3. 便於移植,用C/C++寫的庫可以方便在其他的嵌入式平臺上再次使用。
本篇文章環境為Android studio2.2.3
流程
1,新建android工程,其中建立載入本地java類,這裡命名為MyLocalUtil.class。
public class MyLocalUtil {
{
System.loadLibrary("china" );
}
/**
* 讓C程式碼做加法運算,把結果返回
* @param x
* @param y
* @return
*/
public native int add(int x, int y);
/**
* 從java傳入字串,C程式碼程序拼接
*
* @param s I am from java
* @return I am form java add I am from C
*/
public native String addString(String s);
/**
* 讓C程式碼給每個元素都加上10
* @param intArray
* @return
*/
public native int[] increaseArrayEles(int[] intArray);
/*
* 應用: 檢查密碼是否正確, 如果正確返回200, 否則返回400
*/
public native int checkPwd(String pwd);
}
這裡要注意的是,有些教程在寫載入本地Library時,前面會加上static關鍵字,作用不言而喻,是希望在類載入前就呼叫本地檔案。
這裡,我們沒有這種需求,就沒有新增static關鍵字。
2.通過提示創建出.c檔案
如果沒有在gradle(app)裡面配置的話,是提示不出來的。所以這一步進行還需要在gradle檔案defaultConfig裡面新增
ndk{
//對應MyLocalUtil中載入本地檔名稱(必須)
moduleName "china"
// 指定要ndk需要相容的架構(這樣其他依賴包裡mips,x86,armeabi,arm-v8之類的so會被過濾掉)
abiFilters "armeabi","armeabi-v7a","x86"
}
對應的gradle配置檔案截圖如下
有了上面的操作,通過選擇提示,我們就會在專案的app裡面看到自動建立了cpp資料夾,裡面有我們自動生成的c檔案。
同時c檔案裡面會自動生成jni方法。這裡的命名是有規範的,這裡Java_renk_addndk_MyLocalUtil_add。》Java_包名類名方法名。在JNI中變數型別也不在是java裡面的資料型別,關於資料型別變化請看下錶:
基本型別對映
非基本型別對映
這裡還要注意的是自動生成的方法裡面的引數(JNIEnv * env, jobject jobj,jint jx, jint jy) ,第一個是C指標,第二個,是java物件的引用。第三四個則是,呼叫本地方法傳入的兩個引數。當然,在本地方法中jint可以和int互用。故在jni方法中可以將add方法這樣寫
JNIEXPORT jint JNICALL Java_renk_addndk_MyLocalUtil_add(JNIEnv * env, jobject jobj,jint jx, jint jy) {
int result = jx + jy;
return result;
};
最後,本地呼叫此方法就可以直接的到結果,不用再轉換資料格式了。當然,現在還沒完。因為我們還沒有.h檔案,其作用有點類似於java程式碼中的介面,提供規範標準。C也一樣,我們直接在C檔案裡面新增方法名是不起作用的。還要在gradle.properties檔案裡面加上android.useDeprecatedNdk=true
3.生成.h檔案。
生成.h檔案需要用到javah命令,具體用法,是開啟,Android studio下面的 Terminal面板。進入到專案中的java檔案路徑中。輸入命令cd app\src\main\java 進入java檔案,然後再輸入 javah -d ..\jni 包名+類名 回車。這樣就會自動在main下面jni資料夾下生成.h檔案。
注意的是,這裡的包名+類名,是載入本地檔案的類名,倘若,你不是專門用一個類來載入native方法的話,就應該使用 寫native方法的那個類的全路徑。..\jni代表同級jni引用路徑。
生成了.h檔案,然後再C檔案引用它。在C檔案頂部#include
public native int[] increaseArrayEles(int[] intArray);
public native String addString(String s);
然後再.h檔案裡面新增相應方法。
JNIEXPORT jstring
JNICALL Java_renk_testndk_MyLocalUtil_addString
(JNIEnv * , jobject, jstring);
JNIEXPORT jintArray
JNICALL Java_renk_testndk_MyLocalUtil_increaseArrayEles
(JNIEnv * , jobject, jintArray);
在.C檔案裡面實現具體方法
JNIEXPORT jintArray
JNICALL Java_renk_addndk_MyLocalUtil_increaseArrayEles(JNIEnv * env, jobject jobj,jintArray jarray) {
//1.得到陣列的長度
//jsize (*GetArrayLength)(JNIEnv*, jarray);
jsize size = (*env)->GetArrayLength(env, jarray);
//2.得到陣列元素
//jint* (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
jint *intArray = (*env)->GetIntArrayElements(env, jarray, JNI_FALSE);
//3.遍歷陣列,給每個元素加上10
int i;
for(i = 0;i<size;i++){
// *(intArray+i) = *(intArray+i) + 10;
*(intArray+i) += 10;
}
//4.返回結果
return jarray;
}
JNIEXPORT jstring JNICALL Java_renk_addndk_MyLocalUtil_sayHello
(JNIEnv * env, jobject jobj,jstring jstr) {
char *fromJava = mJString2CStr(env, jstr);//轉換方法
//拼接函式strcat,C/C++自帶
strcat(fromJava, fromC);//把拼接的結果放在第一引數裡面
//jstring (*NewStringUTF)(JNIEnv*, const char*);
return (*env)->NewStringUTF(env, fromJava);
}
這裡在字串操作的時候有一個轉換,因為java字串與C中的字串是不一樣的,字串在C中是char型別的指標陣列,所以要單獨寫一個轉換方法具體如下
/**
* 把一個jstring轉換成一個c語言的char* 型別.
*/
char *mJString2CStr(JNIEnv * env, jstringjstr) {
char *rtn = "";
//得到java類
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
//新建一個java字串(GB2312)
jstring strencode = (*env)->NewStringUTF(env, "GB2312");
//獲得String類的轉換Byte陣列方法的方法Id
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
//通過方法Id將字串轉化為陣列
jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid,strencode); // String.getByte("GB2312");
//得到陣列長度
jsize alen = (*env)->GetArrayLength(env, barr);
//將陣列元素分別裝入開闢的記憶體
jbyte *ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
if(alen > 0) {
//C/C++自帶,malloc,memcpy函式
rtn = (char *) malloc(alen + 1); //"\0"
memcpy(rtn, ba, alen);
rtn[alen]=0;
}
//釋放記憶體
(*env)->ReleaseByteArrayElements(env, barr, ba,0);
return rtn;
}
好了,這樣就可以呼叫了,在jni裡面主要是用到C的知識比較多,而C 最主要的就是指標啦(陣列就是特殊的指標)。對C/C++不熟悉的,就可以學習一下。
最後,我們在實現一個小功能,用JNI驗證密碼。在java層傳入密碼,在C層驗證密碼是否正確,這樣可以用於本地驗證登陸。
同樣的步驟,先.h檔案寫出相應的方法名,然後再C檔案中寫具體實現。最後在需要用到的地方呼叫該類的方法即可。
.h檔案方法
JNIEXPORT jint
JNICALL Java_renk_addndk_MyLocalUtil_checkPwd
(JNIEnv * , jobject, jstring);
.c檔案方法
JNIEXPORT jint JNICALL Java_renk_addndk_MyLocalUtil_checkPwd(JNIEnv * env, jobject jobj,jstring jstr) {
//伺服器的密碼是
char *origin = "renk";
char *fromUser = mJString2CStr(env, jstr);
//函式比較字串是否相同,C/C++自帶
int code = strcmp(origin, fromUser);
if(code==0){
return 200;
}else{
return 400;
}
}
執行後,把SO檔案拿出來,就可以用了。而且反編譯難度很大。相對很很安全的。
最後,今天要講的東西,就這麼多,以上就是基本型別與引用型別的基本用法,內容很簡單,以後會加大點難度。謝謝。國際慣例,原始碼貼出啦。
C檔案
#include <jni.h>
#include <renk_addndk_MyLocalUtil.h>
JNIEXPORT jint JNICALL Java_renk_addndk_MyLocalUtil_add(JNIEnv * env, jobject jobj,jint jx, jint jy) {
int result = jx + jy;
return result;
};
JNIEXPORT jintArray
JNICALL Java_renk_addndk_MyLocalUtil_increaseArrayEles(JNIEnv * env, jobject jobj,jintArray jarray) {
//1.得到陣列的長度
//jsize (*GetArrayLength)(JNIEnv*, jarray);
jsize size = (*env)->GetArrayLength(env, jarray);
//2.得到陣列元素
//jint* (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
jint *intArray = (*env)->GetIntArrayElements(env, jarray, JNI_FALSE);
//3.遍歷陣列,給每個元素加上10
int i;
for(i = 0;i<size;i++){
// *(intArray+i) = *(intArray+i) + 10;
*(intArray+i) += 10;
}
//4.返回結果
return jarray;
}
JNIEXPORT jstring JNICALL Java_renk_addndk_MyLocalUtil_addString
(JNIEnv * env, jobject jobj,jstring jstr) {
char *fromJava = mJString2CStr(env, jstr);
//I am form java add I am from C
//c:
char *fromC = "add I am from C";
//拼接函式strcat
strcat(fromJava, fromC);//把拼接的結果放在第一引數裡面
//jstring (*NewStringUTF)(JNIEnv*, const char*);
// LOGD("fromJava===%s\n",fromJava);
return (*env)->NewStringUTF(env, fromJava);
}
/**
* 把一個jstring轉換成一個c語言的char* 型別.
*/
char *mJString2CStr(JNIEnv * env, jstringjstr) {
char *rtn = "";
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
jstring strencode = (*env)->NewStringUTF(env, "GB2312");
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid,strencode); // String .getByte("GB2312");
jsize alen = (*env)->GetArrayLength(env, barr);
jbyte *ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
if(alen > 0) {
rtn = (char *) malloc(alen + 1); //"\0"
memcpy(rtn, ba, alen);
rtn[alen]=0;
}
(*env)->ReleaseByteArrayElements(env, barr, ba,0);
return rtn;
}
JNIEXPORT jint JNICALL Java_renk_addndk_MyLocalUtil_checkPwd(JNIEnv * env, jobject jobj,jstring jstr) {
//儲存的密碼是
char *origin = "liguo";
//獲得傳入的密碼。轉換為C識別的型別
char *fromUser = mJString2CStr(env, jstr);
//函式比較字串是否相同
int code = strcmp(origin, fromUser);
if(code==0){
return 200;
}else{
return 400;
}
}
H檔案
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class renk_addndk_MyLocalUtil */
#ifndef _Included_renk_addndk_MyLocalUtil
#define _Included_renk_addndk_MyLocalUtil
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: renk_addndk_MyLocalUtil
* Method: add
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_renk_addndk_MyLocalUtil_add
(JNIEnv *, jobject, jint, jint);
JNIEXPORT jstring
JNICALL Java_renk_addndk_MyLocalUtil_addString
(JNIEnv * , jobject, jstring);
JNIEXPORT jintArray
JNICALL Java_renk_addndk_MyLocalUtil_increaseArrayEles
(JNIEnv * , jobject, jintArray);
JNIEXPORT jint
JNICALL Java_renk_addndk_MyLocalUtil_checkPwd
(JNIEnv * , jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
本地載入類
public class MyLocalUtil {
static {
System.loadLibrary("china");
}
public native int add(int x, int y);
public native int[] increaseArrayEles(int[] intArray);
public native String addString(String s);
public native int checkPwd(String password);
}
build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.0"
defaultConfig {
applicationId "renk.addndk"
minSdkVersion 17
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk{
moduleName "china"
abiFilters "armeabi","armeabi-v7a","x86"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
jniDebuggable true
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.0.1'
testCompile 'junit:junit:4.12'
}
End