Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> 詳細Android Studio + NDK范例

詳細Android Studio + NDK范例

編輯:關於android開發

詳細Android Studio + NDK范例


【本范例所采用的配置】

·系統:Windows7旗艦版,Service Pack 1,32位(最新的NDK已不支持WindowsXP)
·JDK(java包):1.7版
·Android Studio(制作安卓程序的主要工具):1.4版
·SDK(安卓開發工具包):Android Studio 1.4自帶的
·NDK(原生開發工具包,用來做安卓程序的C/C++部分):用Android Studio 1.4內置的鏈接下載
·Experimental Plugin(一個實驗版插件,目前NDK必不可少的助手):NDK自帶的
·gradle(負責安卓程序的編譯):2.5版(目前NDK只支持gradle2.5,版本高了低了都不行)


上述工具,除了Windows7,共有五個,但有些工具是捆綁在別的工具上的,所以,如果你的機器上一個也沒有,要下載的只是這三個:
\

\

JDK在網上很容易搜到。另外兩個,你可以到https://developers.google.com/下載,如果google的網站上不去,國內有一個網站http://www.androiddevtools.cn/收錄了絕大部分安卓開發工具。


【安裝】

假如你的系統從來沒有碰過Android,要做的事情是:


一、安裝JDK 1.7


重要的是記住安裝路徑。我的電腦是雙系統,Windows7在D盤上,所以我裝java的路徑是“D:\Program Files\Java\jdk1.7.0_71”。
過去,在WindowsXP中使用Android Studio,裝java要避免帶空格的路徑,現在Windows7沒有這個限制了,你按默認的路徑安裝即可。


二、給java設環境變量


在電腦桌面左下角點“開始”按鈕,然後依次選“控制面板”、“系統和安全”、“系統”、“環境變量”,打開“環境變量”對話框,這裡有兩個“新建”按鈕,點下面那個(再次強調,是下面那個),建一個新的系統變量,名為“JAVA_HOME”,值為java的安裝路徑(我的是“D:\Program Files\Java\jdk1.7.0_71”)。


\



再建一個新的系統變量,名為“CLASSPATH”,值為“.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar”,注意前面有點和分號。
找到已有的系統變量“PATH”,雙擊它,打開編輯它的窗口,在變量值的末尾加“;%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin”,注意前面有分號。


總結剛才的3個環境變量:
·JAVA_HOME(新建的) java的安裝路徑
·CLASSPATH(新建的) 
.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar
·PATH(改原來的)   ;%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin(加在原值的後面)
改完之後一連串的“確定”,使這些變量得以保存。
驗證java是否裝好的方法:在DOS窗口中輸入java -version回車,若看到版本信息,就是裝好了。


三、安裝AndroidStudio1.4及NDK


安裝過程略。從提示文字上可以看到,這個版本Android Studio把SDK也一並安裝到你電腦裡了。
若不帶SDK,就要單獨下載和安裝SDK,過後還要在Android Studio中填寫SDK安裝路徑。
裝好之後先別著急啟動,在Android Studio的安裝目錄中找到bin文件夾,在其中找到idea.properties,用記事本打開,在其末尾添加一行並保存:
disable.android.first.run=true
如果Windows7不讓你修改這個位於系統盤的文件,那就把它拷貝到別的地方修改,再拷回去覆蓋原文件,這是可以有的,因為Windows7允許系統盤更換文件,只不過先問問你是不是管理員。
做這件事,是為了防止Android Studio啟動時不停地連google服務器(在不翻牆的情況下根本連不上,只能讓程序停在那兒不動)。
然後啟動Android Studio,如果走不動,多半是因為java沒裝好。
出現“Welcome...”窗口後,選“Configure”、“Project Defaults”、“Project Structure”,打開“Project Structure”窗口:


\



你第一次啟動時看到的窗口不是這樣的,“Android NDK location”中沒有東西,可能“JDK location”中也沒有。
“Android SDK lication”肯定有了,因為SDK是這個版本自帶的,它裝好了,路徑也就自動填上了。但JDK可能需要你手工填寫,把java的安裝路徑填進去(也就是剛才設環境變量“JAVA_HOME”時填的路徑)。
至於NDK,先要安裝。
尚未安裝NDK時,在此窗口的“Android NDK location”下會有一個按鈕讓你安裝,點它按提示進行,在翻牆的情況下,經過漫長的等待,Android Studio告訴你在下SDK,其實也在下NDK,下載完在提示文字中就看到了,這是NDK。接著進入NDK安裝,這用不了多久。裝好後就自動填上了NDK的路徑,就成了上圖的樣子。
有人說翻牆麻煩,不如找一個國內的鏈接下載NDK,安裝,把地址告訴Android Studio。但這樣一來,只能在項目中填寫NDK地址,不能在整個程序中固定它。
而且google的官網建議用Android Studio內置的鏈接下載NDK,版本是r10e,必須裝在SDK目錄下的ndk-bundle文件夾中,配套的gradle只能是2.5版,SDK至少是19.0.0版且帶生成工具(參閱tools.android.com/tech-docs/new-build-system/gradle-experimental),既然這麼麻煩,還不如直接用Android Studio內置的鏈接下載。
如果你永遠不需要在項目中寫C或C++代碼,就不用管NDK了,gradle也就用Android Studio自帶的就行了,下一步也就免了。
在這裡還要為NDK設環境變量:
NDK_ROOT(新建) NDK的安裝路徑
PATH(結尾增加) ;%NDK_ROOT%
四、安裝gradle


下載的gradle是個壓縮文件,把它解壓成一個文件夾,放到Android Studio自帶的gradle文件夾旁邊,像這樣:


\



此圖中,gradle-2.4是Android Studio 1.4原配的,gradle-2.5即將取代它的,是NDK要求的。


五、安裝手機驅動


Android Studio有模擬器供你調試,但最好用真機,一是真機調試快,二是能表現所有功能、暴露所有問題。
1.把安卓手機用數據線連在電腦的USB口上,就是你充電用的那根線,把插頭拔下來,只用線,線的細的那頭插在手機上,粗的那頭插在電腦的USB口上。
2.在手機的“開發者選項”中勾選“USB調試”。不同的手機品牌或安卓版本,這個選項的位置有所不同,我手頭這個手機,點“設定”按鈕後可以看到一串選項的底部有“開發者選項”,點開它可以看到“USB調試”這個選項。你的手機怎麼樣,自己找找吧。
3.安裝該手機的驅動程序。可以裝“360手機助手”、“91手機助手”之類的,它發現電腦連上手機,就會自動下載該手機的驅動程序,當它顯示手機型號時,驅動就裝好了,360手機助手是把手機型號顯示在左上角的。這時手機上也會出現USB圖標,拉開它會看見“已連接為媒體設備”。
還有一個跡象表明手機驅動裝好了——電腦的設備管理器顯示“Android Phone”
Android Studio第一次識別手機可能比較慢,可能要依賴“360手機助手”這樣的軟件來裝手機驅動,可能在驅動裝好之後還是找不到手機,但重啟電腦就找到了。或者還有種種稀奇古怪的問題,來回折騰碰巧哪一次找到了,以後就能找到了。手機軟件開發就是這樣,搞不明白就折騰,碰運氣,奇怪的是問題總能解決,要真是永遠解決不了倒好了,再也不用受它的氣了。
說了這麼多,就是一連手機,二裝驅動,三看軟件找到手機沒有。
找到手機,Android Studio會顯示手機型號,在界面下方點“Android Monitor”選項卡,就可以看到手機型號。


\

【入門練習】

一、建新項目


重新啟動Android Studio,在“Welcome...”窗口中選“Start a new Android Studio project”,按提示一步一步“Next”。
首先是起名:
Application name(應用名):就是你要做的程序的名稱,也將是項目名稱。
Company Domain(公司名):一般的格式是“類別.部門.公司”,這個練習只使用了“類別.部門”,填的是“exercise.myself”,意思就是“我自己.練習”。
你修改上面兩項,軟件就會自動更新下面的一項:
Package name(包名):這很重要,是你的程序的標識,C/C++代碼會引用它。你不必現在記住它,因為寫代碼時隨時可以在java代碼中查到它。
然後是項目路徑:
Project location(項目路徑):就是說這個項目的文件裝在哪一個文件夾裡,這個可以隨便設,只要自己記得住。
後面有一步選模板,最好選“Empty Activity”,其他模板會給你預備一堆沒用的組件。


二、改gradle代碼


對於不需要C/C++的項目,這一步完全可以跳過。但是不需要C/C++,也就不用看這篇文章了。
打開項目後,在界面左上方選“Project”,再把目錄展開成這樣:


\



上圖中選中的“gradle-wrapper.properties”,是馬上要修改的。它在目錄中的位置,可表示為“gradle/wrapper/gradle-wrapper.properties”。
雙擊它,就在界面右邊看到了它的代碼,將“2.4”改成“2.5”,如下圖所示。
這麼改,是因為目前的NDK只支持gradle2.5,版本高了或低了都不行。


\



如法炮制,修改build.gradle,將“:1.3.0”改為“-experimental:0.2.0”。
這是因為目前的NDK需要一個叫“experimental”的插件,它目前的版本是0.2.0。
注意:在現在以及後面的代碼修改中,一個字符也不要錯,仔細看本文的說明,一個冒號、一個點也不要漏掉,不然調試時報錯,你都看不懂(調試不會說“你這裡缺了一個分號”,它只會說一句讓你丈二和尚摸不著頭腦的話,甚至跟真正的錯誤八竿子也打不著)。
改完之後,立刻會冒出一個***警告,說gradle已經被你改了,你得在項目中更新它。這件事待會兒再做。

\


接下來修改app/build.gradle(注意,有兩個build.gradle,剛才改的是根目錄下的,現在改的是app目錄下的)。
這裡要改的地方特別多,先用純文本標一下(你別復制這段代碼,因為可能有些數字和你的項目不匹配,你就照著這個模板改你的代碼)。
再次提醒:必要的地方,一個字也別漏掉,一個也別多,哪怕是一個大小寫的區別。


apply plugin: "com.android.model.application'//紅色代碼是新加的

model {
android {
compileSdkVersion=23//這些數字是軟件根據你選擇的版本自動寫的,不必改
buildToolsVersion="23.0.1"
defaultConfig.with{
applicationId="myself.exercise.ndktest"//這是程序包名,用你自己的
          minSdkVersion.apiLevel =11
targetSdkVersion.apiLevel =23
versionCode=1
versionName="1.0"
}
tasks.withType(JavaCompile) {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
}
android.ndk {
moduleName ="lb"//這是將來so文件的名稱,自己取
}
android.buildTypes {//藍色代碼是移動了的
release {
minifyEnabled=true
proguardFiles.add(file("proguard-rules.pro"))//紫色代碼是改過了的
}
}
}
dependencies {
compile fileTree(dir:'libs', include: ['*.jar'])
//testCompile 'junit:junit:4.12'//灰色代碼的句子,你看到了就刪
compile'com.android.support:appcompat-v7:23.0.1'
//compile 'com.android.support:design:23.0.1'
}

改完了再一行一行檢查,這要是錯了,調試會痛苦不堪。
說到這裡得提一下google的態度,把這麼麻煩的事交給用戶,他們自己也挺不好意思的,在官網上說,因為NDK還是實驗版,不得已才讓用戶自己改代碼,他們會逐漸讓這些東西自動化。


三、更新gradle

現在來解決那個***警告的問題。
警告右邊有一個“Sync Now”鏈接,點它,故意引發報錯:


\



它是說,現在的gradle是2.4版的,你設置了2.5,就得找到2.5的安裝路徑。
正如前面所說,目前的NDK只支持gradle2.5,這也就是為什麼我們在安裝階段就把2.5放在2.4的旁邊。


在上述報錯窗口中,點“Gradle settings”,打開如下窗口:


\



點“gradle-2.4”旁邊的“…”按鈕,去找你已經安裝好的gradle-2.5。之後報錯信息沒有了,程序會重建gradle。




四、關於實驗版插件

還有一個隱藏的家伙一直沒有露面,但是跟著NDK一起進入了你的Android Studio,悄悄地進村,打qiang地不要,它就是Experimental Plugin,是C++制作必不可少的。剛才改“build.gradle”已經提到了它,就是“experimental:0.2.0”。
目前Android Studio的C/C++開發離不開這三個家伙(而且對它們的版本有嚴格限制):
·NDK(原生開發工具包,Native Develop Kit)r10e版;
·gradle2.5版;
·Experimental Plugin(實驗版插件)。
總結一下它們的來歷:
·你單獨下載安裝了gradle2.5
·用Android Studio內置的鏈接下載、由Android Studio自動安裝了NDK
·NDK悄悄帶來了實驗版插件
關於此插件的詳情,參閱tools.android.com/tech-docs/new-build-system/gradle-experimental
關於NDK,參閱developer.android.com/intl/zh-cn/ndk/index.html、tools.android.com/tech-docs/android-ndk-preview

五、創建jni文件夾

在app/src/main文件夾上點右鍵,在彈出菜單中選擇“New”、“Folder”、“JNI Folder”,按提示進行。
有一個“Change Folder Location”選項,不需要勾選,因為jni文件夾采用默認的位置(在main文件夾中)就行。
然後main目錄下會出現jni文件夾。


六、創建C++源文件

在jni文件夾上點右鍵,在彈出菜單中選擇“New”、“C/C++ Source File”。
在出現的對話框中,給即將創建的C++文件取名(不要加後綴,軟件會自動加上的)。
有一個選項“Create associated header”,不要勾選它,現在不需要創建頭文件。


之後,jni文件夾中出現了“myCode.cpp”,我們先不管它,先到Android部分做一些准備工作。


\



七、建一個演示按鈕


這是純Android操作,可能你已經熟悉了,但初學者需要知道。
編輯主窗口的xml文件(在此范例裡是“activity_main.xml”)。如果你在界面上看不到它,就到Project目錄的“app/src/main/res/layout/”中找到它,雙擊它打開編輯窗口。
窗口中有個手機圖樣,是模擬軟件顯示的效果,上面有一行字“Hello World”,是AndroidStudio自動加上的顯示文字的控件。雙擊它,會打開一個小窗口,上面有“id”欄,在這裡給該控件取名為“textView”(實際上可以隨便取,但為了和後面的步驟銜接,就取這個名吧),這是java代碼將要引用的。
再添加一個按鈕,方法是:在左邊的組件列表中找到“Button”,拖到手機圖上。
當然你也可以用編輯代碼的方式加這個按鈕,這裡對純Android操作只說最簡單的方法。
雙擊這個按鈕打開小窗口,給它的“id”取名叫“button1”。
然後點上方的“MainActivity.java”選項卡,進入主窗口代碼編輯界面,把代碼改成這樣:


package myself.exercise.ndktest;


import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;


public class MainActivity extends Activity {


TextView textView; //聲明一個標簽
Button button1; //聲明一個按鈕


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);


textView = (TextView)findViewById(R.id.textView); //在xml中找到標簽

button1 = (Button)findViewById(R.id.button1); //在xml中找到按鈕

button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//點按鈕以後發生的事
}
});
}
}


新手注意:頂端的這一句:“package myself.exercise.ndktest;”,表明這個項目的包名是“myself.exercise.ndktest”,以後引用包名時,可以到這兒來拷貝。
另外,在Android Studio中輸入類名(比如“TextView”)時,如果你沒寫錯它卻顯示為紅色,表明Android Studio還沒有識別這個名稱,上面的“import”還缺點什麼,你有兩種辦法:


1.重新輸入它,等待一個提示框出現,再按Tab鍵,Android Studio會自動幫你完成輸入,把相應的“import”添加到代碼中,比如添加“import android.widget.TextView;”,這時就不報錯了。

2.單擊它,當提示框出現時,按“Alt +回車鍵”。


八、建一個新的java類,用來加載C++庫


在“Project”目錄中找到“app/src/main/java/myself.exercise.ndktest”:


\



在這個文件夾上點右鍵,在彈出菜單中選擇“New”、“Java Class”,在彈出窗口中取名“Load”(不要加後綴,軟件會自動加上的):


\



點“OK”之後,“myself.exercise.ndktest”文件夾中就出現了新的類“Load”(其實它的全名是“Load.java”)。雙擊它打開它的代碼窗口,把代碼改成這樣:


package myself.exercise.ndktest;


public class Load {
static { //載入名為“lb”的C++庫
System.loadLibrary("lb");
}
public native int addInt(int a, int b); //調用庫裡的方法“addInt”,這是計算a和b兩個整數相加
}


\



現在“addInt”是紅色的,因為前面的“native”還沒有找到下落,以後等native函數(C/C++函數)有了,這裡自然就不紅了。
現在記住幾個關鍵的名稱:
·即將創建的C++庫的名稱:lb
·加載此庫的java包名:myself.exercise.ndktest
·加載此庫的java類名:Load
·從此庫中調用的方法:int addInt(int a, int b)(參數是兩個整數,返回是整數)

九、生成頭文件


走一遍主菜單“Build > Make Project”,這一步有沒有效果,要看這裡:


\



原來是沒有“Load.class”的,但是“Make Project”之後,它就加進來了。前提是“Make Project”要成功,不成功,什麼也不會改變。至於不成功的原因,可能是你漏了哪個字符,可能是沒有嚴格按本指南操作,也可能你的軟件版本與本文所說的不一樣。軟件開發是由一堆一堆人為的規矩組成的,不是理論物理,不是純粹數學,沒有真理,我們永遠掙扎在別人制定的規矩中,沒辦法,因為我們沒有比爾·蓋茨那樣的本事。
點界面左下角的“Terminal”標簽,打開命令行窗口:


\



在這句話的末尾輸入:
cd app/build/intermediates/classes/debug
按回車鍵,進入“debug”目錄:


\



接著輸入:
javah -jni myself.exercise.ndktest.Load
按回車鍵。


\



注意:“myself.exercise.ndktest”是包名,“Load”是調用C++庫的類的名稱,你開發自己的軟件時,這些要改的。
要是你沒寫錯,“debug”目錄下會有一個頭文件生成:


\



它的後綴是“h”,表明它是C/C++頭文件;它的名字仍然由上述包名和類名組成,只不過點變成了橫槓。
右鍵單擊此文件,在彈出菜單中選擇“Cut”。
然後找到“app/src/main/jni”目錄,在jni文件夾上右鍵單擊,在彈出菜單中選擇“Paste”,在接下來的對話框中什麼也不改直接點“OK”,你會看到頭文件已經被轉移到了jni文件夾中,與我們先前建立的源文件“myCode.cpp”文件在一起。


\



雙擊此文件,可以看到它的代碼:


\



C++的普通語法不解釋了,這裡特別的一段是:


JNIEXPORT jint JNICALL Java_myself_exercise_ndktest_Load_addInt
(JNIEnv *, jobject, jint, jint);


它聲明了方法“addInt”,並指定了可以調用它的java包是“myself.exercise.ndktest”、可以調用它的java類是“Load”。
“jint”與“JNICALL”之間有一個紅色警告,不管它,這是假報錯。
上面有一句“extern "C"”,表明此方法是向外輸出的,是給java程序調用的。
在這裡,我們什麼也不用改。


十、編輯源文件


雙擊“myCode.cpp”打開其代碼編輯窗口,寫入以下代碼:


#include "myself_exercise_ndktest_Load.h"


JNIEXPORT jint JNICALL Java_myself_exercise_ndktest_Load_addInt (JNIEnv *, jobject, jint a, jint b) {
return a + b;
}


這是方法“addInt”的實現——兩個整數相加,得到一個整數結果。

十一、調用庫方法


剛才在“Load.java”中加載了庫,調用了庫方法,現在在主窗口中調用“Load.java”所調用的這個方法。
主窗口的代碼剛才已經貼過了,現在,在“點按鈕以後發生的事”這句話下面添加:


Load load = new Load();
int r = load.addInt(100, 50);
textView.setText("C++庫計算“100+50”的結果是:" + String.valueOf(r));


整個過程是:
1. 調試或發布時,“myCode.cpp”將被編譯為庫“lb”。
2. “Load”加載了庫“lb”,調用了庫中的方法“addInt”。
3. 主窗口的按鈕點擊事件,通過“Load”調用這個方法,算出100+50等於多少。


十二、調試


手機連好,點界面上方的調試按鈕\
過一會兒彈出一個窗口,在其中選擇已連好的手機,點“OK”,等一會兒到手機上看結果。
不出意外的話,手機上會出現剛做的軟件,點其中的按鈕會顯示C++計算的結果:


\

【練習小結】(只說C++的部分)

一、改gradle代碼



1.“gradle/wrapper/gradle-wrapper.properties”中,“2.4”改成“2.5”。
2.“build.gradle”中,“:1.3.0”改為“-experimental:0.2.0”。
3.“app/build.gradle”中:



apply plugin: 'com.android.model.application'//紅色代碼是新加的

model {
android {
compileSdkVersion=23//這些數字是軟件根據你選擇的版本自動寫的,不必改
buildToolsVersion="23.0.1"
defaultConfig.with{
applicationId="myself.exercise.ndktest"//這是程序包名,用你自己的
          minSdkVersion.apiLevel =11
targetSdkVersion.apiLevel =23
versionCode=1
versionName="1.0"
}
tasks.withType(JavaCompile) {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
}
android.ndk {
moduleName ="lb"//這是將來so文件的名稱,自己取
}
android.buildTypes {//藍色代碼是移動了的
release {
minifyEnabled=true
proguardFiles.add(file("proguard-rules.pro"))//紫色代碼是改過了的
}
}
}
dependencies {
compile fileTree(dir:'libs', include: ['*.jar'])
//testCompile 'junit:junit:4.12'//灰色代碼的句子,你看到了就刪
compile'com.android.support:appcompat-v7:23.0.1'
//compile 'com.android.support:design:23.0.1'
}


二、更新gradle

“Sync Now”、“Gradle settings”,找gradle-2.5。


三、創建jni文件夾,在此文件夾中創建C++源文件*.cpp

四、建一個新的java類,用來加載C++庫


代碼模板:
package myself.exercise.ndktest;
public class Load {
static {
System.loadLibrary("lb");
}
public native int addInt(int a, int b);
}


五、生成頭文件*.h


主菜單“Build > Make Project”。
Terminal窗口中:
cd app/build/intermediates/classes/debug回車
javah -jni myself.exercise.ndktest.Load回車
把頭文件挪到jni文件夾中與源文件*.cpp在一起。


六、編輯源文件*.cpp


代碼模板:
#include "myself_exercise_ndktest_Load.h"
JNIEXPORT jint JNICALL Java_myself_exercise_ndktest_Load_addInt (JNIEnv *, jobject, jint a, jint b) {
return a + b;
}


七、調用庫方法


代碼模板:
Load load = new Load();
int r = load.addInt(100, 50);


【更多的jni函數】

剛才處理的只是整數,現在擴展一下。
首先“Load.java”增加了一些函數聲明,先在這裡聲明,以後在C++中實現:


package myself.exercise.ndktest;


public class Load {
static {
System.loadLibrary("lb");
}
public native int addInt(int a, int b); //輸入整數,輸出整數
public native double mulDouble(double a, double b); //輸入實數,輸出實數
public native boolean bigger(float a, float b); //輸入float型實數,輸出布爾值
public native String addString(String a, String b); //輸入字符串,輸出字符串
public native int[] intArray(int[] a); //輸入整數數組,輸出整數數組
public native double[] doubleArray(double[] a); //輸入實數數組,輸出實數數組
public native String[] stringArray(String[] a); //輸入字符串數組,輸出字符串數組
}


然後主窗口增加了一些組件,用來測試這些函數:


\



為了省事,你可以直接把下述代碼拷到“activity_main_xml”中去:



xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">


android:layout_height="wrap_content"
android:id="@+id/textView"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="111dp" />


android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="addInt"
android:id="@+id/button1"
android:layout_marginTop="41dp"
android:layout_below="@+id/textView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />


android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editText1"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:width="150dp" />


android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/editText2"
android:width="200dp"
android:layout_alignTop="@+id/editText1"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />


android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="mulDouble"
android:id="@+id/button2"
android:layout_above="@+id/button3"
android:layout_alignLeft="@+id/button4"
android:layout_alignStart="@+id/button4" />


android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="intArray"
android:id="@+id/button3"
android:layout_below="@+id/button1"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />


android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="doubleArray"
android:id="@+id/button4"
android:layout_below="@+id/button2"
android:layout_centerHorizontal="true" />


android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="stringArray"
android:id="@+id/button5"
android:layout_below="@+id/button3"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />


android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="addString"
android:id="@+id/button6"
android:layout_below="@+id/button5"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />





然後重新生成頭文件(因為新增函數的聲明需要寫在頭文件中,我們自己寫,不如讓Android Studio替我們寫),具體做法是:
再走一遍“Build > Make Project”。
在Terminal窗口中再來一遍:
(如果沒有進入“debug”目錄就)cd app/build/intermediates/classes/debug回車
javah -jni myself.exercise.ndktest.Load回車


“debug”目錄又冒出了一個頭文件“myself_exercise_ndktest_Load.h”,和先前做的頭文件名字一樣。
把它挪到jni文件夾中,覆蓋原來那個頭文件。
新的頭文件中已經包含了新方法的C++聲明:


\



上面又出現了***警告,這是又需要合成gradle了,點右邊的“Sync Now”即可。
對頭文件本身,什麼都不用改。
要改的是源文件“myCode.cpp”,改成這樣:


#include "myself_exercise_ndktest_Load.h"
#include


JNIEXPORT jint JNICALL Java_myself_exercise_ndktest_Load_addInt (JNIEnv *, jobject, jint a, jint b) {
return a + b; //輸入整數,輸出整數
}
JNIEXPORT jdouble JNICALL Java_myself_exercise_ndktest_Load_mulDouble (JNIEnv *, jobject, jdouble a, jdouble b) {
return a * b; //輸入實數,輸出實數
}
JNIEXPORT jboolean JNICALL Java_myself_exercise_ndktest_Load_bigger (JNIEnv *, jobject, jfloat a, jfloat b) {
return a > b; //輸入float型實數,輸出布爾值,判斷a是否大於b
}
char * JstringToCstr(JNIEnv * env, jstring jstr) { //jstring轉換為C++的字符數組指針
char * pChar = NULL;
jclass classString = env->FindClass("java/lang/String");
jstring code = env->NewStringUTF("GB2312");
jmethodID id = env->GetMethodID(classString, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray arr = (jbyteArray)env->CallObjectMethod(jstr, id, code);
jsize size = env->GetArrayLength(arr);
jbyte * bt = env->GetByteArrayElements(arr, JNI_FALSE);
if(size > 0) {
pChar = (char*)malloc(size + 1);
memcpy(pChar, bt, size);
pChar[size] = 0;
}
env->ReleaseByteArrayElements(arr, bt, 0);
return pChar;
}
JNIEXPORT jstring JNICALL Java_myself_exercise_ndktest_Load_addString (JNIEnv * env, jobject, jstring a, jstring b) {
char * pA = JstringToCstr(env, a); //取得a的C++指針
char * pB = JstringToCstr(env, b); //取得b的C++指針
return env->NewStringUTF(strcat(pA, pB)); //輸出a+b
}
JNIEXPORT jintArray JNICALL Java_myself_exercise_ndktest_Load_intArray (JNIEnv * env, jobject, jintArray a) {
//輸入整數數組,將其每個元素加10後輸出
//輸入值為a,輸出值為b
int N = env->GetArrayLength(a); //獲取a的元素個數
jint * pA = env->GetIntArrayElements(a, NULL); //獲取a的指針
jintArray b = env->NewIntArray(N); //創建數組b,長度為N
jint * pB = env->GetIntArrayElements(b, NULL); //獲取b的指針
for (int i = 0; i < N; i++) pB = pA + 10; //把a的每個元素加10,賦值給b中對應的元素
/*//另一種方法
env->SetIntArrayRegion(b, 0, N, pA); //b是a的復制品
for (int j = 0; j < N; j++) pB[j] += 10; //b的每個元素加10
*/
env->ReleaseIntArrayElements(a, pA, 0); //釋放a的內存
env->ReleaseIntArrayElements(b, pB, 0); //釋放b的內存
return b; //輸出b
}
JNIEXPORT jdoubleArray JNICALL Java_myself_exercise_ndktest_Load_doubleArray (JNIEnv * env, jobject, jdoubleArray a) {
//輸入實數數組,將其每個元素乘2後輸出
//輸入值為a,輸出值為b
int N = env->GetArrayLength(a); //獲取a的元素個數
jdouble * pA = env->GetDoubleArrayElements(a, NULL); //獲取a的指針
jdoubleArray b = env->NewDoubleArray(N); //創建數組b,長度為N
jdouble * pB = env->GetDoubleArrayElements(b, NULL); //獲取b的指針
for (int i = 0; i < N; i++) pB= pA* 2; //把a的每個元素乘2,賦值給b中對應的元素
/*//另一種方法
env->SetDoubleArrayRegion(b, 0, N, pA); //b是a的復制品
for (int j = 0; j < N; j++) pB[j] *= 2; //b的每個元素乘2
*/
env->ReleaseDoubleArrayElements(a, pA, 0); //釋放a的內存
env->ReleaseDoubleArrayElements(b, pB, 0); //釋放b的內存
return b; //輸出b
}
JNIEXPORT jobjectArray JNICALL Java_myself_exercise_ndktest_Load_stringArray (JNIEnv * env, jobject, jobjectArray a) {
//輸入字符串數組,顛倒順序後輸出
//輸入值為a,輸出值為b
int N = env->GetArrayLength(a); //獲取a的元素個數
jobjectArray b = env->NewObjectArray(N, env->FindClass("java/lang/String"), env->NewStringUTF("")); //創建數組b,長度為N
for (int i = 0; i < N; i++) { //對於a中的每個元素
jstring ai = (jstring)env->GetObjectArrayElement(a, i); //讀出這個元素的值
env->SetObjectArrayElement(b, N - 1 - i, ai); //賦值給b中倒序對應的元素
env->DeleteLocalRef(ai);//釋放內存
}
return b; //輸出b
}


再把主窗口代碼改成這樣:


package myself.exercise.ndktest;


import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;


public class MainActivity extends Activity {


Load load = new Load();
EditText editText1; EditText editText2;
TextView textView;
Button button1; Button button2; Button button3;
Button button4; Button button5; Button button6;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);


editText1 = (EditText)findViewById(R.id.editText1); //左邊的文字輸入框,以下簡稱“左框”
editText2 = (EditText)findViewById(R.id.editText2); //右邊的文字輸入框,以下簡稱“右框”
textView = (TextView)findViewById(R.id.textView);
button1 = (Button)findViewById(R.id.button1);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//按鈕“addInt”用來計算左框裡的整數和右框裡的整數相加
//你要是不輸入整數,它就當你輸入了“0”
int a = 0;
try { a = Integer.parseInt(String.valueOf(editText1.getText())); }
catch (NumberFormatException e) { }
int b = 0;
try { b = Integer.parseInt(String.valueOf(editText2.getText())); }
catch (NumberFormatException e) { }
int r = load.addInt(a, b);
textView.setText("C++庫計算" + a + "+" + b +"的結果是:" + r);
}
});
button2 = (Button)findViewById(R.id.button2);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//按鈕“mulDouble”用來計算左框裡的實數和右框裡的實數相乘
//你要是不輸入數字,它就當你輸入了“0”
double a = 0;
try { a = Double.parseDouble(String.valueOf(editText1.getText())); }
catch (NumberFormatException e) { }
double b = 0;
try { b = Double.parseDouble(String.valueOf(editText2.getText())); }
catch (NumberFormatException e) { }
double r = load.mulDouble(a, b);
textView.setText("C++庫計算" + a + "*" + b +"的結果是:" + r);
}
});
button3 = (Button)findViewById(R.id.button3);
button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//按鈕“intArray”用來計算左框裡的整數數組每個元素加10會變成什麼,結果放在右框裡
//輸入數組時,元素之間用英文逗號隔開,比如“1,2,3”
//對於每個元素,你要是不輸入整數,它就當你輸入了“0”
//注意不要用中文逗號,不然它無法識別
String str = String.valueOf(editText1.getText());
String[] strArray;
if (str.contains(",")) strArray = str.split(",");
else strArray = new String[] { str };
int N = strArray.length;
int[] array = new int[N];
for (int i = 0; i < N; i++) {
int t = 0;
try { t = Integer.parseInt(strArray); }
catch (NumberFormatException e) { }
array= t;
}
int[] newArray = load.intArray(array);
str = "";
for (int i = 0; i < N; i++) {
if (i < N - 1) str += String.valueOf(newArray) + ",";
else str += String.valueOf(newArray);
}
editText2.setText(str);
textView.setText("C++庫用左邊的數組計算,結果在右邊");
}
});
button4 = (Button)findViewById(R.id.button4);
button4.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//按鈕“doubleArray”用來計算左框裡的實數數組每個元素乘2會變成什麼,結果放在右框裡
//輸入數組時,元素之間用英文逗號隔開,比如“2.5,3.14,8”
//對於每個元素,你要是不輸入數字,它就當你輸入了“0”
//注意不要用中文逗號,不然它無法識別
String str = String.valueOf(editText1.getText());
String[] strArray;
if (str.contains(",")) strArray = str.split(",");
else strArray = new String[] { str };
int N = strArray.length;
double[] array = new double[N];
for (int i = 0; i < N; i++) {
double t = 0;
try { t = Double.parseDouble(strArray); }
catch (NumberFormatException e) { }
array= t;
}
double[] newArray = load.doubleArray(array);
str = "";
for (int i = 0; i < N; i++) {
if (i < N - 1) str += String.valueOf(newArray) + ",";
else str += String.valueOf(newArray);
}
editText2.setText(str);
textView.setText("C++庫用左邊的數組計算,結果在右邊");
}
});
button5 = (Button)findViewById(R.id.button5);
button5.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//按鈕“stringArray”用來把左框裡的字符串數組顛倒順序,結果放在右框裡
//輸入數組時,元素之間用英文逗號隔開,比如“China,USA,England”
//注意不要用中文逗號,不然它把這個逗號當成一個字符,與左右字符串相連成為數組中的一個元素
String str = String.valueOf(editText1.getText());
String[] strArray;
if (str.contains(",")) strArray = str.split(",");
else strArray = new String[] { str };
int N = strArray.length;
String[] newArray = load.stringArray(strArray);
str = "";
for (int i = 0; i < N; i++) {
if (i < N - 1) str += newArray+ ",";
else str += newArray;
}
editText2.setText(str);
textView.setText("C++庫顛倒了左邊數組的順序,結果在右邊");
}
});
button6 = (Button)findViewById(R.id.button6);
button6.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//按鈕“addString”用來把左框裡的字符串與右框裡的字符串相加
String a = String.valueOf(editText1.getText());
String b = String.valueOf(editText2.getText());
String r = load.addString(a, b);
textView.setText("C++庫計算" + a + "+" + b +"的結果是:" + r);
}
});
}
}


好了,調試。
比如我在左邊輸入“1,2,3,4”,點“intArray”按鈕,右邊就會是“11,12,13,14”,這是C++處理數組的結果,還可以試其他按鈕,詳細情況在主窗口代碼的注釋中。


\

【更多更多的jni函數】
基本的函數,語句是相似的,比如取得數組指針,對於int數組,這個函數是GetIntArrayElements,對於byte數組,就是GetByteArrayElements,把Int改成Byte就行了。下面用byte總結一下最常用的函數:


private jobjectArray arrToArrArr(JNIEnv * env, jbyteArray arr) {
int N = env->GetArrayLength(arr); //取得數組長度
jbyte * pArr = env->GetByteArrayElements(arr , NULL); //取得數組指針
int M = 16;
jobjectArray arrArr = env->NewObjectArray(M, env->FindClass("[B"), NULL); //創建二維數組
for (int i2 = 0; i2 < M; i2++) {
jbyteArray arrCopy = env->NewByteArray(N); //創建一維數組
jbyte * pArrCopy = env->GetByteArrayElements(arrCopy, NULL); //取得相應指針
for (int j = 0; j < N; j++) pArrCopy[j] = pArr[j] + 1; //一維數組元素操作
env->SetObjectArrayElement(arrArr, i2, arrCopy); //二維數組元素操作
env->ReleaseByteArrayElements(arrCopy, pArrCopy, 0); //釋放一維數組指針
env->DeleteLocalRef(arrCopy); //釋放對它的引用
}
jobject obj = env->GetObjectArrayElement(arrArr, 0); //取二維數組的元素
jbyteArray newArr = (jbyteArray)obj; //強制轉換
int thisN = env->GetArrayLength(newArr);
jbyte * pNewArr = env->GetByteArrayElements(newArr, NULL); //取指針
for (int i3 = 0; i3 < thisN; i3++) pNewArr[i3] += 2; //元素操作
env->SetObjectArrayElement(arrArr, M - 1, newArr); //用修改後的一維數組替代二維數組的最後一行
env->ReleaseByteArrayElements(newArr, pNewArr, 0); //釋放這個一維數組的指針
env->DeleteLocalRef(newArr); //釋放對它的引用
env->ReleaseByteArrayElements(arr, pArr, 0); //釋放最初那個一維數組的指針
return arrArr;
}
JNIEXPORT jobjectArray JNICALL Java_myself_exercise_ndktest_Load_arrToArrArr(JNIEnv * env, jobject, jbyteArray arr) {
return arrToArrArr(env, arr);
}


jni中的基本數據類型有:
jboolean:對應於C++的bool、java的boolean
jbyte:對應於C++和java的byte
jchar:對應於C++和java的char
jshort:對應於C++和java的short
jint:對應於C++和java的int
jlong:對應於C++和java的long
jfloat:對應於C++和java的float
jdouble:對應於C++和java的double
帶“JNIEXPORT”的外聯語句應該使用“j”字頭數據類型,而在jni內部,對於基本數據類型,一般來說既可以用jni式的,也可以用C++式的,比如定義一個名叫“flag”的變量,可以用“jboolean flag = false”,也可以用“bool flag = false”,涉及到整數,可以用“jint”,也可以用“int”。但byte是個例外,jni不支持“byte”這個關鍵詞,只能用“jbyte”。


數組類型有jbooleanArray、jbyteArray、jcharArray、jshortArray、jintArray、jlongArray、jfloatArray、jdoubleArray,實際上就是每個基本類型都有一個Array。它們的函數寫法也是類似的,比如上面那段代碼,把“byte”統一替換成“int”,把“Byte”統一替換成“Int”,就是jint型的函數了(不過涉及到二維數組時,“FindClass("[B")”要改成“FindClass("[I")”)。
jni數組可以寫成C++指針,比如:


jcharArray chArr = env->NewCharArray(10);
jchar * pChArr = env->GetCharArrayElements(chArr, NULL);
jintArray intArr = env->NewIntArray(10);
jint * pIntArr = env->GetIntArrayElements(intArr, NULL);
for (int i = 0; i < 10; i++) {
pChArr = 65 + i;
pIntArr = pChArr + 100;
}
env->ReleaseCharArrayElements(chArr, pChArr, 0);
env->ReleaseIntArrayElements(intArr, pIntArr, 0);


可以寫成C++式的:


char chArr[10];
int intArr[10];
for (int i = 0; i < 10; i++) {
*(chArr + i) = 65 + i;
*(intArr + i) = *(chArr + i) + 100;
}
free(chArr);
free(intArr);


但不能、也不可能寫成java式的,像“char[] chArr = new char[10]”這樣的句子進去以後直接就報錯了,就變紅了。
字符串是比較特別的類型,按理說它是字符數組,但它的函數寫法與眾不同:


jstring str = env->NewStringUTF("abcde"); //創建字符串
const jchar * pJchars = env->GetStringChars(str, NULL); //獲得Unicode編碼的字符串指針
const char * pChars = env->GetStringUTFChars(str, NULL); //獲得UTF-8編碼的字符串指針
const jbyte * pBytes = env->GetStringUTFChars(str, NULL); //獲得與UTF-8編碼的字符串等價的byte數組指針
int N = env->GetStringLength(str); //獲得Unicode編碼的字符串長度
int start = 0; int n = N - 1;
jchar * jchBuf;
env->GetStringRegion(str, start, n, jchBuf); //從序號為start的字符開始,把n個字符復制給Unicode編碼的jchar數組
int utfN = env->GetStringUTFLength(str); //獲得UTF-8編碼的字符串長度
char * chBuf;
env->GetStringUTFRegion(str, start, n, chBuf); //從序號為start的字符開始,把n個字符復制給UTF-8編碼的char數組
const jchar * cr = env->GetStringCritical(str, NULL); //獲得編碼格式的字符串指針
env->ReleaseStringChars(str, pJchars); //由上述函數產生的各種指針的釋放
env->ReleaseStringUTFChars(str, pChars);
env->ReleaseStringUTFChars(str, pBytes);
env->ReleaseStringChars(str, jchBuf);
env->ReleaseStringUTFChars(str, chBuf);
env->ReleaseStringCritical(str, cr);


