Android之NDK開發(fā) eclipse 篇

Android之NDK開發(fā)

溫馨提示:本文講述的是NDK開發(fā)的一下基礎(chǔ)知識(shí),并沒有太過高深的開發(fā)技巧,如果是大牛可以抱著隨便看看的心態(tài)閱讀本文,如果是小菜鳥,可能還需要回頭學(xué)一學(xué)C語言的基礎(chǔ)知識(shí)(本文也會(huì)提到部分的C語言知識(shí))。

什么是NDK,所謂的NDK就是在SDK前面又加上了“原生”二字,即Native Development Kit,因此又被Google稱為“NDK”,也就是本地語言(C&C++)開發(fā)包。而在Android開發(fā)中經(jīng)常接觸的Android-SDK,(software development kit)軟件開發(fā)包(只支持java語言開發(fā))。
所以可以簡(jiǎn)單的說NDK就是使用C或者C++來開發(fā)安卓。
那么為什么Android是用java開發(fā)的,又需要用到C或者C++來協(xié)助開發(fā)呢?原因有以下幾點(diǎn):

1、眾所周知,利用SDK編寫的代碼,生成的APK,很容易就可以反編譯了,安全性極為不高,而利用NDK開發(fā)的庫,不容易被反編譯,保密性,安全性都提高了。

2、很多開源工程和大型工程都是C&C++代碼,把它們轉(zhuǎn)換為純java語言顯然是不可能的。

3、C&C++的代碼運(yùn)行速度和效率都比java快很多。

然而在我短暫的開發(fā)經(jīng)驗(yàn)中,并沒有遇到使用NDK開發(fā)Android的情況,真是悲劇呀!但是不管怎樣,掌握NDK開發(fā)Android還是有必要的。
好了,既然知道NDK是什么之后,我們就來學(xué)習(xí)如何使用NDK開發(fā)Android吧。

關(guān)于JNI

我們要通過C語言來實(shí)現(xiàn)Android的功能,但是問題在于Android是基于java語言開發(fā)的,兩種語言怎能互通呢?
答案就是通過JNI來實(shí)現(xiàn)兩者之間的互通。
JNI即java native interface:java本地接口,通過JNI可以實(shí)現(xiàn)java和C代碼之間相互調(diào)用,我們可以把看做是翻譯。
理論就講到這里,那么接下來我們直接開始NDK開發(fā)嗎?當(dāng)然不是,如果你對(duì)C語言一竅不通,那么對(duì)于NDK開發(fā)根本無從著手,所以必須先學(xué)C語言基礎(chǔ)。以下是一些C語言的基礎(chǔ):

http://www.itdecent.cn/p/65d762db7048

使用Eclipse進(jìn)行JNI開發(fā)

Java調(diào)用C中的方法

在JNI開發(fā)中分為兩大部分,即是用java調(diào)用C中的方法,以及C調(diào)用Java中的方法,現(xiàn)在就讓我們來學(xué)習(xí)一下java如何調(diào)用C中的方法吧。

在java中進(jìn)行的步驟

首先要在Java中聲明本地方法,即要調(diào)用C方法的java方法,這是沒有方法體的接口,關(guān)鍵字是native,代碼如下:

//聲明本地方法 使用native關(guān)鍵字 本地方法不用實(shí)現(xiàn)
public native String hello_FromC();

使用hello_FromC()即使調(diào)用C語言的方法,然后返回一個(gè)String類型的參數(shù)回來。

在C中進(jìn)行的操作

首先要項(xiàng)目底下創(chuàng)建一個(gè)jni的文件夾,里面存放的是C的相關(guān)代碼,然后再創(chuàng)建hello.c文件,代碼如下:

#include <stdlib.h>
#include <stdio.h>
#include <jni.h>

// JNINativeInterface結(jié)構(gòu)體中定義了大量的函數(shù)指針 這些函數(shù)指針在jni開發(fā)中很常用
// (*env)->
//jobject  調(diào)用本地函數(shù)的java對(duì)象 在這個(gè)例子中 就是MainActivity的實(shí)例
//c本地函數(shù)命名規(guī)則  Java_包名_類名_本地方法名
//jstring     (*NewStringUTF)(JNIEnv*, const char*);
jstring Java_com_itheima_jnihello_MainActivity_hello_1FromC(JNIEnv* env,jobject thiz){
    char* cstr = "hello from c!";
    return (*env)->NewStringUTF(env,cstr);
}

