1.問(wèn)題與場(chǎng)景
在處理串口讀取數(shù)據(jù)時(shí)遇到的一個(gè)問(wèn)題醒串。讀取串口數(shù)據(jù)啟動(dòng)了一個(gè)線(xiàn)程去進(jìn)行輪詢(xún)讀取,如果串口中有數(shù)據(jù)就正常讀取安拟,并且返回讀取到的字節(jié)長(zhǎng)度系瓢,如果串口沒(méi)有數(shù)據(jù)寫(xiě)入時(shí)去進(jìn)行讀取的話(huà)這個(gè)操作是被阻塞住的浮庐。如果一直沒(méi)有數(shù)據(jù)寫(xiě)入弹惦,那就一直不能讀取返回矿卑。這就導(dǎo)致了一個(gè)問(wèn)題喉恋,就是線(xiàn)程永遠(yuǎn)都會(huì)被阻塞在讀取的那個(gè)地方。想要結(jié)束釋放這個(gè)線(xiàn)程的資源顯然是做不到的母廷,因?yàn)榫€(xiàn)程被阻塞住了轻黑。
2.個(gè)人思考
思考1:既然線(xiàn)程是被阻塞住了,那么我就想能不能通過(guò)線(xiàn)程中斷來(lái)進(jìn)行線(xiàn)程的停止琴昆。經(jīng)測(cè)試:調(diào)用線(xiàn)程的中斷方法(Thread.interrupted())氓鄙,線(xiàn)程并沒(méi)有接收到中斷信號(hào),所以此方法行不通业舍。
思考2:經(jīng)過(guò)上面的方法不行之后抖拦,又嘗試了使用線(xiàn)程池去執(zhí)行讀取數(shù)據(jù),然后調(diào)用線(xiàn)程池的shutdownNow()方法企圖想把線(xiàn)程資源釋放舷暮,結(jié)果還是不行态罪。后又嘗試了AsyncTask去進(jìn)行讀取,然后使用AsyncTask的cancel()方法去進(jìn)行線(xiàn)程資源釋放下面,結(jié)果還是不行复颈。這些方法都不能使被阻塞的線(xiàn)程得到資源的釋放。
思考3:后詢(xún)問(wèn)同事這種場(chǎng)景的問(wèn)題該怎么解決沥割,有什么方案推薦一下耗啦?同事說(shuō)用select(我以為說(shuō)的是Selector,NIO里面的一個(gè)類(lèi)java.nio.channels包下面的)我就去查閱Selector的使用方法驯遇,后經(jīng)研究發(fā)現(xiàn)芹彬,這個(gè)Selector是實(shí)現(xiàn)非阻塞IO的一種方式蓄髓,我看到的都是處理Sockect的叉庐,與我讀取串口數(shù)據(jù)使用場(chǎng)景不同。至此還是沒(méi)有想到解決辦法会喝。(因?yàn)楫?dāng)時(shí)沒(méi)有想到select這個(gè)技術(shù)陡叠,本人也沒(méi)了解過(guò)select,所以沒(méi)解決肢执。)
思考4:?jiǎn)栴}得不到解決枉阵,但是又想解決這個(gè)資源釋放的問(wèn)題。沒(méi)辦法就百度了selector.select()的方法是怎么實(shí)現(xiàn)的预茄,雖然我的場(chǎng)景不是sockect兴溜,但是我就想知道這個(gè)selector.select()被阻塞之后是怎么能返回的侦厚。百度了selector.select()之后發(fā)現(xiàn)很多前輩都提到select、poll拙徽、epoll刨沦。前兩者select、poll我沒(méi)聽(tīng)過(guò)膘怕,但是epoll這個(gè)我知道想诅,在安卓的Handler機(jī)制的MessageQueue.java的next()方法的nativePollOnce()方法的native源碼里面用的就是epoll機(jī)制。epoll機(jī)制大概的原理就是監(jiān)聽(tīng)一個(gè)fd岛心。如果fd有修改来破,那nativePollOnce()就會(huì)返回繼續(xù)執(zhí)行下面的代碼,同時(shí)nativePollOnce()還支持超時(shí)返回忘古。好家伙這不正是我想要實(shí)現(xiàn)的功能嗎徘禁?
思考5:既然網(wǎng)上都把select和poll和epoll進(jìn)行比較,我就去看了很多它們的介紹髓堪。其實(shí)三者都是可以實(shí)現(xiàn)我想要的功能晌坤。最終我選擇了select,因?yàn)橥陆o我指的路是select而且它實(shí)現(xiàn)起來(lái)相對(duì)容易旦袋,并且它雖然監(jiān)聽(tīng)fd的數(shù)量上有限制骤菠,但是已經(jīng)可以滿(mǎn)足我的需求了。
3.代碼實(shí)現(xiàn)
因?yàn)閟elect只有c代碼的實(shí)現(xiàn)疤孕,所以在android上只能使用JNI的方式商乎。
3.1創(chuàng)建一個(gè) MySelect.h 文件,內(nèi)容如下祭阀。
#include
#include
#include
#include
#include
#include
#include
JNIEXPORT jint
Java_myselect_MySelect_nativeRead(JNIEnv *env, jclass clazz, jobject file_descriptor,
jbyteArray data, jint datalen) ;
JNIEXPORT jint
Java_myselect_MySelect_nativeWrite(JNIEnv *env, jclass clazz, jobject file_descriptor,
jbyteArray data, jint datalen) ;
3.2創(chuàng)建一個(gè)MySelect.c文件鹉戚,內(nèi)容如下:
#include "Myselect.h"
#include "android/log.h"
static const char *TAG="caifeng";
#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args)
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
JNIEXPORT jint Java_myselect_MySelect_nativeRead(JNIEnv *env,jclass clazz,jobject file_descriptor,
jbyteArray data,jint datalen) {
jclass FileDescriptorClass = (*env)->FindClass(env,"java/io/FileDescriptor");
jfieldID descriptorID = (*env)->GetFieldID(env,FileDescriptorClass,"descriptor","I");
jint descriptor = (*env)->GetIntField(env, file_descriptor,descriptorID);
return uart_recv(descriptor,data,datalen);
}
JNIEXPORT jint Java_myselect_MySelect_nativeWrite(JNIEnv *env,jclass clazz,jobject file_descriptor,
jbyteArray data,jint datalen) {
jclass FileDescriptorClass = (*env)->FindClass(env,"java/io/FileDescriptor");
jfieldID descriptorID = (*env)->GetFieldID(env,FileDescriptorClass,"descriptor","I");
jint descriptor = (*env)->GetIntField(env, file_descriptor,descriptorID);
LOGD("descriptor寫(xiě)的 = %d",descriptor);
return uart_send(descriptor,data,datalen);
}
/**
*串口接收數(shù)據(jù)*要求啟動(dòng)后,在pc端發(fā)送ascii文件*/
int uart_recv(int serial_fd,char *data,int datalen)
{
int len=0,ret = -1;
fd_set fs_read;
struct timeval tv_timeout;
FD_ZERO(&fs_read);
FD_SET(serial_fd, &fs_read);
tv_timeout.tv_sec =40000;//這個(gè)是秒
tv_timeout.tv_usec =0;//這個(gè)是微秒
ret = select(serial_fd+1, &fs_read,NULL,NULL, &tv_timeout);
return ret;
// while(FD_ISSET(serial_fd,&fs_read))
// {
//
// FD_ZERO(&fs_read);
// FD_SET(serial_fd,&fs_read);
// ret = select(serial_fd+1, &fs_read, NULL, NULL, &tv_timeout);
// LOGD("descriptor select ret = %d",ret);
// printf("ret = %d\n", ret);
// //如果返回0专控,代表在描述符狀態(tài)改變前已超過(guò)timeout時(shí)間,錯(cuò)誤返回-1
//
//// if(FD_ISSET(serial_fd, &fs_read)) {
//// len = read(serial_fd, data, datalen);
//// LOGD("descriptor read len = %d",ret);
//// printf("len = %d\n", len);
//// return 1;
//// } else {
//// LOGD("descriptor select--select");
//// perror("select");
//// }
// }
// return ret;
}
/**
*串口發(fā)送數(shù)據(jù)*@fd:串口描述符*@data:待發(fā)送數(shù)據(jù)*@datalen:數(shù)據(jù)長(zhǎng)度*/
int uart_send(int serial_fd,char *data,int datalen)
{
int len =0;
len = write(serial_fd, data, datalen);//實(shí)際寫(xiě)入的長(zhǎng)度
if(len == datalen) {
return len;
}else {
tcflush(serial_fd,TCOFLUSH);//TCOFLUSH刷新寫(xiě)入的數(shù)據(jù)但不傳送
return -1;
}
return 0;
}
3.3編譯后加載so庫(kù):創(chuàng)建一個(gè)MySelect.java類(lèi):
package myselect;
import java.io.FileDescriptor;
public class MySelect {
static {
System.loadLibrary("myselect");
}
public static int read(FileDescriptor fileDescriptor,byte[] data,int datalen){
return nativeRead(fileDescriptor,data,datalen);
}
public static int write(FileDescriptor fileDescriptor,byte[] data,int datalen){
return nativeWrite(fileDescriptor,data,datalen);
}
public static native int nativeRead(FileDescriptor fileDescriptor,byte[] data,int datalen);
public static native int nativeWrite(FileDescriptor fileDescriptor,byte[] data,int datalen);
}
3.4使用MySelect.java里面的函數(shù)進(jìn)行讀取抹凳。
package com.rk.device.selecttest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Trace;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import android_serialport_api.SerialPort;
import myselect.MySelect;
public class MainActivity extends AppCompatActivity {
private SerialPort mSerialPort;
private static final String TTYS4 ="/dev/ttyS4";
private static final int IBAUDRATE =115200;
FileDescriptor mFd;
Button read,write;
Thread t1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("MainActivity",Log.getStackTraceString(new Throwable()));
read = findViewById(R.id.read);
write = findViewById(R.id.write);
try {
mSerialPort =new SerialPort(new File(TTYS4),IBAUDRATE,0);
mFd =mSerialPort.getmFd();
}catch (IOException e) {
e.printStackTrace();
}
read.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
t1 =new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.interrupted()) {
int len =MySelect.read(mFd,new byte[1024],1024);
Log.d("MainActivity","len=" + len);
if (len >0) {
try {
byte[]buff =new byte[1024];
len =mSerialPort.getInputStream().read(buff);
for (int i =0; i < len; i++) {
Log.d("MainActivity","讀取到的值=" +buff[i]);
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
Log.d("MainActivity","結(jié)束");
}
});
t1.start();
}
});
write.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// t1.interrupt();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(50);
}catch (InterruptedException e) {
e.printStackTrace();
}
try {
mSerialPort.getOutputStream().write(new byte[]{10,12,53});
}catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d("MainActivity",Log.getStackTraceString(new Throwable()));
}
}
3.5 下面是串口類(lèi)SerialPort.java 的定義:
package android_serialport_api;
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class SerialPort {
private static final String TAG ="SerialPort";
/*
* Do not remove or rename the field mFd: it is used by native method 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 {
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 setters
public InputStream getInputStream() {
return mFileInputStream;
}
public OutputStream getOutputStream() {
return mFileOutputStream;
}
public FileDescriptor getmFd() {
return mFd;
}
// JNI
private native static FileDescriptor open(String path,int baudrate,int flags);
public native void close();
static {
System.loadLibrary("serial_port");
}
}
System.loadLibrary("serial_port");這里加載的是我提前編譯好打開(kāi)串口的工具庫(kù)。網(wǎng)上也有一些打開(kāi)串口的其他方案伦腐。至此釋放線(xiàn)程資源的辦法就如上所述赢底。原理就是:使用select去監(jiān)聽(tīng)串口打開(kāi)的那個(gè)fd,如果監(jiān)聽(tīng)到有數(shù)據(jù)寫(xiě)入柏蘑,那就返回len>0 如果返回len>0 我們?cè)儆么诖蜷_(kāi)的流去進(jìn)行讀取幸冻,而不是直接使用流的read()去讀取。如果沒(méi)有數(shù)據(jù)寫(xiě)入咳焚,并且超時(shí)了就返回len<0.超時(shí)之后我們就可以釋放線(xiàn)程資源了洽损,或者讀取到數(shù)據(jù)之后也可以進(jìn)行資源的釋放。其實(shí)就是在getInputStream().read();之前添加select的監(jiān)聽(tīng)革半。這樣就可以防止線(xiàn)程長(zhǎng)時(shí)間被阻塞了碑定。如遇到IO阻塞問(wèn)題流码,也可以使用這種方案。
項(xiàng)目工程鏈接:
鏈接:https://pan.baidu.com/s/1_VXa2ixy2N5FLbCfBjHLuA
提取碼:x9gc