1. 程式人生 > >這是一篇讓你少走彎路的 JNI/NDK 例項教程

這是一篇讓你少走彎路的 JNI/NDK 例項教程

關於 JNI 的基礎就不多說了,這篇文章主要講解如何在 AS 中用 ndk-build 和 用 cmake 去構建我們的 JNI 工程,並總結他們的特點以及優缺點。

通過這篇文章,你講學習到:

  • 用 AS 構建自己的 JNI 工程
  • 學會使用 mk 去載入自己的 so 檔案
  • 學會呼叫第三方 so 或 .a 的方法 (工程提供測試的 so )
  • 學會使用 camke,體驗絲般順滑的 C/C++ 編寫體驗

1、ndk-build

先用傳統的方式,即 ndk-build 的方式 
首先,新建一個工程,配置 ndk 的環境: 
這裡寫圖片描述

然後,新建一個工程,在 gradle.properties 中,新增如下: 
android.useDeprecatedNdk=true 
這裡寫圖片描述

接著,先使用 AS 自帶的功能,在 module 中的 build.gradle 新增 so 庫的名字:

這裡寫圖片描述

新建一個類,用來生成 native 方法:

public class JniUtils {

    static {
        System.loadLibrary("JNIDemo");
    }
    public static native String getName();
}

接著,就是生成 class 檔案了,先 build module 一下 
(如果嫌麻煩,可以跳到快捷設定,不用寫這麼麻煩,不過我建議你還是操作一遍)

開啟 cmd,或者用 as 的 Terminal ,這裡用cmd演示,去到你的工程路徑下,生成我們需要的 .h 檔案 : 
這裡寫圖片描述

首先,我們需要設定 src 的根路徑 ,如果不先設定根路徑,一般會提示找不到類,用 set classpath 的命令,指向你的 java 檔案:

這裡寫圖片描述

然後,再使用 javah 去生成 .h 檔案,即上面的 JniUtils:

這裡寫圖片描述

就可以看到生成了 .h 檔案,如下圖:

這裡寫圖片描述

接著,我們新建一個 jni 的資料夾: 
這裡寫圖片描述

把 .h 檔案複製過去,然後複製多一份 .h 檔案,字尾名改為.cpp ,如下:

#ifdef __cplusplus
#endif
#include <jni.h>
extern "C"
JNIEXPORT jstring JNICALL Java_com_zhengsr_jnidemo_JniUtils_getName
        (JNIEnv *env, jobject obj) {
    return
env->NewStringUTF("這是個 jni 測試"); }

make module 一下,會發現,已經生成了 so 庫:

這裡寫圖片描述

最後再 MainActivity 中呼叫即可看到效果。

1.1、配置快捷方式

如果每次都這樣,想想都覺得崩潰,這個時候,我們就可以配置快捷方式,這樣就不用每次都開終端去輸入,怎麼配置呢?

去到 Setting 選擇 external tools ,新建一個 ,命名為 javah,(忽略我配置的 ndk_build,後面會用到): 
這裡寫圖片描述

配置以下引數:

這裡寫圖片描述

  • program 為要執行的命令
  • parameters ,先設定路徑,然後就是把命令敲一遍,注意是 /src/main/jni ,如果你的路徑不一樣,記得修改
  • working directory 是 .h 的生成路徑

然後在你的 jni 類中,按住右鍵:

這裡寫圖片描述

之後會彈出一個彈窗,可以自己輸入 .h 的名字 (ps:先把以前的去掉):

這裡寫圖片描述

效果如下:

這裡寫圖片描述

接下來的步驟,就跟上面的差不多了,這裡就不贅述了。

1.2、編寫自己的 mk

上面已經說過,我們並沒有 mk 的檔案,這是因為 as 用了自身的mk,如果我們需要引入第三方的so或者.a,或者需要特殊配置時,就需要編寫自己的 mk 檔案了。 
關於 mk 的學習,可以參考這篇文章 (寫得還不錯),這裡就不多說了: 
http://blog.csdn.net/mynameishuangshuai/article/details/52577228

回到 build.gradle ,先把上面的 ndk 的屬性去掉,然後新增:

這裡寫圖片描述

在 jni 路徑,新增 Android.mk 和 application.mk :

這裡寫圖片描述

首先,先編寫 Android.mk :

#設定路徑
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := jniutils
LOCAL_SRC_FILES := jniutils.cpp

include $(BUILD_SHARED_LIBRARY)

可以看到,我們把 jni 的 so 的名字改成了 jniutils,用於區別,記得改 JniUtils 中 loadLibrary 的名字,不然報錯了,別怪我沒提醒;

Application.mk 則如下:

APP_ABI:=all

指定生成所有平臺下的 so。

由於我們使用了 mk 編譯了,as 並不知道,我們要像剛才配置 javah 那樣,配置一下 ndk-build ,配置資訊如下:

這裡寫圖片描述

引數已經解釋過了,然後在 jni 的資料夾上右鍵,編譯一下:

這裡寫圖片描述

可以看到,生成的 so 包如下:

這裡寫圖片描述

這樣,我們就完成了我們的編譯了,run 一下,就可以看到你想要的結果了。

