一、前言
在 Qt for Android 中没办法像在嵌入式linux中一样直接使用 ioctl
等底层函数,这是因为因为 Android 平台的安全性和权限限制。
在 Android 中,访问设备硬件和系统资源需要特定的权限,并且需要通过 Android 系统提供的 API 来进行。Android 平台为了保障系统的安全性和稳定性,限制了应用程序对底层硬件和系统的直接访问。
Qt for Android 是建立在 Android NDK 和 Java 层之上的,它提供了一种跨平台的开发框架,允许开发者使用 C++ 和 Qt API 来开发 Android 应用程序。但是,由于 Android 平台的限制,Qt for Android 也受到了 Android 平台的限制,无法直接访问底层设备或调用底层系统函数。
我们需要通过 Java 层的 JNI 接口来间接访问,通过 JNI 接口调用底层的系统函数或设备驱动程序,下面我们通过一个socketcan的调用实例才讲解。
二、编写 JNI 接口
在Android路径下,新建一个jni文件夹,新建文件socketcan_native.c,部分代码内容如下:
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <net/if.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <fcntl.h>
#include <string.h>
#define STATUS_OK 0
#define STATUS_ERR -1
static int sock_fd;
static int m_isopen;
void sockcan_close()
{
close(sock_fd);
system(ip_cmd_can_close);
m_isopen = STATUS_ERR;
}
int sockcan_open(int bitrate)
{
//创建套接口
sock_fd = socket(AF_CAN,SOCK_RAW,CAN_RAW);
if(sock_fd < 0)
{
return STATUS_ERR;
}
//绑定can0设备与套接口
struct ifreq ifr;
struct sockaddr_can addr;
strcpy(ifr.ifr_name,"can0");
ioctl(sock_fd,SIOCGIFINDEX,&ifr);
ifr.ifr_ifindex = if_nametoindex(ifr.ifr_name);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
if(bind(sock_fd,(struct sockaddr *)&addr,sizeof(addr))<0)
{
perror("bind error!\n");
return STATUS_ERR;
}
//配置
int flags;
flags = fcntl(sock_fd,F_GETFL,0);
// flags |= O_NONBLOCK;//非阻塞
flags &= ~O_NONBLOCK;//阻塞
fcntl(sock_fd,F_SETFL,flags);
m_isopen = STATUS_OK;
return STATUS_OK;
}
JNIEXPORT jint JNICALL
Java_com_example_socketcan_SocketCANJNI_socketCanOpen(JNIEnv *env, jobject thiz, jint baudrate)
{
return sockcan_open(baudrate);
}
JNIEXPORT jint JNICALL
Java_com_example_socketcan_SocketCANJNI_socketCanWrite
(JNIEnv *env, jobject thiz, jint canId,jint dataLen,jint externFlag,jint remoteFlag,jbyteArray datas)
{
if(sock_fd <= 0)
return -1;
if(dataLen > 8)
return -2;
//获取实例的变量array的值
int nArrLen = (*env)->GetArrayLength(env,datas);
char *chArr = (char*)(*env)->GetByteArrayElements(env,datas, 0);
struct can_frame txframe;
memcpy(txframe.data, chArr, nArrLen);
txframe.can_id = canId;
if(externFlag)
txframe.can_id |= CAN_EFF_FLAG;
if(remoteFlag)
txframe.can_id |= CAN_RTR_FLAG;
txframe.can_dlc = dataLen;
return write(sock_fd, &txframe, sizeof(struct can_frame));
}
2.1. jni名称规范
jni接口的名称比如Java_com_example_socketcan_SocketCANJNI_socketCanWrite,这里是需要规范的,我们接下来会用两种方法调用,第一种传入JNIEnv指针直接调用,这种情况接口名称无所谓,第二种我们把jni接口编译成so,通过java层调用,这时候名称就很重要的,Java本地方法名以Java_
开头,后面跟着类名、下划线和方法名,也就是Java_包名_类名_方法名。方法名要驼峰写法,比如setBaud(),不能写set_baud()。
2.2. 封装jni接口
在Android/jni文件夹下,新建文件Android.mk,内容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := socketcan_native
LOCAL_SRC_FILES := socketcan_native.c
include $(BUILD_SHARED_LIBRARY)
然后在该目录使用ndk对其进行打包生成so文件,在libs文件夹下,会生成四个不同环境下的的so文件。
三、直接调用Jni接口
Qt侧声明接口,直接调用!!!
extern "C" JNIEXPORT jint JNICALL
JJava_com_example_socketcan_SocketCANJNI_socketCanOpen
(JNIEnv *env, jobject thiz, jint baudrate);
extern "C" JNIEXPORT jint JNICALL
Java_com_example_socketcan_SocketCANJNI_socketCanWrite
(JNIEnv *env, jobject thiz, jint canId,jint dataLen,jint externFlag,jint remoteFlag,jbyteArray datas);
jint attachResult = QAndroidJniEnvironment::javaVM()->AttachCurrentThread(reinterpret_cast<JNIEnv**>(&env), NULL);
if (attachResult != JNI_OK) {
qDebug() << "Failed to attach current thread to JVM";
}
JJava_com_example_socketcan_SocketCANJNI_socketCanOpen(env, NULL, 500000);
这种方法不需要使用jni编译的so库,等于通过jni调用的c代码,使用JNI接口可能会降低Qt应用程序的跨平台性,因为它会依赖于Java虚拟机的存在和正确配置,并且如果发生不正确的内存管理可能会导致内存泄漏或者内存错误。
四:Java层调用
右键项目新建一个SocketCANJNI.java文件
按示例我们需要把java文件要放在android/src/com/example/socketcan文件夹下,然后我们编辑文件,内容如下
package com.tbbpower.android;
import org.qtproject.qt5.android.bindings.QtActivity;
/***********************************
jint 对应 Java 层的 int
jboolean 对应 Java 层的 boolean
jbyte 对应 Java 层的 byte
jchar 对应 Java 层的 char
jshort 对应 Java 层的 short
jlong 对应 Java 层的 long
jfloat 对应 Java 层的 float
jdouble 对应 Java 层的 double
jobject 对应 Java 层的 Object 类型
jstring 对应 Java 层的 String 类型
jarray 对应 Java 层的数组类型(例如 jintArray 对应 int[])
***********************************/
public class SocketCANJNI{
static {
System.loadLibrary("socketcan_native");
}
public static native int open(int baudrate);
public static native int close();
public static native int setFilter(int extFrame, int startCanId,int endCanId);
public static native int write(int canId,int dataLen,int externFlag,int remoteFlag,byte[] datas);
public static native int read(int[] paramDatas,byte[] datas);
}
在示例中,备注了jni接口的方法名和java层的方法的类型对应。
在示例中,java层的方法名和jni的方法名一定要对应,否则会崩溃哦。
libsocketcan_native.so文件要放在android/libs文件夹下一起编译。
QT层调用:
QAndroidJniObject::callStaticMethod<jint>("com/tbbpower/android/SocketCANJNI",
"open",
"(I)I",
9600);
几个参数都简单,解释一下第三个参数(I)I,在Qt中,该方法用于调用Java类中的静态方法。第三个参数是一个字符串,它规定了被调用的 Java 方法的签名。Java 方法的签名描述了方法的参数类型和返回类型,签名的格式如下:
(参数类型1, 参数类型2, ...)返回类型
其中,参数类型和返回类型使用特定的字母代码表示,如下所示:
Z
:booleanB
:byteC
:charS
:shortI
:intJ
:longF
:floatD
:doubleV
:void[
:数组(后跟数组元素类型的签名,如[I
表示int[]
)
对于引用类型,使用L
作为前缀,L
后面跟着类的完全限定名,以分号结尾,如Ljava/lang/String;
表示String
类型。
举例来说,如果你要调用一个方法,它接受一个整数和一个字符串作为参数,并返回一个整数,你的方法签名将如下所示:
(ILjava/lang/String;)I
其中,I
表示整数,Ljava/lang/String;
表示字符串
五:接口调试
在 JNI 中,无法直接使用 printf
打印输出,我们需要使用Android NDK 提供的控制台输出函数进行log打印,可以输出多种类型的日子,常用的有debug\info\warn\error。
#include <jni.h>
#include <android/log.h>
JNIEXPORT void JNICALL
Java_com_example_package_ClassName_methodName(JNIEnv *env, jobject thiz) {
// 输出日志信息
__android_log_print(ANDROID_LOG_INFO, "Tag", "Your log message");
}
而Java层,我们要使用java自带的打印函数进行打印,方法类似。
import android.util.Log;
public class USBReceiver extends BroadcastReceiver {
private static final String TAG = "USBReceiver";
@Override
public void onReceive(Context context, Intent intent) {
try
{
Log.d(TAG, "onReceive");
}
catch (Exception ex)
{
Log.d(TAG, "onReceive-failed");
}
}
}