
第5章 全局大喇叭-詳解廣播機(jī)制
了解網(wǎng)絡(luò)通信原理的應(yīng)該會(huì)知道,在一個(gè)IP網(wǎng)絡(luò)范圍中,最大的IP地址是被保留作為廣播地址來(lái)使用的。比如某個(gè)網(wǎng)絡(luò)的IP范圍是192.168.0.XXX,子網(wǎng)掩碼是255.255.255.0,那么這個(gè)網(wǎng)絡(luò)的廣播地址就是192.168.0.255。廣播數(shù)據(jù)包會(huì)被發(fā)送到同一網(wǎng)絡(luò)上的所有端口,這樣在該網(wǎng)絡(luò)中的每臺(tái)主機(jī)都將會(huì)收到這條廣播。
1.廣播機(jī)制簡(jiǎn)介
Android中的每個(gè)應(yīng)用程序都可以對(duì)自己感興趣的廣播進(jìn)行注冊(cè),這樣該程序就只會(huì)接收到自己所關(guān)心的廣播內(nèi)容,這些廣播可能是來(lái)自于系統(tǒng)的,也可能是來(lái)自于其他應(yīng)用程序的。Android提供了一套完整的API,允許應(yīng)用程序自由的發(fā)送和接收廣播。
標(biāo)準(zhǔn)廣播
是一種完全異步執(zhí)行的廣播,在廣播發(fā)出之后,所有的廣播接收器幾乎都會(huì)在同一時(shí)刻接收到這條廣播消息,因此它們之間沒(méi)有任何先后順序可言。這種廣播的效率會(huì)比較高,但同時(shí)也意味著它是無(wú)法被截?cái)嗟摹?/p>有序廣播
則是一種同步執(zhí)行的廣播,在廣播發(fā)出之后,同一時(shí)刻只會(huì)有一個(gè)廣播接收器能夠收到這條廣播消息,當(dāng)這個(gè)廣播接收器中的邏輯執(zhí)行完畢后,廣播才會(huì)繼續(xù)傳遞。所以此時(shí)的廣播接收器是有先后順序的,優(yōu)先級(jí)高的廣播接收器就可以先收到廣播消息,并且前面的廣播接收器還可以截?cái)嗾趥鬟f的廣播,這樣后面的廣播接收器就無(wú)法收到廣播消息了。
2.動(dòng)態(tài)注冊(cè)監(jiān)聽(tīng)網(wǎng)絡(luò)變化
注冊(cè)廣播的方式一般有兩種,在代碼中注冊(cè)和在AndroidManifest.xml中注冊(cè),其中前者被稱為動(dòng)態(tài)注冊(cè),后者被稱為靜態(tài)注冊(cè)。
創(chuàng)建一個(gè)廣播接收器,需要新建一個(gè)類,讓它繼承自BroadcastReceiver,并重寫父類的onReceive()方法就可以了。
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private NetworkChangeReceiver networkChangeReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
networkChangeReceiver = new NetworkChangeReceiver();
registerReceiver(networkChangeReceiver,intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(networkChangeReceiver);
}
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(MainActivity.this, "network changes", Toast.LENGTH_SHORT).show();
}
}
}
在MainActivity中定義了一個(gè)內(nèi)部類NetworkChangeReceiver,這個(gè)類是繼承自BroadcastReceiver的,并重寫了父類的onReceive()方法。
在onCreate()方法中,首先我們創(chuàng)建了一個(gè)IntentFilter的實(shí)例,并給它添加一個(gè)值為android.net.conn.CONNECTIVITY_CHANGE的action,我們的廣播接收器想要監(jiān)聽(tīng)什么廣播,就在這里添加相應(yīng)的action,接下來(lái)創(chuàng)建了一個(gè)NetworkChangeReceiver的實(shí)例,然后調(diào)用registerReceiver()方法進(jìn)行注冊(cè),將NetworkChangeReceiver的實(shí)例和IntentFilter的實(shí)例都傳了進(jìn)去,這樣NetworkChangeReceiver就會(huì)收到所有值為android.net.conn.CONNECTIVITY_CHANGE的廣播。
動(dòng)態(tài)注冊(cè)的廣播接收器一定都要取消注冊(cè)才行,這里我們是在onDestroy()方法中通過(guò)調(diào)用unregisterReceiver()方法來(lái)實(shí)現(xiàn)的。
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//Toast.makeText(MainActivity.this, "network changes", Toast.LENGTH_SHORT).show();
ConnectivityManager connectionManager = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectionManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isAvailable()) {
Toast.makeText(MainActivity.this, "network is available", Toast.LENGTH_SHORT).show();
}
else {
Toast.makeText(MainActivity.this, "network is unavailable", Toast.LENGTH_SHORT).show();
}
}
}
首先通過(guò)getSystemService()方法得到了ConnectivityManager的實(shí)例,這是一個(gè)系統(tǒng)服務(wù)類,專門用于管理網(wǎng)絡(luò)連接的。然后調(diào)用它的getActiveNetworkInfo()方法可以得到NetworkInfo的實(shí)例,接著調(diào)用NetworkInfo的isAvailable()方法,就可以判斷出當(dāng)前是否有網(wǎng)絡(luò)了。
Android系統(tǒng)為了保護(hù)用戶設(shè)備的安全和隱私,做了嚴(yán)格的規(guī)定:如果程序需要進(jìn)行一些對(duì)用戶來(lái)說(shuō)比較敏感的操作,就必須在配置文件中聲明權(quán)限才可以,否則程序?qū)?huì)直接崩潰。
3.靜態(tài)注冊(cè)實(shí)現(xiàn)開(kāi)機(jī)啟動(dòng)
動(dòng)態(tài)注冊(cè)的廣播接收器可以自由地控制注冊(cè)和注銷,在靈活性方面有很大的優(yōu)勢(shì),但是它也存在著一個(gè)缺點(diǎn),即必須要在程序啟動(dòng)之后才能接收到廣播,因?yàn)樽?cè)的邏輯是寫在onCreate()方法中的,讓程序在未啟動(dòng)的情況下就能接收到廣播,需要使用靜態(tài)注冊(cè)的方式
-
Exported:表示是否允許這個(gè)廣播接收器接收本程序以外的廣播 -
Enabled:表示是否啟用這個(gè)廣播接收器
靜態(tài)的廣播接收器一定要在AndroidManifest.xml文件中注冊(cè)才可以使用。
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
</receiver>
<application>標(biāo)簽內(nèi)出現(xiàn)了一個(gè)新的標(biāo)簽<receiver>,所有靜態(tài)的廣播接收器都是在這里進(jìn)行注冊(cè)的,它的用法其實(shí)和<activity>標(biāo)簽非常相似,也是通過(guò)android:name來(lái)指定具體注冊(cè)哪一個(gè)廣播接收器。
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
Android系統(tǒng)啟動(dòng)完成后會(huì)發(fā)出一道值為android.intent.action.BOOT_COMPLETED的廣播,因此我們?cè)?code><intent-filter>標(biāo)簽里添加了相應(yīng)的action。另外,監(jiān)聽(tīng)系統(tǒng)開(kāi)機(jī)廣播也是需要聲明系統(tǒng)權(quán)限的,我們使用<uses-permission>標(biāo)簽又加入了一條android.permission.RECEIVE_BOOT_COMPLETED權(quán)限。
需要注意的是,不要再onReceiver()方法中添加過(guò)多的邏輯或者進(jìn)行任何的耗時(shí)操作,因?yàn)樵趶V播接收器中是不允許開(kāi)啟線程的,當(dāng)onReceive()方法運(yùn)行了較長(zhǎng)時(shí)間而沒(méi)有結(jié)束時(shí),程序就會(huì)報(bào)錯(cuò),因此廣播接收器更多的是扮演一種打開(kāi)程序其他組件的角色,比如創(chuàng)建一條狀態(tài)欄通知,或者啟動(dòng)一個(gè)服務(wù)等。
4.發(fā)送自定義廣播
// 1.
public void initView() {
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v){
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);
}
});
}
// 2.
<receiver
android:name=".MyBroadcastReceiver"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
首先構(gòu)建出了一個(gè)Intent對(duì)象,并把要發(fā)送的廣播的值傳入,然后調(diào)用了Context的sendBroadcast()方法將廣播發(fā)送出去,這樣所有監(jiān)聽(tīng)com.example.broadcasttest.MY_BROADCAST這條廣播的廣播接收器就會(huì)收到消息。
廣播是使用Intent進(jìn)行傳遞的,因此你還可以在Intent中攜帶一些數(shù)據(jù)傳遞給廣播接收器。
發(fā)送有序廣播
廣播是一種可以跨進(jìn)程的通信方式,我們應(yīng)用程序發(fā)出的廣播,其他的應(yīng)用程序也是可以收到的。
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendOrderedBroadcast(intent,null);
}
});
將sendBroadcast()方法改成sendOrderedBroadcast()方法。sendOrderedBroadcast()方法接收兩個(gè)參數(shù),第一個(gè)參數(shù)仍然是Intent,第二個(gè)參數(shù)是一個(gè)與權(quán)限相關(guān)的的字符串,這里傳入null就行了。
<receiver
android:name=".AnotherBroadcastReceiver"
android:exported="true"
android:enabled="true">
<intent-filter
android:priority="100">
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
通過(guò)android:priority屬性給廣播接收器設(shè)置了優(yōu)先級(jí),優(yōu)先級(jí)比較高的廣播接收器就可以先收到廣播。
public class AnotherBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show();
abortBroadcast();
}
}
onReceiver()方法中調(diào)用了abortBroadcast()方法,就表示將這條廣播截?cái)?,后面的廣播接收器將無(wú)法在接收到這條廣播。
5.使用本地廣播
前面我們發(fā)送和接收的廣播全部屬于系統(tǒng)全局廣播,即發(fā)出的廣播可以被其他任何應(yīng)用程序接收到,并且我們也可以接收到來(lái)自于其他任何應(yīng)用程序的廣播。
為了能夠簡(jiǎn)單地解決廣播的安全性問(wèn)題,Android引入了一套本地廣播機(jī)制,使用這個(gè)機(jī)制發(fā)出的廣播只能在應(yīng)用程序的內(nèi)部進(jìn)行傳遞,并且廣播接收器也只能接收來(lái)自本應(yīng)用程序發(fā)出的廣播。
本地廣播主要是使用了一個(gè)LocalBroadcastManager來(lái)對(duì)廣播進(jìn)行管理。
public class MainActivity extends AppCompatActivity
{
private IntentFilter intentFilter;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
localBroadcastManager = LocalBroadcastManager.getInstance(this);
button = (Button) findViewById(R.id.button);
initEvent();
intentFilter = new IntentFilter();
intentFilter.addAction("com.example.broadcast.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver,intentFilter);
}
@Override
protected void onDestroy()
{
super.onDestroy();
localBroadcastManager.unregisterReceiver(localReceiver);
}
public void initEvent()
{
button.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
Intent intent = new Intent("com.example.broadcast.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent);
}
});
}
class LocalReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
Toast.makeText(context, "received in local broadcast", Toast.LENGTH_SHORT).show();
}
}
}
首先是通過(guò)LocalBroadcastManager的getInstance()方法得到了一個(gè)他的實(shí)例,然后在注冊(cè)廣播接收器的時(shí)候調(diào)用的是LocalBroadcastManager的sendBroadcast()方法。
另外還有一點(diǎn)需要說(shuō)明,本地廣播是無(wú)法通過(guò)靜態(tài)注冊(cè)的方式來(lái)接收的,其實(shí)這也完全可以理解,因?yàn)殪o態(tài)注冊(cè)主要就是為了讓程序在未啟動(dòng)的時(shí)候,也能收到廣播,而發(fā)送本地廣播時(shí),我們的程序肯定是已經(jīng)啟動(dòng)了,因此也完全不需要使用靜態(tài)注冊(cè)的功能。
**優(yōu)勢(shì): **
1.可以明確地知道正在發(fā)送的廣播不會(huì)離開(kāi)我們的程序,因此不必?fù)?dān)心機(jī)密數(shù)據(jù)泄露。
2.其他的程序無(wú)法將廣播發(fā)送到我們程序的內(nèi)部,因此不需要擔(dān)心會(huì)有安全漏洞的隱患。
3.發(fā)送本地廣播比發(fā)送系統(tǒng)全局廣播將會(huì)更加高效。
6.廣播的最佳實(shí)踐-實(shí)現(xiàn)強(qiáng)制下線功能
實(shí)現(xiàn)強(qiáng)制下線功能的思路比較簡(jiǎn)單,只需要在界面上彈出一個(gè)對(duì)話框,讓用戶無(wú)法進(jìn)行其他任何操作,必須要點(diǎn)擊對(duì)話框中的確定按鈕,然后回到登錄界面即可。
強(qiáng)制下線功能需要先關(guān)閉掉所有的活動(dòng),然后回到登錄界面。
public class ActivtyCollector
{
public static List<Activity> activityList = new ArrayList<>();
public static void addActivity(Activity activity)
{
activityList.add(activity);
}
public static void removeActivity(Activity activity)
{
activityList.remove(activity);
}
public static void finishAll()
{
for (Activity activity : activityList)
{
if (!activity.isFinishing())
{
activity.finish();
}
}
}
}
Activity在Destroy之前,activity.isFinishing返回false,Activityon在Destroy之后,返回true
public class LoginActivity extends BaseActivity
{
private Button button;
private EditText editText_Account,editText_Password;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
initView();
initEvent();
}
public void initView()
{
button = (Button) findViewById(R.id.button_login);
editText_Account = (EditText) findViewById(R.id.edit_account);
editText_Password = (EditText) findViewById(R.id.edit_password);
}
public void initEvent()
{
button.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
String account = editText_Account.getText().toString();
String password = editText_Password.getText().toString();
if (account.equals("admin") && password.equals("123456"))
{
Intent intent = new Intent(LoginActivity.this,MainActivity.class);
startActivity(intent);
finish();
}
else
{
Toast.makeText(LoginActivity.this, "輸入的賬號(hào)或密碼有誤?。?!", Toast.LENGTH_SHORT).show();
}
}
});
}
}
public class MainActivity extends BaseActivity
{
private Button button;
public static String Tag = "com.example.broadcastbestpractice_FORCE_OFFLINE";
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.button_send);
initEvent();
}
public void initEvent()
{
button.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
Intent intent = new Intent(Tag);
sendBroadcast(intent);
}
});
}
}
MainActivity只是發(fā)送了一個(gè)標(biāo)準(zhǔn)廣播。
強(qiáng)制用戶下線的邏輯并不是寫在MainActivity里的,而是應(yīng)該寫在接收這條廣播的廣播接收器里面,這樣強(qiáng)制下線的功能就不會(huì)依附于任何的界面,不管是在程序的任何地方,只需要發(fā)出一條這樣的廣播,就可以完成強(qiáng)制下線的操作了。
注冊(cè)的靜態(tài)的廣播接收器,是沒(méi)有辦法在onReceive()方法里彈出對(duì)話框這樣的UI控件的,而我們顯然也不可能在每個(gè)活動(dòng)中都去注冊(cè)一個(gè)動(dòng)態(tài)的廣播接收器。
public class BaseActivity extends AppCompatActivity
{
private IntentFilter intentFilter;
private ForceOfflineReceiver forceOfflineReceiver;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
ActivtyCollector.addActivity(this);
}
@Override
protected void onDestroy()
{
super.onDestroy();
ActivtyCollector.removeActivity(this);
}
@Override
protected void onResume()
{
super.onResume();
intentFilter = new IntentFilter();
intentFilter.addAction(MainActivity.Tag);
forceOfflineReceiver = new ForceOfflineReceiver();
registerReceiver(forceOfflineReceiver,intentFilter);
}
@Override
protected void onPause()
{
super.onPause();
unregisterReceiver(forceOfflineReceiver);
}
class ForceOfflineReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, final Intent intent)
{
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("警告!");
builder.setCancelable(false);
builder.setMessage("賬號(hào)在別處登錄,你被迫下線,請(qǐng)重新登錄");
builder.setPositiveButton("確定", new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
ActivtyCollector.finishAll();
Intent intent1 = new Intent(BaseActivity.this,LoginActivity.class);
startActivity(intent1);
}
});
builder.show();
}
}
}
注意這里一定要調(diào)用builder.setCancelable(false)將對(duì)話框設(shè)為不可取消。
我們始終需要保證只有處于棧頂?shù)幕顒?dòng)才能接收到這條強(qiáng)制下線廣播,非棧頂?shù)幕顒?dòng)不應(yīng)該也沒(méi)有必要去接收這條廣播,所以寫在onResume()和onPause()方法里就可以很好的解決這個(gè)問(wèn)題,當(dāng)一個(gè)活動(dòng)失去棧頂位置時(shí)就會(huì)自動(dòng)取消廣播接收器的注冊(cè)。