Android 轮询操作优化

01.轮询操作是啥

  • 什么叫轮训请求?
    • 简单理解就是App端每隔一定的时间重复请求的操作就叫做轮训请求,比如:App端每隔一段时间上报一次定位信息,App端每隔一段时间拉去一次用户状态等,这些应该都是轮训请求。
  • 为何不用长连接代替轮训操作?
    • 长连接并不是稳定的可靠的,而执行轮训操作的时候一般都是要稳定的网络请求,而且轮训操作一般都是有生命周期的,即在一定的生命周期内执行轮训操作,而长连接一般都是整个进程生命周期的,所以从这方面讲也不太适合。

02.轮训请求实践

2.1 与长连接相关的轮训请求

  • 具体的代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    /**
    * 轮询操作,测试实际开发中轮询操作逻辑。实际开发中比如抽奖活动,30秒一次轮询请求接口更新页面
    */
    public class LoopRequestService extends Service {
    /**
    * 是否已经绑定了推送设备id
    */
    private volatile isBindDevIdEnum isBindDevId = isBindDevIdEnum.NONE;
    public enum isBindDevIdEnum {
    NONE, RUN, SUCCESS
    }

    public static final String ACTION = "LoopService";
    /**
    * 客户端执行轮询的时间间隔,该值由StartQueryInterface接口返回,默认设置为30s
    */
    public static int LOOP_INTERVAL_SECS = 5;
    /**
    * 当前服务是否正在执行
    */
    public static boolean isServiceRunning = false;
    /**
    * 定时任务工具类
    */
    public static Timer timer = new Timer();

    private Context context;

    public LoopRequestService() {
    isServiceRunning = false;
    }

    //-------------------------------使用闹钟执行轮询服务------------------------------------

    /**
    * 启动轮询服务
    */
    public static void startLoopService(Context context) {
    if (context == null){
    return;
    }
    quitLoopService(context);
    LogUtils.i("LoopService开启轮询服务,轮询间隔:" + LOOP_INTERVAL_SECS + "s");
    AlarmManager manager = (AlarmManager) context.getApplicationContext()
    .getSystemService(Context.ALARM_SERVICE);
    //开启服务service
    Intent intent = new Intent(context.getApplicationContext(), LoopRequestService.class);
    intent.setAction(LoopRequestService.ACTION);
    PendingIntent pendingIntent = PendingIntent.getService(context.getApplicationContext(),
    1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    /*
    * 闹钟的第一次执行时间,以毫秒为单位,可以自定义时间,不过一般使用当前时间。需要注意的是,本属性与第一个属性(type)密切相关,
    * 如果第一个参数对应的闹钟使用的是相对时间(ELAPSED_REALTIME和ELAPSED_REALTIME_WAKEUP),那么本属性就得使用相对时间(相对于系统启动时间来说),
    * 比如当前时间就表示为:SystemClock.elapsedRealtime();
    * 如果第一个参数对应的闹钟使用的是绝对时间(RTC、RTC_WAKEUP、POWER_OFF_WAKEUP),那么本属性就得使用绝对时间,
    * 比如当前时间就表示为:System.currentTimeMillis()。
    */
    if (manager != null) {
    manager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
    LOOP_INTERVAL_SECS * 1000, pendingIntent);
    }
    }

    /**
    * 停止轮询服务
    */
    public static void quitLoopService(Context context) {
    if (context == null){
    return;
    }
    LogUtils.i("LoopService关闭轮询闹钟服务...");
    AlarmManager manager = (AlarmManager) context.getApplicationContext()
    .getSystemService(Context.ALARM_SERVICE);
    Intent intent = new Intent(context.getApplicationContext(), LoopRequestService.class);
    intent.setAction(LoopRequestService.ACTION);
    PendingIntent pendingIntent = PendingIntent.getService(context.getApplicationContext(),
    1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    if (manager != null) {
    manager.cancel(pendingIntent);
    }
    // 关闭轮询服务
    LogUtils.i("LoopService关闭轮询服务...");
    context.stopService(intent);
    }

    @Override
    public void onCreate() {
    super.onCreate();
    context = getApplicationContext();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
    LogUtils.i("LoopService开始执行轮询服务... \n 判断当前用户是否已登录...");
    // 若当前网络不正常或者是用户未登陆,则不再跳转
    if (NetworkUtils.isConnected()) {
    // 判断当前长连接状态,若长连接正常,则关闭轮询服务
    LogUtils.i("LoopService当前用户已登录... \n 判断长连接是否已经连接...");
    if (isBindDevId != null && isBindDevId == isBindDevIdEnum.SUCCESS) {
    LogUtils.i("LoopService已经绑定id成功,退出轮询服务...");
    quitLoopService(context);
    } else {
    if (isServiceRunning) {
    return START_STICKY;
    }
    // 启动轮询拉取消息
    startLoop();
    }
    } else {
    LogUtils.i("LoopService没有网络时,关闭轮询服务...");
    quitLoopService(context);
    }
    return START_STICKY;
    }

    @Override
    public void onDestroy() {
    super.onDestroy();
    LogUtils.i("LoopService轮询服务退出,执行onDestroy()方法,inServiceRunning赋值false");
    isServiceRunning = false;
    timer.cancel();
    timer = new Timer();
    }

    @Override
    public IBinder onBind(Intent intent) {
    throw new UnsupportedOperationException("Not yet implemented");
    }

    /**
    * 启动轮询拉去消息
    */
    private int count = 0;
    private void startLoop() {
    if (timer == null) {
    timer = new Timer();
    }
    timer.schedule(new TimerTask() {
    @Override
    public void run() {
    isServiceRunning = true;
    LogUtils.i("LoopService执行轮询操作...轮询服务中请求getInstance接口...");
    //开始执行轮询的网络请求操作sendLoopRequest();
    //这里只是假设数据操作
    if (count==5){
    isBindDevId = isBindDevIdEnum.SUCCESS;
    }
    count++;
    }
    }, 0, LOOP_INTERVAL_SECS * 1000);
    }
    }

2.2 与页面相关的轮训请求

2.2.1 使用TimerTask实现轮询
  • 在当前Activity页面有一个定时的拉去订单信息的轮训请求,下面具体看一下这个定时请求的执行逻辑:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * TimerTask对象,主要用于定时拉去服务器信息
    */
    public class Task extends TimerTask {
    @Override
    public void run() {
    L.i("开始执行执行timer定时任务...");
    handler.post(new Runnable() {
    @Override
    public void run() {
    isFirstGetData = false;
    getData(true);
    }
    });
    }
    }
  • 当用户退出这个页面的时候需要清除这里的轮训操作。所以在Activity的onDesctory方法中执行了清除timerTask的操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Override
    public void onDestroy() {
    super.onDestroy();
    if (timer != null) {
    timer.cancel();
    timer = null;
    }
    if (timerTask != null) {
    timerTask.cancel();
    timerTask = null;
    }
    }
  • 这样当用户打开这个页面的时候初始化TimerTask对象,每个一分钟请求一次服务器拉取订单信息并更新UI,当用户离开页面的时候清除TimerTask对象,即取消轮训请求操作。可以发现上面我们看到的与长连接和页面相关的轮训请求服务都是通过timer对象的定时任务实现的轮训请求服务。

2.2.2 通过Handler对象实现轮训请求
  • 下面我们来看一个通过Handler异步消息实现的轮训请求服务。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    /**
    * 默认的时间间隔:1分钟
    */
    private static int DEFAULT_INTERVAL = 60 * 1000;
    /**
    * 异常情况下的轮询时间间隔:5秒
    */
    private static int ERROR_INTERVAL = 5 * 1000;
    /**
    * 当前轮询执行的时间间隔
    */
    private static int interval = DEFAULT_INTERVAL;
    /**
    * 轮询Handler的消息类型
    */
    private static int LOOP_WHAT = 10;
    /**
    * 是否是第一次拉取数据
    */
    private boolean isFirstRequest = false;
    /**
    * 第一次请求数据是否成功
    */
    private boolean isFirstRequestSuccess = false;

    /**
    * 开始执行轮询,正常情况下,每隔1分钟轮询拉取一次最新数据
    * 在onStart时开启轮询
    */
    private void startLoop() {
    L.i("页面onStart,需要开启轮询");
    loopRequestHandler.sendEmptyMessageDelayed(LOOP_WHAT, interval);
    }

    /**
    * 关闭轮询,在界面onStop时,停止轮询操作
    */
    private void stopLoop() {
    L.i("页面已onStop,需要停止轮询");
    loopRequestHandler.removeMessages(LOOP_WHAT);
    }

    /**
    * 处理轮询的Handler
    */
    private Handler loopRequestHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
    // 如果首次请求失败,
    if (!isFirstRequestSuccess) {
    L.i("首次请求失败,需要将轮询时间设置为:" + ERROR_INTERVAL);
    interval = ERROR_INTERVAL;
    } else {
    interval = DEFAULT_INTERVAL;
    }

    L.i("轮询中-----当前轮询间隔:" + interval);

    loopRequestHandler.removeMessages(LOOP_WHAT);

    // 首次请求为成功、或者定位未成功时执行重定位,并加载网点数据
    if (!isFirstRequestSuccess || !Config.locationIsSuccess()) {
    isClickLocationButton = false;
    doLocationOption();
    } else {
    loadData();
    }
    System.gc();
    loopRequestHandler.sendEmptyMessageDelayed(LOOP_WHAT, interval);
    }
    };
  • 这里是通过Handler实现的轮训操作,其核心原理就是在handler的handlerMessage方法中,接收到消息之后再次发送延迟消息,这里的延迟时间就是我们定义的轮训间隔时间,这样当我们下次接收到消息的时候又一次发送延迟消息,从而造成我们时时发送轮训消息的情景。