Android Ndk(Beginner ‘s guide)(3.1)

来源:互联网 发布:深圳黑马程序员就业 编辑:程序博客网 时间:2024/06/11 04:26

使用JNI连接java和C/C++

(我就懂java的基本语法,所以后面很多翻译可能有错 )
在本章我们将学习以下东西:

1.在java与本地代码之间传递与接收基本类型、对象、数组

2.在本地代码里面处理java对象

3.从本地代码中引起异常

    Jni是一项巨大的高度技术性的科目,如果要深入研究需要一整本书来详细讲解。然而我们在这章将集中了解在他是怎么连接
java和c++的。

与java基本类型的联系

你可能已经迫不及待的想学更多的东西,而不是前面几章里面简单的MyProject:传递参数,接收返回值,产生异常,追踪客体目标。通过这章我们将会完成不同数据类型的值的存储,以基本数据类型和字符串开始。
    一个简单的Java GUI是由一个关键字(一个特殊的字符串),类型(整形,字符串等等)和一个与所选类型有关的值所定义的接口。接口被插入或者修改的数据将会驻留在本地代码里面。接口也可以被Java客户端回调。下面的图片展示了一个程序大概会被怎样构架:


动手时间——建立一个本地值/关键字Store

    首先实现Java方面的代码:

    1.建立一个Java/C++的混合项目

命名为Store
主包为com.packtpub
主Activity为StoreActivity
不要忘了在根目录下面创建Jni文件夹
java方面将会包含三个源文件 Store.java    StoreType.java  和 StoreActivity.java

    2.建立一个新的Store类导入与本地library同名的库,定义我们store将要听过的函数。Store是我们本地代码的前端。它仅仅支持整型和字符串型

public class Store {static {System.loadLibrary(“store”);}public native int getInteger(String pKey);public native void setInteger(String pKey, int pInt);public native String getString(String pKey);public native void setString(String pKey, String pString);}

3.以enum建立StoreType.java支持特定的几种数据类型

public enum StoreType {Integer, String}

4.在res/layout/main.xml文件里面按照下面的图片建立Java GUI。你可以是用ADT图形输出来设计,或者更简单的从Store_part3-1里面直接复制过去。


5.应用程序的GUI和Store需要被绑定在一起。这就是StoreActivity类将要实现的。当activity被创建,创建GUI的元素:类型容器就会与StoreType枚举类绑定在一起。GetValue和SetValue按钮就会触发私有函数onGetValue()和onSetValue()最后,初始化一个新的store的对象:

public class StoreActivity extends Activity {private EditText mUIKeyEdit, mUIValueEdit;private Spinner mUITypeSpinner;private Button mUIGetButton, mUISetButton;private Store mStore;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);// Initializes components and binds buttons to handlers....mStore = new Store();}

6.定义onGetValue()函数,根据GUI里面选择的StoreType类型获得一个返回值

private void onGetValue() {String lKey = mUIKeyEdit.getText().toString();StoreType lType = (StoreType) mUITypeSpinne.getSelectedItem();switch (lType) {case Integer:mUIValueEdit.setText(Integer.toString(mStore.getInteger(lKey)));break;case String:mUIValueEdit.setText(mStore.getString(lKey));break;}}

7.在StoreActivity里面添加onSetValue()函数来插入或者更新一个值。Entry的值需要根据他的类型来被描述
如果值的格式不正确,将会有一个提示信息显示:

...private void onSetValue() {String lKey = mUIKeyEdit.getText().toString();String lValue = mUIValueEdit.getText().toString();StoreType lType = (StoreType) mUITypeSpinner.getSelectedItem();try {switch (lType) {case Integer:mStore.setInteger(lKey, Integer.parseInt(lValue));break;case String:mStore.setString(lKey, lValue);break;}} catch (NumberFormatException eNumberFormatException) {displayError(“Incorrect value.”);}}private void displayError(String pError) {Toast.makeText(getApplicationContext(), pError,Toast.LENGTH_LONG).show();}}

Java方面的代码已经完成,也定义了本地方法的原型,我们将写本地代码。你可以查看最终代码获取帮助
8.在Jni目录下面,创建定义了数据结构体的Store.h。创建一个StoreType枚举出匹配java方面的类型。
同时创建Store。StoreEntry包含了一个关键字(C字符串),类型和一个值。StoreValue就是一个包含所有可能值
的集合体:

