欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结

程序员文章站 2022-06-29 08:16:23
前言 都知道的,Android基于Linux系统,然后覆盖了一层由Java虚拟机为核心的壳系统。跟一般常见的Linux+Java系统不同的,是其中有对硬件驱动进行支持,以避开GPL开源协议限制的HAL硬件抽象层。 大多数时候,我们使用JVM语言进行编程,比如传统的Java或者新贵Kotlin。碰到对 ......

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命令行程序的规范方法,以及调用混合了第三方库略微复杂的命令行程序。

android studio配置

第一个配置是安装android的sdk,这是开发android程序必须的。
进入android studio的设置界面,mac的快捷键是command+,,windows和linux版本请自行从菜单中选择。
在设置界面中,从左侧顺序选择:appearance&behavior -> system settings -> android sdk,可以进入到sdk的设置。
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
右侧的sdk版本列表中,最前面显示了✔️或者后面显示了installed,表示该版本的sdk已经安装。通常如果没有特殊需要,只安装1个最新版本的sdk即可。图中我是因为某些项目特殊的要求,安装了两个特定不同版本的sdk。
希望安装某版本的sdk,只要点选相应行最前面的多选框,然后单击右下角确认按钮即可安装。
如果不是自己从头开始,而是接手了其他开发人员的源码,源码中可能指定了特定版本的sdk。这时候可以修改其项目配置文件中版本的设置,到你安装的sdk版本。更简单的方法是直接在这里安装对应的sdk,防止因为版本依赖出现的很多繁琐问题。

第二个配置的是ndk,还在刚才sdk设置的界面中,点击界面上侧中间的“sdk tools”标签,可以进入到ndk设置的界面。
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
ndk的设置没有那么多的选择,只要安装就好,已经安装碰到有新版本,也可以随性选择更新或者使用老版本继续。ndk不同版本间的兼容性都还不错,大多都不用担心。
ndk的设置是android开发中,java/c混合编程需要的。

第三个配置是增加一个外部工具javah,这个工具是将java编写的“包装”文件,转换一个c/c++的.h文件。虽然java/c++都是面向对象语言,但两者的面向对象实现是不同的。所以在java中某个类的方法,转换到c++的世界中,是使用很长的函数名来做区分。这种情况使用手工编写虽然效果一样,但很容易出错,使用javah工具则能自动完成。
在android studio设置界面左侧的列表中,顺序选择tools -> external tools,单击右侧界面左下角的“+”,新建一个工具,比如就叫"javah"。
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
其中三个需要设置的内容分别是:

  • 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,如果是在开始界面,直接点击主界面上的按钮;也可以在文件菜单中选择。
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
选择基本的empty activity就好。
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
接着是项目的设置,项目名称、存储位置这些都不用说了,最低的api版本决定了你的程序可以在最低什么版本的android手机上执行,如果没有特殊需要,尽量可以低一点,毕竟android手机的升级比例,比ios是低了好多倍的。
这样,项目就建立完成,android studio使用标准模板,对项目做了初始化。我们可以在这个基础上再添加自己的内容。

从屏幕左侧项目文件的列表中,选择app -> res -> layout -> acitvity_main.xml文件,文件会在右侧打开,模式是交互式的界面设计器。在其中,按照下图的样子,我们增加一个textview控件和一个按钮。文本框是为了将来显示输出的结果,按钮当然就是开始执行的触发器。
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
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手机插在电脑上进行真机调试。
执行的结果如图:
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
点击两次按钮后,画面变为:
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
好了,我们的基本实验平台准备完成,下面才是进入正题。

调用jni库

每个jni库都分为两部分,一个是c++编写的.so动态链接库,另一部分则是java对这个动态链接库的封装。我们先从java部分看起。

编写jni库的java封装类

开始写这个jni库之前,我们首先要对这个库的总体功能、结构划分、接口类型充分做好规划,这样才能保证两种语言之间的顺畅调用。因为尚没有一种工具可以同时有效的对两种语言进行跟踪调试,所以在接口部分如果碰到问题,往往只能在大量的日志输出中去查找线索,费时费力。
作为一个简单的演示,我们的jni库功能很简单,从java封装的角度看,我们有一个名为jnilib的java类,其中包含一个方法,叫calltocpp,这个方法,将会在c++中来实现。
在文件列表中,选择mainactivity.java所在的包名,点击右键,选择new->java class。
一切选用默认设置,类名为jnilib。
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
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程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
此时最好将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());
    }

现在可以完整的编译一遍了,如果没有错误发生,就在模拟器中执行来测试。
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
点击calljni按钮后,文本框显示的信息表示jni正常执行了。

解析包含jni库的apk安装文件

先上一张apk包的文件结构图片吧:
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
包含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++代码进行调试,那个工作你肯定是使用另外的开发工具完成的。

然后事情并没有结束,我们打开编译结果的文件夹看一看,是类似下面的样子:
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
其中同样会根据cpu类型不同,分为几个文件夹,这是预料之中的。但中间除了有我们需要的hello可执行文件,还会有本已打包的jni库.so文件,以及一些编译输出信息和中间文件。而这些,就成为了我们的垃圾文件,需要排除在外。
可以把下面代码,添加在app/build.gradle中,externalnativebuild上面的位置,跟externalnativebuild处在同一级:

    aaptoptions {
        ignoreassetspattern '!*.txt:!*.so:!*debug:!*release:!*.a'
    }

这里要吐槽一下android studio gradle脚本的设计。通常讲,ignoreassetspattern关键词已经有了“忽略、排除”的含义,是个否定词。而在其中的设置中,又对每个需要排除的内容,前面增加“!”否定,实在是反人类啊......

现在如果编译一遍,看看打包的结果,当然也只是完成了打包,我们还没有执行这个程序。
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
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。
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
这次要添加的代码不仅仅是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程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结

还可以详细探究可执行文件,拷贝到android设备之后的细节。这个使用adb工具连接到设备上就能看出来,请看下面执行的截图:
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结

编译带有扩展库的可执行文件

前面的例子,我们已经认识到了ndk的强大。而ndk-build编译工具,基本属于一个makefile的工作方式。
然而在linux庞大的开源社区中,多种编译管理工具都同时存在。其实不仅仅android,即便在桌面版的linux版本中,编译不同的软件包,也是一件费时费力的事情。
因此想继承开源社区的庞大优势,除了上面讲到的这些必要工作,把软件包编译到android的环境中,是最主要需要完成的工作。
这个话题太大,内容太多也太分散,我们的文章是远远无法涵盖的。以最常用的openssl开源库为例,github上有一个编译脚本,值得参考:

我们下面只演示一下,在自己的程序中,调用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文件:
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
按键响应代码:

    public void bt4_click(view view){
        c = c+1;
        textview1.settext("click:"+c+"\n"+callelf("md5 teststring"));
    }

作为md5参数的字符串,在正式的程序中,肯定应当是从某些计算中获取,或者从屏幕的输入框读取。这里直接使用一个常量“teststring”。
最后还有特别容易忘的一个地方,就是copyelfs中可执行文件的列表:

    string[] assetsfiles={
            "hello","md5"
    };

不得不承认,有了上一小节的基础,增加个可执行程序或者第三方库,都不算什么工作量。
程序的执行结果如下:
Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结
还可以在台式电脑中验证一下计算的结果:

$ 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