写于2014年那个办公室停电导致热爆了汗流浃背的夏天。
NDK是由谷歌娘提供的,某种意义上就是可以让android使用c开发的第“三”方sdk,所以,正常来说eclipse是没有配置这个东西的,当然如我所云,我只考虑用最小的工程成本(较少的时间保证一定质量)来实现我的目标,所有我使用的是由谷歌提供的标准的ADT,下载完安装后可以自动完成android开发的基本配置,也就是可以直接拿来写HelloAndroid的开发工具,当然,普通的android开发也不是我这里讨论的问题,所以以下涉及到java下完成android开发的问题我默认所有人都能理解我说的东西,不会做解释,我的解释会放在如何使用NDK上(啰嗦的我啊愿萌萌的油乎乎永安你的灵魂,普天齐明!我受到攻击hp-250,我触发被动天赋厚脸皮hp+2500)。
另外由于我配置完也过了有大半个月了,所以有些细节可能不会记得很清楚,也可能会遗漏一些细节,如果我日后还活着而且想起来了,我发四一定会回来修改补充这篇博客。或者在配置过程中遇到有什么问题可以留言补充,有缘的话我一定会看到的。
Android严格来说是linux的一个分支,换个角度讲,android就是一个被修改过的linux系统,linux系统运行直接用c写的东西当然是没问题的,这也是为什么android可以允许ndk的存在的一个核心原因。而偶们平时使用的c开发版本里边,大方向上可能可以算作有两种,一种是GUNC,一种是个windows用的C(原谅无知的我又忘了叫什么了,要不简称wc吧),这名字不是太重点,重点是这两种c虽然大同小异,但是却会有严重的跨平台问题,gunc是个unix系统使用为主,也就是在unix环境下编译gunc是个事半功倍的事情,wc的问题同。linux是unix的分支,所以linux下也是使用gunc的,换个角度说,我这段想表达的问题很简单,windows下做NDK会多很多麻烦,根据我的原则以及我的开发环境考虑,我勇敢的放弃了在windows下的努力(显然我是被折腾的不行了才放弃的,做项目跟做研究毕竟有差别,项目工程有工期的鸭梨,使用一个更稳定更可控的平台是更好的选择),我使用的开发环境是unix的另外一个分支,mac,也就是苹果的操作系统(显然全名太长了我有限的脑容量是记不住的,打个mac意思一下咯)。换个角度说,android和mac都是属于unix系统的大类中,就像香港人和深圳人要交流总比深圳人跟加州人要交流容易些一样,mac下做android的工作也比在windows下做要容易一些(说白了就是给android做windows开发工具的人比较少——)。换个角度,linux下做ndk也会更容易些,可能某些细节不完全一样,但大体应该是接近的。
嗯嗯,上一章其实没什么好看的,可以跳过,这章开始正式讨论使用ndk过程中遇到(或者说需要解决的问题)。
宏观上偶们先梳理一下逻辑,整个过程中偶们需要解决哪些问题,后续章节会逐个讲述这些问题的具体解决方案(有些方案并不唯一,有兴趣可以自己再研究)。
调用ndk首先要解决的就是ndk在哪的问题?ndk不是凭空出现的,可以到google提供ndk的地方去下载,由于网络的原因可能有些人没法下载到,可以百度到一些第三方的地方下载,注意使用的操作系统是mac,64位,版本肯定要最新的啦,旧的版本可能会出现有些功能不支持的问题。
下载好ndk以后当然是要让开发工具,也就是eclipse君知道有这个东西已经存在在电脑深深的脑海里,TA的硬盘里,TA的内存里,TA的废纸篓里。
eclipse知道怎么从垃圾箱里找出ndk以后,就需要配置ndk开发环境了,android调用ndk的方式一般是通过调用动态链接库的方式完成的,所以所谓配置ndk开发设置,实质上是为了配置编译器如何编译写好的c代码。说真的这块很多麻烦事,我当时是被弄得很崩溃。
显然java无法那么简单就可以调用c的函数,所以需要使用ndk提供的中间接口,这个接口一式两份,一份是用java编写的,另一份使用C编写。这两份协议内容实质是一样的,虽然两者用的语言不一样,就像一些国际商业合同可能会一份是中文,一份是英文一样。
ndk提供的与标准c类似jint、jstring等“c的数据类型”(就是说,这些数据类型虽然姓“j”,但它还是一个c的数据类型。就像金刚石没有金属,也跟某只猩猩没有关系;铅笔也不是铅做的一样。“j”只是描述了这个数据类型具有某些特殊性质),通过这些数据类型可以通过ndk与java中的对应int类型、string类型实现自由转换。
由于动态链接库的后缀是*.so,所以下文统称so文件(体谅下我鼠标手键盘爪多年打字不易)。当你的代码写完以后,就需要开始编译了,编译有两种结果,All In or Nothing(成功或者失败=。=)。成功的话会生成一个so文件,这个so文件可以认为是由以下三个部分编译而成的(你写的代码,你调用的库,ndk的接口),虽然编译so文件并不需要ndk接口的java部分,但如果没有这个部分,这个so文件是无法正常使用的,就像一辆车,没有钥匙是没法开的(神马,你会撬锁,那确实可以用,就是成本略高)使用这个so文件配合ndk接口的java部分就可以形成一个完成的NDK动态链接库了。
到这一步就已经很接近成功了,把动态链接库加入已有的项目,留意之前准备好的ndk接口中的java部分,在正常的java函数中调用它吧,相信自己,你可以的。
调试神马的,就不多说了,大伙都懂的,使用NDK可以考虑配合TDD的开发模式,原理不多说,显然会跑题。
就像所说的第一个问题,先下载好ndk的开发包,解压缩,记下保存的路径,例如
“/Applications/adt-bundle-mac-x86_64-20140321/android-ndk-r9d/”。
显然,java的“编译器”是不认识C的东西滴,同理,C滴东西也不认识java是神马,所以偶们需要通过一些接口协议来完成这样一部分,也就是实现让java能调用C的函数,并正确地把java的对象传给C,同时C能正确的获得并解析Java传过来的对象,并正确地把结果转换为java的对象传回去。
java的对象可以看做两种,一种是如int、double、string这样的值对象,另一种是自己建立的继承自object的自定义对象(以下成为object对象)。事实上两种对象都可以传递给C进行处理,但显然后一种由于是自定义的会更复杂,作为进阶内容,我就不啰嗦了,这里就啰嗦一下普通的数值类型,以下以String类型在java与C的通讯为例,其他的数值传递问题可以举一反三,或者自己百度=。=
假设偶们现在要做的事情是输入一个字符串,然后返回这个字符串的长度,偶们首先实现一个java版的。新建一个叫做NdkForJava的类,package com.hellondk;类定义如下:
public class NdkForJava
{
public int SizeOfString(String str)
{
return str.length();
}
}
这个函数显然不是很难,但里边包含了这些关键点:首先,实现了String参数的传入;再者,对传入的String参数进行了计算;最后,返回了一个int参数。这几点已经可以完整的应付最基本的NDK调用问题了。
那这个函数的NDK协议应该怎么写呢?其实很简单,将上一个函数的主体部分删掉(也就是别写主体,光起名就好),再增加一个关键字native即可:
public native int SizeOfString(String str);
这里有几个点跟java的普通函数写法有些不同,首先,增加了关键字native,也就是NDK中的N,该关键字声明了这个函数的实现交由NDK完成,然后,省略了函数主体,因为函数主体是用C完成的嘛,要在这写了就能用,还要java干嘛=。=
好,就酱,java部分的通讯协议就酱就可以了。
先回到之前创建的“IHateNDK.cpp”中,添加第一行语句,添加一个NDK专用的头文件jni.h,里边包含了java与c实现通讯的核心函数及对象,要完成通讯,这个部分是必须有的,也是ndk专用的一个,标准C(显然我介里说的是GUN-C)没有的头文件。
完成了include以后,根据java协议的情况,声明一个函数如下:
extern "C"
{
JNIEXPORT jint Java_com_hellondk_NdkForJava_SizeOfString(JNIEnv* env, jobject thiz, jstring str);
}
这里有很多关键点:
显然这个函数名隐含天地大道,好像又符合某种天地之理,怎么看和都好像信息量很大的样子,似乎跟java的协议有某种关联(呸,瞎子都看得出来啦,再啰嗦板砖伺候;啊,这要靠领悟的喂)。
C协议中的函数名为了跟java协议中的函数名对应,所以命名的格式(区分大小写)为Java_com_包名1_包名2_……包名n类名_函数名。
NDK使用了“反射”的技术完成了通过java调用C函数的功能,所以这个名字非常重要,千万不能写错,写错的话编译器是无法正常加载NDK中的函数的。
extern "C"
{
//...
}
C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。
前边偶们知道了NDK函数的名字灰常重要,就可以理解为什么这里要使用extern这个关键字了,正如百科所说,为了防止反射被破坏,为了守护函数名的平衡,贯彻爱与正义的邪恶……咳咳跑题了……虽然我一直用C来称呼,但就像本po开篇所说,其实这个C是包含了CPP的含义,因为某个懒货才没写那么清楚。
顾名思义,这是个port,拆解开来应该是Java Native Interface Extra Port(顾名思义尼槑啊谁特么能思的出那么复杂的解释)。好好好,这个关键字的功能就是告诉编译器,别看这个函数名字长的搓,参数也莫名其妙,这可是是在java那边挂了号的,别把TA与普通的C函数等同视之。
可以看到以前常见的int、string类型前边都多了一个恶心难看的j,像这样的jint、jstring就是java的值类型在c函数中的定义,NDK提供了一系列的jxxx用来定义各种数据类型,参考对应如下:
对于特殊的数组类型,则可以通过jxxxArray来传递。
注意,这个图样图森破的数据类型基本就拿来完成从java数据到C数据或者C数据到java数据的转换的中间类型就好了,最好别哪来直接做运算,有时候会产生很多很奇怪的结果(例如内存泄漏)。
所以原来的java函数返回值是int,这里的返回值就改成了jint,原来的java函数参数是String,所以这里就改成了jstring。嗯,么么哒(么么哒你槑啊,没发现多出了两个参数啊,那个thiz是什么来的啊拼的辣么奇葩的是日语还是韩语啊)。
关于这个嘛,今天天气好像很好的样子。
好,这个问题提的很好,能提出这个问题说明童鞋你的数学已经有一定境界了,都快超过幼儿园的小盆友了,灰常好灰常好。
好好言归正传,所有的ndk下的jniexport协议,也就是说已经在java那边挂上号的函数,天生自带天赋,第一个参数必须是JNIEnv类型,第二个参数必须是jobject类型。也就是说,即便java那边的native函数没有一个参数,这边对应的jniexport函数也要有这两个参数。
JNIEnv指针是JVM创建的,用于Native的c/c++方法操纵Java执行栈中的数据,比如Java Class, Java Method等。JNIEnv中定义了一组函数指针,c/c++ Native程序是通过这些函数指针操纵Java数据。这样设计的好处是:你的c/c++ 程序不需要依赖任何函数库,或者DLL。由于JVM可能由不同的厂商实现,不同厂商有自己不同的JNI实现,如果要求这些厂商暴露约定好的一些头文件和库,这不是灵活的设计。
转载自别人的博客
thiz指代的是调用这个JNI函数的Java对象,有点类似于C++中的this指针。但因为在这里是特殊指定java的对象,所以与一般的this做了一个区分,使用了thiz。
【至于为什么会出现这个东西,其实可以追溯到java是一个完全面向对象的开发语言,而C却是一个函数式面向过程的语言,但由于这东东以讲又是进阶内容,其实就是如果面向对象开发学的比较扎实或者做的比较多的话很好理解,而且不理解也不怎么妨碍基本使用,所以就不详叙了】
由于java部分才是Android的主体,所以整个流程都是由java部分的代码驱动的,一般来说ndk只用于完成计算的部分,所以偶们可以把java看作发送方,ndk看作接收方,java发送的数据需要通过ndk进行一些转换,才能交给C的部分进行进一步的计算,所以数据从接受后的操作方式来说可以归纳为三类(个人意见),以下对它们的基本操作进行分类说明:
有一些操作是对所有的这些类型都有效的,首先要考虑的一个问题就是内存的管理分配问题,当java部分声明并给一个对象分配了内存空间以后,把这个对象的引用参数传递给ndk之后,ndk就需要面临一种选择,即,之后使用C对这些参数进行操作的时候,是直接对这个对象的内存本身进行操作呢,还是将该对象的内存拷贝一份,再对拷贝进行操作。
这两种方案各有利弊,直接操作的话,有可能会导致内存泄漏、内存覆写等不容易控制的后果,需要在开发的时候花费更多的注意力去关注这部分对象(内存)的生命周期。拷贝法简单直接,而且由于是在拷贝上执行操作,所以可以不用担心对java部分产生影响,只要维护好自己的生命周期即可。
再者,在实际开发的过程中,特别是涉及到数组等线性表或者非线性表的操作时,如果使用自动变量,我遇到过很多次同样的代码,由于自动释放的时机不同,导致有些手机可以正常运行,有些手机无法正常运行的情况,这也是NDK技术不是非常成熟的表现,为了避免这个问题,建议在使用的时候,尽可能手动分配和管理C部分的内存。
由于刚刚声明的函数SizeOfString传递的参数String有特殊的使用方式,我下边会专门独立拿出来说说,这边为了展示标准值类型的传递,偶们先做这么一个函数,完成对输入变量的加法运算,这个函数的java部分如下:
public native int Plus(int a, int b);
然后根据偶们之前提到的NDK部分的命名原则,偶们可以写出它的ndk形式,注意大小写噢:
JNIEXPORT jint Java_com_hellondk_NdkForJava_Plus(JNIEnv* env, jobject thiz, jint a, jint b);
然后偶们现在在函数主体中就得到了一个jint的数据a,和另一个jint的数据b。现在第一步偶们要做的事情是把这个jint的数据转换为int类型,方便偶们做进一步的处理。从jin.h的头文件中偶们可以找到如下的定义:
以jint为例,如果再追溯的话可以看到:
typedef __int32_t int32_t;
然后是:
typedef int __int32_t;
如果传入的是这类数据类型,毫无疑问传入的将会是一个值参,所以这个时候你可以放心的对传入的参数进行各种操作,因为你的操作都是直接发生在拷贝的副本上的,如果没有意外的话,这个副本会随着函数的结束而被释放(考虑到使用NDK的童鞋肯定要有C的背景,我这里可能会借用一些C的术语来描述某些JAVA没有的状态,相信C基础扎实的您一定可以明白我想表达的意思=。=好吧就是我词穷的不知道该怎么说)。其实跟java中对int参数的处理一下,无论你在函数对对传入的int数组怎么加减乘除,主调用函数中的传入参数并不会发生改变。
简单的说,jint其实是int数据别名。所以Plus函数的某种写法可以是:
JNIEXPORT jint Java_com_hellondk_NdkForJava_Plus(JNIEnv* env, jobject thiz, jint a, jint b)
{
int pA = a;
int pB = b;
return pA + pB;
}
或者是:
JNIEXPORT jint Java_com_hellondk_NdkForJava_Plus(JNIEnv* env, jobject thiz, jint a, jint b)
{
return a+ b
}
其他的上图列出的数据类型都可以这样用,那么为什么还要弄个jint出来呢?因为下边有数组的问题需要解决。
本节参考
这一块会比较复杂,偶们都知道,java默认的数组提供了很多额外的功能,例如说你可以直接访问数组的长度。但是,对应的C的数组是没有这些信息的,在NDK这边获得的最终将是一个对应数据类型的指针(甚至可以是jobject类型),就像C对数组的处理一个,你得到的只是数组第一个数据所在的内存地址,后边的访问要靠索引器(实际上是内存地址的平移)来进行访问了。
偶们先来看看怎么获得数组的第一个索引的指针,首先先创造一个进阶的Plus函数,该函数
完成了把数组a和数组b中的所有数值相加并返回的功能,偶们先定义它的Java部分:
public native int Plus(int[]a, int[]b);
然后是NDK部分
JNIEXPORT jint Java_com_hellondk_NdkForJava_Plus(JNIEnv* env, jobject thiz, jintArray a, jintArray b);
好,第一步,偶们要决定是否创建一份该数组的拷贝,记得前边说的么,创建或者不创建各有各的优点,所以偶们可以定义一个参数来标记它:
jboolean ifCopy = JNI_FALSE;//JNI_TRUE;
可选的结果有两个,JNI_FALSE和JNI_TRUE,其实这是jni提供的宏定义,实际上就是常数0和常数1,按照避免“魔数”的原则,我更愿意这样来写。顾名思义,False的时候,不会对传入的数组进行拷贝,而是直接在原数组的内存上进行操作;True的时候会进行拷贝,在拷贝的数组上进行操作。
然后,一般来说偶们还需要获得数组的大小,要不然很可能会发生索引越界的情况:
int lengthOfArrayA = env->GetArrayLength(a);
int lengthOfArrayB = env->GetArrayLength(b);
之后,偶们开始考虑怎么获得数组,就像偶们所熟知的那样,C下边的数组是一个指针:
int* arrayA = env->GetIntArrayElements(a,&ifCopy);
int* arrayB = env->GetIntArrayElements(b,&ifCopy);
这里要留心的是,GetIntArrayElements函数的第二个参数是一个执行jboolean类型的指针,所以偶们这里需要对前边声明的ifCopy增加一个&符号,以传入正确的参数。
OK,现在数组有了,数组的长度也有了,偶们可以完成下一步的相加的工作了:
int sum = 0;
for(int i = 0; i < lengthOfArrayA; i ++)
sum += arrayA[i];
for(int i = 0; i < lengthOfArrayB; i ++)
sum += arrayB[i];
这一块是标准的C++写法,不多啰嗦了。按照一般的逻辑,下一步应该直接用return语句把结果返回,这个函数就结束了。
不过很遗憾,这样做的话,在有些手机上会出现内存泄露的问题,因为在执行env->GetIntArrayElements函数的时候,无论传入的ifCopy是True还是False,返回的数组指针(在本例中就是arrayA和arrayB啦)所指向的那一片内存(没错,是那一片,整一个数组所有的内存,而不仅仅是索引为0的位置的内存)都会被系统锁定,防止被java自动回收,这个锁定是需要手动解除的,所以在执行完上述的计算过程以后,偶们还需要这样解除被锁定的内存,否则会产生内存泄漏(根据我用过的这么多开发机,确实有些机器不会在这里发生错误,但为了尽可能的兼容更多机型,手动解锁还是必须的步骤)。
在这里偶们先来看函数原型:
void ReleaseIntArrayElements(jintArray array, jint* elemts, jint mode);
参数array就是这个数组来源的jxxxArray对象,elems就是刚才使用GetXXXElements函数获得的数组指针,mode是由jni.h定义的三种模式,其中有:
#define JNI_COMMIT 1 /* copy content, do not free buffer */
#define JNI_ABORT 2 /* free buffer w/o copying back */
另外,还可以取值为0,此时表示在更新数组元素后释放elems缓冲器;取JNI_COMMIT的时候,表示在更新数组元素后不释放elems缓冲器 ;取JNI_ABORT的时候,表示不更新数组元素释放elems缓冲器。一般来说,偶们取0。
所以在本例中,偶们需要执行:
env->ReleaseIntArrayElements(a,arrayA,0);
env->ReleaseIntArrayElements(b,arrayB,0);
最后完成return的工作,合起来偶们得到的函数可以是这样的:
JNIEXPORT jint Java_com_hellondk_NdkForJava_Plus(JNIEnv* env, jobject thiz, jintArray a, jintArray b)
{
jboolean ifCopy = JNI_FALSE;//JNI_TRUE;
int lengthOfArrayA = env->GetArrayLength(a);
int lengthOfArrayB = env->GetArrayLength(b);
int* arrayA = env->GetIntArrayElements(a,&ifCopy);
int* arrayB = env->GetIntArrayElements(b,&ifCopy);
int sum = 0;
for(int i = 0; i < lengthOfArrayA; i ++)
sum += arrayA[i];
for(int i = 0; i < lengthOfArrayB; i ++)
sum += arrayB[i];
env->ReleaseIntArrayElements(a,arrayA,0);
env->ReleaseIntArrayElements(b,arrayB,0);
return sum;
}
啊哈,好像又解决了一个问题耶,休息,休息一会~
(´Д`)好吧其实在数组这块还有一个问题,木有错,上边只讨论了如何处理传入的数组,但是如果有童鞋想传出一个数组该肿么办?
介是一个复杂的问题,偶们,呃,偶们还是来聊聊天气吧。
好,为了说明这个问题,我先创建一个新的函数,它的功能是返回一个排序从1到100的数组,系不系很激动,那TA的java和ndk部分应该是什么样的呢?
快速解决一下Java的部分:
public native int[] ArrayFrom1To100();
快速的解决一下C的部分,弄个数组出来。
表示我懒筋烦了,于是TA决定就假设存在一个int* resultArray的数组已经弄好了,这个数组的长度是100,就等传出去。
JNIEXPORT jint Java_com_hellondk_NdkForJava_ArrayFrom1To100(JNIEnv* env, jobject thiz, jintArray a, jintArray b)
{
int length = 100;
int* resultArray = new int[length];
//自己给数组赋值完成1到100的壮举
}
为了把数据传出去,偶们必须先创建一个jintArray:
jintArray result = env->NewIntArray(length);
这时偶们需要使用SetIntArrayRegion函数,偶们首先来看看函数原型:
void SetIntArrayRegion(jintArray array, jsize start, jsize len, const jint* buf);
这个函数的功能就是快速把一个int数组的值拷贝到一个jintArray里边,其中要求创建jintArray的时候,这个jintArray的长度不能小于被拷贝的数组的长度(也就是len-start),否则就会出现错误。array表示准备被赋值的数组,也是偶们打算return出去的数组;start指从被拷贝数组中开始拷贝的起始位置(根据题目需求,这里我会从0开始拷贝,但不意味着不能从18,36,甚至40开始拷,但这样会不符合这个函数的需求);len指从start位置开始,连续拷贝的数据的数量(同样根据题目要求,这里偶们需要拷贝100个,但同样会违背偶们函数的需求);buf就是偶们在C环境下完成的数组的索引0的指针。因此偶们应该执行的语句是:
env->SetIntArrayRegion(result,0,length,resultArray);
最后,偶们处理一下数组的内存管理问题,就可以return了,所以偶们应该看到的完整函数是:
JNIEXPORT jint Java_com_hellondk_NdkForJava_ArrayFrom1To100(JNIEnv* env, jobject thiz, jintArray a, jintArray b)
{
int length = 100;
int* resultArray = new int[length];
//自己给数组赋值完成1到100的壮举
jintArray result = env->NewIntArray(length);
env->SetIntArrayRegion (result,0,length,resultArray);
if(resultArray)
{
delete resultArray;
resultArray = 0;
}
return result;
}
最后还是要强调一下,由于我之前提过的自动变量的回收问题,尽可能不要使用自动变量完成数组的操作,改用手动管理,要不有些手机上边会爆出莫名其妙的错。我一贯认为这些不一定会出但是有可能会出问题,特别是跨设备的问题,最好能避免尽量避免。
在ndk的部分,回到偶们刚刚写好的函数SizeOfString,偶们接收到了一个jstring的对象str,这也就是java调动SizeOfString函数时发送过来的对象,但jstring并不能直接进行操作,偶们需要对其进行一些处理以将其转换为C下边可用的字符串对象。其实严格来说,String对象是指是一个char类型的数组,但由于这是最常用的对象,所以ndk把它独立出来作为一个传统的只对象,操作上跟其他的jxxxArray类似,所以获取String的方法跟其他数组大同小异,但要留心的是,函数略有不同:
jboolean isCopy = JNI_FALSE;
const char* cStr = env->GetStringUTFChars(str,&isCopy);
//计算长度的问题由于某个我的懒筋犯了所以掠过
env->ReleaseStringUTFChars(str,cStr);
用到的Get函数需要使用GetStringUTFChars函数,UTF的含义相信能看到这块的童鞋应该不用我多啰嗦啦(但你还是啰嗦了=。=),同样是用完以后使用Release函数处理掉。
这块其实不复杂,参考上前边其他的数组操作就可以完成只是对应函数略不一样,我为什么还要独立把String拿出来说一下呢?
因为首先string是一种非常常用的变量;然后,如果想返回一个String是一个很麻烦的事情。当然有童鞋会说啦,我返回一个charArray,再在java里把它处理成String不就行了么,呃,其实可以算作一种解决方案,但因为String的特殊性,我再提供一种方案(所以这块内容可以算作可有可无的内容啦;骚年图样吖,你看看为什么要加UTF就知道这事情在java那边也没那么简单~好吧这是我瞎猜的,无节操掠过)。
偶们还是先来构造一个基本的函数,它的功能是返回一个字符串,字符串的内容是经典的”Hello World”。
java部分:
public native String HelloWorld();
那,偶们来看看NDK的部分:
JNIEXPORT jstring Java_com_hellondk+NdkForJava_HelloWOrld(JNIEnv* env, jobject thiz);
嗯,看起来好像跟前边差不多嘛,为什么要单独拎出来讨论呢?那么,偶们再看看jstring的具体定义:
typedef jobject jstring;
咦?为什么jstring会变成一个jobject的类型?因为,String虽然是一个很常用的类型,但事实上,它在java里边并没有被视为值类型,简单说,它一般情况下都是被用作引用参数的。所以,偶们在实现jstring的时候,跟之前的部分有些区别:
JNIEXPORT jstring Java_com_hellondk+NdkForJava_HelloWOrld(JNIEnv* env, jobject thiz)
{
char* qrText = "Hello World";
jstring jStr = env->NewStringUTF(qrText);
return jStr;
}
抱头跑……现在其实是要思考这样一个问题,如果我想返回的是一个String[]数组肿么破? 让偶们轻微的调整一个HelloWorld的这个函数,让它返回一个String的数组,数组中分别是“Hello”和“World”,应该怎么办?
首先是Java的部分:
public native String[] HelloWorld();
然后是NDK的部分:
JNIEXPORT jstring Java_com_hellondk+NdkForJava_HelloWOrld(JNIEnv* env, jobject thiz);
在这里偶们要注意一点,是不存在jstringArray这个对象的,虽然jni要typedef一下也不难,但这样很可能会造成不必要的误会,所以jni没有做(虽然我更倾向于认为是偷懒=。=以己度人),所以偶们要返回的其实是一个jobjectArray。
第一步来说,偶们应该先new一个objectArray出来,但这里有一些与之前不同的地方,由于jobject可以是任何java的对象,也就是包括但不止限于String类型,所以偶们要用“反射”的方法指定新生成的objectArray具体是什么类型的数组:
jclass typeString = env->FindClass("java/lang/String");
后边的字符串是特指String类型的,其实还有很多其它用法,这里只把String作为一种最常用的特殊情况拿出来讨论。
然后请先看这么一个new函数:
jobjectArray resultArray = env->NewObjectArray((jsize)2, typestring, NULL);
现在,偶们声明了一个长度为2,每个数据都是NULL,数据类型为java的String类型的数组。可以看到,这个数组是空的,偶们要先准备一些字符串塞进去,例如:
char* text1 = "Hello";
char* text2 = "World";
jstring jstr1 = env->NewStringUTF(text1);
jstring jstr2 = env->NewStringUTF(text2);
字符串好办,但怎么塞却是个问题,还记得之前其他Array的写法吗?过程差不多:
env->SetObjectArrayElement(resultArray,0,jstr1);
env->SetObjectArrayElement(resultArray,0,jstr2);
分别把jstr1和jstr2放到索引为0和索引为1的位置。那么,这个时候偶们可以放心的return数组了么?很遗憾,不是的,偶们还需要管理一下jstr的生存周期问题,由于SetObjectArrayElement做的是把目标对象拷贝了一份,所以,set完成之后,jstr1就没用了,但由于ndk的机制,new出来的jstring并不会自动回收,所以偶们还有手动删除一下:
env->DeleteLocalRef(jstr1);
env->DeleteLocalRef(jstr2);
好,现在看看这个函数的完整写法:
JNIEXPORT jstring Java_com_hellondk+NdkForJava_HelloWOrld(JNIEnv* env, jobject thiz)
{
jclass typeString = env->FindClass("java/lang/String");
jobjectArray resultArray = env->NewObjectArray((jsize)2, typestring, NULL);
char* text1 = "Hello";
char* text2 = "World";
jstring jstr1 = env->NewStringUTF(text1);
jstring jstr2 = env->NewStringUTF(text2);
env->SetObjectArrayElement(resultArray,0,jstr1);
env->SetObjectArrayElement(resultArray,0,jstr2);
env->DeleteLocalRef(jstr1);
env->DeleteLocalRef(jstr2);
return resultArray;
}
对于这种对象,我只能说,我不会用。对于我来说使用成本太高了,有兴趣的童鞋可以自己研究下=。=上文的String[]类型的处理是jobject的一种情况,更复杂的情况下还可以在ndk中调用java对象的特定函数完成一些工作,但这些都有点遥远,而且偶们用ndk的目的不就是为了使用C的高效运算能力么?所以这块我没有太大的动力去研究,起码放在我现在这个项目来看(懒就懒啦,死性不改)。
其实咧,上边神马接口吖,神马编译吖,别以为就可以用了,因为,NDK属于比较手动的工具,编译成链接库还要设置很多东西滴,而这些设置是没有界面给你方便的使用的(没银性啊),甚至还需要用一种特定的语言书写(相当于又编一个程序啦-^-),语言的复杂来说,我也不太懂,我这里只介绍一些基本的设置,复杂的使用方式请自己再研究啦~
要设置mk文件之前,显然偶们需要先创建一个,如果是第一次创建mk文件,可能eclipse会提示安装一个插件,按提示安装即可,这个插件只是用来快速格式化文本的,我觉得应该不是必须品,但对优雅的IDE(显然eclipse不算)有重度依赖症的我毫不犹豫的就装了,就算烂,也总比没有好(不是说好了不吐槽了喂~)。
一般来说,add native support以后会默认创建一个Android.mk文件,但如果没有的话也不要紧,在jni文件夹中右键-New-File,然后文件命名为Android.mk即可,注意后缀和大小写。
一个最简单的mk文件是这样的:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := IHateNDK
LOCAL_SRC_FILES := IHateNDK.cpp
include $(BUILD_SHARED_LIBRARY)
第一行相当指定了当前文件夹为默认根目录,之后所有关于文件(路径)的操作都会基于这个目录来进行。
第三行使用了一个include命令,这里的include命令并不是C语言中的引用某个文件的意思,而是执行某个命令,这里的代码中,第三行意思是清空当前所有的命令(也可以理解另起一组编译选项),第八行表示建立一个动态链接库。
第五行命令确定了即将编译出来的so文件的名称,如前文所言,所有的动态链接库都是而且必须以lib+这里写的名字+.so作为结尾才能正常使用。
第六行命令列出了所有要编译的c/cpp文件的目录,没错,是所有,也就是如果你有100个cpp的文件,麻烦用空格作为分隔符把所有的名字带后缀全部写出来!
好吧这个是逗你玩的,还是有一些方法可以省略一部分文件名,但确实还是很不方便,更多的参数及设置之后再介绍。
现在通过这个文件偶们可以简单的完成动态链接库libIHateNDK.so的编译(command+b)。
现在偶们来简单介绍一些进阶设置的情况:
这里就介绍四个比较重要的操作符,更多内容请自己研究。
首先是“:=”操作符,该操作符的意思是初始化并把右边的值赋给左边。
然后是“+=”操作符,该操作符的意思是在左边已有的设置基础上增加一个参数值。这个有点难理解,一会统一用一个例子来解释一下。
之后是“\”操作符,这个操作符的意思是下一行跟当前行是连在一起的,由于mk文件的命令使用换行的方式表示结束,所以必要时需要通过“\”来对一些较长的命令进行排版。
最后是“#”操作符,该操作符的功能就是注释本行中该操作符之后的所有文本。
下边偶们来看一个例子:
#这是甲写法
LOCAL_SRC_FILES := IHateNDK.cpp IHateJNI.cpp
#这是乙写法a
LOCAL_SRC_FILES := IHateNDK.cpp \
IHateJNI.cpp
#这是乙写法b(错误写法)
LOCAL_SRC_FILES := IHateNDK.cpp
IHateJNI.cpp
#这是丙写法
LOCAL_SRC_FILES := IHateNDK.cpp
LOCAL_SRC_FILES := IHateJNI.cpp
简单的说,甲写法、乙写法a和丁写法可以认为是等价的,而乙写法b是一个错误示范,考虑到用得着NDK的童鞋肯定都是学贯CJ滴银,我就不多啰嗦了。
如果有100个文件,我难道要把100个文件都写一遍么?不要哇(我已抓狂),明明只要把jni文件夹下的所有文件都默认索引了就可以了吧?(但这样就缺少订制能力了,某大神推推眼镜道。qu shi=。=)
NDK提供了一种折衷的方案,起码我只找到这种啦,再好的暂时没见着。可以让编译器自动引用某个文件夹下的所有指定后缀的文件(至于能不能编译成功就看骚年你真正的技术了),而且注意,这个引用是非循环的,也就是说,被引用的文件夹下的子目录内的文件并不会被搜索,所以,折衷方案就是,你把所有文件夹的路径写一遍(啊多么痛的领悟啊这是那么无聊啊)。
这个命令的原理是首先创建一张列表,列表上标注了所有文件夹的路径,以及该路径需要被引用的文件的后缀名:
MY_CPP_LIST := $(wildcard $(LOCAL_PATH)/*.cpp)
这里引用的是默认目录,也就是jni文件下所有的后缀为cpp的文件,是不是有理有据让人信服?如果偶们要再增加一个文件夹,可以使用之前提到的“+=”操作符:
MY_CPP_LIST := $(wildcard $(LOCAL_PATH)/*.cpp)
MY_CPP_LIST += $(wildcard $(LOCAL_PATH)/sbndk/*.cc)
这一句在原来的基础上增加了jni目录下bigint目录下所有后缀为cc的文件。好,按照这个方式可以创建一个很长的列表,但是怎么让这个列表生效呢?如下:
LOCAL_SRC_FILES += $(MY_CPP_LIST$(LOACL_PATH)/%...%/
搞定,这样编译的时候编译器就会自动索引列表中所有你指名后缀的文件啦。
这是个奇怪的世界,我一直没弄明白有什么用,不过确实有一些参数需要在这个文件里边设置,如果默认没有的话,跟创建Android.mk一样的方式创建吧。
【参考手册:http://www.oschina.net/question/565065_93983】
我没能解决调试的问题,所以只能使用Log的方式来输出一些信息辅助调试,这个过程略烦躁(我躁狂症晚期没救了)。
首先在需要Log的文件中加入头文件:
然后在Android.mk文件中设置参数:
调用函数如下:
((void)__android_log_print(ANDROID_LOG_WARN, “logcat中tag列的值","要打印的具体字符串" ))
由于Log的等级问题,可以把参数ANDROID_LOG_WARN替换为如下几种:
__android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,“***”) // LOG类型:debug
__android_log_print(ANDROID_LOG_INFO,LOG_TAG,“***”) // LOG类型:info
__android_log_print(ANDROID_LOG_WARN,LOG_TAG,“***”) // LOG类型:warning
__android_log_print(ANDROID_LOG_ERROR,LOG_TAG,“***”) // LOG类型:error
__android_log_print(ANDROID_LOG_FATAL,LOG_TAG,“***”) // LOG类型:verbose
Log的结果会输出在Logcat中,相信用Java的银肯定比我熟,我继续犯懒去咯。
相信stl的重要性我不必多言,如果不知道stl是神马的童鞋相信要么是已经超越了stl,要么是还没到用得着stl的时候,这章可以先不看了。
在NDK目录中,存在着一个神秘的文件夹,里边全是各式各样的stl头文件,但是骚年以为只要把这些头文件include进去就可以了么?可没有那么简单,但偶们首先可以先找到TA。
由于NDK提供了四个版本的stl库供选择,但最推荐的是stlport_static,所以我调用的也是这个(其实是另外几个根本编译不起来)。
首先打开Application.mk文件,新增参数:
APP_STL := stlport_static
APP_CPPFLAGS := -frtti
LOCAL_CPPFLAGS += -fexceptions
然后,暂时想不起来还要添加什么了,在项目上点击右键-New-Folder,选择Advanced,再勾上Link to alternate location (Linked Folder)。Browse,找到NDK开发包\sources\cxx-stl\stlport\stlport,注意是两个stlport噢,是里边的那个,选中它,但别打开,因为需要引用的就是这个文件夹。
以vector为例,在需要使用vector的地方引用vector。
#include
std::vector<int>* test = new std::vector<int>();
然后的事就不多说啦。
商业机密(懒病病入膏肓放弃治疗)。
在其他的Java项目下的libs文件夹,如果没有则手动创建一个,再在libs文件夹下再创建一个armeabi文件夹,把编译好的so文件放在这个文件夹里,注意so文件的名字一定是’lib’开头的,这个问题我已经提过好多次啦。
在需要调用偶们ndk的java接口的地方增加一段静态代码:
static
{
System.loadLibrary("IHateNDK");
}
注意这里的名字是没有lib的,但文件夹中的名字一定要有lib开头(啰嗦成瘾无药可救)。
然后,在你想用的地方进行灰翔吧~~~
解决方案:
显然从文字上就能看出是因为使用的AndroidManifest文件中的支持的最低版本的参数和NDK编译配置文件中的最低版本配置不相符,将两者统一即可(以下以14为例,不一定要14,只要统一即可)。
在Application.mk文件中修改(或增加)关键字:
APP_PLATFORM := android-14
修改AndroidManifest文件中的android:minSdkVersion=”14”
果然跟我用之前猜的差不多,eclipse不愧是我非常非常没有猿粪的开发工具,我简直已经无力吐槽了,为什么稳定性这么糟糕的东西居然还能被如此多大牛开发者甘之如饴?一群叫嚣着要开发出世界上最人性化UI的人连自己用的工具都不稳定(人性化?呵呵),就像一个拿着漏勺烂锅残口菜刀的乞丐跟你说他能做世界上最好的叫花鸡一样——你信吗?当然,厨师可以不会(一般也不)生产菜刀,生产菜刀的也不一定是厨师,但起码要能分辨出好的菜刀;开发者可以不会开发开发者工具,但开发者工具一定是开发者开发的。
项目的需求是使用NDK将偶们以前使用的一些在xcode下编译的用于objective-c的c++静态库(显然,偶们有源码)移植到ndk的环境中,NDK是由谷歌娘提供的可以允许java的安卓程序在安卓平台下以c/cpp(由于所有人都知道c跟cpp的关系,不知道的人请不要浪费时间看这篇东西了,然后那个反斜杠打起来很麻烦,以下所有写c或cpp的地方请自己脑补另外一半)的方式(实际上跟语言特性有关,不多说,反正运算速度比java下直接执行要快很多)运行一部分函数的功能,加快运算速度。因为偶们原有的项目涉及到大量的图形运算,显然java本身并不擅长做这些东西(原谅我一生java黑),他已经被java各种不稳定(显然,这里更多是IDE,也就是eclipse的问题)弄的无数次想砸电脑了,我不否认java在软件开发史上的地位,以及作出的贡献,就像我不否认无声黑白电视是一个伟大的发明一样)(不擅长并不代表不能做,请明白擅长的意思是很容易就能做好)(关于java的问题请各位读者老爷不要跟我讨论了,我这里用到也是迫不得已,请相信我如果有的选根本不想碰这个东西,也对此不感兴趣,所以我的使用完全是为了达到目的而完成的,如有得罪请见谅,萝卜白菜各有所好,谢绝人参公鸡,如果有忍不住的请相信我也不是那种忍得住的银)。