Android:使用Service处理息屏后的WebSocket的服务端推送消息并传递给前端

发布于:2024-12-18 ⋅ 阅读:(82) ⋅ 点赞:(0)

前言

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