本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了,
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以后閱讀和查閱,最后,感激感激郭霖先生提供這么好的書籍,
第5章 全域大喇叭——詳解廣播機制
如果你了解網路通信原理應該會知道,在一個IP網路范圍中,最大的IP地址是被保留作為廣播地址來使用的,
比如某個網路的IP范圍是192.168.0.XXX,子網掩碼是255.255.255.0,那么這個網路的廣播地址就是192.168.0.255,廣播資料包會被發送到同一網路上的所有埠,這樣在該網路中的每臺主機都將會收到這條廣播,為了便于進行系統級別的訊息通知,Android也引入了一套類似的廣播訊息機制,
5.1 廣播機制簡介
為什么說Android中的廣播機制更加靈活呢?
- 注冊和接收自己感興趣的廣播
Android中的每個應用程式都可以對自己感興趣的廣播進行注冊,這樣該程式就只會接收到自己所關心的廣播內容,這些廣播可能是來自于系統的,也可能是來自于其他應用程式的,
- 完整API
Android提供了一套完整的API,允許應用程式自由地發送和接收廣播,
- 發送/接收廣播的方法
發送廣播的方法,就是借助之前稍微提到過學過的Intent,而接收廣播的方法則需要引入一個新的概念——廣播接收器(Broadcast Receiver),
Android中的廣播主要可以分為兩種型別:標準廣播和有序廣播,
- 標準廣播(Normal broadcasts)
是一種完全異步執行的廣播,在廣播發出之后,所有的廣播接收器幾乎都會在同一時刻接收到這條廣播訊息,因此它們之間沒有任何先后順序可言,這種廣播的效率會比較高,但同時也意味著它是無法被截斷的,標準廣播的作業流程如圖:

- 有序廣播
是一種同步執行的廣播,在廣播發出之后,同一時刻只會有一個廣播接收器能夠收到這條廣播訊息,當這個廣播接收器中的邏輯執行完畢后,廣播才會繼續傳遞,
所以此時的廣播接收器是有先后順序的,優先級高的廣播接收器就可以先收到廣播訊息,并且前面的廣播接收器還可以截斷正在傳遞的廣播,這樣后面的廣播接收器就無法收到廣播訊息了,有序廣播的作業流程如圖:

5.2 接收系統廣播
Android內置了很多系統級別的廣播,我們可以在應用程式中通過監聽這些廣播來得到各種系統的狀態資訊,比如手機開機完成后會發出一條廣播,電池的電量發生變化會發出一條廣播,時間或時區發生改變也會發出一條廣播,等等,如果想要接收到這些廣播,就需要使用廣播接收器,下面我們就來看一下它的具體用法,
5.2.1 動態注冊監聽網路變化
廣播接收器可以自由地對自己感興趣的廣播進行注冊,這樣當有相應的廣播發出時,廣播接收器就能夠收到該廣播,并在內部處理相應的邏輯,
注冊廣播的方式一般有兩種,在代碼中注冊和在AndroidManifest.xml中注冊,其中前者也被稱為動態注冊,后者也被稱為靜態注冊,
那么該如何創建一個廣播接收器呢?
其實只需要新建一個類,讓它繼承自BroadcastReceiver,并重寫父類的onReceive()方法就行了,這樣當有廣播到來時,onReceive()方法就會得到執行,具體的邏輯就可以在這個方法中處理,
那我們就先通過動態注冊的方式撰寫一個能夠監聽網路變化的程式,借此學習一下廣播接收器的基本用法吧,新建一個BroadcastTest專案,然后修改MainActivity中的代碼,如下所示:
package com.zhouzhou.broadcasttest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.Toast;
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(context,"network changes",Toast.LENGTH_SHORT).show();
}
}
}
- 在MainActivity中定義了一個內部類NetworkChangeReceiver,
這個類是繼承自BroadcastReceiver的,并重寫了父類的onReceive()方法,這樣每當網路狀態發生變化時,onReceive()方法就會得到執行,這里只是簡單地使用Toast提示了一段文本資訊,
- onCreate()方法,首先創建了一個IntentFilter的實體,并給它添加了一個值為android.net.conn.CONNECTIVITY_CHANGE的action,
為什么要添加這個值呢?因為當網路狀態發生變化時,系統發出的正是一條值為android.net.conn.CONNECTIVITY_CHANGE的廣播,也就是說我們的廣播接收器想要監聽什么廣播,就在這里添加相應的action,
- 創建了一個NetworkChangeReceiver的實體,然后呼叫registerReceiver()方法進行注冊,將NetworkChangeReceiver的實體和IntentFilter的實體都傳了進去,
這樣NetworkChangeReceiver就會收到所有值為android.net.conn.CONNECTIVITY_CHANGE的廣播,也就實作了監聽網路變化的功能,
- 動態注冊的廣播接收器一定都要取消注冊才行,
這里我們是在onDestroy()方法中通過呼叫unregisterReceiver()方法來實作的,
整體來說,代碼還是非常簡單的,現在運行一下程式,首先你會在注冊完成的時候收到一條廣播,圖示如下:

