NDK

Android NDK 是能將 C 或 C++ 嵌入到 Android 應用中的工具。html

  • 在平臺之間移植其應用。
  • 重複使用現有庫,或者提供其本身的庫供重複使用。
  • 在某些狀況下提升性能,特別是像遊戲這種計算密集型應用。

原生共享庫:NDK 從 C/C++ 源代碼編譯這些庫或 .so 文件。java

原生靜態庫:NDK 也可編譯靜態庫或 .a 文件,而您可將靜態庫關聯到其餘庫。android

Java 原生接口 (JNI):JNI 是 Java 和 C++ 組件用以互相通訊的接口。ios

應用二進制接口 (ABI):ABI 能夠很是精確地定義應用的機器代碼在運行時應該如何與系統交互。NDK 根據這些定義編譯 .so 文件。git

JNI

JNI是Java Native Interface。 它定義了Android從託管代碼(用Java或Kotlin編程語言編寫)編譯的字節碼的方式,以與本機代碼(用C / C ++編寫)進行交互。程序員

JavaVM and JNIEnv

JNI定義了兩個關鍵數據結構,「JavaVM」和「JNIEnv」。 這二者基本上都是指向函數表指針的指針。 (在C++版本中,它們是帶有指向函數表的指針的類,以及用於指向表中的每一個JNI函數的成員函數。)JavaVM提供「調用接口」函數,容許您建立和銷燬JavaVM的。理論上,每一個進程能夠有多個JavaVM,但Android只容許一個。github

JNIEnv提供了大多數JNI功能。 本地函數都接收JNIEnv做爲第一個參數。web

JNIEnv用於線程本地存儲。 所以,沒法在線程之間共享JNIEnv。 若是一段代碼沒有其餘方法來獲取它的JNIEnv,你應該共享JavaVM,並使用GetEnv來發現線程的JNIEnv。 (假設它有一個;請參閱下面的AttachCurrentThread。)shell

JNIEnv和JavaVM的C聲明與C++聲明不一樣。 「jni.h」包含文件提供了不一樣的typedef,具體取決於它是否包含在C或C ++中。 所以,在兩種語言包含的頭文件中包含JNIEnv參數是一個壞主意。express

Thread

全部線程都是Linux線程,由內核調度。 它們一般從託管代碼(使用Thread.start)啓動,但也能夠在其餘地方建立,而後附加到JavaVM。 例如,使用pthread_create啓動的線程可使用JNI AttachCurrentThread或AttachCurrentThreadAsDaemon函數附加。 在鏈接線程以前,它沒有JNIEnv,也沒法進行JNI調用。

附加本機建立的線程會致使構造java.lang.Thread對象並將其添加到「main」ThreadGroup,使調試器能夠看到它。 在已經鏈接的線程上調用AttachCurrentThread是一個無效操做。

Android不會掛起執行本地代碼的線程。 若是正在進行垃圾收集,或者調試器已發出掛起請求,則Android將在下次進行JNI調用時暫停該線程。

經過JNI鏈接的線程必須在退出以前調用DetachCurrentThread。 若是直接對此進行編碼很麻煩,在Android 2.0(Eclair)及更高版本中,您可使用pthread_key_create來定義將在線程退出以前調用的析構函數,並從那裏調用DetachCurrentThread。(將該鍵與pthread_setspecific一塊兒使用以將JNIEnv存儲在線程局部存儲中;這樣它將做爲參數傳遞給析構函數。)

jclass, jmethodID, and jfieldID

若是要從本地代碼訪問對象的字段,請執行如下操做:

  • 使用FindClass獲取類的類對象引用
  • 使用GetFieldID獲取字段的字段ID
  • 使用適當的內容(例如GetIntField)獲取字段的內容

一樣,要調用方法,首先要獲取類對象引用,而後獲取方法ID。 ID一般只是指向內部運行時數據結構的指針。 查找它們可能須要進行屢次字符串比較,可是一旦有了它們,實際調用獲取字段或調用方法的速度很是快。

若是性能很重要,那麼查看值一次並將結果緩存在本地代碼中會頗有用。 因爲每一個進程限制一個JavaVM,所以將此數據存儲在靜態本地結構中是合理的。

在卸載類以前,類引用,字段ID和方法ID保證有效。 只有在與ClassLoader關聯的全部類均可以進行垃圾回收時,纔會卸載類,這種狀況不多見,但在Android中並不是不可能。 但請注意,jclass是類引用,必須經過調用NewGlobalRef進行保護(請參閱下一節)。

若是您想在加載類時緩存ID,並在卸載和從新加載類時自動從新緩存它們,初始化ID的正確方法是將一段代碼添加到相應的代碼中。

/*
 * We use a class initializer to allow the native code to cache some
 * field offsets. This native function looks up and caches interesting
 * class/field/method IDs. Throws on failure.
 */
private static native void nativeInit();

static {
    nativeInit();
}
複製代碼

在執行ID查找的C / C ++代碼中建立nativeClassInit方法。 在初始化類時,代碼將執行一次。 若是該類被卸載而後從新加載,它將再次執行。

Local and global references

每一個參數都傳遞給本機方法,幾乎JNI函數返回的每一個對象都是「本地引用」。 這意味着它在當前線程中當前本機方法的持續時間內有效。 即便在本機方法返回後對象自己繼續存在,引用也無效。

這適用於jobject的全部子類,包括jclass,jstring和jarray。 (當啓用擴展JNI檢查時,運行時將警告您大多數引用誤用。)

獲取非本地引用的惟一方法是經過函數NewGlobalRef和NewWeakGlobalRef。

若是要保留較長時間段的引用,則必須使用「全局」引用。 NewGlobalRef函數將本地引用做爲參數並返回全局引用。 在調用DeleteGlobalRef以前,保證全局引用有效。

這種模式一般在緩存從FindClass返回的jclass時使用,例如:

jclass localClass = env->FindClass("MyClass"); 
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
複製代碼

全部JNI方法都接受本地和全局引用做爲參數。 對同一對象的引用可能具備不一樣的值。 例如,在同一對象上對NewGlobalRef的連續調用的返回值可能不一樣。 要查看兩個引用是否引用同一對象,必須使用IsSameObject函數。 切勿在本機代碼中將引用與==進行比較。

這樣作的一個結果是您不能假定對象引用在本機代碼中是常量或惟一的。 表示對象的32位值可能與方法的一次調用不一樣,而且兩個不一樣的對象可能在連續調用上具備相同的32位值。 不要將jobject值用做鍵。

程序員必須「不要過分分配」本地引用。 實際上,這意味着若是您正在建立大量本地引用,也許在運行對象數組時,您應該使用DeleteLocalRef手動釋放它們,而不是讓JNI爲您執行此操做。 實現僅須要爲16個本地引用保留插槽,所以若是您須要更多,則應該隨意刪除或使用EnsureLocalCapacity / PushLocalFrame來保留更多。

請注意,jfieldIDs和jmethodIDs是不透明的類型,而不是對象引用,不該傳遞給NewGlobalRef。 GetStringUTFChars和GetByteArrayElements等函數返回的原始數據指針也不是對象。 (它們能夠在線程之間傳遞,而且在匹配的Release調用以前有效。)

一個不尋常的案例值得單獨說起。 若是使用AttachCurrentThread附加本機線程,則運行的代碼將永遠不會自動釋放本地引用,直到線程分離。 您建立的任何本地引用都必須手動刪除。 一般,在循環中建立本地引用的任何本機代碼可能須要進行一些手動刪除。

當心使用全局引用。 全局引用多是不可避免的,但它們很難調試,而且可能致使難以診斷的內存(錯誤)行爲。 在其餘條件相同的狀況下,具備較少全局引用的解決方案可能更好。

UTF-8 and UTF-16 strings

Java編程語言使用UTF-16。

爲方便起見,JNI還提供了使用Modified UTF-8的方法。 修改後的編碼對C代碼頗有用,由於它將\ u0000編碼爲0xc0 0x80而不是0x00。 關於這一點的好處是你能夠依靠C風格的零終止字符串,適合與標準的libc字符串函數一塊兒使用。 缺點是您沒法將任意UTF-8數據傳遞給JNI並指望它可以正常工做。

若是可能,使用UTF-16字符串操做一般會更快。 Android目前不須要GetStringChars中的副本,而GetStringUTFChars須要分配和轉換爲UTF-8。 請注意,UTF-16字符串不是以零結尾的,而且容許使用\ u0000,所以您須要掛起字符串長度以及jchar指針。

不要忘記釋放你獲得的字符串。 字符串函數返回jchar *或jbyte *,它們是原始數據的C樣式指針,而不是本地引用。 它們在調用Release以前保證有效,這意味着在本機方法返回時它們不會被釋放。

傳遞給NewStringUTF的數據必須採用Modified UTF-8格式。 一個常見的錯誤是從文件或網絡流中讀取字符數據並將其交給NewStringUTF而不對其進行過濾。 除非您知道數據是有效的MUTF-8(或7位ASCII,這是兼容的子集),不然您須要刪除無效字符或將它們轉換爲正確的修改的UTF-8格式。 若是不這樣作,UTF-16轉換可能會提供意外結果。 CheckJNI - 默認狀況下爲模擬器打開 - 掃描字符串並在VM收到無效輸入時停止VM。

Primitive arrays

JNI提供了訪問數組對象內容的函數。 雖然一次只能訪問一個條目的對象數組,但能夠直接讀取和寫入基元數組,就好像它們是用C語句聲明的同樣。

