管理Android網(wǎng)絡(luò)的使用情況

2018-08-02 18:38 更新

編寫(xiě):kesenhoo - 原文:http://developer.android.com/training/basics/network-ops/managing.html

這一課會(huì)介紹如何細(xì)化管理使用的網(wǎng)絡(luò)資源。如果我們的程序需要執(zhí)行大量網(wǎng)絡(luò)操作,那么應(yīng)該提供用戶(hù)設(shè)置選項(xiàng),來(lái)允許用戶(hù)控制程序的數(shù)據(jù)偏好。例如,同步數(shù)據(jù)的頻率,是否只在連接到 WiFi 才進(jìn)行下載與上傳操作,是否在漫游時(shí)使用套餐數(shù)據(jù)流量等等。這樣用戶(hù)才不大可能在快到達(dá)流量上限時(shí),禁止我們的程序獲取后臺(tái)數(shù)據(jù),因?yàn)樗麄兛梢跃_控制我們的 app 使用多少數(shù)據(jù)流量。

關(guān)于如何編寫(xiě)一個(gè)最小化下載與網(wǎng)絡(luò)操作對(duì)電量影響的程序,請(qǐng)參考:優(yōu)化電池壽命和高效下載。

示例NetworkUsage.zip

檢查設(shè)備的網(wǎng)絡(luò)連接

設(shè)備可以有許多種網(wǎng)絡(luò)連接。這節(jié)課主要關(guān)注使用 Wi-Fi 或移動(dòng)網(wǎng)絡(luò)連接的情況。關(guān)于所有可能的網(wǎng)絡(luò)連接類(lèi)型,請(qǐng)看 ConnectivityManager。

通常 Wi-Fi 是比較快的。移動(dòng)數(shù)據(jù)通常都是需要按流量計(jì)費(fèi),會(huì)比較貴。通常我們會(huì)選擇讓 app 在連接到 WiFi 時(shí)去獲取大量的數(shù)據(jù)。

在執(zhí)行網(wǎng)絡(luò)操作之前,檢查設(shè)備當(dāng)前連接的網(wǎng)絡(luò)連接信息是個(gè)好習(xí)慣。這樣可以防止我們的程序在無(wú)意間連接使用了非意向的網(wǎng)絡(luò)頻道。如果網(wǎng)絡(luò)連接不可用,那么我們的應(yīng)用應(yīng)該優(yōu)雅地做出響應(yīng)。為了檢測(cè)網(wǎng)絡(luò)連接,我們需要使用到下面兩個(gè)類(lèi):

  • ConnectivityManager:它會(huì)回答關(guān)于網(wǎng)絡(luò)連接的查詢(xún)結(jié)果,并在網(wǎng)絡(luò)連接改變時(shí)通知應(yīng)用程序。
  • NetworkInfo:描述一個(gè)給定類(lèi)型(就本節(jié)而言是移動(dòng)網(wǎng)絡(luò)或 Wi-Fi)的網(wǎng)絡(luò)接口狀態(tài)。

這段代碼檢查了 Wi-Fi 與移動(dòng)網(wǎng)絡(luò)的網(wǎng)絡(luò)連接。它檢查了這些網(wǎng)絡(luò)接口是否可用(也就是說(shuō)網(wǎng)絡(luò)是通的)及是否已連接(也就是說(shuō)網(wǎng)絡(luò)連接存在,并且可以建立 socket 來(lái)傳輸數(shù)據(jù)):

private static final String DEBUG_TAG = "NetworkStatusExample";
...
ConnectivityManager connMgr = (ConnectivityManager)
        getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
boolean isWifiConn = networkInfo.isConnected();
networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
boolean isMobileConn = networkInfo.isConnected();
Log.d(DEBUG_TAG, "Wifi connected: " + isWifiConn);
Log.d(DEBUG_TAG, "Mobile connected: " + isMobileConn);

請(qǐng)注意我們不應(yīng)該僅僅靠網(wǎng)絡(luò)是否可用來(lái)做出決策。由于 isConnected() 能夠處理片狀移動(dòng)網(wǎng)絡(luò)(flaky mobile networks),飛行模式和受限制的后臺(tái)數(shù)據(jù)等情況,所以我們應(yīng)該總是在執(zhí)行網(wǎng)絡(luò)操作前檢查 isConnected()。

一個(gè)更簡(jiǎn)潔的檢查網(wǎng)絡(luò)是否可用的示例如下。getActiveNetworkInfo() 方法返回一個(gè) NetworkInfo 實(shí)例,它表示可以找到的第一個(gè)已連接的網(wǎng)絡(luò)接口,如果返回 null,則表示沒(méi)有已連接的網(wǎng)絡(luò)接口(意味著網(wǎng)絡(luò)連接不可用):

public boolean isOnline() {
    ConnectivityManager connMgr = (ConnectivityManager)
            getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
    return (networkInfo != null && networkInfo.isConnected());
}

我們可以使用 NetworkInfo.DetailedState,來(lái)獲取更加詳細(xì)的網(wǎng)絡(luò)信息,但很少有這樣的必要。

管理網(wǎng)絡(luò)的使用情況

我們可以實(shí)現(xiàn)一個(gè)偏好設(shè)置的 activity ,使用戶(hù)能直接設(shè)置程序?qū)W(wǎng)絡(luò)資源的使用情況。例如:

  • 可以允許用戶(hù)僅在連接到 Wi-Fi 時(shí)上傳視頻。
  • 可以根據(jù)諸如網(wǎng)絡(luò)可用,時(shí)間間隔等條件來(lái)選擇是否做同步的操作。

寫(xiě)一個(gè)支持連接網(wǎng)絡(luò)和管理網(wǎng)絡(luò)使用的 app,manifest 里需要有正確的權(quán)限和 intent filter。

  • manifest 文件里包括下面的權(quán)限:

  • 我們可以為 ACTION_MANAGE_NETWORK_USAGE action(Android 4.0中引入)聲明 intent filter,表示我們的應(yīng)用定義了一個(gè)提供控制數(shù)據(jù)使用情況選項(xiàng)的 activity。ACTION_MANAGE_NETWORK_USAGE 顯示管理指定應(yīng)用程序網(wǎng)絡(luò)數(shù)據(jù)使用情況的設(shè)置。當(dāng)我們的 app 有一個(gè)允許用戶(hù)控制網(wǎng)絡(luò)使用情況的設(shè)置 activity 時(shí),我們應(yīng)該為 activity 聲明這個(gè) intent filter。在章節(jié)概覽提供的示例應(yīng)用中,這個(gè) action 被 SettingsActivity 類(lèi)處理,它提供了偏好設(shè)置 UI 來(lái)讓用戶(hù)決定何時(shí)進(jìn)行下載。

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

    <uses-sdk android:minSdkVersion="4"
           android:targetSdkVersion="14" />

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

    <application
        ...>
        ...
        <activity android:label="SettingsActivity" android:name=".SettingsActivity">
             <intent-filter>
                <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
                <category android:name="android.intent.category.DEFAULT" />
          </intent-filter>
        </activity>
    </application>
</manifest>

實(shí)現(xiàn)一個(gè)首選項(xiàng) Activity

正如上面 manifest 片段中看到的那樣,SettingsActivity 有一個(gè) ACTION_MANAGE_NETWORK_USAGE action 的 intent filter。SettingsActivity 是 PreferenceActivity 的子類(lèi),它展示一個(gè)偏好設(shè)置頁(yè)面(如下兩張圖)讓用戶(hù)指定以下內(nèi)容:

  • 是否顯示每個(gè) XML 提要條目的總結(jié),或者只是每個(gè)條目的一個(gè)鏈接。
  • 是否在網(wǎng)絡(luò)連接可用時(shí)下載 XML 提要,或者僅僅在 Wi-Fi 下下載。

