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。
- 准备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
- 编译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:
- cd ~/pkg-config-0.22
- ./configure --with-internal-glib
- make
- 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/* .
- 编译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的不同之处在于以上两个红字选项。这个是在参考多个文档后的总结。
- 编译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
- 集成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搜索都能搞定。