爲了使接口儘量高效而不約束VM實現,Get <PrimitiveType> ArrayElements系列調用容許運行時返回指向實際元素的指針,或者分配一些內存並進行復制。 不管哪一種方式,返回的原始指針都保證有效,直到發出相應的Release調用(這意味着,若是數據未被複制,則數組對象將被固定,而且不能做爲壓縮的一部分從新定位 堆)。 您必須釋放您得到的每一個數組。 此外,若是Get調用失敗,則必須確保您的代碼稍後不會嘗試釋放NULL指針。

您能夠經過傳入isCopy參數的非NULL指針來肯定是否複製了數據。 這不多有用。

Release調用採用一個mode參數,該參數能夠包含三個值之一。 運行時執行的操做取決於它是否返回指向實際數據的指針或其副本:

  • 0

    實際:數組對象未固定。

    複製:複製數據。釋放帶有副本的緩衝區。

  • JNI_COMMIT

    實際:什麼都不作。

    複製:複製數據。沒有釋放帶有副本的緩衝區。

  • JNI_ABORT

    實際:數組對象未固定。早期的寫入不會停止。

    複製:釋放帶有副本的緩衝區;對它的任何改變都會丟失。

檢查isCopy標誌的一個緣由是知道在更改數組後是否須要使用JNI_COMMIT調用Release - 若是您在進行更改和執行使用數組內容的代碼之間交替,則能夠跳過 無操做提交。 檢查標誌的另外一個可能緣由是有效處理JNI_ABORT。 例如,您可能但願獲取一個數組,將其修改到位,將片斷傳遞給其餘函數,而後丟棄更改。 若是您知道JNI正在爲您製做新副本,則無需建立另外一個「可編輯」副本。 若是JNI將原件傳給你,那麼你須要製做本身的副本。

若是* isCopy爲false,則假設您能夠跳過Release調用是一個常見的錯誤(在示例代碼中重複)。 不是這種狀況。 若是沒有分配複製緩衝區,則原始內存必須固定,而且不能被垃圾收集器移動。

另請注意,JNI_COMMIT標誌不會釋放數組,您最終須要使用不一樣的標誌再次調用Release。

Region calls

除了Get <Type> ArrayElements和GetStringChars這樣的調用以外,當你想要作的就是複製數據時,這可能會很是有用。 考慮如下:

jbyte* data = env->GetByteArrayElements(array, NULL);
if (data != NULL) {
    memcpy(buffer, data, len);
    env->ReleaseByteArrayElements(array, data, JNI_ABORT);
}
複製代碼

這會抓取數組,將第一個len字節元素複製出來,而後釋放數組。 根據實現,Get調用將固定或複製數組內容。 代碼複製數據(多是第二次),而後調用Release; 在這種狀況下,JNI_ABORT確保沒有第三個副本的機會。

人們能夠更簡單地完成一樣的事情:

env->GetByteArrayRegion(array, 0, len, buffer);
複製代碼

這有幾個好處:

  • 須要一個JNI調用而不是2,從而減小開銷。
  • 不須要固定或額外的數據副本。
  • 下降程序員錯誤的風險 - 沒有在發生故障後忘記調用Release的風險。

一樣,您可使用Set <Type> ArrayRegion調用將數據複製到數組中,使用GetStringRegion或GetStringUTFRegion將字符複製到String中。

Exceptions

異常處於待處理狀態時,您不能調用大多數JNI函數。 您的代碼應該注意到異常(經過函數的返回值,ExceptionCheck或ExceptionOccurred)並返回,或者清除異常並處理它。

在異常處於掛起狀態時,您能夠調用的惟一JNI函數是:

DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
複製代碼

許多JNI調用均可以拋出異常,但一般會提供一種更簡單的方法來檢查失敗。 例如,若是NewString返回非NULL值,則無需檢查異常。 可是,若是調用方法(使用相似CallObjectMethod的函數),則必須始終檢查異常,由於若是拋出異常,返回值將無效。

請注意,解釋代碼拋出的異常不會展開本機堆棧幀,Android也不支持C ++異常。 JNI Throw和ThrowNew指令只是在當前線程中設置了一個異常指針。 從本機代碼返回託管後,將注意並正確處理該異常。

本機代碼能夠經過調用ExceptionCheck或ExceptionOccurred來「捕獲」異常,並使用ExceptionClear清除它。 像往常同樣,丟棄異常而不處理它們可能會致使問題。

沒有用於操做Throwable對象自己的內置函數,因此若是你想(好比)獲取異常字符串,你須要找到Throwable類,查找getMessage的方法ID「()Ljava / lang / String ;「,調用它,若是結果是非NULL,則使用GetStringUTFChars獲取能夠傳遞給printf(3)或等效的東西。

Extended checking

JNI進行的錯誤檢查不多。 錯誤一般會致使崩潰。 Android還提供了一種名爲CheckJNI的模式,其中JavaVM和JNIEnv函數表指針切換到在調用標準實現以前執行擴展系列檢查的函數表。

附加檢查包括:

  • 數組:嘗試分配負大小的數組。
  • 錯誤的指針:將錯誤的jarray / jclass / jobject / jstring傳遞給JNI調用,或者將NULL指針傳遞給具備非可空參數的JNI調用。
  • 類名:將類名稱的「java / lang / String」樣式傳遞給JNI調用。
  • 關鍵調用:在「關鍵」get和相應的release之間進行JNI調用。
  • 直接ByteBuffers:將錯誤的參數傳遞給NewDirectByteBuffer。
  • 異常:在異常掛起時進行JNI調用。
  • JNIEnv * s:從錯誤的線程中使用JNIEnv *。
  • jfieldIDs:使用NULL jfieldID,或使用jfieldID將字段設置爲錯誤類型的值(例如,嘗試將StringBuilder分配給String字段),或者使用jfieldID爲靜態字段設置實例字段或 反之亦然,或者使用來自一個類的jfieldID和另外一個類的實例。
  • jmethodIDs:在進行Call *方法JNI調用時使用錯誤的jmethodID:錯誤的返回類型,靜態/非靜態不匹配,'this'(非靜態調用)或錯誤類(靜態調用)的錯誤類型。
  • 引用:對錯誤的引用使用DeleteGlobalRef / DeleteLocalRef。
  • 釋放模式:將錯誤釋放模式傳遞給釋放調用(0,JNI_ABORT或JNI_COMMIT之外的其餘模式)。
  • 類型安全:從本機方法返回不兼容的類型(從聲明爲返回String的方法返回StringBuilder,好比說)。
  • UTF-8:將無效的Modified UTF-8字節序列傳遞給JNI調用。

(仍未檢查方法和字段的可訪問性:訪問限制不適用於本機代碼。)

有幾種方法能夠啓用CheckJNI。

若是您正在使用模擬器,則默認狀況下CheckJNI處於啓用狀態。

若是您有root設備,則可使用如下命令序列在啓用CheckJNI的狀況下從新啓動運行時:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start
複製代碼

在其中任何一種狀況下,當運行時啓動時,您將在logcat輸出中看到相似的內容:

D AndroidRuntime: CheckJNI is ON
複製代碼

若是您有常規設備,則可使用如下命令:

adb shell setprop debug.checkjni 1
複製代碼

這不會影響已經運行的應用程序,但從那時起啓動的任何應用程序都將啓用CheckJNI。 (將屬性更改成任何其餘值或只是從新啓動將再次禁用CheckJNI。)在這種狀況下,您將在下次應用程序啓動時在logcat輸出中看到相似的內容:

D Late-enabling CheckJNI
複製代碼

您還能夠在應用程序的清單中設置android:debuggable屬性,以便爲您的應用啓用CheckJNI。 請注意,Android構建工具將自動爲某些構建類型執行此操做。

Native libraries

可使用標準System.loadLibrary從共享庫加載本機代碼。

實際上,舊版本的Android在PackageManager中存在錯誤,致使本機庫的安裝和更新不可靠。 ReLinker項目爲此和其餘本機庫加載問題提供了變通方法。

從靜態類初始化程序調用System.loadLibrary(或ReLinker.loadLibrary)。 參數是「未修飾」的庫名稱,所以要加載libfubar.so,您將傳入「fubar」。

運行時有兩種方法能夠找到本機方法。 可使用RegisterNatives顯式註冊它們,也可讓運行時使用dlsym動態查找它們。 RegisterNatives的優勢是你能夠預先檢查符號是否存在,並且除了JNI_OnLoad以外,你不能導出任何東西,從而能夠擁有更小更快的共享庫。 讓運行時發現函數的優勢是編寫的代碼略少。

要使用RegisterNatives:

  • 提供JNIEXPORT jint JNI_OnLoad(JavaVM * vm,void * reserved)函數。
  • 在JNI_OnLoad中,使用RegisterNatives註冊全部本機方法。
  • 使用-fvisibility = hidden構建,以便只從您的庫中導出JNI_OnLoad。 這會產生更快,更小的代碼,並避免與加載到應用程序中的其餘庫發生潛在衝突(但若是應用程序在本機代碼中崩潰,則會建立不太有用的堆棧跟蹤)。

靜態初始化程序應以下所示:

static {
    System.loadLibrary("fubar");
}
複製代碼

若是用C ++編寫,JNI_OnLoad函數看起來應該是這樣的:

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    // Get jclass with env->FindClass.
    // Register methods with env->RegisterNatives.

    return JNI_VERSION_1_6;
}
複製代碼

要使用本機方法的「發現」,您須要以特定方式命名它們(有關詳細信息,請參閱JNI規範)。 這意味着若是方法簽名是錯誤的,那麼在第一次實際調用該方法以前,您將不會知道它。

若是您只有一個具備本機方法的類,則對System.loadLibrary的調用在該類中是有意義的。 不然你應該從應用程序進行調用,這樣你就知道它老是被加載,而且老是提早加載。

從JNI_OnLoad進行的任何FindClass調用都將解析用於加載共享庫的類加載器的上下文中的類。 一般,FindClass使用與Java堆棧頂部的方法關聯的加載器,或者若是沒有(由於線程剛剛附加),它使用「系統」類加載器。 這使得JNI_OnLoad成爲查找和緩存類對象引用的便利位置。

