移动跨平台框架开发之二:android重用c++库

android平台下重用c++库的原理比较古老,就是java与c++的jni。它的难度比ios下要大不少。Obj-c与c++可以混合编码,无缝集成,而java与c++不能混合,对象间不能互相引用。此难点一。

 

另一个难点与ios下相似,就是对第三方库的编译。虽然有ios的经验,但似乎并没有可供android借鉴之处。这里需要说明的是,我准备作的是在代码中以c++的方式调用这些第三方库,因此它们不需要提供java的接口,也就是说不需要这些库的java binding。

 

以下除了cryptopp是在ubuntu 12.04上编译的以外,其余的编译环境均为macos 10.7.4。

 

  1. 准备ndk环境

参考https://developer.android.com/tools/sdk/ndk/index.html

system参数指定你的编译平台。

platform参数指定你想支持的最低版本,跟你在AndroidManifest.xml中的 android:minSdkVersion值一致。

1.1.     macos 10.7.4

cd /Users/chenfeng/program/ android-ndk-r8e

sudo . /build/tools/make-standalone-toolchain.sh --system=darwin-x86_64 --platform=android-8 --install-dir=/opt/android-toolchain

1.2.     ubuntu 12.04

cd /home/chenfeng/program/android-ndk-r8e

sudo ./build/tools/make-standalone-toolchain.sh --system=linux-x86_64 --platform=android-8 --install-dir=/opt/android-toolchain

 

 

  1. 编译zmq

2.1.     编译zmq c++库

参考http://www.zeromq.org/build:android

export OUTPUT_DIR=/Users/chenfeng/lib/android/zeromq-android

 

2.1.1.    常见问题:config.sub和config.guess版本太旧

问题:

连续执行这两步

./autogen.sh

./configure --enable-static --disable-shared --host=arm-linux-androideabi --prefix=$OUTPUT_DIR --with-uuid=$OUTPUT_DIR LDFLAGS="-L$OUTPUT_DIR/lib" CPPFLAGS="-fPIC -I$OUTPUT_DIR/include" LIBS="-lgcc"

执行后面一步时,提示

checking host system type... Invalid configuration `arm-linux-androideabi': system `androideabi' not recognized

configure: error: /bin/sh config/config.sub arm-linux-androideabi failed

 

原因分析:

是config.sub 和 config.guess这两个文件太旧。这是因为你画蛇添足地执行了./autogen.sh,导致config下的这两个文件被系统自带的覆盖。

 

解决方案:

以下两个都是可行的。

l   执行./configure…之前不执行./autogen.sh。

l   下载最新的config.guess和config.sub,覆盖系统自带的。

n   到http://git.savannah.gnu.org/gitweb/?p=config.git;a=tree下载config.guess和config.sub两个文件

n   将此两个文件拷贝到/usr/local/share/automake-1.11   //automake的安装目录

n   然后执行前面两步

 

2.2.     编译jzmq库

由于我们并不会调用zmq的java接口。因此这一步并非必需。供

2.2.1.    常见问题:未安装pkg-config

问题:

在执行./autogen.sh时找不到pkg-config

解决方案:

Get pkg-config from http://pkgconfig.freedesktop.org/releases/pkg-config-0.28.tar.gz
Unzip this in home directory and pkg-config-0.22 will be created.
Run the following commands:

  1. cd ~/pkg-config-0.22 
  2. ./configure --with-internal-glib
  3. make
  4. sudo make install

2.2.2.    常见问题:找不到java include files

问题:

在执行./configure --host=arm-linux-androideabi --prefix=$OUTPUT_DIR --with-zeromq=$OUTPUT_DIR CPPFLAGS="-fPIC -I$OUTPUT_DIR/include" LDFLAGS="-L$OUTPUT_DIR/lib" --disable-version LIBS="-luuid"

提示

configure: error: cannot find java include files

 

解决方案:

export JAVA_HOME=`/usr/libexec/java_home -v 1.7`

export JAVAC=$JAVA_HOME/bin/javac

 

2.2.3.    常见问题:找不到jni_md.h

问题:

make时提示

/Library/Java/JavaVirtualMachines/1.7.0.jdk/Contents/Home/include/jni.h:45:20: fatal error: jni_md.h: No such file or directory

 

解决方案

cd /Library/Java/JavaVirtualMachines/1.7.0.jdk/Contents/Home/include