首先我們要我們看C代碼中的方法,如下:

  jstring Java_com_itheima_jnihello_MainActivity_hello_1FromC(JNIEnv* env,jobject thiz){}

其中的jstring是什么呢,實(shí)質(zhì)上是因?yàn)镃語言中并沒有String類型,所以經(jīng)過JNI轉(zhuǎn)換為了jsting(將java轉(zhuǎn)換為C能認(rèn)識(shí)的類型),緊接著就是一大串的方法名,這是否有規(guī)則呢?
在JNI開發(fā)里面C中的方法名是有規(guī)則的,即 Java_包名類名本地方法名 ,這里面不能有錯(cuò)誤,否則就無法調(diào)用!
然后我們?cè)诳捶椒ㄖ械膮?shù) JNIEnv* env ,帶了一個(gè)*號(hào)明顯是指針的意思,但它代表是什么意思呢?
這是我們可以找到我們的NDK-bundle文件夾(NDK下載目錄),然后進(jìn)入include目錄尋找jni.h文件,詳細(xì)路徑如下android-sdk\ndk-bundle\platforms\android-21\arch-arm\usr\include,其中的android-21\arch-arm指的是Android對(duì)應(yīng)版本下的平臺(tái)。
在jni.h中我們看到一堆的結(jié)構(gòu)體和變量,并且看到大量使用typedef自定義類型,將對(duì)應(yīng)的變量和方法轉(zhuǎn)換為便于識(shí)別的變量名或者簡(jiǎn)化過長(zhǎng)的方法名。其中我們看到

typedef _jstring*       jstring; //jstring指針
typedef const struct JNINativeInterface* JNIEnv;

這時(shí)我們就懂了,jstring是_jstring的一級(jí)指針量;
JNIEnv 是結(jié)構(gòu)體JNINativeInterface 的一級(jí)指針,而JNINativeInterface結(jié)構(gòu)體中定義了大量的函數(shù)指針,這些函數(shù)指針在jni開發(fā)中很常用。
此時(shí)我們就很清楚JNIEnv* env 是結(jié)構(gòu)體JNINativeInterface 的二級(jí)指針。
而jobject則是調(diào)用本地函數(shù)的java對(duì)象,在這個(gè)例子中(前面的native方法是在MainActivity中調(diào)用的)就是MainActivity的實(shí)例。
這一切都弄懂了就好辦的多了。

然后我們?cè)趲O碌膬尚写a:

  char* cstr = "hello from c!";
  return (*env)->NewStringUTF(env,cstr);

第一行容易理解,char是C語言中的字符串類型,表示這是一個(gè)字符串,但是下面那句呢?
其中env 表示的是取出二級(jí)指針env中的一級(jí)指針的內(nèi)存地址,而 -> 符號(hào)則是在C中的簡(jiǎn)介引用運(yùn)算符,即env或者說此時(shí)轉(zhuǎn)換為的結(jié)構(gòu)體JNINativeInterface引用了其中的方法NewStringUTF(env,cstr)。
我們?cè)俅位氐絡(luò)ni.h文件下查看這個(gè)方法的信息,我們查到這行代碼:

  jstring     (*NewStringUTF)(JNIEnv*, const char*);

使用這行代碼我們將C語言中的字符串轉(zhuǎn)換成UTF格式的java能夠識(shí)別的數(shù)據(jù)類型,然后返回給java,這便是其中的所有意思。

最后我們回到j(luò)ava中,調(diào)用之前定義好的public native String hello_FromC();方法,就能夠直接獲得從C中傳過來的字符串了。

當(dāng)然目前為止只是將所有的代碼寫好,但是我們還無法調(diào)用C中的方法,我們還需要完成以下幾步
1、在jni目錄下新建Android.mk文件

  LOCAL_PATH := $(call my-dir)

  include $(CLEAR_VARS)

  LOCAL_MODULE    := hello   #指定了生成的動(dòng)態(tài)鏈接庫的名字
  LOCAL_SRC_FILES := hello.c #指定了C的源文件叫什么名字

  include $(BUILD_SHARED_LIBRARY)  # 制定要生成動(dòng)態(tài)鏈接庫