64-bit considerations

要支持使用64位指針的體系結構,在Java域中存儲指向本機結構的指針時,請使用long字段而不是int。

Unsupported features/backwards compatibility

支持全部JNI 1.6功能,但如下狀況除外:

  • DefineClass未實現。 Android不使用Java字節碼或類文件,所以傳入二進制類數據不起做用。

爲了向後兼容較舊的Android版本,您可能須要注意:

  • 動態查找本機函數

在Android 2.0(Eclair)以前,在搜索方法名稱時,'$'字符未正確轉換爲「_00024」。 解決此問題須要使用顯式註冊或將本機方法移出內部類。

  • 分離線程

在Android 2.0(Eclair)以前,不可能使用pthread_key_create析構函數來避免「退出前必須分離線程」檢查。 (運行時也使用了一個pthread鍵析構函數,因此它首先要看哪一個被調用。)

  • 弱全局引用

在Android 2.2(Froyo)以前,沒有實現弱全局引用。 較舊的版本會強烈拒絕使用它們的嘗試。 您可使用Android平臺版本常量來測試支持。 在Android 4.0(Ice Cream Sandwich)以前,弱全局引用只能傳遞給NewLocalRef,NewGlobalRef和DeleteWeakGlobalRef。 (該規範強烈鼓勵程序員在對它們作任何事情以前建立對弱全局變量的硬引用,因此這不該該是任何限制。) 從Android 4.0(Ice Cream Sandwich)開始,弱全局引用能夠像任何其餘JNI引用同樣使用。

  • 本地引用

直到Android 4.0(冰淇淋三明治),本地引用其實是直接指針。 Ice Cream Sandwich添加了支持更好的垃圾收集器所需的間接,但這意味着在舊版本中沒法檢測到大量JNI錯誤。在Android 8.0以前的Android版本中,本地引用的數量限制爲特定於版本的限制。 從Android 8.0開始,Android支持無限制的本地引用。

  • 使用GetObjectRefType肯定引用類型

直到Android 4.0(冰淇淋三明治),因爲使用直接指針(見上文),才能正確實現GetObjectRefType。 相反,咱們使用了一種啓發式方法,按順序查看弱全局表,參數,本地表和全局表。 第一次找到你的直接指針時,它會報告你的引用是它正在檢查的類型。 這意味着,例如,若是您在全局jclass上調用了GetObjectRefType,該jclass剛好與做爲靜態本機方法的隱式參數傳遞的jclass相同,那麼您將得到JNILocalRefType而不是JNIGlobalRefType。

常見問題:爲何我會收到UnsatisfiedLinkError?

在處理本機代碼時,看到這樣的故障並不罕見:

java.lang.UnsatisfiedLinkError: Library foo not found
複製代碼

在某些狀況下,它意味着它所說的 - 找不到庫。 在其餘狀況下,庫存在但沒法經過dlopen(3)打開,而且能夠在異常的詳細消息中找到失敗的詳細信息。

您可能遇到「未找到庫」例外的常見緣由:

  • 該庫不存在或應用程序沒法訪問。使用adb shell ls -l <path>檢查其存在和權限。

  • 該庫不是使用NDK構建的。這可能致使對設備上不存在的函數或庫的依賴性。

另外一類UnsatisfiedLinkError失敗以下:

java.lang.UnsatisfiedLinkError: myfunc at Foo.myfunc(Native Method) at Foo.main(Foo.java:10)
複製代碼

在logcat中,您將看到:

W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
複製代碼

這意味着運行時試圖找到匹配的方法可是不成功。 一些常見的緣由是:

  • 該庫未加載。 檢查logcat輸出以獲取有關庫加載的消息。

  • 因爲名稱或簽名不匹配,找不到該方法。 這一般是由:

    對於惰性方法查找,沒法使用extern「C」和適當的可見性(JNIEXPORT)聲明C ++函數。 請注意,在冰淇淋三明治以前,JNIEXPORT宏不正確,所以使用帶有舊jni.h的新GCC將不起做用。 您可使用arm-eabi-nm查看庫中出現的符號; 若是它們看起來很糟糕(相似於_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass而不是Java_Foo_myfunc),或者若是符號類型是小寫的't'而不是大寫的'T',那麼你須要調整聲明。

    對於顯式註冊,輸入方法簽名時會出現輕微錯誤。 確保您傳遞給註冊調用的內容與日誌文件中的簽名匹配。 請記住,'B'是字節,'Z'是布爾值。 簽名中的類名組件以'L'開頭,以';'結尾,使用'/'分隔包/類名,並使用''分隔內部類名稱(Ljava / util / Map Entry;,好比說 )。

常見問題:爲何FindClass找不到個人class?

(大多數建議一樣適用於使用GetMethodID或GetStaticMethodID查找方法的失敗,或者使用GetFieldID或GetStaticFieldID的字段。)

