最近看到了一个开源的push项目:kpush,整个项目包含了客户端和服务端的源码,强烈推荐下。决定分析下该项目的源码,学习下作者的解决方案。android端源码比较简单,我们就从简单的入手,本篇详细分析下android端的源码。
1. 目录结构
我们来看下android工程的目录结构
KPushDemo
----src
--------cn.kpush
--------cn.kpush_demo
--------cn.vimer.ferry
--------cn.vimer.netkit
其中cn.kpush是android端push sdk,实现了push的所有功能,cn.vimer.ferry主要是实现了tcp客户端连接/发送/接收数据功能,cn.vimer.netkit主要是数据序列化及反序列化的功能。关于ferry和netkit我们可以先不关注其代码,我们将把精力集中在kpush上。
2. 初始化/数据接收流程
我们先看下MainActivity中的代码,在MainActivity中的onCreate函数中有如下代码:
KPush.init(this); KPush.setDebug(true); // KPush.setAliasAndTags("dante", new String[]{"a", "c"});然后看下KPush.java中的init函数
public static void init(Context context_) { context = context_; setDebug(false); Intent intent = new Intent(context, PushService.class); intent.setAction(Config.INTENT_ACTION_SERVICE_START); context_.startService(intent); }可以看到init函数中主要一个功能就是start PushService服务,那么接下来我们分析下PushService的处理流程。
我们首先看下PushService.java文件中的onCreate函数:
@Override public void onCreate() { super.onCreate(); KLog.d(""); // 初始化的时候 userAuthed = false; // 因为service可能重新进来 DeviceInfo.init(this); handler = new Handler(); regEventCallback(); allocServer(); // 启动心跳 heartbeat(); }DeviceInfo.init(this)主要工作是完成设备信息初始化操作,获取应用包名/版本号/设备id等一些信息。
regEventCallback()顾名思义就是注册一些事件的回调,用于处理相关的事件
allocServer()顾名思义是分配服务器
heartbeat()用于发送心跳包,以保持与服务器的连接
接下来我们分析allocServer函数,allocServer函数代码就一行:
private void allocServer() { // 申请 server new AllocServerTask().execute(); }AllocServerTask代码如下:
private class AllocServerTask extends AsyncTask<String, Integer, Integer> { JSONObject jsonData; @Override protected void onPreExecute() { } @Override protected Integer doInBackground(String... params) { try { HttpClient httpClient = new DefaultHttpClient(); // 连接超时 httpClient.getParams().setParameter( CoreConnectionPNames.CONNECTION_TIMEOUT, Config.HTTP_CONNECT_TIMEOUT * 1000); // 读取超时 httpClient.getParams().setParameter( CoreConnectionPNames.SO_TIMEOUT, Config.HTTP_READ_TIMEOUT * 1000); String allocServerUrl = String.format(Config.ALLOC_SERVER_URL, DOMAIN); KLog.d("allocServerUrl: " + allocServerUrl); HttpPost httpPost = new HttpPost(allocServerUrl); JSONObject jsonObject = new JSONObject(); jsonObject.put("appkey", DeviceInfo.getAppkey()); jsonObject.put("channel", DeviceInfo.getChannel()); jsonObject.put("device_id", DeviceInfo.getDeviceId()); jsonObject.put("device_name", DeviceInfo.getDeviceName()); jsonObject.put("os_version", DeviceInfo.getOsVersion()); jsonObject.put("os", Config.OS); jsonObject.put("sdk_version", Config.SDK_VERSION); String postBody = Utils.packData(SECRET_KEY, jsonObject); if (postBody == null) { // 一般不会出现在这 KLog.e(String.format("packData fail. jsonData: %s", jsonObject.toString())); return -1; } KLog.d("postBody: " + postBody); httpPost.setEntity(new StringEntity(postBody)); HttpResponse httpResponse = httpClient.execute(httpPost); int code = httpResponse.getStatusLine().getStatusCode(); if (code == 200) { String recvBody = EntityUtils.toString(httpResponse .getEntity()); jsonData = Utils.unpackData(SECRET_KEY, recvBody); KLog.d("jsonData: " + jsonData); if (jsonData == null) { KLog.e(String .format("unpackData invalid: %s", recvBody)); return -3; } KLog.d("succ"); // 解析成功 return 0; } else { KLog.e(String.format("status code invalid: %d", code)); return -4; } } catch (Exception e) { KLog.e("fail: " + e.toString()); return -2; } } @Override protected void onProgressUpdate(Integer... progresses) { } @Override protected void onPostExecute(Integer result) { KLog.d("result: " + result); if (result != 0) { // 说明失败 allocServerLater(); return; } try { serverHost = jsonData.getJSONObject("server").getString("host"); serverPort = jsonData.getJSONObject("server").getInt("port"); userId = jsonData.getJSONObject("user").getLong("uid"); userKey = jsonData.getJSONObject("user").getString("key"); } catch (Exception e) { KLog.e("fail: " + e.toString()); allocServerLater(); return; } // 清零 failConnectTimes = 0; connectToServer(); } @Override protected void onCancelled() { KLog.d(""); allocServerLater(); } }AllocServerTask其实就是一个AsyncTask,执行一些后台操作,那这些操作到底是什么呢,接下来我们就一一分析下到底这个task中都干了些什么?首先我们到doInBackground函数中,该函数中主要就做了三件事情:
1. 打开一个http client
2. 把DeviceInfo中的设备信息组织成json串,然后http post到http://demo.kpush.cn/server/alloc
3. 解析post的response消息到jsonData变量中
然后我们进入到onPostExecute函数中,可以看到在该函数中,主要从jsonData中取出host/port/uid/key四个数据,然后调用connectToServer函数,看到connectToServer函数名称及上面取出的四个数据,我们可以猜测出来是要连接到服务器上,可以看下connectToServer具体代码:
private void connectToServer() { Ferry.getInstance().init(serverHost, serverPort); if (Ferry.getInstance().isRunning()) { if (!Ferry.getInstance().isConnected()) { Ferry.getInstance().connect(); } } else { Ferry.getInstance().start(); } }通过上述代码可以验证我们的猜测,主要就是通过ferry,连接到serverHost:serverPort TCP服务器上。到目前位置,感觉流程已经走完了,难道只是连接上服务器就结束了吗?答案是否定的,还记得onCreate函数中的regEventCallback吗?接下来我们就看看这个函数,流程如何从connectToServer流转到了regEventCallback函数中的呢?regEventCallback函数如下:
private void regEventCallback() { Ferry.getInstance().addEventCallback(new Ferry.CallbackListener() { @Override public void onOpen() { userLogin(); } @Override public void onRecv(IBox ibox) { Box box = (Box) ibox; KLog.d("box: " + box); JSONObject jsonData = Utils.unpackData(SECRET_KEY, box.body); KLog.d("box.body: " + box.body); KLog.d("jsonData: " + jsonData); if (box.cmd == Proto.EVT_NOTIFICATION) { if (jsonData != null) { try { int notificationID = jsonData.getInt("id"); recvNotification(notificationID); boolean silent = false; if (jsonData.has("silent")) { silent = jsonData.getBoolean("silent"); } if (!silent) { showNotification(notificationID, jsonData.getString("title"), jsonData.getString("content")); } } catch (Exception e) { KLog.e(String.format("exc occur. e: %s, box: %s", e, box)); } } } } @Override public void onClose() { userAuthed = false; KLog.e(""); // Ferry.getInstance().connect(); // 从获取IP开始 allocServer(); } @Override public void onError(int code, IBox ibox) { KLog.e(String.format("code: %d", code)); } }, this, "main"); }看到该函数中的onOpen回调函数了没,当我们在connectToServer函数中调用Ferry.getInstance().start()去连接tcp服务器,如果连接上就会调用onOpen函数,这是整个流程就流转到了regEventCallback。我们看到onOpen回调函数中就一行代码:userLogin(); 可以知道这个函数是实现用户登录的,该函数具体代码如下:
private void userLogin() { Box box = new Box(); box.cmd = Proto.CMD_LOGIN; JSONObject jsonObject = new JSONObject(); try { jsonObject.put("uid", userId); jsonObject.put("key", userKey); } catch (Exception e) { KLog.e("exc occur. e: " + e); } String body = Utils.packData(SECRET_KEY, jsonObject); KLog.d("body: " + body); box.body = body == null ? null : body.getBytes(); Ferry.getInstance().send(box, new Ferry.CallbackListener() { @Override public void onSend(IBox ibox) { } @Override public void onRecv(IBox ibox) { Box box = (Box) ibox; if (box.ret != 0) { // 几秒后再重试 userLoginLater(); } else { // 登录成功 userAuthed = true; failConnectTimes = 0; sendPendingMsgs(); } } @Override public void onError(int code, IBox ibox) { KLog.e(String.format("code: %d", code)); userLoginLater(); } @Override public void onTimeout() { KLog.e(""); userLoginLater(); } }, 5, this); }可以看到该函数中,把uid和key封装到box,然后通过ferry发送到tcp server,然后等待服务器的响应消息,判断是否登录成功,如果失败则进行重试操作。
到此整个初始化流程已经完成了,接下来就等待接收推送的消息并进行响应了。regEventCallback函数中的onRecv函数就是用于接收push消息,然后进行相应的处理。
3. 数据结构
上面已经分析了基本流程,接下来本节将分析数据结构定义,打开apk,并然后通过管理后台推送一条消息,我们可以看到logcat中有如下的log输出:
05-13 16:20:34.790 435 679 I ActivityManager: Start proc cn.kpush_demo for activity cn.kpush_demo/.MainActivity: pid=23280 uid=10057 gids={50057, 3003, 1028, 1015} 05-13 16:20:34.880 23280 23280 D kpush : [main(1) PushService.java:58 onCreate]: 05-13 16:20:34.890 23280 23280 D kpush : [main(1) DeviceInfo.java:37 init]: packageName: cn.kpush_demo, appVersion: 1, deviceId: b1ff4b0a-2020-33e4-9fb4-e5c3fb76d0c5, osVersion: 19, deviceName: J1 05-13 16:20:34.890 23280 23280 D kpush : [main(1) PushService.java:76 onStartCommand]: action: cn.kpush.intent.SERVICE_START 05-13 16:20:34.890 23280 23294 D kpush : [AsyncTask #1(225) PushService.java:458 doInBackground]: allocServerUrl: http://demo.kpush.cn/server/alloc 05-13 16:20:34.930 23280 23294 D kpush : [AsyncTask #1(225) PushService.java:478 doInBackground]: postBody: {"sign":"036fe07a2b1a72dc1c6ef6e467f5121b","data":"{\"appkey\":\"7d357c9b4ce1414fb27f077b54fb5a8f\",\"sdk_version\":5,\"os\":\"android\",\"device_id\":\"b1ff4b0a-2020-33e4-9fb4-e5c3fb76d0c5\",\"device_name\":\"J1\",\"channel\":\"MAIN\",\"os_version\":19}"} 05-13 16:20:34.970 435 451 I ActivityManager: Displayed cn.kpush_demo/.MainActivity: +200ms 05-13 16:20:35.130 23280 23294 D kpush : [AsyncTask #1(225) PushService.java:488 doInBackground]: jsonData: {"ret":0,"user":{"uid":211,"key":"41675b5ab24d461c9111f5c6c197f74b"},"server":{"port":29100,"host":"115.28.224.64"}} 05-13 16:20:35.130 23280 23294 D kpush : [AsyncTask #1(225) PushService.java:496 doInBackground]: succ 05-13 16:20:35.140 23280 23280 D kpush : [main(1) PushService.java:516 onPostExecute]: result: 0 05-13 16:20:35.170 23280 23280 D kpush : [main(1) PushService.java:191 userLogin]: body: {"sign":"75195f6043da7cbd17f74b90fbbac6f2","data":"{\"uid\":211,\"key\":\"41675b5ab24d461c9111f5c6c197f74b\"}"} 05-13 16:20:39.870 435 702 I ActivityManager: START u0 {cmp=cn.kpush_demo/.NextActivity} from pid 23280 05-13 16:20:39.940 435 451 I ActivityManager: Displayed cn.kpush_demo/.NextActivity: +65ms 05-13 16:21:09.350 23280 23280 D kpush : [main(1) PushService.java:131 onRecv]: box: magic: 2037952207, version: 0, flag: 0, packetLen: 170, cmd: 10001, ret: 0, sn: 0, bodyLen: 146 05-13 16:21:09.350 23280 23280 D kpush : [main(1) PushService.java:133 onRecv]: box.body: [B@4207f618 05-13 16:21:09.360 23280 23280 D kpush : [main(1) PushService.java:134 onRecv]: jsonData: {"silent":false,"id":592,"content":"test_content","title":"test_title"}由log可以看到allocServerUrl: http://demo.kpush.cn/server/alloc,这个http server是用于分配uid/key/tcp server等资源,客户端通过post请求,post的内容为
{"sign":"036fe07a2b1a72dc1c6ef6e467f5121b","data":"{\"appkey\":\"7d357c9b4ce1414fb27f077b54fb5a8f\",\"sdk_version\":5,\"os\":\"android\",\"device_id\":\"b1ff4b0a-2020-33e4-9fb4-e5c3fb76d0c5\",\"device_name\":\"J1\",\"channel\":\"MAIN\",\"os_version\":19}"}post的response数据为
{"ret":0,"user":{"uid":211,"key":"41675b5ab24d461c9111f5c6c197f74b"},"server":{"port":29100,"host":"115.28.224.64"}}其中user为用户信息,在userLogin中使用,server则是tcp server的地址,用于客户端连接到该tcp server上。
当我们通过管理后台推送一条消息是,可以看到接收到的消息格式为:
{"silent":false,"id":592,"content":"test_content","title":"test_title"}其中title为推送消息的标题,content则为推送的消息内容。
到此客户端源码分析完毕,基本流程如图所示