Android程序中,內嵌ELF可執行文件-- Android開發C語言混合編程總結

前言

都知道的,Android基於Linux系統,而後覆蓋了一層由Java虛擬機爲核心的殼系統。跟通常常見的Linux+Java系統不一樣的,是其中有對硬件驅動進行支持,以避開GPL開源協議限制的HAL硬件抽象層。
大多數時候,咱們使用JVM語言進行編程,好比傳統的Java或者新貴Kotlin。碰到對速度比較敏感的項目,好比遊戲,好比視頻播放。咱們就會用到Android的JNI技術,使用NDK的支持,利用C++開發高計算量的模塊,供給上層的Java程序調用。
本文先從一個最簡單的JNI例子來開始介紹Android中Java和C++的混合編程,隨後再介紹Android直接調用ELF命令行程序的規範方法,以及調用混合了第三方庫略微複雜的命令行程序。java

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,防止由於版本依賴出現的不少繁瑣問題。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

  • javah程序路徑:$JDKPath$/bin/javah,這個跟jdk安裝的路徑有關。
  • 命令行參數:-classpath . -jni -d $ModuleFileDir$/src/main/jni $FileClass$,主要指定輸出路徑。
  • 工做目錄:$ModuleFileDir$/src/main/Java,當前項目路徑。

至此Android Studio的主要設置就完成了,固然只是最基本必須的設置,若是本身還有其它需求,相似git倉庫地址等,能夠再自行設置。
下面就能夠開始進行項目的開發。github

先準備一個基本的Android程序

在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庫

每一個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

相關文章
相關標籤/搜索