Android之NDK开发
温馨提示:本文讲述的是NDK开发的一下基础知识,并没有太过高深的开发技巧,如果是大牛可以抱着随便看看的心态阅读本文,如果是小菜鸟,可能还需要回头学一学C语言的基础知识(本文也会提到部分的C语言知识)。
什么是NDK,所谓的NDK就是在SDK前面又加上了“原生”二字,即Native Development Kit,因此又被Google称为“NDK”,也就是本地语言(C&C++)开发包。而在Android开发中经常接触的Android-SDK,(software development kit)软件开发包(只支持java语言开发)。
所以可以简单的说NDK就是使用C或者C++来开发安卓。
那么为什么Android是用java开发的,又需要用到C或者C++来协助开发呢?原因有以下几点:
1、众所周知,利用SDK编写的代码,生成的APK,很容易就可以反编译了,安全性极为不高,而利用NDK开发的库,不容易被反编译,保密性,安全性都提高了。
2、很多开源工程和大型工程都是C&C++代码,把它们转换为纯java语言显然是不可能的。
3、C&C++的代码运行速度和效率都比java快很多。
然而在我短暂的开发经验中,并没有遇到使用NDK开发Android的情况,真是悲剧呀!但是不管怎样,掌握NDK开发Android还是有必要的。
好了,既然知道NDK是什么之后,我们就来学习如何使用NDK开发Android吧。
关于JNI
我们要通过C语言来实现Android的功能,但是问题在于Android是基于java语言开发的,两种语言怎能互通呢?
答案就是通过JNI来实现两者之间的互通。
JNI即java native interface:java本地接口,通过JNI可以实现java和C代码之间相互调用,我们可以把看做是翻译。
理论就讲到这里,那么接下来我们直接开始NDK开发吗?当然不是,如果你对C语言一窍不通,那么对于NDK开发根本无从着手,所以必须先学C语言基础。以下是一些C语言的基础:
使用Eclipse进行JNI开发
Java调用C中的方法
在JNI开发中分为两大部分,即是用java调用C中的方法,以及C调用Java中的方法,现在就让我们来学习一下java如何调用C中的方法吧。
在java中进行的步骤
首先要在Java中声明本地方法,即要调用C方法的java方法,这是没有方法体的接口,关键字是native,代码如下:
//声明本地方法 使用native关键字 本地方法不用实现
public native String hello_FromC();
使用hello_FromC()即使调用C语言的方法,然后返回一个String类型的参数回来。
在C中进行的操作
首先要项目底下创建一个jni的文件夹,里面存放的是C的相关代码,然后再创建hello.c文件,代码如下:
#include <stdlib.h>
#include <stdio.h>
#include <jni.h>
// JNINativeInterface结构体中定义了大量的函数指针 这些函数指针在jni开发中很常用
// (*env)->
//jobject 调用本地函数的java对象 在这个例子中 就是MainActivity的实例
//c本地函数命名规则 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是什么呢,实质上是因为C语言中并没有String类型,所以经过JNI转换为了jsting(将java转换为C能认识的类型),紧接着就是一大串的方法名,这是否有规则呢?
在JNI开发里面C中的方法名是有规则的,即 Java_包名类名本地方法名 ,这里面不能有错误,否则就无法调用!
然后我们在看方法中的参数 JNIEnv* env ,带了一个*号明显是指针的意思,但它代表是什么意思呢?
这是我们可以找到我们的NDK-bundle文件夹(NDK下载目录),然后进入include目录寻找jni.h文件,详细路径如下android-sdk\ndk-bundle\platforms\android-21\arch-arm\usr\include,其中的android-21\arch-arm指的是Android对应版本下的平台。
在jni.h中我们看到一堆的结构体和变量,并且看到大量使用typedef自定义类型,将对应的变量和方法转换为便于识别的变量名或者简化过长的方法名。其中我们看到
typedef _jstring* jstring; //jstring指针
typedef const struct JNINativeInterface* JNIEnv;
这时我们就懂了,jstring是_jstring的一级指针量;
JNIEnv 是结构体JNINativeInterface 的一级指针,而JNINativeInterface结构体中定义了大量的函数指针,这些函数指针在jni开发中很常用。
此时我们就很清楚JNIEnv* env 是结构体JNINativeInterface 的二级指针。
而jobject则是调用本地函数的java对象,在这个例子中(前面的native方法是在MainActivity中调用的)就是MainActivity的实例。
这一切都弄懂了就好办的多了。
然后我们在带剩下的两行代码:
char* cstr = "hello from c!";
return (*env)->NewStringUTF(env,cstr);
第一行容易理解,char是C语言中的字符串类型,表示这是一个字符串,但是下面那句呢?
其中env 表示的是取出二级指针env中的一级指针的内存地址,而 -> 符号则是在C中的简介引用运算符,即env或者说此时转换为的结构体JNINativeInterface引用了其中的方法NewStringUTF(env,cstr)。
我们再次回到jni.h文件下查看这个方法的信息,我们查到这行代码:
jstring (*NewStringUTF)(JNIEnv*, const char*);
使用这行代码我们将C语言中的字符串转换成UTF格式的java能够识别的数据类型,然后返回给java,这便是其中的所有意思。
最后我们回到java中,调用之前定义好的public native String hello_FromC();方法,就能够直接获得从C中传过来的字符串了。
当然目前为止只是将所有的代码写好,但是我们还无法调用C中的方法,我们还需要完成以下几步
1、在jni目录下新建Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello #指定了生成的动态链接库的名字
LOCAL_SRC_FILES := hello.c #指定了C的源文件叫什么名字
include $(BUILD_SHARED_LIBRARY) # 制定要生成动态链接库
2、调用ndk-build编译c代码生成动态链接库.so文件 文件的位置 lib->armeabi->.so
3、 在java代码中加载动态链接库 System.loadlibrary("动态链接库的名字"); Android.mkLOCAL_MODULE所指定的名字
4、最后会在libs目录下生产so文件
注:在window下可使用命令行工具,进入项目所在目录,然后使用ndk-build命令进行编译(要在环境变量中配置ndk的目录)
这一切做好之后就是在java做收尾工作了,必须在java中添加静态代码块,标明要使用的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关键字 本地方法不用实现
public native String hello_FromC();
}
jni简便开发流程
* ① 写java代码 native 声明本地方法
* ② 添加本地支持 右键单击项目->andorid tools->add native surport
* 如果发现 finish不能点击需要给工作空间配置ndk目录的位置
* window->preferences->左侧选择android->ndk 把ndk解压的目录指定进来
* ③ 如果写的是.c的文件 先修改一下生成的.cpp文件的扩展名 不要忘了 相应修改Android.mk文件中LOCAL_SRC_FILES的值
* ④ javah生成头文件 在生成的头文件中拷贝c的函数名到.c的文件
* ⑤ 解决CDT插件报错的问题
* 右键单击项目选择 properties 选测 c/c++ general->paths and symbols->include选项卡下->点击add..->file system 选择ndk目录下 platforms文件夹 对应平台下(项目支持的最小版本)
usr 目录下 arch-arm -> include 确定后 会解决代码提示和报错的问题
* ⑥编写C函数 如果需要单独编译一下c代码就在c/c++视图中找到小锤子
* 如果想直接运行到模拟器上 就不用锤子了
* ⑦ java代码中不要忘了 system.loadlibrary();
java中传递不同参数给C
java中传递不同参数给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类型传递比较简单,查看jni.h,仅仅是将int起别买为jint而已,如下:
typedef int jint;
传递String类型
从java传递String类型给C语言较为复杂,首先要将String转换为char*类型,转换方法代码如下:
char* _JString2CStr(JNIEnv* env, jstring jstr) {
char* rtn = NULL;
// 通过反射的方式获取String对象
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
// 获取编码格式
jstring strencode = (*env)->NewStringUTF(env,"GB2312");
// 通过字节码获取String中的getBytes方法,但是getBytes有多个重载方法,因此第四个参数表示方法签名(表示要使用哪个方法)
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
// 设置字符编码格式,以免转码错误,在这里使用String .getByte("GB2312");方法,这个方法返回的是一个byte的数组,而在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;
}
在经过转换后,C获得了char*类型才能运行,这里获取了java的String类型,然后再返回一个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){
//调用工具方法把 java中的string 类型 转换成 C 语言中的 char*
char* cstr = _JString2CStr(env,jstr);
//调用strlen 获取 cstr 字符串的长度
int length = strlen(cstr);
int i;
for(i = 0;i<length;i++){
*(cstr+i) += 1;
}
return (*env)->NewStringUTF(env,cstr);
}
在这段代码中,我们传入了abc字符串,然后再c语言中将每个单词+1,也就是将ASCII表中所代表的位置往后挪一位,得到了bcd。
java中个传递int数组给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输出内容
Android.mk文件增加以下内容
LOCAL_LDLIBS += -llog
C代码中增加以下内容
#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函数起别名 写死了前两个参数 第一个参数 优先级 第二个参数TAG
__VA_ARGS__ 可变参数的固定写法
LOGI(...)在调用的时候 用法跟printf()一样,如: LOGD("length = %d",length);
在C中调用java
java可以从C中获取数据,调用C的方法,那么反过来亦然,下面我们来看看如果让C调用Java的方法和参数。
首先定义一个类,这个类中包含要被C调用的方法,以及C调用java方法之后,java要获取的数据,代码如下:
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调用java空方法
public void helloFromJava(){
System.out.println("hello from java");
}
//C调用java中的带两个int参数的方法
public int add(int x,int y) {
return x+y;
}
//C调用java中参数为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__)
/**
* 把一个jstring转换成一个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调用java空方法
JNIEXPORT void JNICALL Java_com_itheima_callbackjava_JNI_callbackvoidmethod
(JNIEnv * env, jobject clazz){
//jclass (*FindClass)(JNIEnv*, const char*);
//① 获取字节码对象
jclass claz = (*env)->FindClass(env,"com/itheima/callbackjava/JNI");
//②获取Method对象
//jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
jmethodID methodID =(*env)->GetMethodID(env,claz,"helloFromJava","()V");
//③通过字节码对象创建一个Object
//④通过对象调用方法
//void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
(*env)->CallVoidMethod(env,clazz,methodID);
}
//C调用java中的带两个int参数的方法
JNIEXPORT void JNICALL Java_com_itheima_callbackjava_JNI_callbackintmethod
(JNIEnv * env, jobject clazz){
//① 获取字节码对象
jclass claz =(*env)->FindClass(env,"com/itheima/callbackjava/JNI");
//②获取Method对象
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调用java中参数为string的方法
JNIEXPORT void JNICALL Java_com_itheima_callbackjava_JNI_callbackStringmethod
(JNIEnv * env, jobject clazz){
//① 获取字节码对象
jclass claz =(*env)->FindClass(env,"com/itheima/callbackjava/JNI");
//② 获取Method对象
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);
//通过字节码对象创建 java对象 在这儿就是创建了mainactivity的对象
//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);
}
要点:
-
① 找到字节码对象
* //jclass (*FindClass)(JNIEnv*, const char*); * //第二个参数 要回调的java方法所在的类的路径 "com/itheima/callbackjava/JNI"
-
② 通过字节码对象找到方法对象
* //jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); * 第二个参数 字节码对象 第三个参数 要反射调用的java方法名 第四个参数 要反射调用的java方法签名 * javap -s 要获取方法签名的类的全类名 项目/bin/classes 运行javap
-
③ 通过字节码创建 java对象(可选) 如果本地方法和要回调的java方法在同一个类里可以直接用 jni传过来的java对象 调用创建的Method
* jobject obj =(*env)->AllocObject(env,claz); * 当回调的方法跟本地方法不在一个类里 需要通过刚创建的字节码对象手动创建一个java对象 * 再通过这个对象来回调java的方法 * 需要注意的是 如果创建的是一个activity对象 回调的方法还包含上下文 这个方法行不通!!!回报空指针异常
-
④ 反射调用java方法
* //void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...); * 第二个参数 调用java方法的对象 第三个参数 要调用的jmethodID对象 可选的参数 调用方法时接收的参数