利用树莓派与安卓搭建监控系统

在本文中,使用一个简单的方法搭建了一个可以在局域网中使用的监控系统(应用:安卓遥控智能小车,并在安卓端查看实时图像)

所需软件

  1. mjpg-streamer:这是一个开源的推流工具,地址在这里,支持多种输入方式,可以直接读取摄像头,也可以使用opencv,在github中也说明了方法
  2. Visual Studio + Xamarin:在本文中,将使用Xamarin来进行安卓开发,使用原生的开发方式(例如Android Studio)也可以,方法类似

步骤

  1. 按照mjpg-streamer的文档的说明,开启推流,下面的例子是直接使用摄像头:
mjpg-streamer -o "/YOUR/PATH/TO/output_http.so" -i "/YOU\/PATH/TO/input_uvc.so"

注意一定要将YOUR/PATH/TO改为你自己的相应的路径~

成功执行上面的命令后,打开浏览器,输入http://localhost:8080/?action=stream就可以看到图像啦
http://localhost:8080/?action=snapshot则是获取一个截图

上面所说的是,在本机推流,所以如果要在树莓派上的话,就迁移到树莓派上即可。如果在树莓派上的话,就需要将localhost改为树莓派的ip地址

安卓端

由于mjpg-streamer已经将图像推到一个网页上了,所以在安卓端我们也无需费事,直接拖一个WebView即可,将WebView的Source属性设置为树莓派的IP地址即可~


以上

在线程中使用Toast以及其他有关UI更新的API

背景与声明

博主在写这篇文章的时候,使用的环境是Xamarin.Form,Xamarin.Android也适用。不过考虑到Xamarin.Form/Android会运行在Android环境中,所以本文内容也适用于原生Android开发。

但是与原生Android开发的不同点在于,Xamarin.From/Android使用的是C#语言,而原生Android开发使用的是Java/Kotlin,所以在语法、库以及编程习惯等多方面会有不同,请读者悉知。

序言

在Android中,为了良好的用户体验,是不允许在UI进程(主进程)中进行一些可能运行比较长的时间的操作的(例如网络操作和文件操作)。其原因也简单,如果在主进程中进行了这样的操作(例如写文件),可能会导致UI长时间无响应,进而影响用户体验。所以这样的操作一般要放在单独的进程中。

正文

我们现在解决了在UI中执行长时间操作的问题(具体方法待补充),但是如果我们如果需要在“长时间操作”中执行UI更新怎么办?直接在里面写吗?在下面的代码中,写了一个方法,而这个方法将会以线程的形式来执行:

private void Run()
{
    // Do something below.
        // ...
        // Make a Toast
    Android.Widget.Toast.MakeText(Android.App.Application.Context, "这是一个Toast", ToastLength.Short).Show();
        // Do something else.
        // Blah Blah ...
}

但是如果这样写了代码,我们就会遇到一个异常

Can't toast on a thread that has not called Looper.prepare()

这是为什么呢?在报错中提到的Looper.prepare()又是什么呢?

关于Looper类

Looper是一个类,主要用来为一个线程开启一个消息循环。在默认情况下,Android没有为每一个线程开启消息循环(关键是Android也不能确定我们的线程到底需不需要这个消息循环,如果万一我们不需要,而Android又为我们开启了,那岂不是浪费资源?)。

为什么要用Looper

更新UI(Toast也算是更新UI的一种),是需要主线程来做的。如果真的要子线程(也就是我们自己开启的线程)来做,可能会引起一系列的问题,况且Android也不允许我们这样做。实际上不仅Android中,在之前编写C#图形界面的时候,也有这种不允许子线程更新UI的情况。

所以如果我们需要更新UI,需要使用消息队列来处理这个操作。也就是说,我们需要把更新UI的操作串行化。具体的方法是使用一个消息队列。如果我们需要在子线程中更新UI,就要将一个更新UI的操作以一个消息(可以理解为请求)的形式发布出去,由管理UI的线程(主线程)来处理,至于到底执行不执行,以及如何执行,就是主线程的事了。