確保類名字符串具備正確的格式。 JNI類名以包名開頭,並以斜槓分隔,例如java / lang / String。 若是你正在查找一個數組類,你須要從適當數量的方括號開始,而且還必須用'L'和';'包裝類,因此String的一維數組將是[Ljava/lang/String;。 若是您正在查找內部類,請使用「$」而不是「.」。 一般,在.class文件上使用javap是查找類的內部名稱的好方法。

若是您正在使用ProGuard,請確保ProGuard沒有刪除您的class。 若是您的類/方法/字段僅用於JNI,則會發生這種狀況。

若是類名看起來正確,則可能會遇到類加載器問題。 FindClass但願在與您的代碼關聯的類加載器中啓動類搜索。 它檢查調用堆棧,它看起來像:

Foo.myfunc(Native Method) 
Foo.main(Foo.java:10)
複製代碼

最頂層的方法是Foo.myfunc。 FindClass找到與Foo類關聯的ClassLoader對象並使用它。

這一般會作你想要的。 若是您本身建立一個線程(可能經過調用pthread_create而後將其與AttachCurrentThread一塊兒附加),您可能會遇到麻煩。 如今您的應用程序中沒有堆棧幀。 若是今後線程調用FindClass,JavaVM將從「system」類加載器開始,而不是與應用程序關聯的類加載器,所以嘗試查找特定於應用程序的類將失敗。

有幾種方法能夠解決這個問題:

  • 在JNI_OnLoad中進行一次FindClass查找,並緩存類引用以供之後使用。 做爲執行JNI_OnLoad的一部分而進行的任何FindClass調用都將使用與調用System.loadLibrary的函數關聯的類加載器(這是一個特殊規則,用於使庫初始化更方便)。 若是您的應用程序代碼正在加載庫,則FindClass將使用正確的類加載器。

  • 經過聲明本機方法獲取Class參數而後傳遞Foo.class,將類的實例傳遞給須要它的函數。

  • 在某個地方緩存對ClassLoader對象的引用,並直接發出loadClass調用。 這須要一些努力。

常見問題:如何與本機代碼共享原始數據?

您可能會發現本身須要從託管代碼和本機代碼訪問大型原始數據緩衝區。 常見示例包括操縱位圖或聲音樣本。 有兩種基本方法。

您能夠將數據存儲在byte []中。 這容許從託管代碼進行很是快速的訪問。 可是,在本機方面,您沒法保證無需複製便可訪問數據。 在某些實現中,GetByteArrayElements和GetPrimitiveArrayCritical將返回託管堆中原始數據的實際指針,但在其餘實現中,它將在本機堆上分配緩衝區並複製數據。

另外一種方法是將數據存儲在直接字節緩衝區中。 這些可使用java.nio.ByteBuffer.allocateDirect或JNI NewDirectByteBuffer函數建立。 與常規字節緩衝區不一樣,存儲不在託管堆上分配,而且始終能夠直接從本機代碼訪問(使用GetDirectBufferAddress獲取地址)。 根據直接字節緩衝區訪問的實現方式,從託管代碼訪問數據可能很是慢。

選擇使用哪一個取決於兩個因素:

  • 大多數數據訪問是否會發生在用Java或C / C ++編寫的代碼中?

  • 若是數據最終傳遞給系統API,那麼它必須採用什麼形式? (例如,若是數據最終傳遞給採用byte []的函數,則在直接ByteBuffer中進行處理多是不明智的。)

若是沒有明確的贏家,請使用直接字節緩衝區。 對它們的支持直接構建在JNI中,而且在未來的版本中性能應該獲得改善。

JNI數據類型

extern "C"
JNIEXPORT jint
JNICALL
Java_com_dodola_traphooks_MainActivity_intFromJNI(
        JNIEnv *env,
        jobject) {
    int result = add(1, 2);
    ALOG("%d=====", result);
    return result;
}
複製代碼

JNIEXPORT

在 Windows 中,定義爲__declspec(dllexport)。由於Windows編譯 dll 動態庫規定,若是動態庫中的函數要被外部調用,須要在函數聲明中添加此標識,表示將該函數導出在外部能夠調用。

在 Linux/Unix/Mac os/Android 這種 Like Unix系統中,定義爲__attribute__ ((visibility ("default")))

GCC 有個visibility屬性, 該屬性是說, 啓用這個屬性:

  • 當-fvisibility=hidden時

    動態庫中的函數默認是被隱藏的即 hidden. 除非顯示聲明爲__attribute__((visibility("default"))).

  • 當-fvisibility=default時

    動態庫中的函數默認是可見的.除非顯示聲明爲__attribute__((visibility("hidden"))).

JNICALL

在類Unix中無定義,在Windows中定義爲:_stdcall ,一種函數調用約定。

【注意】:類Unix系統中這兩個宏能夠省略不加。

JNIEnv

其中JNIEnv類型實際上表明瞭Java環境,經過這個JNIEnv* 指針,就能夠對Java端的代碼進行操做。例如,建立Java類中的對象,調用Java對象的方法,獲取Java對象中的屬性等等。JNIEnv的指針會被JNI傳入到本地方法的實現函數中來對Java端的代碼進行操做。

android NDK開發

JNI_OnLoad & JNI_UnLoad

JNI在加載時,會調用JNI_OnLoad,而卸載時會調用JNI_UnLoad,因此咱們能夠經過在JNI_OnLoad裏面註冊咱們的native函數來實現JNI。經過重寫JNI_OnLoad(),在JNI_OnLoad()中將函數註冊到Android中,以便能經過Java訪問。

pthread_once()

某個事件在整個程序中僅執行一次,不肯定是那個線程執行。在多線程環境中,有些事僅須要執行一次。一般當初始化應用程序時,能夠比較容易地將其放在main函數中。但當你寫一個庫時,就不能在main裏面初始化了,你能夠用靜態初始化,但使用一次初始化(pthread_once)會比較容易些。

int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
功能:本函數使用初值爲PTHREAD_ONCE_INIT的once_control變量保證init_routine()函數在本進程執行序列中僅執行一次。
複製代碼

在多線程編程環境下,儘管pthread_once()調用會出如今多個線程中,init_routine()函數僅執行一次,究竟在哪一個線程中執行是不定的,是由內核調度來決定。 Linux Threads使用互斥鎖和條件變量保證由pthread_once()指定的函數執行且僅執行一次,而once_control表示是否執行過。 若是once_control的初值不是PTHREAD_ONCE_INIT(Linux Threads定義爲0),pthread_once() 的行爲就會不正常。 在LinuxThreads中,實際"一次性函數"的執行狀態有三種:NEVER(0)、IN_PROGRESS(1)、DONE (2),若是once初值設爲1,則因爲全部pthread_once()都必須等待其中一個激發"已執行一次"信號,所以全部pthread_once ()都會陷入永久的等待中;若是設爲2,則表示該函數已執行過一次,從而全部pthread_once()都會當即返回0。

#include<iostream>
#include<pthread.h>
using namespace std;
 
pthread_once_t once = PTHREAD_ONCE_INIT;
 
void once_run(void)
{
        cout<<"once_run in thread "<<(unsigned int )pthread_self()<<endl;
}
 
void * child1(void * arg)
{
        pthread_t tid =pthread_self();
        cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;
        pthread_once(&once,once_run);
        cout<<"thread "<<tid<<" return"<<endl;
}
 
 
void * child2(void * arg)
{
        pthread_t tid =pthread_self();
        cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;
        pthread_once(&once,once_run);
        cout<<"thread "<<tid<<" return"<<endl;
}
 
int main(void)
{
        pthread_t tid1,tid2;
        cout<<"hello"<<endl;
        pthread_create(&tid1,NULL,child1,NULL);
        pthread_create(&tid2,NULL,child2,NULL);
        sleep(10);
        cout<<"main thread exit"<<endl;
        return 0;
}

執行結果:

hello
thread 3086535584 enter
once_run in thread 3086535584
thread 3086535584 return
thread 3076045728 enter
thread 3076045728 return
main thread exit
複製代碼

pthread_key_t和pthread_key_create()

在多線程程序中,全部線程共享程序中的變量。如今有一全局變量,全部線程均可以使用它,改變它的值。而若是每一個線程但願能單獨擁有它,那麼就須要使用線程存儲了。表面上看起來這是一個全局變量,全部線程均可以使用它,而它的值在每個線程中又是單獨存儲的。這就是線程存儲的意義。

線程存儲的具體用法。

  1. 建立一個類型爲 pthread_key_t 類型的變量。

  2. 調用 pthread_key_create() 來建立該變量。該函數有兩個參數,第一個參數就是上面聲明的 pthread_key_t 變量,第二個參數是一個清理函數,用來在線程釋放該線程存儲的時候被調用。該函數指針能夠設成 NULL ,這樣系統將調用默認的清理函數。

  3. 當線程中須要存儲特殊值的時候,能夠調用 pthread_setspcific() 。該函數有兩個參數,第一個爲前面聲明的 pthread_key_t 變量,第二個爲 void* 變量,這樣你能夠存儲任何類型的值。

  4. 若是須要取出所存儲的值,調用 pthread_getspecific() 。該函數的參數爲前面提到的 pthread_key_t 變量,該函數返回 void * 類型的值。

下面是前面提到的函數的原型:

int pthread_setspecific(pthread_key_t key, const void *value);

void *pthread_getspecific(pthread_key_t key);

int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
複製代碼

下面是一個如何使用線程存儲的例子:

#include <malloc.h>
#include <pthread.h>
#include <stdio.h>

static pthread_key_t thread_log_key;

void write_to_thread_log (const char* message)
{
	FILE* thread_log = (FILE*) pthread_getspecific (thread_log_key);
	fprintf (thread_log, 「%s\n」, message);
}

/* Close the log file pointer THREAD_LOG. */
void close_thread_log (void* thread_log)
{
	fclose ((FILE*) thread_log);	
}

void* thread_function (void* args)
{
	char thread_log_filename[20];
	FILE* thread_log;
	/* Generate the filename for this thread’s log file. */
	sprintf (thread_log_filename, 「thread%d.log」, (int) pthread_self ());
	/* Open the log file. */
	thread_log = fopen (thread_log_filename, 「w」);
	/* Store the file pointer in thread-specific data under thread_log_key. */
	pthread_setspecific (thread_log_key, thread_log);
	write_to_thread_log (「Thread starting.」);
	/* Do work here... */
	return NULL;
}

int main ()
{
	int i;
	pthread_t threads[5];
	pthread_key_create (&thread_log_key, close_thread_log);
	/* Create threads to do the work. */
	for (i = 0; i < 5; ++i)
		pthread_create (&(threads[i]), NULL, thread_function, NULL);
	/* Wait for all threads to finish. */
	for (i = 0; i < 5; ++i)
		pthread_join (threads[i], NULL);
	return 0;
}  
複製代碼

JNI子線程FindClass

在進行jni開發時,Java調用C語言通常都處於主線程中的,可是使用JNI開發,不少狀況都是須要開啓子線程的(畢竟不能阻塞主線程)

void void *th_fun(void *arg) {}//是子線程的回調函數,我認爲就至關於Java裏的`Runnable`任務,可是在C語言裏是能夠傳遞參數的。
pthread_create(&tid, NULL/*不多用到*/, th_fun/*子線程回調*/, (void *) "no1"/*傳遞給子線程的參數*/);
複製代碼

有時候在子線程會去調用Java方法,那麼如何調用尼?通常咱們都會經過env->FIndClass來調用,可是如何在子線程回調函數裏拿到env尼?將env設爲全局引用,這是一個解決方案,可是env本就是與線程相關的,若是設爲全局引用給其餘線程調用,這樣就搞混亂了,因此很差。那麼如何解決尼?其實咱們能夠經過JavaVM來解決,JavaVM表明的是Java虛擬機,全部工做都是從JavaVM開始的,每一個Java程序表明一個JavaVM,Android裏每一個Android程序都的JavaVM都是同樣的。解決方案以下:

static JavaVM *javaVM;

//動態庫加載時會執行
//兼容Android SDK 2.2以後,2.2沒有這個函數
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    LOGI("%s", "JNI_OnLoad");
    javaVM = vm;
    return JNI_VERSION_1_4;
}
void *th_fun(void *arg) {
    JNIEnv *env = NULL;
    int isAttacked = 0;
    int status = (*javaVM)->GetEnv(javaVM, (void **) &env, JNI_VERSION_1_4);
    if (status < 0) {
        isAttacked = 1;
        (*javaVM)->AttachCurrentThread(javaVM, &env, NULL);
    }
}
複製代碼

有時候會在子線程去調用Java類,可是在咱們建立的子線程(經過pthread_create建立)中調用FindClass查找非系統類時會失敗(查找系統類不會失敗),返回值爲NULL,爲何尼?這是由於經過AttachCurrentThread附加到虛擬機的線程在查找類時只會經過系統類加載器進行查找,不會經過應用類加載器進行查找,所以能夠加載系統類,可是不能加載非系統類,如本身在java層定義的類會返回NULL。

那麼如何解決尼?主要有如下兩個方案

獲取classLoader,經過調用classLoader的loadClass來加載自定義類。適合自定義類比較多的狀況

在主線程建立一個全局的自定義類引用。適合自定義類比較少的狀況

#include <jni.h>
#include <pthread.h>
#include <android/log.h>
#include <stdio.h>
#include <unistd.h>

#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"dadou",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"dadou",FORMAT,##__VA_ARGS__);


static JavaVM *javaVM;
static jobject class_loader_obj_ = NULL;
static jmethodID find_class_mid_ = NULL;
static jclass global_ref = NULL;

//動態庫加載時會執行
//兼容Android SDK 2.2以後,2.2沒有這個函數
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    LOGI("%s", "JNI_OnLoad");
    javaVM = vm;
    LOGI("a=%d,b=%d", vm == NULL, javaVM == NULL);
    //--------------------------------------------方案一--------------------------------------------
