在Android中实现推送方式的底层原理与推送的知识及相关解决方案

jopen 9年前

最近一个月一直在考虑实现一种让Android开发者一个人就能完成的推送功能库。因为现有的推送功能,全部都需要服务器端配合,不断测试,即使使用第三方库也需要很长一段时间的测试。这里就是我最近研究的一个小小的成果:http://git.oschina.net/kymjs/KJPush

推送功能在Android应用开发中已经非常普遍了,本文就是来探讨下Android中推送的底层原理与实现推送功能的一些解决方案。

1、什么是推送?

     当我们开发需要和服务器交互的应用程序时,基本上都需要获取服务器端的数据,比如开源中国客户端,在有人评论或回复你的时候,客户端需要知道,并作出相应处理。要获取服务器上的信息,有两种方法:第一种是客户端使用Pull(拉)的方式,就是隔一段时间就去服务器上获取一下信息,看是否有更新的信息出现。第二种就是服务器使用Push(推送)的方式,当服务器端有新信息了,则把最新的信息Push到客户端上。这样,客户端就能自动的接收到消息。

      Push是服务端主动发消息给客户端,现在有很多第三方推送框架:例如百度推送、极光推送、个推等等,都是基于之前说的第二种方式也就是服务器使用Push的方式。因为第一时间知道数据发生变化的是服务器自己,所以Push的优势是实时性高。但服务器主动推送需要单独开发一套能让客户端持久连接的服务端程序。但有些情况下并不需要服务端主动推送,而是在一定的时间间隔内客户端主动发起查询,这种时候就应该使用Pull的方式去获取。很多人认为Push方式没有任何消耗,其实不然采用Push方式需要长时间维持一条客户端与服务器端通信的socket长连接,依旧是很费流量与电量。如果轮询策略配置的好,消耗的电与数据流量绝不比维持一个socket连接使用的多。譬如有这样一个app,实时性要求不高,每天只要能获取10次最新数据就能满足要求了,这种情况显然轮询更适合一些,推送显得太浪费,而且更耗电。

2、如何实现轮询请求

第一种方式是在一个Service中创建一个计时器,如下代码是在网上找的一段类似实现(节选)