#ifndef _STORE_H_#define _STORE_H_#include “jni.h”#include <stdint.h>#define STORE_MAX_CAPACITY 16typedef enum {StoreType_Integer, StoreType_String} StoreType;typedef union {int32_t mInteger;char* mString;} StoreValue;typedef struct {char* mKey;StoreType mType;StoreValue mValue;} StoreEntry;typedef struct {StoreEntry mEntries[STORE_MAX_CAPACITY];int32_t mLength;} Store;...

9.最后声明一个函数来创建,找到,回收entry来结束Store.h。JNIEnv和jstring类型已经在jni.h的头文件里面定义了

...int32_t isEntryValid(JNIEnv* pEnv, StoreEntry* pEntry,StoreType pType);StoreEntry* allocateEntry(JNIEnv* pEnv, Store* pStore, jstring pKey);StoreEntry* findEntry(JNIEnv* pEnv, Store* pStore, jstring pKey,int32_t* pError);void releaseEntryValue(JNIEnv* pEnv, StoreEntry* pEntry);
所有这些函数都在jni/Store.c里面,isEntryValid()仅仅检查一个entry是否被分配以及类型是否正确:
#include “Store.h”#include <string.h>int32_t isEntryValid(JNIEnv* pEnv, StoreEntry* pEntry,StoreType pType) {if ((pEntry != NULL) && (pEntry->mType == pType)) {return 1;}return 0;}...

10.findEntry()函数用来比较所有传递进来的值,直到找到一个正确的。他接收的是一个jstring格式的参数,这个参数是java用来代替C代码对应格式的string,而不是标准的C语言string格式。jstring不能直接在本地代码中使用。事实上,Java和C的string是完全不同的两种概念。在Java中,String是一个有着成员方法的实体对象,而在C中,string只是一组字符数组。
为了从Java String中获取对应的C string,可以使用JNI API函数 GetStringUTFChars()来获取一个零时字符缓存。然后就能使用标准的C流程来处理。使用了GetStringUTFChars()就必须使用对应的ReleaseStringUTFChars()来释放零时的缓冲空间:

StoreEntry* findEntry(JNIEnv* pEnv, Store* pStore, jstring pKey,Int32_t* pError) {StoreEntry* lEntry = pStore->mEntries;StoreEntry* lEntryEnd = lEntry + pStore->mLength;const char* lKeyTmp = (*pEnv)->GetStringUTFChars(pEnv, pKey,NULL);if (lKeyTmp == NULL) {if (pError != NULL) {*pError = 1;}return;}while ((lEntry < lEntryEnd)&& (strcmp(lEntry->mKey, lKeyTmp) != 0)) {++lEntry;}(*pEnv)->ReleaseStringUTFChars(pEnv, pKey, lKeyTmp);return (lEntry == lEntryEnd) ? NULL : lEntry;}...

11.在Store.c里面,实现allocateEntry()函数,它既能用来创建一个新的entry,当值存在的时候也能用来返回一个已经存在的。如果entry是新的,在函数外面的内存里面转换成C的格式

StoreEntry* allocateEntry(JNIEnv* pEnv, Store* pStore, jstring pKey){Int32_t lError = 0;StoreEntry* lEntry = findEntry(pEnv, pStore, pKey, &lError);if (lEntry != NULL) {releaseEntryValue(pEnv, lEntry);} else if (!lError) {if (pStore->mLength >= STORE_MAX_CAPACITY) {return NULL;}lEntry = pStore->mEntries + pStore->mLength;const char* lKeyTmp = (*pEnv)->GetStringUTFChars (pEnv, pKey, NULL);if (lKeyTmp == NULL) {return;}lEntry->mKey = (char*) malloc(strlen(lKeyTmp));strcpy(lEntry->mKey, lKeyTmp);(*pEnv)->ReleaseStringUTFChars(pEnv, pKey, lKeyTmp);++pStore->mLength;}return lEntry;}...

12.Store.c代码中最后一个函数是releaseEntryValue(),用来释放为一个值分配的内存空间。目前只有string是被动态分配和需要被回收的

void releaseEntryValue(JNIEnv* pEnv, StoreEntry* pEntry) {int i;switch (pEntry->mType) {case StoreType_String:free(pEntry->mValue.mString);break;}}#endif

13.使用javah像前面第2章里面为com.packtpub.Store生成JNI的头文件。jni/com_packtpub_Store.h应该被生成
14.现在我们的功能函数和JNI头文件已经生成了。我们需要写JNI的资源文件。com_packtpub_Store.c。唯一的Store线程被保存在一个静态的变量里面当library被装载的时候才创建。

#include “com_packtpub_Store.h”#include “Store.h”#include <stdint.h>#include <string.h>static Store gStore = { {}, 0 };

