網上關于 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