文档章节

自己动手做推送

kymjs张涛
 kymjs张涛
发布于 2015/01/13 19:34
字数 2471
阅读 7555
收藏 312

最近一个月一直在考虑实现一种让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中的实现方式。

© 著作权归作者所有

kymjs张涛

kymjs张涛

粉丝 512
博文 64
码字总数 76485
作品 4
普陀
Android工程师
私信 提问
加载中

评论(34)

eric_huang
eric_huang
张哥!被第三方助手杀掉的问题是如何解决的,能否给个链接
huof
huof
学习!!
扑街
最新demo不会定时调用onPullData接口啊
肖滔
肖滔

引用来自“小小程序员”的评论

不是这么理解的,我记得前年公司开发APP,首先想到的是自己写推送,特么无耻的是,那么所谓的手机卫士、手机管家把你的拦截掉了;只好用第三方比如百度推送之类的

用第三方的推送就不会把杀掉吗?
大漠真人
大漠真人
h
haibinzero
快快开发吧,等这个PROXY强大了自然不会被杀掉
h
haibinzero
安卓端开发一个包 让其他应用程序集成进去,在手机端统一各应用的PUSH请求,当然为了保证PUSH的用户数据安全,这个服务只返回有更新和无更新并不会返回推送的信息,应用获取更新标识后再去自己的服务器上获取数据
h
haibinzero
你应该叫OS搭建一个ServerProxy目标是让所有的应用都去刷这个服务器,这个服务器制定一个PUSH的协议,帮助你连接到你的服务器,然后采用非常小的协议来返回给你推送的情况,这样大家做的应用都去请求它
kymjs张涛
kymjs张涛 博主

引用来自“一哭一哭”的评论

引用来自“张涛OSC”的评论

引用来自“一哭一哭”的评论

请问一下哥们你解决了三方杀死服务的问题了吗

第三方的问题解决了,测试过手机管家和360的卫士

小米一键清理呢
没有试过
Android极光推送之前台弹出对话框

Android开发中,经常遇到推送信息,笔者最近也遇到开发中当应用在前台运行时,需要弹出对话框,后台运行需要在通知栏里显示:由于之前没有做过相关的开发,并且在百度上也没有找到好的例子,...

qq_35703234
2017/08/16
0
0
PHP实现帝联CDN加速推送URL

由于公司的网站用的PHP写的,而帝联提供的API是java写的,为了实现后台更新首页时自动推送首页URL到帝联的CDN服务器更新缓存,自己动手写个PHP的API <?php function sendURL($pushurldata,$...

蜗牛奔跑
2016/03/04
53
0
用wordpress撸了一个笑话采集站,从内容到推广没写过一行代码

上个月接触wordpress以来,发现php真的是很屌丝(适合草根站长快速建站), 一直想弄一个采集站,也自己动手写过nodejs爬虫,有机会开源出来,但是采集什么内容一直是一个头疼的问题,想来想...

蓝猫163
2016/10/08
3.7K
10
OSChina 技术周刊第十八期 —— 2015 年 OSC 源创会行程计划

每周技术抢先看,总有你想要的! 移动开发 【软件】开源 Android ORM 框架 OpenDroid 【博客】自己动手做推送 前端开发 【软件】jQuery 全屏滚动插件 fullPage.js 服务端开发/管理 【软件】W...

OSC编辑部
2015/01/18
1K
0
Git详细教程(二)

Git —— 目前世界上最先进的分布式版本控制系统,高端大气上档次! 上一篇:Git详细教程(一) 三、远程仓库 Git是分布式版本控制系统,同一个Git仓库,可以分布到不同的机器上。怎么分布呢...

my_杨哥
2017/12/20
0
0

没有更多内容

加载失败,请刷新页面

加载更多

【2019年8月版本】OCP 071认证考试最新版本的考试原题-第9题

Choose three Which three statements are true about views in an Orade batabase? A) A SELECT statement cannot contain a where clause when querying a view contaning a WHERE clause ......

oschina_5359
13分钟前
1
0
[JSON].connectionValue()

本文转载于:专业的前端网站➭[JSON].connectionValue() 语法: [JSON].connectionValue() 说明: 将对象的所有键值接连成新的字符串值 返回: [String] 示例: Set a = toJson()c = Array(1,2,...

前端老手
15分钟前
2
0
云计算给大数据分析工具带来了什么

如果大数据是一块蛋糕,那么大数据分析工具就是切蛋糕的刀叉。人们都期待着能用“刀叉”从大数据中挖出自己想要的“价值”,因此大数据分析工具被人们寄予厚望。而云计算技术的兴起似乎又给大...

青果云小潘
16分钟前
1
0
centOS7下es的使用

安装启动es7.4.0 docker pull docker.elastic.co/elasticsearch/elasticsearch:7.4.0docker run -d -p 9200:9200 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elast......

无畏的老巨人
24分钟前
1
0
iptables删除命令中的相关问题

最近在做一个中间件的配置工作,在配置iptables的时候,当用户想删除EIP(即释放当前连接),发现使用iptables的相关命令会提示错误。iptables: Bad rule (does a matching rule exist in t...

xiangyunyan
57分钟前
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部