MQTT篇二 -- paho客户端与保活

2,565 阅读5分钟

前言

上篇文章说到使用 MqttAndroidClient 创建连接,在特定的开发环境中时常出现mqtt连接“假死”,客户端收不到新消息,也无法收到断线回调信息,无法触发重连的问题。经过大量实测并分析最后发现是由于在低内存的Android环境上,MqttService很容易被系统回收,导致该现象发送。这篇文章我们结合资料来讨论一下这个问题。

Service保活

首先看看官方文档中,对服务为什么会被系统回收的解释及如何才能降低被回收概率的说明。

The Android system stops a service only when memory is low and it must recover system resources for the activity that has user focus.

这一段话解释了Android系统为什么要回收Service:当系统内存紧张,回收Service释放内存,优先保障持有焦点的Activity使用。本质是Google在系统层面保障用户的使用体验。而“保活”Service,则是App厂商在应用层保障用户体验或自身需求(利润)的重要手段。

ps: 对立统一是宇宙的基本规律。

继续看文档

If the service is bound to an activity that has user focus, it's less likely to be killed; if the service is declared to run in the foreground, it's rarely killed.If the system kills your service, it restarts it as soon as resources become available, but this also depends on the value that you return from onStartCommand().

文档中列举了一些提高Service优先级的方法:包括将服务绑定到拥有用户焦点的Activity,将服务设置为前台服务,设置onStartCommand()的返回值。此外,还可以在清单中设置 android:priority的值提高其优先级。

注意这几个词:“less likely to be/不太可能”,“rarely/罕有”;官方在文档中直接告诉了我们防止Service被回收的方式,按照文档编写Service理论上已经可以解决95%的问题了,下面我们来看MqttService的具体实现。

扒一下源码

先看下官方对MqttService的说明注释:

The android service which interfaces with an MQTT client implementation.

The main API of MqttService is intended to pretty much mirror the IMqttAsyncClient with appropriate adjustments for the Android environment.

注释说MqttService是针对Android系统编写的,主要负责管理mqtt的连接、关闭、重连、消息发布/接收等操作。我们来看看连接和重连这两个重要方法的实现。

 /**
   * Connect to the MQTT server specified by a particular client
   */
public void connect(String clientHandle, MqttConnectOptions connectOptions,
      String invocationContext, String activityToken)
      throws MqttSecurityException, MqttException {
	    MqttConnection client = getConnection(clientHandle);
	  	client.connect(connectOptions, null, activityToken);
  }
/**
 * Request all clients to reconnect if appropriate
 */
void reconnect() {
	traceDebug(TAG, "Reconnect to server, client size=" + connections.size());
	for (MqttConnection client : connections.values()) {
		traceDebug("Reconnect Client:",client.getClientId() + '/' + client.getServerURI());
		if(this.isOnline()){
			client.reconnect();
		}
	}
}

可以看到连接和重连方法都是通过委托MqttConnection对象去完成具体操作,而MqttConnection类实现了MqttCallbackExtended接口,该接口继承自MqttCallback,接口方法如下:

public void connectComplete(boolean reconnect, String serverURI);

public void connectionLost(Throwable cause);

public void messageArrived(String topic, MqttMessage message) throws Exception;

public void deliveryComplete(IMqttDeliveryToken token);

通过源码整个调用链清晰的展现在我们面前:

  • 我们通过创建MqttAndroidClient对象来设置mqtt客户端的连接信息,并发起连接;
  • MqttAndroidClient内部实现IMqttAsyncClient接口方法,这些方法的实现交由MqttService完成;
  • 而MqttService通过new MqttConnection对象的方式,将这些方法丢给了MqttConnection类;
  • MqttConnection类,实现了MqttCallback接口,并最终实现了这些方法(连接、关闭、收发等)通过接口回调通知我们连接状态。

至此调用结束,在这一链条中,如果MqttService被系统回收,我们无法收到新消息及连接状态改变消息,无法触发断线重连机制,这样的一个调用模式是存有风险的的。根据Service文档中列举的方法,我们看看MqttService有没有实现保活。

  • 服务绑定到拥有用户焦点的Activity
if (mqttService == null) { // First time - must bind to the service
        Intent serviceStartIntent = new Intent();
        serviceStartIntent.setClassName(myContext, SERVICE_NAME);
        Object service = myContext.startService(serviceStartIntent);
        if (service == null) {
            IMqttActionListener listener = token.getActionCallback();
            if (listener != null) {
                listener.onFailure(token, new RuntimeException(
                        "cannot startservice" + SERVICE_NAME));
                }
            }

            // We bind with BIND_SERVICE_FLAG (0), leaving us the manage the lifecycle
            // until the last time it is stopped by a call to stopService()
            myContext.bindService(serviceStartIntent, serviceConnection,
                    Context.BIND_AUTO_CREATE);
        if (!receiverRegistered) 
            registerReceiver(this);
        } else {
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    doConnect();
                    //Register receiver to show shoulder tap.
                    if (!receiverRegistered) registerReceiver(MqttAndroidClient.this);
                }
        });
    }

在MqttAndroidClient的connect方法中,通过传入context,使用startService()、bindService()的方法开启服务。

  • 设置前台服务、设置onStartCommand()的返回值
 @Override
  public int onStartCommand(final Intent intent, int flags, final int startId) {
    // run till explicitly stopped, restart when
    // process restarted
	registerBroadcastReceivers();

    return START_STICKY;
  }
@SuppressWarnings("deprecation")
    private void registerBroadcastReceivers() {
        if (networkConnectionMonitor == null) {
            networkConnectionMonitor = new MqttService.NetworkConnectionIntentReceiver();
            registerReceiver(networkConnectionMonitor, new IntentFilter(
                    ConnectivityManager.CONNECTIVITY_ACTION));
        }

        if (Build.VERSION.SDK_INT < 14 /**Build.VERSION_CODES.ICE_CREAM_SANDWICH**/) {
            // Support the old system for background data preferences
            ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
            backgroundDataEnabled = cm.getBackgroundDataSetting();
            if (backgroundDataPreferenceMonitor == null) {
                backgroundDataPreferenceMonitor = new MqttService.BackgroundDataPreferenceReceiver();
                registerReceiver(
                        backgroundDataPreferenceMonitor,
                        new IntentFilter(
                                ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED));
            }
        }
    }

在onStartCommand方法中,动态注册了广播,并设置返回值为START_STICKY。这里广播的作用是用来监测网络状态改变,目的是为了实现断网情况下的重连。

设置返回值为START_STICKY,告诉系统在内存足够时重新创建Service,再次调用onStartCommand方法。但是MqttService并未在方法中应对这种情况,即便服务恢复,也无法接收到新消息。另外MqttService没有设置为前台进程,达不到Google官方文档中说的“it's rarely killed”。可以看出,如果出现Service被回收的情况,paho Android客户端无法为我们提供解决办法。

结尾

最后草草收尾一下吧,在Android中使用MQTT进行消息推送,网上资料很多,大多数都是推荐使用paho客户端,其中许多评论下都有提到无法收到消息,无法重连的问题。我写这两篇文章也是记录一下自己如何使用的,以及遇到的问题。另外GitHub上还有其他高星的mqtt客户端也可以多样选择。本来还想介绍其他两个客户端的,最后还是作罢,大同小异看着文档就能上手了。最后的最后,还是欢迎大家多多交流,分享踩坑经历,互相进步。