在Android系統中,廣播(Broadcast)是在組件之間傳播數據(Intent)的一種機制;這些組件甚至是可以位于不同的進程中,這樣它就像Binder機制一樣,起到進程間通信的作用;本文通過一個簡單的例子來學習Android系統的廣播機制,為后續分析廣播機制的源代碼作準備。
在Android系統中,為什么需要廣播機制呢?廣播機制,本質上它就是一種組件間的通信方式,如果是兩個組件位于不同的進程當中,那么可以用Binder機制來實現,如果兩個組件是在同一個進程中,那么它們之間可以用來通信的方式就更多了,這樣看來,廣播機制似乎是多余的。然而,廣播機制卻是不可替代的,它和Binder機制不一樣的地方在于,廣播的發送者和接收者事先是不需要知道對方的存在的,這樣帶來的好處便是,系統的各個組件可以松耦合地組織在一起,這樣系統就具有高度的可擴展性,容易與其它系統進行集成。
在軟件工程中,是非常強調模塊之間的高內聚低耦合性的,不然的話,隨著系統越來越龐大,就會面臨著越來越難維護的風險,最后導致整個項目的失敗。Android應用程序的組織方式,可以說是把這種高內聚低耦合性的思想貫徹得非常透徹,在任何一個Activity中,都可以使用一個簡單的Intent,通過startActivity或者startService,就可以把另外一個Activity或者Service啟動起來為它服務,而且它根本上不依賴這個Activity或者Service的實現,只需要知道它的字符串形式的名字即可,而廣播機制更絕,它連接收者的名字都不需要知道。
不過話又說回來,廣播機制在Android系統中,也不算是什么創新的東西。如果讀者了解J2EE或者COM,就會知道,在J2EE中,提供了消息驅動Bean(Message-Driven Bean),用來實現應用程序各個組件之間的消息傳遞;而在COM中,提供了連接點(Connection Point)的概念,也是用來在應用程序各個組間間進行消息傳遞。無論是J2EE中的消息驅動Bean,還是COM中的連接點,或者Android系統的廣播機制,它們的實現機理都是消息發布/訂閱模式的事件驅動模型,消息的生產者發布事件,而使用者訂閱感興趣的事件。
廢話說了一大堆,現在開始進入主題了,和前面的文章一樣,我們通過具體的例子來介紹Android系統的廣播機制。在這個例子中,有一個Service,它在另外一個線程中實現了一個計數器服務,每隔一秒鐘就自動加1,然后將結果不斷地反饋給應用程序中的界面線程,而界面線程中的Activity在得到這個反饋后,就會把結果顯示在界面上。為什么要把計數器服務放在另外一個線程中進行呢?我們可以把這個計數器服務想象成是一個耗時的計算型邏輯,如果放在界面線程中去實現,那么勢必就會導致應用程序不能響應界面事件,最后導致應用程序產生ANR(Application Not Responding)問題。計數器線程為了把加1后的數字源源不斷地反饋給界面線程,這時候就可以考慮使用廣播機制了。
首先在Android源代碼工程中創建一個Android應用程序工程,名字就稱為Broadcast吧。關于如何獲得Android源代碼工程,請參考在Ubuntu上下載、編譯和安裝Android最新源代碼一文;關于如何在Android源代碼工程中創建應用程序工程,請參考在Ubuntu上為Android系統內置Java應用程序測試Application Frameworks層的硬件服務一文。這個應用程序工程定義了一個名為shy.luo.broadcast的package,這個例子的源代碼主要就是實現在這里了。下面,將會逐一介紹這個package里面的文件。
首先,我們在src/shy/luo/broadcast/ICounterService.java文件中定義計數器的服務接口:
package shy.luo.broadcast;
public interface ICounterService {
public void startCounter(int initVal);
public void stopCounter();
}
這個接口很簡單,它只有兩個成員函數,分別用來啟動和停止計數器;啟動計數時,還可以指定計數器的初始值。
接著,我們來看一個應用程序的默認Activity的實現,在src/shy/luo/broadcast/MainActivity.java文件中:
package shy.luo.broadcast;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends Activity implements OnClickListener {
private final static String LOG_TAG = "shy.luo.broadcast.MainActivity";
private Button startButton = null;
private Button stopButton = null;
private TextView counterText = null;
private ICounterService counterService = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
startButton = (Button)findViewById(R.id.button_start);
stopButton = (Button)findViewById(R.id.button_stop);
counterText = (TextView)findViewById(R.id.textview_counter);
startButton.setOnClickListener(this);
stopButton.setOnClickListener(this);
startButton.setEnabled(true);
stopButton.setEnabled(false);
Intent bindIntent = new Intent(MainActivity.this, CounterService.class);
bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE);
Log.i(LOG_TAG, "Main Activity Created.");
}
@Override
public void onResume() {
super.onResume();
IntentFilter counterActionFilter = new IntentFilter(CounterService.BROADCAST_COUNTER_ACTION);
registerReceiver(counterActionReceiver, counterActionFilter);
}
@Override
public void onPause() {
super.onPause();
unregisterReceiver(counterActionReceiver);
}
@Override
public void onDestroy() {
super.onDestroy();
unbindService(serviceConnection);
}
@Override
public void onClick(View v) {
if(v.equals(startButton)) {
if(counterService != null) {
counterService.startCounter(0);
startButton.setEnabled(false);
stopButton.setEnabled(true);
}
} else if(v.equals(stopButton)) {
if(counterService != null) {
counterService.stopCounter();
startButton.setEnabled(true);
stopButton.setEnabled(false);
}
}
}
private BroadcastReceiver counterActionReceiver = new BroadcastReceiver(){
public void onReceive(Context context, Intent intent) {
int counter = intent.getIntExtra(CounterService.COUNTER_VALUE, 0);
String text = String.valueOf(counter);
counterText.setText(text);
Log.i(LOG_TAG, "Receive counter event");
}
};
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
counterService = ((CounterService.CounterBinder)service).getService();
Log.i(LOG_TAG, "Counter Service Connected");
}
public void onServiceDisconnected(ComponentName className) {
counterService = null;
Log.i(LOG_TAG, "Counter Service Disconnected");
}
};
}
MainActivity的實現也很簡單,它在創建(onCreate)的時候,會調用bindService函數來把計數器服務(CounterService)啟動起來,它的第二個參數serviceConnection是一個ServiceConnection實例。計數器服務啟動起來后,系統會調用這個實例的onServiceConnected函數將一個Binder對象傳回來,通過調用這個Binder對象的getService函數,就可以獲得計數器服務接口。這里,把這個計數器服務接口保存在MainActivity的counterService成員變量中。同樣,當我們調用unbindService停止計數器服務時,系統會調用這個實例的onServiceDisconnected函數告訴MainActivity,它與計數器服務的連接斷開了。
注意,這里通過調用bindService函數來啟動Service時,這個Service與啟動它的Activity是位于同一個進程中,它不像我們在前面一篇文章Android系統在新進程中啟動自定義服務過程(startService)的原理分析中所描述那樣在新的進程中啟動服務,后面我們再寫一篇文章來分析bindService啟動服務的過程。
在MainActivity的onResume函數中,通過調用registerReceiver函數注冊了一個廣播接收器counterActionReceiver,它是一個BroadcastReceiver實例,并且指定了這個廣播接收器只對CounterService.BROADCAST_COUNTER_ACTION類型的廣播感興趣。當CounterService發出一個CounterService.BROADCAST_COUNTER_ACTION類型的廣播時,系統就會把這個廣播發送到counterActionReceiver實例的onReceiver函數中去。在onReceive函數中,從參數intent中取出計數器當前的值,顯示在界面上。
MainActivity界面上有兩個按鈕,分別是Start Counter和Stop Counter按鈕,點擊前者開始計數,而點擊后者則停止計數。
計數器服務CounterService實現在src/shy/luo/broadcast/CounterService.java文件中:
package shy.luo.broadcast;
import android.app.Service;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
public class CounterService extends Service implements ICounterService {
private final static String LOG_TAG = "shy.luo.broadcast.CounterService";
public final static String BROADCAST_COUNTER_ACTION = "shy.luo.broadcast.COUNTER_ACTION";
public final static String COUNTER_VALUE = "shy.luo.broadcast.counter.value";
private boolean stop = false;
private final IBinder binder = new CounterBinder();
public class CounterBinder extends Binder {
public CounterService getService() {
return CounterService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public void onCreate() {
super.onCreate();
Log.i(LOG_TAG, "Counter Service Created.");
}
@Override
public void onDestroy() {
Log.i(LOG_TAG, "Counter Service Destroyed.");
}
public void startCounter(int initVal) {
AsyncTask<Integer, Integer, Integer> task = new AsyncTask<Integer, Integer, Integer>() {
@Override
protected Integer doInBackground(Integer... vals) {
Integer initCounter = vals[0];
stop = false;
while(!stop) {
publishProgress(initCounter);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
initCounter++;
}
return initCounter;
}
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
int counter = values[0];
Intent intent = new Intent(BROADCAST_COUNTER_ACTION);
intent.putExtra(COUNTER_VALUE, counter);
sendBroadcast(intent);
}
@Override
protected void onPostExecute(Integer val) {
int counter = val;
Intent intent = new Intent(BROADCAST_COUNTER_ACTION);
intent.putExtra(COUNTER_VALUE, counter);
sendBroadcast(intent);
}
};
task.execute(0);
}
public void stopCounter() {
stop = true;
}
}
這個計數器服務實現了ICounterService接口。當這個服務被binderService函數啟動時,系統會調用它的onBind函數,這個函數返回一個Binder對象給系統。上面我們說到,當MainActivity調用bindService函數來啟動計數器服務器時,系統會調用MainActivity的ServiceConnection實例serviceConnection的onServiceConnected函數通知MainActivity,這個服務已經連接上了,并且會通過這個函數傳進來一個Binder遠程對象,這個Binder遠程對象就是來源于這里的onBind的返回值了。
函數onBind返回的Binder對象是一個自定義的CounterBinder實例,它實現了一個getService成員函數。當系統通知MainActivity,計數器服務已經啟動起來并且連接成功后,并且將這個Binder對象傳給MainActivity時,MainActivity就會把這個Binder對象強制轉換為CounterBinder實例,然后調用它的getService函數獲得服務接口。這樣,MainActivity就通過這個Binder對象和CounterService關聯起來了。
當MainActivity調用計數器服務接口的startCounter函數時,計數器服務并不是直接進入計數狀態,而是通過使用異步任務(AsyncTask)在后臺線程中進行計數。這里為什么要使用異步任務來在后臺線程中進行計數呢?前面我們說過,這個計數過程是一個耗時的計算型邏輯,不能把它放在界面線程中進行,因為這里的CounterService啟動時,并沒有在新的進程中啟動,它與MainActivity一樣,運行在應用程序的界面線程中,因此,這里需要使用異步任務在在后臺線程中進行計數。
異步任務AsyncTask的具體用法可以參考官方文檔http://developer.android.com/reference/android/os/AsyncTask.html。它的大概用法是,當我們調用異步任務實例的execute(task.execute)方法時,當前調用線程就返回了,系統啟動一個后臺線程來執行這個異步任務實例的doInBackground函數,這個函數就是我們用來執行耗時計算的地方了,它會進入到一個循環中,每隔1秒鐘就把計數器加1,然后進入休眠(Thread.sleep),醒過來,再重新這個計算過程。在計算的過程中,可以通過調用publishProgress函數來通知調用者當前計算的進度,好讓調用者來更新界面,調用publishProgress函數的效果最終就是直入到這個異步任務實例的onProgressUpdate函數中,這里就可以把這個進度值以廣播的形式(sendBroadcast)發送出去了,這里的進度值就定義為當前計數服務的計數值。
當MainActivity調用計數器服務接口的stopCounter函數時,會告訴函數doInBackground停止執行計數(stop = true),于是,函數doInBackground就退出計數循環,然后將最終計數結果返回了,返回的結果最后進入到onPostExecute函數中,這個函數同樣通過廣播的形式(sendBroadcast)把這個計數結果廣播出去。
計算器服務就介紹到這里了,下面我們看看應用程序的配置文件AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="shy.luo.broadcast"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".CounterService"
android:enabled="true">
</service>
</application>
</manifest>
這個配置文件很簡單,只是告訴系統,它有一個Activity和一個Service。
再來看MainActivity的界面文件,它定義在res/layout/main.xml文件中:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10px"
android:orientation="horizontal"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="4px"
android:gravity="center"
android:text="@string/counter">
</TextView>
<TextView
android:id="@+id/textview_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="0">
</TextView>
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<Button
android:id="@+id/button_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/start">
</Button>
<Button
android:id="@+id/button_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/stop" >
</Button>
</LinearLayout>
</LinearLayout>
這個界面配置文件也很簡單,等一下我們在模擬器把這個應用程序啟動起來后,就可以看到它的截圖了。
應用程序用到的字符串資源文件位于res/values/strings.xml文件中:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Broadcast</string>
<string name="counter">Counter: </string>
<string name="start">Start Counter</string>
<string name="stop">Stop Counter</string>
</resources>
最后,我們還要在工程目錄下放置一個編譯腳本文件Android.mk:
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_PACKAGE_NAME := Broadcast
include $(BUILD_PACKAGE)
接下來就要編譯了。有關如何單獨編譯Android源代碼工程的模塊,以及如何打包system.img,請參考
如何單獨編譯Android源代碼中的模塊一文。
執行以下命令進行編譯和打包:
USER-NAME@MACHINE-NAME:~/Android$ mmm packages/experimental/Broadcast
USER-NAME@MACHINE-NAME:~/Android$ make snod
這樣,打包好的Android系統鏡像文件system.img就包含我們前面創建的Broadcast應用程序了。
再接下來,就是運行模擬器來運行我們的例子了。關于如何在Android源代碼工程中運行模擬器,請參考
在Ubuntu上下載、編譯和安裝Android最新源代碼一文。
執行以下命令啟動模擬器:
USER-NAME@MACHINE-NAME:~/Android$ emulator
模擬器啟動起,就可以App Launcher中找到Broadcast應用程序圖標,接著把它啟動起來,然后點擊界面上的Start Counter按鈕,就可以把計數器服務啟動起來了,計數器服務又通過廣播把計數值反饋給MainActivity,于是,我們就會在MainActivity界面看到計數器的值不斷地增加了:

這樣,使用廣播的例子就介紹完了。回顧一下,使用廣播的兩個步驟:
1. 廣播的接收者需要通過調用registerReceiver函數告訴系統,它對什么樣的廣播有興趣,即指定IntentFilter,并且向系統注冊廣播接收器,即指定BroadcastReceiver:
IntentFilter counterActionFilter = new IntentFilter(CounterService.BROADCAST_COUNTER_ACTION);
registerReceiver(counterActionReceiver, counterActionFilter);
這里,指定感興趣的廣播就是CounterService.BROADCAST_COUNTER_ACTION了,而指定的廣播接收器就是counterActonReceiver,它是一個BroadcastReceiver類型的實例。
2. 廣播的發送者通過調用sendBroadcast函數來發送一個指定的廣播,并且可以指定廣播的相關參數:
Intent intent = new Intent(BROADCAST_COUNTER_ACTION);
intent.putExtra(COUNTER_VALUE, counter);
sendBroadcast(intent)
這里,指定的廣播為CounterService.BROADCAST_COUNTER_ACTION,并且附帶的帶參數當前的計數器值counter。調用了sendBroadcast函數之后,所有注冊了CounterService.BROADCAST_COUNTER_ACTION廣播的接收者便可以收到這個廣播了。
在第1步中,廣播的接收者把廣播接收器注冊到ActivityManagerService中;在第2步中,廣播的發送者同樣是把廣播發送到ActivityManagerService中,由ActivityManagerService去查找注冊了這個廣播的接收者,然后把廣播分發給它們。
在第2步的分發的過程,其實就是把這個廣播轉換成一個消息,然后放入到接收器所在的線程消息隊列中去,最后就可以在消息循環中調用接收器的onReceive函數了。這里有一個要非常注意的地方是,由于ActivityManagerService把這個廣播放進接收器所在的線程消息隊列后,就返回了,它不關心這個消息什么時候會被處理,因此,對廣播的處理是異步的,即調用sendBroadcast時,這個函數不會等待這個廣播被處理完后才返回。
下面,我們以一個序列圖來總結一下,廣播的注冊和發送的過程:

虛線上面Step 1到Step 4步是注冊廣播接收器的過程,其中Step 2通過LoadedApk.getReceiverDispatcher在LoadedApk內部創建了一個IIntentReceiver接口,并且傳遞給ActivityManagerService;虛線下面的Step 5到Step 11是發送廣播的過程,在Step 8中,ActivityManagerService利用上面得到的IIntentReceiver遠程接口,調用LoadedApk.performReceiver接口,LoadedApk.performReceiver接口通過ActivityThread.H接口的post函數將這個廣播消息放入到ActivityThread的消息隊列中去,最后這個消息在LoadedApk的Args.run函數中處理,LoadedApk.Args.run函數接著調用MainActivity.BroadcastReceiver的onReceive函數來最終處理這個廣播。
文章開始的時候,我們提到,舉這個例子的最終目的,是為了進一步學習Android系統的廣播機制,因此,在接下來的兩篇文章中,我們將詳細描述上述注冊廣播接收器和發送廣播的過程:
1. Android應用程序注冊廣播接收器(registerReceiver)的過程分析;
2. Android應用程序發送廣播(sendBroadcast)的過程分析。
相信學習完這兩篇文章后,能夠加深對Android系統廣播機制的了解,敬請關注。
作者:Luoshengyang 發表于2011-8-31 1:12:32
原文鏈接
posted on 2012-04-17 21:32
mixer-a 閱讀(1248)
評論(0) 編輯 收藏