嘛,好久不来了,忙成狗,不过收获很多,本文就是其中之一。
最近把JNI这玩意儿深度学习了下,之前虽然也做过,但都比较松散,没有系统的把整个知识框架和技术体系梳理过。网上也看了很多博文,基本说的都是环境配置然后一个Hello from jni的玩意儿,然后就没有然后了,基本很少有具体的应用以及对相应核心api的介绍,对其思想的介绍就更少了,所以想自己还是总结一篇出来,并不是为了装逼,就是怕过半年自己又全还给自己了(谁信啊)……
先来张葛大爷的图镇楼!
一、JNI到底是干嘛用的
百度都能查到的官方解释我就不多说了。我自己的理解是两方面,一方面主要用于各种复杂算法的执行,C的效率高自不必说,更重要的是so的破解难度要远远大于apk了,很多公司就靠算法活着的,没有JNI用他们要死了~;另一方面,JavaNativeInterface,作为Java和C间的bridge,我们可以把很多java代码可能无法实现的事情丢到C层去搞。
二、怎么跑起来一个带JNI代码的工程
我用的是AS2.1,主要学姿势,所以默认各位看官的环境已经折腾好,如果没折腾好百度一找一洗脸盆,学一下就好了。AS现在对NDK的支持还是很感人的low,不过编译很轻松,所以凑合着用吧。简单步骤总结下,不一样的同学不用喷,这不是重点:
1.写个类,写好自己需要的native方法,像这样:
package com.amuro.ndkcompactdemo.chapter_1;
/**
* Created by Amuro on 2016/6/23.
*/
public class Chapter1JNI
{
static
{
System.loadLibrary("ndk_compact");
}
public native String stringFromJNI();
public native int add(int a, int b);
public native String addString(String str);
public native int[] addArray(int[] array);
}
2.写好之后,编译你的代码,保证通过。然后用AS自带的Termial进入工程根目录,我的根目录是这样的:
D:\DevelopAS\workspace\NDKCompactDemo
好,然后进入根目录的以下目录:
D:\DevelopAS\workspace\NDKCompactDemo\app\build\intermediates\classes\debug
然后输入命令javah -jni com.amuro.ndkcompactdemo.chapter_1.Chapter1JNI
就这样会在刚才那个目录下生成一个.h头文件。不要问我javah是什么也不要问我h文件是什么,自己百度。生成头文件代码如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI */
#ifndef _Included_com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
#define _Included_com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
* Method: stringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_amuro_ndkcompactdemo_chapter_11_Chapter1JNI_stringFromJNI
(JNIEnv *, jobject);
/*
* Class: com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
* Method: add
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_com_amuro_ndkcompactdemo_chapter_11_Chapter1JNI_add
(JNIEnv *, jobject, jint, jint);
/*
* Class: com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
* Method: addString
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_amuro_ndkcompactdemo_chapter_11_Chapter1JNI_addString
(JNIEnv *, jobject, jstring);
/*
* Class: com_amuro_ndkcompactdemo_chapter_1_Chapter1JNI
* Method: addArray
* Signature: ([I)[I
*/
JNIEXPORT jintArray JNICALL Java_com_amuro_ndkcompactdemo_chapter_11_Chapter1JNI_addArray
(JNIEnv *, jobject, jintArray);
#ifdef __cplusplus
}
#endif
#endif
3.在工程的src/main目录下创建一个jni文件夹,把刚才生成的h文件复制过来。
4.对Gradle进行配置
1)app module的build.gradle下增加ndk配置,完整gradle文件如下:
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
applicationId "com.amuro.ndkcompactdemo"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
ndk{
moduleName "ndk_compact"
ldLibs "log", "z", "m" //添加依赖库文件,因为有log打印等
abiFilters "armeabi", "armeabi-v7a", "x86"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.3.0'
}
第一个是你自己的so库名称,第二个看注释,第三个复制粘贴就好
2)在gradle.properties中添加:
android.useDeprecatedNdk=true
不要问我为什么。
5.针对之前生成的h文件,写自己的源文件,c或cpp均可,记得要把h文件include进来。
6.然后就可以编译执行啦,是不是好简单,AS帮我们把以前Eclipse需要的Android.mk文件的编写给自动化了。
三、用JNI你需要知道的一些知识
1.指针和引用
Java把C最复杂的知识给封装了,而回到C的我们不得不把他们重新捡起来,记得我当年刚学也是被折腾的死去活来,其实想明白之后真的不复杂。如果你C的知识都还给谭老师了,建议你花个一两天先去捡回来,再回头搞会发现很多源码理解起来就轻松多了。这里举个例子,看代码:
先定义一个结构体:
struct Student
{
int age;
char* name;
};
然后定义一个指针指向它:
typedef const struct Student* Stu;
然后我们要怎么用呢:
func(Stu* student)
{
printf("%d", (*student)->age);
printf("%d", (**student).age);
}
int main()
{
struct Student s = {10, "amuro", 100};
Stu stu = &s;
printf("%d", stu->age);
printf("%d", (*stu).age);
func(&stu);
}
运行结果四者打印出来的是10。大概解释一下:
stu是s的指针,也就是说stu里是s的地址,所以stu在赋值时等号右边必须是一个地址值,而&就是取地址符。
从指针中取值的时候有两种方法,代码里都写了。
函数传递指针的时候要特别小心func的参数其实是一个指向stu的指针,也就是指针的指针。所以传值的时候应该把stu的地址传过去。而函数中的student保存的是stu的地址,所以*student才是取出stu地址里保存的s的地址,这时候就可以进行取值操作了。画个图大家就更好理解了:
举这个例子是为了方便大家理解JNIEnv *env这个玩意儿,其实我们自己函数中拿到的env就是刚才例子里的student,所以我们要调用env中的方法的时候,才会这样写:(*env)->MethodName
2.Java标准类和JNI接口的对应关系
图我就直接盗了,反正口诀就是java类前面加个j就好了,然后首字母改小写。
3.jni.h在哪里
很多帖子都会列一堆jni函数,看了一头雾水,其实只要我们找到源码,很多问题会非常容易解决。jni.h的源码在我们下载的NDK包里。我的地址是:
D:\DevelopAS\asSDK\ndk-bundle\platforms\android-23\arch-arm\usr\include
所有的方法都在里面了,然后根据需求去里面找就好了。
文档的话看这里:
http://docs.oracle.com/javase/7/docs/technotes/guides/jni
看到这种密密麻麻的文档当时就想死了。
4.函数签名
很重要,在C层反射的时候,都需要传这东西,百度能找到很多详细的帖子,我这里就取其精华了。
注意注意,这个表写错了,long的签名是J,我当时就被坑死了,查了好久才查出来!
举个例子,比如
public String doSomething(int a, String x, long[] b);
签名就是:(ILjava/lang/String;[J)Ljava/lang/String;
括号里是参数列表,注意非原生类一定要在前面加L后面加;,括号后面是返回值。分号一定不要忘记!!!!
其实还能用javap命令来搞的,但是个人觉得还不如理解之后自己手写,熟悉之后效率高多了。这里就不赘述了。
5.JNIEnv是个啥
1)JNIEnv是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境 ;
2)JNIEnv 与 JavaVM : 注意区分这两个概念;
– JavaVM : JavaVM 是 Java虚拟机在 JNI 层的代表, JNI 全局只有一个;
– JNIEnv : JavaVM 在线程中的代表, 每个线程都有一个, JNI 中可能有很多个 JNIEnv;
3)JNIEnv 作用 :
– 调用 Java 函数 : JNIEnv 代表 Java 运行环境, 可以使用 JNIEnv 调用 Java 中的代码;
– 操作 Java 对象 : Java 对象传入 JNI 层就是 Jobject 对象, 需要使用 JNIEnv 来操作这个 Java 对象;
4)JNIEnv 体系结构
线程相关 : JNIEnv 是线程相关的, 即 在 每个线程中 都有一个 JNIEnv 指针, 每个JNIEnv 都是线程专有的, 其它线程不能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv;
– 当前线程有效 : JNIEnv 只在当前线程有效, JNIEnv 不能在 线程之间进行传递, 在同一个线程中, 多次调用 JNI层方法, 传入的 JNIEnv 是相同的;
– 本地方法匹配多JNIEnv : 在 Java 层定义的本地方法, 可以在不同的线程调用, 因此 可以接受不同的 JNIEnv;
5)JNIEnv 结构 : 由上面的代码可以得出, JNIEnv 是一个指针, 指向一个线程相关的结构, 线程相关结构指向 JNI 函数指针 数组, 这个数组中存放了大量的 JNI 函数指针, 这些指针指向了具体的 JNI 函数;
四、JNI要怎么用
这个话题太大了,就我自己的总结来看,比较常用的其实无非就是四种,一种是操作基本类型,一种是String,一种是操作我们自定义的类型,一种就是数组。
1.常用核心API接口
妈蛋,做表格好蛋疼,就继续用截图代替吧……
基本看到名字都能猜到意思了,有了方法名字,想要知道怎么用,传哪些参数的话,去查文档或者我刚才说的jni.h源文件就好啦。
2.还是回去用Eclipse吧
用了一下AS之后真是觉得弱爆了,虽然编译方便了很多。要搞复杂的NDK开发还是要靠Eclipse,自己写Android.mk文件和Application.mk文件吧。
下面总结下这两个文件的常用配置:
3.我能想到最痛苦的事,是没有代码提示~
记忆力拔群的手写代码大神请绕道。
反正一开始用AS我是快疯了,基本没法写代码,写一行就要去查一下jni.h的头文件看看方法名和传入参数的定义,同时也不能调试,只能跑起来之后看日志或者报错,效率低下不能忍。那怎样让熟悉的代码提示出现呢,这里提供两个方法。
方法一:
1)你需要一个微软的VisualStudio,版本不必太高,我用的2008,建一个工程,选dll工程。
2)从前面说的jni.h的头文件目录下,把jni.h和另一个叫jni_md.h的文件,加上你自己javah生成的头文件一起copy到studio项目目录下,像这样:
然后在你的工程里把这些头文件导入:
然后自动提示就出来啦啦啦啦~
也没有要死的各种红线绿线。
3)编译出的dll文件其实是可以拿来用的(学过操作系统的童鞋都知道,dll动态链接库其实就是windows上的so),这里有一个注意点,就是如果你安装的是64位的java,记得一定要把VS的编译条件改成x64,不然编译出来的dll文件java是没法解析的。把你生成的dll文件的路径配置到环境变量的PATH下面,然后写一个java程序就能用load这个dll啦。我的配置是这样的:
D:\VisualStudio\Projects\NDKTest\x64\Debug;(这里面有个NDKTest.dll文件)
4)写个Java程序去测试这个JNI代码吧,System.loadLibrary的时候,lib名就传“NDKTest”就OK了。这个适合测试算法类的JNI,测试成功后直接拿到Android程序里用就好了。毕竟对C的支持,VS是无出其右的。
方法2:Eclipse增加代码提示
1)我相信这个才是很多童鞋想要的~为了把这个搞出来花了我一整天折腾各种环境和配置,Orz
2)确认你的eclipse安装了cdt,如果没有,去官网找下载地址去,我的是这里:
http://download.eclipse.org/tools/cdt/releases/juno
注意这个地址是在eclipse的install new software中使用的,不要直接网页打开。
3)下载MinGW,安装MinGW,配置MinGW,请看教程:
http://blog.csdn.net/hujingn/article/details/5849516
4)新建C项目,选择如图,记得右边一定要选MinGW
5)好了,出现了我们熟悉的代码界面,stdio.h,stdlib.h都能看到源码了,爽,但是这时候并没有jni的源码,别急,右键项目,把ndk-bundle里源码的地址配置进去:
具体看图,我就不贴了。配置完成确认OK。
6)激动人心的时候终于到了,加上jni.h的include,看到变蓝色没,变就对了。这时候我们就可以直接F3啦~然后,我们就可以把我们的javah生成的头文件拷过来了,然后的然后请看图:
这下想看什么源码,都轻松愉快了,有些小算法,还可以在下面的main函数里自己测试。
差不多就是这些东西了,剩下的就是学习各种API的使用了,作为程序员这个是必备技能了。跪求谷歌尽快完美支持NDK开发,这样折腾实在是太痛苦了。如果各位有更好更优秀的方式,欢迎留言讨论~
就酱~