Android JNI与Android NDK扫盲
引言
什么是JNI和�Android NDK
JNI
是Java Native Interface
的缩写,它提供了若干的API实现了Java和其他语言的通信
(主要是C&C++
).
就是说我们使用在Java代码里面去使用其他语言现有的API.JNI
不局限与Android平台.Android NDK
是在SDK前面又加上了“原生”二字,即Native Development Kit
,因此又被Google称为NDK
。
它是Google公司给我们提供的一套开发工具,就是为了使Android程序能够执行C&C++
等部分原生代码.
使用JNI有什么用
- 代码的保护,由于APK的Java层代码很容易被
反编译
,而C/C++库反汇难度较大。 - 在NDK中调用第三方C/C++库,因为大部分的
开源库
都是用C/C++代码编写的。 - 便于
移植
,用C/C++写的库可以方便在其他的嵌入式平台上再次使用。 - 因为Android应用程序是跑在虚拟机上面,所以有些底层的硬件调用需要使用更底层的语言去调用.
环境
本机的环境
- 操作系统:
OSX 10.11.5
- Java版本:
build 1.8.0_51-b16
- NDK版本:
r11c
- IDE:
Eclipse Mars.1 Release (4.5.1)
- Android工程:
- minSDKVersion :
19
targetSDKVersion : `19`
buildToolsVersion : `19`
具体的自己去下载,这里给一下NDK的下载地址
PS:自备梯子
配置NDK的执行路径
解压NDK,路径信息不要出现中文
配置环境变量(OSX为例)
我的NDK目录路径为
/Users/August/android-ndk-r11c
,下面自行修改NDK目录
- 编辑
profile
文件: 使用自己熟悉的编辑器打开~/.profile
文件 - 在
~/.profile
添加:export PATH="/Users/August/android-ndk-r11c:$PATH"
- 保存
~/.profile
文件. - 使配置文件生效:
source ~/.profile
如果是
Windows系统
的话,跟上面雷同,不过是直接修改环境变量,而不是去修改profile
文件.
Windows用户可以
戳这里
配置Eclipse环境
这里假设你已经开始在Eclipse上面做过�Android开发了
- 选择Eclipse的设置,设置里搜索
NDK
,选择下面的NDK
然后在右边选项卡设置NDK的路径(/Users/August/android-ndk-r11c
) - 然后我们右键项目,选择
Android Tools
->Add native support
->输入C/C++的源文件名(这里是用test)
- 然后我们发现工程里面多了一个
jni
的文件夹,我们打开后发现新建了test.cpp
,和Android.mk
.我们右键test.cpp
->rename
->改成test.c
,并且把Android.mk
里面的test.cpp
改成test.c
. - 你有没有发现,你的jni文件中
#include<jni.h>
报Unresolved inclusion: <jni.h>
的错误了?没事,右键项目
->Properties
->C/C++ General
->Paths and Symbols
->Add
->File System
->选择/Users/August/android-ndk-r11c/platforms/android-19/arch-arm/usr/include
.因为这里使用的Android工程师API19,所以这里我们对应选择android-19的arm平台
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := test
LOCAL_SRC_FILES := test.c
include $(BUILD_SHARED_LIBRARY)
其中test就是模块名,test.c就是需要编译的源文件
Demo1:Hello world
native方法声明
我们在
MainActivity
中声明方法public native String getHelloFromC();
,这里getHelloFromC()
主要实现我们会在C语言里面实现.
方法调用
很简单,
getHelloFromC()
就能够得到函数返回的值.你可以打个Log或者弹个吐司.
使用javah
我们可以使用
javah 包名.类名
去生成native
函数的C语言声明
编写函数
-
命令后环境
切换到项目的src目录
下面 - 输入
javah com.example.jniproject.MainActivity
- 回到Eclipse中,右键
src
文件夹,选择refresh
操作 - 我们看到多了一个文件,找到对应的方法声明
JNIEXPORT jstring JNICALL Java_com_example_jniproject_MainActivity_getHelloFromC (JNIEnv *, jobject);
后,我们拷贝声明代码到test.c
- 最后把
test.c
的代码修改成
#include <jni.h>
jstring Java_com_example_jniproject_MainActivity_getHelloFromC(JNIEnv *env,
jobject obj) {
char* cstr = "Hello World!";
jstring jstr = (*env)->NewStringUTF(env, cstr);
return jstr;
}
修改的
4->5
修改的内容:
- 去掉
JNIEXPORT
,JNICALL
. - 增加
JNIEnv *
参数和jobject
的env
和obj
参数名
env变量
: Java虚拟机环境
obj
: 调用该代码的主体
变量类型
上面我们可以看到
jstring
这种类型.是什么鬼?
通过源文件查看(Windows下按住ctrl键
鼠标左键点击jstring
)我们可以看到.无论是JNIEnv
还是jstring
都是在jni.h
里面有两份定义.第一份是C语言
,第二份是C++
的
- 公用类型
#ifdef HAVE_INTTYPES_H
# include <inttypes.h> /* C99 */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#else
typedef unsigned char jboolean; /* unsigned 8 bits */
typedef signed char jbyte; /* signed 8 bits */
typedef unsigned short jchar; /* unsigned 16 bits */
typedef short jshort; /* signed 16 bits */
typedef int jint; /* signed 32 bits */
typedef long long jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#endif
- C的类型
typedef void* jobject;
typedef jobject jclass;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jobjectArray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jobject jthrowable;
typedef jobject jweak;
- C++的类型
typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
typedef _jarray* jarray;
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray* jbyteArray;
typedef _jcharArray* jcharArray;
typedef _jshortArray* jshortArray;
typedef _jintArray* jintArray;
typedef _jlongArray* jlongArray;
typedef _jfloatArray* jfloatArray;
typedef _jdoubleArray* jdoubleArray;
typedef _jthrowable* jthrowable;
typedef _jobject* jweak;
通过变量类型名称的字面意思,我们也可以跟Java里面的类型对应上.
需要注意的是,例如Java
中传递String
类型参数,在C语言
里面需要把String
转换成char[]等类型才能操作.
同时我们也看下NewStringUTF
这个函数
- C++版本
jstring NewStringUTF(const char* bytes) {
return functions->NewStringUTF(this, bytes);
}
- C版本
jstring (*NewStringUTF)(JNIEnv*, const char*);
从
this
实参我们不难看出,C++使用了面向对象的方法.而C语言的仅仅是结构体包装.所以当我们使用的时候对env
这个变量使用也不一样.在C版本
中是(*env)->xxx
,在C++版本
中是env->xxx
.
更多JNI函数用法参考
<<The Java(TM) Native Interface–Programmer’s Guide and Specification>>
中的JNI FUnctions
章节.网上有电子版
加载模块
虽然现在模块是还没编译出来,但是我们运行程序的时候.NDK会自动帮我们编译.我们先写入加载的代码:
public class MainActivity extends Activity {
static {
System.loadLibrary("test");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(this, getHelloFromC(), Toast.LENGTH_SHORT).show();
}
public native String getHelloFromC();
}
System.loadLibrary
的参数是模块的名字,关于名字有两个常用的方法识别:
- 自己的模块:直接查看
Android.mk
的LOCAL_MODULE
参数 - 别人的模块:例如
libtest.so
,把lib
和.so
去掉.剩下的test
就是模块名称了
走起
直接运行程序,自动编译模块
Demo2:字符串传参
我们来实现一个移位的加解密
具体步骤上面都有,所以就简要说一下步骤.
定义函数
```java
public native String encode(String str, int length);
public native String decode(String str, int length);
```
生成native
函数版本的函数声明
```
javah com.example.jniproject.MainActivity
```
修改函数声明并添加到test.c
中
```c
jstring Java_com_example_jniproject_MainActivity_encode(JNIEnv *env,
jobject obj, jstring orgin, jint length) {
jstring encrypt;
return encrypt;
}
jstring Java_com_example_jniproject_MainActivity_decode(JNIEnv * env,
jobject obj, jstring encrypt, jint length) {
jstring orgin;
return orgin;
}
```
编写test.c
具体代码
jstring Java_com_example_jniproject_MainActivity_encode(JNIEnv *env,
jobject obj, jstring orgin, jint length) {
jstring encrypt;
char *cstr = (*env)->GetStringUTFChars(env, orgin, 0);
int i;
for (i = 0; i < length; i++) {
cstr[i] += 1;
}
encrypt = (*env)->NewStringUTF(env, cstr);
return encrypt;
}
jstring Java_com_example_jniproject_MainActivity_decode(JNIEnv * env,
jobject obj, jstring encrypt, jint length) {
jstring orgin;
char *cstr = (*env)->GetStringUTFChars(env, encrypt, 0);
int i;
for (i = 0; i < length; i++) {
cstr[i] -= 1;
}
orgin = (*env)->NewStringUTF(env, cstr);
return orgin;
}
从上面例子(
模板
)可以看到,需要操作类型的时候,传参
跟返回值
都要转换成对应的语言的类型.
这篇博客大概了解一下什么是JNI
,其实我也是刚在学习.就mark一下吧.希望大家多多交流.
配置Eclipse的javah
其实我们也可以去配置Eclipse的启动配置,不用手动切换目录去生成
C/C++
的文件声明
第一步
导航菜单
->Run
->External Tools
->External Tools Configurations
->Program
->选项卡左上角加号
->出现新配置项
第二步
说一下几个需要填写的东西
-
Name
: 启动配置名称 -
Location
: 启动项的外部文件路径 -
Working Directory
: 外部文件的工作目录 -
Arguments
: 外部文件运行的参数
下面是我的参考配置,除了Location外,其他可以直接拷贝
-
Name:
javah
-
Location :
/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/javah
-
Working Directory :
${project_loc}/src
-
Arguments :
-d ${project_loc}/jni ${java_type_name}
apply
上面配置后切换到Refresh
选项卡,然后选中The project containing the selected resource
再apply
一次就ok了
第三步
假设现在我们要为
MainActity
生成对应的C/C++
声明文件.
- 选中或者打开
MainActity.java
,一定要保证当前鼠标焦点在需要生成的Java文件中
-
Run
->External Tools
->javah
,直接就看到jni
目录下面生成了头文件