Android program, the embedded ELF executable files - Android Development C language mixed programming summary

Foreword

All know, Android is based on Linux system, then covered with a Java virtual machine as the core-shell system. With the general common Linux + Java system different is that there will be support for hardware drivers, hardware abstraction layer HAL to avoid the GPL open source license restrictions.
Most of the time, we use the JVM programming language, such as Java or traditional upstart Kotlin. Encounter velocity sensitive items, such as games, such as video playback. We will use the Android JNI technology, supports the NDK, use C ++ to develop high computational modules, Java program providing upper call.
We start with a simple JNI examples in this article to introduce a mixed start Android programming in Java and C ++ and later see how Android calls direct method ELF specification command-line program, and calling a mixture of third-party libraries slightly more complicated command-line program.

Android Studio Configuration

The first configuration is to install the Android SDK, it is necessary to develop Android applications.
Android Studio to enter the settings interface, Mac shortcuts are Command+ ,, Windows and Linux versions, please choose from the menu.
Set the interface, select the order from the left: Appearance & Behavior -> System Settings -> Android SDK, the SDK can access the settings.

SDK version of the list on the right, shows the front or back shows the Installed ✔️, indicates the version of the installed SDK. Usually if no special needs, just install a new version of the SDK. I figure because some of the items of special requirements, specific two different versions of the SDK installed.
Want to install a version of the SDK, simply click the appropriate line foremost checkbox, and then click the OK button to install the bottom right corner.
If I had not starting from scratch, but rather took the source code from other developers, the source code may specify a particular version of the SDK. This time you can modify the project configuration file version is set to the SDK version you have installed. The simplest method is more direct here to install the corresponding SDK, prevent many complicated problems occur because the version-dependent.

The second configuration is NDK, the interface is also provided just SDK, the middle upper side click interface "SDK Tools" tab, enter the interface NDK provided.

NDK settings do not have much choice, as long as the installation is like, have been installed across the new version, you can also choose to update the casual or continue using the old version. Compatibility between different versions of NDK are pretty good, most of all do not worry.
The Android NDK is set development, Java / C hybrid programming required.

The third configuration is to add an external tool javah, this tool is written in Java "wrapper" file, convert a C / C ++'s .h file. Although Java / C ++ is an object-oriented language, but both object-oriented implementation is different. Therefore, the method of a class in Java, C ++ to convert the world, is to use a very long function names do distinction. Although this case the use of hand-coded the same effect, but it is prone to error, use javah tool is able to automatically complete.
Android Studio disposed in the left list interface, sequentially selects Tools -> External Tools, "+ ", a new interface tool lower left corner of the right click, for example called "javah".

Three of which content needs to be set are:

  • javah Path: $JDKPath$/bin/javahThis path related with jdk installation.
  • Command line parameters: -classpath . -jni -d $ModuleFileDir$/src/main/jni $FileClass$mainly specify the output path.
  • Working Directory: $ModuleFileDir$/src/main/JavaThe current project path.

So far Android Studio's main setup is complete, of course, only the most basic essential settings, if they have other needs, similar git repository address, etc., you can then set your own.
Here you can start developing projects.

First prepare a basic Android application

Select New Project in Android Studio interface, if it is at the beginning of the interface, just click the button on the main interface; you can also choose the File menu.

Select the basic Empty Activity like.

Followed by a set of projects, project name, the storage location of these do not have to say, the lowest API version determines your program may be executed at the lowest what version of Android phones, if no special needs, try to be a little lower, after all, Android phones upgrade ratio is lower than the good times of iOS.
In this way, the project is the establishment of complete, Android Studio to use a standard template, the project did initialization. We can add your own content on this basis.

From the list on the left side of the screen project file, select the app -> res -> layout - > acitvity_main.xml file, the file will open in the right mode is an interactive interface designer. In which, according to the way in the figure below, we add a TextView control and a button. Text box to trigger future results display output, of course, is the button to start execution.

We'll revise the TextView control name, called textView1. Button changed the name of button1, in addition to onClick property of the button to add a call: bt1_click.
Interface section is complete, remember to save, then you can turn off this file.

At this time, Android Studio interface will appear in the position MainActivity.java file. This is after the new project automatically open files and program files is the main window of the project. We first edit window layout file, this file is hidden behind.
We refer to the section in the library file, add the following two lines:

import android.widget.TextView;
import android.view.View;

These two lines are our next program will use the library references.
In the variable declaration part of the class, this increase in two lines:

    TextView textview1;
    int c=0;

The first row is declared a text box for the interface associated with the editor added just text box.
c variable is a simple counter, we want every click of a button, this counter is incremented by 1 to confirm that we are responding to every click, rather than the program without any feedback to the user.
In onCreatethe end, increase the associated text box code function:

        textview1=(TextView)findViewById(R.id.textView1);

TextView1 is when we screen editing, a text box from the name behind R.id..
Next, the last class, button clicks increase the response handler:

    public void bt1_click(View view){
        c = c+1;
        textview1.setText("click:"+c);
    }

Clarity, we have completed this part of the code and then copied over again:

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);
    }
}

Program is complete, you can choose Make Project to compile the project from the Build menu. Then select Run 'app' in the Run menu.
If this is the first use Android Studio, you may also need to be reminded that you create a new Android emulator to execute the program. Of course, you can put the debugging is enabled Android phone plugged into the computer for device debugging.
Implementation of the results shown in Figure:

Click the button twice, the screen becomes:

Well, our basic experimental platform ready, here is the entry to the topic.

Call JNI library

Each JNI library is divided into two parts, one is written in C ++ .so dynamic link library, and the other part is the Java package for the dynamic link library. We start with the Java section looks.

JNI libraries written in Java wrapper classes

开始写这个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

Guess you like

Origin www.cnblogs.com/andrewwang/p/11024891.html