@Awille
2019-01-12T12:31:01.000000Z
字数 13615
阅读 84
Android Ndk开发
目前开发中设计到了串口读写的问题,这需要使用jni,鉴于以前没有实践过,下面尝试下踩踩坑。
目前在Android Studio3.0以上的版本可以直接构建一个ndk demo,ide会自动创建一个例子,但是我觉的这样还是不是十分了解ndk的一个细节开发流程,害怕以后开发遇到坑不会解决,所以决定还是自己尝试一下,熟悉一下流程。如果觉得赶时间的建议暂时就不要看这篇文章了,直接创建含有c++ support的项目文件就可以了,直接就有demo在老方法上踩踩坑,熟悉一下流程。
1、下载并配置ndk以及c++开发相关工具。
在目录Tool->SDK Manager->System Settings->Android SDK->Sdk Tools
勾选一下三个工具:
Android NDK: native develop kit,jni开发工具。首先要明确JNI是java语言特性,是用来让java与其他语言进行交互的接口(如c,c++),它与android无关,而ndk是android开发工具包,是用来将c,c++编译成的so库打包到apk文件当中的。
CMake:外部构建工具,c,c++编译工具,用来加载so库,
LLDB:c、c++代码调试器
2、在local.properties指定ndk路径
ndk.dir=C:\\Android\\Sdk\\ndk-bundle
sdk.dir=C:\\Android\\Sdk
3、在gradle.properties文件夹当中,添加属性:(如果你使用的是android.mk和application.mk进行配置才需要使用下面的属性,使用cmake可以跳过):
android.useDeprecateNdk=true
//这里的声明是为了说明对旧版本ndk的支持
1、在java的类当中声明本地方法
2、将java文件编译成class文件
3、用javah 生成.h头文件
4、完成c++代码的编写
5、编译so库
6、链接so库
在ndk开发大致中应该也是这个流程,但是应该会根据具体的ndk环境
首先感受下ndk的编译过程:在这流程的实现上会有些许不同。
在用编译android studio直接生成的ndk demo的时候,查看assemble task的执行,我们可以看到下面这样的执行日志:
Build native-lib x86_64
[1/2] Building CXX object CMakeFiles/native-lib.dir/src/main/cpp/native-lib.cpp.o
[2/2] Linking CXX shared library ..\..\..\..\build\intermediates\cmake\debug\obj\x86_64\libnative-lib.so
Build native-lib x86
[1/2] Building CXX object CMakeFiles/native-lib.dir/src/main/cpp/native-lib.cpp.o
[2/2] Linking CXX shared library ..\..\..\..\build\intermediates\cmake\debug\obj\x86\libnative-lib.so
Build native-lib arm64-v8a
[1/2] Building CXX object CMakeFiles/native-lib.dir/src/main/cpp/native-lib.cpp.o
[2/2] Linking CXX shared library ..\..\..\..\build\intermediates\cmake\debug\obj\arm64-v8a\libnative-lib.so
Build native-lib armeabi-v7a
[1/2] Building CXX object CMakeFiles/native-lib.dir/src/main/cpp/native-lib.cpp.o
[2/2] Linking CXX shared library ..\..\..\..\build\intermediates\cmake\debug\obj\armeabi-v7a\libnative-lib.so
可以看到这里就是cmake编译不同cpu架构下的c++文件,并且绑定so库。 想验证的同学可到具体目录下查看相关的源文件以及编译生成的so库。
这里AS是默认全平台编译的,如果你需要编译那么多,只要特定的几个平台,那么在gradle的 android的defaultconfig字段当中,指定编译平台就好了:
ndk {
//设置支持的SO库架构(开发者可以根据需要,选择一个或多个平台的so)
abiFilters "armeabi-v7a", "arm64-v8a"
}
现在版本的android studio都是默认使用cmake工具进行ndk开发的,如果在老项目是沿用ndk-build进行ndk开发,那么就只好继续使用,但是老项目还没有ndk模块或创建一个全新的项目,推荐使用这种方式进行
1、在模块->src->main 目录之下创建新的文件夹,用于存放我们写的c或c++代码文件。
(我这里创建文件夹名为 jni)
2、创建新的源文件 (如serialPort.cpp)
这里先创建空文件,目前主要目的是把ndk的开发流程跑通
1、在模块跟目录下创建File文件,文件名字一定要是CMakeLists.txt,这是一个纯文本文件。
2、创建文件以后可以采用就可以往里面添加脚本命令来创建原生库。
# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build
# 声明 CMake工具的最低版本
cmake_minimum_required(VERSION 3.4.1)
# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add.library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.
add_library( # Specifies the name of the library.
serial
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
# 这里可以指定多个
src/main/jni/SerialPort.cpp )
看到上面add_library的设置so库参数名当中,我设置的为serial,Cmake采用以下规范来为库文件命名:
lib库名称.so
所以可以知道文件名为libserial.so
(下面这个在找不到头文件的时候才要管):
使用 add_library() 向您的 CMake 构建脚本添加源文件或库时,Android Studio 还会在您同步项目后在 Project 视图下显示关联的标头文件。不过,为了确保 CMake 可以在编译时定位您的标头文件,您需要将 include_directories() 命令添加到 CMake 构建脚本中并指定标头的路径
include_directories(src/main/cpp/include/)
假如你需要使用ndk提供的一些api库,你可以在cmake脚本文件当中加上去:
find_library( # Defines the name of the path variable that stores the# location of the NDK library.log-lib# Specifies the name of the NDK library that# CMake needs to locate.log )
log为ndk提供的android特定日志的支持库,这里将log库的路径存储在log_lib变量当中。
为了确保你编译的so库可以调用你添加的api库,您需要使用Cmake构建脚本中的target_link_libraties命令关联库:
target_link_libraties专门用来链接库的,当你需要在你的原生库中调用一些别的库,你都需要这个链接。默认情况下,库依赖项是传递的。当这个目标链接到另一个目标时,链接到这个目标的库也会出现在另一个目标的连接线上。这个传递的接口存储在interface_link_libraries的目标属性中,可以通过设置该属性直接重写传递接口。
# Links your native library against one or more other native libraries.target_link_libraries( # Specifies the target library.serial# Links the log library to the target library.${log-lib} )
在ndk当中,还会以源代码的形式包含一些库,可以用add_library将这些源代码编译成so库,然后target_link_libraries添加到你想要使用这个编译出来的so库 的 so库当中。
这里的预构建库,可以理解为已经编译好的so库,假若你想直接依赖一个别人给你的so库,则使用下面的Cmake 命令:
这个与编译so库相似,需要使用IMPORTED标志告知Cmake你只希望将库导入到项目当中。
下面参数的第一个依然是so库的名字,你可以自己选。记得与下面set_target_properties的第一个参数照应。
add_library( imported-libSHAREDIMPORTED )
接着使用set_target_properties命令指定要导入so库的路径。
某些库在不同的ABI(Application Binary Interface)(CPU架构:x86, x86_64, arm64-v8a, armeabi-v7a)中,提供有不同的so库,你想一次性添加的话,可以使用
ANDROID_ABI 路径变量,这变量存储了ndk默认支持的cpu架构名字。
假如你想要指定平台,参考上面提到的ndk字段指定abiFilters的值。
set_target_properties( # Specifies the target library.imported-lib# Specifies the parameter you want to define.PROPERTIES IMPORTED_LOCATION# Provides the path to the library you want to import.imported-lib/src/${ANDROID_ABI}/libimported-lib.so )
同样为了确保Cmake可以定位到你的头文件,也需要使用include_directories包含头文件路径。
include_directories( imported-lib/include/ )
互相链接:serial 链接 imported-lib, imported-lib链接log-lob
target_link_libraries(serial imported-lib ${log-lib})
public class SerialPort {static {System.loadLibrary("serial");//参数为so库名称}}
意思即是将grale与你的cmakelists脚本文件相关联,让androidstudio识别到脚本文件。
方法1:直接模块的grale文件当中,在android字段里面添加字段:
// Encapsulates your external native build configurations.externalNativeBuild {// Encapsulates your CMake build configurations.cmake {// Provides a relative path to your CMake build script.path "CMakeLists.txt"}}
注:如果您想要将 Gradle 关联到现有 ndk-build 项目,请使用 ndkBuild {} 块而不是 cmake {},并提供 Android.mk 文件的相对路径。如果 Application.mk 文件与您的 Android.mk 文件位于相同目录下,Gradle 也会包含此文件。
另外,还可以在defaultConfig当中,配置另外一个externalNativeBuild{}块,为Cmake或ndkbuild 指定相关参数(这一步我们可以暂时忽略)
下面我们同步下项目,如果同步到项目出错,请认真检查才编写Cmakelists文件的时候,相关路径是否写对了,比如add_library指定c++源文件的时候。
同步成功后,编译项目,你可以看到编译成功的so文件,在/Users/will/AndroidStudioProjects/HelloJniOld/app/build/intermediates/cmake/debug/obj 路径之下,我这里只编译了debug版本,所以在debug目录之下。
开发流程大致如此,后面就是jni开发中的技术问题了。
1、首先要了解 JNI c++ 文件的方法命名规范:
Java_包名类名方法
2、JNI数据类型和类型对应关系表,这里可以自己上网找,里面描述了JNI类型(即是卸载c++ 文件中的类型)跟java类型的对照。这里给你一个 网址吧,先大概浏览下,继续往下面的流程走 :
Jni本地类型与java类型对照表
知道以上两个规则之后,我们假若在包 com.example.will 下的类 Serial 中有个方法 public native String JniTest(); 那么c++ 中文件为:
extern "C" JNIEXPORT jstring JNICALLJava_com_example_will_Serial_JniTest(JNIEnv *env, jobject instance) {// TODOreturn (*env)->NewStringUTF(env, returnValue);}
其中 JNIEXPORT 与 JNICALL为JNI中宏定义,我们暂时不管
extern "C":这是用来c与c++混编的,的主要作用就是为了能够正确实现C++代码调用其他C语言代码,如果创建的是cpp文件,一定要加否则会报错找不到本地函数的实现方法,但如果创建的是c文件,就不用写
JNIEnv* : 指向jni环境的指针
jobject:即java对象中的this
创建c++文件跟c文件,对env指针的操作不一样,差别在哪里自己上网查,这里说明下:
return (*env)->NewStringUTF(env, returnValue);
这是c的写法
c++的写法为:
return env->NewStringUTF(returnValue);
下面开始尝试:
实现函数:
#include <jni.h>#include <string>#include <iostream>using namespace std;//函数的格式名字:Java_包名_类名_方法//所以函数名字为: Java_com_example_will_hellojniold_SerialPort_testJni 返回参数与传入参数看后面extern "C" JNIEXPORT jstring JNICALLJava_com_example_will_hellojniold_SerialPort_testJni(JNIEnv *env, jobject instance) {char* returnValue = "Hello";return env->NewStringUTF(returnValue);}
其中java文件声明为:
package com.example.will.hellojniold;public class SerialPort {static {System.loadLibrary("serial");}public native String testJni();}
在mainactivity 的oncreate中调用:
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Log.e("Awille", new SerialPort().testJni());}
跑起来跑起来:在logcat中看到:
11-19 20:23:52.650 1773-1773/? E/Awille: Hello
#include <android/log.h>//在需要log的地方使用:__android_log_print(ANDROID_LOG_DEBUG, "Awille", "in c++ success");
查看logcat输出:
11-19 21:03:08.319 1857-1857/com.example.will.hellojniold E/Awille: in c++ success
Bingo 下面揭开串口开发的神秘面纱;
下面是c++有关串口读写的相关函数说明:(可以先看下面的实现,看到不懂再来翻这里)
1、tcgetattr函数的说明
2、c文件打开函数open说明第三个参数是打开文件的方式
在我们使用这个函数的时候,是传入flag为0的,看c里面的实现是 0_RDWR|flag 可见一可读写加0代表的含义打开文件。在c++的fcntl.h文件当中,有对0的宏定义,0代表的是
#define _O_RDONLY 0x0000
只读,而0_RDWR代表的值是2,所以 0_RDWR|0 = 0_RDWR|_O_RDONLY, 那么我们传入的flag值是希望当文件不是可读写时,我们希望以只读的方式打开,如果可读都不能打开才报错。
3、tcgetattr函数说明
tcgetattr函数用于获取与终端相关的参数。参数fd为终端的文件描述符,返回的结果保存在termios 结构体中,该结构体一般包括如下的成员:
termios结构体的定义,tcflag_t类型是unsigned int, cc_t c_cc是unsigned char, NCCS值为19。
struct termios {tcflag_t c_iflag;tcflag_t c_oflag;tcflag_t c_cflag;tcflag_t c_lflag;cc_t c_line;cc_t c_cc[NCCS];};
4、有关串口读写的相关知识 //有关串口部分在博客的偏后部分
c++实现:
#include <jni.h>#include <string>#include <android/log.h>#include <iostream>#include <sys/ioctl.h>#include <fcntl.h>#include <pty.h>#include <unistd.h>using namespace std;static char* TAG = "SerialPort";//鉴于在android日志当中输出太麻烦了,这里用宏做做一个封装#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args);//函数的格式名字:Java_包名_类名_方法//所以函数名字为: Java_com_example_will_hellojniold_SerialPort_testJni 返回参数与传入参数看后面extern "C" JNIEXPORT jstring JNICALLJava_com_example_will_serialport_SerialProvider_Serial_JniTest(JNIEnv *env, jobject instance) {char* returnValue = "Hello";__android_log_print(ANDROID_LOG_ERROR, TAG, "Hey, i am from c++");return env->NewStringUTF(returnValue);}//speed_t为unsigned int 类型 剩下的B值全部都有定义 在sys/ioctl.h文件当中static speed_t getBaudrate(jint bau) {switch (bau) {case 0: return B0;case 50: return B50;case 75: return B75;case 110: return B110;case 134: return B134;case 150: return B150;case 200: return B200;case 300: return B300;case 600: return B600;case 1200: return B1200;case 1800: return B1800;case 2400: return B2400;case 4800: return B4800;case 9600: return B9600;case 19200: return B19200;case 38400: return B38400;case 57600: return B57600;case 115200: return B115200;case 230400: return B230400;case 460800: return B460800;case 500000: return B500000;case 576000: return B576000;case 921600: return B921600;case 1000000: return B1000000;case 1152000: return B1152000;case 1500000: return B1500000;case 2000000: return B2000000;case 2500000: return B2500000;case 3000000: return B3000000;case 3500000: return B3500000;case 4000000: return B4000000;default: return -1;}}//__________________________________________________________________extern "C"JNIEXPORT jobject JNICALLJava_com_example_will_serialport_SerialProvider_Serial_open(JNIEnv *env, jclass type, jstring path_,jint baudrate, jint flags) {int fd;//文件描述符,每个文件会有唯一的文件描述符speed_t speed;jobject mFileDescriptor;speed = getBaudrate(baudrate);if (speed == -1) { //获取波特率失败LOGE("Invalid baudrate");return NULL;}jboolean iscopy;const char* path_uft = env->GetStringUTFChars(path_,&iscopy);//0_RDWR :代表可读可写 0_RDONLY:只读 O_WRONLY:只写LOGE("Opening serial port %s with flags 0x%x", path_uft, 0_RDWR | flags);//open函数为c++打开文件函数,定义在fcntl.h文件当中fd = open(path_uft, 0_RDWR | flags);LOGE("open() fd = %d", fd);if (fd == -1) {//要么串口地址不存在,要么没有权限LOGE("Cannot open the port");return NULL;}//如果打开成功,则对串口进行配置struct termios cfg;if (tcgetattr(fd, &cfg)) {LOGE("tcgetattr() failed");close(fd);return NULL;}//设置读取串口的波特率cfmakeraw(&cfg);cfsetispeed(&cfg, speed);cfsetospeed(&cfg, speed);if (tcsetattr(fd, TCSANOW, &cfg)) {LOGE("tcsetattr() failed");close(fd);return NULL;}//创建匹配的文件描述符jclass cFileDescriptor = env -> FindClass("java/io/FileDescriptor");jmethodID iFileDescriptor = env -> GetMethodID(cFileDescriptor, "<init>", "()V");jfieldID descriptorID = env -> GetFieldID(cFileDescriptor,"descriptor", "I");mFileDescriptor = env -> NewObject(cFileDescriptor, iFileDescriptor);env -> SetIntField(mFileDescriptor, descriptorID, (jint)fd);return mFileDescriptor;}//__________________________________________________________________extern "C"JNIEXPORT void JNICALLJava_com_example_will_serialport_SerialProvider_Serial_close(JNIEnv *env, jobject instance) {jclass SerialPortClass = env -> GetObjectClass(instance);jclass FileDescriptorClass = env-> FindClass("java/io/FileDescriptor");jfieldID mFdID = env->GetFieldID(SerialPortClass, "mFd", "Ljava/io/FileDescriptor;");jfieldID descriptorID = env->GetFieldID(FileDescriptorClass, "descriptor", "I");jobject mFd = env->GetObjectField(instance, mFdID);jint descriptor = env->GetIntField(mFd, descriptorID);LOGE("close(fd = %d)", descriptor);close(descriptor);}
2、在c的部分完成了以后,java部分就比较简单了:
public class Serial {static {System.loadLibrary("serial");//参数为so库名称}public native String JniTest();private native static FileDescriptor open(String path, int baudrate, int flags);public native void close();private FileDescriptor mFd;private FileInputStream mFileInputStream;private FileOutputStream mFileOutputStream;public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException {/* Check access permission */if (!device.canRead() || !device.canWrite()) {try {/* Missing read/write permission, trying to chmod the file */Process su;su = Runtime.getRuntime().exec("/system/bin/su");String cmd = "chmod 666 " + device.getAbsolutePath() + "\n"+ "exit\n";su.getOutputStream().write(cmd.getBytes());if ((su.waitFor() != 0) || !device.canRead()|| !device.canWrite()) {throw new SecurityException();}} catch (Exception e) {e.printStackTrace();throw new SecurityException();}}mFd = open(device.getAbsolutePath(), baudrate, flags);if (mFd == null) {Log.e(TAG, "native open returns null");throw new IOException();}mFileInputStream = new FileInputStream(mFd);mFileOutputStream = new FileOutputStream(mFd);}// Getters and setterspublic InputStream getInputStream() {return mFileInputStream;}public OutputStream getOutputStream() {return mFileOutputStream;}}
得到输入输出流后面就是对流进行操作了
3、有关串口的路径问题
在一般情况下,串口路径都存储在/dev路径之下
public Vector<File> getDevices() {if (mDevices == null) {mDevices = new Vector<File>();File dev = new File("/dev");File[] files = dev.listFiles();int i;for (i=0; i<files.length; i++) {if (files[i].getAbsolutePath().startsWith(mDeviceRoot)) {Log.d(TAG, "Found new device: " + files[i]);mDevices.add(files[i]);}}}return mDevices;}
## Android.mk文件的编写(待续)
LOCAL_PATH := $(call my-dir)//指定.mk的路径 $(call my-dir)调用ndk内部的函数获得当前.mk的路径//每个android.mk文件必须以定义LOCAL_PATH为开始,用于在开发tree中查找源文件//宏my-dir由build system提供,返回包含android.mk的目录路径include $(CLEAR_VARS)//清空除了local_path之外所有LOCAL_xxx变量的值//CLEAR_VARS变量由build System提供,并指向一个指定的TARGET_PLATFORM := android-17LOCAL_MODULE := serial_port_frameworkLOCAL_SRC_FILES := SerialPort.cLOCAL_LDLIBS := -lloginclude $(BUILD_SHARED_LIBRARY)
1、在要使用本地方法的build.gradle配置文件当中,添加ndk节点:
在android字段的defaultConfig字段中添加你打开节点:
ndk {moduleName "serial_jni" //编译的so库的名字stl "stlport_static" //使用stl标准库,默认情况下是无法使用的ldLibs "log" //log表示加入android的调试日志,只要导入 #include <android/log.h>//就可以使用 _android_log_print方法打印日志到logcat当中}