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搜索都能搞定。