15.在生成的JNI头文件的帮助下,完成getInteger()和setInteger()函数(在com_packtpub_Store.c里面)
第一个函数用来在store里面查找已经确认过的关键字,并返回其对应的值。如果发生什么异常,就会返回一个默认值。
第二个函数用来分配一个新的entry并保存新的integer值在里面(新建一个或者重新只用已有的)。注意这里C的int类型可以直接转换成Java的jint,反过来也一样。事实上他们是一样的类型:

JNIEXPORT jint JNICALL Java_com_packtpub_Store_getInteger(JNIEnv* pEnv, jobject pThis, jstring pKey) {StoreEntry* lEntry = findEntry(pEnv, &gStore, pKey, NULL);if (isEntryValid(pEnv, lEntry, StoreType_Integer)) {return lEntry->mValue.mInteger;} else {return 0.0f;}}JNIEXPORT void JNICALL Java_com_packtpub_Store_setInteger(JNIEnv* pEnv, jobject pThis, jstring pKey, jint pInteger) {StoreEntry* lEntry = allocateEntry(pEnv, &gStore, pKey);if (lEntry != NULL) {lEntry->mType = StoreType_Integer;lEntry->mValue.mInteger = pInteger;}}

16.处理String的时候需要更加留心。Java的string并不是真正的原始数据,在第11步里面的jstring和char *是不能互换的。
使用NewStringUTF()创建Java String. 第2个函数setString()用GetStringUTFChars()和SetStringUTFChars()把Java String转换成C string

JNIEXPORT jstring JNICALL Java_com_packtpub_Store_getString(JNIEnv* pEnv, jobject pThis, jstring pKey) {StoreEntry* lEntry = findEntry(pEnv, &gStore, pKey, NULL);if (isEntryValid(pEnv, lEntry, StoreType_String)) {return (*pEnv)->NewStringUTF(pEnv, lEntry->mValue.mString);}else {return NULL;}}JNIEXPORT void JNICALL Java_com_packtpub_Store_setString(JNIEnv* pEnv, jobject pThis, jstring pKey, jstring pString) {const char* lStringTmp = (*pEnv)->GetStringUTFChars(pEnv,pString, NULL);if (lStringTmp == NULL) {return;}StoreEntry* lEntry = allocateEntry(pEnv, &gStore, pKey);if (lEntry != NULL) {lEntry->mType = StoreType_String;jsize lStringLength = (*pEnv)->GetStringUTFLength(pEnv,pString);lEntry->mValue.mString =(char*) malloc(sizeof(char) * (lStringLength + 1));strcpy(lEntry->mValue.mString, lStringTmp);}(*pEnv)->ReleaseStringUTFChars(pEnv, pString, lStringTmp);}

17.最后按照下面的格式写Android.mk。库名为store.以及列出的两个C文件。在项目的根目录使用ndk-build来编译C代码:

LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_CFLAGS := -DHAVE_INTTYPES_HLOCAL_MODULE := storeLOCAL_SRC_FILES := com_packtpub_Store.c Store.cinclude $(BUILD_SHARED_LIBRARY)

刚刚完成了什么?

运行程序,使用不同的值以及类型保存值。然后从本地方法里面取得输入的值。我们已经完成了C和Java之间的传递与接收int类型的值。这些值被store以string类型保存着。输入的值可以根据他们的值或者类型取回。整型在native调用之间有几种不同的状态,首先在Java里面是int,然后在传进/传出java代码的时候是jint格式。最后在本地代码中是int/int32_t。显然我们可以保存JNI的代表类型jint在本地代码里面因为两种类型是相等的。更普遍的,原始数据型不同语言只有有对应代替


另一方面,Java string需要一个固定的转换方法转换到C string这样可以允许使用标准C方法来处理字符串。事实上jstring并不是一个典型的char *数组仅仅是一个Java String对象的引用。仅仅只能在java方使用。
转换是由JNI方法GetStringUTFChars()实现的同时必须调用ReleaseStringUTFChars()来释放。本质上,这个转转分配了一个新的字符串缓冲区。结果的C string是用UTF-8编码的允许使用标准C处理。改良的UTF-8可以代表标准的ASCII字符串,并且可以延伸成几种扩展类型。这种格式与java的UTF-16是不同的。在获取本地strings的时候为了避免内部的转换,JNI同样也提供了GetStringChars()和ReleaseStringCharset(),返回UTF-16。与GetStringLength()连接的时候强烈建议使用它(因为GetStringUTFLength()可以使用标准函数strlen()(改良的UTF-8格式)来代替)