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的设置。
右侧的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