移動跨平台框架開發之二: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搜索都能搞定。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM