Android 學習記錄三:廣播

廣播機制簡介

Android中的廣播機制更加靈活,因為Android中的每個應用程序都可以對自己感興趣的廣播進行注冊,這樣該程序就只會接收到自己所關(guān)心的廣播內(nèi)容,這些廣播可能是來自系統(tǒng)的,也可能是來自于其他應用程序的。Android提供了一套完整的API,允許應用程序自由地發(fā)送或者接受廣播。發(fā)送廣播的方法其實之前有稍微提到過有一下,就是借助第二章的Intent。接受廣播需要用到一個新的工具,廣播接收器。
Android中的廣播可以分為兩類,標準廣播和有序廣播。

標準廣播

標準廣播是一種完全異步執(zhí)行的廣播。在廣播發(fā)出之后,所有的廣播接收器幾乎都會在同一時刻接收到這條廣播信息 ,因此它們之間沒有任何先后順序可言。這種廣播效率較高,但是同時也意味著它是無法被階段的。

有序廣播

有序廣播是一種同步執(zhí)行的廣播。在廣播發(fā)出之后,同一時刻只有一個廣播接收器能夠收到這條廣播消息。當這個廣播接收器中的邏輯執(zhí)行完畢之后廣播才會繼續(xù)傳遞。優(yōu)先級高的廣播接收器就可以先收到廣播消息,并且前面的廣播接收器還可以截斷正在傳遞的廣播。

接收系統(tǒng)廣播

Android內(nèi)置了很多系統(tǒng)級別的廣播,我們可以在應用程序張通過監(jiān)聽這些廣播來得到各種系統(tǒng)的狀態(tài)信息。比如手機開機、電池電量發(fā)生變化、時間或者時區(qū)發(fā)生改變等等。如果想要接收到這些廣播就需要使用廣播接收器。

動態(tài)注冊監(jiān)聽網(wǎng)絡變化

廣播接收器可以自由地對自己感興趣的廣播進行豬兒,這樣當有相應的廣播發(fā)出時,廣播接收器就能夠收到該廣播,并且在內(nèi)部處理相應的邏輯。
注冊廣播的方式一般也有兩種,在代碼中注冊或者在AndroidManifest.xml中注冊,其中前者也被稱為動態(tài)注冊,后者就是靜態(tài)注冊。
如何創(chuàng)建呢?其實只需要新建一個類,讓它繼承自BroadcastReceiver,并且重寫父類的onReceive方法就行了。這樣當有廣播到來時,onReceive方法就會得到執(zhí)行,具體的邏輯就可以在這個方法中處理。

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();
        networkChangeReceiver = new NetworkChangeReceiver();
        intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
        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中定義了一個內(nèi)部類NetworkChangeRectiver,這個類繼承自BroadcastReceiver,重寫了父類的onReceive方法,這樣每當網(wǎng)絡狀態(tài)發(fā)生變化,onReceive方法就會得到執(zhí)行。
觀察一下onCreate方法,我們創(chuàng)建了一個IntentFilter實例,并給他添加了一個值為android.net.conn.CONNECTIVITY_CHANGEaction。當系統(tǒng)網(wǎng)絡狀態(tài)發(fā)生變化是,系統(tǒng)發(fā)出的正是一條值為android.net.conn.CONNECTIVITY_CHANGE的廣播。我們的廣播接收器想要監(jiān)聽什么廣播,就在這里添加相應的action即可。接下來創(chuàng)建了一個NetworkChangeReceiver的實例,然后調(diào)用registerReceiver方法進行注冊,將NetworkChangeReceiver的實例和IntentFilter的實例都傳了進去,這樣NetworkChangeRecevier就會受到所有值為android.net.conn.CONNECTIVITY_CHANGE的廣播。
最后記得,動態(tài)注冊的廣播接收器一定都要取消注冊才行,這里我們是在onDestroy方法中通過調(diào)用unregisterReceiver來實現(xiàn)的。

細化調(diào)整

只是提醒網(wǎng)絡發(fā)生了變化還是不夠人性化,最好能準確地告訴用戶當前有網(wǎng)絡還是沒有。因此需要進一步的優(yōu)化。

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();
}

這里直接監(jiān)控系統(tǒng)網(wǎng)絡,需要在AndroidManifest.xml里面注冊一下權(quán)限。

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

訪問 開發(fā)者手冊 可以查看Android系統(tǒng)所有可以聲明的權(quán)限。

靜態(tài)注冊實現(xiàn)開機啟動

動態(tài)注冊的廣播接收器可以自由地控制祖冊預祝校,在靈活性方面有很大的優(yōu)勢,但是也存在著一個缺點,就是必須要在程序啟動之后才能接收到廣播,因為注冊的邏輯是寫在onCreate方法中的。如果要讓程序在未啟動的情況就能接收到廣播,就需要使用靜態(tài)注冊的方式了。
這里讓程序及收一條開機廣播,當收到這條廣播,就可以在onReceive方法里執(zhí)行相應的邏輯,從而實現(xiàn)開機啟動的功能。
創(chuàng)建一個BootCompleteReceiver類。

public class BootCompleteReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent){
        Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();
    }
}

這里不再使用內(nèi)部類的方式來定義廣播接收器。我們再將這個接收器注冊到AndroidManifest.xml中,將這個廣播接收器的類名注冊進去。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.oujitsune.broadcasttest">

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    
    <application
        ...
        <receiver android:name=".BootCompleteReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>
    </application>

</manifest>

<application>標簽內(nèi)出現(xiàn)了一個新的標簽<receiver>,所有靜態(tài)注冊的廣播接收器都是在這里注冊的。用法和<activity>標簽非常相似,首先通過android:name來指定具體注冊哪一個廣播接收器,然后再<intent-filter>標簽里加入想要接受的廣播就行了。
另外,監(jiān)聽系統(tǒng)開機廣播也是需要聲明權(quán)限的。

發(fā)送自定義廣播

接下來我們來看看怎么在應用程序中發(fā)送自定義的廣播。

發(fā)送標準廣播

在發(fā)送廣播之前,我們還是需要先定義一個廣播接收器來接受廣播才行。

public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent){
        Toast.makeText(context, "received in MyBroadcastReceiver", Toast.LENGTH_LONG).show();
    }
}

注冊一下。
我們一會兒要發(fā)的就是com.example.broadcasttest.MY_BROADCAST。

<receiver android:name=".MyBroadcastReceiver">
    <intent-filter>
        <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
    </intent-filter>
</receiver>

發(fā)送有序廣播

廣播是一種跨進程的通信方式,這一點從前面接收系統(tǒng)廣播的時候就可以看出來了。因此我們在應用程序內(nèi)發(fā)出的廣播,其他的應用程序也是可以收到的。
想要發(fā)送有序廣播,只需要修改一行代碼。
onClick方法中改為sendOrderedBroadcast(intent, null);。
這個方法接受兩個參數(shù),第一個仍然是Intent,第二個是與權(quán)限相關(guān)的字符串。
如何設定廣播接收器的先后順序呢?自然是在注冊的時候設定了。
修改AndroidManifest.xml中的代碼:
<intent-filter android:priority="100">,即在intent-filter標簽下修改優(yōu)先級為100。
然后在接收器重寫的onReceive方法中調(diào)用abortBroadcast方法,這個方法截斷接收到的廣播。

使用本地廣播

前面我們發(fā)送和接受的廣播都是系統(tǒng)的全局廣播,發(fā)出的廣播可以被其他任何應用程序接收到。這樣容易引起安全問題,比如我們發(fā)送一些攜帶關(guān)鍵性數(shù)據(jù)的廣播,可能被其他應用程序接貨,或者其他的程序不停地想廣播接收器里發(fā)送各種垃圾廣播。
為了解決廣播的安全性問題,Android引入了一套本地廣播的機制,使用這個機制發(fā)出的廣播只能在應用程序內(nèi)部進行床底,并且廣播接收器也只能接受來自本應用程序發(fā)出的廣播,這樣就不會有安全性問題了。
本地廣播并不復雜,主要就是使用了一個LocalBroadcastManager來對廣播進行管理,并且提供了發(fā)送廣播和注冊廣播接收器的方法。下面我們就通過具體的實例來嘗試一下。

public class MainActivity extends AppCompatActivity {

    private IntentFilter intentFilter;
    private LocalReceiver localReceiver;
    private LocalBroadcastManager localBroadcastManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        localBroadcastManager = localBroadcastManager.getInstance(this);

        Button button_broadcast = (Button) findViewById(R.id.button_broadcast);
        button_broadcast.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
                localBroadcastManager.sendBroadcast(intent);
            }
        });

        intentFilter = new IntentFilter();
        intentFilter.addAction("com.example.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_LONG).show();
        }
    }
}

