iTOP-4412全能版采用四核Cortex-A9,主频为1.4GHz-1.6GHz,配备S5M8767 电源管理,集成USB HUB,选用高品质板对板连接器稳定可靠,大厂生产,做工精良。接口一应俱全,开发更简单,搭载全网通4G、支持WIFI、蓝牙、陀螺仪、CAN总线、RS485总线、500万摄像头等模块,稳定运行Android 4.0.3/Android 4.4操作,系统通用Linux-3.0.15+Qt操作系统(QT支持5.7版本),Ubuntu版本:12.04,接口智能分配 方便好用。
【交流群2】258811263(获取开源资料)
【迅为B站】北京迅为电子的个人空间-北京迅为电子个人主页-哔哩哔哩视频
第八十八章 安卓JNI开发指南
88.1 JNI介绍
什么是JNI?
JNI 的全称是 Java Native Interface,即 Java 本地接口,用一句话来说就是 JNI 是属于 Java 的,尽管我们是在安卓上使用,但是和安卓没有直接关系。我们只是通过 JNI 来在 Java 代码里面调用 C 或者 C++ 的代码。如下图所示:
通过上面的图我们可以发现 java 不仅可以通过 jni 来调用 c/c++,反过来 c/c++也可以来调用 java 的一些程序,所以,JNI 让 java 和 c/c++有了交互的功能。
如果大家看过我们的 QT 视频,那么这个 JNI 就有点类似于 QT 里面的 C 和 C++混合编程。既然我们可以通过 JNI 在 Java 代码里面调用 C 或者 C++,那么我们就可以通过安卓应用来控制底层硬件了,比如说打开 LED,关闭 LED 的操作。
为什么要学习JNI?
我们主要是做嵌入式开发,但是现在随着安卓工控和界面越来越来多,我们和安卓打的交道越来越多,几年前更是安卓系统开发人员一将难求,虽然说我们嵌入式开发人员并不需要去写上层的应用,比如手机 APP 等,但是我们要给写安卓上层应用的工程师提供相应的接口。那么我们就用到了 JNI,我们要生成相应的库提供给上次开发人员调用。所以我们要学。
JNI 环境安装
因为 JNI 是属于 java 的,我们需要先在电脑上安装 java 环境。Java 的版本需要大于 1.1,因为 1.1以后才支持 JNI
如何生成so 库?
生成 so 库我们有俩种方法, 第一种是使用 CMake , CMake 是跨平台编译工具, 要配合CMakeList.txt 和 gradle 来使用。第二种是使用 NDK 自带的脚本工具 ndk-build 配合 Android.mk 来使用。这里我只介绍第一种方法,使用 ndk-build 的方法现在不是太常用了。
88.2 使用CMake编译工具来生成so库
1.下载 CMake 编译工具
可以通过 AndroidStudio->SDK->SDK Tools->-CMake 来下载。如果在安装 AndroidStudio 的时候已经安装了,则不必再次安装,如下图所示:
2.建一个 Native c++项目
打开 AndroidStudio,选择新建一个项目,如下图所示:
然后选择 Native c++,并点击下一步,如下图所示:
这里我们把工程名字设置成 JniTest,保存路径设置成 G:\adnroid\PRO\JNI(大家依据自己的电脑选择路径即可,路径不要有中文),语言设置为 Java,其他保持不变,然后点击下一步。如下图所示:
C++标准这里我们选择 C++11,然后点击下一步,如下图所示:
稍等片刻,编译完成如下图所示:
我们新创建的这个工程会给我们自动创建一个 jni 的例子,我们来一起分析下这个例子,然后在来仿写我们自己的 jni,先学会爬,然后在学会跑,我们打开 MainActivity,如下图所示:
在这个文件里面我们定义了一个 stringFromJNI()函数,并且我们调用了 native-lib 库,如下图所示:
然后我们打开 src/main/cpp/native-lib.cpp 这个 cpp 文件,native-lib 就是这个文件编译得到的,如下图所示:
然后之前我们已经提到了,使用 CMake 编译 so 库,我们需要配合 CMakeList.txt 和 gradle 来使用,那我我们来看一下我们创建的这个工程有没有这俩个文件,如下图所示:
那我们编译生成的 so 库文件是在哪里呢?我们点击一下编译,如下图所示:
然后我们切换到 project,进到 G:\adnroid\PRO\JNI\app\build\intermediates\cmake\debug\obj 下面,就可以看到我们生成的 so 库了,那么这个库你就可以给别人来使用了,如下图所示:
88.3 分析例子的JNI代码
上一节我们了解我们创建的这个工程这样的一个 JNI 架构,那么这一节我们来编译一个自己的 so 库, 要编译自己的 so 库,我们就要先来写一个 JNI 的代码,所以我们先来一起分析下他给我们的这个例子的代码,如下图所示:
避免编译器按照 C++的方法来编译 C 函数
extern "C"
关键字 JNIEXPORT 的作用是表示这个函数可以被其他的函数调用,这个有点类似于 C++的 PUBLIC 修饰符
JNIEXPORT
关键字 JNICALL 为空,没有含义
JNICALL
关键字定义如下:
关键字 jstring 表示的是字符串类型,与之类似的还有 jint,jchar,与之 c 语言对应的类型就是 int 和char。
jstring
Java_com_example_jnitest_MainActivity_stringFromJNI 是函数名的名称,这个我们不能随便定义,我们使用的是静态注册,所以他有格式要求,与之对应的还有动态注册,这里我们不做介绍。
格式:Java _包名 _ 类名_Java 需要调用的方法名
按照这个格式我们可以知道,包名为 com_example_jnitest,也就是 MainActivity 所在的包名,如下图所示:
类名为 MainActivity,也就是定义 native 函数的类,也就是 MainActivity,如下图所示:
Java 需要调用的方法名为 stringFromJNI,也就是我们定义的 public native String stringFromJNI(),如下图所示:
参数 JNIEnv* env,与 java 进行交互的相关的函数
JNIEnv* env
定义如下:
参数 jobject,代表 native 函数的 java 类的实例。
定义了一个字符串 Hello from C++,并返回这个字符串
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
88.4 实战-仿写JNI代码
接下来我们来仿写一个这个方法,我们已打开 led 灯代码为例。
1. 我们先在 MainActivity 里面实现三个方法,分别对应打开,关闭,和控制,我们添加以下代码:
public native int LedOpen();
public native int LedClos();
public native int LedIoctl(int num,int en);
如下图所示:
2.然后我们打开 native-lib.cpp 文件,仿写 jin 代码,分别仿写打开,关闭,控制的实现,代码如下:
extern "C" JNIEXPORT jint JNICALL
Java_com_example_jnitest_MainActivity_LedOpen(
JNIEnv* env, jobject /* this */) {
return 0;
}
extern "C" JNIEXPORT jint JNICALL
Java_com_example_jnitest_MainActivity_LedClos(
JNIEnv* env,
jobject /* this */) {
return 0;
}
extern "C" JNIEXPORT jint JNICALL
Java_com_example_jnitest_MainActivity_LedIoctl(
JNIEnv* env,
jobject /* this */,
jint num,
jint en) {
return 0;
}
如下图所示:
3.接下来我们添加控制 led 的 c 代码,这部分的代码和我们直接用 C 控制 led 的代码是一样的,我们先添加需要的头文件,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h> #include <string.h> #include <stdint.h> #include <termios.h> #include <android/log.h> #include <sys/ioctl.h>
如下图所示:
4.我们在 Java_com_example_jnitest_MainActivity_LedOpen 里面添加打开 led 设备节点的 C 代码: 先添加头文件和 fd 句柄的定义 代码如下
#include <android/log.h>为安卓打印调试信息的头文件。
#include <android/log.h>
int fd = 0;
如下图所示:
然后在 Java_com_example_jnitest_MainActivity_LedOpen 里面添加打开 led 节点的代码,代码如下:
fd = open("/dev/leds_ctl", O_RDWR | O_NDELAY | O_NOCTTY); if(fd <= 0){
android_log_print(ANDROID_LOG_INFO, "serial", "open /dev/leds_ctl Error");
}else{
android_log_print(ANDROID_LOG_INFO, "serial", "open /dev/leds_ctl Sucess fd=%d", fd);
}
如下图所示:
5.在 Java_com_example_jnitest_MainActivity_LedClos 里面添加关闭 fd 的代码,代码如下:
if(fd > 0) close(fd);
如下图所示:
6.在 Java_com_example_jnitest_MainActivity_LedIoctl 添加 led 控制的代码,代码如下:
ioctl(fd,num,en);
如下图所示:
7.我们点击一下编译,这样就可以在默认路径下生成我们这个 so 库了,如下图所示:
我们切换到 project 下面,在默认路径下可以看到我们生成的 so 库,如下图所示:
- 我们可以看到,我们生成的库有 arm64-v8a,armeabi-v7a,x86,x86_64 的库,但是我们实际上并不需要这么多库,在现阶段,我们需要需要 armeabi-v7a 就可以满足大部分的需求了,armeabi-v7a 兼容X86,ARMV7,ARMV8,而且这个库生成的路径很不好找,我们能不能改一下他生成的路径并只生成我们需要的库呢?答案当然是可以的。
(1)更改 so 库的生成目录,我们打开 CMakeList.txt,添加下面的一句设置:
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
简化这个设置就是:
set(原路径,目标路径,生成那些平台对应的库文件)
参数解析如下:
CMAKE_CURRENT_SOURCE_DIR:这个是 cmake 的库的原路径
/../jniLibs/:这个是指与 CMakeList.txt 所在目录的同一级目录
ANDROID_ABI :生成那些平台对应的库文件。
我们打开 CMakeList.txt,在里面添加这句代码,代码如下:
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
如下图所示:
(2)生成指定平台的 so 库,我们打开 gradle,这句代码的意思就是说添加只生成 armeabi-v7a 的库
abiFilters "armeabi-v7a"
如下图所示:
(3)我们把之前生成的 so 的库删掉,不然编译会出问题,因为不能同时有俩个一样的库,如下图所示:
(4)点击编译,编译完成我们就可以在 main 目录下看到我们生成的 armeabi-v7a 的库了,如下图所示:
这个我们就得到了 so 库,这样既保护了我们的代码,又方便别人使用,那么我们要怎么使用呢?
88.5 使用编译好的so库(实战)
上一小节我们已经编译了 so 库,那么我们或者其他人拿到这个 so 后要怎么使用呢?这一节我们通过写一个简单的 app 来给大家演示下。
我们打开 AS,然后新建一个项目,选择一个空的 Activity,如下图所示:
这里我要注意一下,我们的包名要和我们调用的 jni 库的包名一样,否则会出问题,之前我们在写 jni 的时候,我们的包名如下图所示:
包名:
那么我们在新建工程的时候包名也要是这个,如下图所示:
创建成功以后,我们找到我们新创建的这个工程的所在目录,然后把我们之前编译好的 so 库放进去。存放 so 库的文件夹一定要为 jniLibs,名字不要改,如下图所示:
Jnilibs 文件夹放的就是我们生成的 so 库,如下图所示:
然后我们点击一下 AS 软件的同步,如下所示:
同步完成以后就可以在工程目录下看到我们这个 so 的文件夹了,如下图所示:
然后我们在 MainActivity 里面引用我们这个库,我们添加以下代码:
static {
System.loadLibrary("native-lib");
}
如下图所示:
然后我们声明一下方法,在 MainActivity.java 里面添加以下代码,代码如下:
|
public native int LedOpen();
public native int LedClos();
public native int LedIoctl(int num,int en);
如下图所示:
然后我们打开 activity_main.xml 文件,
然后删掉以下代码:
设置俩个 button,代码如下:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
>
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="LED1 OFF" />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="87dp"
android:text="LED1 ON" />
</LinearLayout>
添加完如下图所示:
然后打开 MainActivity.java,声明俩个按键,并且在按键的点击事件里面打开设备节点和进行控制 led 灯的操作,代码如下:
声明俩个按键:
private Button led1_on;
private Button led1_off;
按键的点击事件
led1_on = (Button) findViewById(R.id.button1);
led1_off = (Button) findViewById(R.id.button2);
led1_on.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
LedOpen();
LedIoctl(1, 1);
}
});
led1_off.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view) {
LedOpen();
LedIoctl(0, 0);
}
});
}
如下图所示:
然后添加以下代码:
import android.view.View;
import android.widget.Button;
如下图所示:
然后我们连接开发板,这里我们使用的是 IMX6Q 开发板安卓 4.4 系统,这里一定要使用开发板,不然看不到点灯效果,连接好以后,我们点击运行,如下图所示:
然后我们就可以看到 app 已经成功安装到了开发板上,并且可以控制 led 灯