Android Studio1.4.x JNI開發基礎 - 簡單例項

KingsLanding發表於2015-11-19

  接上一篇,搭建好基於Android Studio的環境之後,編寫native程式碼相對來說也比較簡單了。在Android上編寫Native程式碼和在Linux編寫C/C++程式碼還是有區別,Native程式碼一般需要與JVM互動資料,需要遵循一定的規範,本文來介紹一下基本的JNI程式碼寫法。

  我們還是從例項出發,配置好Android Studio工程之後,我們需要建立jni目錄和在jni目下建立c/c++檔案和相應的標頭檔案,建立方式見下圖。

  在例項工程中我們建立了NdkSample.cpp 和 NdkSample.h,原始碼見下面:

#include "NdkSample.h"

JNIEXPORT jstring JNICALL Java_com_zyp_ndktest_MainActivity_sayHello
        (JNIEnv *env, jclass cls, jstring j_str)
{
    const char *c_str = nullptr;
    char buff[128] = {0};
    jboolean isCopy;    
    c_str = env->GetStringUTFChars(j_str, &isCopy);
    printf("isCopy:%d\n",isCopy);
    if(c_str == NULL)
    {
        return NULL;
    }
    printf("C_str: %s \n", c_str);
    sprintf(buff, "hello %s", c_str);
    env->ReleaseStringUTFChars(j_str, c_str);
    return env->NewStringUTF(buff);
}  
#ifndef NDKTEST_NDKSAMPLE_H
#define NDKTEST_NDKSAMPLE_H

#include "jni.h"
#include <stdio.h>
#include <string.h>

extern "C" {
JNIEXPORT jstring JNICALL
        Java_com_zyp_ndktest_MainActivity_sayHello(JNIEnv *env, jclass type,
                                                       jstring filename);
}

#endif //NDKTEST_NDKSAMPLE_H

  現在來簡單介紹一下,首先是NdkSample.h檔案,剛剛建立的時候只有相應的預處理命令,我們在標頭檔案預處理命令之間加上 jni.h ,stdio.h ,string.h 後兩個非必要。將我們要在java層呼叫的介面宣告出來,放在extern "c"{} 中(告訴編譯器按照C標準進行編譯)。第一次接觸jni的同學看到那麼複雜的函式命名和奇怪的JNIEXPORT ,JNICALL,JNIEnv之類的估計有點不習慣,本文就不詳細介紹它們的意思,其實你跟蹤原始碼它們就是幾個巨集(JNIEnv是一個結構體儲存當前環境的上下文),其它的jstring,jclass之類的很好理解就是在Native環境中對JVM中java對應結構的一種表示方式。

  函式命令方式是包名加activity名加函式名,表如我們在java層中的包名是java.com.zyp.ndktest,在MainActivity中呼叫sayHello函式,則jni層函式命名就要寫成上面的方式。

  接下來看NdkSample.cpp檔案中函式的定義。ni層的函式還需要多兩個引數,一個是JNIEnv * ,一個是jclass。我們在java層呼叫的時候就只用傳遞前兩個引數之外的引數。例子中我們想從java層傳遞一個String型別的引數到jni層,jni層從JVM中取資料的時候取到的卻是jstring型別,在jni層我們不能直接使用需要轉換。這裡我們通過env->GetStringUTFChars(j_str, &isCopy)函式來完成,將j_str所在的地址轉換並賦值給const char *型別的指標,之後我們就可以通過該指標訪問那塊記憶體了。注意這裡是const char * 表示該指標指向的記憶體區域的內容是不可以改變的,java中的String 也是自帶final屬性的。GetStringUTFChars()實際上是將JVM內部的Unicode轉化成為了C/C++認識的UTF-8的格式的字串,注意這個函式內部發生了記憶體分配,相當於是拷貝了一份Unicode然後進行轉化,所以後面需要ReleaseStringUTFChars()來釋放記憶體。

  最後該函式返回一個新的構建好的jstring型別給java層,為了將C/C++層的UTF-8字串轉換為JVM中的Unicode字串,需要呼叫另外一個函式NewStringUTF()來完成轉換。

  我們注意到JVM中的內容jni中不能直接操作需要進行轉換,jni中的內同也要進行轉換,因此也有大量的相關jni介面存在,後面文章中會挑選一些來講解。

  此外,函式中JNIEnv *env這個引數需要說一下,在C和C++中使用方式是不一樣的,不要搞混了。在C中,看到JNIEnv 我們實質是取得了JNINativeInterface* (JNIEnv指標的指標),我們得使用**env獲取結構體,從而才能使用結構體裡面的方法。在C++中,看到JNIEnv我們實質是取得了JNIEnv*(JNIEnv結構體的指標),我們可以直接使用env->使用結構體裡面的方法。注意我們呼叫GetStringUTFChars()的方式,但是注意和Java_com_zyp_ndktest_MainActivity_sayHello()進行區別。

  現在來看看我們在java層中如何呼叫jni層的介面,看下面程式碼:

package com.zyp.ndktest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        String ret = sayHello("zhuzhu");
        Log.i("JNI_INFO", ret);
    }

    static {
        System.loadLibrary("NdkSample");
    }

    public native static String sayHello(String str);
}

  我們首先要通過System.loadLibrary()載入jni程式碼編譯後生成的.so,但是這個庫的名字怎麼來的呢,注意回過頭去看上一篇中gradle ndk{}中的內容,我們是在那裡進行的命名的;然後還要宣告native static 型別的該函式。然後直接呼叫就好了。執行結果見下圖。

  希望通過這篇文章能夠讓大家入門JNI開發^_^。

 

相關文章