你不知道的Android NDK開發

      上篇文章講解了有關JNI知識,如果還未了解 JNI,詳情請看Android JNI開發。那麼到底如何生成一個“so”文件?帶着這個疑問,開始我們的文章。

一、概述。

      “so”文件是使用C/C++編寫生成的,在Android 平臺上快速編譯、打包該文件,那麼就要使用NDK了,當然不使用NDK,也可以生成“so”文件,讀者如果感興趣,可以自行查找文檔。

      NDK全稱爲native development kit本地語言(C&C++)開發包。簡單來說利用NDK,可以開發純C&C++的代碼,然後編譯成庫,讓Java程序調用。NDK開發的可以稱之爲底層開發或者jni(java  native interface)層開發。

      NDK是一系列工具的集合,NDK提供了一系列的工具,可以幫助開發者進行c/c++的開發,並能自動將.so打包成apk。NDK集成了交叉編譯器,並提供了相應的mk文件可以做到隔離CPU,平臺,ABI等差異,只需修改mk文件即可。開發人員只需要簡單修改mk文件,就可以創建出“.so”文件。NDK還提供了一份穩定的功能有限的API頭文件聲明。

PS:NDK和JNI的區別是什麼呢?

     簡單來說,NDK是工具,使用NDK可以更方便、快捷的開發JNI,NDK開發是基於JNI的。而JNI,是Java提出的協議,從Java1.1開始,JNI就已經是Java平臺的一部分,它允許Java代碼和其他語言寫的代碼進行交互,JNI開發便是基於此開發。

    使用NDK開發JNI的步驟如下;

1.JNI接口的設計;
2.使用C/C++實現本地方法;
3.使用NDK生成JNI動態鏈接庫.so文件;
4.將動態鏈接庫複製到Java/Android工程,調用,運行即可。

二、NDK配置。

1.下載NDK。

2.配置環境變量。(配置環境變量,是爲了方便使用命令ndk-build腳本進行NDK編譯。)


例如,Windows系統配置如下:

 環境變量 PATH 下追加 :F:\android-ndk-r13b-windows-x86_64\android-ndk-r13b

配置完成後。打開cmd,輸入ndk-build,出現如下如所示,則配置成功。


3.配置NDK路徑。

      我使用的是Android Studio。

      點菜單欄的File->ProjectStructure…->在打開的窗口中左側選中SDKLocation->在右側Android NDK Location中填入NDK目錄所在路徑,如下圖所示:


三、生成so庫。

1.新建一個Module

2.新建CalculateUtils類定義一個native方法。例如,

public class CalculateUtils {

    static {
        System.loadLibrary("calculate_jni");
    }
    public native String getStringFromNative(String str);//本地方法
}

3.生成jni目錄及對應的頭文件。

  打開Terminalcd到工程的“src/main/java”目錄;

(1).輸入指令

javah -jni cn.xinxing.jnitest.CalculateUtils

此時,刷新Module,便會出現一個名爲“cn_xinxing_jnitest_CalculateUtils.h”的文件,


在“src/main”下,新建一個jni目錄,將生成的“cn_xinxing_jnitest_CalculateUtils.h”的文件複製到該目錄中。還有另一種更加簡單的實現方式。

(2).在“src/main/java”路徑下,輸入

 javah -d ../jni 你的包名.引用本地方法的類的名稱

javah意思是生成一個.h頭文件,-d ../jni的意思是生成一個名字叫做jni的文件夾(directory),位置是在當前目錄(src/main/java)的上一級目錄(即src/main目錄)。

例如:

javah -d ../jni cn.xinxing.jnitest.CalculateUtils


這種方式同樣也生成jni目錄及對應的頭文件,並且更加簡單,值得推薦!

4.實現本地方法。

在jni目錄下新建一個 c或者cpp文件。來實現頭文件裏面聲明的方法。例如“cn_xinxing_jnitest_CalculateUtils”,具體實現如下,

#include <cn_xinxing_jnitest_CalculateUtils.h>

JNIEXPORT jstring JNICALL  Java_cn_xinxing_jnitest_CalculateUtils_getStringFromNative
        (JNIEnv * env, jobject obj, jstring str){
    return str;
}

5.生成“so”文件。

生成“so”文件有兩種方式,一種是直接使用 ndk-build 命令,另外一種是使用Android Studio的gradle來自動編譯生成。下面分別用這兩種方式實現。

(1).使用gradle腳本。

需要配置build.gradle文件。

      這裏的build.gradle是指該Module模塊下的build.gradle,不是整個工程的build.gradle文件。在模塊的build.gradle的defaultConfig下加入以下配置:

...  
 ndk {
            moduleName "calculate_jni"
            abiFilters "armeabi", "armeabi-v7a", "x86"
        }