上面的代碼基本和原先用的動態(tài)注冊接收器和發(fā)送廣播是一樣的,只不過現(xiàn)在首先是通過LocalBroadcastManagergetInstance方法得到了它的一個實例,然后再注冊廣播接收器的時候調(diào)用的是LocalBroadcastManagerregisterReceiver方法,在發(fā)送廣播的時候調(diào)用的是LocalBroadcastManagersendBroadcast方法。
另外注明:本地廣播的無法通過靜態(tài)注冊的方法來接收的。
最后再來盤點一下本地廣播的優(yōu)勢吧:

  1. 可以明確地指定正在發(fā)送的廣播不會離開我們的程序,因此不用擔心數(shù)據(jù)泄露。
  2. 其他程序無法將廣播發(fā)送到我們程序內(nèi)部,因此不需要擔心會有安全漏洞的隱患。
  3. 發(fā)送本地廣播相比發(fā)送全局廣播更加高效。

實踐——實現(xiàn)強制下線功能

強制下線功能只需要彈出一個對話框,讓用戶只能點擊確定按鈕,回到登錄界面。為了避免需要在每一個活動中添加一個對話框,用廣播實現(xiàn)是一個好辦法。
關(guān)閉所有的活動只需要用AcitivityCollector類來管理所有的活動,然后用BaseActivity類作為所有活動的父類。
首先我們創(chuàng)建一個LoginActivity作為登錄界面,用表格布局來實現(xiàn)一下布局就可以了。

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:stretchColumns="1">

    <TableRow>
        <TextView
            android:layout_height="wrap_content"
            android:text="Account: "/>
        <EditText
            android:id="@+id/account"
            android:layout_height="wrap_content"
            android:hint="Input you account"/>
    </TableRow>

    <TableRow>
        <TextView
            android:layout_height="wrap_content"
            android:text="Password: " />
        <EditText
            android:id="@+id/password"
            android:layout_height="wrap_content"
            android:inputType="textPassword"/>
    </TableRow>

    <TableRow>
        <Button
            android:id="@+id/login"
            android:layout_height="wrap_content"
            android:layout_span="2"
            android:text="Login"/>
    </TableRow>

</TableLayout>
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.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 v) {
                String account = accountEdit.getText().toString();
                String password = passwordEdit.getText().toString();
                if (account.equals("254886715") && password.equals("Zh980728xl")){
                    Intent intent = new Intent(LoginActivity.this, FirstActivity.class);
                    startActivity(intent);
                    finish();
                } else
                    Toast.makeText(LoginActivity.this, "account or password is invalid", Toast.LENGTH_LONG).show();
            }
        });
    }
}

LoginActivity里判斷一下賬號密碼,如果正確就進入FirstActivity
后面的活動我們只要實現(xiàn)一下強制下線就可以了。
添加一個強制下線按鈕,發(fā)送一條強制下線廣播,然后重寫一個廣播接收器就可以實現(xiàn)了。

//廣播接收器
public class GetOutReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(final Context context, Intent intent){
        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context);
        dialogBuilder.setTitle("Warning");
        dialogBuilder.setMessage("You are forced to be offline. Please try to login again.");
        dialogBuilder.setCancelable(false);
        dialogBuilder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                ActivityCollector.finishAll();
                Intent intent = new Intent(context, LoginActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                context.startActivity(intent);
            }
        });
        AlertDialog alertDialog = dialogBuilder.create();
        alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
        alertDialog.show();
    }
}

這個廣播接收器里顯然不只是之前僅僅一個Toast那么簡單,還加入了AlertDialog.Builder來構(gòu)建一個對話框,注意一定要調(diào)用setCancelable方法將對話框設置為不可取消,然后使用setPositiveButton方法來給對話框注冊確定程序,當用戶點擊了確定按鈕,就調(diào)用ActivityCollectfinishAll方法來銷毀所有活動,因此一定要給Intent加入FLAG_ACTIVITY_NEW_TASK這個標志,最后還需要把對話框里的類型設置為TYPE_SYSTEM_ALERT,不然會無法在廣播接收器里彈出。
這樣,主要邏輯都完成了。還需要注冊一下。
這里有幾點需要注意,首先由于我們在ForceOfflineReceiver里彈出了一個系統(tǒng)級別的對話框,因此必須要進行聲明android.permission.SYSTEM_ALERT_WINDOW權(quán)限,然后對LoginActivity進行注冊,并且把它設置為主活動。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容