当前位置: 萬仟网 > IT编程>移动开发>Android > Android程序中,内嵌ELF可执行文件-- Android开发C语言混合编程总结

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

2019年06月15日 06:43  | 萬仟网IT编程  | 我要评论

前言

都知道的,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上有一个编译脚本,值得参考:

我们下面只演示一下,在自己的程序中,调用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

您可能感兴趣的文章:

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

◎已有 0 人评论

Copyright © 2019  萬仟网 保留所有权利. 粤ICP备17035492号-1
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com