然后按下Home鍵回到主界面(注意不能按Back鍵,否則onDestroy()方法會執行),接著打開Settings程式→Data usage進入到資料使用詳情界面,然后嘗試著開關Cellular data按鈕來啟動和禁用網路,你就會看到有Toast提醒你網路發生了變化,圖示如下:

不過,只是提醒網路發生了變化還不夠人性化,最好是能準確地告訴用戶當前是有網路還是沒有網路,因此我們還需要對上面的代碼進行進一步的優化,修改MainActivity中的代碼,如下所示:
package com.zhouzhou.broadcasttest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.widget.Toast;
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) {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
//下面一行會爆紅,點擊紅色小燈泡第一行就行,意思是在AndroidManifest.xml檔案中添加:
//<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isAvailable()){
Toast.makeText(context,"network is available",Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(context,"network is unavailable",Toast.LENGTH_SHORT).show();
}
}
}
}
在onReceive()方法中,首先通過getSystemService()方法得到了ConnectivityManager的實體,這是一個系統服務類,專門用于管理網路連接的,然后呼叫它的getActiveNetworkInfo()方法可以得到NetworkInfo的實體,接著呼叫NetworkInfo的isAvailable()方法,就可以判斷出當前是否有網路了,最后我們還是通過Toast的方式對用戶進行提示,
另外,這里有非常重要的一點需要說明,Android系統為了保護用戶設備的安全和隱私,做了嚴格的規定:如果程式需要進行一些對用戶來說比較敏感的操作,就必須在組態檔中宣告權限才可以,否則程式將會直接崩潰,比如這里訪問系統的網路狀態就是需要宣告權限的,打開AndroidManifest.xml檔案,在里面加入如下權限就可以訪問系統網路狀態了:(就是上面代碼中爆紅的位置的說明)
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
其實Android中有許多操作都是需要宣告權限才可以進行的,后面我們還會不斷使用新的權限,
不過目前這個訪問系統網路狀態的權限還是比較簡單的,只需要在AndroidManifest.xml檔案中宣告一下就可以了,而Android 6.0系統中引入了更加嚴格的運行時權限,從而能夠更好地保證用戶設備的安全和隱私,關于這部分內容我們將在第7章中學習,
現在重新運行程式,然后按下Home鍵→Settings→Data usage,進入到資料使用詳情界面,關閉Cellular data會彈出無網路可用的提示,如圖:

然后重新打開Cellular data又會彈出網路可用的提示,
5.2.2 靜態注冊實作開機啟動
動態注冊的廣播接收器可以自由地控制注冊與注銷,在靈活性方面有很大的優勢,但是它也存在著一個缺點,即必須要在程式啟動之后才能接收到廣播,因為注冊的邏輯是寫在onCreate()方法中的,
那么有沒有什么辦法可以讓程式在未啟動的情況下就能接收到廣播呢?這就需要使用靜態注冊的方式了,
這里我們準備讓程式接收一條開機廣播,當收到這條廣播時就可以在onReceive()方法里執行相應的邏輯,從而實作開機啟動的功能,可以使用Android Studio提供的快捷方式來創建一個廣播接收器,右擊com.zhouzhou.broadcasttest包→New→Other→BroadcastReceiver,會彈出如圖所示的視窗:


