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

来源:https://www.cnblogs.com/andrewwang/archive/2019/06/14/11024891.html
-Advertisement-
Play Games

前言 都知道的,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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • RK292X是一種低端平板電腦、Android攜帶型GPS等數字多媒體應用的低功耗高性能處理器解決方案,並將單核CortexA9與Neon和FPU協處理器以及128KBL2緩存集成在一起。 許多嵌入式強大的硬體引擎為高端應用程式提供了優化的性能。Rk292x支持1080p@60fps的幾乎全格式視頻 ...
  • 介紹 Tina wifi 管理開發介面和 Demo 代碼。 硬體平臺:AW R16、R8、R18、R58 及 R11系統版本:Tina v1.0 及以上版本 2. Wi-Fi manager 相關說明wifimanager 部分代碼是 Tina 平臺管理 wifi 與 AP 連接模塊。主要功能包括打 ...
  • 先總體佈局,有的佈局可以分為幾部分,比如頭部、中部、底部,代碼如上。 用include layout=""在佈局中引入其他的佈局,這樣結構就比較清楚,方便佈局模塊化,復用佈局。 簡單的,可以直接一個xml文件佈局。 ...
  • Hi3136是一款同時支持DVB-S(ETS 300 421)、DVB-S2(ETS 302 307)和DirecTV(ITU-R BO.1294 System B)標準的衛星數字電視通道接收晶元。晶元完成衛星數字信號從基帶採樣到MPEG-TS流輸出的全數字處理過程。在DVB-S2方面,晶元支持QP ...
  • QCA9379將先進的2x2雙頻802.11acMU-MIMO WiFi藍牙4.2結合在一塊高性能、小形狀的片上系統(SoC)中。支持增強的WiFi/Bluetooth與藍牙專用(第三個)天線共存。 QCA9379 SoC旨在將WLAN和藍牙低能(LEE)技術無縫集成在一種單晶元解決方案中,它提供了 ...
  • 眾所周知,一款沒有動畫的 app,就像沒有靈魂的肉體,給用戶的體驗性很差。現在的 android 在動畫效果方面早已空前的發展,1.View 動畫框架 2.屬性動畫框架 3.Drawable 動畫。相比後後兩者,View 動畫框架在 Android 的最開始就已經出現,即有著非常容易學習的有點,卻也... ...
  • 跟綜下來發現control_sound這個函數中if (behavior == MMI_NOTI_SND_BEHA_NO_PLAY){#ifdef MMI_NOTI_MGR_UTplay_sound = MMI_FALSE;snd_action = action;play_sound_id = to ...
  • QCA9377將先進的1x1雙頻段802.11acMUMIMOWiFi+藍牙5結合在一個高性能、低功耗、小尺寸的晶元系統 (SoC)中。 QCA9377 soc設計用於在單晶元解決方案中提供無線區域網和藍牙低能量技術的卓越集成,QCA 9377 SoC提供低 功率雙頻(2.4&5 GHz)、1流(1 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...