// JNIEnv *env = NULL;
// int status = (*javaVM)->GetEnv(javaVM, (void **) &env, JNI_VERSION_1_4);
// if (status == JNI_OK) {//我認爲最好在JNI_OnLoad裏拿到classLoader的全局引用
// jclass classLoaderClass = (*env)->FindClass(env, "java/lang/ClassLoader");
// jclass adapterClass = (*env)->FindClass(env, "com/example/thread/UUIDUtils");
// if (adapterClass) {
// jmethodID getClassLoader = (*env)->GetStaticMethodID(env, adapterClass,
// "getClassLoader",
// "()Ljava/lang/ClassLoader;");
// jobject obj = (*env)->CallStaticObjectMethod(env, adapterClass, getClassLoader);
// class_loader_obj_ = (*env)->NewGlobalRef(env, obj);
// find_class_mid_ = (*env)->GetMethodID(env, classLoaderClass, "loadClass",
// "(Ljava/lang/String;)Ljava/lang/Class;");
// (*env)->DeleteLocalRef(env, classLoaderClass);
// (*env)->DeleteLocalRef(env, adapterClass);
// (*env)->DeleteLocalRef(env, obj);
// }
// }
    //----------------------------------------------------------------------------------------------
    return JNI_VERSION_1_4;
}

//子線程的回調
/**
 * 在子線程中,不能經過env->FindClass來獲取自定義類,(*env)->FindClass(env, "com/example/thread/UUIDUtils");返回NULL,
 * (*env)->FindClass(env,"java/lang/String");可以正確的返回
 * 解決方案一:獲取classLoader,經過調用classLoader的loadClass來加載自定義類。適合自定義類比較多的狀況
 * 解決方案二:在主線程建立一個全局的自定義類引用。適合自定義類比較少的狀況
 * @param arg
 * @return
 */
void *th_fun(void *arg) {
    JNIEnv *env = NULL;
    int isAttacked = 0;
    int status = (*javaVM)->GetEnv(javaVM, (void **) &env, JNI_VERSION_1_4);
    if (status < 0) {
        isAttacked = 1;
        (*javaVM)->AttachCurrentThread(javaVM, &env, NULL);
    }
    jclass clazz = NULL;
    //--------------------------------------------方案一--------------------------------------------
// jstring class_name = (*env)->NewStringUTF(env, "com/example/thread/UUIDUtils");
// clazz = (*env)->CallObjectMethod(env, class_loader_obj_, find_class_mid_,
// class_name);
// (*env)->DeleteLocalRef(env, class_name);
// if (clazz != NULL) {
// jmethodID get_mid = (*env)->GetStaticMethodID(env, clazz, "get",
// "()Ljava/lang/String;");
// jobject uuid = (*env)->CallStaticObjectMethod(env, clazz, get_mid);
// char *uuid_cstr = (char *) (*env)->GetStringUTFChars(env, uuid, NULL);
// LOGI("uuid : %s", uuid_cstr);
// (*env)->ReleaseStringUTFChars(env, uuid, uuid_cstr);
// }
    //----------------------------------------------------------------------------------------------
    //--------------------------------------------方案二--------------------------------------------
    if (global_ref != NULL) {
        jmethodID get_mid = (*env)->GetStaticMethodID(env, global_ref, "get",
                                                      "()Ljava/lang/String;");
        jobject uuid = (*env)->CallStaticObjectMethod(env, global_ref, get_mid);
        char *uuid_cstr = (char *) (*env)->GetStringUTFChars(env, uuid, NULL);
        LOGI("uuid : %s", uuid_cstr);
        (*env)->ReleaseStringUTFChars(env, uuid, uuid_cstr);
    }
    //----------------------------------------------------------------------------------------------

    char *no = (char *) arg;
    int i = 0;
    for (i = 0; i < 5; i++) {
        LOGI("thread %s, i:%d", no, i);
        if (i == 4) {
            if (class_loader_obj_ != NULL) {
                (*env)->DeleteGlobalRef(env, class_loader_obj_);
            }
            //採用方案二時須要釋放全局引用
            //---------------釋放---------
            if (global_ref != NULL) {
                LOGI("%s", "開始釋放全局引用")
                (*env)->DeleteGlobalRef(env, global_ref);
            }
            //----------------------------

            //下面的函數必須最後執行,在後面再使用env會報錯
            if (isAttacked == 1) {
                //解除關聯
                (*javaVM)->DetachCurrentThread(javaVM);//必須在離開當前線程以前執行
            }
            pthread_exit((void *) 0);
        }
        sleep(1);
    }

}


//JavaVM 表明的是Java虛擬機,全部工做都是從JavaVM開始的,每一個Java程序表明一個JavaVM,Android裏每一個Android程序都的JavaVM都是同樣的
//能夠經過JavaVM獲取到每一個線程關聯的JNIEnv


//如何獲取JavaVM?
//1.在JNI_OnLoad函數中獲取
//2.(*env)->GetJavaVM(env,&javaVM);
//每一個線程都有獨立的JNIEnv
JNIEXPORT jstring JNICALL Java_com_example_thread_MainActivity_stringFromJNI(
        JNIEnv *env, jobject /* this */ object) {
    char str[] = "Hello from C";
    jclass clazz = (*env)->FindClass(env, "com/example/thread/UUIDUtils");
    global_ref = (*env)->NewGlobalRef(env, clazz);
    pthread_t tid;//子線程id
    //建立一個子線程
    pthread_create(&tid, NULL/*不多用到*/, th_fun/*子線程回調*/, (void *) "no1"/*傳遞給子線程的參數*/);
    return (*env)->NewStringUTF(env, str);
}
複製代碼
public class UUIDUtils {
 public static ClassLoader getClassLoader() {
  return UUIDUtils.class.getClassLoader();
 }
 public static String get(){
  return UUID.randomUUID().toString();
 }
}
複製代碼

若是您自行建立線程(可能經過調用 pthread_create,而後使用 AttachCurrentThread 進行附加),可能會遇到麻煩。如今您的應用中沒有堆棧幀。若是今後線程調用 FindClass,JavaVM 會在「系統」類加載器(而不是與應用關聯的類加載器)中啓動,所以嘗試查找特定於應用的類將失敗。

您能夠經過如下幾種方法來解決此問題:

在 JNI_OnLoad 中執行一次 FindClass 查找,而後緩存類引用以供往後使用。在執行 JNI_OnLoad 過程當中發出的任何 FindClass 調用都會使用與調用 System.loadLibrary 的函數關聯的類加載器(這是一條特殊規則,用於更方便地進行庫初始化)。若是您的應用代碼要加載庫,FindClass 會使用正確的類加載器。

// Java層的本地方法的容器類
#define JNI_CLASS_TEXTURE_CAPTURE "com/zpw/sdk/sink/player/TextureCapture"

// Java層的方法名,簽名,native 方法體
static JNINativeMethod g_methods[] = {
    {"nativeInit",          "()V",          (void *)nativeInit},
    {"nativeStart",         "(IIIZ)I",      (void *)nativeStart},
    {"nativeStop",          "()I",          (void *)nativeStop},
    {"nativeDraw",          "(IIIJ[F)I",    (void *)nativeDraw}
};

jclass cls = (*env)->FindClass(env, JNI_CLASS_TEXTURE_CAPTURE);
(*env)->RegisterNatives(env, cls, g_methods, sizeof(g_methods) / sizeof(g_methods[0]));
(*pEnv)->UnregisterNatives(pEnv, cls);
複製代碼

經過聲明原生方法來獲取 Class 參數,而後傳入 Foo.class,從而將類的實例傳遞給須要它的函數。

在某個便捷位置緩存對 ClassLoader 對象的引用,而後直接發出 loadClass 調用。

JavaVM.AttachCurrentThread() & JavaVM.DetachCurrentThread()

不少時候,你的native代碼創建本身的線程(好比創建線程監聽),並在合適的時候回調 Java 代碼,在線程中沒辦法直接獲取JNIEnv,此時須要將JavaVM保存在全局,獲取JNIEnv的實例須要把你的線程 Attach到JavaVM上去,調用的方法是 JavaVM::AttachCurrentThread

JNIEnv* env;
GetJVM()->AttachCurrentThread(&env, nullptr);
複製代碼

使用完以後你 須要調用 JavaVM::DetachCurrentThread函數解綁線程。

GetJVM()->DetachCurrentThread();
複製代碼

須要注意的是對於一個已經綁定到JavaVM上的線程調用AttachCurrentThread不會有任 何影響。若是你的線程已經綁定到了JavaVM上,你還能夠經過調用JavaVM::GetEnv獲取 JNIEnv,若是你的線程沒有綁定,這個函數返回JNI_EDETACHED。

封裝一個 智能指針類自動完成這些操做:

class JNIEnvPtr {
public:
    JNIEnvPtr() : env_{nullptr}, need_detach_{false} {
        if (GetJVM()->GetEnv((void**) &env_, JNI_VERSION_1_6) ==
            JNI_EDETACHED) {
            GetJVM()->AttachCurrentThread(&env_, nullptr);
            need_detach_ = true;
        }
    }

    ~JNIEnvPtr() {
        if (need_detach_) {
            GetJVM()->DetachCurrentThread();
        }
    }

    JNIEnv* operator->() {
        return env_;
    }

private:
    JNIEnvPtr(const JNIEnvPtr&) = delete;
    JNIEnvPtr& operator=(const JNIEnvPtr&) = delete;

private:
    JNIEnv* env_;
    bool need_detach_;
};
複製代碼

這個類在構造函數中調用AttachCurrentThread在析構中調用DetachCurrentThread,然 後重載->操做符。你能夠像下面這樣使用這個工具類。

NativeClass::NativeMethod() {
    JNIEnvPtr env;
    env->CallVoidMethod(instance, method, args...);
}
複製代碼

JavaVM.ExceptionCheck() & JavaVM.ExceptionClear()

異常檢測

處理異常狀況從檢測開始,並找出是否發生異常

  • 函數返回後檢查異常的發生
  • 檢查特殊功能返回值

JNI函數(如FindClass())在找不到特定類時返回特殊值。 表面是ClassCircularityError,OutOfmemoryError,ClassFormatError或NoClassDefFoundError中的任何一個異常。 FindClass()所作的是,若是出現上述任何異常,它將返回NULL。 所以,咱們能夠檢查返回的值並採起適當的步驟來處理這種狀況。