2、調(diào)用ndk-build編譯c代碼生成動(dòng)態(tài)鏈接庫.so文件 文件的位置 lib->armeabi->.so
3、 在java代碼中加載動(dòng)態(tài)鏈接庫 System.loadlibrary("動(dòng)態(tài)鏈接庫的名字"); Android.mkLOCAL_MODULE所指定的名字
4、最后會(huì)在libs目錄下生產(chǎn)so文件
注:在window下可使用命令行工具,進(jìn)入項(xiàng)目所在目錄,然后使用ndk-build命令進(jìn)行編譯(要在環(huán)境變量中配置ndk的目錄)

這一切做好之后就是在java做收尾工作了,必須在java中添加靜態(tài)代碼塊,標(biāo)明要使用的C文件,全部代碼如下:

public class MainActivity extends Activity {
    static{
        System.loadLibrary("hello");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void click(View v){
        //System.loadLibrary("hello");
        String result = hello_FromC();
        Toast.makeText(getApplicationContext(), result, 0).show();
    }
    //聲明本地方法 使用native關(guān)鍵字 本地方法不用實(shí)現(xiàn)

    public native String hello_FromC();

}

jni簡(jiǎn)便開發(fā)流程

* ① 寫java代碼 native 聲明本地方法
* ② 添加本地支持 右鍵單擊項(xiàng)目->andorid tools->add native surport
    * 如果發(fā)現(xiàn) finish不能點(diǎn)擊需要給工作空間配置ndk目錄的位置
    * window->preferences->左側(cè)選擇android->ndk 把ndk解壓的目錄指定進(jìn)來
* ③ 如果寫的是.c的文件 先修改一下生成的.cpp文件的擴(kuò)展名 不要忘了 相應(yīng)修改Android.mk文件中LOCAL_SRC_FILES的值
* ④ javah生成頭文件 在生成的頭文件中拷貝c的函數(shù)名到.c的文件
* ⑤ 解決CDT插件報(bào)錯(cuò)的問題
* 右鍵單擊項(xiàng)目選擇 properties 選測(cè) c/c++ general->paths and symbols->include選項(xiàng)卡下->點(diǎn)擊add..->file system 選擇ndk目錄下 platforms文件夾 對(duì)應(yīng)平臺(tái)下(項(xiàng)目支持的最小版本)
 usr 目錄下 arch-arm -> include  確定后 會(huì)解決代碼提示和報(bào)錯(cuò)的問題
* ⑥編寫C函數(shù) 如果需要單獨(dú)編譯一下c代碼就在c/c++視圖中找到小錘子
*  如果想直接運(yùn)行到模擬器上 就不用錘子了
* ⑦ java代碼中不要忘了 system.loadlibrary();

java中傳遞不同參數(shù)給C

java中傳遞不同參數(shù)給C語言,其處理方法也不同,具體要看jni.h中的方法。

傳遞int類型

如傳遞int類型,那么C中能直接處理,c中的方法如下:

