網上關於 Androidpn 的文章不少,但是大都是基於應用層面來介紹這個開源項目的,今天我帶大家從源碼層面深入的分析 Androidpn 的內部結構,也算是對最近工作的一個總結吧,不多說,跟我一起看代碼!
一、Androidpn 開源項目
Androidpn 開源項目托管地址:http://sourceforge.net/projects/androidpn/
Androidpn 開源項目自身描述:This is an open source project to provide push notification support for Android, a xmpp based notification server and a client tool kit.
二、源碼分析
在程序的入口 DemoAppActivity 中設置通知的 icon 並開啟消息接收服務,代碼如下:
Number:1-1
ServiceManager serviceManager = new ServiceManager(this); serviceManager.setNotificationIcon(R.drawable.notification); serviceManager.startService();
在上面的代碼中可以看到程序對 ServiceManager 進行了初始化操作,在 ServiceManager 類的構造函數中我們可以看到程序對傳遞過來的 context 進行了判斷,如果這個 context 是一個 Activity 實例,緊接着會獲取對應的包名和類名。之后再去加載 res/raw/androidpn.properties 配置文件中的參數信息,並將讀取到的信息和之前從 context 中獲取的包名和類名一起存入首選項中。
Number:2-1
public ServiceManager(Context context) { this.context = context; if (context instanceof Activity) { Activity callbackActivity = (Activity) context; callbackActivityPackageName = callbackActivity.getPackageName(); callbackActivityClassName = callbackActivity.getClass().getName(); } props = loadProperties(); apiKey = props.getProperty("apiKey", ""); xmppHost = props.getProperty("xmppHost", "127.0.0.1"); xmppPort = props.getProperty("xmppPort", "5222"); sharedPrefs = context.getSharedPreferences(Constants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); Editor editor = sharedPrefs.edit(); editor.putString(Constants.API_KEY, apiKey); editor.putString(Constants.VERSION, version); editor.putString(Constants.XMPP_HOST, xmppHost); editor.putInt(Constants.XMPP_PORT, Integer.parseInt(xmppPort)); editor.putString(Constants.CALLBACK_ACTIVITY_PACKAGE_NAME, callbackActivityPackageName); editor.putString(Constants.CALLBACK_ACTIVITY_CLASS_NAME, callbackActivityClassName); editor.commit(); }
完成上述操作之后,緊接着調用 ServiceManager.startService() 方法來開啟服務,實際上 ServiceManager 只是一個普通的類,方法 ServiceManager.startService() 只是開啟一個子線程來開啟真正的服務類 NotificationService ,許多人認為開一個線程不停的去開啟服務會不會消耗相當一部分資源?答案是不會的,因為服務的生命周期決定了onCreate() 方法在服務被創建時調用,該方法只會被調用一次,無論調用多少次 startService() 方法,服務也只被創建一次,細心的讀者會發現 Androidpn 的作者在 NotificationService 類的 onStart(Intent intent, int startId) 方法中沒有做任何事,而是在onCreate() 方法中完成了諸多操作。
Number:3-1
public void startService() { Thread serviceThread = new Thread(new Runnable() { public void run() { Intent intent = NotificationService.getIntent(); context.startService(intent); } }); serviceThread.start(); }
下面我們來看 NotificationService 類的onCreate() 方法中都完成什么操作?
Number:4-1
public void onCreate() { telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); sharedPrefs = getSharedPreferences(Constants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); deviceId = telephonyManager.getDeviceId(); Editor editor = sharedPrefs.edit(); editor.putString(Constants.DEVICE_ID, deviceId); editor.commit(); if (deviceId == null || deviceId.trim().length() == 0 || deviceId.matches("0+")) { if (sharedPrefs.contains("EMULATOR_DEVICE_ID")) { deviceId = sharedPrefs.getString(Constants.EMULATOR_DEVICE_ID, ""); } else { deviceId = (new StringBuilder("EMU")).append((new Random(System.currentTimeMillis())).nextLong()).toString(); editor.putString(Constants.EMULATOR_DEVICE_ID, deviceId); editor.commit(); } } xmppManager = new XmppManager(this); taskSubmitter.submit(new Runnable() { public void run() { NotificationService.this.start(); } }); }
在上面的方法中作者獲取了設備號並將設備號存入了首選項中,同時還對在模擬器下運行的情況做了處理,這些操作是次要的。真正的核心的操作是 taskSubmitter 里調用了NotificationService.this.start(),這里的 NotificationService.this 完成了 NotificationService 的實例化,我們可以看到 NotificationService 類的構造方法中完成了 NotificationReceiver、ConnectivityReceiver、PhoneStateChangeListener、Executors、TaskSubmitter、TaskTracker 等類的實例化。
Number:5-1
public NotificationService() { notificationReceiver = new NotificationReceiver(); connectivityReceiver = new ConnectivityReceiver(this); phoneStateListener = new PhoneStateChangeListener(this); executorService = Executors.newSingleThreadExecutor(); taskSubmitter = new TaskSubmitter(this); taskTracker = new TaskTracker(this); }
NotificationService 的實例化完成后調用的start() 方法中注冊了一個廣播接收者 NotificationReceiver 用來處理從服務器推送過來的消息;同時還注冊了一個廣播接收者來監聽網絡連接狀況,如果有網絡連接,則執行 xmppManager.connect(),如果沒有網絡連接,則執行 xmppManager.disconnect()。但是在start() 方法中最終還是會執行 xmppManager.connect()。
Number:6-1
private void start() { registerNotificationReceiver(); registerConnectivityReceiver(); xmppManager.connect(); }
再來看看 xmppManager.connect() 方法中都做了些什么?程序在這個方法中提交了一個登錄任務:submitLoginTask(),在提交的登錄任務中又提交了一個注冊任務:submitRegisterTask(),同時將新建的登錄任務添加到任務集合中並交由 TaskTracker 來對添加的任務進行監視,此時 TaskTracker 的計數加一。
Number:7-1
public void connect() { submitLoginTask(); }
Number:7-2
private void submitLoginTask() { submitRegisterTask(); addTask(new LoginTask()); }
下面繼續來看新添加的登錄任務 new LoginTask() 具體做了什么?看 Number:8-2 代碼,程序在登錄任務線程的 run() 方法中首先去判斷當前客戶端是否已經經過身份驗證,驗證身份的代碼請看 Number:8-1 。
如果沒有通過身份驗證:xmppManager 會獲取當前連並接攜帶着從首選項中讀取的 username 和password 執行登錄操作,登錄成功后 xmppManager 會在登錄成功的連接上添加連接監聽器PersistentConnectionListener,這個監聽器可以監聽連接關閉和和連接錯誤,並在連接錯誤的情況下執行重連操作。接下來會在當前連接上添加包過濾器 PacketFilter packetFilter 和包監聽器 NotificationPacketListener packetListener,包過濾器用來校驗從服務器發送過來的數據包是否符合 NotificationIQ 格式,打開 NotificationIQ 類我們可以看到這個類中定義了數據包中需要封裝的信息:id、apiKey、title、message、uri。包監聽器則是用來真正處理從服務器發過來的數據。請看 Number:8-3 代碼,在 NotificationPacketListener 類的 processPacket(Packet packet) 方法中程序首先會判斷獲得的數據包是否是 NotificationIQ 的一個實例,如果是程序會調用 NotificationIQ 的getChildElementXML() 方法將數據包中攜帶的信息拼裝為一個字符串進行判斷動作是否為發送廣播,如果動作為發送廣播,程序會將數據包的信息填充到 Intent 中並發送廣播,注意這個廣播中填充到 Intent 的動作名稱 Constants.ACTION_SHOW_NOTIFICATION 為顯示廣播,這個動作被另一個廣播接收者 NotificationReceiver (該廣播接收者在之前的 NotificationService 的start() 方法中已經注冊)所監聽。
另外需要注意的是,如果客戶端在登錄過程中出現INVALID_CREDENTIALS_ERROR_CODE = "401" 錯誤,在 Number:8-2 的代碼中我們可以看到程序執行了 xmppManager.reregisterAccount() 操作和 xmppManager.startReconnectionThread() 操作。在 xmppManager.reregisterAccount() 操作中程序會刪除保存在首選項中的 username 和password 並重新提交登錄任務 submitLoginTask(),在這個登錄任務中依次再嵌套執行注冊、連接任務。這些任務執行完畢之后程序繼續調用 xmppManager.startReconnectionThread() 執行重連操作。如果客戶端在登錄過程中出現不可預知的錯誤,在 Number:8-2 的代碼中我們可以看到程序執直接調用 xmppManager.startReconnectionThread() 來執行重連操作。
如果已經通過身份驗證:意味着客戶端已經登錄成功,程序直接調用 xmppManager.runTask() 方法來執行之前添加到任務集合中的任務 new LoginTask(),同時 TaskTracker 的計數減一。
Number:8-1
private boolean isAuthenticated() { return connection != null && connection.isConnected() && connection.isAuthenticated(); }
Number:8-2
private class LoginTask implements Runnable { final XmppManager xmppManager; private LoginTask() { this.xmppManager = XmppManager.this; } public void run() { if (!xmppManager.isAuthenticated()) { Log.d(LOGTAG, "username=" + username); Log.d(LOGTAG, "password=" + password); try { xmppManager.getConnection().login(xmppManager.getUsername(), xmppManager.getPassword(), XMPP_RESOURCE_NAME); Log.d(LOGTAG, "Loggedn in successfully"); if (xmppManager.getConnectionListener() != null) { xmppManager.getConnection().addConnectionListener(xmppManager.getConnectionListener()); } PacketFilter packetFilter = new PacketTypeFilter(NotificationIQ.class); PacketListener packetListener = xmppManager.getNotificationPacketListener(); connection.addPacketListener(packetListener, packetFilter); xmppManager.runTask(); } catch (XMPPException e) { Log.e(LOGTAG, "LoginTask.run()... xmpp error"); Log.e(LOGTAG, "Failed to login to xmpp server. Caused by: " + e.getMessage()); String INVALID_CREDENTIALS_ERROR_CODE = "401"; String errorMessage = e.getMessage(); if (errorMessage != null && errorMessage.contains(INVALID_CREDENTIALS_ERROR_CODE)) { xmppManager.reregisterAccount(); return; } xmppManager.startReconnectionThread(); } catch (Exception e) { Log.e(LOGTAG, "LoginTask.run()... other error"); Log.e(LOGTAG, "Failed to login to xmpp server. Caused by: " + e.getMessage()); xmppManager.startReconnectionThread(); } } else { Log.i(LOGTAG, "Logged in already"); xmppManager.runTask(); } } }
Number:8-3
public class NotificationPacketListener implements PacketListener { private static final String LOGTAG = LogUtil.makeLogTag(NotificationPacketListener.class); private final XmppManager xmppManager; public NotificationPacketListener(XmppManager xmppManager) { this.xmppManager = xmppManager; } public void processPacket(Packet packet) { Log.d(LOGTAG, "NotificationPacketListener.processPacket()..."); Log.d(LOGTAG, "packet.toXML()=" + packet.toXML()); if (packet instanceof NotificationIQ) { NotificationIQ notification = (NotificationIQ) packet; if (notification.getChildElementXML().contains("androidpn:iq:notification")) { String notificationId = notification.getId(); String notificationApiKey = notification.getApiKey(); String notificationTitle = notification.getTitle(); String notificationMessage = notification.getMessage(); String notificationUri = notification.getUri(); Intent intent = new Intent(Constants.ACTION_SHOW_NOTIFICATION); intent.putExtra(Constants.NOTIFICATION_ID, notificationId); intent.putExtra(Constants.NOTIFICATION_API_KEY, notificationApiKey); intent.putExtra(Constants.NOTIFICATION_TITLE, notificationTitle); intent.putExtra(Constants.NOTIFICATION_MESSAGE, notificationMessage); intent.putExtra(Constants.NOTIFICATION_URI, notificationUri); xmppManager.getContext().sendBroadcast(intent); } } } }
Number:8-4
public void reregisterAccount() { removeAccount(); submitLoginTask(); runTask(); }
NotificationReceiver 在接收到NotificationPacketListener 中發出的廣播后,先判斷Intent 中攜帶的動作和自己所收聽的動作是否一致,如果一致,則繼續從Intent 中取出Intent 所攜帶的信息並調用 Notifier 的notify(String notificationId, String apiKey, String title, String message, String uri) 來發送通知。
Number:9-1
public final class NotificationReceiver extends BroadcastReceiver { private static final String LOGTAG = LogUtil.makeLogTag(NotificationReceiver.class); public NotificationReceiver() { } public void onReceive(Context context, Intent intent) { Log.d(LOGTAG, "NotificationReceiver.onReceive()..."); String action = intent.getAction(); Log.d(LOGTAG, "action=" + action); if (Constants.ACTION_SHOW_NOTIFICATION.equals(action)) { String notificationId = intent.getStringExtra(Constants.NOTIFICATION_ID); String notificationApiKey = intent.getStringExtra(Constants.NOTIFICATION_API_KEY); String notificationTitle = intent.getStringExtra(Constants.NOTIFICATION_TITLE); String notificationMessage = intent.getStringExtra(Constants.NOTIFICATION_MESSAGE); String notificationUri = intent.getStringExtra(Constants.NOTIFICATION_URI); Notifier notifier = new Notifier(context); notifier.notify(notificationId, notificationApiKey, notificationTitle, notificationMessage, notificationUri); } } }
Notifier 在發送通知之前會先去首選項中讀取用戶的配置信息,如果配置信息中 Constants.SETTINGS_NOTIFICATION_ENABLED 的值為 true,然后開始組裝通知並為通知進行參數配置,這些操作完成后再調用 NotificationManager 將組裝好的通知發送出去。至此,在客戶端已經注冊的前提下,執行的登錄、接收服務器數據包、發送廣播、發送通知的流程就結束了,添加在當前連接上的NotificationPacketListener 會一直監聽從服務器發送過來的數據包並重復執行數據包解析、發送廣播、發送通知的操作。
但是需要注意的是從代碼 Number:7-1 至代碼 Number:9-1 的流程是以客戶端已經完成注冊為前提的;如果客戶端是第一次執行消息推送的服務,顯然不會直接進入到登錄的邏輯中來,讓我們繼續跳到 Number:7-2 中的岔路口,程序在提交登錄任務的內部嵌套着提交了一個注冊任務 submitRegisterTask(),繼續來看這個注冊任務做了什么操作。在這個注冊任務中繼續將新建的注冊任務添加到任務集合中並交由 TaskTracker 來對添加的任務進行監視,此時 TaskTracker 的計數加一;與此同時內嵌提交了一個連接任務submitConnectTask()。
Number:10-1
private void submitRegisterTask() { submitConnectTask(); addTask(new RegisterTask()); }
先來看登錄任務中做了什么操作?參看代碼 Number:11-1。
如果沒有注冊:則使用UUID生成2個隨機數作為 username 和 password,同時實例化 Registration,將創建的包過濾器和包監聽器添加到當前連接上,然后使用 Registration 實例將生成的 username 和 password 作為屬性添加到 Registration 實例上,再由當前連接調用 connection.sendPacket(registration) 向服務器發送數據包執行注冊操作。創建的包監聽器會監聽並處理服務器會送的數據包,PacketListener 在接收到服務器會送的數據包后,同樣會判斷數據包的格式是否符合包過濾器中定義的格式,只有格式匹配的情況下進行后續處理。在格式匹配的情況下,程序繼續進行判斷:如果服務器返回信息的類型是 IQ.Type.ERROR 則進行報錯處理;如果服務器返回信息的類型是 IQ.Type.RESULT 證明在服務器注冊成功,這時程序會將 username 和 password 存儲到首選項中,之后程序直接調用 xmppManager.runTask() 方法來執行之前添加到任務集合中的任務 new LoginTask(),同時 TaskTracker 的計數減一。
如果已經注冊:意味着首選項中已經有了配置信息,程序直接調用 xmppManager.runTask() 方法來執行之前添加到任務集合中的任務 new LoginTask(),同時 TaskTracker 的計數減一。
Number:11-1
private class RegisterTask implements Runnable { final XmppManager xmppManager; private RegisterTask() { xmppManager = XmppManager.this; } public void run() { if (!xmppManager.isRegistered()) { final String newUsername = newRandomUUID(); final String newPassword = newRandomUUID(); Registration registration = new Registration(); PacketFilter packetFilter = new AndFilter(new PacketIDFilter(registration.getPacketID()), new PacketTypeFilter(IQ.class)); PacketListener packetListener = new PacketListener() { public void processPacket(Packet packet) { Log.d("RegisterTask.PacketListener", "processPacket()....."); Log.d("RegisterTask.PacketListener", "packet=" + packet.toXML()); if (packet instanceof IQ) { IQ response = (IQ) packet; if (response.getType() == IQ.Type.ERROR) { if (!response.getError().toString().contains("409")) { Log.e(LOGTAG, "Unknown error while registering XMPP account! " + response.getError().getCondition()); } } else if (response.getType() == IQ.Type.RESULT) { xmppManager.setUsername(newUsername); xmppManager.setPassword(newPassword); Log.d(LOGTAG, "username=" + newUsername); Log.d(LOGTAG, "password=" + newPassword); Editor editor = sharedPrefs.edit(); editor.putString(Constants.XMPP_USERNAME, newUsername); editor.putString(Constants.XMPP_PASSWORD, newPassword); editor.commit(); Log.i(LOGTAG, "Account registered successfully"); xmppManager.runTask(); } } } }; connection.addPacketListener(packetListener, packetFilter); registration.setType(IQ.Type.SET); registration.addAttribute("username", newUsername); registration.addAttribute("password", newPassword); connection.sendPacket(registration); } else { Log.i(LOGTAG, "Account registered already"); xmppManager.runTask(); } } }
至此,在客戶端已經連接到服務器的前提下,執行的注冊、登錄、接收服務器數據包、發送廣播、發送通知的流程就結束了,添加在當前連接上的 NotificationPacketListener 會一直監聽從服務器發送過來的數據包並重復執行數據包解析、發送廣播、發送通知的操作。
同樣需要注意的是從代碼 Number:10-1 至代碼 Number:11-1 的流程是以客戶端已經連接到服務器為前提的;如果客戶端是第一次執行消息推送的服務,顯然也不會直接進入到注冊的邏輯中來,讓我們繼續跳到 Number:10-1 中的岔路口,程序在提交注冊任務的內部嵌套着提交了一個連接任務 submitConnectTask(),繼續來看這個連接任務做了什么操作。在這個連接任務中程序直接將新建的連接任務添加到任務集合中並交由 TaskTracker 來對添加的任務進行監視,此時 TaskTracker 的計數加一。
Number:12-1
private void submitConnectTask() { addTask(new ConnectTask()); }
繼續來看連接任務中做了什么操作?參看代碼 Number:13-1。
如果沒有連接到服務器:程序會從首選項中讀取 xmppHost 和 xmppPort 並使用 XMPPConnection 通過配置信息實例化一個連接,然后再由該連接執行連接操作。連接成功后,程序調用xmppManager.runTask() 方法來執行之前添加到任務集合中的任務 new RegisterTask(),同時 TaskTracker 的計數減一。
如果已經連接到服務器:程序直接調用 xmppManager.runTask() 方法來執行之前添加到任務集合中的任務 new RegisterTask(),同時 TaskTracker 的計數減一。
Number:13-1
private class ConnectTask implements Runnable { final XmppManager xmppManager; private ConnectTask() { this.xmppManager = XmppManager.this; } public void run() { if (!xmppManager.isConnected()) { // Create the configuration for this new connection ConnectionConfiguration connConfig = new ConnectionConfiguration(xmppHost, xmppPort); connConfig.setSecurityMode(SecurityMode.required); connConfig.setSASLAuthenticationEnabled(false); connConfig.setCompressionEnabled(false); XMPPConnection connection = new XMPPConnection(connConfig); xmppManager.setConnection(connection); try { // Connect to the server connection.connect(); Log.i(LOGTAG, "XMPP connected successfully"); // packet provider ProviderManager.getInstance().addIQProvider("notification", "androidpn:iq:notification", new NotificationIQProvider()); } catch (XMPPException e) { Log.e(LOGTAG, "XMPP connection failed", e); } xmppManager.runTask(); } else { Log.i(LOGTAG, "XMPP connected already"); xmppManager.runTask(); } } }
至此,在客戶端執行的連接、注冊、登錄、接收服務器數據包、發送廣播、發送通知的流程就結束了,添加在當前連接上的 NotificationPacketListener 會一直監聽從服務器發送過來的數據包並重復執行數據包解析、發送廣播、發送通知的操作。
二、后續問題
▐ 關於服務器重啟客戶端自動重連服務器的問題?
▐ 在 XmppManager 的 addTask(Runnable runnable) 方法中添加 runTask() 方法即可解決。
private void addTask(Runnable runnable) { taskTracker.increase(); synchronized (taskList) { if (taskList.isEmpty() && !running) { running = true; futureTask = taskSubmitter.submit(runnable); if (futureTask == null) { taskTracker.decrease(); } } else { /** * runTask(); 解決服務器端重啟后,客戶端不能成功連接 Androidpn 服務器 */ runTask(); taskList.add(runnable); } } }
▐ 關於使用設備ID或 MAC替換源碼中的 UUID作為 username 和 password 帶來的問題?
如果把客戶端隨機生成的UUID代碼,替換為設備的ID或者MAC作為用戶名,服務器端會出現重復插入的錯誤。
把客戶端的數據清除(或卸載后重新安裝),那么 SharedPreferences 里的數據也會被清除,然而服務器端又有我們手機的設備 ID,這時客戶端啟動程序從首選項中讀取不到 username 和 password 會重新拿着相同的設備 ID 提交給服務器進行注冊,這時服務器端就會出現重復插入的問題。
▐ 在服務器端保存用戶信息的時候,檢查數據庫中是否存在該用戶。
▐ Android 消息推送的其他途徑
▐ 極光推送
網站參考地址 : http://www.jpush.cn/
▐ Google Cloud Messaging for Android
網站參考地址 : http://developer.android.com/google/gcm/index.html
▐ MQTT 協議推送
客戶端下載地址 : https://github.com/tokudu/AndroidPushNotificationsDemo
服務器下載地址 : https://github.com/tokudu/PhpMQTTClient
三、相關閱讀