超詳細的安卓ndk編譯的兩種方式(ndk-build和cmake)
一、概述
搞安卓的工作中難免需要使用native的方法,高效,安全。。。優點就不說了。以前使用到native方法的時候,都是臨時抓起來一種方式就用了,也沒詳細整理兩種方式的差別和詳細的使用方式,雖然不復雜,但是中間還是有很多小細節需要注意的。雖然ndk-build的方式谷歌官方已經不支援使用了,新版的studio和ndk中工具中已經將其移除了,但是還是有必要了解怎麼使用的,以備不時之需,cmake是官方推薦的方式,試用起來也很方便,基本studio把需要的步驟都給你建立好了,當離開studio,手寫的時候你還能知道怎麼寫嗎?第一步寫什麼,第二部寫什麼來著?今天就主要總結了下兩種編譯方式的詳細步驟和方法,以免日後忘記了。
二、ndk-build方式
1.新建Java類,宣告native方法和引數
eg:
public class NdkJniUtils {
public native String getCLanguageString();
}
2.使用的地方引用加入的native工具類
eg:
NdkTest ndkTest = new NdkTest();
TextView tv2 = (TextView) findViewById(R.id.tv_2);
tv2.setText(ndkTest.getStringFromC());
3.編譯一下工程,將native工具類編譯為class檔案
找到指定目錄:
projectname\app\build\intermediates\classes\debug
輸入命令列:
javac HelloJNI.java
或者makeproject進行編譯
注:命令列編譯後的class檔案會生成在當前路徑下
4.找到對應的class檔案
利用Android Studio的Terminal,進入你自己的Android工程檔案的對應的class目錄,
在Terminal中輸入命令
cd \app\build\intermediates\classes\debug
或者在進入指定目錄後再開啟控制檯即可找到編譯成功的class檔案
5.利用javah生成對應的 .h標頭檔案
androidstudio的Terminal中cd到~/workspace/projectname/app/src/main/java目錄
在Terminal中輸入命令
- 方式一:
輸入
javah -classpath . -jni com.ang.test.ndk.Java2CJni (-jni為預設值可省略)
注意:classpath後面有個 "." 前後都有空格
com.ang.test.ndk.Java2CJni 是自己要轉換.h檔案的類的全路徑名;
- 方式二:
javah -classpath F:\Demo\Test\app\src\main\java com.ang.test.ndk.Java2CJni
F:\Demo\Test\app\src\main\java 要生成.h檔案的類的全路徑 com.ang.test.ndk.Java2CJni 就是包名+類名
指令用法說明:
javah -d jni -jni -classpath ..\..\build\intermediates\classes\debug com.example.application.JniTest
使用以上命令需要先對native類進行編譯為class檔案
Javah命令的引數說明如下:
-d<dir> 輸出目錄,後面跟上要生成的目錄名
-jni 生成Jni樣式的標標頭檔案
-classpath<path> 指定載入類的路徑,後面跟上你要生成標頭檔案的這個類的路徑,例如:
..\..\..\build\intermediates\classes\debug(這個是類所在的路徑)
com.example.application.JniTest(類的包名)
用法:
javah [options] <classes> 其中, [options] 包括:
-o <file> 輸出檔案 (只能使用 -d 或 -o 之一)
-d <dir> 輸出目錄
-v -verbose 啟用詳細輸出
-h --help -? 輸出此訊息
-version 輸出版本資訊
-jni 生成 JNI 樣式的標標頭檔案 (預設值)
-force 始終寫入輸出檔案
-classpath <path> 從中載入類的路徑
-bootclasspath <path> 從中載入引導類的路徑
<classes> 是使用其全限定名稱指定的 (例如, java.lang.Object)。
在我們正常使用的時候只需要簡單的幾個引數即可,我們以Hello這個類來舉例說明:
javah -d E:\Kongfuzi\HelloJNI com.sahadev.regix.Hello
6.編寫C/C++程式碼
建立jni目錄,編寫對應的.c或者.cpp檔案,引入生成的.h標頭檔案,以及從.h中拷貝方法體,實現方法,實現函式時注意引數和標頭檔案中定義的有所不同,定義時只定義引數型別,沒有定義引數名
//
// Created by wangjp on 2018/10/18.
//
#include <jni.h>
#include <string>
#include <com_demo_testc_NdkTest.h>
using namespace std;
/*
* Class: com_demo_testc_NdkTest
* Method: getStringFromC
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_demo_testc_NdkTest_getStringFromC
(JNIEnv *env, jobject instance) {
return env->NewStringUTF("自己編寫的c檔案");
}
/*
* Class: com_demo_testc_NdkTest
* Method: getStringWithParams
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_demo_testc_NdkTest_getStringWithParams
(JNIEnv *env, jobject instance, jstring paras_) {
const char *paras = env->GetStringUTFChars(paras_, 0);
string s = string(paras);
string s1 = "我的c檔案";
s = s + s1;
env->ReleaseStringUTFChars(paras_, paras);
return env->NewStringUTF(s.c_str());
}
7.編寫編譯的配置檔案
- 編輯Android.mk檔案:
#每個Android.mk檔案必須以LOCAL_PATH開頭,在整個開發中,它通常別用做定位資原始檔,例如,功能巨集my-dir提供給編譯系統當前的路徑。
LOCAL_PATH := $(call my-dir)
#CLEAR_VARS指編譯系統提供一個特殊的GUN MakeFile來為你清除所有的LOCAL_XXX變數,LOCAL_PATH不會被清除。使用這個變數是因為在編譯系統時,所有的控制檔案都會在一個GUN Make上下文進行執行,而在此上下文中所有的LOCAL_XXX都是全域性的。
include $(CLEAR_VARS)
#LOCAL_MODULE變數是為了確定模組名,並且必須要定義。這個名字必須是唯一的同時不能含有空格。會自動的為檔案新增適當的字首或字尾,模組名為“foo”它將會生成一個名為“libfoo.so”檔案。
LOCAL_MODULE := myjni
#包含一系列被編譯進模組的C 或C++資原始檔
LOCAL_SRC_FILES := JNI_C++.cpp
#指明一個GUN Makefile指令碼,並且收集從最近“include$(CLEAR_VARS)”下的所有LOCALL_XXX變數的資訊,最後告訴編譯系統如何正確的進行編譯。將會生成一個靜態庫hello-jni.a檔案或者動態庫libhello-jni.so。
include $(BUILD_SHARED_LIBRARY)
總結:
LOCAL_PATH即為呼叫命令的所在目錄,你在哪個目錄下使用cmd命令,這裡就會返回它的路徑地址
LOCAL_MODULE你生成的檔名稱是什麼,輸出之後會自動在名稱的前後加上lib和.so
LOCAL_SRC_FILES要對哪個檔案進行編譯
- 編輯Application.mk
可沒有,主要指定so呼叫庫名以及編譯的so對應CPU平臺
若沒有,則可在build.gradle中設定,此時使用系統進行編譯,而非使用ndk-build
APP_MODULES := MyJni
APP_ABI := all all代表全平臺
8.編譯方式選擇
- 使用gradle進行ndk編譯
在app module目錄下的build.gradle中設定庫檔名(生成的so檔名)。找到gradle檔案的defaultConfig這項,在裡面新增如下內容:
defaultConfig {
......
ndk{
moduleName "YanboberJniLibName" //生成的so名字
ldLibs "log", "z", "m" //新增依賴庫檔案,如果有log列印等
abiFilters "armeabi", "armeabi-v7a", "x86" //輸出指定cpu體系結構下的so庫。
}
}
externalNativeBuild {
ndkBuild { path file("src/main/java/jni/Android.mk")
}
}
sourceSets {
main {
jni.srcDirs('src/main/java/jni')
}
}
此種生成的so檔案在app/build/intermediates/ndkBuild/debug下
- 直接使用指令ndk-build在c的原始檔目錄進行編譯
defaultConfig {
......
sourceSets.main{
jni.srcDirs = []
jniLibs.srcDirs "src/main/java/libs"
}
}
9.在native方法的申明中引用so庫
static {
System.loadLibrary("YanboberJniLibName"); //defaultConfig.ndk.moduleName
}
10.可能遇到的問題
- Error:Execution failed for task ':jnilib:compileReleaseNdk'.
> Error: Your project contains C++ files but it is not using a supported native build system.
解決:
意思是專案中沒有使用NDK的配置,解決方法是在gradle.properties檔案中新增如下配置:
android.useDeprecatedNdk=true
需要注意的是你的jni所在module 的gradle需要如下配置,一般來說在建立jnifolder時就已經自動建立了:
sourceSets {
main {
jni.srcDirs = ['src/main/java/jni', 'src/main/java/cpp']
}
}
- 在C++標頭檔案中加入#include <string>
ndk-build後報錯
fatal error: 'string' file not found
#include <string>
^~~~~~~~
1 error generated.
解決:
在網上搜索了一大圈, 原來是需要讓Android NDK支援STL(Standard Template Library)
將Application.mk放在jni目錄下,新增以下內容:
APP_STL := stlport_static
三:CMake方式
1.建立專案時勾選包含c/c++檔案,或者手動建立cpp目錄並建立.c/.cpp檔案,建立CMakeLists.txt檔案
2.在對應的類中定義對應的native方法,並實現.c/.cpp中對應的native方法
3.配置CMakeLists.txt檔案,主要關注以下幾個方法
- 設定構建本地庫所需要的CMake的最低版本
cmake_minimum_required(VERSION 3.4.1)
- 設定本地庫的名稱,型別,路徑
建立並命名一個庫,將其設定為靜態或共享動態(安卓只支援載入動態so庫,但是動態庫依賴庫可以是靜態庫.a),並提供其原始碼的相對路徑。您可以定義多個庫,併為您構建CMake。Gradle會自動將共享庫與APK打包。
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
- 找到需要新增的其他本地依賴庫,配置其名稱,路徑
搜尋指定的預構建庫並將路徑儲存為變數。因為CMake在預設情況下在搜尋路徑中包含系統庫,所以您只需要指定要新增的公共NDK庫的名稱
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 )
- 將需要依賴的三方庫引入本地庫
指定CMake應該連結到目標庫的庫。您可以連結多個庫,例如在這個構建指令碼中定義的庫、預構建的第三方庫或系統庫。
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
4.配置build.gradle檔案
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.demo.testc"
minSdkVersion 15
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
// 配置需要的cpu平臺
ndk {
abiFilters "armeabi", "armeabi-v7a","arm64-v8a", "x86"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
// cmake方式編譯時再開啟,不能和ndk-build方式共存
// cmake {
// path "CMakeLists.txt"
// }
// 使用gradle編譯ndk時開啟,指定編譯方式
ndkBuild {
path file("src/main/java/jni/Android.mk")
}
}
//此種為使用ndk-build命令列編譯成so後指定資源
// sourceSets {
// main {
// jniLibs.srcDirs('src/main/java/libs')
// }
// }
// 此種為使用gradle編譯ndk,指定編譯的c原始檔
sourceSets {
main {
jni.srcDirs('src/main/java/jni')
}
}
}
5.在對應的類中呼叫native方法
首先使用靜態程式碼塊載入動態庫
static { System.loadLibrary("YanboberJniLibName"); //defaultConfig.ndk.moduleName }
然後即可呼叫動態庫中的函式
6.可能遇到的問題
- 部分平臺的so無法編譯出來,如armeabi,mips,mips64等,如果在gradle中強制指定這些平臺,studio將會報錯
解決:ndk的版本高於r16,目前最新的是18
16.1.4479499 |
Update Available: 18.1.5063045 |
總結: