都知道的,Android基於Linux系統,而後覆蓋了一層由Java虛擬機爲核心的殼系統。跟通常常見的Linux+Java系統不一樣的,是其中有對硬件驅動進行支持,以避開GPL開源協議限制的HAL硬件抽象層。
大多數時候,咱們使用JVM語言進行編程,好比傳統的Java或者新貴Kotlin。碰到對速度比較敏感的項目,好比遊戲,好比視頻播放。咱們就會用到Android的JNI技術,使用NDK的支持,利用C++開發高計算量的模塊,供給上層的Java程序調用。
本文先從一個最簡單的JNI例子來開始介紹Android中Java和C++的混合編程,隨後再介紹Android直接調用ELF命令行程序的規範方法,以及調用混合了第三方庫略微複雜的命令行程序。java
第一個配置是安裝Android的SDK,這是開發Android程序必須的。
進入Android Studio的設置界面,Mac的快捷鍵是Command
+,
,Windows和Linux版本請自行從菜單中選擇。
在設置界面中,從左側順序選擇:Appearance&Behavior -> System Settings -> Android SDK,能夠進入到SDK的設置。
右側的SDK版本列表中,最前面顯示了✔️或者後面顯示了Installed,表示該版本的SDK已經安裝。一般若是沒有特殊須要,只安裝1個最新版本的SDK便可。圖中我是由於某些項目特殊的要求,安裝了兩個特定不一樣版本的SDK。
但願安裝某版本的SDK,只要點選相應行最前面的多選框,而後單擊右下角確認按鈕便可安裝。
若是不是本身從頭開始,而是接手了其餘開發人員的源碼,源碼中可能指定了特定版本的SDK。這時候能夠修改其項目配置文件中版本的設置,到你安裝的SDK版本。更簡單的方法是直接在這裏安裝對應的SDK,防止由於版本依賴出現的不少繁瑣問題。android
第二個配置的是NDK,還在剛纔SDK設置的界面中,點擊界面上側中間的「SDK Tools」標籤,能夠進入到NDK設置的界面。
NDK的設置沒有那麼多的選擇,只要安裝就好,已經安裝碰到有新版本,也能夠隨性選擇更新或者使用老版本繼續。NDK不一樣版本間的兼容性都還不錯,大多都不用擔憂。
NDK的設置是Android開發中,Java/C混合編程須要的。c++
第三個配置是增長一個外部工具javah,這個工具是將Java編寫的「包裝」文件,轉換一個C/C++的.h文件。雖然Java/C++都是面嚮對象語言,但二者的面向對象實現是不一樣的。因此在Java中某個類的方法,轉換到C++的世界中,是使用很長的函數名來作區分。這種狀況使用手工編寫雖然效果同樣,但很容易出錯,使用javah工具則能自動完成。
在Android Studio設置界面左側的列表中,順序選擇Tools -> External Tools,單擊右側界面左下角的「+」,新建一個工具,好比就叫"javah"。
其中三個須要設置的內容分別是:git
$JDKPath$/bin/javah
,這個跟jdk安裝的路徑有關。-classpath . -jni -d $ModuleFileDir$/src/main/jni $FileClass$
,主要指定輸出路徑。$ModuleFileDir$/src/main/Java
,當前項目路徑。至此Android Studio的主要設置就完成了,固然只是最基本必須的設置,若是本身還有其它需求,相似git倉庫地址等,能夠再自行設置。
下面就能夠開始進行項目的開發。github
在Android Studio界面選擇New Project,若是是在開始界面,直接點擊主界面上的按鈕;也能夠在文件菜單中選擇。
選擇基本的Empty Activity就好。
接着是項目的設置,項目名稱、存儲位置這些都不用說了,最低的API版本決定了你的程序能夠在最低什麼版本的Android手機上執行,若是沒有特殊須要,儘可能能夠低一點,畢竟Android手機的升級比例,比iOS是低了好多倍的。
這樣,項目就創建完成,Android Studio使用標準模板,對項目作了初始化。咱們能夠在這個基礎上再添加本身的內容。golang
從屏幕左側項目文件的列表中,選擇app -> res -> layout -> acitvity_main.xml文件,文件會在右側打開,模式是交互式的界面設計器。在其中,按照下圖的樣子,咱們增長一個TextView控件和一個按鈕。文本框是爲了未來顯示輸出的結果,按鈕固然就是開始執行的觸發器。
TextView控件咱們修改一下名字,叫textView1。按鈕的名字改成button1,另外爲按鈕的onClick屬性增添一個調用:bt1_click。
界面部分就完成了,記着存盤,而後能夠關掉這個文件。編程
這時候,Android Studio界面會顯示在MainActivity.java文件的位置。這是新建項目以後自動打開的文件,也是這個項目的主窗口程序文件。咱們首先編輯窗口布局文件的時候,這個文件被隱藏在了後面。
咱們在文件的庫引用部分,增長以下兩行:小程序
import android.widget.TextView; import android.view.View;
這兩行是咱們接下來的程序會使用到的庫引用。
在類的變量聲明部分,增長這樣兩行:數組
TextView textview1; int c=0;
第一行是聲明一個文本框,用於關聯到剛纔界面編輯器中加入的文本框。
c變量就是一個簡單的計數器,咱們但願每點擊一次按鈕,這個計數器累加1,從而確認咱們每次點擊都被響應了,而不是程序沒有任何反饋給用戶。
在onCreate
函數的最後,增長關聯文本框的代碼:android-studio
textview1=(TextView)findViewById(R.id.textView1);
R.id.後面的textView1就是咱們在界面編輯的時候,爲文本框起的名字。
接着,在類的最後,增長按鈕點擊響應的處理函數:
public void bt1_click(View view){ c = c+1; textview1.setText("click:"+c); }
清晰起見,咱們把這部分完成的代碼再抄過來一遍:
package com.test.calljni; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; import android.view.View; public class MainActivity extends AppCompatActivity { TextView textview1; int c=0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textview1=(TextView)findViewById(R.id.textView1); } public void bt1_click(View view){ c = c+1; textview1.setText("click:"+c); } }
程序完成,能夠從Build菜單選擇Make Project編譯項目。而後在Run菜單選擇Run 'app'。
若是是第一次使用Android Studio,你還可能會被提醒須要你新建一個Android模擬器來執行程序。固然也能夠把打開了調試功能的Android手機插在電腦上進行真機調試。
執行的結果如圖:
點擊兩次按鈕後,畫面變爲:
好了,咱們的基本實驗平臺準備完成,下面纔是進入正題。
每一個JNI庫都分爲兩部分,一個是C++編寫的.so動態連接庫,另外一部分則是Java對這個動態連接庫的封裝。咱們先從Java部分看起。
開始寫這個JNI庫以前,咱們首先要對這個庫的整體功能、結構劃分、接口類型充分作好規劃,這樣才能保證兩種語言之間的順暢調用。由於尚沒有一種工具能夠同時有效的對兩種語言進行跟蹤調試,因此在接口部分若是碰到問題,每每只能在大量的日誌輸出中去查找線索,費時費力。
做爲一個簡單的演示,咱們的JNI庫功能很簡單,從Java封裝的角度看,咱們有一個名爲JniLib的Java類,其中包含一個方法,叫callToCpp,這個方法,將會在C++中來實現。
在文件列表中,選擇MainActivity.java所在的包名,點擊右鍵,選擇New->Java Class。
一切選用默認設置,類名爲JniLib。
Android Studio會自動生成並打開一個JniLib.java文件。其中只有一個而空白的類定義。咱們在其中繼續編寫本身的內容。
這個封裝類的代碼很是簡單,咱們直接列出所有:
package com.test.calljni; public class JniLib { static { System.loadLibrary("JniLib"); } public static native String callToCpp(); }
其中的靜態部分,至關於構造函數了,直接載入一個動態連接庫,名稱爲「JniLib」。這個是對於Java來講的庫名,實際對應的文件名將是libJniLib.so。就是說,Android在載入動態連接庫的時候,自動在給定的連接庫名稱前面添加「lib」,後面添加「.so」後綴。這個咱們在後面還會更直觀的展現。
接着是聲明一個native類型的函數,callToCpp(),native表示這個函數將在剛剛載入的libJniLib.so中實現,也就是將由C++來實現。
下面是利用這個JniLib類,生成C++使用的.h頭文件。
在Android Studio界面的左側列表中,用鼠標右鍵點擊JniLib文件,彈出菜單中選擇External Tools -> javah,這個javah就是咱們前面創建的附加工具。
此時最好將Android Studio左側的視圖從默認的「Android」方式修改到「Project」方式,這樣能更清晰的看到目錄層次關係。
隨後左側列表中,跟Java文件夾同級,會出現一個jni文件夾,其中有一個文件:com_test_calljni_JniLib.h,這就是剛纔由javah自動生成的。
頭文件生成到src/main/jni目錄,這是咱們在javah擴展工具設定的時候所肯定下來的。
在列表中雙擊com_test_calljni_JniLib.h文件打開,其內容爲:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_test_calljni_JniLib */ #ifndef _Included_com_test_calljni_JniLib #define _Included_com_test_calljni_JniLib #ifdef __cplusplus extern "C" { #endif /* * Class: com_test_calljni_JniLib * Method: callToCpp * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_test_calljni_JniLib_callToCpp (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif
Java_com_test_calljni_JniLib_callToCpp函數定義這一行,對應就是咱們在Java JniLib類中所聲明的callToCpp方法。整個函數名中包含了封裝語言Java/Java包名com.test.calljni/類名JniLib/方法名callToCpp幾個部分。
請注意文件第一行的提醒信息,這個頭文件的內容不要自行修改,若是修改Java封裝文件JniLib.java致使了類名、函數名的變化,應當重複上一步,使用javah工具從新完整生成頭文件。
繼續用C++編寫咱們的函數實現。用鼠標右鍵點擊列表中的jni文件夾,新建一個c++源文件,名稱定爲JniLib.cpp。
內容以下:
#include "com_test_calljni_JniLib.h" JNIEXPORT jstring JNICALL Java_com_test_calljni_JniLib_callToCpp (JNIEnv *env, jclass){ return (*env).NewStringUTF("從cpp返回的文本。"); };
c++代碼中,首先是引用剛纔由javah生成的頭文件,這是爲了保證c++中定義的函數,嚴格吻合Java封裝類中所指定的類型。
函數的定義比較長,能夠從.h文件中直接拷貝進來。由於JNIEnv參數咱們會用到,因此咱們在後面添加一個具體的變量名,這裏用「env」。
函數中只有一條語句,就是返回一個文本字符串,使用JNI中提供的NewStringUTF函數把這個C++的字符串轉換爲一個Java的String對象。
使用NDK系統編譯JNI庫,還須要有兩個文件,都將位於src/main/jni文件夾中,一個是Application.mk文件,內容只有一行:
APP_ABI := all
ABI是應用程序二進制接口的縮寫,指的是Android主機的CPU類型,不一樣CPU須要有不一樣的二進制接口類型。
Java是一種跨CPU的語言,並不要求指定特定的CPU。而C/C++語言,在不一樣的CPU上,都須要進行特定的編譯。
這裏設定APP_ABI爲all,指的是咱們寫的這個JniLib庫,將接受全部NDK支持的CPU類型。NDK在編譯的時候,會自動編譯多個不一樣CPU須要的動態連接庫。並都打包在最終的APK文件中。
在不一樣的Android系統安裝的時候,會自動選擇正確的CPU類型安裝其中一種。
接着看第二個NDK編譯所需文件,Android.mk:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := JniLib LOCAL_SRC_FILES := JniLib.cpp include $(BUILD_SHARED_LIBRARY)
用過Makefile的人應當看上去感受很熟悉。這個就至關於Makefile的主文件,用於描述如何編譯咱們的JNI庫。固然由於咱們其中大量的使用了NDK已有的環境變量和腳本,因此Applcation.mk/Android.mk實際都將被NDK的主體Makefile調用,最終完成完整的編譯。
其中LOCAL_MODULE變量所指定的名稱,就是咱們編譯以後的模塊名稱,這個跟JniLib.java中加載的類名,必須是一致的。
有了這些,若是用過命令行的話,咱們能夠直接在命令行對JNI部分進行編譯了。
但做爲一個完整的程序,咱們更但願JNI部分,也能在總體Android Studio項目編譯的時候編譯,並一塊兒打包進APK。
因此咱們修改一下本項目的Gradle腳本,增長NDK編譯的配置。Gradle是Android Studio中所採用的開源工具,用於項目的管理和自動構建。
在Android Studio左側列表中找到app/build.gradle文件,雙擊打開。在項目的主目錄下還有一個build.gradle文件,不要誤選到那一個。
在android一節中,defaultConfig之下、buildTypes之上增長以下代碼:
externalNativeBuild { ndkBuild { path "src/main/jni/Android.mk" } }
表示本項目使用ndk編譯JNI庫,本項目JNI庫的編譯腳本爲src/main/jni/Android.mk文件。還能夠選擇使用CMAKE系統來編譯JNI項目,不過爲了避免擴展太大的話題,這裏就不講了。對CMAKE情有獨鍾的開發者能夠搜索相關資料。
爲了能看的清楚,貼一次完整的app/build.gradle文件:
apply plugin: 'com.android.application' android { compileSdkVersion 28 defaultConfig { applicationId "com.test.calljni" minSdkVersion 19 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } externalNativeBuild { ndkBuild { path "src/main/jni/Android.mk" } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }
至此,JNI部分的完整定義就完成了。
JNI庫的效果,還要修改一下咱們程序的MainActivity類,才能體現出來。否則JNI庫會被編譯,會被打包,但並無什麼用。
首先修改項目的佈局文件activity_main.xml文件,在當前按鈕的右邊,再增長一個按鈕,名稱爲button2,onClick設置爲bt2_click,順便也爲按鈕設置一個新的顯示字符串「CALLJNI」。修改完成存盤,關閉文件。
這個小例子重點是說明同C/C++語言的混合編程,因此不少細節都從簡了,好比剛纔按鈕的顯示信息,都應當是定義在資源文件中的,而不是在這裏直接使用常量字符串。常量字符串雖然簡便,但沒法完成多國語言自動切換等基本功能,在正式的項目中應當避免這樣使用。
接着在MainActivity.java文件中,增長點擊事件處理程序,添加在bt1_click定義的下面就成:
public void bt2_click(View view){ c = c+1; textview1.setText("click:"+c+"\n"+JniLib.callToCpp()); }
如今能夠完整的編譯一遍了,若是沒有錯誤發生,就在模擬器中執行來測試。
點擊CALLJNI按鈕後,文本框顯示的信息表示JNI正常執行了。
先上一張apk包的文件結構圖片吧:
包含JNI庫的安裝包,比日常的安裝包多一個lib文件夾。其中按照支持的CPU類型,再細緻分類。最終裏面是JNI庫的二進制文件。
在咱們這個例子中,就是libJniLib.so,如同前面說過的。
APK包安裝的時候,根據肯定的硬件平臺,實際只有一個對應的.so文件會被安裝的設備上。
調用完整的可執行文件,這在Android中並非官方推薦的。但一般基於Linux系統的編程,這又是不可避免的。不少必要操做,若是開發系統的SDK支持不足,或者用起來不方便。均可以經過直接訪問系統層參數文件或者系統層可執行文件來完成。
不一樣的操做系統,有不一樣的可執行文件格式。好比Windows的EXE/PE格式,macOS的Mach-O。在Linux上,就是ELF格式。
做爲C語言爲主要編程工具的Linux系統,擁有龐大的ELF可執行資源,幾乎全部的程序都是直接、或者間接由ELF可執行程序完成的,甚至包括JVM自己。
一些新興語言,好比golang,也提供了直接生成Android二進制文件的交叉編譯功能。
因此讓Android程序直接能夠同ELF可執行程序互動,不只僅是同C語言混合編程的問題,而是這樣能夠得到大量社區資源的支持。不少開源項目拿來,不多的修改,就能夠在Android程序的背後發揮做用。
早期的Android系統調用可執行程序很是容易,把編譯好的程序拷貝到Android中,設置爲可執行屬性,就能夠執行了。
隨着Android系統的升級,安全性愈來愈好,除非root,上面這種方式已經不靈了。愈來愈多的限制讓直接執行內嵌的可執行文件變得再也不可行。
在當前的Android版本中,在APK程序中內嵌可執行文件,須要經過如下幾個步驟:
首先固然是準備一個C/C++代碼,好比咱們用一個最經典的Hello World。這麼多年以來,這竟然是兼容性最好的代碼了:)
#include<stdio.h> int main(int argc, char **argv){ printf("你好世界, I'm hello.c\n"); return 0; }
文件名叫hello.c,放到jni文件夾下面。
而後配置Android.mk文件,以編譯這個代碼。
把下面的代碼放置到Android.mk的最後:
include $(CLEAR_VARS) LOCAL_MODULE := hello LOCAL_SRC_FILES := hello.c include $(BUILD_EXECUTABLE)
仔細看,其實只有最後一行有區別,根據英文應當能理解含義,就是編譯爲可執行文件的意思。
由於內置可執行文件並非官方推薦的方式,因此編譯的結果,並不會被自動打包到安裝包APK。
經由Gradle調用ndk-build編譯的結果保存在以下的路徑:
# Debug版本 app/build/intermediates/ndkBuild/debug/obj/local/ # Release版本 app/build/intermediates/ndkBuild/release/obj/local/
一樣在Gradle的設置中,能夠指定把具體的內容打包到Android的assets文件夾中。assets文件夾中包含的是程序運行所需的資源文件,因此這裏,也是把可執行文件,當作資源、數據文件,嵌入在APK中。
請把下面代碼,放置到app/build.gradle文件,android.defaultConfig一節的最後:
sourceSets{ main{ assets{ srcDirs = ['build/intermediates/ndkBuild/debug/obj/local'] } } }
sourceSets.main.assets.srcDirs的設置實際是一個數組,能夠包含多個路徑。若是開發的項目還有別的數據文件須要打包,能夠在這裏增添本身的內容。
注意上面示例中設置中的路徑,是個不完美的地方。當前指向了debug調試編譯輸出的結果。在開發完成,正式投產的時候,應當換到release輸出結果,也即:build/intermediates/ndkBuild/release/obj/local
。否則包含的二進制文件中間會有調試信息,除了文件尺寸會大,也形成不安全因素。
其實我我的經常使用的方式,是直接用Release方式編譯一遍整個項目,而後release文件夾中就會有二進制編譯結果。隨後Gradle的設置,就一直保持在release版本的打包。反正你也不可能用Android Studio對C/C++代碼進行調試,那個工做你確定是使用另外的開發工具完成的。
而後事情並無結束,咱們打開編譯結果的文件夾看一看,是相似下面的樣子:
其中一樣會根據CPU類型不一樣,分爲幾個文件夾,這是預料之中的。但中間除了有咱們須要的hello可執行文件,還會有本已打包的JNI庫.so文件,以及一些編譯輸出信息和中間文件。而這些,就成爲了咱們的垃圾文件,須要排除在外。
能夠把下面代碼,添加在app/build.gradle中,externalNativeBuild上面的位置,跟externalNativeBuild處在同一級:
aaptOptions { ignoreAssetsPattern '!*.txt:!*.so:!*debug:!*release:!*.a' }
這裏要吐槽一下Android Studio Gradle腳本的設計。一般講,ignoreAssetsPattern關鍵詞已經有了「忽略、排除」的含義,是個否認詞。而在其中的設置中,又對每一個須要排除的內容,前面增長「!」否認,實在是反人類啊......
如今若是編譯一遍,看看打包的結果,固然也只是完成了打包,咱們尚未執行這個程序。
APK中多了一個assets文件夾,其中根據CPU類型分類,hello已經在裏面了。
這個工做是最複雜的部分,至少比咱們演示中顯示一個字符串複雜多了。
好在這個程序很是通用,把這個類留着,之後全部同類程序均可以直接拿來使用。
在java文件夾本身的包名上右鍵點擊鼠標,增長一個Java類,命名爲CopyElfs。在生成的java文件中,把下面的代碼帖進去:
package com.test.calljni; import android.content.Context; import android.content.res.AssetManager; import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; import java.util.Arrays; import java.util.List; import android.os.Build; public class CopyElfs { String TAG="Ce_Debug:"; Context ct; String appFileDirectory,executableFilePath; AssetManager assetManager; List resList; String cpuType; String[] assetsFiles={ "hello" }; CopyElfs(Context c){ ct=c; appFileDirectory = ct.getFilesDir().getPath(); executableFilePath = appFileDirectory + "/executable"; // cpuType = Build.SUPPORTED_ABIS[0]; cpuType = Build.CPU_ABI; assetManager = ct.getAssets(); try { resList = Arrays.asList(ct.getAssets().list(cpuType+"/")); Log.d(TAG,"get assets list:"+resList.toString()); } catch (IOException e){ Log.e(TAG, "Error list assets folder:", e); } } boolean resFileExist(String filename){ File f=new File(executableFilePath+"/"+filename); if (f.exists()) return true; return false; } void copyFile(InputStream in, OutputStream out){ try { byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } } catch (IOException e){ Log.e(TAG, "Failed to read/write asset file: ", e); } }; private void copyAssets(String filename) { InputStream in = null; OutputStream out = null; Log.d(TAG, "Attempting to copy this file: " + filename); try { in = assetManager.open(cpuType+"/"+filename); File outFile = new File(executableFilePath, filename); out = new FileOutputStream(outFile); copyFile(in, out); in.close(); in = null; out.flush(); out.close(); out = null; } catch(IOException e) { Log.e(TAG, "Failed to copy asset file: " + filename, e); } Log.d(TAG, "Copy success: " + filename); } void copyAll2Data(){ int i; File folder=new File(executableFilePath); if (!folder.exists()){ folder.mkdir(); } for(i=0;i<assetsFiles.length;i++){ if (!resFileExist(assetsFiles[i])){ copyAssets(assetsFiles[i]); File execFile = new File(executableFilePath+"/"+assetsFiles[i]); execFile.setExecutable(true); } } } String getExecutableFilePath(){ return executableFilePath; } }
類成員assetsFiles數組中,能夠包含多個可執行文件,把文件名放在這裏,就會被拷貝到Android設備的/data/data/包名/files/excutable/文件夾,並設置爲能夠執行。
接着在MainActivity類的onCreate成員中,增長對拷貝可執行文件功能的調用:
CopyElfs ce; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textview1=(TextView)findViewById(R.id.textView1); ce = new CopyElfs(getBaseContext()); ce.copyAll2Data(); }
作了這麼多準備性工做,開始真正對程序的調用。
首先仍是修改佈局文件,再增長一個按鈕,名稱叫button3,顯示字符串是「CALLELF」,onClick的事件處理函數是bt3_click。
此次要添加的代碼不只僅是bt3_click方法,還要對調用命令行程序以及獲取其結果單獨抽象爲一個方法。
考慮到還要增長一些對應的類成員變量,和庫文件的引用。咱們把完整的MainActivity.java代碼列出來:
package com.test.calljni; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; import android.view.View; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import android.util.Log; public class MainActivity extends AppCompatActivity { String TAG="Main_Debug:"; TextView textview1; int c=0; CopyElfs ce; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textview1=(TextView)findViewById(R.id.textView1); ce = new CopyElfs(getBaseContext()); ce.copyAll2Data(); } public void bt1_click(View view){ c = c+1; textview1.setText("click:"+c); } public void bt2_click(View view){ c = c+1; textview1.setText("click:"+c+"\n"+JniLib.callToCpp()); } public String callElf(String cmd){ Process p; String tmpText; String execResult = ""; try { p = Runtime.getRuntime().exec(ce.getExecutableFilePath() + "/"+cmd); BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); while ((tmpText = br.readLine()) != null) { execResult += tmpText+"\n"; } }catch (IOException e){ Log.i(TAG,e.toString()); } return execResult; } public void bt3_click(View view){ c = c+1; textview1.setText("click:"+c+"\n"+callElf("hello")); } }
如今已經完整了,能夠編譯而後在模擬器執行來嘗試一下。
還能夠詳細探究可執行文件,拷貝到Android設備以後的細節。這個使用adb工具鏈接到設備上就能看出來,請看下面執行的截圖:
前面的例子,咱們已經認識到了NDK的強大。而ndk-build編譯工具,基本屬於一個Makefile的工做方式。
然而在Linux龐大的開源社區中,多種編譯管理工具都同時存在。其實不只僅Android,即使在桌面版的Linux版本中,編譯不一樣的軟件包,也是一件費時費力的事情。
所以想繼承開源社區的龐大優點,除了上面講到的這些必要工做,把軟件包編譯到Android的環境中,是最主要須要完成的工做。
這個話題太大,內容太多也太分散,咱們的文章是遠遠沒法涵蓋的。以最經常使用的OpenSSL開源庫爲例,GitHub上有一個編譯腳本,值得參考:
https://github.com/lllkey/android-openssl-build
咱們下面只演示一下,在本身的程序中,調用openssl庫的方式。實際在Android SDK以及Java標準庫中,都已經有不少編、解碼功能足以知足應用。因此這裏只是用於演示操做的方法,正式開發中,要根據實際須要選擇開源庫來使用。
首先咱們把上面編譯好的openssl庫下載到本地,放到跟當前的Android項目平級就好,其實路徑隨意本身定,只要在接下來的設置中,指到正確的路徑就沒有問題。
$ git clone https://github.com/lllkey/android-openssl-build.git
由於這個開源庫並不是咱們項目的一部分,咱們只把它的編譯結果,連接到咱們的項目中:
$ cd calljni/app/src/main/jni $ ln -s /home/andrew/dev/android/android-openssl-build/result/ openssl #注意上面的路徑,應當是你clone下來的真實路徑 $ ls -lh openssl/ total 0 drwxr-xr-x 4 andrew staff 136B Jun 4 08:48 arm64-v8a drwxr-xr-x 4 andrew staff 136B Jun 4 08:48 armeabi-v7a drwxr-xr-x 4 andrew staff 136B Jun 4 08:48 x86 drwxr-xr-x 4 andrew staff 136B Jun 4 08:48 x86_64
下面咱們寫一個小程序,用於調用openssl庫中的md5編碼功能,程序名爲md5.c,放置在jni路徑下面:
#include <stdio.h> #include <string.h> #include <openssl/md5.h> void openssl_md5(const char *data, int size, char *rs){ unsigned char buf[16]; memset(buf,0,16); MD5_CTX c; MD5_Init(&c); MD5_Update(&c,data,size); MD5_Final(buf,&c); char tmp[3]; strcpy(rs,""); int i; for (i = 0; i < 16; i++){ sprintf(tmp,"%02x",buf[i]); strcat(rs,tmp); } } int main(int argc, char **argv){ if (argc != 2){ printf("Wrong argument.\n"); return 1; } char md5str[33]; openssl_md5(argv[1],strlen(argv[1]),md5str); printf("%s\n",md5str); return 0; }
而後是修改Android.mk編譯腳本,此次增長的是三部分。兩個是已經編譯完成的openssl Android版本庫;一個是咱們新增的md5.c編譯。編譯時還要知足,根據不一樣的CPU類型,選擇不一樣的openssl庫,而且編譯對應的CPU版本md5可執行文件。這個過程當中,須要使用不一樣的預約義環境參量來完成這個工做:
include $(CLEAR_VARS) LOCAL_MODULE := ssl LOCAL_SRC_FILES := $(LOCAL_PATH)/openssl/$(TARGET_ARCH_ABI)/lib/libssl.a include $(PREBUILT_STATIC_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := crypto LOCAL_SRC_FILES := $(LOCAL_PATH)/openssl/$(TARGET_ARCH_ABI)/lib/libcrypto.a include $(PREBUILT_STATIC_LIBRARY) include $(CLEAR_VARS) LOCAL_SHARED_LIBRARIES := \ ssl \ crypto LOCAL_C_INCLUDES += $(LOCAL_PATH)/openssl/$(TARGET_ARCH_ABI)/include LOCAL_MODULE := md5 LOCAL_SRC_FILES := md5.c include $(BUILD_EXECUTABLE)
上面的代碼中:
想必你也想到了,還要在MainActivity.java中,增長調用md5的代碼,固然還有layout文件:
按鍵響應代碼:
public void bt4_click(View view){ c = c+1; textview1.setText("click:"+c+"\n"+callElf("md5 testString")); }
做爲md5參數的字符串,在正式的程序中,確定應當是從某些計算中獲取,或者從屏幕的輸入框讀取。這裏直接使用一個常量「testString」。
最後還有特別容易忘的一個地方,就是CopyElfs中可執行文件的列表:
String[] assetsFiles={ "hello","md5" };
不得不認可,有了上一小節的基礎,增長個可執行程序或者第三方庫,都不算什麼工做量。
程序的執行結果以下:
還能夠在臺式電腦中驗證一下計算的結果:
$ echo -n "testString" | md5 536788f4dbdffeecfbb8f350a941eea3
md5程序,使用了openssl的靜態連接庫.a文件。在Android4以後的版本中,若是不作root,彷佛暫時沒有好辦法使用.so動態連接庫。
JNI則可使用.so文件,這時候在Android.mk中,應當使用$(PREBUILT_SHARED_LIBRARY)參量,來講明一個.so的預約義動態連接庫。
使用了第三方的動態連接庫,在調用JNI的時候也有額外一點須要注意,就是在載入本身的JNI庫以前,必須把用到的依賴庫,首先載入進來,不然直接載入JNI庫會報錯:
public class JniLib { static { System.loadLibrary("crypto"); System.loadLibrary("ssl"); System.loadLibrary("JniLib"); } .......
最後是本文中所使用的示例代碼: 連接: https://pan.baidu.com/s/1yDU0q5nikorSyD0av0Ue5w 提取碼: 86yp