並不能用string來代替jstring,因為jni中根本就沒有string這個寫法。但對於char*指針,可以用C++的函數,比如strcpy。
一個簡單的例子,注冊校驗中常用的,比較兩個字符串是否相同:


bool sameJstring(JNIEnv *env, jstring a, jstring b) {
if (a == NULL) {
if (b == NULL) return true;
else return false;
} else {
if (b == NULL) return false;
else {
int aN = env->GetStringUTFLength(a);
int bN = env->GetStringUTFLength(b);
if (aN <= 0) {
if (bN <= 0) return true;
else return false;
} else {
if (bN <= 0) return false;
else {
if (aN != bN) return false;
else {
const char * pA = env->GetStringUTFChars(a, NULL); //得到這兩個字符串的指針
const char * pB = env->GetStringUTFChars(b, NULL);
bool result = true;
for (int i = 0; i < aN; i++) {
if (pA != pB) { //指到每個位置的字符,進行比較
result = false;
break;
}
}
env->ReleaseStringUTFChars(a, pA);
env->ReleaseStringUTFChars(b, pB);
return result;
}
}
}
}
}
}


更多的例子到網上搜吧,寫不下了。
網上很多例子是老版本NDK的,與Eclipse合作的,有很多這樣的句子:“(*env)->函數名(env,其他參數)”。這個,到了本文所說的NDK版本中,統一替換成“env->函數名(其他參數)”,就是把前面那個“env”兩邊的括號和星號去掉,把參數中那個“env,”去掉。
有時候這樣還報錯,你可以考慮是不是表達式左右的類型不匹配,試試強制轉換,比如老版本的“jobjectArray arr = (*env)->GetObjectField(env, packageInfo, fieldID);”改成“jobjectArray arr = env->GetObjectField(packageInfo, fieldID_signatures);”之後還報錯,就試試“jobjectArray arr = (jobjectArray)env->GetObjectField(packageInfo, fieldID_signatures);”。
但有些老句子是徹底不行了,比如“int iReturn = __system_property_get(key, value);”調試時報錯“Error:(87) undefined reference to `__system_property_get'”,這是因為NDK10已經把“__system_property_get”廢了,這沒辦法。
有一類例子,幾乎從來沒人把它說清楚,就是引用java的Context。
比如說查簽名,這得通過java查,要把java的環境(Context)導進來,才知道裡面的東西比如簽名是什麼樣的。在網上的例子中,這類函數都有一個參數“jobject context”,有時候寫成“jobject obj”之類的,但是,從來就沒有人說清楚這個context或obj從哪來,很容易讓人誤解為是這樣:


jstring getSignature(JNIEnv *env, jobjectcontext) {
jstring currentSignature;
//獲取簽名
return currentSignature;
}
Jboolean compareSignatures(JNIEnv *env, jstring legalSignature, jstring currentSignature) {
jboolean result;
//比較正確的簽名和當前的簽名
return result;
}
JNIEXPORT Jboolean JNICALL Java_myself_exercise_ndktest_Load_compareSignatures(JNIEnv * env, jobjectcontext) {
return compareSignatures(env,context); //以為上面的context是從這兒來的,錯!
}


其實,這個context需要一個專門的函數,從java中調用:


/*獲取當前應用的Context*/
jobjectgetContext(JNIEnv *env) {
jclass activityThread = env->FindClass("android/app/ActivityThread");
jmethodID currentActivityThread = env->GetStaticMethodID(activityThread, "currentActivityThread", "()Landroid/app/ActivityThread;");
jobject at = env->CallStaticObjectMethod(activityThread, currentActivityThread);
jmethodID getApplication = env->GetMethodID(activityThread, "getApplication", "()Landroid/app/Application;");
jobject context = env->CallObjectMethod(at, getApplication);
return context;
}
/*獲取當前應用簽名的Hashcode*/
int getSignatureHashcode(JNIEnv *env) {
jobjectcontext= getContext(env); //context是這麼來的!
jclass contextClass = env->GetObjectClass(context);
jmethodID methodID_getPackageManager = env->GetMethodID(contextClass, "getPackageManager", "()Landroid/content/pm/PackageManager;");
jobject packageManager = env->CallObjectMethod(context, methodID_getPackageManager);
jclass packageManagerClass = env->GetObjectClass(packageManager);
jmethodID methodID_getPackageInfo = env->GetMethodID(packageManagerClass, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jmethodID methodID_getPackageName = env->GetMethodID(contextClass, "getPackageName", "()Ljava/lang/String;");
jstring packageName = (jstring)env->CallObjectMethod(context, methodID_getPackageName);
jobject packageInfo = env->CallObjectMethod(packageManager, methodID_getPackageInfo, packageName, 64);
jclass packageinfoClass = env->GetObjectClass(packageInfo);
jfieldID fieldID_signatures = env->GetFieldID(packageinfoClass, "signatures", "[Landroid/content/pm/Signature;");
jobjectArray signature_arr = (jobjectArray)env->GetObjectField(packageInfo, fieldID_signatures);
jobject signature = env->GetObjectArrayElement(signature_arr, 0);
jclass signatureClass = env->GetObjectClass(signature);
jmethodID methodID_hashcode = env->GetMethodID(signatureClass, "hashCode", "()I");
jint hashCode = env->CallIntMethod(signature, methodID_hashcode);
return hashCode;
}
/*獲取當前應用簽名的jstring*/
jstring getSignature(JNIEnv *env) {
jobjectcontext= getContext(env);//context是這麼來的!
jclass contextClass = env->GetObjectClass(context);
jmethodID methodID_getPackageManager = env->GetMethodID(contextClass, "getPackageManager", "()Landroid/content/pm/PackageManager;");
jobject packageManager = env->CallObjectMethod(context, methodID_getPackageManager);
jclass packageManagerClass = env->GetObjectClass(packageManager);
jmethodID methodID_getPackageInfo = env->GetMethodID(packageManagerClass, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jmethodID methodID_getPackageName = env->GetMethodID(contextClass, "getPackageName", "()Ljava/lang/String;");
jstring packageName = (jstring)env->CallObjectMethod(context, methodID_getPackageName);
jobject packageInfo = env->CallObjectMethod(packageManager, methodID_getPackageInfo, packageName, 64);
jclass packageinfoClass = env->GetObjectClass(packageInfo);
jfieldID fieldID_signatures = env->GetFieldID(packageinfoClass, "signatures", "[Landroid/content/pm/Signature;");
jobjectArray signature_arr = (jobjectArray)env->GetObjectField(packageInfo, fieldID_signatures);
jobject signature = env->GetObjectArrayElement(signature_arr, 0);
jclass signatureClass = env->GetObjectClass(signature);
jmethodID methodID_toCharsString = env->GetMethodID(signatureClass, "toCharsString", "()Ljava/lang/String;");
return (jstring)env->CallObjectMethod(signature, methodID_toCharsString);
}
/*比較兩個jstring是否相同*/
bool sameJstring(JNIEnv *env, jstring a, jstring b) {
if (a == NULL) {
if (b == NULL) return true;
else return false;
} else {
if (b == NULL) return false;
else {
int aN = env->GetStringUTFLength(a);
int bN = env->GetStringUTFLength(b);
if (aN <= 0) {
if (bN <= 0) return true;
else return false;
} else {
if (bN <= 0) return false;
else {
if (aN != bN) return false;
else {
const char * pA = env->GetStringUTFChars(a, NULL);
const char * pB = env->GetStringUTFChars(b, NULL);
bool result = true;
for (int i = 0; i < aN; i++) {
if (pA != pB) {
result = false;
break;
}
}
env->ReleaseStringUTFChars(a, pA);
env->ReleaseStringUTFChars(b, pB);
return result;
}
}
}
}
}
}
/*判斷是否正版(簽名未被篡改)*/
jboolean compareSignatures(JNIEnv *env) {
///比對整個簽名
jstring signature = getSignature(env);
jstring legalSignature = env->NewStringUTF("把這個字符串替換成你自己程序的正確簽名");
return sameJstring(env, signature, legalSignature);
///只比對簽名Hashcode
/*jint hashCode = getSignatureHashcode(env);
jint legalHashCode = 0; //把這個“0”替換成你自己程序的正確簽名的HashCode
return hashCode == legalHashCode;*/
}
/*JNIEXPORT那一段就不要了,比較的結果不要輸出給java,就在jni內部使用。
而且,在實際應用中,簡單地返回一個jboolean是不行的,讓人找到改成true就破解了。即使在jni內部,變量也可以被修改。能增加破解難度的辦法是把校驗過程包在一個加密的區塊中,結果不外露,向外輸出的只是一些被它控制的變量,這些變量的值是不固定的、有無限的取值范圍可以讓程序正常運轉、又有無限的取值范圍讓程序混亂,因此別人無法通過修改這些變量來達到正版,他破解的唯一辦法是把這個區塊的秘鑰找到。
還有,這些函數的名稱,不要用傻乎乎的“getSignature”之類,要用誰也看不懂的詞,代碼中也不要出現“感謝你使用正版”之類不打自招的字符串,即使區塊已經加密,也要保持良好的習慣。
有人也許要問:區塊加密之後肯定要重新簽名,簽名變了就得回jni裡面修改,修改過後又得重新加密,加密過後簽名又變了……別說了,下載一個可以調用作者jks的簽名工具就行了(比如360加固保的簽名工具),簽名會恢復原狀的*/

【jni的注意事項】

最重要的是釋放內存。jni不像java那樣保姆,不會幫你打掃房間的,垃圾要自己掃。
基本數據類型不用掃,但引用類型(jstring、帶“array”的)用過之後必須掃。不掃的結果是什麼呢,不像在家裡一樣躺在垃圾堆裡還能睡著,jni循環幾輪就可能死給你看。
清掃垃圾大致的規律是:有指針就要掃。因此可以這樣:寫完一段代碼,從上往下搜*符號,有一個算一個都在用完後釋放。對於jni指針(jintArray之類的),釋放語句都是帶“Release”的(如env->ReleaseIntArrayElements);對於純C++指針,用free來釋放,前面都舉過例子。
特別提醒:用完才釋放,別沒用完就把人家給滅了那就查不出什麼來了。
其實jni非常脆弱,即使釋放,循環次數多了也吃不消。你會遇到這種情況:某個復雜的函數,裡面有N多循環,動用了N多指針,你打算從0算到1000,在手機上一調試,到100就死球了,甚至到20就跳出“已停止”的訃告,這還算好的,要是按鈕按下後永遠起不來直到人工重啟,那才氣人。這種情況很可能是超負荷了,那就小批小批地傳給jni計算,它有個特點,執行完java傳來的一個任務會清理一次內存,比代碼中的“Release”清理得徹底,所以你讓它多和java對話,內存就“時時勤拂拭,勿使惹塵埃”了。
但也有可能是代碼本身的問題,這在jni裡很難查。Jni打不了斷點,調試的勞動量可想而知。那就不要在jni裡寫太多的東西,當然我們也不指望把整個軟件都搬到jni裡去。還有一個好習慣,代碼先在Android裡調試,邏輯上沒問題了再改成jni格式搬進cpp文件裡,這時候再出問題就是jni或C++的問題,而且經我使用,函數本身的bug是很少的,出問題幾乎總是內存崩潰,解決的辦法也就很簡單了,分段給。
jni內部的全局變量,用數組會出問題,重復調用它會死機,但基本數據類型不怕這個。
用到jni的Android程序還有個怪癖,不讓改目錄。你改了以後,它會失去項目結構,打開後就無法顯示目錄樹也無法調試,即使你恢復了原來的文件夾名,它也無法恢復了。在做jni之前,就要想好文件目錄,不變了。時時備份也是必須的,google自己都說,NDK是個實驗性的東西,我們就更不知道它會出什麼妖蛾子。

【發布】

像普通的Android項目一樣,“Build > Generate Signed APK”,按提示進行。
結果有什麼不同呢?剛才那些C++代碼體現在哪裡呢?你找到發布的apk文件,用WinRAR打開,裡面的lib文件夾有一堆子文件夾,每個裡面都有一個後綴為so的文件,它就是C++的輸出結果。之所以有這麼多so,是因為手機CPU有多種,一部手機打開程序時會自動尋找與它匹配的so庫來加載。
即使沒有發布,在調試後,項目的“app\build\outputs\apk”目錄下也會有調試版的apk生成,裡面同樣有那些裝著so文件的文件夾。
因此,so文件是裹在apk裡面發給用戶的,並不像Windows的dll那樣放在可執行文件外面。
對於so文件的調用,Android做得比較周到,該鏈接的都在java和C++代碼裡接好了,不會出現Windows經常出現的那種動態鏈接庫又依賴其他庫而客戶的機器上沒有這些庫而無法打開的情況。
但是有一點不方便,Android的so庫不能單獨制作,至少目前官網上說還不支持單獨生成so庫,它必須在Android項目裡與java代碼一起編譯,這樣就很慢,而且要改一點點東西也要把整個包含java和C++的項目都打開。
有一種叫“VisualGDB”的軟件,可以作為VisualStudio的插件來編寫帶C++的Android項目,寫的過程要快些了,但輸出一樣慢,因為它還是在用Android的SDK。而且它編代碼的自由度比AndroidStudio差很遠。






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