在C#中,处理的方法是使用一个委托来异步地执行UI更新操作,而在Android中则是使用一个消息队列,在更新UI前,准备好一个消息队列,然后执行操作,这样,要执行的UI更新操作就进入到了消息队列中,再由主线程来执行具体的操作。具体使用的方法是,在执行UI更新之前,调用Looper.prepare()方法。例如我们Toast一下:

// 前面的代码省略
Looper.Prepare();
Android.Widget.Toast.MakeText(Android.App.Application.Context, "这是一个Toast", ToastLength.Short).Show();

这样就行了吗?答案是否定的,我们Prepare()了一个Looper那么下面的指令就会进入到队列里,但是我们还要说明,到哪里我们的代码就不进入队列了,方法是使用Looper.Loop()方法。完整的代码如下:

Looper.Prepare();
Android.Widget.Toast.MakeText(Android.App.Application.Context, "这是一个Toast", ToastLength.Short).Show();
Looper.Loop();

使用Looper的弊端

有一点要说明一下,在一个进程(或者是Handler)中,Looper.Loop()方法后面的代码是不会被执行到的,也就是说,如果有下面这样的代码:

Looper.Prepare();
Android.Widget.Toast.MakeText(Android.App.Application.Context, "这是一个Toast", ToastLength.Short).Show();
Looper.Loop();
// Some code below Looper.Loop()

那么// Some code below Looper.Loop()是不会执行的,所以这种方法的应用也比较有限。例如,我们在一个线程(或者Handler)中只需要在最后才需要更新UI,就可以使用这种方法。但是如果我们需要一直开一个进程(例如一直侦听一个端口),那么就不太合适了(实际上,如果我们如果真的有这种需求的话,会用Service来实现吧(* ̄︶ ̄))

不过既然提到了,就继续说吧~

使用Handler来实现

这个方法说到底还是使用消息队列,不过好处在于,我们不需要使用Looper,也就意味着,在更新UI的代码后面,我们还可以放其他的代码~

我们可以使用Handler来处理消息:

private Handler msgHandler = new Handler((Message msg) =>
{
        Android.Widget.Toast.MakeText(Android.App.Application.Context, "这是一个Toast", ToastLength.Short).Show();
});

