NDK动态编译本地库(回调方法的实现、JNI_OnLoad)

一、准备工作

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、成功运行则说明成功了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值