一、准备工作
1、开发环境
开发工具:Android Studio 3.1.3
JDK版本:jdk 1.8.0_91
NDK版本: ndk 16.1.4479499
CMake版本:cmake 3.6.4111459
Gradle版本:2.3.2
BuildTools版本:26.0.0
2、环境搭建并创建项目
- 从官网下载对应JDK和NDK,并将其解压到合适路径,配置系统环境变量。(不知道如何做的请问度娘)
- 启动Android Studio,创建项目并勾选Include C++ support,其他操作与正常创建项目相同。
- File -> Project Structure -> SDK Location,修改JDK loacation与Android NDK location为自己指定的路径,点击OK使设置生效。
- 点击Android Studio右上角的SDK Manager,勾选Show Package Details,下载相应版本的CMake,当然也可以下载最新版本的NDK,不过可能会出现版本不兼容导致的编译问题。
二、开发
1、定义接口类
根据项目需求,定义对应的方法函数:
package com.jimu.adas;
import com.jimu.adas.bean.ScreenOutInfo;
import com.jimu.adas.interfaces.JniInterface;
public class SerialPort {
private JniInterface mJniInterface = null;
//回调过程中接收的数据类型
private static ScreenOutInfo mScreenOutInfo = null;
//本地库对应的native方法
public native void serialOpen();
public native void serialReceive();
public native int serialSend(char[] buffer);
public native int serialClose();
/**
* 回调
* @param value
*/
public static void callbackJni(ScreenOutInfo value) {
mScreenOutInfo = value;
}
public void setJniInterfaceCallback(JniInterface jniInterface) {
this.mJniInterface = jniInterface;
jniInterface.updateUI(mScreenOutInfo);
}
//加载本地库,对应为libserial_port.so
static {
System.loadLibrary("serial_port");
}
}
2、底层实现java对应的native方法
#include <stdio.h>
#include "serial_screen.h"
#include "serial_tty.h"
#include <android/log.h>
#include <jni.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define CLASS_NAME "com/jimu/adas/SerialPort"
#define CLASS__INFO_NAME "com/jimu/adas/bean/ScreenOutInfo"
JavaVM *cached_jvm;
/*全局变量*/
jclass Class_C, Class_C_out_info;
jmethodID mid, callback_mid;
static int fd;
static unsigned char get_check_sum(void *data, size_t size) {
int i;
unsigned int sum = 0;
unsigned char *buf = (unsigned char *)data;
for (i = 0; i < size; i++) {
sum += buf[i];
}
sum = (sum >> 8) + (sum & 0xFF);
sum = (sum >> 8) + (sum & 0xFF);
sum = ~sum;
return (unsigned char)sum;
}
//底层解析收到的数据
static void process(JNIEnv *env, unsigned char *buf, size_t size){
if (size >= sizeof(screen_out_info_t)){
screen_out_info_t info = *(screen_out_info_t *)(buf + size - sizeof(screen_out_info_t));
LOGD("%s %d %d %d %d\n", __func__, __LINE__, info.flag, sizeof(info), get_check_sum(&info, sizeof(screen_out_info_t) -1 ));
if(info.flag == 0x5aa5 && get_check_sum(&info, sizeof(screen_out_info_t) -1 ) != 0) {
jobject obj = (*env)->NewObject(env, Class_C_out_info, mid, info.flag, info.command,
info.ldw, info.hmw, info.pcw, info.left_lane_exit,
info.right_lane_exit, info.aeb_status, info.ped, info.fcw, info.reserve, info.checksum);
(*env)->CallStaticVoidMethod(env, Class_C, callback_mid, obj);
}
}
}
JNIEXPORT void JNICALL open_serial
(JNIEnv *env, jobject jobject1){
if (fd > 0) {
serial_close(fd);
}
fd = serial_open("/dev/ttyMT5", 19200);
LOGI(" ########## fd = %d", fd);
}
JNIEXPORT void JNICALL serial_receive
(JNIEnv *env, jobject jobject1){
if (fd <= 0) {
return;
}
unsigned char buf[1024];
int size = serial_read(fd, buf, sizeof(buf));
LOGI(" ########## bufSize = %d", size);
if (size >= 8){
LOGI(" ##buf0 = %d, buf1=%d, buf2=%d, buf3=%d, buf4=%d, buf5 = %d, buf6=%d, buf7=%d", buf[0],buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]);
}
process(env, buf, size);
}
JNIEXPORT jint JNICALL serial_send
(JNIEnv *env, jobject jobject1, jcharArray info){
unsigned char* pBuffer = (*env)->GetCharArrayElements(env, info, NULL);
int count = (*env)->GetArrayLength(env, info);
if (fd <= 0) {
LOGI(" ########## fd = %d", fd);
return -1;
}
pBuffer[9] = get_check_sum(pBuffer, count - 1);
int size = serial_write(fd, pBuffer, count);
LOGI(" ##buf0 = %d, buf1=%d, buf2=%d, buf3=%d, buf4=%d, buf5 = %d, buf6=%d, buf7=%d, buf[9]=%d",
pBuffer[0],pBuffer[1], pBuffer[2], pBuffer[3], pBuffer[4], pBuffer[5], pBuffer[6], pBuffer[7], pBuffer[9]);
(*env)->ReleaseCharArrayElements(env, info, pBuffer, 0);
return size == sizeof(screen_in_info_t) ? 0 : -1;
}
JNIEXPORT jint JNICALL close_serial
(JNIEnv *env, jobject jobject1){
if (fd > 0){
serial_close(fd);
}
return JNI_TRUE;
}
//对应的java层native方法
//每个集合元素的第一个字段为java层的native方法名
//第二个字段为描述函数的参数和返回值的字符串
//第三个字段是函数指针,指向与java函数对应的C函数
static JNINativeMethod method_table[] = {
{"serialOpen", "()V", (void *) open_serial},
{"serialReceive", "()V", (void *) serial_receive},
{"serialSend", "([C)I", (void *) serial_send},
{"serialClose", "()I", (void *) close_serial}
};
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *jvm, void *reserved) {
JNIEnv *env;
jclass cls;
jclass cls_info;
cached_jvm = jvm;
//指定JNI版本
if ((*jvm)->GetEnv(jvm, (void **) &env, JNI_VERSION_1_6)) {
return JNI_ERR;//JNI 版本不支持
}
/*
* 返回jclass
* */
cls = (*env)->FindClass(env, CLASS_NAME);
if (cls == NULL) {
return JNI_ERR;
}
cls_info = (*env)->FindClass(env, CLASS__INFO_NAME);
if (cls_info == NULL) {
return JNI_ERR;
}
/*
* 弱引用,允许cls可以unloaded
* */
Class_C = (jclass) (*env)->NewWeakGlobalRef(env, cls);
if (Class_C == NULL) {
return JNI_ERR;
}
Class_C_out_info = (jclass) (*env)->NewWeakGlobalRef(env, cls_info);
//自定义类型的init方法
mid = (*env)->GetMethodID(env, Class_C_out_info, "<init>", "(IIIIIIIIIIII)V");
if (mid == NULL){
return JNI_ERR;
}
callback_mid = (*env)->GetStaticMethodID(env, Class_C, "callbackJni", "(Lcom/jimu/adas/bean/ScreenOutInfo;)V");
if (callback_mid == NULL){
return JNI_ERR;
}
LOGD(" $$$$$$%s %d \n", __func__, __LINE__);
/*
* 注册方法
* */
(*env)->RegisterNatives(env, cls, method_table, sizeof(method_table) / sizeof(method_table[0]));
LOGD(" After %s %d \n", __func__, __LINE__);
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *jvm, void *reserved) {
JNIEnv *env;
if ((*jvm)->GetEnv(jvm, (void **) &env, JNI_VERSION_1_6)) {
return;//JNI 版本不支持
}
(*env)->DeleteGlobalRef(env, Class_C);
(*env)->DeleteGlobalRef(env, Class_C_out_info);
return;
}
Andoird 中使用了一种不同传统Java JNI的方式来定义其native的函数。其中很重要的区别是Andorid使用了一种Java 和 C 函数的映射表数组,并在其中描述了函数的参数和返回值。这个数组的类型是JNINativeMethod,定义如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
第一个变量name是Java中函数的名字。
第二个变量signature,用字符串是描述了函数的参数和返回值,如代码块中的"([C)I"
第三个变量fnPtr是函数指针,指向C函数。
第二个变量的获取方式:
方法1、通过Android Studio的Terminal工具,执行指令
cd app\build\intermediates\classes\debug\package
javap -p -s SerialPort.class
方法2、同时点击WIN+R打开电脑命令窗口,键入指令
javap -p -s D:\Application\Fpga\app\build\intermediates\classes\debug\package\SerialPort.class
两种方法都会出现下图信息,其中每个函数下descriptor字段即为第二个变量。
3、CMakeLists.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
serial_port
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/serial_port.c
src/main/cpp/serial_tty.c)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
serial_port
# Links the target library to the log library
# included in the NDK.
${log-lib} )
创建Include C++ support项目时会自动生成Hello World的CMakeLists.txt文件,根据文件中提示进行相关修改。
4、build.gradle
编辑app目录下的build.gradle文件,在defaultConfig闭包中加入如下代码块:
ndk{
moduleName "serial_port"//本地库名,对应为libserial_port.so
abiFilters "armeabi", "armeabi-v7a", "x86"//支持的ABI构架
}
5、Activity调用,运行
new SerialPort().setJniInterfaceCallback(new JniInterface() {
@Override
public void updateUI(ScreenOutInfo screenOutInfo) {
if (screenOutInfo == null){
return;
}
tv.setText("flag:"+screenOutInfo.getFlag()+"\n"+"checksum:"+screenOutInfo.getChecksum());
}
});
运行成功即可在app\build\intermediates\cmake\debug\obj目录下找到存在文件libserial_port.so的三个ABI文件夹,至此,编译成功。
三、其他项目调用该本地库
1、创建项目,不用勾选Include C++ support;
2、在main目录下创建jniLibs文件夹,并将编译生成的ABI文件夹copy到jniLibs文件夹中;
3、将SerialPort.java拷贝到与编译时相同的路径下,值得一提的是,路径必须相同,否则会出现编译错误;
4、在app目录下的build.gradle的android闭包中添加如下代码
sourceSets.main {
jniLibs.srcDirs 'src/main/jniLibs'
}
5、成功运行则说明成功了。