上面的代码实际上是一个Action(这是C#里面的概念!)

而使用它的方法也很简单,假设下面的代码将要在一个进程中执行:

Message m = msgHandler.ObtainMessage();    // 注意,这里的msgHandler就是我们在上面声明的Handler,所以一定要注意其作用域。
m.Arg1 = Resource.String.string_SomeString_1;    // Arg1 和 Arg2 只能是int类型,所以一般用来传递Resource里面的东西。
m.Arg2 = Resource.String.string_SomeString_2;
m.Obj = "Message类的Obj成员,可以指定任何类型的数据,这个比较自由"。
msgHandler.SendMessage(m);

在使用Message的时候,一定要注意,Arg1Arg2只能赋值为int,所以只能使用Resource里面的物件(string、图片甚至Layout等等),而Obj就比较自由了,可以配置各种类型(因为Obj就是Object嘛~(* ̄︶ ̄))。

结束

EOF

在Android中进行Socket编程

【此处应该有关于Socket编程和网络模型的介绍】

Java中常用的有关网络的类

针对不同的网络通信层次,Java给我们提供的网络功能有四大类:

InetAddress: 用于标识网络上的硬件资源
URL: 统一资源定位符,通过URL可以直接读取或者写入网络上的数据
Socket和ServerSocket: 使用TCP协议实现网络通信的Socket相关的类
Datagram: 使用UDP协议,将数据保存在数据报中,通过网络进行通信

Android 中的Socket编程模型

安卓中的Socket编程模型
在android中,实现Socket通信需要四个步骤:
1. 创建ServerSocket 或者Socket, 前者是作为服务器所需要的,而后者是作为客户端所需要的。在创建相应的Socket的时候,需要制定相应的地址和端口(或者侦听端口)
2. 打开Socket对应的输入/输出流
3. 按照协议对输入/输出流进行操作
4. 按照顺序关闭输入/输出流,以及Socket

Socket服务端

如果需要在安卓上建立服务端,那么我们需要这样做:
1. 建立ServerSocket,指定要侦听的IP地址和端口(注意IP地址不能是Localhost对应的地址,很多时候localhost对应的地址是127.0.0.1,有时会侦听不到)
2. 调用accept()方法来监听
3. 连接建立后,通过输入流读取客户端发来的消息;
4. 通过输出流响应客户端;
5. 按照顺序关闭输入/输出流,以及Socket来结束通信
示例代码:

public void DoServer(string IP, int port)
{
    ServerSocket serverSocket = new ServerSocket(port); // 创建ServerSocket
    Socket socket = serverSocket.accept() // 侦听端口,接受请求
    InputStream inStream = socket.getInputStream(); //创建InputStream
    OutputStream outStream = socket.getOutputStream(); //创建OutputStream
    InputStreamReader inputStreamReader = new InputStreamReader(inStream, "UTF-8"); //创建InputStreamReader,并配置编码,此处的编码应该和客户端发送的时候的编码一致,否则会出现乱码
    BufferedReader bufReader = new BufferedReader(inputStreamReader); //创建缓冲读取,这样我们就可以从缓冲区读取数据,而不用担心数据丢失
    while(string data = bufReader.readLine() != null) //循环读取缓冲区
    {
        // do something you want
        System.out.println(data);
    }
    socket.shutdown();
    socket.close();
}

Socket客户端

在客户端,需要做的事情就会少一些(不用侦听),一共需要大概四步:
1. 创建Socket对象,说明服务端的IP和端口号;
2. 通过输出流向服务端发送信;
3. 通过输入流获取服务端发来的消息;
4. 关闭有关的Socket;
示例代码:

public void DoClient(string IP, int port)
{
    Socket socket = new Socket(IP, port); // 创建Socket,指明服务端的IP和端口
    OutputStream outStream = socket.getOutputStream(); // 从socket创建输出流
    PrintWriter printWriter = new PrintWriter(outStream); //创建流写入器
    // do something with printWriter
    printWriter.write("Hello, server!");
    printWriter.flush(); // 这一步是必要的,有时向printWriter写入的数据不会立即发送,这时我们就需要flush一下
    socket.shutdownOutput(); //关闭输出流
    socket.close();

Note

  • 需要注意的一点是,在Android中不允许在主线程中进行网络操作,如果这样做了,那么程序会在进行网络链接的时候崩溃/闪退/Crash,崩溃的原因是抛出了“NetworkOnMainThreadException”异常参见此处

    The explanation as to why this occurs is well documented on the Android developer’s site:

    A NetworkOnMainThreadException is thrown when an application attempts to perform a networking operation on its main thread. This is only thrown for applications targeting the Honeycomb SDK or higher. Applications targeting earlier SDK versions are allowed to do networking on their main event loop threads, but it’s heavily discouraged. Some examples of other operations that JellyBean, ICS and HoneyComb and won’t allow you to perform on the UI thread are:

    1. Opening a Socket connection (i.e. new Socket()).
    2. HTTP requests (i.e. HTTPClient and HTTPUrlConnection).
    3. Attempting to connect to a remote MySQL database.
    4. Downloading a file (i.e. Downloader.downloadFile()).

    If you are attempting to perform any of these operations on the UI thread, you must wrap them in a worker thread. The easiest way to do this is to use of an AsyncTask, which allows you to perform asynchronous work on your user interface. An AsyncTask will perform the blocking operations in a worker thread and will publish the results on the UI thread, without requiring you to handle threads and/or handlers yourself.