1. 程式人生 > >超詳細的安卓ndk編譯的兩種方式(ndk-build和cmake)

超詳細的安卓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

總結: