本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。 每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,感激 ...
本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼: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_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_width="match_parent"
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_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Account:"/>
<EditText
android:id="@+id/account"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Password"/>
<EditText
android:id="@+id/password"
android:layout_width="0dp"
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_width="match_parent"
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_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/force_offline"
android:layout_width="match_parent"
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的內容,今天先學到這裡。