1.3、在 build.gradle 中配置編譯

從上面中,我們可以看到,如果改動了 .cpp 的方法,每次都要 ndk-build 一下,其實是很煩的; 
所以我們可以在 build.gradle 中,新增任務,在每次 run 的時候,自動編譯。

build 應該這樣配置:

這裡寫圖片描述

完整 build.gradle 檔案如下:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.2"
    defaultConfig {
        applicationId "com.zhengsr.jnidemo"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    sourceSets {
        main{
            jni.srcDirs=[]; //禁用as自動生成mk
            jniLibs.srcDirs 'src/main/jniLibs' //這裡設定 so 生成的位置
        }
    }
    //設定編譯任務,編譯ndkBuild
    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn 'ndkBuild'
    }
}
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
    //應該都看得明白,就不解釋了
    commandLine "C:\\Users\\Administrator\\AppData\\Local\\Android\\Sdk\\ndk-bundle\\ndk-build.cmd",
            'NDK_PROJECT_PATH=build/intermediates/ndk',
            'NDK_LIBS_OUT=src/main/jniLibs',
            'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
            'NDK_APPLICATION_MK=src/main/jni/Application.mk'
}

....

接下來,我們在 jniutils.cpp 中,把返回的字串改一下:

這裡寫圖片描述

直接run,可以看到效果: 
這裡寫圖片描述

1.4、引入第三方 so,.a 包

很多時候,像一些比較涉及加密或者核心程式碼,都是用 so 庫來實現,java 只要編寫對應的 jni 即可,這裡就涉及到引入第三方包的問題,怎麼寫呢? 
首先,我們需要有個第三方的 so 庫,這裡我從網上下載了一個,下載地址在 github 的demo 中;目錄如下: 
這裡寫圖片描述

在引入第三方 so 庫的時候,需要特別注意的是,這個 so 你要選擇好版本,如果你的 so 是32的,而你在 appliaction.mk 的API版本中,選擇了 all 或者 arm64-v8a等,那麼編譯肯定是報錯的; 
一般手機是 armeabi ,模擬器是 x86 ,機頂盒等板子是 arm64-v8a 的, 我的模擬器剛好是 x86_64 的,所以,這裡引入的 so 庫是 x86_64 下的,匯入之後,目錄如下: 
這裡寫圖片描述

重新編寫 mk 檔案:

LOCAL_PATH := $(call my-dir)
#引入第三方 so 
include $(CLEAR_VARS)
LOCAL_MODULE    := vvw
LOCAL_SRC_FILES := libvvw.so
LOCAL_EXPORT_C_INCLUDES := include
include $(PREBUILT_SHARED_LIBRARY)


include $(CLEAR_VARS)
LOCAL_MODULE    := jniutils
LOCAL_SRC_FILES := jniutils.cpp
LOCAL_LDLIBS :=-llog

#引入第三方編譯模組
LOCAL_SHARED_LIBRARIES := \
vvw

include $(BUILD_SHARED_LIBRARY)

如果匯入的工程報錯,可以試著 APP_ABI 為 x86 ,替換相應的 so 。 
接著,我們在 java 類這裡,新增一個 呼叫 so 方法的 java 方法 getIntValue :

public class JniUtils {

    static {
        System.loadLibrary("jniutils");
        System.loadLibrary("vvw");
    }

    public static native String getName();

    public static native int getIntValue(int a,int b);
}

JniUtils.cpp 的程式碼如下:

#include <jni.h>
#include <string>
#include "include/vvwUtils.h"

extern "C" jstring Java_com_zhengsr_jnidemo_JniUtils_getName(
        JNIEnv* env,
        jobject /* this */) {
    return env->NewStringUTF("獲取兩數字之和:");
}

extern "C" jint Java_com_zhengsr_jnidemo_getIntValue(
        JNIEnv* env,
        jobject obj,jint a,jint b) {
    # addMethod 為 libvvw.so 的方法
    return addMethod(a,b);
}

修改一下 MainActivity.java

這裡寫圖片描述

效果如下; 
這裡寫圖片描述

2、使用 cmake 的方式

上面的 demo 中,寫 c/c++ 的時候,並沒有任何提示,這真的是讓人崩潰啊,寫了都不知道寫對了沒有。所以,在 as 2.2.2 之後,as 就支援用 cmake 的方式去編寫 jni 了,而使用 camke,除了 c/c++ 有提示之外,在 jni 的配置上,也更加的人性化,如果是新建專案,我是推薦你用 camke 的構建方式去編寫。 
官方中文文件如下 
https://developer.android.google.cn/studio/projects/add-native-code.html

首先,在新建工程的時候,勾選上 c++ support ( 3.0 往下拉才有)

這裡寫圖片描述

一路 next ,然後有兩個提示框:

這裡寫圖片描述 
這兩個也勾選上,解釋如下:

  • Exceptions Support:如果您希望啟用對 C++ 異常處理的支援,請選中此複選框。如果啟用此複選框,Android Studio 會將 -fexceptions 標誌新增到模組級 build.gradle 檔案的 cppFlags 中,Gradle 會將其傳遞到 CMake。
  • Runtime Type Information Support:如果您希望支援 RTTI,請選中此複選框。如果啟用此複選框,Android Studio 會將 -frtti 標誌新增到模組級 build.gradle 檔案的 cppFlags 中,Gradle 會將其傳遞到 CMake。

