前言
之前我们在使 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收到的的数据。