network-settings1.png network-settings2.png

Figure 1. 首選項(xiàng) activity

下面是 SettingsActivity。請(qǐng)注意它實(shí)現(xiàn)了 OnSharedPreferenceChangeListener。當(dāng)用戶(hù)改變了他的偏好,就會(huì)觸發(fā) onSharedPreferenceChanged(),這個(gè)方法會(huì)設(shè)置 refreshDisplay 為 true(這里的變量存在于自己定義的 activity,見(jiàn)下一部分的代碼示例)。這會(huì)使得當(dāng)用戶(hù)返回到 main activity 的時(shí)候進(jìn)行刷新:

(請(qǐng)注意,代碼中的注釋?zhuān)坏貌徽f(shuō),Googler 寫(xiě)的 Code 看起來(lái)就是舒服)

public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Loads the XML preferences file
        addPreferencesFromResource(R.xml.preferences);
    }

    @Override
    protected void onResume() {
        super.onResume();

        // Registers a listener whenever a key changes
        getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onPause() {
        super.onPause();

       // Unregisters the listener set in onResume().
       // It's best practice to unregister listeners when your app isn't using them to cut down on
       // unnecessary system overhead. You do this in onPause().
       getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
    }

    // When the user changes the preferences selection,
    // onSharedPreferenceChanged() restarts the main activity as a new
    // task. Sets the the refreshDisplay flag to "true" to indicate that
    // the main activity should update its display.
    // The main activity queries the PreferenceManager to get the latest settings.

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        // Sets refreshDisplay to true so that when the user returns to the main
        // activity, the display refreshes to reflect the new settings.
        NetworkActivity.refreshDisplay = true;
    }
}

響應(yīng)偏好設(shè)置的改變

當(dāng)用戶(hù)在設(shè)置界面改變了偏好,它通常都會(huì)對(duì) app 的行為產(chǎn)生影響。在下面的代碼示例中,app 會(huì)在 onStart() 方法中檢查偏好設(shè)置。如果設(shè)置的類(lèi)型與當(dāng)前設(shè)備的網(wǎng)絡(luò)連接類(lèi)型相一致,那么程序就會(huì)下載數(shù)據(jù)并刷新顯示。(例如, 如果設(shè)置是"Wi-Fi" 并且設(shè)備連接了 Wi-Fi)。

(這是一個(gè)很好的代碼示例,如何選擇合適的網(wǎng)絡(luò)類(lèi)型進(jìn)行下載操作)

public class NetworkActivity extends Activity {
    public static final String WIFI = "Wi-Fi";
    public static final String ANY = "Any";
    private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest";

    // Whether there is a Wi-Fi connection.
    private static boolean wifiConnected = false;
    // Whether there is a mobile connection.
    private static boolean mobileConnected = false;
    // Whether the display should be refreshed.
    public static boolean refreshDisplay = true;

    // The user's current network preference setting.
    public static String sPref = null;

    // The BroadcastReceiver that tracks network connectivity changes.
    private NetworkReceiver receiver = new NetworkReceiver();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Registers BroadcastReceiver to track network connection changes.
        IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
        receiver = new NetworkReceiver();
        this.registerReceiver(receiver, filter);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // Unregisters BroadcastReceiver when app is destroyed.
        if (receiver != null) {
            this.unregisterReceiver(receiver);
        }
    }

    // Refreshes the display if the network connection and the
    // pref settings allow it.

    @Override
    public void onStart () {
        super.onStart();

        // Gets the user's network preference settings
        SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);

        // Retrieves a string value for the preferences. The second parameter
        // is the default value to use if a preference value is not found.
        sPref = sharedPrefs.getString("listPref", "Wi-Fi");

        updateConnectedFlags();

        if(refreshDisplay){
            loadPage();
        }
    }

    // Checks the network connection and sets the wifiConnected and mobileConnected
    // variables accordingly.
    public void updateConnectedFlags() {
        ConnectivityManager connMgr = (ConnectivityManager)
                getSystemService(Context.CONNECTIVITY_SERVICE);

        NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
        if (activeInfo != null && activeInfo.isConnected()) {
            wifiConnected = activeInfo.getType() == ConnectivityManager.TYPE_WIFI;
            mobileConnected = activeInfo.getType() == ConnectivityManager.TYPE_MOBILE;
        } else {
            wifiConnected = false;
            mobileConnected = false;
        }
    }

    // Uses AsyncTask subclass to download the XML feed from stackoverflow.com.
    public void loadPage() {
        if (((sPref.equals(ANY)) && (wifiConnected || mobileConnected))
                || ((sPref.equals(WIFI)) && (wifiConnected))) {
            // AsyncTask subclass
            new DownloadXmlTask().execute(URL);
        } else {
            showErrorPage();
        }
    }