可以看到,這里將廣播接收器命名為BootCompleteReceiver, Exported屬性表示是否允許這個廣播接收器接收本程式以外的廣播,Enabled屬性表示是否啟用這個廣播接收器,勾選這兩個屬性,點擊Finish完成創建,
然后修改BootCompleteReceiver中的代碼,如下所示:
package com.zhouzhou.broadcasttest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class BootCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO: This method is called when the BroadcastReceiver is receiving
// an Intent broadcast.
//throw new UnsupportedOperationException("Not yet implemented");
Toast.makeText(context,"Boot Complete",Toast.LENGTH_LONG).show();
}
}
只是在onReceive()方法中使用Toast彈出一段提示資訊,另外,靜態的廣播接收器一定要在AndroidManifest.xml檔案中注冊才可以使用,不過由于我們是使用Android Studio的快捷方式創建的廣播接收器,因此注冊這一步已經被自動完成了,打開AndroidManifest.xml檔案瞧一瞧,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.broadcasttest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastTest">
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true"></receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

可以看到,<application>標簽內出現了一個新的標簽<receiver>,所有靜態的廣播接收器都是在這里進行注冊的,它的用法其實和<activity>標簽非常相似,也是通過android:name來指定具體注冊哪一個廣播接收器,而enabled和exported屬性則是根據我們剛才勾選的狀態自動生成的,
不過目前BootCompleteReceiver還是不能接收到開機廣播的,我們還需要對AndroidManifest. xml檔案進行修改才行,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.broadcasttest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastTest">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
</application>
</manifest>
現在重新運行程式后,程式就已經可以接收開機廣播了,將模擬器關閉并重新啟動,在啟動完成之后就會收到開機廣播(這里測驗太多次啦!嗚嗚嗚~啟動程式,再將模擬器關閉,是(手機)長按關機鍵,喚出關機按鈕,關機,然后再重新啟動,啟動之后,馬上按Home鍵,就是得趕上沒有完全開機之前,進入到桌面,然后就是等待完全開機,就會跳出Toast:“Boot Complete”啦!):

到目前為止,在廣播接收器的onReceive()方法中都只是簡單地使用Toast提示了一段文本資訊,當你真正在專案中使用到它的時候,就可以在里面撰寫自己的邏輯,
需要注意的是,不要在onReceive()方法中添加過多的邏輯或者進行任何的耗時操作,因為在廣播接收器中是不允許開啟執行緒的,當onReceive()方法運行了較長時間而沒有結束時,程式就會報錯,因此廣播接收器更多的是扮演一種打開程式其他組件的角色,比如創建一條狀態欄通知,或者啟動一個服務等,這幾個概念我們會在后面的章節中學到,
5.3 發送自定義廣播
廣播主要分為兩種型別:標準廣播和有序廣播,在本節中我們就將通過實踐的方式來看一下這兩種廣播具體的區別,
5.3.1 發送標準廣播
在發送廣播之前,還是需要先定義一個廣播接收器來準備接收此廣播才行,不然發出去也是白發,因此新建一個MyBroadcastReceiver,代碼如下所示:
package com.zhouzhou.broadcasttest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO: This method is called when the BroadcastReceiver is receiving
// an Intent broadcast.
//throw new UnsupportedOperationException("Not yet implemented");
Toast.makeText(context,"received in MyBroadcastReceiver",Toast.LENGTH_SHORT).show();
}
}
這里當MyBroadcastReceiver收到自定義的廣播時,就會彈出“received in MyBroadcastReceiver”的提示,然后在AndroidManifest.xml中對這個廣播接收器進行修改:
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.zhouzhou.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
這里讓MyBroadcastReceiver接收一條值為com.zhouzhou.broadcasttest. MY_BROADCAST的廣播,因此待會兒在發送廣播的時候,我們就需要發出這樣的一條廣播,接下來修改activity_main.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_
android:layout_height="wrap_content"
android:text="Send Broadcast"/>
</LinearLayout>
這里在布局檔案中定義了一個按鈕,用于作為發送廣播的觸發點,然后修改MainActivity中的代碼,如下所示:
package com.zhouzhou.broadcasttest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
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);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent("com.zhouzhou.broadcasttest.MY_BROADCAST");
//書中缺少:intent.setComponent("廣播接收器得包的路徑名","廣播接收器的類路徑名")
intent.setComponent(new ComponentName("com.zhouzhou.broadcasttest","com.zhouzhou.broadcasttest.MyBroadcastReceiver"));
sendBroadcast(intent);
}
});
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) {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isAvailable()){
Toast.makeText(context,"network is available",Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(context,"network is unavailable",Toast.LENGTH_SHORT).show();
}
}
}
}
在按鈕的點擊事件里面加入了發送自定義廣播的邏輯,首先構建出了一個Intent物件,并把要發送的廣播的值傳入,然后呼叫了Context的sendBroadcast()方法將廣播發送出去,這樣所有監聽com.zhouzhou.broadcasttest.MY_BROADCAST這條廣播的廣播接收器就會收到訊息,此時發出去的廣播就是一條標準廣播,重新運行程式,并點擊一下Send Broadcast按鈕,效果如圖:

這樣就成功完成了發送自定義廣播的功能,另外,由于廣播是使用Intent進行傳遞的,因此還可以在Intent中攜帶一些資料傳遞給廣播接收器,
5.3.2 發送有序廣播
廣播是一種可以跨行程的通信方式,這一點從前面接收系統廣播的時候就可以看出來了,因此在我們應用程式內發出的廣播,其他的應用程式應該也是可以收到的,
為了驗證這一點,我們需要再新建一個BroadcastTest2專案,點擊Android Studio導航欄→File→New→New Project進行創建,將專案創建好之后,還需要在這個專案下定義一個廣播接收器,用于接收上一小節中的自定義廣播,新建AnotherBroadcastReceiver,代碼如下所示:
package com.zhouzhou.broadcasttest2;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class AnotherBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO: This method is called when the BroadcastReceiver is receiving
// an Intent broadcast.
//throw new UnsupportedOperationException("Not yet implemented");
Toast.makeText(context,"received in AnotherBroadcastReceiver",Toast.LENGTH_SHORT).show();
}
}
這里仍然是在廣播接收器的onReceive()方法中彈出了一段文本資訊,然后在AndroidManifest.xml中對這個廣播接收器進行修改,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.broadcasttest2">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastTest2">
<receiver
android:name=".AnotherBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<!-- 同樣接收的是com.example.broadcasttest.MY_BROADCAST這條廣播 -->
<action android:name="com.zhouzhou.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
可以看到,AnotherBroadcastReceiver同樣接收的是com.zhouzhou.broadcasttest.MY_BROADCAST這條廣播,現在運行BroadcastTest2專案將這個程式安裝到模擬器上,然后重新回到BroadcastTest專案的主界面,并點擊一下Send Broadcast按鈕,就會分別彈出兩次提示資訊,如圖所示:

這樣就強有力地證明了,我們的應用程式發出的廣播是可以被其他的應用程式接收到的,到目前為止,程式里發出的都還是標準廣播,現在嘗試一下發送有序廣播,
重新回到BroadcastTest專案,然后修改MainActivity中的代碼,如下所示:
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);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent("com.zhouzhou.broadcasttest.MY_BROADCAST");
//書中缺少:intent.setComponent("廣播接收器得包的路徑名","廣播接收器的類路徑名")
intent.setComponent(new ComponentName("com.zhouzhou.broadcasttest","com.zhouzhou.broadcasttest.MyBroadcastReceiver"));
//sendBroadcast(intent);
sendOrderedBroadcast(intent,null);
}
});
...
}
...
}
可以看到,發送有序廣播只需要改動一行代碼,即將sendBroadcast()方法改成send-OrderedBroadcast()方法,sendOrderedBroadcast()方法接收兩個引數,第一個引數仍然是Intent,第二個引數是一個與權限相關的字串,這里傳入null就行了,
現在重新運行程式,并點擊Send Broadcast按鈕,發現兩個應用程式仍然都可以接收到這條廣播,看上去好像和標準廣播沒什么區別嘛,不過別忘了,這個時候的廣播接收器是有先后順序的,而且前面的廣播接收器還可以將廣播截斷,以阻止其繼續傳播,
那么該如何設定廣播接收器的先后順序呢?當然是在注冊的時候進行設定的了,修改AndroidManifest.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.broadcasttest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastTest">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<!-- 通過android:priority屬性給廣播接收器設定了優先級,優先級比較高的廣播接收器就可以先收到廣播, -->
<intent-filter android:priority="100">
<action android:name="com.zhouzhou.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
</application>
</manifest>
通過android:priority屬性給廣播接收器設定了優先級,優先級比較高的廣播接收器就可以先收到廣播,這里將MyBroadcastReceiver的優先級設成了100,以保證它一定會在AnotherBroadcastReceiver之前收到廣播,
既然已經獲得了接收廣播的優先權,那么MyBroadcastReceiver就可以選擇是否允許廣播繼續傳遞了,修改MyBroadcastReceiver中的代碼,如下所示:
package com.zhouzhou.broadcasttest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context,"received in MyBroadcastReceiver",Toast.LENGTH_SHORT).show();
abortBroadcast();
}
}
書中講述:“如果在onReceive()方法中呼叫了abortBroadcast()方法,就表示將這條廣播截斷,后面的廣播接收器將無法再接收到這條廣播,現在重新運行程式,并點擊一下Send Broadcast按鈕,你會發現,只有MyBroadcastReceiver中的Toast資訊能夠彈出,說明這條廣播經過MyBroadcastReceiver之后確實是終止傳遞了,”實際上,在4.4以上,abortBroadcast()方法不能實作攔截功能了,上面的測驗,并沒有成功攔截,
5.4 使用本地廣播
前面我們發送和接收的廣播全部屬于系統全域廣播,即發出的廣播可以被其他任何應用程式接收到,并且我們也可以接收來自于其他任何應用程式的廣播,
這樣就很容易引起安全性的問題,比如說我們發送的一些攜帶關鍵性資料的廣播有可能被其他的應用程式截獲,或者其他的程式不停地向我們的廣播接收器里發送各種垃圾廣播,
為了能夠簡單地解決廣播的安全性問題,Android引入了一套本地廣播機制,使用這個機制發出的廣播只能夠在應用程式的內部進行傳遞,并且廣播接收器也只能接收來自本應用程式發出的廣播,這樣所有的安全性問題就都不存在了,
本地廣播的用法并不復雜,主要就是使用了一個LocalBroadcastManager來對廣播進行管理,并提供了發送廣播和注冊廣播接收器的方法,下面我們就通過具體的實體來嘗試一下它的用法,修改MainActivity中的代碼,如下所示:
package com.zhouzhou.broadcasttest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
localBroadcastManager = LocalBroadcastManager.getInstance(this);//獲取實體
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent("com.zhouzhou.broadcasttest.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent);//發送本地廣播
}
});
intentFilter = new IntentFilter();
intentFilter.addAction("com.zhouzhou.broadcasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver,intentFilter);//注冊本地廣播監聽器
}
@Override
protected void onDestroy() {
super.onDestroy();
localBroadcastManager.unregisterReceiver(localReceiver);
}
class LocalReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context,"received local broadcast",Toast.LENGTH_SHORT).show();
}
}
}
實這基本上就和前面所學的動態注冊廣播接收器以及發送廣播的代碼是一樣的,只不過現在首先是通過LocalBroadcastManager的getInstance()方法得到了它的一個實體,然后在注冊廣播接收器的時候呼叫的是LocalBroadcastManager的registerReceiver()方法,在發送廣播的時候呼叫的是LocalBroadcastManager的sendBroadcast()方法,僅此而已,這里我們在按鈕的點擊事件里面發出了一條com.zhouzhou.broadcasttest.LOCAL_BROADCAST廣播,然后在LocalReceiver里去接收這條廣播,重新運行程式,并點擊SendBroadcast按鈕,效果如圖:

可以看到,LocalReceiver成功接收到了這條本地廣播,并通過Toast提示了出來,如果你還有興趣進行實驗,可以嘗試在BroadcastTest2中也去接收com.zhouzhou.broadcasttest.LOCAL_BROADCAST這條廣播,答案是顯而易見的,肯定無法收到,因為這條廣播只會在BroadcastTest程式內傳播,(經過測驗,竟然也能收到!!!)
另外還有一點需要說明,本地廣播是無法通過靜態注冊的方式來接收的,其實這也完全可以理解,因為靜態注冊主要就是為了讓程式在未啟動的情況下也能收到廣播,而發送本地廣播時,我們的程式肯定是已經啟動了,因此也完全不需要使用靜態注冊的功能,
盤點一下使用本地廣播的幾點優勢:
? 可以明確地知道正在發送的廣播不會離開我們的程式,因此不必擔心機密資料泄漏,
? 其他的程式無法將廣播發送到我們程式的內部,因此不需要擔心會有安全漏洞的隱患,
? 發送本地廣播比發送系統全域廣播將會更加高效,
5.5 廣播的最佳實踐——實作強制下線功能
強制下線功能,很多的應用程式都具備這個功能,比如你的QQ號在別處登錄了,就會將你強制擠下線,其實實作強制下線功能的思路也比較簡單,只需要在界面上彈出一個對話框,讓用戶無法進行任何其他操作,必須要點擊對話框中的確定按鈕,然后回到登錄界面即可,
可是這樣就存在著一個問題,因為當我們被通知需要強制下線時可能正處于任何一個界面,難道需要在每個界面上都撰寫一個彈出對話框的邏輯?我們完全可以借助本章中所學的廣播知識,來非常輕松地實作這一功能,
新建一個BroadcastBestPractice專案,然后開始動手吧,強制下線功能需要先關閉掉所有的活動,然后回到登錄界面,先創建一個ActivityCollector類用于管理所有的活動,代碼如下所示:
package com.zhouzhou.broadcastbestpractice;
import android.app.Activity;
import java.util.ArrayList;
import java.util.List;
//ActivityCollector類 用于管理所有的活動
public class ActivityCollector {
public static List<Activity> activities = new ArrayList<>();
public static void addActivity(Activity activity) {
activities.add(activity);
}
public static void removeActivity(Activity activity) {
activities.remove(activity);
}
public static void finishAll() {
for (Activity activity : activities) {
if (! activity.isFinishing()) {
activity.finish();
}
}
activities.clear();
}
}
然后創建BaseActivity類作為所有活動的父類,代碼如下所示:
package com.zhouzhou.broadcastbestpractice;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
//BaseActivity類 作為所有活動的父類
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityCollector.addActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}
創建一個登錄界面的活動,新建LoginActivity,并讓Android Studio幫我們自動生成相應的布局檔案,然后編輯布局檔案activity_login.xml,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_
android:layout_height="60dp">
<TextView
android:layout_
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Account:"/>
<EditText
android:id="@+id/account"
android:layout_
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_
android:layout_height="60dp">
<TextView
android:layout_
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Password"/>
<EditText
android:id="@+id/password"
android:layout_
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:inputType="textPassword"/>
</LinearLayout>
<Button
android:id="@+id/login"
android:layout_
android:layout_height="60dp"
android:text="Login"/>
</LinearLayout>
使用LinearLayout撰寫出了一個登錄布局,最外層是一個縱向的LinearLayout,里面包含了3行直接子元素,第一行是一個橫向LinearLayout,用于輸入賬號資訊;第二行也是一個橫向的LinearLayout,用于輸入密碼資訊;第三行是一個登錄按鈕,接下來修改LoginActivity中的代碼,如下所示:
package com.zhouzhou.broadcastbestpractice;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class LoginActivity extends BaseActivity {
private EditText accountEdit;
private EditText passwordEdit;
private Button login;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
accountEdit = (EditText) findViewById(R.id.account);
passwordEdit = (EditText) findViewById(R.id.password);
login = (Button) findViewById(R.id.login);
login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String account = accountEdit.getText().toString();
String password = passwordEdit.getText().toString();
//如果賬號是admin且密碼是123456,就認為登錄成功
if (account.equals("admin") && password.equals("123456")) {
Intent intent = new Intent(LoginActivity.this,MainActivity.class);
startActivity(intent);
finish();
}else {
Toast.makeText(LoginActivity.this,"account or password is invalid",Toast.LENGTH_SHORT).show();
}
}
});
}
}
模擬了一個非常簡單的登錄功能,首先要將LoginActivity的繼承結構改成繼承自BaseActivity,然后呼叫findViewById()方法分別獲取到賬號輸入框、密碼輸入框以及登錄按鈕的實體,
接著在登錄按鈕的點擊事件里面對輸入的賬號和密碼進行判斷,如果賬號是admin并且密碼是123456,就認為登錄成功并跳轉到MainActivity,否則就提示用戶賬號或密碼錯誤,
因此,你就可以將MainActivity理解成是登錄成功后進入的程式主界面了,這里我們并不需要在主界面里提供什么花哨的功能,只需要加入強制下線功能就可以了,修改activity_main.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_
android:layout_height="match_parent">
<Button
android:id="@+id/force_offline"
android:layout_
android:layout_height="wrap_content"
android:text="Send force offline broadcast"/>
</LinearLayout>
只有一個按鈕而已,用于觸發強制下線功能,然后修改MainActivity中的代碼,如下所示:
package com.zhouzhou.broadcastbestpractice;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button forceOffline = (Button) findViewById(R.id.force_offline);
forceOffline.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent("com.zhouzhou.broadcastbestpractice.FORCE_OFFLINE");
sendBroadcast(intent);
}
});
}
}
這里有個重點,在按鈕的點擊事件里面發送了一條廣播,廣播的值為com.zhouzhou.broadcastbestpractice.FORCE_OFFLINE,這條廣播就是用于通知程式強制用戶下線的,
也就是說強制用戶下線的邏輯并不是寫在MainActivity里的,而是應該寫在接收這條廣播的廣播接收器里面,這樣強制下線的功能就不會依附于任何的界面,不管是在程式的任何地方,只需要發出這樣一條廣播,就可以完成強制下線的操作了,
那么毫無疑問,接下來我們就需要創建一個廣播接收器來接收這條強制下線廣播,唯一的問題就是,應該在哪里創建呢?由于廣播接收器里面需要彈出一個對話框來阻塞用戶的正常操作,但如果創建的是一個靜態注冊的廣播接收器,是沒有辦法在onReceive()方法里彈出對話框這樣的UI控制元件的,而我們顯然也不可能在每個活動中都去注冊一個動態的廣播接收器,那么到底應該怎么辦呢?
答案其實很明顯,只需要在BaseActivity中動態注冊一個廣播接收器就可以了,因為所有的活動都是繼承自BaseActivity的,修改BaseActivity中的代碼,如下所示:
package com.zhouzhou.broadcastbestpractice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
//BaseActivity類 作為所有活動的父類
public class BaseActivity extends AppCompatActivity {
private ForceOfflineReceiver receiver;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityCollector.addActivity(this);
}
@Override
protected void onResume() {
super.onResume();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("com.zhouzhou.broadcastbestpractice.FORCE_OFFLINE");
receiver = new ForceOfflineReceiver();
registerReceiver(receiver,intentFilter);
}
@Override
protected void onPause() {
super.onPause();
if (receiver != null){
unregisterReceiver(receiver);
receiver = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
class ForceOfflineReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("Warning");
builder.setMessage("You are forced to be offline.Please try to login again");
builder.setCancelable(false);
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
ActivityCollector.finishAll();//銷毀所有活動
Intent intent = new Intent(context,LoginActivity.class);
context.startActivity(intent);//重新啟動LoginActivity
}
});
builder.show();
}
}
}
ForceOfflineReceiver中的代碼,這次onReceive()方法里不僅僅彈出一個Toast了,首先肯定是使用AlertDialog.Builder來構建一個對話框,注意這里一定要呼叫setCancelable()方法將對話框設為不可取消,否則用戶按一下Back鍵就可以關閉對話框繼續使用程式了,
然后使用setPositiveButton()方法來給對話框注冊確定按鈕,當用戶點擊了確定按鈕時,就呼叫ActivityCollector的finishAll()方法來銷毀掉所有活動,并重新啟動LoginActivity這個活動,
我們是怎么注冊ForceOfflineReceiver這個廣播接收器的,這里重寫了onResume()和onPause()這兩個生命周期函式,然后分別在這兩個方法里注冊和取消注冊了ForceOfflineReceiver,
那么為什么要這樣寫呢?之前不都是在onCreate()和onDestroy()方法里來注冊和取消注冊廣播接收器的么?這是因為我們始終需要保證只有處于堆疊頂的活動才能接收到這條強制下線廣播,非堆疊頂的活動不應該也沒有必要去接收這條廣播,所以寫在onResume()和onPause()方法里就可以很好地解決這個問題,當一個活動失去堆疊頂位置時就會自動取消廣播接收器的注冊,
這樣的話,所有強制下線的邏輯就已經完成了,接下來我們還需要對AndroidManifest.xml檔案進行修改,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.broadcastbestpractice">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastBestPractice">
<activity android:name=".MainActivity"/>
<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
這里只需要,將主活動設定為LoginActivity而不再是MainActivity,因為你肯定不希望用戶在沒登錄的情況下就能直接進入到程式主界面吧?嘗試運行一下程式,首先會進入到登錄界面,并可以在這里輸入賬號和密碼,如圖:

