2012年2月29日星期三

在新的Android工程中调用已经编译好的so库文件代码

依旧是Android,NDK,JNI相关的问题。
  照着ndk帮助的说明,运行<ndk>/samples目录下的hello-jni工程后,你一定想知道如何在自己的项目里调用别人已经编译好的.so库文件。于是新建一个Android工程,将hello-jni例子中ndk-build后的结果:libs文件夹(包括其下所有内容)拷贝到新建工程的根目录下,仿照hello-jni的代码,调用native方法:
   1: package com.my.SoTest;
   2:  
   3: import android.app.Activity;
   4: import android.os.Bundle;
   5: import android.widget.TextView;
   6:  
   7: public class SoTestActivity extends Activity {
   8:     /** Called when the activity is first created. */
   9:     @Override
  10:     public void onCreate(Bundle savedInstanceState) {
  11:         super.onCreate(savedInstanceState);
  12:         
  13:         TextView tv=new TextView(this);
  14:         tv.setText(stringFromJNI());
  15:         setContentView(tv);
  16:     }
  17:     public native String stringFromJNI();
  18:     static {
  19:         System.loadLibrary("hello-jni");
  20:     }
  21: }

  运行后,logcat记录以下错误:


No implementation found for native Lcom/my/SoTest/SoTestActivity;.stringFromJNI ()Ljava/lang/String;
...
java.lang.UnsatisfiedLinkError: stringFromJNI

  在hello-jni的代码注释里明确说明,UnsatisfiedLinkError错误是由于native代码中没有该方法的实现而引起的,但stringFromJNI这个方法,在c代码中是有的;前一个错误就比较明确了,是在Lcom/my/SoTest/SoTestActivity中去找这个方法实现的,打开hello-jni.c文件看看该函数头,其中有这样的字样:Java_com_example_hellojni_HelloJni_stringFromJNI,而com.example.hellojni恰好是例子工程的package name,HelloJni则是class的名称。也就是说,我们.so中函数声明涉及到的package name和class name与调用它的package name和class name不符。google一下印证了这个猜测,但提供现成解决办法的不多,总不能自己工程的package name必须和.so里的一样吧。
  搜索看到了一篇NDK的教程,Using NDK to Call C code from Android Apps(需翻墙),里面写明了在一个普通Android工程中,如何从零开始组装自己的native代码。大致的步骤是,首先写一个java类,里面声明一些需要实现native方法(函数签名即可),然后用javah命令来生成对应的header文件,里面的内容是自动生成的native函数声明,然后照着这个声明实现各个函数,最后编译,用最初的java类调用。
  受此启发,我们在拿到别人已经编译好的.so文件后,可首先新建一个java类,所在package的名称和class名称都与.so文件中函数签名提示的一致,在这个类中加入native方法的声明。这样在别处就可以用这个wrapper调用so库中的函数了。

2012年2月27日星期一

