Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Mac下使用Eclipse實現Android中調用C/C++(NDK)基礎詳細教程

Mac下使用Eclipse實現Android中調用C/C++(NDK)基礎詳細教程

編輯:關於Android編程

需求

NDK是由谷歌娘提供的,某種意義上就是可以讓android使用c開發的第“三”方sdk,所以,正常來說eclipse是沒有配置這個東西的,當然如我所雲,我只考慮用最小的工程成本(較少的時間保證一定質量)來實現我的目標,所有我使用的是由谷歌提供的標准的ADT,下載完安裝後可以自動完成android開發的基本配置,也就是可以直接拿來寫HelloAndroid的開發工具,當然,普通的android開發也不是我這裡討論的問題,所以以下涉及到java下完成android開發的問題我默認所有人都能理解我說的東西,不會做解釋,我的解釋會放在如何使用NDK上(啰嗦的我啊願萌萌的油乎乎永安你的靈魂,普天齊明!我受到攻擊hp-250,我觸發被動天賦厚臉皮hp+2500)。

另外由於我配置完也過了有大半個月了,所以有些細節可能不會記得很清楚,也可能會遺漏一些細節,如果我日後還活著而且想起來了,我發四一定會回來修改補充這篇博客。或者在配置過程中遇到有什麼問題可以留言補充,有緣的話我一定會看到的。

Android嚴格來說是linux的一個分支,換個角度講,android就是一個被修改過的linux系統,linux系統運行直接用c寫的東西當然是沒問題的,這也是為什麼android可以允許ndk的存在的一個核心原因。而偶們平時使用的c開發版本裡邊,大方向上可能可以算作有兩種,一種是GUNC,一種是個windows用的C(原諒無知的我又忘了叫什麼了,要不簡稱wc吧),這名字不是太重點,重點是這兩種c雖然大同小異,但是卻會有嚴重的跨平台問題,gunc是個unix系統使用為主,也就是在unix環境下編譯gunc是個事半功倍的事情,wc的問題同。linux是unix的分支,所以linux下也是使用gunc的,換個角度說,我這段想表達的問題很簡單,windows下做NDK會多很多麻煩,根據我的原則以及我的開發環境考慮,我勇敢的放棄了在windows下的努力(顯然我是被折騰的不行了才放棄的,做項目跟做研究畢竟有差別,項目工程有工期的鴨梨,使用一個更穩定更可控的平台是更好的選擇),我使用的開發環境是unix的另外一個分支,mac,也就是蘋果的操作系統(顯然全名太長了我有限的腦容量是記不住的,打個mac意思一下咯)。換個角度說,android和mac都是屬於unix系統的大類中,就像香港人和深圳人要交流總比深圳人跟加州人要交流容易些一樣,mac下做android的工作也比在windows下做要容易一些(說白了就是給android做windows開發工具的人比較少——)。換個角度,linux下做ndk也會更容易些,可能某些細節不完全一樣,但大體應該是接近的。

綜述

嗯嗯,上一章其實沒什麼好看的,可以跳過,這章開始正式討論使用ndk過程中遇到(或者說需要解決的問題)。

宏觀上偶們先梳理一下邏輯,整個過程中偶們需要解決哪些問題,後續章節會逐個講述這些問題的具體解決方案(有些方案並不唯一,有興趣可以自己再研究)。

讓開發工具Eclipse知道NDK已經存在。

調用ndk首先要解決的就是ndk在哪的問題?ndk不是憑空出現的,可以到google提供ndk的地方去下載,由於網絡的原因可能有些人沒法下載到,可以百度到一些第三方的地方下載,注意使用的操作系統是mac,64位,版本肯定要最新的啦,舊的版本可能會出現有些功能不支持的問題。

下載好ndk以後當然是要讓開發工具,也就是eclipse君知道有這個東西已經存在在電腦深深的腦海裡,TA的硬盤裡,TA的內存裡,TA的廢紙簍裡。

配置ndk的開發設置(參數)。

eclipse知道怎麼從垃圾箱裡找出ndk以後,就需要配置ndk開發環境了,android調用ndk的方式一般是通過調用動態鏈接庫的方式完成的,所以所謂配置ndk開發設置,實質上是為了配置編譯器如何編譯寫好的c代碼。說真的這塊很多麻煩事,我當時是被弄得很崩潰。

實現java和c通訊的接口(以下稱ndk接口)。

顯然java無法那麼簡單就可以調用c的函數,所以需要使用ndk提供的中間接口,這個接口一式兩份,一份是用java編寫的,另一份使用C編寫。這兩份協議內容實質是一樣的,雖然兩者用的語言不一樣,就像一些國際商業合同可能會一份是中文,一份是英文一樣。

ndk提供的與標准c類似jint、jstring等“c的數據類型”(就是說,這些數據類型雖然姓“j”,但它還是一個c的數據類型。就像金剛石沒有金屬,也跟某只猩猩沒有關系;鉛筆也不是鉛做的一樣。“j”只是描述了這個數據類型具有某些特殊性質),通過這些數據類型可以通過ndk與java中的對應int類型、string類型實現自由轉換。

編譯出動態鏈接庫(顯然靜態鏈接庫也是可以的,但還是那句話,一切根據需求來)。

由於動態鏈接庫的後綴是*.so,所以下文統稱so文件(體諒下我鼠標手鍵盤爪多年打字不易)。當你的代碼寫完以後,就需要開始編譯了,編譯有兩種結果,All In or Nothing(成功或者失敗=。=)。成功的話會生成一個so文件,這個so文件可以認為是由以下三個部分編譯而成的(你寫的代碼,你調用的庫,ndk的接口),雖然編譯so文件並不需要ndk接口的java部分,但如果沒有這個部分,這個so文件是無法正常使用的,就像一輛車,沒有鑰匙是沒法開的(神馬,你會撬鎖,那確實可以用,就是成本略高)使用這個so文件配合ndk接口的java部分就可以形成一個完成的NDK動態鏈接庫了。

將動態鏈接庫添加入已有的android項目中。

到這一步就已經很接近成功了,把動態鏈接庫加入已有的項目,留意之前准備好的ndk接口中的java部分,在正常的java函數中調用它吧,相信自己,你可以的。

調試和……

調試神馬的,就不多說了,大伙都懂的,使用NDK可以考慮配合TDD的開發模式,原理不多說,顯然會跑題。

基本的配置

就像所說的第一個問題,先下載好ndk的開發包,解壓縮,記下保存的路徑,例如
“/Applications/adt-bundle-mac-x86_64-20140321/android-ndk-r9d/”。

打開你的eclipse(也就是我這裡的ADT),在ADT—Preferences-Android-NDK裡邊把這個路徑設置一下,這樣ADT就知道你的NDK在哪裡了。
這裡寫圖片描述
這裡寫圖片描述 假設現在新建了一個Android的項目,或者已經有了一個Android的項目,“HelloNDK”。
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述vcP8w/ujrNXi0flBUEvU2tTL0NC1xMqxuvKyxcTc1f3It7zT1NijrLXju/dGaW5pc2ihozxiciAvPg0KPGltZyBhbHQ9"這裡寫圖片描述" src="/uploadfile/Collfiles/20160516/2016051609211278.png" title="\" /> 這個時候你會發現你的Project裡邊多出了一個jni的文件夾,木有錯,介個文件夾就是給你放*.c*.cpp*.h這些文件的地方啦。裡邊默認生成了兩個文件,一個”IHateNDK.cpp”,一個“Android.mk”。注意,第一個文件不是必須的,裡邊什麼都沒有,可以自己另外創建,第二個文件才是必須存在的,關於cpp、mk文件的說明,後邊會有進一步的介紹,本章只討論配置的過程。
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述 點擊Project—Build All,這樣就能完成對文件的編譯了
這裡寫圖片描述 Build的結果會輸出在Console窗口裡邊
這裡寫圖片描述 編譯成功的SO文件會保存在項目工程的libs\armeabi\目錄下。 到這一步為止,編譯一個新的NDK動態鏈接庫的流程就走完了,換個角度講,剩下的就是如何調用以及如何完成代碼的部分了。

java與c++之間的通信

顯然,java的“編譯器”是不認識C的東西滴,同理,C滴東西也不認識java是神馬,所以偶們需要通過一些接口協議來完成這樣一部分,也就是實現讓java能調用C的函數,並正確地把java的對象傳給C,同時C能正確的獲得並解析Java傳過來的對象,並正確地把結果轉換為java的對象傳回去。

java的對象可以看做兩種,一種是如int、double、string這樣的值對象,另一種是自己建立的繼承自object的自定義對象(以下成為object對象)。事實上兩種對象都可以傳遞給C進行處理,但顯然後一種由於是自定義的會更復雜,作為進階內容,我就不啰嗦了,這裡就啰嗦一下普通的數值類型,以下以String類型在java與C的通訊為例,其他的數值傳遞問題可以舉一反三,或者自己百度=。=

協議的Java部分。

假設偶們現在要做的事情是輸入一個字符串,然後返回這個字符串的長度,偶們首先實現一個java版的。新建一個叫做NdkForJava的類,package com.hellondk;類定義如下:

public class NdkForJava
{
    public int SizeOfString(String str)
    {
        return str.length();
    }
}

這個函數顯然不是很難,但裡邊包含了這些關鍵點:首先,實現了String參數的傳入;再者,對傳入的String參數進行了計算;最後,返回了一個int參數。這幾點已經可以完整的應付最基本的NDK調用問題了。

那這個函數的NDK協議應該怎麼寫呢?其實很簡單,將上一個函數的主體部分刪掉(也就是別寫主體,光起名就好),再增加一個關鍵字native即可:

public native int SizeOfString(String str);

這裡有幾個點跟java的普通函數寫法有些不同,首先,增加了關鍵字native,也就是NDK中的N,該關鍵字聲明了這個函數的實現交由NDK完成,然後,省略了函數主體,因為函數主體是用C完成的嘛,要在這寫了就能用,還要java干嘛=。=

好,就醬,java部分的通訊協議就醬就可以了。

協議的C部分。

先回到之前創建的“IHateNDK.cpp”中,添加第一行語句,添加一個NDK專用的頭文件jni.h,裡邊包含了java與c實現通訊的核心函數及對象,要完成通訊,這個部分是必須有的,也是ndk專用的一個,標准C(顯然我介裡說的是GUN-C)沒有的頭文件。

完成了include以後,根據java協議的情況,聲明一個函數如下:

extern "C"
{
    JNIEXPORT jint Java_com_hellondk_NdkForJava_SizeOfString(JNIEnv* env, jobject thiz, jstring str);
}

這裡有很多關鍵點:

函數名

顯然這個函數名隱含天地大道,好像又符合某種天地之理,怎麼看和都好像信息量很大的樣子,似乎跟java的協議有某種關聯(呸,瞎子都看得出來啦,再啰嗦板磚伺候;啊,這要靠領悟的喂)。

C協議中的函數名為了跟java協議中的函數名對應,所以命名的格式(區分大小寫)為Java_com_包名1_包名2_……包名n類名_函數名。

NDK使用了“反射”的技術完成了通過java調用C函數的功能,所以這個名字非常重要,千萬不能寫錯,寫錯的話編譯器是無法正常加載NDK中的函數的。

關鍵字extern

extern "C"
{
    //...
}

C++語言在編譯的時候為了解決函數的多態問題,會將函數名和參數聯合起來生成一個中間的函數名稱,而C語言則不會,因此會造成鏈接時找不到對應函數的情況,此時C函數就需要用extern “C”進行鏈接指定,這告訴編譯器,請保持我的名稱,不要給我生成用於鏈接的中間函數名。

前邊偶們知道了NDK函數的名字灰常重要,就可以理解為什麼這裡要使用extern這個關鍵字了,正如百科所說,為了防止反射被破壞,為了守護函數名的平衡,貫徹愛與正義的邪惡……咳咳跑題了……雖然我一直用C來稱呼,但就像本po開篇所說,其實這個C是包含了CPP的含義,因為某個懶貨才沒寫那麼清楚。

關鍵字JNIEXPORT。

顧名思義,這是個port,拆解開來應該是Java Native Interface Extra Port(顧名思義尼槑啊誰特麼能思的出那麼復雜的解釋)。好好好,這個關鍵字的功能就是告訴編譯器,別看這個函數名字長的搓,參數也莫名其妙,這可是是在java那邊掛了號的,別把TA與普通的C函數等同視之。

常用參數。

可以看到以前常見的int、string類型前邊都多了一個惡心難看的j,像這樣的jint、jstring就是java的值類型在c函數中的定義,NDK提供了一系列的jxxx用來定義各種數據類型,參考對應如下:
這裡寫圖片描述
這裡寫圖片描述
對於特殊的數組類型,則可以通過jxxxArray來傳遞。

注意,這個圖樣圖森破的數據類型基本就拿來完成從java數據到C數據或者C數據到java數據的轉換的中間類型就好了,最好別哪來直接做運算,有時候會產生很多很奇怪的結果(例如內存洩漏)。

所以原來的java函數返回值是int,這裡的返回值就改成了jint,原來的java函數參數是String,所以這裡就改成了jstring。嗯,麼麼哒(麼麼哒你槑啊,沒發現多出了兩個參數啊,那個thiz是什麼來的啊拼的辣麼奇葩的是日語還是韓語啊)。

關於這個嘛,今天天氣好像很好的樣子。

好,這個問題提的很好,能提出這個問題說明童鞋你的數學已經有一定境界了,都快超過幼兒園的小盆友了,灰常好灰常好。

好好言歸正傳,所有的ndk下的jniexport協議,也就是說已經在java那邊掛上號的函數,天生自帶天賦,第一個參數必須是JNIEnv類型,第二個參數必須是jobject類型。也就是說,即便java那邊的native函數沒有一個參數,這邊對應的jniexport函數也要有這兩個參數。

JNIEnv指針是JVM創建的,用於Native的c/c++方法操縱Java執行棧中的數據,比如Java Class, Java Method等。JNIEnv中定義了一組函數指針,c/c++ Native程序是通過這些函數指針操縱Java數據。這樣設計的好處是:你的c/c++ 程序不需要依賴任何函數庫,或者DLL。由於JVM可能由不同的廠商實現,不同廠商有自己不同的JNI實現,如果要求這些廠商暴露約定好的一些頭文件和庫,這不是靈活的設計。
轉載自別人的博客

thiz指代的是調用這個JNI函數的Java對象,有點類似於C++中的this指針。但因為在這裡是特殊指定java的對象,所以與一般的this做了一個區分,使用了thiz。

【至於為什麼會出現這個東西,其實可以追溯到java是一個完全面向對象的開發語言,而C卻是一個函數式面向過程的語言,但由於這東東以講又是進階內容,其實就是如果面向對象開發學的比較扎實或者做的比較多的話很好理解,而且不理解也不怎麼妨礙基本使用,所以就不詳敘了】

java數據與c數據之間的轉換

由於java部分才是Android的主體,所以整個流程都是由java部分的代碼驅動的,一般來說ndk只用於完成計算的部分,所以偶們可以把java看作發送方,ndk看作接收方,java發送的數據需要通過ndk進行一些轉換,才能交給C的部分進行進一步的計算,所以數據從接受後的操作方式來說可以歸納為三類(個人意見),以下對它們的基本操作進行分類說明:

綜述

有一些操作是對所有的這些類型都有效的,首先要考慮的一個問題就是內存的管理分配問題,當java部分聲明並給一個對象分配了內存空間以後,把這個對象的引用參數傳遞給ndk之後,ndk就需要面臨一種選擇,即,之後使用C對這些參數進行操作的時候,是直接對這個對象的內存本身進行操作呢,還是將該對象的內存拷貝一份,再對拷貝進行操作。

這兩種方案各有利弊,直接操作的話,有可能會導致內存洩漏、內存覆寫等不容易控制的後果,需要在開發的時候花費更多的注意力去關注這部分對象(內存)的生命周期。拷貝法簡單直接,而且由於是在拷貝上執行操作,所以可以不用擔心對java部分產生影響,只要維護好自己的生命周期即可。

再者,在實際開發的過程中,特別是涉及到數組等線性表或者非線性表的操作時,如果使用自動變量,我遇到過很多次同樣的代碼,由於自動釋放的時機不同,導致有些手機可以正常運行,有些手機無法正常運行的情況,這也是NDK技術不是非常成熟的表現,為了避免這個問題,建議在使用的時候,盡可能手動分配和管理C部分的內存。

標准的int、boolean、byte等值類型;

由於剛剛聲明的函數SizeOfString傳遞的參數String有特殊的使用方式,我下邊會專門獨立拿出來說說,這邊為了展示標准值類型的傳遞,偶們先做這麼一個函數,完成對輸入變量的加法運算,這個函數的java部分如下:

public native int Plus(int a, int b);

然後根據偶們之前提到的NDK部分的命名原則,偶們可以寫出它的ndk形式,注意大小寫噢:

JNIEXPORT jint Java_com_hellondk_NdkForJava_Plus(JNIEnv* env, jobject thiz, jint a, jint b);

然後偶們現在在函數主體中就得到了一個jint的數據a,和另一個jint的數據b。現在第一步偶們要做的事情是把這個jint的數據轉換為int類型,方便偶們做進一步的處理。從jin.h的頭文件中偶們可以找到如下的定義:
這裡寫圖片描述
以jint為例,如果再追溯的話可以看到:

typedef __int32_t int32_t;

然後是:

typedef int __int32_t;

如果傳入的是這類數據類型,毫無疑問傳入的將會是一個值參,所以這個時候你可以放心的對傳入的參數進行各種操作,因為你的操作都是直接發生在拷貝的副本上的,如果沒有意外的話,這個副本會隨著函數的結束而被釋放(考慮到使用NDK的童鞋肯定要有C的背景,我這裡可能會借用一些C的術語來描述某些JAVA沒有的狀態,相信C基礎扎實的您一定可以明白我想表達的意思=。=好吧就是我詞窮的不知道該怎麼說)。其實跟java中對int參數的處理一下,無論你在函數對對傳入的int數組怎麼加減乘除,主調用函數中的傳入參數並不會發生改變。

簡單的說,jint其實是int數據別名。所以Plus函數的某種寫法可以是:

JNIEXPORT jint Java_com_hellondk_NdkForJava_Plus(JNIEnv* env, jobject thiz, jint a, jint b)
{
    int pA = a;
    int pB = b;
    return pA + pB;
}

或者是:

JNIEXPORT jint Java_com_hellondk_NdkForJava_Plus(JNIEnv* env, jobject thiz, jint a, jint b)
{
    return a+ b
}

其他的上圖列出的數據類型都可以這樣用,那麼為什麼還要弄個jint出來呢?因為下邊有數組的問題需要解決。

標准的int、boolean、byte等值類型的數組類型,也就是int[]、boolean[]、byte[]等;

本節參考
這一塊會比較復雜,偶們都知道,java默認的數組提供了很多額外的功能,例如說你可以直接訪問數組的長度。但是,對應的C的數組是沒有這些信息的,在NDK這邊獲得的最終將是一個對應數據類型的指針(甚至可以是jobject類型),就像C對數組的處理一個,你得到的只是數組第一個數據所在的內存地址,後邊的訪問要靠索引器(實際上是內存地址的平移)來進行訪問了。

偶們先來看看怎麼獲得數組的第一個索引的指針,首先先創造一個進階的Plus函數,該函數
完成了把數組a和數組b中的所有數值相加並返回的功能,偶們先定義它的Java部分:

public native int Plus(int[]a, int[]b);

然後是NDK部分

JNIEXPORT jint Java_com_hellondk_NdkForJava_Plus(JNIEnv* env, jobject thiz, jintArray a, jintArray b);

好,第一步,偶們要決定是否創建一份該數組的拷貝,記得前邊說的麼,創建或者不創建各有各的優點,所以偶們可以定義一個參數來標記它:

jboolean ifCopy = JNI_FALSE;//JNI_TRUE;

可選的結果有兩個,JNI_FALSE和JNI_TRUE,其實這是jni提供的宏定義,實際上就是常數0和常數1,按照避免“魔數”的原則,我更願意這樣來寫。顧名思義,False的時候,不會對傳入的數組進行拷貝,而是直接在原數組的內存上進行操作;True的時候會進行拷貝,在拷貝的數組上進行操作。

然後,一般來說偶們還需要獲得數組的大小,要不然很可能會發生索引越界的情況:

int lengthOfArrayA = env->GetArrayLength(a);
int lengthOfArrayB = env->GetArrayLength(b);

之後,偶們開始考慮怎麼獲得數組,就像偶們所熟知的那樣,C下邊的數組是一個指針:

int* arrayA = env->GetIntArrayElements(a,&ifCopy);
int* arrayB = env->GetIntArrayElements(b,&ifCopy);

這裡要留心的是,GetIntArrayElements函數的第二個參數是一個執行jboolean類型的指針,所以偶們這裡需要對前邊聲明的ifCopy增加一個&符號,以傳入正確的參數。

OK,現在數組有了,數組的長度也有了,偶們可以完成下一步的相加的工作了:

int sum = 0;
for(int i = 0; i < lengthOfArrayA; i ++)
    sum += arrayA[i];

for(int i = 0; i < lengthOfArrayB; i ++)
    sum += arrayB[i];

這一塊是標准的C++寫法,不多啰嗦了。按照一般的邏輯,下一步應該直接用return語句把結果返回,這個函數就結束了。

不過很遺憾,這樣做的話,在有些手機上會出現內存洩露的問題,因為在執行env->GetIntArrayElements函數的時候,無論傳入的ifCopy是True還是False,返回的數組指針(在本例中就是arrayA和arrayB啦)所指向的那一片內存(沒錯,是那一片,整一個數組所有的內存,而不僅僅是索引為0的位置的內存)都會被系統鎖定,防止被java自動回收,這個鎖定是需要手動解除的,所以在執行完上述的計算過程以後,偶們還需要這樣解除被鎖定的內存,否則會產生內存洩漏(根據我用過的這麼多開發機,確實有些機器不會在這裡發生錯誤,但為了盡可能的兼容更多機型,手動解鎖還是必須的步驟)。

在這裡偶們先來看函數原型:

void ReleaseIntArrayElements(jintArray array, jint* elemts, jint mode);

參數array就是這個數組來源的jxxxArray對象,elems就是剛才使用GetXXXElements函數獲得的數組指針,mode是由jni.h定義的三種模式,其中有:

#define JNI_COMMIT 1 /* copy content, do not free buffer */
#define JNI_ABORT 2 /* free buffer w/o copying back */

另外,還可以取值為0,此時表示在更新數組元素後釋放elems緩沖器;取JNI_COMMIT的時候,表示在更新數組元素後不釋放elems緩沖器 ;取JNI_ABORT的時候,表示不更新數組元素釋放elems緩沖器。一般來說,偶們取0。
所以在本例中,偶們需要執行:

env->ReleaseIntArrayElements(a,arrayA,0);
env->ReleaseIntArrayElements(b,arrayB,0);

最後完成return的工作,合起來偶們得到的函數可以是這樣的:

JNIEXPORT jint Java_com_hellondk_NdkForJava_Plus(JNIEnv* env, jobject thiz, jintArray a, jintArray b)
{
    jboolean ifCopy = JNI_FALSE;//JNI_TRUE;

    int lengthOfArrayA = env->GetArrayLength(a);
    int lengthOfArrayB = env->GetArrayLength(b);

    int* arrayA = env->GetIntArrayElements(a,&ifCopy);
    int* arrayB = env->GetIntArrayElements(b,&ifCopy);

    int sum = 0;
    for(int i = 0; i < lengthOfArrayA; i ++)
        sum += arrayA[i];

    for(int i = 0; i < lengthOfArrayB; i ++)
        sum += arrayB[i];

    env->ReleaseIntArrayElements(a,arrayA,0);
    env->ReleaseIntArrayElements(b,arrayB,0);

    return sum;
}

啊哈,好像又解決了一個問題耶,休息,休息一會~

(′Д`)好吧其實在數組這塊還有一個問題,木有錯,上邊只討論了如何處理傳入的數組,但是如果有童鞋想傳出一個數組該腫麼辦?

介是一個復雜的問題,偶們,呃,偶們還是來聊聊天氣吧。

好,為了說明這個問題,我先創建一個新的函數,它的功能是返回一個排序從1到100的數組,系不系很激動,那TA的java和ndk部分應該是什麼樣的呢?

快速解決一下Java的部分:

public native int[] ArrayFrom1To100();

快速的解決一下C的部分,弄個數組出來。

表示我懶筋煩了,於是TA決定就假設存在一個int* resultArray的數組已經弄好了,這個數組的長度是100,就等傳出去。

JNIEXPORT jint Java_com_hellondk_NdkForJava_ArrayFrom1To100(JNIEnv* env, jobject thiz, jintArray a, jintArray b)
{
    int length = 100;
    int* resultArray = new int[length];
//自己給數組賦值完成1到100的壯舉
}

為了把數據傳出去,偶們必須先創建一個jintArray:

jintArray result = env->NewIntArray(length);

這時偶們需要使用SetIntArrayRegion函數,偶們首先來看看函數原型:

void SetIntArrayRegion(jintArray array, jsize start, jsize len, const jint* buf);

這個函數的功能就是快速把一個int數組的值拷貝到一個jintArray裡邊,其中要求創建jintArray的時候,這個jintArray的長度不能小於被拷貝的數組的長度(也就是len-start),否則就會出現錯誤。array表示准備被賦值的數組,也是偶們打算return出去的數組;start指從被拷貝數組中開始拷貝的起始位置(根據題目需求,這裡我會從0開始拷貝,但不意味著不能從18,36,甚至40開始拷,但這樣會不符合這個函數的需求);len指從start位置開始,連續拷貝的數據的數量(同樣根據題目要求,這裡偶們需要拷貝100個,但同樣會違背偶們函數的需求);buf就是偶們在C環境下完成的數組的索引0的指針。因此偶們應該執行的語句是:

env->SetIntArrayRegion(result,0,length,resultArray);

最後,偶們處理一下數組的內存管理問題,就可以return了,所以偶們應該看到的完整函數是:

JNIEXPORT jint Java_com_hellondk_NdkForJava_ArrayFrom1To100(JNIEnv* env, jobject thiz, jintArray a, jintArray b)
{
    int length = 100;
    int* resultArray = new int[length];
    //自己給數組賦值完成1到100的壯舉

    jintArray result = env->NewIntArray(length);

    env->SetIntArrayRegion  (result,0,length,resultArray);

    if(resultArray)
    {
        delete resultArray;
        resultArray = 0;        
    }

    return result;
}

最後還是要強調一下,由於我之前提過的自動變量的回收問題,盡可能不要使用自動變量完成數組的操作,改用手動管理,要不有些手機上邊會爆出莫名其妙的錯。我一貫認為這些不一定會出但是有可能會出問題,特別是跨設備的問題,最好能避免盡量避免。

String類型的問題

在ndk的部分,回到偶們剛剛寫好的函數SizeOfString,偶們接收到了一個jstring的對象str,這也就是java調動SizeOfString函數時發送過來的對象,但jstring並不能直接進行操作,偶們需要對其進行一些處理以將其轉換為C下邊可用的字符串對象。其實嚴格來說,String對象是指是一個char類型的數組,但由於這是最常用的對象,所以ndk把它獨立出來作為一個傳統的只對象,操作上跟其他的jxxxArray類似,所以獲取String的方法跟其他數組大同小異,但要留心的是,函數略有不同:

jboolean isCopy = JNI_FALSE;
const char* cStr = env->GetStringUTFChars(str,&isCopy);

//計算長度的問題由於某個我的懶筋犯了所以掠過

env->ReleaseStringUTFChars(str,cStr);

用到的Get函數需要使用GetStringUTFChars函數,UTF的含義相信能看到這塊的童鞋應該不用我多啰嗦啦(但你還是啰嗦了=。=),同樣是用完以後使用Release函數處理掉。

這塊其實不復雜,參考上前邊其他的數組操作就可以完成只是對應函數略不一樣,我為什麼還要獨立把String拿出來說一下呢?

因為首先string是一種非常常用的變量;然後,如果想返回一個String是一個很麻煩的事情。當然有童鞋會說啦,我返回一個charArray,再在java裡把它處理成String不就行了麼,呃,其實可以算作一種解決方案,但因為String的特殊性,我再提供一種方案(所以這塊內容可以算作可有可無的內容啦;騷年圖樣吖,你看看為什麼要加UTF就知道這事情在java那邊也沒那麼簡單~好吧這是我瞎猜的,無節操掠過)。

偶們還是先來構造一個基本的函數,它的功能是返回一個字符串,字符串的內容是經典的”Hello World”。

java部分:

public native String HelloWorld();

那,偶們來看看NDK的部分:

JNIEXPORT jstring Java_com_hellondk+NdkForJava_HelloWOrld(JNIEnv* env, jobject thiz);

嗯,看起來好像跟前邊差不多嘛,為什麼要單獨拎出來討論呢?那麼,偶們再看看jstring的具體定義:

typedef jobject jstring;

咦?為什麼jstring會變成一個jobject的類型?因為,String雖然是一個很常用的類型,但事實上,它在java裡邊並沒有被視為值類型,簡單說,它一般情況下都是被用作引用參數的。所以,偶們在實現jstring的時候,跟之前的部分有些區別:

JNIEXPORT jstring Java_com_hellondk+NdkForJava_HelloWOrld(JNIEnv* env, jobject thiz)
{
    char* qrText = "Hello World";
    jstring jStr = env->NewStringUTF(qrText);
    return jStr;
}

抱頭跑……現在其實是要思考這樣一個問題,如果我想返回的是一個String[]數組腫麼破? 讓偶們輕微的調整一個HelloWorld的這個函數,讓它返回一個String的數組,數組中分別是“Hello”和“World”,應該怎麼辦?

首先是Java的部分:

public native String[] HelloWorld();

然後是NDK的部分:

JNIEXPORT jstring Java_com_hellondk+NdkForJava_HelloWOrld(JNIEnv* env, jobject thiz);

在這裡偶們要注意一點,是不存在jstringArray這個對象的,雖然jni要typedef一下也不難,但這樣很可能會造成不必要的誤會,所以jni沒有做(雖然我更傾向於認為是偷懶=。=以己度人),所以偶們要返回的其實是一個jobjectArray。

第一步來說,偶們應該先new一個objectArray出來,但這裡有一些與之前不同的地方,由於jobject可以是任何java的對象,也就是包括但不止限於String類型,所以偶們要用“反射”的方法指定新生成的objectArray具體是什麼類型的數組:

jclass typeString = env->FindClass("java/lang/String");

後邊的字符串是特指String類型的,其實還有很多其它用法,這裡只把String作為一種最常用的特殊情況拿出來討論。
然後請先看這麼一個new函數:

jobjectArray resultArray = env->NewObjectArray((jsize)2, typestring, NULL);

現在,偶們聲明了一個長度為2,每個數據都是NULL,數據類型為java的String類型的數組。可以看到,這個數組是空的,偶們要先准備一些字符串塞進去,例如:

char* text1 = "Hello";
char* text2 = "World";

jstring jstr1 = env->NewStringUTF(text1);
jstring jstr2 = env->NewStringUTF(text2);

字符串好辦,但怎麼塞卻是個問題,還記得之前其他Array的寫法嗎?過程差不多:

env->SetObjectArrayElement(resultArray,0,jstr1);
env->SetObjectArrayElement(resultArray,0,jstr2);

分別把jstr1和jstr2放到索引為0和索引為1的位置。那麼,這個時候偶們可以放心的return數組了麼?很遺憾,不是的,偶們還需要管理一下jstr的生存周期問題,由於SetObjectArrayElement做的是把目標對象拷貝了一份,所以,set完成之後,jstr1就沒用了,但由於ndk的機制,new出來的jstring並不會自動回收,所以偶們還有手動刪除一下:

env->DeleteLocalRef(jstr1);
env->DeleteLocalRef(jstr2);

好,現在看看這個函數的完整寫法:

JNIEXPORT jstring Java_com_hellondk+NdkForJava_HelloWOrld(JNIEnv* env, jobject thiz)
{
    jclass typeString = env->FindClass("java/lang/String");

    jobjectArray resultArray = env->NewObjectArray((jsize)2, typestring, NULL);

    char* text1 = "Hello";
    char* text2 = "World";

    jstring jstr1 = env->NewStringUTF(text1);
    jstring jstr2 = env->NewStringUTF(text2);

    env->SetObjectArrayElement(resultArray,0,jstr1);
    env->SetObjectArrayElement(resultArray,0,jstr2);

    env->DeleteLocalRef(jstr1);
    env->DeleteLocalRef(jstr2);

    return resultArray;
}

所有不是這些標准值類型的對象類型(既可以是自定義的對象,也可以是java自己提供的其他對象)。

對於這種對象,我只能說,我不會用。對於我來說使用成本太高了,有興趣的童鞋可以自己研究下=。=上文的String[]類型的處理是jobject的一種情況,更復雜的情況下還可以在ndk中調用java對象的特定函數完成一些工作,但這些都有點遙遠,而且偶們用ndk的目的不就是為了使用C的高效運算能力麼?所以這塊我沒有太大的動力去研究,起碼放在我現在這個項目來看(懶就懶啦,死性不改)。

MK文件的說明

其實咧,上邊神馬接口吖,神馬編譯吖,別以為就可以用了,因為,NDK屬於比較手動的工具,編譯成鏈接庫還要設置很多東西滴,而這些設置是沒有界面給你方便的使用的(沒銀性啊),甚至還需要用一種特定的語言書寫(相當於又編一個程序啦-^-),語言的復雜來說,我也不太懂,我這裡只介紹一些基本的設置,復雜的使用方式請自己再研究啦~

Android.mk

要設置mk文件之前,顯然偶們需要先創建一個,如果是第一次創建mk文件,可能eclipse會提示安裝一個插件,按提示安裝即可,這個插件只是用來快速格式化文本的,我覺得應該不是必須品,但對優雅的IDE(顯然eclipse不算)有重度依賴症的我毫不猶豫的就裝了,就算爛,也總比沒有好(不是說好了不吐槽了喂~)。

一般來說,add native support以後會默認創建一個Android.mk文件,但如果沒有的話也不要緊,在jni文件夾中右鍵-New-File,然後文件命名為Android.mk即可,注意後綴和大小寫。

一個最簡單的mk文件是這樣的:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := IHateNDK
LOCAL_SRC_FILES := IHateNDK.cpp

include $(BUILD_SHARED_LIBRARY)

第一行相當指定了當前文件夾為默認根目錄,之後所有關於文件(路徑)的操作都會基於這個目錄來進行。

第三行使用了一個include命令,這裡的include命令並不是C語言中的引用某個文件的意思,而是執行某個命令,這裡的代碼中,第三行意思是清空當前所有的命令(也可以理解另起一組編譯選項),第八行表示建立一個動態鏈接庫。

第五行命令確定了即將編譯出來的so文件的名稱,如前文所言,所有的動態鏈接庫都是而且必須以lib+這裡寫的名字+.so作為結尾才能正常使用。

第六行命令列出了所有要編譯的c/cpp文件的目錄,沒錯,是所有,也就是如果你有100個cpp的文件,麻煩用空格作為分隔符把所有的名字帶後綴全部寫出來!

好吧這個是逗你玩的,還是有一些方法可以省略一部分文件名,但確實還是很不方便,更多的參數及設置之後再介紹。

現在通過這個文件偶們可以簡單的完成動態鏈接庫libIHateNDK.so的編譯(command+b)。

現在偶們來簡單介紹一些進階設置的情況:

基本操作符

這裡就介紹四個比較重要的操作符,更多內容請自己研究。

首先是“:=”操作符,該操作符的意思是初始化並把右邊的值賦給左邊。

然後是“+=”操作符,該操作符的意思是在左邊已有的設置基礎上增加一個參數值。這個有點難理解,一會統一用一個例子來解釋一下。

之後是“\”操作符,這個操作符的意思是下一行跟當前行是連在一起的,由於mk文件的命令使用換行的方式表示結束,所以必要時需要通過“\”來對一些較長的命令進行排版。

最後是“#”操作符,該操作符的功能就是注釋本行中該操作符之後的所有文本。

下邊偶們來看一個例子:

#這是甲寫法
LOCAL_SRC_FILES := IHateNDK.cpp IHateJNI.cpp

#這是乙寫法a
LOCAL_SRC_FILES := IHateNDK.cpp \
                   IHateJNI.cpp


#這是乙寫法b(錯誤寫法)
LOCAL_SRC_FILES := IHateNDK.cpp 
                   IHateJNI.cpp

#這是丙寫法
LOCAL_SRC_FILES := IHateNDK.cpp
LOCAL_SRC_FILES := IHateJNI.cpp

簡單的說,甲寫法、乙寫法a和丁寫法可以認為是等價的,而乙寫法b是一個錯誤示范,考慮到用得著NDK的童鞋肯定都是學貫CJ滴銀,我就不多啰嗦了。

快速添加多個代碼文件

如果有100個文件,我難道要把100個文件都寫一遍麼?不要哇(我已抓狂),明明只要把jni文件夾下的所有文件都默認索引了就可以了吧?(但這樣就缺少訂制能力了,某大神推推眼鏡道。qu shi=。=)

NDK提供了一種折衷的方案,起碼我只找到這種啦,再好的暫時沒見著。可以讓編譯器自動引用某個文件夾下的所有指定後綴的文件(至於能不能編譯成功就看騷年你真正的技術了),而且注意,這個引用是非循環的,也就是說,被引用的文件夾下的子目錄內的文件並不會被搜索,所以,折衷方案就是,你把所有文件夾的路徑寫一遍(啊多麼痛的領悟啊這是那麼無聊啊)。

這個命令的原理是首先創建一張列表,列表上標注了所有文件夾的路徑,以及該路徑需要被引用的文件的後綴名:

MY_CPP_LIST := $(wildcard $(LOCAL_PATH)/*.cpp)

這裡引用的是默認目錄,也就是jni文件下所有的後綴為cpp的文件,是不是有理有據讓人信服?如果偶們要再增加一個文件夾,可以使用之前提到的“+=”操作符:

MY_CPP_LIST := $(wildcard $(LOCAL_PATH)/*.cpp)
MY_CPP_LIST += $(wildcard $(LOCAL_PATH)/sbndk/*.cc)

這一句在原來的基礎上增加了jni目錄下bigint目錄下所有後綴為cc的文件。好,按照這個方式可以創建一個很長的列表,但是怎麼讓這個列表生效呢?如下:

LOCAL_SRC_FILES += $(MY_CPP_LIST$(LOACL_PATH)/%...%/

搞定,這樣編譯的時候編譯器就會自動索引列表中所有你指名後綴的文件啦。

Application.mk

這是個奇怪的世界,我一直沒弄明白有什麼用,不過確實有一些參數需要在這個文件裡邊設置,如果默認沒有的話,跟創建Android.mk一樣的方式創建吧。

【參考手冊:http://www.oschina.net/question/565065_93983】

Log的問題

我沒能解決調試的問題,所以只能使用Log的方式來輸出一些信息輔助調試,這個過程略煩躁(我躁狂症晚期沒救了)。

首先在需要Log的文件中加入頭文件:

然後在Android.mk文件中設置參數:

調用函數如下:

((void)__android_log_print(ANDROID_LOG_WARN, “logcat中tag列的值","要打印的具體字符串" ))

由於Log的等級問題,可以把參數ANDROID_LOG_WARN替換為如下幾種:

__android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,“***”) // LOG類型:debug
__android_log_print(ANDROID_LOG_INFO,LOG_TAG,“***”) // LOG類型:info
__android_log_print(ANDROID_LOG_WARN,LOG_TAG,“***”) // LOG類型:warning
__android_log_print(ANDROID_LOG_ERROR,LOG_TAG,“***”) // LOG類型:error
__android_log_print(ANDROID_LOG_FATAL,LOG_TAG,“***”) // LOG類型:verbose

Log的結果會輸出在Logcat中,相信用Java的銀肯定比我熟,我繼續犯懶去咯。

STL的問題

相信stl的重要性我不必多言,如果不知道stl是神馬的童鞋相信要麼是已經超越了stl,要麼是還沒到用得著stl的時候,這章可以先不看了。

在NDK目錄中,存在著一個神秘的文件夾,裡邊全是各式各樣的stl頭文件,但是騷年以為只要把這些頭文件include進去就可以了麼?可沒有那麼簡單,但偶們首先可以先找到TA。

由於NDK提供了四個版本的stl庫供選擇,但最推薦的是stlport_static,所以我調用的也是這個(其實是另外幾個根本編譯不起來)。

首先打開Application.mk文件,新增參數:

APP_STL := stlport_static
APP_CPPFLAGS := -frtti
LOCAL_CPPFLAGS += -fexceptions

然後,暫時想不起來還要添加什麼了,在項目上點擊右鍵-New-Folder,選擇Advanced,再勾上Link to alternate location (Linked Folder)。Browse,找到NDK開發包\sources\cxx-stl\stlport\stlport,注意是兩個stlport噢,是裡邊的那個,選中它,但別打開,因為需要引用的就是這個文件夾。

以vector為例,在需要使用vector的地方引用vector。

#include
std::vector* test = new std::vector();

然後的事就不多說啦。

Zxing的問題

商業機密(懶病病入膏肓放棄治療)。

在Java項目下調用編譯好的so動態鏈接庫

在其他的Java項目下的libs文件夾,如果沒有則手動創建一個,再在libs文件夾下再創建一個armeabi文件夾,把編譯好的so文件放在這個文件夾裡,注意so文件的名字一定是’lib’開頭的,這個問題我已經提過好多次啦。
這裡寫圖片描述
在需要調用偶們ndk的java接口的地方增加一段靜態代碼:

static
{
    System.loadLibrary("IHateNDK");
}

注意這裡的名字是沒有lib的,但文件夾中的名字一定要有lib開頭(啰嗦成瘾無藥可救)。

然後,在你想用的地方進行灰翔吧~~~

報錯和解決方案

編譯的時候Console報錯WARNING: APP_PLATFORM android-19 is larger than android:minSdkVersion 8 in ./AndroidManifest.xml
這裡寫圖片描述

解決方案:

顯然從文字上就能看出是因為使用的AndroidManifest文件中的支持的最低版本的參數和NDK編譯配置文件中的最低版本配置不相符,將兩者統一即可(以下以14為例,不一定要14,只要統一即可)。

在Application.mk文件中修改(或增加)關鍵字:

APP_PLATFORM := android-14

修改AndroidManifest文件中的android:minSdkVersion=”14”

吐槽

果然跟我用之前猜的差不多,eclipse不愧是我非常非常沒有猿糞的開發工具,我簡直已經無力吐槽了,為什麼穩定性這麼糟糕的東西居然還能被如此多大牛開發者甘之如饴?一群叫囂著要開發出世界上最人性化UI的人連自己用的工具都不穩定(人性化?呵呵),就像一個拿著漏勺爛鍋殘口菜刀的乞丐跟你說他能做世界上最好的叫花雞一樣——你信嗎?當然,廚師可以不會(一般也不)生產菜刀,生產菜刀的也不一定是廚師,但起碼要能分辨出好的菜刀;開發者可以不會開發開發者工具,但開發者工具一定是開發者開發的。

項目的需求是使用NDK將偶們以前使用的一些在xcode下編譯的用於objective-c的c++靜態庫(顯然,偶們有源碼)移植到ndk的環境中,NDK是由谷歌娘提供的可以允許java的安卓程序在安卓平台下以c/cpp(由於所有人都知道c跟cpp的關系,不知道的人請不要浪費時間看這篇東西了,然後那個反斜槓打起來很麻煩,以下所有寫c或cpp的地方請自己腦補另外一半)的方式(實際上跟語言特性有關,不多說,反正運算速度比java下直接執行要快很多)運行一部分函數的功能,加快運算速度。因為偶們原有的項目涉及到大量的圖形運算,顯然java本身並不擅長做這些東西(原諒我一生java黑),他已經被java各種不穩定(顯然,這裡更多是IDE,也就是eclipse的問題)弄的無數次想砸電腦了,我不否認java在軟件開發史上的地位,以及作出的貢獻,就像我不否認無聲黑白電視是一個偉大的發明一樣)(不擅長並不代表不能做,請明白擅長的意思是很容易就能做好)(關於java的問題請各位讀者老爺不要跟我討論了,我這裡用到也是迫不得已,請相信我如果有的選根本不想碰這個東西,也對此不感興趣,所以我的使用完全是為了達到目的而完成的,如有得罪請見諒,蘿卜白菜各有所好,謝絕人參公雞,如果有忍不住的請相信我也不是那種忍得住的銀)。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved