先啰嗦一段,从学习Android以来一直会看到这个JNI,偶尔也看到要写c/c++的代码,其实从心里就是有些排斥的,毕竟我学的是Java,我学习一个JNI我还得学会c++,c其实是学了一遍了,但是长期不用基本也就忘了,虽然基本的都是看得懂的,但是编码并不是看得懂就行的,要自己能写,所以其实打心底是排斥JNI的.但是学习Android的时间越长,我发现JNI是支撑Android运行的一大模块,比如我们常见的那些播放音频的功能其实底层就是使用了JNI,这样子就让Java代码可以调用底层的c代码,从而可以操纵硬件的目的,所以其实JNI我们可以理解为Java和(c/c++)之间的桥梁,举例一段源码:
MediaPlayer m = new MediaPlayer(); m.start();
/** * Starts or resumes playback. If playback had previously been paused, * playback will continue from where it was paused. If playback had * been stopped, or never started before, playback will start at the * beginning. * * @throws IllegalStateException if it is called in an invalid state */ public void start() throws IllegalStateException { stayAwake(true); _start(); } private native void _start() throws IllegalStateException;
这是播放音频控件的开始播放方法的有关源码,我们可以看到,在start()方法中,调用了一个_start()的方法,这个方法是用native关键字标识的,这就是我们以前经常听到的本地方法,意思就是说这个方法的具体实现是通过c/c++代码实现的,这就是JNI的简单案例
ndk环境的搭建,这里亲们就去谷歌官网去下载就行了,下载完毕解压,然后在eclipse里面关联一下即可
下面让我带大家入门
我带大家做一个简单的加法来学习JNI,首先我们得声明一个方法
private native int add(int i,int j);
方法类似于上面的_start()方法是没有方法体的,类似于接口中的方法声明对不对,没有方法体,然后我们在activity的oncreate()方法中调用这个方法来实现
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); int result = add(3,5); System.out.println("result == " + result); }
调用的时候就和普通方法一样调用就可以了,这样子就可以了?
很明显这样子不够,因为方法的实现我们还没做呢!我们说实现是需要c/c++来实现的,这里设计到书写c/c++代码了.
步骤:
1.在项目根目录建立一个文件夹jni,里面用来书写c/c++代码
2.在jni文件夹下新建一个c/c++源码文件
3.里面自然需要实现我们之前定义个那个方法add了
int add(int i, int j){ return i + j; }
c中写一个add方法就是上面这样子书写的,但是使用JNI的时候需要加上JNI的一些规范
首先就是方法名字,规范如下:
Java_包名_类名_方法名
所以代码就需要改成这样子
int Java_com_example_day01_MainActivity_add(int i, int j){ return i + j; }
都是用下划线隔开的,请注意,然后就是参数列表,这里有两个参数是固定的,必须要写,如下:
int Java_com_example_day01_MainActivity_add(JNIEnv* e, jobject thiz, int i, int j) { return i + j; }
可以看到这里多了两个参数,一个是JNIEnv*和jobject
JNIEnv*:这里做一下解释,我们的java代码是运行在虚拟机里面的,所以利用JNI在实现功能的同时,需要用到虚拟机环境的指针,也就是需要环境的支持,并且这是一个指针,指向运行环境
jobject是调用方法的时候的对象,这里也就是我们的MainActivity对象
做完了这一切我们就实现了add方法,当然了别忘了添加一些必要的头文件,完整的c代码是
#include <stdio.h> #include <stdlib.h> #include <jni.h> jint Java_com_example_day01_MainActivity_add(JNIEnv* e, jobject thiz, int i, int j) { return i + j; }
这里有一个jni.h头文件是使用jni功能所必需的,还有这里的返回值jint,其实就是谷歌为了让代码更有可读性预定义的一些名字,这里我点进去
可以看到就是一个int,这里也做一个解释,为啥要这样子,直接适应int不是挺好的.
其实这里你用int和jint都是没有什么区别的,但是我们想一下,在c中是没有对象的概念的,加入你java里面传入的是一个数组,或者传入一个对象,在c中都是一个指针而已,指向了你传入的对象,在c中都是void*表示,这时候你写的代码过几天去看,你还知道当初的void*这里的参数,到底是数组呢还是对象,还是字符串呢?你不知道了,你只能去调用的地方去瞧瞧,很明显这里降低了代码的可阅读性,所以谷歌工程师为我们预定义了很多的关键字来表示不同的对象
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;
可以看到这些的本质的是void*,但是我们可以用jobject和jarray这些来说明参数的含义,这样子就可阅读性很强
最后我们写的c/c++代码最后都会被打包成.so文件,所以我们使用的时候需要在Avtivity里面先加载类库
public class MainActivity extends Activity { static { System.loadLibrary("hello"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); int result = add(3,5); System.out.println("result == " + result); } private native int add(int i,int j); }
我们的.so文件要打包成功还需要有一个android.mk,直接从ndk自带的例子里面复制过来就行了
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := hello LOCAL_SRC_FILES := hello.c include $(BUILD_SHARED_LIBRARY)
里面就是这几句话,只要修改hello所在的地方就可以了,你可以把上面的两个hello改成其他的任意名字
利用ndk里面的ndk-build.cmd进行打包我们的c代码
当然了用eclipse关联了ndk之后,直接运行项目就可以了,打包的工作交给eclipse