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

前言都知道的,Android基于Linux系统,然后覆盖了一层由Java虚拟机为核心的壳系统。跟一般常见的Linux Java系统不同的,是其中有对硬件驱动进行支持,以避开GPL开源协议限制的HAL硬件抽象层。大多数时候,我们使用JVM语言进行编程,比如传统的Java或者新贵Kotlin。碰到对速度比较敏感的项目,比如游戏,比如视频播放。我们就会用到Android的JNI技术,使用NDK的支持,利...

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 __cplusplusextern “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库,将接受所

源文地址:https://www.guoxiongfei.cn/cntech/19400.html