cp darwin/* .

 

  1. 编译protobuf

依照zmq,依序执行:

export PATH=/opt/android-toolchain/bin:$PATH

export OUTPUT_DIR=/Users/chenfeng/lib/android/protobuf-android     //存放.h 和lib.a的目录

./configure --enable-static --disable-shared --host=arm-linux-androideabi --prefix=$OUTPUT_DIR LDFLAGS="-L$OUTPUT_DIR/lib" CPPFLAGS="-fPIC -I$OUTPUT_DIR/include" --enable-cross-compile --with-protoc=protoc LIBS="-lgcc"

make

make install

与zmq的不同之处在于以上两个红字选项。这个是在参考多个文档后的总结。

 

  1. 编译cryptopp

在macox上尝试失败,转而在ubuntu 12.04上编译。

cryptopp与上述两个库的不同之处在于源代码工程没有用autotool这一套东西,因此无法通过为configure指定选项来生成交叉编译的makefile。因此,有两种方法可供选择。一种是修改makefile,另一种是在android工程中通过写jni的android.mk来编译。显然前者更为方便。

参考http://morgwai.pl/ndkTutorial/

 

对GNUmakefile作以下修改

l   switch the target architecture (-march option) from native to armv5te

l   remove linker option to use glibc pthreads (LDFLAGS += -pthread option)

l   添加LDLIBS += -lgnustl_shared

 

依序执行。

export PATH=/opt/android-toolchain/bin:$PATH

export CXX=/opt/android-toolchain/bin/arm-linux-androideabi-g++

export PREFIX=/home/chenfeng/lib/android/cryptopp-android

make

make install

 

  1. 集成c++源代码和lib的android工程

这是本篇最为困难的部分。前面说过,android重用c++库比ios复杂得多。因为obj-c与c++可以混合,而java与c++之间是隔离的,因此无法在java代码中直接生成c++对象。

如果你对这一领域一片空白,建议你首先作两件事:

5.1.     ovewview文档

参考https://developer.android.com/tools/sdk/ndk/index.html的Exploring the hello-jni Sample这一章节

参考下载的android-ndk-r8e/docs/OVERVIEW.html的III. NDK development in practice: 这一章节

5.2.     典型sample

建议参考下载的android-ndk-r8e/samples/two-libs这个例子。原因是它既生成了一个lib.a库,相当于我们这里的zmq/protobuf/cryptopp这些第三方库,又生成了一个lib.so库,相当于我们要重用的自身的库。

 

有了以上基础,就可以动手开始编码了。与ios类似,这里要解决两个问题:java调用c++函数,c++回调java函数。如果像在大多数示例中展示的,由java对象调用c++函数,在该c++函数中直接回调该java对象的方法,那就太简单了。我们两个方向的调用是在不同的上下文中,由独立的事件触发。

 

5.3.     java调用c++

主要涉及两方面的工作。

5.3.1.    一个独立的wrapper(或称adapter)c++文件

以下是我的MsgAdapter.cpp片段。

static MsgSender *msgSender;

JavaVM *g_jvm;

jobject listener = 0;

 

extern "C" {

       JNIEXPORT void JNICALL Java_com_roadclouding_aholdem_ZMQService_makeMsgSender(JNIEnv* env, jobject thiz);

       …

              JNIEXPORT void JNICALL Java_com_roadclouding_aholdem_ZMQService_00024ZMQThread_setListener(JNIEnv* env, jobject thiz, jobject jlistener);

};

 

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved)

{

       g_jvm = jvm;  // cache the JavaVM pointer

       return JNI_VERSION_1_6;

}

 

JNIEXPORT void JNICALL Java_com_roadclouding_aholdem_ZMQService_makeMsgSender(JNIEnv* env, jobject thiz)

{

    g_socketLocalSvr.bind("inproc://lifecycle");

    msgSender = new MsgSender(g_socketLocalSvr);

}

 

JNIEXPORT void JNICALL Java_com_roadclouding_aholdem_ZMQService_00024ZMQThread_setListener(JNIEnv* env, jobject thiz, jobject jlistener)

{

       AndroidGameController *controller =  new AndroidGameController();

       msgDispatcher->setController(controller);

       listener = env->NewGlobalRef(jlistener);

       controller->_listener = listener;

}

 

注意几点:

l   你可以在一个函数中生成全局c++对象,供以后在另一个函数中调用。如上面的msgSender。

l   extern "C"不可以省略。

l   JNIEXPORT void JNICALL不可以省略。

l   Java的嵌套类的表示法为outerClass_00024innerClass。

l   必须保存JavaVM *jvm供后续回调中使用。下节进一步解释。

l   如果要保存java对象供后续引用,必须用NewGlobalRef把local reference转为global reference。

5.3.2.    在java类中声明native c++函数

这就比较简单,在声明前加native;在需要的地方直接调用,就像调用java函数一样。

以下是我的代码片段。

public class ZMQService extends Service {

       …

    private native void makeMsgSender();

    private native void sendMsg(String msg);

    private native void reconnect();

private native void checkin();

public class ConnectivityChangeReceiver extends BroadcastReceiver {

      @Override

      public void onReceive(Context context, Intent intent) {

             if (isConnectedToInternet()) {

                    Log.d(TAG, "reconnect");

                    reconnect();

                    checkin();

             }

      }

}

}

 

5.4.     c++回调java

也涉及两方面的工作。

5.4.1.    取得java对象的方法入口

c++回调java的复杂性已经部分体现在上一节中。g_jvm和用 NewGlobalRef 得到的listener就是为取得java对象的方法入口进而回调准备的。

以下是我的代码片段,它由收到特定消息触发。

extern JavaVM *g_jvm;

void AndroidGameController::onCheckin()

{

    JNIEnv * g_env;

    int getEnvStat = g_jvm->GetEnv((void **)&g_env, JNI_VERSION_1_6);

       …

    jclass cls = g_env->GetObjectClass(_listener);

    assert (cls != 0);

    jmethodID mid = g_env->GetMethodID(cls, "onCheckin", "()V");

    assert (mid != 0);

    g_env->CallVoidMethod(_listener, mid);

       …

}

 

注意,由于取得对象和方法的入口必须用到JNIEnv,这就是上一步要保存JavaVM的原因,由它通过GetEnv来取得。

 

5.4.2.    在java中实现回调函数

这个非常简单。

public class ViewMsgListener implements MsgListener {

       @Override

       public void onCheckin() {

              // TODO Auto-generated method stub

              Log.d(TAG, "ViewMsgListener onCheckin");

       }

 

}

 

5.5.     编译调试常见问题

编译也分为两步。

l   ndk-build把c++文件编译出lib.so

l   在eclipse环境下与编译纯java一样编译整个工程。

中间碰到了不少问题。

5.5.1.    不认识string

问题:

fatal error: string: No such file or directory

 

解决方案:

这是没有加入stl库导致的。

Create a "Application.mk" file and write "APP_STL := gnustl_static " in it.

用APP_STL:= stlport_static可以解决这个问题,但产生下面这个问题。

 

5.5.2.    stl库不兼容

问题:

/Users/chenfeng/program/android-ndk-r8e/sources/cxx-stl/stlport/stlport/stl/_cstdlib.h:131:13: error: conflicting types for 'abs'

 

解决方案:

Application.mk中用APP_STL:= gnustl_static取代APP_STL := stlport_static

 

5.5.3.    不认识'namespace'

问题:

/Users/chenfeng/program/android-ndk-r8e/sources/cxx-stl/gnu-libstdc++/4.6/include/bits/stringfwd.h:43:1: error: unknown type name 'namespace'

 

解决方案:

这是因为它被当成c文件。

把.c重命名为.cpp就可以了。

按下葫芦起了瓢,出现以下问题。

 

5.5.4.    函数原型不一致

 

问题:

error: base operand of '->' has non-pointer type 'JNIEnv {aka _JNIEnv}'

 

解决方案:

这是因为'JNIEnv在c和c++下的宏定义不同。

把适用于c的语法:const char *str = (*env)->GetStringUTFChars(env, prompt, 0);

改为适用于c++的语法:const char *str = env->GetStringUTFChars(msg, 0);

 

5.5.5.    未链接stl库

问题:

stl_tree.h:1013: error: undefined reference to 'std::_Rb_tree_insert_and_rebalance(bool, std::_Rb_tree_node_base*, std::_Rb_tree_node_base*, std::_Rb_tree_node_base&)'

 

解决方案:

拷贝android-ndk-r8e/sources/cxx-stl/gnu-libstdc++/4.6/libs/armeabi-v7a下的 libgnustl_static.a到工程里,并在android.mk中指定

LOCAL_LDFLAGS += -L$(LOCAL_PATH)/network         //你拷贝目的地的工程的子目录

LOCAL_LDLIBS := … -lgnustl_static

 

还碰到其它问题,google搜索都能搞定。

posted on 2013-07-23 22:10  深圳市路云信息科技有限公司  阅读(3217)  评论(0编辑  收藏  举报