使用CMake構建Android JNI工程

weixin_34148340發表於2019-02-08

Android Studio(2.2+)構建native庫可以使用原生構建工具包ndk-build,也可以使用外部構建工具CMake,搭配Gradle外掛可以方便的構建原生庫,進行Android JNI的開發。使用Android Studio建立的native工程預設使用的是CMake構建工具,本文從零開始介紹使用CMake搭建一個JNI工程。

為了闡述方便,我們以建立一個預設的Android工程為例,不使用建立嚮導裡的Include C++ Support或者建立C++工程。現在我們在native程式碼(C++)中實現一個獲取字串並返回的操作,然後使用java jni來呼叫。

一、下載NDK和構建工具

開啟Android Studio -> Perferences -> Appearance&Behavoir -> System Setting -> Android SDK,或者直接在左側搜尋Android SDK,選擇SDK Tools,下載NDK、CMake、LLDB這三個工具包。


1762735-232ef3adf80e11a3.png

新建的native工程(Include C++ Support)在local.properties中都會配置ndk的預設的路徑:

ndk.dir=/Users/derek/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/derek/Library/Android/sdk

如果沒有,或者ndk在別的目錄下,需要手動新增或修改路徑。

二、在java類中宣告native方法

在java中宣告要使用的native方法,這些方法以native字首,只需宣告,無需實現。這些方法可以宣告為static或非static方法,可以是任何訪問許可權。

package com.tsia.example.jnitest;
...
public class MainActivity extends Activity {
  ...
  public native String stringFromJNI(String str);
}

三、新增C/C++程式碼

在c++程式碼中需新增和native方法對應的函式,注意如下幾點:

  1. 檔名稱可隨意指定,可以只有原始檔
  2. 標頭檔案或原始檔中要#include <jni.h>
  3. 方法宣告要和java中的native方法對應:
  • 方法名稱。Java_包名_類名_方法名,包名也使用_分隔。
  • 引數。
    • 第一個引數為JNIEnv *
    • 如果為static方法,第二個引數為jclass;如果非static方法,第二個引數為jobject
    • 從第三個引數開始,和java的native方法的引數型別和順序要一一對應
  • 返回值。
    • 返回值型別要對應java中的型別
    • 需要JNIEXPORT、JNICALL字首

方法格式可以概括為:JNIEXPORT <返回型別> JNICALL Java_<包名><類名><方法名>(JNIEnv *, jobject,<引數>); 包名中的“.”用下劃線“_”代替。

#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring JNICALL Java_com_tsia_example_jnitest_MainActivity_stringFromJNI
  (JNIEnv *env, jobject jobj, jstring str) {

  const char* c_name = env->GetStringUTFChars(str, NULL);

  std::string hello = "Hello, ";
  std::string name(c_name);

  return env->NewStringUTF((hello+name).c_str());
  }

如果擔心函式宣告寫錯,可以使用命令列生成native方法對應的C++函式標頭檔案,只需要到java檔案所在的包名目錄下執行javah命令,比如在com所在目錄下執行:

tsias-MacBook-Pro:java tsia$ javah -jni com.tsia.example.jnitest.MainActivity

執行後會在該目錄下生成一個標頭檔案:com_tsia_example_jnitest_MainActivity.h,自動生成的格式為包名+類名.h,中間會使用_分隔。(這個檔名為命令自動生成的格式,可以隨意修改)

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_tsia_example_jnitest_MainActivity */

#ifndef _Included_com_tsia_example_jnitest_MainActivity
#define _Included_com_tsia_example_jnitest_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_tsia_example_jnitest_MainActivity
 * Method:    stringFromJNI
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_tsia_example_jnitest_MainActivity_stringFromJNI
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

可以在.h檔案中看到所有native方法的宣告,格式就是我們上文講的一樣。手動編寫時也可參考自動生成標頭檔案的格式,避免出錯。

在呼叫native方法的時候會匹配函式名和引數,需要按照格式書寫,不可隨意修改。

四、編寫CMakeLists.txt檔案

接下來就是建立CMake的構建指令碼,它是一個純文字檔案,必須命名為CMakeLists.txt。構建指令碼用來告訴CMake將如何建立一個so庫,例子中我們要將c++程式碼編譯成一個名為native-lib的庫,給jni呼叫。

cmake_minimum_required(VERSION 3.4.1)
add_library(
       # 設定so檔名稱.
       native-lib

       # 設定這個so檔案為動態庫(SHARED)。靜態庫使用STATIC
       SHARED

       # c/c++原始檔的相對路徑(相對於CMakeLists.txt)
       src/main/java/jnitest.cpp)

當工程編譯的時候,Gradle會自動將動態庫native-lib庫打包到APK中。指令碼中指定的庫名稱為native-lib,但實際CMake生成的名稱為libnative-lib.so。
CMake 使用以下規範來為庫檔案命名:

lib庫名稱.so

五、配置Gradle關聯

在構建應用時,Gradle 會以依賴項的形式執行CMake,並將共享的庫打包到的 APK中,因此我們需要提供一個指向 CMake指令碼檔案的路徑。

手動配置

externalNativeBuild {} 塊新增到模組級 build.gradle 檔案中,並使用 cmake {} 對其進行配置

android {
  ...
  defaultConfig {...}
  buildTypes {...}

  externalNativeBuild {
    cmake {
      path "CMakeLists.txt"
    }
  }
}

這裡的path為相對於build.gradle檔案的路徑,需正確配置。

使用Android Studio配置關聯

從IDE左側開啟 Project 窗格並選擇 Android檢視,右鍵點選您想要關聯到原生庫的模組(例如 app 模組),並從選單中選擇 Link C++ Project with Gradle。

1762735-fb01567a88bea28c.png

選擇使用CMake構建,並制定CMakeLists的路徑。
1762735-31ded20437f58c75.png

完成後可以看到模組級build.gradle 檔案中會增加externalNativeBuild {} 塊配置,和我們手動配置的結果是一樣的。因為gradle配置有修改,sync project下。

此時執行build -> Make Module 'app',可以看到所有架構的so都會打到APK的lib目錄下。


1762735-21bdd35612cf7723.png

五、在java檔案中載入so庫

在Java程式碼中載入so庫時,請使用您在 CMake 構建指令碼中指定的名稱。如CMake生成的而檔案是libnative-lib.so,只要指定載入native-lib即可。

package com.tsia.example.jnitest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        String ret = stringFromJNI("world");
        Log.i("jnitest", ret+"");
    }
    
    public native String stringFromJNI(String str);
}

然後在程式碼中呼叫native方法列印結果。執行後列印結果: Hello, world

上述就是一個簡單的jni工程搭建步驟,如下是在構建和執行時的基本過程:

  1. Gradle呼叫外部構建指令碼CMakeLists.txt
  2. CMake按照構建指令碼中的命令將 C++原始檔jnitest.cpp 編譯到共享的物件庫中,並命名為libnative-lib.so,Gradle隨後在編譯的時候會將其打包到APK中。
  3. 執行時,應用的 MainActivity 會使用 System.loadLibrary() 載入原生庫。這時候應用可以使用庫的原生函式 stringFromJNI(String str)
  4. MainActivity.onCreate() 呼叫 stringFromJNI("world"),這將返回“Hello, world”並列印。

參考:https://developer.android.com/studio/projects/add-native-code

相關文章