  JNIEXPORT jint JNICALL Java_com_itheima_javapassdata_JNI_add
  (JNIEnv * env, jobject clazz, jint x, jint y){
  return x+y;
  }

int類型傳遞比較簡(jiǎn)單,查看jni.h,僅僅是將int起別買為jint而已,如下:

typedef int             jint;

傳遞String類型

從java傳遞String類型給C語言較為復(fù)雜,首先要將String轉(zhuǎn)換為char*類型,轉(zhuǎn)換方法代碼如下:

char* _JString2CStr(JNIEnv* env, jstring jstr) {
     char* rtn = NULL;
   // 通過反射的方式獲取String對(duì)象
     jclass clsstring = (*env)->FindClass(env, "java/lang/String");
   // 獲取編碼格式
     jstring strencode = (*env)->NewStringUTF(env,"GB2312");
   // 通過字節(jié)碼獲取String中的getBytes方法,但是getBytes有多個(gè)重載方法,因此第四個(gè)參數(shù)表示方法簽名(表示要使用哪個(gè)方法)
     jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
   // 設(shè)置字符編碼格式,以免轉(zhuǎn)碼錯(cuò)誤,在這里使用String .getByte("GB2312");方法,這個(gè)方法返回的是一個(gè)byte的數(shù)組,而在jni中以jbyteArray表示
     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;
}

在經(jīng)過轉(zhuǎn)換后,C獲得了char*類型才能運(yùn)行,這里獲取了java的String類型,然后再返回一個(gè)jstring類型給java

// Class:     com_itheima_javapassdata_JNI
//Method:    sayHelloInC
//  Signature: (Ljava/lang/String;)Ljava/lang/String;
JNIEXPORT jstring JNICALL Java_com_itheima_javapassdata_JNI_sayHelloInC
  (JNIEnv * env, jobject clazz, jstring jstr){
    //調(diào)用工具方法把 java中的string 類型 轉(zhuǎn)換成 C 語言中的 char*
    char* cstr = _JString2CStr(env,jstr);
    //調(diào)用strlen 獲取 cstr 字符串的長(zhǎng)度
    int length = strlen(cstr);
    int i;
    for(i = 0;i<length;i++){
        *(cstr+i) += 1;
    }
    return (*env)->NewStringUTF(env,cstr);
}

在這段代碼中,我們傳入了abc字符串,然后再c語言中將每個(gè)單詞+1,也就是將ASCII表中所代表的位置往后挪一位,得到了bcd。

java中個(gè)傳遞int數(shù)組給C

//  Class:     com_itheima_javapassdata_JNI
// Method:    arrElementsIncrease
// Signature: ([I)[I
JNIEXPORT jintArray JNICALL Java_com_itheima_javapassdata_JNI_arrElementsIncrease
  (JNIEnv * env, jobject clazz, jintArray jArray){
    //jsize       (*GetArrayLength)(JNIEnv*, jarray);
    jsize length =(*env)->GetArrayLength(env,jArray);
    LOGD("length = %d",length);
    //jboolean iscopy;
    //jint*       (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
    int* arrayPointer =(*env)->GetIntArrayElements(env,jArray,NULL);
    int i;
    for(i = 0;i<length;i++){
        *(arrayPointer+i) += 10;
    }
    return jArray;
}

C代碼中向logcat輸出內(nèi)容

    Android.mk文件增加以下內(nèi)容
    LOCAL_LDLIBS += -llog
    C代碼中增加以下內(nèi)容
    #include <android/log.h>
    #define LOG_TAG "System.out"
    #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 C的宏定義 起別名  #define LOG_TAG "System.out" 給"System.out"起別名LOG_TAG
 #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
 給 __android_log_print函數(shù)起別名  寫死了前兩個(gè)參數(shù) 第一個(gè)參數(shù) 優(yōu)先級(jí) 第二個(gè)參數(shù)TAG
 __VA_ARGS__ 可變參數(shù)的固定寫法
 LOGI(...)在調(diào)用的時(shí)候 用法跟printf()一樣,如: LOGD("length = %d",length);

在C中調(diào)用java

java可以從C中獲取數(shù)據(jù),調(diào)用C的方法,那么反過來亦然,下面我們來看看如果讓C調(diào)用Java的方法和參數(shù)。
首先定義一個(gè)類,這個(gè)類中包含要被C調(diào)用的方法,以及C調(diào)用java方法之后,java要獲取的數(shù)據(jù),代碼如下:

public class JNI {
    static{
        System.loadLibrary("callback");
    }
    private Context mContext;
    public JNI(Context context){
        mContext = context;
    }
    public native void callbackvoidmethod();

    public native void callbackintmethod();

    public native void callbackStringmethod();