/**   * 短信推送服务类,在后台长期运行,每个一段时间就向服务器发送一次请求   * @author jerry   */  public class PushSmsService extends Service {            @Override      public void onCreate() {          this.client = new AsyncHttpClient();          this.myThread = new MyThread();          this.myThread.start();          super.onCreate();      }            private class MyThread extends Thread {          @Override          public void run() {              String url = "你请求的网络地址";              while (flag) {                  // 每个10秒向服务器发送一次请求                  Thread.sleep(10000);                  // 采用get方式向服务器发送请求                  client.get(url, new AsyncHttpResponseHandler() {                      @Override                      public void onSuccess(int statusCode, Header[] headers,                              byte[] responseBody) {                          try {                              JSONObject result = new JSONObject(new String(                                      responseBody, "utf-8"));                              int state = result.getInt("state");                              // 假设偶数为未读消息                              if (state % 2 == 0) {                                  String content = result.getString("content");                                  String date = result.getString("date");                                  String number = result.getString("number");                                  notification(content, number, date);                              }                          } catch (Exception e) {                              e.printStackTrace();                          }                      }  }

但是用Sleep,TimerTask,都会增大Service被系统回收的可能,更合适的方法是使用AlarmManager这个系统的计时器去管理。

实现方法如下,你可以在这里看到完整的实现方式

private void startRequestAlarm() {          cancelRequestAlarm();          // 从1秒后开始,每隔2分钟执行getOperationIntent()          // 注意,这个2分钟只是正常情况下的2分钟,实际情况可能不同系统的处理策略而被延长,比如坑爹的粗粮系统上可能被延长至5分钟          mAlarmMgr.setRepeating(AlarmManager.RTC_WAKEUP,                  System.currentTimeMillis() + 1000, KJPushConfig.PALPITATE_TIME,                  getOperationIntent());      }        /**       * 即使启动PendingIntent的原进程结束了的话,PendingIntent本身仍然还存在,可在其他进程(       * PendingIntent被递交到的其他程序)中继续使用.       * 如果我在从系统中提取一个PendingIntent的,而系统中有一个和你描述的PendingIntent对等的PendingInent,       * 那么系统会直接返回和该PendingIntent其实是同一token的PendingIntent,       * 而不是一个新的token和PendingIntent。然而你在从提取PendingIntent时,通过FLAG_CANCEL_CURRENT参数,       * 让这个老PendingIntent的先cancel()掉,这样得到的pendingInten和其token的就是新的了。       */      private void cancelRequestAlarm() {          mAlarmMgr.cancel(getOperationIntent());      }        /**       * 采用轮询方式实现消息推送<br>       * 每次被调用都去执行一次{@link #PushReceiver}onReceive()方法       *        * @return       */      private PendingIntent getOperationIntent() {          Intent intent = new Intent(this, PushReceiver.class);          intent.setAction(KJPushConfig.ACTION_PULL_ALARM);          PendingIntent operation = PendingIntent.getBroadcast(this, 0, intent,                  PendingIntent.FLAG_UPDATE_CURRENT);          return operation;      }

这样就可以在最大程度上解决因为自己实现计时器造成的计时不准确或计时器被系统回收的问题。

但是仅仅这样还没办法实现一个完善且稳定的轮询推送库,做推送最大的问题有三个:电量消耗,数据流量消耗,服务持久化。

3、电量消耗优化与数据流量消耗优化:

这两个问题其实可以合并成一个问题,因为请求服务器其实也是一个费电的事情。与维持一个长连接类似,要实现推送功能,不管是维持一个长连接或者是定时请求服务器都需要耗费网络数据流量,而只不过长连接是一个细水长流不断耗费,而轮询是一次一大断数据的耗费。这样就需要一种可行的策略去配置,让轮询按照我们想要的方式去执行。目前我采用的思路是当手机处于GPRS模式时降低轮询的频率,每5分钟请求一次服务器,当手机处于WiFi模式时每2分钟请求一次服务器,同时设置如果熄灭屏幕则停止推送请求,当屏幕熄灭20秒后杀死推送进程,这样不仅不需要考虑维护一个进程的消耗同时也节省了数据流量的使用。

4、服务持久化

相信这是一个很多人都遇到的问题,网上也有很多类似的问题,像QQ微信这种应用做的就非常好,不管使用第三方手机助手或者使用系统停止一个应用(不是设置里面的那种停止,是长按Home键的那种),后台Service都不会被回收。很可惜,我目前只能做到保证一个Service不被第三方手机助手回收,可以防止部分手机长按Home键停止,但是例如粗粮的MIUI系统,依旧会杀死我的Service且无法恢复。目前为止我依旧没有找到一个公开的完美解决办法,如果你知道如何解决,请不吝指教。下面我就简单讲讲如何最大程度的维护一个Service。

以前在做音乐播放器的时候,相信很多人都遇到了,在应用开启过多的时候,后台播放音乐的Service独立进程会被系统杀死。

在Android的ActivityManager中有一个内部类RunningAppProcessInfo,用来记录当前系统中进程的状态,如下是其中的一些值:

       /**           * Constant for {@link #importance}: this is a persistent process.           * Only used when reporting to process observers.           * @hide           */          public static final int IMPORTANCE_PERSISTENT = 50;            /**           * Constant for {@link #importance}: this process is running the           * foreground UI.           */          public static final int IMPORTANCE_FOREGROUND = 100;                    /**           * Constant for {@link #importance}: this process is running something           * that is actively visible to the user, though not in the immediate           * foreground.           */          public static final int IMPORTANCE_VISIBLE = 200;                    /**           * Constant for {@link #importance}: this process is running something           * that is considered to be actively perceptible to the user.  An           * example would be an application performing background music playback.           */          public static final int IMPORTANCE_PERCEPTIBLE = 130;                    /**           * Constant for {@link #importance}: this process is running an           * application that can not save its state, and thus can't be killed           * while in the background.           * @hide           */          public static final int IMPORTANCE_CANT_SAVE_STATE = 170;                    /**           * Constant for {@link #importance}: this process is contains services           * that should remain running.           */          public static final int IMPORTANCE_SERVICE = 300;                    /**           * Constant for {@link #importance}: this process process contains           * background code that is expendable.           */          public static final int IMPORTANCE_BACKGROUND = 400;                    /**           * Constant for {@link #importance}: this process is empty of any           * actively running code.           */          public static final int IMPORTANCE_EMPTY = 500;

一般数值大于RunningAppProcessInfo.IMPORTANCE_SERVICE的进程都长时间没用或者空进程了

一般数值大于RunningAppProcessInfo.IMPORTANCE_VISIBLE的进程都是非可见进程,也就是在后台运行着

第三方清理软件清理的一般是大于IMPORTANCE_VISIBLE的值,所以要想不被杀死就需要将自己的进程降低到IMPORTANCE_VISIBLE以下,也就是可见进程的程度。在每一个Service中有一个方法叫startForeground,也就是以可见进程的模式启动,这里是在SDK源码中的实现与注释,可以看到,它会在通知栏持续显示一个通知,但只需要将id传为0即可避免通知的显示。当然要取消这种可见进程等级的设置只需要调用stopForgeround即可。

/**       * Make this service run in the foreground, supplying the ongoing       * notification to be shown to the user while in this state.       * By default services are background, meaning that if the system needs to       * kill them to reclaim more memory (such as to display a large page in a       * web browser), they can be killed without too much harm.  You can set this       * flag if killing your service would be disruptive to the user, such as       * if your service is performing background music playback, so the user       * would notice if their music stopped playing.       */      public final void startForeground(int id, Notification notification) {          try {              mActivityManager.setServiceForeground(                      new ComponentName(this, mClassName), mToken, id,                      notification, true);          } catch (RemoteException ex) {          }      }

这里由于篇幅有限就讲这么多了,希望详细了解进程优先级提升的可以看看ActivityManager源码中的定义以及KJPush中的实现方式。

来自:http://my.oschina.net/kymjs/blog/367325