...

}

檢測(cè)網(wǎng)絡(luò)連接變化

最后一部分是關(guān)于 BroadcastReceiver 的子類(lèi):NetworkReceiver。 當(dāng)設(shè)備網(wǎng)絡(luò)連接改變時(shí),NetworkReceiver 會(huì)監(jiān)聽(tīng)到 CONNECTIVITY_ACTION,這時(shí)需要判斷當(dāng)前網(wǎng)絡(luò)連接類(lèi)型并相應(yīng)的設(shè)置好 wifiConnected 與 mobileConnected。這樣做的結(jié)果是下次用戶(hù)回到 app 時(shí),app 只會(huì)下載最新返回的結(jié)果。如果 NetworkActivity.refreshDisplay 被設(shè)置為 true,app 會(huì)更新顯示。

我們需要控制好 BroadcastReceiver 的使用,不必要的聲明注冊(cè)會(huì)浪費(fèi)系統(tǒng)資源。示例應(yīng)用在 onCreate() 中注冊(cè) BroadcastReceiver NetworkReceiver,在 onDestroy() 中銷(xiāo)毀它。這樣做會(huì)比在 manifest 里面聲明 <receiver> 更輕巧。當(dāng)我們?cè)?manifest 里面聲明一個(gè) <receiver>,我們的程序可以在任何時(shí)候被喚醒,即使我們已經(jīng)好幾個(gè)星期沒(méi)有運(yùn)行這個(gè)程序了。而通過(guò)前面的辦法注冊(cè)NetworkReceiver,可以確保用戶(hù)離開(kāi)我們的應(yīng)用之后,應(yīng)用不會(huì)被喚起。如果我們確實(shí)要在 manifest 中聲明 <receiver>,且確保知道何時(shí)需要使用到它,那么可以在合適的地方使用 setComponentEnabledSetting() 來(lái)開(kāi)啟或者關(guān)閉它。

下面是 NetworkReceiver 的代碼:

public class NetworkReceiver extends BroadcastReceiver {   

@Override
public void onReceive(Context context, Intent intent) {
    ConnectivityManager conn =  (ConnectivityManager)
        context.getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = conn.getActiveNetworkInfo();

    // Checks the user prefs and the network connection. Based on the result, decides whether
    // to refresh the display or keep the current display.
    // If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection.
    if (WIFI.equals(sPref) && networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
        // If device has its Wi-Fi connection, sets refreshDisplay
        // to true. This causes the display to be refreshed when the user
        // returns to the app.
        refreshDisplay = true;
        Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show();

    // If the setting is ANY network and there is a network connection
    // (which by process of elimination would be mobile), sets refreshDisplay to true.
    } else if (ANY.equals(sPref) && networkInfo != null) {
        refreshDisplay = true;

    // Otherwise, the app can't download content--either because there is no network
    // connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there 
    // is no Wi-Fi connection.
    // Sets refreshDisplay to false.
    } else {
        refreshDisplay = false;
        Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show();
    }
}


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)