如果輸入的賬號是admin,密碼是123456,點擊登錄按鈕就會進入到程式的主界面:

這時點擊一下發送廣播的按鈕,就會發出一條強制下線的廣播,ForceOfflineReceiver里收到這條廣播后會彈出一個對話框提示用戶已被強制下線,如圖:

這時用戶將無法再對界面的任何元素進行操作,只能點擊確定按鈕,然后會重新回到登錄界面,這樣,強制下線功能就已經完整地實作了,
5.6 Git時間——初識版本控制工具
Git是一個開源的分布式版本控制工具,它的開發者就是鼎鼎大名的Linux作業系統的作者Linus Torvalds,Git被開發出來的初衷是為了更好地管理Linux內核,而現在卻早已被廣泛應用于全球各種大中小型的專案中,
5.6.1 安裝Git
由于Git和Linux作業系統都是同一個作者,Git在Linux上的安裝是最簡單方便的,比如使用的是Ubuntu系統,只需要打開shell界面,并輸入:
sudo apt-get install git-core
按下回車后輸入密碼,即可完成Git的安裝,
使用Windows作業系統,如何在Windows上安裝Git,不同于Linux, Windows上可無法通過一行命令就完成安裝了,需要先把Git的安裝包下載下來,訪問網址https://git-for-windows.github.io/,可以看到如圖頁面:

目前最新的git for windows版本是2.35.1.12,如果你下載的時候發現又有新的版本,可以嘗試一下最新版本的Git,點擊Download按鈕可以開始下載,下載完成后雙擊安裝包進行安裝,之后一直點擊“下一步”就可以完成安裝了,
5.6.2 創建代碼倉庫
雖然在Windows上安裝的Git是可以在圖形界面上進行操作的,并且AndroidStudio也支持以圖形化的形式操作Git,但是這里并不建議你這樣做,因為Git的各種命令才是你應該掌握的核心技能,不管你是在哪個作業系統中,使用命令來操作Git肯定都是通用的,
而圖形化的操作應該是在你能熟練掌握命令用法的前提下,進一步提升你作業效率的手段,那么我們現在就來嘗試一下如何通過命令來使用Git,如果你使用的是Linux系統,就先打開shell界面,如果使用的是Windows系統,就從開始里找到GitBash并打開,
首先應該配置一下你的身份,這樣在提交代碼的時候Git就可以知道是誰提交的了,命令如下所示:
$ git config --global user.name "Zhouzhou"
$ git config --global user.email "[email protected]"
配置完成后,還可以使用同樣的命令來查看是否配置成功,只需要將最后的名字和郵箱地址去掉即可,
$ git config --global user.name
$ git config --global user.email
如圖:

然后,就可以開始創建代碼倉庫了,倉庫(Repository)是用于保存版本管理所需資訊的地方,所有本地提交的代碼都會被提交到代碼倉庫中,如果有需要還可以再推送到遠程倉庫中,
這里我們嘗試著給BroadcastBestPractice專案建立一個代碼倉庫,先進入到BroadcastBestPractice專案的目錄下面,如圖:

然后在這個目錄下面輸入如下命令:
git init
只需要一行命令就可以完成創建代碼倉庫的操作,如圖所示:

倉庫創建完成后,會在BroadcastBestPractice專案的根目錄下生成一個隱藏的.git檔案夾,這個檔案夾就是用來記錄本地所有的Git操作的,可以通過ls-al命令來查看一下,如圖所示:

5.6.3 提交本地代碼
代碼倉庫建立完之后就可以提交代碼了,只需要使用add和commit命令就可以了,add用于把想要提交的代碼先添加進來,而commit則是真正地去執行提交操作,比如我們想添加build.gradle檔案,就可以輸入如下命令:
git add build.gradle

這是添加單個檔案的方法,想添加某個目錄,只需要在add后面加上目錄名就可以了,比如將整個app目錄下的所有檔案都進行添加,就可以輸入如下命令:
git add app

可是這樣一個個地添加感徑訓是有些復雜,一次性就把所有的檔案都添加好:只需要在add的后面加上一個點,就表示添加所有的檔案了,命令如下所示:
git add .

現在BroadcastBestPractice專案下所有的檔案都已經添加好了,我們可以來提交一下了,輸入如下命令:
git commit -m "First commit."

注意,在commit命令的后面,一定要通過-m引數來加上提交的描述資訊,沒有描述資訊的提交被認為是不合法的,這樣所有的代碼就已經成功提交了,關于Git的內容,今天先學到這里,
個人學習筆記,針對本人在自學中遇到的問題,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/472333.html
標籤:Android