工程已經給了我們一個 jni 的例子,而它的編譯方式就是通過 CMakeLists.txt 來構建的。 
下面是對 CMakeLists.txt 的解釋,由於篇幅,這裡會刪掉一些註釋:

cmake_minimum_required(VERSION 3.4.1)
#這裡會把  native-lib.cpp 轉換成共享庫,並命名為  native-lib
add_library( # 庫的名字
             native-lib

             # 設定成共享庫
             SHARED

             # 庫的原檔案
             src/main/cpp/native-lib.cpp )

#如果需要使用第三方庫,則可以使用 find_library 來找到,比如這裡的 log 這個庫
find_library( 
              # so庫的變數路徑名字,在關聯的時候是使用
              log-lib
              #你需要關聯的so名字
              log )

#因為使用了第三方庫,所以,這裡我們通過 link 這這個庫新增進來
target_link_libraries( # 關聯的so的路徑變數名
                       native-lib
                       #把上面的 log 中的關聯的變數名 log-lib 新增進來即可
                       ${log-lib} )如果要新增庫,則使用 add_library,括號以空格區分,如果要使用第三方庫,比如列印的 log 這個庫,就通過 find_library 的方式新增,最後通過 target_link_libraries 把原始檔的庫,和第三方的庫變數名引進來,注意第三方庫是個路徑變數名,所以 ${}的方式引用。

相較傳統配置,如果對 mk 不熟悉的小夥伴,估計會很喜歡 cmake 的方式.

2.1 用 cmake 寫 jni

按照上面的方式,新建 JniUtils.java 這個類:

public class JniUtils {
    static {
        System.loadLibrary("jniutils");
    }
    public static native String getName();
}

然後編寫,jniutils.cpp,你會驚喜地發現,竟然有提示!!

#include <jni.h>
#include <string>
extern "C"
jstring
Java_com_zhengsr_jnidemo_camke_JniUtils_getName(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "這是使用 camke 的編譯方式啦";
    return env->NewStringUTF(hello.c_str());
}

接下來就是 用 add_library 的方式,我們把 jniutils 加進來:

這裡寫圖片描述

同步一下即可,修改一下 mainactivity,執行,效果如下:

這裡寫圖片描述

可以看到,使用 cmake 的方式,除了有程式碼提示,在新增類上,簡直不能太方便了。

2.2、引入第三方 so 庫

官方推薦,每次庫變動之前,先 clean project 一下,所以,先clean 一下,免得出現找不到 so 的情況; 
接著,我們新增一下第三方so,還是上面的 libvvw.so ,目錄如下:

這裡寫圖片描述

接著,我們需要制定一下 ndk 編譯時的 型別,不然會增加一個 mips 的型別,這個是編不過的。

這裡寫圖片描述

接著,則是配置最重要的 CMakeLists.txt 了,具體如下:

cmake_minimum_required(VERSION 3.4.1)

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

#匯入第三方so包,並宣告為 IMPORTED 屬性,指明只是想把 so 匯入到專案中
add_library( vvw
             SHARED
             IMPORTED )
#指明 so 庫的路徑,CMAKE_SOURCE_DIR 表示 CMakeLists.txt 的路徑
set_target_properties( 
            vvw
            PROPERTIES IMPORTED_LOCATION
            ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libvvw.so )

#指明標頭檔案路徑,不然會提示找不到 so 的方法
include_directories(scr/main/cpp/include/ )

add_library(jniutils SHARED src/main/cpp/jniutils.cpp)

target_link_libraries( # Specifies the target library.
                        jniutils
                        #關聯第三方 so
                        vvw
                       ${log-lib} )

註釋已經寫得很清楚了,關鍵是要寫對 so 的路徑,不然會提示 missing and no rules to make 等錯誤; 
jniutils.cpp 的程式碼如下:

#include <jni.h>
#include <string>
#include "include/vvwUtils.h"

extern "C" jstring Java_com_zhengsr_jnidemo_1camke_JniUtils_getName(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "這是使用 camke 的編譯方式啦,還獲取到兩數之和啦: ";
    return env->NewStringUTF(hello.c_str());
}

extern "C" jint Java_com_zhengsr_jnidemo_1camke_JniUtils_getIntValue(
        JNIEnv* env,
        jobject obj,jint a,jint b) {

    return addMethod(a,b);
}

效果如下: 
這裡寫圖片描述

3、總結

不管是 ndk-build 傳統的方式,還是 cmake 的方式,都有一定的可取之處,當然,在我看來, cmake 無論在學習成本還是程式碼編寫提示上都要優於 ndk-build。 
如果是新建專案,我建議還是用 cmake 的方式,畢竟只 c/c++ 有提示這一點,我相信你也拒絕不了的。 
當然,實際專案上,還有動態載入 so 的方法,這裡就不深入了,這裡就當做個 入門介紹吧。