jclass jcls =
   env->FindClass("org/jnidemo/SomeClass");
{
   /* Handle exception here or free up any resources held
      Exception remains pending until control returns back
      to the Java code.
   */
   return;
}
複製代碼

可是,有些狀況下,當本機代碼嘗試訪問超出其數組大小的Java數組的元素而且JVM拋出ArrayIndexOutOfBoundsException時,沒法返回標記異常的值,例如,數組超出綁定異常。 在這種狀況下,咱們能夠在異常發生時調用Java對象的函數。 在本機代碼中,咱們能夠作的是在本機函數調用以後調用ExceptionOccurred()或ExceptionCheck()JNI函數。 ExceptionOccurred()返回異常對象的引用,ExceptionCheck()分別返回JNI_TRUE或JNI_FALSE,分別是異常是否發生。

jthrowable flag = env->ExceptionOccurred();
{
   /* Handle exception here or free up any resources held
      Exception remains pending until control returns back
      to the Java code.
   */
   return;
}

jboolean flag = env->ExceptionCheck();
if (flag) {
   /* Handle exception here or free up any resources held
      Exception remains pending until control returns back
      to the Java code.
   */
   return;
}
複製代碼

一旦檢測到異常,咱們能夠:

  • 當控制返回到Java代碼時處理它
jboolean flag = env->ExceptionCheck();
if (flag) {
   /* Handle exception here or free up any resources held
      Exception remains pending until control returns back
      to the Java code.
   */
   return;
}
複製代碼
  • 在本機代碼中處理它
jboolean flag = env->ExceptionCheck();
if (flag) {
   env->ExceptionClear();

   /* code to handle exception */
}
複製代碼
  • 在本機代碼中處理它並傳播Java的新異常

    jint Throw(jthrowable obj)

    jint ThrowNew(jclass clazz, const char *message)

這裏值得一提的是,在當即遇到throw方法時,控制不會轉移到Java代碼; 相反,它會一直等到遇到return語句。 throw方法和return語句之間能夠有代碼行。 throw和JNI函數在成功時返回零,不然返回負值。

if(...){
   jclass jcls =
      env->FindClass("java/lang/Exception");
   jboolean flag = env->ExceptionCheck();
   if (flag) {
      env->ExceptionClear();
      /* code to handle exception */
   }
   env->ThrowNew(jcls, "error message");
   return;
}
複製代碼

char * strdup(const char *str1)

返回指向以null結尾的字節字符串的指針,該字符串是str1指向的字符串的副本。必須將返回的指針傳遞給free以免內存泄漏。

若是發生錯誤,則返回空指針而且能夠設置errno。

做爲動態內存TR的全部函數,只有在實現定義__STDC_ALLOC_LIB__且用戶在包含string.h以前將__STDC_WANT_LIB_EXT2__定義爲整數常量1時,才保證strdup可用。

char strstr( const char str, const char* substr )

查找str指向的以null結尾的字節字符串中substr指向的以null結尾的字節字符串的第一個匹配項。不比較終止空字符。

若是str或substr不是指向以null結尾的字節字符串的指針,則行爲是未定義的。

指向str中找到的子字符串的第一個字符的指針,若是沒有找到這樣的子字符串則指向NULL。 若是substr指向空字符串,則返回str。

#include <string.h>
#include <stdio.h>
 
void find_str(char const* str, char const* substr) 
{
    char* pos = strstr(str, substr);
    if(pos) {
        printf("found the string '%s' in '%s' at position: %ld\n", substr, str, pos - str);
    } else {
        printf("the string '%s' was not found in '%s'\n", substr, str);
    }
}
 
int main(void) 
{
    char* str = "one two three";
    find_str(str, "two");
    find_str(str, "");
    find_str(str, "nine");
    find_str(str, "n");
 
    return 0;
}
複製代碼

Output:

found the string 'two' in 'one two three' at position: 4
found the string '' in 'one two three' at position: 0
the string 'nine' was not found in 'one two three'
found the string 'n' in 'one two three' at position: 1
複製代碼

int sscanf( const char *buffer, const char *format, ... )

buffer指向要從中讀取的以null結尾的字符串的指針

format指向以null結尾的字符串的指針,指定如何讀取輸入。

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
#include <stddef.h>
#include <locale.h>
 
int main(void)
{
    int i, j;
    float x, y;
    char str1[10], str2[4];
    wchar_t warr[2];
    setlocale(LC_ALL, "en_US.utf8");
 
    char input[] = "25 54.32E-1 Thompson 56789 0123 56ß水";
    /* parse as follows:
       %d: an integer
       %f: a floating-point value
       %9s: a string of at most 9 non-whitespace characters
       %2d: two-digit integer (digits 5 and 6)
       %f:  a floating-point value (digits 7, 8, 9)
       %*d: an integer which isn't stored anywhere ' ': all consecutive whitespace %3[0-9]: a string of at most 3 decimal digits (digits 5 and 6) %2lc: two wide characters, using multibyte to wide conversion */ int ret = sscanf(input, "%d%f%9s%2d%f%*d %3[0-9]%2lc", &i, &x, str1, &j, &y, str2, warr); printf("Converted %d fields:\ni = %d\nx = %f\nstr1 = %s\n" "j = %d\ny = %f\nstr2 = %s\n" "warr[0] = U+%x warr[1] = U+%x\n", ret, i, x, str1, j, y, str2, warr[0], warr[1]); #ifdef __STDC_LIB_EXT1__ int n = sscanf_s(input, "%d%f%s", &i, &x, str1, (rsize_t)sizeof str1); // writes 25 to i, 5.432 to x, the 9 bytes "thompson\0" to str1, and 3 to n. #endif } 複製代碼

Output:

Converted 7 fields:
i = 25
x = 5.432000
str1 = Thompson
j = 56
y = 789.000000
str2 = 56
warr[0] = U+df warr[1] = U+6c34
複製代碼

CMake

Android Studio 2.2 及更高版本,使用 NDK 和 CMake 將 C 及 C++ 代碼編譯到原生庫中。以後,Android Studio 會使用 IDE 的集成構建系統 Gradle 將您的庫封裝到 APK。

CMake 是一個跨平臺的安裝(編譯)工具,能夠用簡單的語句來描述全部平臺的安裝(編譯過程)。他可以輸出各類各樣的 Makefile 或者 project 文件,CMake 並不直接建構出最終的軟件,而是產生標準的建構檔(如 Makefile 或 projects)。

構建 CMake

要指示 CMake 從原生源代碼建立一個原生庫,請將 cmake_minimum_required() 和 add_library() 命令添加到您的構建腳本中:

# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.

cmake_minimum_required(VERSION 3.4.1)

# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add.library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.

add_library( # Specifies 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 )
複製代碼

使用 add_library() 向您的 CMake 構建腳本添加源文件或庫時,Android Studio 還會在您同步項目後在 Project 視圖下顯示關聯的標頭文件。不過,爲了確保 CMake 能夠在編譯時定位您的標頭文件,您須要將 include_directories() 命令添加到 CMake 構建腳本中並指定標頭的路徑:

add_library(...)

# Specifies a path to native header files.
include_directories(src/main/cpp/include/)
複製代碼

CMake 使用如下規範來爲庫文件命名:

lib庫名稱.so
複製代碼

例如,若是您在構建腳本中指定「native-lib」做爲共享庫的名稱,CMake 將建立一個名稱爲 libnative-lib.so 的文件。不過,在 Java 代碼中加載此庫時,請使用您在 CMake 構建腳本中指定的名稱:

static {
    System.loadLibrary(「native-lib」);
}
複製代碼

注:若是您在 CMake 構建腳本中重命名或移除某個庫,您須要先清理項目,Gradle 隨後纔會應用更改或者從 APK 中移除舊版本的庫。要清理項目,請從菜單欄中選擇 Build > Clean Project。

Android Studio 會自動將源文件和標頭添加到 Project 窗格的 cpp 組中。使用多個 add_library() 命令,您能夠爲 CMake 定義要從其餘源文件構建的更多庫。

添加 NDK API

Android NDK 提供了一套實用的原生 API 和庫。經過將 NDK 庫包含到項目的 CMakeLists.txt 腳本文件中,您可使用這些 API 中的任意一種。

預構建的 NDK 庫已經存在於 Android 平臺上,所以,您無需再構建或將其打包到 APK 中。因爲 NDK 庫已是 CMake 搜索路徑的一部分,您甚至不須要在您的本地 NDK 安裝中指定庫的位置 - 只須要向 CMake 提供您但願使用的庫的名稱,並將其關聯到您本身的原生庫。

將 find_library() 命令添加到您的 CMake 構建腳本中以定位 NDK 庫,並將其路徑存儲爲一個變量。您可使用此變量在構建腳本的其餘部分引用 NDK 庫。如下示例能夠定位 Android 特定的日誌支持庫並將其路徑存儲在 log-lib 中:

find_library( # Defines the name of the path variable that stores the
              # location of the NDK library.
              log-lib

              # Specifies the name of the NDK library that
              # CMake needs to locate.
              log )
複製代碼

爲了確保您的原生庫能夠在 log 庫中調用函數,您須要使用 CMake 構建腳本中的 target_link_libraries() 命令關聯庫:

find_library(...)

# Links your native library against one or more other native libraries.
target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the log library to the target library.
                       ${log-lib} )
複製代碼

NDK 還以源代碼的形式包含一些庫,您在構建和關聯到您的原生庫時須要使用這些代碼。您可使用 CMake 構建腳本中的 add_library() 命令,將源代碼編譯到原生庫中。要提供本地 NDK 庫的路徑,您可使用 ANDROID_NDK 路徑變量,Android Studio 會自動爲您定義此變量。

如下命令能夠指示 CMake 構建 android_native_app_glue.c,後者會將 NativeActivity 生命週期事件和觸摸輸入置於靜態庫中並將靜態庫關聯到 native-lib:

add_library( app-glue
             STATIC
             ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c )

# You need to link static libraries against your shared native library.
target_link_libraries( native-lib app-glue ${log-lib} )
複製代碼

添加其餘預構建庫

添加預構建庫與爲 CMake 指定要構建的另外一個原生庫相似。不過,因爲庫已經預先構建,您須要使用 IMPORTED 標誌告知 CMake 您只但願將庫導入到項目中:

add_library( imported-lib
             SHARED
             IMPORTED )
複製代碼

而後,您須要使用 set_target_properties() 命令指定庫的路徑,以下所示。

某些庫爲特定的 CPU 架構(或應用二進制接口 (ABI))提供了單獨的軟件包,並將其組織到單獨的目錄中。此方法既有助於庫充分利用特定的 CPU 架構,又能讓您僅使用所需的庫版本。要向 CMake 構建腳本中添加庫的多個 ABI 版本,而沒必要爲庫的每一個版本編寫多個命令,您可使用 ANDROID_ABI 路徑變量。此變量使用 NDK 支持的一組默認 ABI,或者您手動配置 Gradle 而讓其使用的一組通過篩選的 ABI。例如:

add_library(...)
set_target_properties( # Specifies the target library.
                       imported-lib

                       # Specifies the parameter you want to define.
                       PROPERTIES IMPORTED_LOCATION

                       # Provides the path to the library you want to import.
                       imported-lib/src/${ANDROID_ABI}/libimported-lib.so )
複製代碼

爲了確保 CMake 能夠在編譯時定位您的標頭文件,您須要使用 include_directories() 命令,幷包含標頭文件的路徑:

include_directories( imported-lib/include/ )
複製代碼

要將預構建庫關聯到您本身的原生庫,請將其添加到 CMake 構建腳本的 target_link_libraries() 命令中:

target_link_libraries( native-lib imported-lib app-glue ${log-lib} )
複製代碼

在您構建應用時,Gradle 會自動將導入的庫打包到 APK 中。您可使用 APK 分析器驗證 Gradle 將哪些庫打包到您的 APK 中。

將 Gradle 關聯到您的原生庫

要將 Gradle 關聯到您的原生庫,您須要提供一個指向 CMake 或 ndk-build 腳本文件的路徑。在您構建應用時,Gradle 會以依賴項的形式運行 CMake 或 ndk-build,並將共享的庫打包到您的 APK 中。Gradle 還使用構建腳原本瞭解要將哪些文件添加到您的 Android Studio 項目中,以便您能夠從 Project 窗口訪問這些文件。若是您的原生源文件沒有構建腳本,則須要先建立 CMake 構建腳本,而後再繼續。

將 Gradle 關聯到原生項目後,Android Studio 會更新 Project 窗格以在 cpp 組中顯示您的源文件和原生庫,在 External Build Files 組中顯示您的外部構建腳本。

注:更改 Gradle 配置時,請確保經過點擊工具欄中的 Sync Project 應用更改。此外,若是在將 CMake 或 ndk-build 腳本文件關聯到 Gradle 後再對其進行更改,您應當從菜單欄中選擇 Build > Refresh Linked C++ Projects,將 Android Studio 與您的更改同步。

  • Link C++ Project with Gradle
  • 手動配置 Gradle

要手動配置 Gradle 以關聯到您的原生庫,您須要將 externalNativeBuild {} 塊添加到模塊級 build.gradle 文件中,並使用 cmake {} 或 ndkBuild {} 對其進行配置:

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

  // Encapsulates your external native build configurations.
  externalNativeBuild {

    // Encapsulates your CMake build configurations.
    cmake {

      // Provides a relative path to your CMake build script.
      path "CMakeLists.txt"
    }
  }
}
複製代碼