點擊同步按鈕,如果沒有報錯,此時   點擊Build->Make Module ‘jnitest’進行編譯,生成.so庫文件,如果沒有報錯,那麼最終可以在路徑:’jnitest‘->build->intermediates->ndk->debug->lib下:

如果在編譯時可能報錯,例如



那麼需要在gradle.properties文件末尾添加android.useDeprecatedNdk=true就ok了!

其他錯誤,請根據提示,解決就行!

(2).使用 ndk-build 命令。

使用ndk-build 生成“so”文件,我們還需要在“jni”目錄中創建兩個文件,分別是Android.mkApplication.mk。下面分別看看他們的具體實現!

Application.mk,這個文件用來配置編譯平臺相關內容,我們最常用的估計只是APP_ABI字段,它用來指定我們需要基於哪些CPU架構的.so文件,當然你可以配置多個平臺。下面指定了兩個平臺。

APP_ABI := armeabi armeabi-v7a
Android.mk,文件用來指定源碼編譯的配置信息,例如工作目錄,編譯模塊的名稱,參與編譯的文件等。

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := calculate_jni
LOCAL_SRC_FILES := cn_xinxing_jnitest_CalculateUtils.cpp
include $(BUILD_SHARED_LIBRARY)
    LOCAL_PATH:設置工作目錄,而my-dir則會返回Android.mk文件所在的目錄。
    CLEAR——VARS:清除幾乎所有以LOCAL開頭的變量(不包括LOCAL_PATH)。
    LOCAL_MODULE:指定生成動態庫名。
    LOCAL_SRC_FILES:用來指定生成動態庫的源文件   。
    BUILD_SHARED_LIBRARY:作用是指定生成的靜態庫或者共享庫在運行時依賴的共享庫模塊列表。

按照上面的步驟,創建並輸入相應的內容後,我們就可以生成“so”文件了。    打開Terminal,cd到工程的“src/main/jni”目錄,輸入“ndk-build”命令後,回車,如果你看到下面的截圖,那就編譯成功了!


然後在Module目錄結構中(刷新或者點擊目錄),就可以看到新多了“libs”目錄,


可以看到生成了兩個平臺的“so”文件。

5.驗證“so”文件中的方法。

創建一個Module,將上面生成的所有平臺中的“so”文件複製到該Modulesrc/main目錄下的jniLibs目錄,如果沒有該目錄,則手動傳建一個,然後將CalculateUtils類複製到該Module中,一定要記得類名和包名必須和原CalculateUtils類(生成“so”文件的CalculateUtils類)保持一致。否則可能會報錯“java.lang.UnsatisfiedLinkError: Native method not found:...”。


下面我們就可以調用jni方法了。在Activity中調用jni方法,

public class MainActivity extends Activity {

    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CalculateUtils calculateUtils=new CalculateUtils();
        Log.e(TAG, calculateUtils.getStringFromNative("zhangsan------------>"));
    }
    
}
運行後截圖,

打印出我們傳遞的字符串。說明生成“so”文件以及實現JNI成功!

ps:在"so"文件中打印log。

(1).配置build.gradle文件。

...  
ndk {
            moduleName "calculate_jni"
            ldLibs "log"
            abiFilters "armeabi", "armeabi-v7a"
        }
(2).修改cn_xinxing_jnitest_CalculateUtils.cpp文件,

#include <cn_xinxing_jnitest_CalculateUtils.h>
#include <android/log.h>
#define TAG "jni"
#define LOGV(...)__android_log_print(ANDROID_LOG_VERBOSE,TAG,__VA_ARGS__)
JNIEXPORT jstring JNICALL  Java_cn_xinxing_jnitest_CalculateUtils_getStringFromNative
        (JNIEnv * env, jobject obj, jstring str){
    LOGV("log from native");
    return str;
}
編譯生成“so”文件。然後調用該“so”文件,截圖如下:


四、總結。

1.在配置build.gradle時,如果我們的jni源碼沒有在‘jni’目錄中,編譯的話會報錯,此時,我們需要在build.gradle文件中指定jni源碼的路徑,如下配置即可,

	android {
//
   //...
    sourceSets {
        main {
            //你的源碼目錄
             jniLibs.srcDirs  'src/main/jnis'
        }
    }
} 

2.爲何生成一個“libxxx.so”文件呢?

 可能有的人會問,爲何定義的“so”文件與生成的“so”文件名不一樣呢?一個是“calculate_jni”,生成的“so”文件是“libcalculate_jni.so”。libxxx.so的命名規則,沿襲Linux傳統,lib<something>.so是類庫文件名稱的格式,在Java的System.loadLibrary(" something ")方法中指定庫名稱時,不能包括 前綴—— lib,以及後綴——.so。所以在加載“so”文件時,需要注意文件名。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章