目录
本文导读
需求效果
代码实现
1)Android 为了确保用户流畅的操作体验,一些耗时的任务不能够在 UI 线程中运行,像网络访问/通信就属于这类任务,因此必须新开线程执行这些操作。
2)Android 规定除了 UI 线程外,其它线程都不可以对 UI 控件访问和操控。
3)当后台线程获取到数据(如 UDP 监听到的消息)之后,需要将这些数据显示到 UI 界面上时,这就又涉及到了 Android 的线程间数据传递问题。
4)Android 的许多操作(如网络访问、手机存储访问等),都需要在 主配置文件 AndroidManifest.xml 中先声明权限。
package com.example.administrator.helloworld;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.MediaController;
import android.widget.VideoView;
import com.example.administrator.helloworld.thread.UdpThread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 一个应用程序可以有1个或多个活动,而没有任何限制。
* 每个为应用程序所定义的活动都需要在 AndroidManifest.xml 中声明,应用的主活动的意图过滤器标签中需要包含 MAIN 动作和 LAUNCHER 类别
* 如果 MAIN 动作还是 LAUNCHER 类别没有在活动中声明,那么应用程序的图标将不会出现在主屏幕的应用列表中。
*/
public class MainActivity extends AppCompatActivity {
/**
* android.widget.VideoView:视频播放器控件
* myHandler:用于线程间通信的内部类 Handler
*/
private VideoView videoView;
public MyHandler myHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindViews();
/** 创建 handler 并与 looper 绑定
* 主线程 MainActivity extends AppCompatActivity,AppCompatActivity 的祖上有 ContextWrapper
* android.content.ContextWrapper#getMainLooper() :获取 Looper
* */
myHandler = new MyHandler(MainActivity.this.getMainLooper());
/**
* 创建新线程用于循环监听 UDP 消息
* 虽然如果将 监听UDP的线程 直接放在本类中,操作会简单一些,但是为了更加清晰,推荐新建线程类,
* 所以要将创建好的 Handler 传递到后台的 UDP 线程中去
*/
UdpThread udpThread = new UdpThread(myHandler);
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(udpThread);
}
/**
* 绑定视屏控件
* 绑定之后,后台 UDP 线程监听发来的在线视频地址,然后传回给主线程进行播放
*/
private void bindViews() {
videoView = findViewById(R.id.videoView);
/**
* 为 VideoView 视图设置媒体控制器,设置了之后就会自动由进度条、前进、后退等操作
*/
videoView.setMediaController(new MediaController(this));
/**视频准备完成时回调
* */
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
Log.i("Wmx Logs::", "--------------视频准备完毕,可以进行播放.......");
}
});
/**
* 视频播放完成时回调
*/
videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
/**播放完成时,再次循环播放*/
videoView.start();
Log.i("Wmx Logs::", "--------------视频播放完成,再次进行播放.......");
}
});
/**
* 视频播放发送错误时回调
*/
videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Log.i("Wmx Logs::", "--------------视频播放发送错误.......");
return false;
}
});
}
/**
* 自定义 android.os.Handler,用于接收后台线程传递过来给主线程的数据
* Handler 是 Android 中专门用来处理线程间传递数据的工具
*/
public class MyHandler extends Handler {
/**
* android.os.Looper 主要功能是为特定单一线程运行一个消息环,一个线程对应一个 Looper,同样一个 looper 对应一个线程。
* 一个线程创建时本身是没有自己的 looper (只有主线程有),因此需要手动创建 Looper ,然后将 Looper 与线程相关联,
* 操作方式:在需要关联的 looper 的线程中调用 Looper.prepare(预备),之后再调用 Looper.loop(循环) 启动 looper,如下所示:
*
* class LooperThread extends Thread {
* public Handler mHandler;
*
* public void run() {
* Looper.prepare();
*
* mHandler = new Handler() {
* public void handleMessage(Message msg) {
* // process incoming messages here
* }
* };
*
* Looper.loop();
* }
* }
* 将 looper 与线程关联的时候,looper 会同时生产一个 messageQueue(消息队列),looper 会不停的从 messageQueue 中取出消息(Message),
* 然后线程就可以根据 Message 中的内容进行相应的操作。
* 在创建 Handler 的时候,需要与特定的 looper 绑定,这样通过 handler 就可以把 message 传递给特定的 looper,继而传递给特定的线程。
* 线程---Looper---Handler:一个 looper 可以对应多个 handler,而一个 handler 只能对应一个 looper
*
* @param L
*/
public MyHandler(Looper L) {
super(L);
}
/**
* 必须重写这个方法,用于处理 android.os.Message,
* 当 后台线程调用 android.os.Handler#sendMessage(android.os.Message) 方法发送消息后
* 下面的 handleMessage(Message msg) 就会自动触发,然后处理消息
*/
@Override
public void handleMessage(Message message) {
/**
* android.os.Bundle 就是消息中的数据
* android.os.BaseBundle#getString(java.lang.String):取值的 key 不存在时,返回 null
*/
Bundle bundle = message.getData();
String messageText = bundle.getString("messageText");
Log.i("Wmx Logs::", "handleMessage 接收到消息>>>" + messageText);
/**android.widget.VideoView#stopPlayback():停止视频,释放资源
* android.widget.VideoView#setVideoURI(android.net.Uri):重新绑定视频资源
* android.widget.VideoView#start():再次开始播放
* */
if (messageText != null && !"".equals(messageText)) {
videoView.stopPlayback();
videoView.setVideoURI(Uri.parse(messageText));
videoView.start();
}
}
}
}
package com.example.administrator.helloworld.thread;
import android.os.Bundle;
import android.os.Message;
import android.util.Log;
import com.example.administrator.helloworld.MainActivity;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.charset.Charset;
/**
* 后台监听 UDP 消息的子线程
*/
public class UdpThread implements Runnable {
/**
* TAG:日志标签
* messageText:UDP 监听到的消息文本,每次不能超过 1024 字节
* myHandler:用于给主线程发送消息的 Handler
*/
private final String TAG = "Wmx Log::";
private String messageText;
public MainActivity.MyHandler myHandler;
public UdpThread(MainActivity.MyHandler myHandler) {
this.myHandler = myHandler;
}
@Override
public void run() {
Log.i("Wmx Logs:: ", "Udp 新线程开启..........." + Thread.currentThread().getName());
DatagramSocket datagramSocket = null;
/** 数据接收大小设置为 1024 字节,超出部分是接收不到的
*/
byte[] buffer = new byte[1024];
DatagramPacket datagramPacket;
try {
/**
* InetSocketAddress(String hostname, int port):网络套接字地址,同时指定监听的 ip 与 端口
* A valid port value is between 0 and 65535.
* InetSocketAddress(int port):网络套接字地址,指定监听的 端口,ip 默认为移动设备本机 ip
* */
InetSocketAddress socketAddress = new InetSocketAddress(9090);
/**DatagramSocket(SocketAddress bindaddr):根据绑定好的 SocketAddress 创建 UDP 数据包套接字
* DatagramSocket(int port):只指定 监听的端口时,IP 默认为移动设备本机 ip
*/
datagramSocket = new DatagramSocket(socketAddress);
datagramPacket = new DatagramPacket(buffer, buffer.length);
/**循环监听*/
while (true) {
datagramSocket.receive(datagramPacket);
/**读取数据
* 指定使用 UTF-8 编码,对于中文乱码问题,遵循对方发送时使用什么编码,则接收时也使用同样的编码的原则*/
messageText = new String(datagramPacket.getData(), 0, datagramPacket.getLength(), Charset.forName("UTF-8"));
/**
* 可以创建一个新的 Message,但是推荐调用 handler 的 obtainMessage 方法获取 Message,
* 这个方法的作用是从系统的消息池中取出一个 Message,这样就可以避免 Message 创建和销毁带来的资源浪费。
*
*/
Message obtainMessage = myHandler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putString("messageText", messageText);
/**为发送的消息设置数据*/
obtainMessage.setData(bundle);
/**发送消息*/
myHandler.sendMessage(obtainMessage);
Log.i("WMx Logs::", " UDP 监听到消息>>>>>" + messageText + " >>> 线程:" + Thread.currentThread().getName() + ">>为主线程传输完毕...");
}
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (datagramSocket != null) {
if (!datagramSocket.isConnected()) {
datagramSocket.disconnect();
}
if (!datagramSocket.isClosed()) {
datagramSocket.close();
}
/**即使抛异常了,也要再次监听*/
new Thread(new UdpThread(myHandler)).start();
}
}
}
}