指定可選配置

您能夠在模塊級 build.gradle 文件的 defaultConfig {} 塊中配置另外一個 externalNativeBuild {} 塊,爲 CMake 或 ndk-build 指定可選參數和標誌。與 defaultConfig {} 塊中的其餘屬性相似,您也能夠在構建配置中爲每一個產品風味重寫這些屬性。

例如,若是您的 CMake 或 ndk-build 項目定義多個原生庫,您可使用 targets 屬性僅爲給定產品風味構建和打包這些庫中的一部分。如下代碼示例說明了您能夠配置的部分屬性:

android {
  ...
  defaultConfig {
    ...
    // This block is different from the one you use to link Gradle
    // to your CMake or ndk-build script.
    externalNativeBuild {

      // For ndk-build, instead use ndkBuild {}
      cmake {

        // Passes optional arguments to CMake.
        arguments "-DANDROID_ARM_NEON=TRUE", "-DANDROID_TOOLCHAIN=clang"

        // Sets optional flags for the C compiler.
        cFlags "-D_EXAMPLE_C_FLAG1", "-D_EXAMPLE_C_FLAG2"

        // Sets a flag to enable format macro constants for the C++ compiler.
        cppFlags "-D__STDC_FORMAT_MACROS"
      }
    }
  }

  buildTypes {...}

  productFlavors {
    ...
    demo {
      ...
      externalNativeBuild {
        cmake {
          ...
          // Specifies which native libraries to build and package for this
          // product flavor. If you don't configure this property, Gradle // builds and packages all shared object libraries that you define // in your CMake or ndk-build project. targets "native-lib-demo" } } } paid { ... externalNativeBuild { cmake { ... targets "native-lib-paid" } } } } // Use this block to link Gradle to your CMake or ndk-build script. externalNativeBuild { cmake {...} // or ndkBuild {...} } } 複製代碼

指定 ABI

默認狀況下,Gradle 會針對 NDK 支持的 ABI 將您的原生庫構建到單獨的 .so 文件中,並將其所有打包到您的 APK 中。若是您但願 Gradle 僅構建和打包原生庫的特定 ABI 配置,您能夠在模塊級 build.gradle 文件中使用 ndk.abiFilters 標誌指定這些配置,以下所示:

android {
  ...
  defaultConfig {
    ...
    externalNativeBuild {
      cmake {...}
      // or ndkBuild {...}
    }

    ndk {
      // Specifies the ABI configurations of your native
      // libraries Gradle should build and package with your APK.
      abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a',
                   'arm64-v8a'
    }
  }
  buildTypes {...}
  externalNativeBuild {...}
}
複製代碼

在 Gradle 中使用 CMake 變量

將 Gradle 關聯到您的 CMake 項目後,您可配置特定 NDK 變量,以改變 CMake 構建您原生庫的方式。要將參數從模塊級 build.gradle 文件傳送到 CMake,請使用如下 DSL:

android {
...
defaultConfig {
    ...
    // This block is different from the one you use to link Gradle
    // to your CMake build script.
    externalNativeBuild {
      cmake {
        ...
        // Use the following syntax when passing arguments to variables:
        // arguments "-DVAR_NAME=ARGUMENT".
        arguments "-DANDROID_ARM_NEON=TRUE",
        // If you're passing multiple arguments to a variable, pass them together: // arguments "-DVAR_NAME=ARG_1 ARG_2" // The following line passes 'rtti' and 'exceptions' to 'ANDROID_CPP_FEATURES'. "-DANDROID_CPP_FEATURES=rtti exceptions" } } } buildTypes {...} // Use this block to link Gradle to your CMake build script. externalNativeBuild { cmake {...} } } 複製代碼

下表介紹在將 CMake 與 NDK 搭配使用時,您能夠配置的部分變量。

瞭解 CMake 構建命令

瞭解在對 Android 進行交叉編譯時,Android Studio 所使用的具體構建參數,將有助於調試 CMake 構建問題。

Android Studio 會將其用於執行 CMake 構建的構建參數保存於 cmake_build_command.txt 文件。Android Studio 會針對您應用指向的每一個應用二進制界面 (ABI),以及這些 ABI 的每一個構建類型(即發行或調試),爲每一個具體配置生成 cmake_build_command.txt 文件副本。Android Studio 隨後會將其生成的文件放置於如下目錄:

<project-root>/<module-root>/.externalNativeBuild/cmake/<build-type>/<ABI>/
複製代碼

提示:在 Android Studio 中,您可以使用鍵盤快捷鍵 (shift+shift) 快速瀏覽這些文件,並在輸入字段輸入 cmake_build_command.txt。

如下代碼片斷舉例說明用於構建指向 armeabi-v7a 架構的可調式版 hello-jni 示例的 CMake 參數。

Executable : /usr/local/google/home/{$USER}/Android/Sdk/cmake/3.6.3155560/bin/cmake
arguments :
-H/usr/local/google/home/{$USER}/Dev/github-projects/googlesamples/android-ndk/hello-jni/app/src/main/cpp
-B/usr/local/google/home/{$USER}/Dev/github-projects/googlesamples/android-ndk/hello-jni/app/.externalNativeBuild/cmake/arm7Debug/armeabi-v7a
-GAndroid Gradle - Ninja
-DANDROID_ABI=armeabi-v7a
-DANDROID_NDK=/usr/local/google/home/{$USER}/Android/Sdk/ndk-bundle
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=/usr/local/google/home/{$USER}/Dev/github-projects/googlesamples/android-ndk/hello-jni/app/build/intermediates/cmake/arm7/debug/obj/armeabi-v7a
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_MAKE_PROGRAM=/usr/local/google/home/{$USER}/Android/Sdk/cmake/3.6.3155560/bin/ninja
-DCMAKE_TOOLCHAIN_FILE=/usr/local/google/home/{$USER}/Android/Sdk/ndk-bundle/build/cmake/android.toolchain.cmake
-DANDROID_NATIVE_API_LEVEL=23
-DANDROID_TOOLCHAIN=clang
jvmArgs :
複製代碼

構建參數

下表突出顯示用於 Android 的 CMake 關鍵構建參數。這些構建參數並不是由開發者設置。相反,Android Plugin for Gradle 會根據您項目的 build.gradle 配置,設置這些參數。

CMake 中的 YASM 支持

NDK 爲構建以 YASM 編寫的彙編代碼提供 CMake 支持,以便在 x86 和 x86-64 架構上運行。YASM 是基於 NASM 彙編程序且針對 x86 和 x86-64 架構的開源彙編程序。

該程序可用於將彙編語言程序或例程與 C 代碼關聯,以便從您的彙編代碼訪問 C 庫或函數。您還能在編譯完的 C 代碼中添加簡短的彙編例程,以充分利用匯編代碼提供的更出色的機器性能。

要使用 CMake 構建彙編代碼,請在您項目的 CMakeLists.txt 中做出如下變動:

  • 調用 enable_language,且值設置爲 ASM_NASM。
  • 根據您是構建共享庫仍是可執行二進制文件來決定調用 add_library 或 add_executable。在參數中,傳入源文件列表。源文件包括 YASM 中彙編程序的 .asm 文件,以及關聯 C 庫或函數的 .c 文件。

如下片斷展現如何配置您的 CMakeLists.txt,以將 YASM 程序構建爲共享庫。

cmake_minimum_required(VERSION 3.6.0) 
enable_language(ASM_NASM) 
add_library(test-yasm SHARED jni/test-yasm.c jni/print_hello.asm)
複製代碼

Makefile

Makefile主要就是管理整個工程的編譯、連接,好比說一個.c文件你要先用GCC編譯成.o,而後多個.o再連接成可執行文件,若是你寫好一個Makefile,那麼你只須要在控制檯輸入一個make回車就行了。雖然各個廠商的Makefile語法會有寫區別,但大致是相似的,這裏就很少作區分了,本文是一個大雜燴。

通常在工程是根目錄下會有一個主Makefile文件做爲編譯入口,在控制檯輸入make後執行的就是這個文件,在子目錄中也有一個Makefile文件,每一個Makefile文件管理本身所在的目錄(固然,這不是絕對的)。

語法

符號

\: 反斜槓,指下一行還算本行,有時單行太長會進行換行,但Makefile語法上換行就是一條語句的結束了,爲了代碼閱讀方便會在行末使用一個「\」來進行換行
$:取變量的值,如var := 123 那麼 $(var)就表示var這個變量的值,也就是123(變量能夠不加括號,但通常都加)若要表示$時則用兩個$表示($$)
=: 是最基本的賦值,但值是整個文件最後一次賦值的值,如a=1 b=$(a) a=2,此時b的值爲2(比較奇葩因此通常不用)
:=:  是覆蓋以前的值,依賴與當前所在位置,如a:=1 b=$(a) a:=2,此時b的值爲1(比較常見的賦值邏輯,經常使用的賦值方式)
?=: 是若是沒有被賦值過就賦予等號後面的值,若是賦值過就再也不賦值(通常用於參數的默認值,Makefile間能夠傳參)
+=:  是添加等號後面的值,鏈接以後中間會有一個空格,如 a=abc a+=def,a的值爲abc def(通常用於添加依賴文件)
複製代碼

關鍵字

ifeq  ifneq ifdef ifndef else endif 想必這些不用解釋吧
include:包含文件,即在所在位置展開文件,和c文件包含頭文件相似,若是找不到且Makefile不會建立這個文件,那麼編譯報錯(No such file or directory)
-include:相似include,但找不到文件時不報錯
sinclude:同-include,GNU所支持的書寫方式
複製代碼

函數

函數的調用方法:很像變量的使用,也是以「$」來標識的,參數間用「,」隔開
$(<function> <arguments1>,<arguments2>... )
$(subst <from>,<to>,<text>):字符串替換,把字串<text>中的<from>字符串替換成<to>,返回替換後的字符串
$(strip <string>):去掉<string>字串中開頭和結尾的空字符
$(findstring <find>,<in>):在字串<in>中查找<find>字串,若是找到,那麼返回<find>,不然返回空字符串
$(filter <pattern>,<text>):過濾器,將<text>集中符合<pattern>的過濾出來。如$(filter %.o,a.o a.c a.h)結果爲a.o
$(dir <names...>):從文件名序列<names>中取出目錄部分。目錄部分是指最後一個反斜槓(「/」)之
前的部分。若是沒有反斜槓,那麼返回「./」。如$(dir src/foo.c hacks)返回值是「src/ ./」
$(notdir <names...>):從文件名序列<names>中取出文件名。即最後一個反斜槓「/」以後的部分
$(suffix <names...>):從文件名序列<names>中取出各個文件名的後綴
$(basename <names...>):從文件名序列<names>中取出各個文件名的前綴。如$(basename src/foo.c)結果爲src/foo
$(foreach <var>,<list>,<text>):把參數<list>中的單詞逐一取出放到參數<var>變量中,而後再執行<text>表達式。每一次<text>會返回一個字符串,循環結束後,<text>的所返回字符串以空格分隔鏈接成新的字符串返回。如names:= a b c d $(foreach n,$(names),$(n).o)結果爲「a.o b.o c.o d.o」
$(if <condition>,<then-part>,<else-part>):if函數,<condition>爲空時返回<then-part>不然返回<else-part>
$(wildcard <string>):基於當前目錄使用通配符列出全部文件
$(patsubst <pattern>,<replacement>,<text>):使用通配符替換字符串。如$(patsubst %.c,%.o,a.c b.c)結果爲a.o b.o
$(shell <cmd>):執行Linux的shell命令
$(lastword <names...>):返回最後一個字串。如$(lastword foo bar)返回bar
$(call <expression>,<parm1>,<parm2>,<parm3>...):函數調用,<expression>中的變量,如$(1),$(2),$(3)等,會被參數<parm1>,<parm2>,<parm3>取代。<expression>的返回值就是call函數的返回值。如$(call $(1) $(2),a,b)結果爲「a b」
$(eval <text>):將text放回Makefile文件當成Makefile腳本再解析一遍。這個有點繞,如$(eval aa:aa.c)至關於直接寫aa:aa.c,這個的意義在於aa:aa.c這個字串是能夠由Makefile腳本生成的
$(error <text ...>):產生一個致命的錯誤,<text ...>是錯誤信息
$(warning <text ...>):產生一個警告,<text ...>是警告信息
複製代碼

命令的變量

Makefile會有一些隱含規則,如.o文件依賴於.c文件,這個咱們能夠不寫,會自動調用編譯C程序的隱含規則的命令「(CC) –c(CFLAGS) $(CPPFLAGS)」來生成,這裏用到的變量值是能夠設置的(通常用來更改默認編譯器)

AR   函數庫打包程序。默認命令是「ar」。
AS  彙編語言編譯程序。默認命令是「as」。
CC  C語言編譯程序。默認命令是「cc」。
CXX  C++語言編譯程序。默認命令是「g++」。
CO  從 RCS文件中擴展文件程序。默認命令是「co」。
CPP  C程序的預處理器(輸出是標準輸出設備)。默認命令是「$(CC) –E」。
FC  Fortran 和 Ratfor 的編譯器和預處理程序。默認命令是「f77」。
GET  從SCCS文件中擴展文件的程序。默認命令是「get」。
LEX  Lex方法分析器程序(針對於C或Ratfor)。默認命令是「lex」。
PC  Pascal語言編譯程序。默認命令是「pc」。
YACC  Yacc文法分析器(針對於C程序)。默認命令是「yacc」。
YACCR  Yacc文法分析器(針對於Ratfor程序)。默認命令是「yacc –r」。
MAKEINFO  轉換Texinfo源文件(.texi)到Info文件程序。默認命令是「makeinfo」。
TEX  從TeX源文件建立TeX DVI文件的程序。默認命令是「tex」。
TEXI2DVI  從Texinfo源文件建立軍TeX DVI 文件的程序。默認命令是「texi2dvi」。
WEAVE  轉換Web到TeX的程序。默認命令是「weave」。
CWEAVE  轉換C Web 到 TeX的程序。默認命令是「cweave」。
TANGLE  轉換Web到Pascal語言的程序。默認命令是「tangle」。
CTANGLE  轉換C Web 到 C。默認命令是「ctangle」。
RM  刪除文件命令。默認命令是「rm –f」。
MAKE 即make
ARFLAGS  函數庫打包程序AR命令的參數。默認值是「rv」。
ASFLAGS  彙編語言編譯器參數。(當明顯地調用「.s」或「.S」文件時)。
CFLAGS  C語言編譯器參數。
CXXFLAGS  C++語言編譯器參數。
COFLAGS  RCS命令參數。
CPPFLAGS  C預處理器參數。( C 和 Fortran 編譯器也會用到)。
FFLAGS  Fortran語言編譯器參數。
GFLAGS  SCCS 「get」程序參數。
LDFLAGS  連接器參數。(如:「ld」)
LFLAGS  Lex文法分析器參數。
PFLAGS  Pascal語言編譯器參數。
RFLAGS  Ratfor 程序的Fortran 編譯器參數。
YFLAGS  Yacc文法分析器參數。
MAKEFILE_LIST make程序在讀取makefile文件的時候將文件名加入此變量,多個文件用空格隔開
複製代碼

顯示命令

在命令前面加@符號表示執行時不顯示命令只顯示輸出。如"@echo 這是輸出字符"在樣在控制檯只輸出"這是輸出字符"若是不加@則會輸出"echo 這是輸出字符"後換行再輸出"這是輸出字符"

相關文章
相關標籤/搜索