NO IMAGE

  在安卓平臺上開發應用,通用的語言是 Java ,而對於從其它平臺遷移到安卓的專案、產品,或者對於慣用 C/C 程式設計的開發人員來講,會希望複用已有的 C/C 程式碼。安卓平臺提供了複用 Native 程式碼的途徑,也提供了編譯 C 程式碼的環境和工具鏈: NDK 。 NDK 是一套工具鏈,有了它,在安卓上使用 C 語言成為可能。其實安卓原本是在 Linux 上套了個 Java 環境,要說不能用C 那才是不可思議的事兒,只是 Google 沒完全開放而已(話說我到現在都在腹黑,為麼不能讓 C 程式設計師在安卓上活得自在些呢,簡直是人為製造障礙)。

  安卓平臺上服用 C 程式碼有兩種方式:

  1. JNI
  2. 原生 C 可執行程式

    JNI 方式

    JNI 原本是Java 提供的一種複用 C 程式碼的框架,安卓又對此進行了一些擴充,加了個 AIDL 用在服務框架中,搞了一套工具,在使用 Android.mk 編譯時可以根據 AIDL 檔案自動生成對應的 Java 程式碼並編譯。

    使用 JNI 主要是把 C 程式碼編譯成動態庫,在 Java 中呼叫。使用的步驟大概是這樣的:

  1. 在 Java 程式碼中宣告 native 方法
  2. 在 JNI(橋接 C 程式碼的這部分 C 程式碼稱之為 JNI 層)層按照命名規則實現與 Java 層對應的本地方法
  3. 在 Java 層載入 C 動態庫

    JNI 方式的例子,如 Qt on Android ,Vitamio ,還有安卓框架本身中的一些例子,如 ServiceManager , android.util.Log 。

    原生 C 可執行程式

    安卓本是 Linux ,呼叫 Native 可執行程式是自然而然的途徑。 Java 也提供了語言層面的支援,Runtime.exec() 函式就是幹這個的。通過 exec() 啟動程序,可以讀取 Native 程序的標準輸出,可以向 Native 程序的標準輸入寫入資料。

    呼叫原生可執行程式的方式的例子, WifiManager 在連線無線網路時會通過控制介面和 wpa_supplicant (用於無線連線的 wpa_suppliacant ,是原生C可執行程式)通訊傳遞諸如掃瞄接入點、選擇網路、連線網路等指令。

    還有,有些安卓系統機頂盒上自帶的寬頻撥號(PPPoE)程式,也是 C 可執行程式, Java 層的 PPPoEService 最終通過呼叫pppd/ppppoe 這樣一些程式來執行實際的撥號過程。

    還有,我們常說的 root ,其實也是通過呼叫一個叫 su 的程式來實現的。

    我們在實際開發中也可以這麼用,比如想看某個目錄下都有什麼檔案,可以直接呼叫 ls 命令,讀取它的標準輸出。

    Java 和 C 程式通訊問題

    我們在安卓上通過 Java 呼叫 C 程式碼時還會遇到程序或執行緒間通訊的問題。

    這裡專門說下 Java 程序和 C 程序、Java 程序和 C 共享庫的通訊問題。

    有時我們的需求很簡單,阻塞式呼叫 C 程式碼,拿到計算結果就達到目的了。比如你通過 JNI 呼叫 C 共享庫的一個 Hash 函式,又比如你通過 Runtime.exec() 呼叫一個 Native 可執行程式來計算 Hash 讀取其標準輸出獲得結果。這些場景足夠簡單,你可以不考慮 Java 和 C 共享庫或者 C Native 可執行程式間的通訊,反正是一錘子買賣也沒啥狀態要維護的。

    但是還有一些複雜的應用,我們必須在 Java 和 C 之間建立一種長期的通訊機制。

    還是舉無線連線的例子好了,有興趣的讀者可以瀏覽 wpa_supplicant 的原始碼,它提供了基於 dbus、unix domain socket 兩種方式的控制介面。

    其實 Linux 常用的程序間通訊機制,如 管道pipe 、socket 、訊號,也都可以用於 Java 層和 C 層的通訊,而且安卓框架就這麼用了。舉個例子,我們都很熟悉的安卓世界的第一個程序 Zygote(實際是 app_process ,啟動後更名為 Zygote ),有一個功能就是啟動 Java 程序,當我們要啟動一個 APK ,該 APK 的程序幾經輾轉最終是由 Zygote 啟動的(可以程序間共享 Java 類庫、C 共享庫,大大節省資源也加快程序啟動過程),而 Zygote 正是通過 socket 接受命令的。

    再舉個訊號的例子,我們看到很多程序管理類的應用,其實都是使用 android.os.Process 來殺程序,而 Process.kill() ,實際上就是給目標程序傳送了一個訊號,非常標準的 Linux 機制。

    還有一個常用的 Java 和 C Native 可執行程式程序間通訊的機制:標準輸入輸出。我們可以向一個程序的標準輸入寫入資料,也可以從它的標準輸出讀取資料。那麼我們就可以定義一套控制協議用來通訊。

    像管道 pipe、socket 既可以用於程序間通訊(Java 和 C Native 可執行程式),也可以用於程序內通訊( Java 和 C 共享庫)。那麼什麼場景下我們會這麼用呢?前面的無線連線服務程式 wpa_supplicant 可以給我們一些提示。

    假如我們用 C 實現了一個服務,而 Java 層需要經常訪問這個服務並且需要得到反饋,那麼就可以這麼幹。

    試著說一個場景,我們用 C 實現了一個可以支援並行下載的模組(單執行緒 select 模型),而 Java 層會頻繁地下載圖片(安卓上 Java 層貌似沒有簡單易用佔用資源又少的http 下載庫),比如一個雲相簿類的應用或視訊類應用,我們就可以把下載動作委託給 C 程序。

    程序間通訊,安卓框架中還有一個在 Linux IPC 框架之上擴充套件出來的新框架 Binder ,很多 Native 系統服務使用 Binder 框架,比如 AudioFlinger ,我們在 Java 層呼叫 AudioManager 設定音量的功能時,最終就是通過 Binder 框架使用了 Native 系統服務 AudioFlinger 的功能,是典型的跨程序呼叫,但是作為 Java 程式,完全不用關心這個。

    想詳細瞭解 Binder 的讀者,可以進一步學習。這裡提一下 Binder 的限制:不是所有 C 程式都可以使用 Binder 來註冊服務,只有被授權的服務如 media.player ,media.camera 之類的 Native 系統服務才可以,如果你是系統開發,可以通過修改授權列表(通過 UID 控制)來允許你的 C 程式註冊服務,而作為應用開發者,就別妄想了,還是考慮使用管道pipe、socket 或者標準輸入輸出穩妥些。

    編譯 C 可執行程式

    為安卓平臺編譯 C 可執行程式,有幾個方法:

    在 APK 中整合和使用 C 可執行程式

    怎樣在 APK 中整合和使用一個 C 可執行程式呢?遵循下列步驟即可:

  1.     可以把可執行程式放在 assets 目錄下,這樣打包成 APK 時會自動打包
  2.     APK 執行時訪問 assets 資料夾內的資源,釋放可執行程式,新增可執行許可權
  3.     使用 Runtime.exec() 啟動可執行程式

    好啦,總算說了個大概,希望對你有所幫助。