在Android上利用NDK编译并使用Spatialite库(Windows环境)

  Sqlite数据库因其体积便携(基于单个文件,跨平台),功能完整(类似RDBMS)受到非常广泛的欢迎,在iOS,Android平台上都提供原生的支持。Spatialite是Sqlite的一个空间扩展,根据官方介绍,Spatialite之于Sqlite,相当于PostGIS之于PostgreSQL。
  Spatialite扩展提供了一系列用于空间操作的SQL函数,例如基础的空间查询,缓冲区生成等,也有高级的类似ArcObjects中ITopologicalOperator,IRelationalOperator等接口的功能。
  在Android平台上使用Spatialite资料比较少,主要是google上的Spatialite for Android和github上的android-spatialite两个工程。前者带有完整的编译步骤,我尝试了这个,其中遇到了些问题,记录下来,方便需要用到的朋友。后者工程似乎带有编译好的libjsqlite.so库,可直接使用。
  我机器的环境是Windows 7 64bit,已经搭建好了Android Eclipse开发环境。由于Spatialite及Sqlite是C/C++的native语言编写,而Android的默认开发语言是Java,所以在Android上编译Spatialite必须用到google提供的NDK(Native Development Kit)环境。NDK环境的搭建比较简单,按照google的文档进行即可:

  1. 下载NDK压缩包(android-ndk-r7b-windows.zip)。我下载的是最新的android-ndk-r7b版本。将其解压到任意目录,下文以<ndk>指代。注意:由于后面cygwin中配置环境变量时不支持带空格的路径,所以建议将其解压到某个盘符的根目录下,比如d:\android-ndk-r7b\;
  2. 设置Windows的环境变量。主要是将NDK的路径添加到PATH中去,方便在任何位置调用ndk-build命令。详细操作不再赘述。此步骤只是为cmd提供了便利;
  3. 由于NDK需求中说明,必须用到GNU Make命令,所以在Windows环境上还需要提前安装Cygwin。这是在Windows上模拟Linux环境的工具。安装步骤参考此贴:Windows环境下Android NDK环境搭建。注意在cygwin的linux环境下,配置$NDK环境变量,方便在任何地方调用ndk-build编译命令。我在安装Cygwin时,只额外勾选了Devel库。

  完成这个步骤后,可尝试编译运行NDK中自带的hello-jni例子,验证NDK编译环境已经搭好。具体步骤参见google的文档。可以发现,native代码需要放在与src同级的jni目录下,该目录下必须至少有Android.mk文件,向NDK系统描述源文件;编译时需要在工程的根目录下运<ndk>/ndk-build命令;利用NDK编译后,Android工程文件夹中会多出libs目录,里面会生成lib<something>.so共享库,这就是我们要用到类库,该文件也会一并打包到apk文件中去。更多的ndk知识请参考<ndk>/documentation.html文件,非常详尽。注:JNI相当于.net平台的p/invoke。
  接下来就要按照Spatialite for Android上的提示,在cygwin环境中编译spatialite库了。

  1. 在windows下利用svn工具对整个project checkout(这次作者用的是Spatialite 3.0.1,PROJ 4.7.0,geos 3.2.2几个库)。会发现有两个工程spatialite-android和spatialite-android-test。我们主要对spatialite-android工程进行编译;
  2. 由于spatialite用到了geos和proj两个库,所以首先需要为该工程下载,配置两个库。具体步骤如下,在cygwin环境中执行:
       1: $ cd jni
       2: $ wget http://download.osgeo.org/geos/geos-3.2.2.tar.bz2
       3: $ tar xvjf geos-3.2.2.tar.bz2
       4: $ cd geos-3.2.2
       5: $ ./configure --build=x86_64-pc-linux-gnu --host=arm-linux-eabi
       6: $ cd ..
       7: $ wget ftp://ftp.remotesensing.org/proj/proj-4.7.0.tar.gz
       8: $ tar xvzf proj-4.7.0.tar.gz
       9: $ cd proj-4.7.0
      10: $ ./configure --build=x86_64-pc-linux-gnu --host=arm-linux-eabi
      11: $ touch src/empty.cpp
      12: $ cd ..

  3. 按照作者的提示,下来就可以直接对工程进行ndk-build了(注:此时仍然在spatialite-android/jni目录下,而非spatialite-android目录)。如果你这么做了,会出现一个错误:


       1: ...
       2: Compile thumb  : proj <= rtodms.c
       3: Compile thumb  : proj <= vector1.c
       4: StaticLibrary  : libproj.a
       5: Compile++ thumb  : geos <= geos_c.cpp
       6: In file included from D:/spatialite-android/jni/geos-3.2.2/source/headers/geos/geom/Coordinate.h:20,
       7:                  from D:/spatialite-android/jni/geos-3.2.2/source/headers/geos/g                                                                                                                eom/Envelope.h:26,
       8:                  from D:/spatialite-android/jni/geos-3.2.2/source/headers/geos/i                                                                                                                ndex/strtree/STRtree.h:27,
       9:                  from D:/spatialite-android/jni/geos-3.2.2/capi/geos_c.cpp:19:
      10: D:/spatialite-android/jni/geos-3.2.2/source/headers/geos/platform.h:29:20: error                                                                                                                : ieeefp.h: No such file or directory
      11: /cygdrive/d/android-ndk-r7b/build/core/build-binary.mk:243: recipe for target `/                                                                                                                cygdrive/d/spatialite-android/obj/local/armeabi/objs-debug/geos/geos-3.2.2/capi/                                                                                                                geos_c.o' failed
      12: make: *** [/cygdrive/d/spatialite-android/obj/local/armeabi/objs-debug/geos/geos                                                                                                                -3.2.2/capi/geos_c.o] Error 1
    image
    提示ieeefp.h: No such file or directory。要修正这个错误,你需要在spatialite-android/jni/geos-3.2.2/source/headers/geos/platform.h文件中大约15行的位置,找到“#define HAVE_IEEEFP_H 1”这句话,并在其下面添加一行“#undef HAVE_IEEEFP_H”,取消对HAVE_IEEEFP_H的定义。保存该文件;

  4. 再次ndk-build,这回发现上面的错误消失了。但又产生了新的错误:


       1: ...
       2: Compile++ thumb  : geos <= InteriorPointLine.cpp
       3: Compile++ thumb  : geos <= InteriorPointPoint.cpp
       4: Compile++ thumb  : geos <= LineIntersector.cpp
       5: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp: In st                                                                                                                atic member function 'static double geos::algorithm::LineIntersector::interpolat                                                                                                                eZ(const geos::geom::Coordinate&, const geos::geom::Coordinate&, const geos::geo                                                                                                                m::Coordinate&)':
       6: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp:224: e                                                                                                                rror: expected unqualified-id before '(' token
       7: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp:232: e                                                                                                                rror: expected unqualified-id before '(' token
       8: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp: In me                                                                                                                mber function 'void geos::algorithm::LineIntersector::computeIntersection(const                                                                                                                 geos::geom::Coordinate&, const geos::geom::Coordinate&, const geos::geom::Coordi                                                                                                                nate&)':
       9: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp:304: e                                                                                                                rror: expected unqualified-id before '(' token
      10: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp:306: e                                                                                                                rror: expected unqualified-id before '(' token
      11: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp: In me                                                                                                                mber function 'int geos::algorithm::LineIntersector::computeIntersect(const geos                                                                                                                ::geom::Coordinate&, const geos::geom::Coordinate&, const geos::geom::Coordinate                                                                                                                &, const geos::geom::Coordinate&)':
      12: D:/spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp:426: e                                                                                                                rror: expected unqualified-id before '(' token
    image在很多位置都提示 error: expected unqualified-id before '(' token,google一下这个错误,发现大概是由于在头文件中对某个函数重复声明冲突引起的。于是打开spatialite-android/jni/geos-3.2.2/source/algorithm/LineIntersector.cpp文件查看,发现基本都与一个名为“ISNAN”的函数有关。在Windows下搜索包含此字符串的文件,又来到了刚才修改过的platform.h文件中,发现这个文件中有与此函数相关的语句。为了解决此问题,尝试对此文件进行再次修改。在大约25行的位置,可以找到“/* #undef HAVE_ISNAN */”这句话,在这句话下面添加一句“#define HAVE_ISNAN 1”。这样做的目的是改变该文件后续的一些编译处理。

  5. 最后再次ndk-build,成功输出libjsqlite.so库。
    image

  现在我们在eclipse中运行spatialite-android这个工程(需要将test-2.3.sqlite数据库adb push到模拟器的sd卡上),就可以看到预期的结果了。
image  关于如何利用java语言调用spatialite的功能,可以详细参考这个工程中jsqlite包下的封装代码。至此我们就完成了在Android平台上使用spatialite库的准备工作。
  此次编译中遇到的问题可参考:http://code.google.com/p/spatialite-android/issues/detail?id=3

2012年2月25日星期六

心力

  心力,可以理解为和通常所讲的“体力”相对而言。是指一个人心理的力量,精神上的能量,或者意念。当然你不可能用意念杀死别人,但你可以用心力拯救你自己。
  从前有一片茫茫大海,比地球上所有大陆都变成海洋后的海洋还要大。这片海面上漂浮着唯一一个木环,直径只有杯口大小;海里面有一只乌龟,双目失明,它一直在水中游动,一万年才浮到水面上呼吸一次。
  我们每个人的生命是有限的,但内心是永恒的。今生今世只不过是内心灵魂的一段短暂旅程,现在的身体只是它寄存的一种形式。上辈子你有可能是只猪,是只考拉,甚至阿凡达,下辈子你有可能是格桑花,是面包树。这些表现形式都不真正属于你,生不带来死不带去,只有内心才是真正的你。但这辈子你能获得这个人身,几率和上面的盲龟浮出海面时,头恰好能钻进木环的几率是一样的。之所以能这么幸运,是前几百世修来的福气。
  每做一件好事或者一件坏事,都是你自己的“业”,好的业力多了,就会有好报。不管你在哪一世,万般带不走,唯有业随身。我们活着,除了要积攒业力,最重要的就是锻炼心力,使自己变得正直,善良,坚韧不拔,温柔如水。
  路上碰到瘸腿的乞丐了,不要鄙视,给些零钱,他们或许就能多吃一碗救命的米饭;拥挤的地铁上碰到不讲卫生的农民工了,不要移动脚步躲开,他们和你一样有父母有儿女,他们盖起的高楼能让你们安居乐业,而你们做出的价值千万的系统也许没有一点实用价值,只是在浪费纳税人的血汗钱,为腐败提供了温床。
  锻炼心力是需要决心和勇气的,但可以从一点一滴做起,比如早晨不赖床,或者开始学一些对人生有意义的东西。最好的办法就是挫折和困难。菩萨和天使不总是带给你好处,还可能是挫折和困难,越是逆境,就越要珍惜。因为在你得到这个磨练机会的时候,别人可能还在把时间挥霍在享乐上。享乐的时间和心力的积累通常是成反比的。
  有一些人等意识到,可能也就心有余,力不足了。与各位共勉。