前言
之前我们在使 RESTful 访问服务端时,一般都是客户端请求服务端应答的方式,这种通讯方式,对于需要持续获取数据的情形都是采用轮询的方式,但是这种方式对两边的性能消耗很大,特别是服务端的压力很大。现在当我们使用WebSocket时,这类问题迎刃而解了。服务端可以根据需要向客户端实时推送消息。
WebSocket的协议很简单,客户端的onMessage事件可以很方便的接收消息。我们可以收到后传递给前端的Activity处理更新UI.
现代手机为了省电,屏幕在很短的时候就关闭了,但有时候我们不希望屏幕一灭,应用也跟着睡着了,手机息屏后,我们仍然希望用户能通过语音给用户传递信息。就像你切换屏幕了,高德能继续给你语音导航一样。这种情况单独使用Activity是无能为力的,Android系统设计为只要屏幕一灭,任何Activity的UI活动都会睡眠。不过,息屏后,后台的WebSocket客户端是能够继续接受消息的,但是没办法传到前端的活动了。这时候我们需要引入Android的Service来处理。
在讲解正文之前我们先回顾一下WebSocket的基本使用。
WebSocket与回调接口
在之前的文章中我们介绍了WebSocket的基本使用。就是利用OkHttp3的WebSocket相关功能搭建自己的调用程序框架。这里为了适应Service的使用,我们对WebSocket程序稍微加以改造一下。
首先我们将静态方法改造为实例方法,并使用单例模式。
其次,我们引入一个回调接口,这个很关键。
WebSocketListenerCallback
public interface WebSocketListenerCallback {
void onDataReceived(String action, String data);
void onFailure(String text);
}
它包含2个方法,
onDataReceived 处理收到消息后怎么办
onFailure 处理通讯失败
我们的WebSocket程序设计为可以设定多个Callback, 在onMessage 收到消息后,逐个执行每个Callback的onDataReceived方法。整个WebSocket程序的代码如下。使用前需要引入Okhttp3库。
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bob.app.C;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
public class WSClient {
private static WSClient instance;
private WebSocket socket;
private final List<WebSocketListenerCallback> callbacks = new ArrayList<>();
private final OkHttpClient client;
private WSClient() {
OkHttpClient.Builder cb = new OkHttpClient.Builder();
cb.readTimeout(C.SOCKET_TIMEOUT, TimeUnit.SECONDS);
cb.connectTimeout(C.SOCKET_TIMEOUT, TimeUnit.SECONDS);
cb.writeTimeout(C.SOCKET_TIMEOUT, TimeUnit.SECONDS);
client = cb.build();
}
public static synchronized WSClient getInstance() {
if (instance == null) {
instance = new WSClient();
}
return instance;
}
public synchronized void init() {
Request request = new Request.Builder().url(C.serverUrl).build();
WSListener listener = new WSListener();
socket = client.newWebSocket(request, listener);
}
public void addCallback(WebSocketListenerCallback callback) {
if (!callbacks.contains(callback)) {
callbacks.add(callback);
}
}
public void removeCallback(WebSocketListenerCallback callback) {
callbacks.remove(callback);
}
public synchronized void send(String action, Object data) {
long txNo = System.currentTimeMillis();
String message = action + "$$" + txNo + "$$" + U.toJSONString(data);
if (socket != null) {
socket.send(message);
}
}
public void disconnect(int code, String reason) {
if (socket != null) {
socket.close(code, reason);
socket = null;
}
}
private class WSListener extends WebSocketListener {
@Override
public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
for (WebSocketListenerCallback callback : callbacks) {
String[] str = text.split("\$\$");
String action = str[0];//操作指令,
String message = str.length > 2 ? str[2] : "";//信息正文
callback.onDataReceived(action, message);
}
}
@Override
public void onFailure(@NonNull WebSocket webSocket, @NonNull Throwable t, @Nullable Response response) {
for (WebSocketListenerCallback callback : callbacks) {
callback.onFailure("Error: " + t.getMessage());//错误信息传出去
}
}
}
}
C是一个常数类,里面有些常数,比如SOCKET_TIMEOUT = 5;
Service接收推送消息
关于Service的基本使用,网上有很多资料。我们这里通过实际的场景应用来消化理解。
我们现在的应用场景就是:收到后台消息后,通过Service传递给前端应用,然后语音播报。
首先自己的Service继承基类,并且需要实现回调方法WebSocketListenerCallback
在初始化WebSocket的时候我们就把Callback传进去。并初始化。
整个Service的完整代码如下:
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.bob.app.common.WSClient;
import com.bob.app.common.WebSocketListenerCallback;
public class ActiveDataService extends Service implements WebSocketListenerCallback {
@Override
public void onCreate() {
super.onCreate();
WSClient wsClient = WSClient.getInstance();//获取实例
wsClient.addCallback(this);//传入Callback
wsClient.init();//执行初始化
}
public void onDataReceived(String action, String str) {
if (A.ACTIVE_DATA.equals(action)) {//如果指令是推送新消息
Intent intent = new Intent(BC.BROADCAST_ACTIVE_DATA);//收到最新实时消息
intent.putExtra("data", str);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
}
public void onFailure(String str) {
Intent intent = new Intent(BC.BROADCAST_SERVER_OFFLINE);//服务端访问异常
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(1, createNotification());
return START_STICKY;
}
private Notification createNotification() {
NotificationChannel channel = new NotificationChannel("channel", "Service", NotificationManager.IMPORTANCE_LOW);
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {
manager.createNotificationChannel(channel);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "channel")
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.fetching_data))
.setSmallIcon(android.R.drawable.ic_notification_overlay)
.setAutoCancel(true)
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_DEFAULT); // Make sure the priority is LOW
return builder.build();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
其中BC.BROADCAST_ACTIVE_DATA和BC.BROADCAST_SERVER_OFFLINE是2个常数字符串,可以自己取名字。
在Android里面,由于Activity和Service的生命周期并不相同,Service息屏后还能活动,因此Service并不能直接引用Activity也不能直接与Activity交互,只能通过广播间接交互。意思就是Service发送广播,Activity可以订阅后处理。上面的代码展示了怎样发送广播。
createNotification是一个常规方法,创建一个通知信息栏。这个会让用户知道一个后台服务在干嘛。有的手机会显示比如华为等,有的手机也默认不显示,比如Oppo等
Activity接收Service广播信息
Service发送的广播,Activity可以通过订阅的方式来使用。
具体的订阅方法如下:
private final BroadcastReceiver receiver1 = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String data = intent.getStringExtra("data");
finishGetActiveData(data);//接收到最新数据,传递到处理方法
}
};
private final BroadcastReceiver receiver2 = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
showLogin();//接收到网络异常,退回到登录界面
}
};
先声明BroadcastReceiver, receiver的目的就是声明收到数据后怎么办。在onReceive方法里接收数据,然后传递到实现方法里面。这里我们声明了2个receiver,一个是收到正常的最新信息后怎么办。另一个是收到网络错误信息后怎么办。
然后在onCreate方法里注册receiver
Intent serviceIntent = new Intent(this, ActiveDataService.class);
ContextCompat.startForegroundService(this, serviceIntent);
LocalBroadcastManager.getInstance(this).registerReceiver(receiver1, new IntentFilter(BC.BROADCAST_ACTIVE_DATA));
LocalBroadcastManager.getInstance(this).registerReceiver(receiver2, new IntentFilter(BC.BROADCAST_SERVER_OFFLINE));
注意其中的 IntentFilter的用法,一般一个receiver只过滤接收一种消息。
这样我们就完成把消息传送给Activity这个数据链路了。
然后我们可以在finishGetActiveData方法里面干该干的事情了,比如调用TTS语音播报啥的。
看到这里,有的小伙伴可能会问,这个Callback我能不能在Activity里将WebSocket初始化,然后传入Callback呢,答案是可以的,可以将Activity实现WebSocketListenerCallback后传入WebSocket,也是一样的效果,还更简单,不用广播。不过前面我们讲过,这种直接使用Activity初始化WebSocket的程序只能在亮屏时功能正常,息屏了程序就跟着睡眠了。
另外,需要强调的一点时,要实现息屏后服务正常,需要手机在设置里关闭省电功能,并且允许应用后台活动。否则写了也白搭。只有用户才有最终决定权。
总结
要想息屏后,部分手机功能正常接收后台推送消息并传递给Activity处理,需要做如下处理:
- WebSocket引入回调接口并在onMesssge中调用。
- 创建一个Service实现回调接口,在回调方法中发送广播
- Activity中声明接收广播的receiver,并实现处理receiver收到的的数据。