    public native void callbackShowToast();
    //C調(diào)用java空方法
    public void helloFromJava(){
        System.out.println("hello from java");
    }
    //C調(diào)用java中的帶兩個(gè)int參數(shù)的方法
    public int add(int x,int y) {
        return x+y;
    }
    //C調(diào)用java中參數(shù)為string的方法
    public void printString(String s){
        System.out.println(s);
    }
    public void showToast(String s){
        Toast.makeText(mContext, s, 0).show();
    }
}

然后C中的代碼如下:

#include <jni.h>
#include <stdlib.h>
#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
/**
 * 把一個(gè)jstring轉(zhuǎn)換成一個(gè)c語言的char* 類型.
 */
char* _JString2CStr(JNIEnv* env, jstring jstr) {
     char* rtn = NULL;
     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;
}

//C調(diào)用java空方法
JNIEXPORT void JNICALL Java_com_itheima_callbackjava_JNI_callbackvoidmethod
  (JNIEnv * env, jobject clazz){
    //jclass      (*FindClass)(JNIEnv*, const char*);
    //① 獲取字節(jié)碼對(duì)象
    jclass claz = (*env)->FindClass(env,"com/itheima/callbackjava/JNI");
    //②獲取Method對(duì)象
    //jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    jmethodID methodID =(*env)->GetMethodID(env,claz,"helloFromJava","()V");
    //③通過字節(jié)碼對(duì)象創(chuàng)建一個(gè)Object
    //④通過對(duì)象調(diào)用方法
    //void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
    (*env)->CallVoidMethod(env,clazz,methodID);

}

//C調(diào)用java中的帶兩個(gè)int參數(shù)的方法
JNIEXPORT void JNICALL Java_com_itheima_callbackjava_JNI_callbackintmethod
  (JNIEnv * env, jobject clazz){
    //① 獲取字節(jié)碼對(duì)象
    jclass claz =(*env)->FindClass(env,"com/itheima/callbackjava/JNI");
    //②獲取Method對(duì)象
    jmethodID methodID = (*env)->GetMethodID(env,claz,"add","(II)I");
    //jint        (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
    int result =(*env)->CallIntMethod(env,clazz,methodID,3,4);
    LOGD("result = %d",result);
}

// C調(diào)用java中參數(shù)為string的方法
JNIEXPORT void JNICALL Java_com_itheima_callbackjava_JNI_callbackStringmethod
  (JNIEnv * env, jobject clazz){
//① 獲取字節(jié)碼對(duì)象
    jclass claz =(*env)->FindClass(env,"com/itheima/callbackjava/JNI");
//② 獲取Method對(duì)象
    jmethodID methodid =(*env)->GetMethodID(env,claz,"printString","(Ljava/lang/String;)V");
//
    jstring result =(*env)->NewStringUTF(env,"hello from c");
    (*env)->CallVoidMethod(env,clazz,methodid,result);
}

// 在C中彈出Toast
JNIEXPORT void JNICALL Java_com_itheima_callbackjava_JNI_callbackShowToast
  (JNIEnv * env, jobject clazz){
    jclass claz =(*env)->FindClass(env,"com/itheima/callbackjava/JNI");
    jmethodID methodid =(*env)->GetMethodID(env,claz,"showToast","(Ljava/lang/String;)V");
    //jobject     (*AllocObject)(JNIEnv*, jclass);
    //通過字節(jié)碼對(duì)象創(chuàng)建 java對(duì)象 在這兒就是創(chuàng)建了mainactivity的對(duì)象
    //jobject obj =(*env)->AllocObject(env,claz);
    jstring result =(*env)->NewStringUTF(env,"hello from c");
    //void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
    (*env)->CallVoidMethod(env,clazz,methodid,result);
}

要點(diǎn):

  • ① 找到字節(jié)碼對(duì)象

      *  //jclass      (*FindClass)(JNIEnv*, const char*);
      *  //第二個(gè)參數(shù) 要回調(diào)的java方法所在的類的路徑 "com/itheima/callbackjava/JNI"
    
  • ② 通過字節(jié)碼對(duì)象找到方法對(duì)象

      * //jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
      * 第二個(gè)參數(shù) 字節(jié)碼對(duì)象 第三個(gè)參數(shù) 要反射調(diào)用的java方法名 第四個(gè)參數(shù) 要反射調(diào)用的java方法簽名
      * javap -s 要獲取方法簽名的類的全類名 項(xiàng)目/bin/classes 運(yùn)行javap
    
  • ③ 通過字節(jié)碼創(chuàng)建 java對(duì)象(可選) 如果本地方法和要回調(diào)的java方法在同一個(gè)類里可以直接用 jni傳過來的java對(duì)象 調(diào)用創(chuàng)建的Method

      * jobject obj =(*env)->AllocObject(env,claz);
      * 當(dāng)回調(diào)的方法跟本地方法不在一個(gè)類里 需要通過剛創(chuàng)建的字節(jié)碼對(duì)象手動(dòng)創(chuàng)建一個(gè)java對(duì)象
      * 再通過這個(gè)對(duì)象來回調(diào)java的方法
      * 需要注意的是 如果創(chuàng)建的是一個(gè)activity對(duì)象 回調(diào)的方法還包含上下文 這個(gè)方法行不通!!!回報(bào)空指針異常
    
  • ④ 反射調(diào)用java方法

      * //void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
      * 第二個(gè)參數(shù) 調(diào)用java方法的對(duì)象 第三個(gè)參數(shù) 要調(diào)用的jmethodID對(duì)象 可選的參數(shù) 調(diào)用方法時(shí)接收的參數(shù)
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容