1. 程式人生 > >Android中NDK開發基礎

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配置檔案截圖如下
gradle
有了上面的操作,通過選擇提示,我們就會在專案的app裡面看到自動建立了cpp資料夾,裡面有我們自動生成的c檔案。
自動生成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引用路徑。
javah
生成了.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