前言 都知道的,Android基於Linux系統,然後覆蓋了一層由Java虛擬機為核心的殼系統。跟一般常見的Linux+Java系統不同的,是其中有對硬體驅動進行支持,以避開GPL開源協議限制的HAL硬體抽象層。 大多數時候,我們使用JVM語言進行編程,比如傳統的Java或者新貴Kotlin。碰到對 ...
前言
都知道的,Android基於Linux系統,然後覆蓋了一層由Java虛擬機為核心的殼系統。跟一般常見的Linux+Java系統不同的,是其中有對硬體驅動進行支持,以避開GPL開源協議限制的HAL硬體抽象層。
大多數時候,我們使用JVM語言進行編程,比如傳統的Java或者新貴Kotlin。碰到對速度比較敏感的項目,比如游戲,比如視頻播放。我們就會用到Android的JNI技術,使用NDK的支持,利用C++開發高計算量的模塊,供給上層的Java程式調用。
本文先從一個最簡單的JNI例子來開始介紹Android中Java和C++的混合編程,隨後再介紹Android直接調用ELF命令行程式的規範方法,以及調用混合了第三方庫略微複雜的命令行程式。
Android Studio配置
第一個配置是安裝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,防止因為版本依賴出現的很多繁瑣問題。
第二個配置的是NDK,還在剛纔SDK設置的界面中,點擊界面上側中間的“SDK Tools”標簽,可以進入到NDK設置的界面。
NDK的設置沒有那麼多的選擇,只要安裝就好,已經安裝碰到有新版本,也可以隨性選擇更新或者使用老版本繼續。NDK不同版本間的相容性都還不錯,大多都不用擔心。
NDK的設置是Android開發中,Java/C混合編程需要的。
第三個配置是增加一個外部工具javah,這個工具是將Java編寫的“包裝”文件,轉換一個C/C++的.h文件。雖然Java/C++都是面向對象語言,但兩者的面向對象實現是不同的。所以在Java中某個類的方法,轉換到C++的世界中,是使用很長的函數名來做區分。這種情況使用手工編寫雖然效果一樣,但很容易出錯,使用javah工具則能自動完成。
在Android Studio設置界面左側的列表中,順序選擇Tools -> External Tools,單擊右側界面左下角的“+”,新建一個工具,比如就叫"javah"。
其中三個需要設置的內容分別是:
- javah程式路徑:
$JDKPath$/bin/javah
,這個跟jdk安裝的路徑有關。
- 命令行參數:
-classpath . -jni -d $ModuleFileDir$/src/main/jni $FileClass$
,主要指定輸出路徑。 - 工作目錄:
$ModuleFileDir$/src/main/Java
,當前項目路徑。
至此Android Studio的主要設置就完成了,當然只是最基本必須的設置,如果自己還有其它需求,類似git倉庫地址等,可以再自行設置。
下麵就可以開始進行項目的開發。
先準備一個基本的Android程式
在Android Studio界面選擇New Project,如果是在開始界面,直接點擊主界面上的按鈕;也可以在文件菜單中選擇。
選擇基本的Empty Activity就好。
接著是項目的設置,項目名稱、存儲位置這些都不用說了,最低的API版本決定了你的程式可以在最低什麼版本的Android手機上執行,如果沒有特殊需要,儘量可以低一點,畢竟Android手機的升級比例,比iOS是低了好多倍的。
這樣,項目就建立完成,Android Studio使用標準模板,對項目做了初始化。我們可以在這個基礎上再添加自己的內容。
從屏幕左側項目文件的列表中,選擇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
函數的最後,增加關聯文本框的代碼:
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庫
每個JNI庫都分為兩部分,一個是C++編寫的.so動態鏈接庫,另一部分則是Java對這個動態鏈接庫的封裝。我們先從Java部分看起。
編寫JNI庫的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++來實現。
由封裝類生成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++編寫我們的函數實現。用滑鼠右鍵點擊列表中的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編譯腳本
使用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中載入的類名,必須是一致的。
Gradle自動編譯NDK項目
有了這些,如果用過命令行的話,我們可以直接在命令行對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部分的完整定義就完成了。
在Java中調用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正常執行了。
解析包含JNI庫的APK安裝文件
先上一張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程式中內嵌可執行文件,需要通過以下幾個步驟:
- 在NDK中編譯對應的源代碼。或者在其它語言環境中,使用對應工具,生成在Android環境可以執行的二進位代碼。
- 除了.so之外的編譯結果,並不會自動打包到APK中。所以編譯出的二進位代碼,需要作為數據文件,放入APK的資源區。
- 在Java代碼中,根據檢測到的CPU類型,把對應的可執行文件,從數據區拷貝到Android設備上,並設置為可執行。
- 在Java代碼中調用可執行程式,並獲取結果。
編譯可執行文件
首先當然是準備一個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
因為內置可執行文件並不是官方推薦的方式,所以編譯的結果,並不會被自動打包到安裝包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已經在裡面了。
把可執行程式拷貝到Android系統
這個工作是最複雜的部分,至少比我們演示中顯示一個字元串複雜多了。
好在這個程式非常通用,把這個類留著,以後所有同類程式都可以直接拿來使用。
在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();
}
執行對Elf執行文件的調用
做了這麼多準備性工作,開始真正對程式的調用。
首先還是修改佈局文件,再增加一個按鈕,名稱叫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)
上面的代碼中:
- $(PREBUILT_STATIC_LIBRARY)指定了預定義的靜態庫文件
- $(LOCAL_PATH)就是指jni文件夾路徑
- $(TARGET_ARCH_ABI)是根據目標CPU的ABI不同,選擇不同的庫文件和C語言頭文件。
想必你也想到了,還要在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