管理網路用量

本課程將說明如何編寫可精細控管網路資源用量的應用程式。如果應用程式需要執行大量網路作業,開發人員應提供使用者設定功能,讓使用者控管應用程式處理資料的習慣,例如其資料同步處理頻率、只有當連上 Wi-Fi 時是否上傳/下載資料,以及漫遊時是否使用行動資料等。有了這些控管功能,在資料用量快超出限制時,使用者就不太可能停用應用程式存取背景資料的權限,因為他們可以精確控管應用程式使用的資料量。

如要進一步瞭解應用程式的網路用量,包括特定時間內,所使用的網路連線類型,請參閱「網頁應用程式」「使用網路分析器檢查網路流量」。一般而言,如要編寫能最大程度減少下載作業和網路連線影響電池續航力的應用程式,可參閱「最佳化電池續航力」「在不消耗電池的情況下轉移資料」

您也可以查看 NetworkConnect範例。

檢查裝置的網路連線

一部裝置可以有多種網路連線類型。本課程著重於介紹如何使用 Wi-Fi 或行動裝置網路連線。如需可用網路類型的完整清單,請參閱 ConnectivityManager

一般而言,Wi-Fi 連線速度通常比較快。此外,行動裝置資料通常是計量付費,且所費不貲。應用程式對此的常見對策,是在有可用 Wi-Fi 時下載大型資料。

執行網路作業之前,建議先檢查網路連線狀態。除此之外,此舉可防止應用程式意外誤用無線電。如果無法連線網路,應用程式正常應會做出回應。一般而言,檢查網路連線會使用下列類別:

  • ConnectivityManager:回答網路連線狀態的查詢。網路連線改變時,也會通知應用程式。
  • NetworkInfo:說明指定類型的網路介面狀態 (當前為行動裝置或 Wi-Fi)。

這段程式碼片段會測試 Wi-Fi 和行動裝置的網路連線。測試會判定這些網路介面是否正常運作,(也就是說,是否有可連線的網路),以及是否處於連線狀態,(也就是說,是否已建立網路連線,以及是否建立通訊端並能傳送資料):

Kotlin

private const val DEBUG_TAG = "NetworkStatusExample"
...
val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
var isWifiConn: Boolean = false
var isMobileConn: Boolean = false
connMgr.allNetworks.forEach { network ->
    connMgr.getNetworkInfo(network).apply {
        if (type == ConnectivityManager.TYPE_WIFI) {
            isWifiConn = isWifiConn or isConnected
        }
        if (type == ConnectivityManager.TYPE_MOBILE) {
            isMobileConn = isMobileConn or isConnected
        }
    }
}
Log.d(DEBUG_TAG, "Wifi connected: $isWifiConn")
Log.d(DEBUG_TAG, "Mobile connected: $isMobileConn")

Java

private static final String DEBUG_TAG = "NetworkStatusExample";
...
ConnectivityManager connMgr =
        (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
boolean isWifiConn = false;
boolean isMobileConn = false;
for (Network network : connMgr.getAllNetworks()) {
    NetworkInfo networkInfo = connMgr.getNetworkInfo(network);
    if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
        isWifiConn |= networkInfo.isConnected();
    }
    if (networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) {
        isMobileConn |= networkInfo.isConnected();
    }
}
Log.d(DEBUG_TAG, "Wifi connected: " + isWifiConn);
Log.d(DEBUG_TAG, "Mobile connected: " + isMobileConn);

請注意,我們不建議您根據是否有可用網路來做決定。在執行網路作業前,請一律先檢查 isConnected(),因為 isConnected() 會處理不穩定的行動網路、飛航模式和存取背景資料受限等情況。

要檢查是否有網路介面可用,請參閱下方詳細說明。方法 getActiveNetworkInfo() 會傳回 NetworkInfo 執行個體,也就是第一個找到的已連線網路介面,或 null如果沒有已連線的網路介面,(也就表示無法建立網路連線):

Kotlin

fun isOnline(): Boolean {
    val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val networkInfo: NetworkInfo? = connMgr.activeNetworkInfo
    return networkInfo?.isConnected == true
}

Java

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

使用 NetworkInfo.DetailedState 可查詢更精確的狀態,但這並非必要。

管理網路用量

您可以實作偏好設定,讓使用者能精控應用程式的網路用量。例如:

  • 只有當裝置連線到 Wi-Fi 網路時,您才可以讓使用者上傳影片。
  • 您可以設定是否同步處理資料的特定標準,例如網路用量高低峰、時間間隔等。

如要編寫具有網路存取及管理網路用量功能的應用程式,您的資訊清單必須具備適當的權限和意圖篩選器。

  • 本節末尾摘錄的資訊清單包含下列權限:
  • 您可以針對 ACTION_MANAGE_NETWORK_USAGE 的動作宣告意圖篩選器,讓應用程式定義活動,以此提供控制資料用量的選項。ACTION_MANAGE_NETWORK_USAGE 會顯示特定應用程式管理網路用量的設定。如應用程式具有允許使用者控管網路用量的設定時,開發人員應宣告該設定的意圖篩選器。

在範例應用程式中,SettingsActivity 類別會處理這個動作,該類別會顯示 UI 偏好設定,讓使用者決定何時下載動態消息。

<?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>

能處理使用者機密資料的應用程式,且以 Android 11 以上為目標版本,可以授予每個程序的網路存取權。透過明確指定可使用網路的程序,您就能將所有不需要上傳資料的程式碼獨立出來。

雖不保證應用程式不會意外上傳資料,但如此一來,可降低因為應用程式出錯造成資料外洩的可能性。

以下範例是一份使用每個程序功能的資訊清單檔案:

<processes>
    <process />
    <deny-permission android:name="android.permission.INTERNET" />
    <process android:process=":withoutnet1" />
    <process android:process="com.android.cts.useprocess.withnet1">
        <allow-permission android:name="android.permission.INTERNET" />
    </process>
    <allow-permission android:name="android.permission.INTERNET" />
    <process android:process=":withoutnet2">
        <deny-permission android:name="android.permission.INTERNET" />
    </process>
    <process android:process="com.android.cts.useprocess.withnet2" />
</processes>

執行偏好設定活動

如同先前本章的資訊清單摘錄所示,範例應用程式的活動 SettingsActivity,有意圖篩選器可處理 ACTION_MANAGE_NETWORK_USAGE 動作。SettingsActivityPreferenceActivity 的子類別。如圖 1 所示,這是應用程式的偏好設定畫面,可讓使用者決定下列功能:

  • 顯示每個 XML 動態消息項目的摘要,或顯示每個項目的連結。
  • 只要一連上網路就下載 XML 動態消息,或僅在有 Wi-Fi 連線時下載。

偏好設定面板 設定網路偏好設定

圖 1。偏好設定活動。

以下是 SettingsActivity。請注意,它實作了 OnSharedPreferenceChangeListener。使用者變更偏好設定時,系統會執行 onSharedPreferenceChanged() 來設定 refreshDisplay 的值為 true。如此一來,使用者返回主要活動時,畫面會重新整理:

Kotlin

class SettingsActivity : PreferenceActivity(), OnSharedPreferenceChangeListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

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

    override fun onResume() {
        super.onResume()

        // Registers a listener whenever a key changes
        preferenceScreen?.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
    }

    override fun 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().
        preferenceScreen?.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
    }

    // When the user changes the preferences selection,
    // onSharedPreferenceChanged() restarts the main activity as a new
    // task. Sets 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 fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
        // 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
    }
}

Java

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

回應偏好設定變更

使用者在設定裡變更偏好設定時,通常都會影響應用程式的運作。在該程式碼片段中,應用程式會檢查 onStart() 中的偏好設定。如果裝置的網路連線符合設定,則應用程式會下載動態消息,並重新整理畫面,例如設定為 "Wi-Fi",且裝置透過 Wi-Fi 連線。

Kotlin

class NetworkActivity : Activity() {

    // The BroadcastReceiver that tracks network connectivity changes.
    private lateinit var receiver: NetworkReceiver

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

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

    public override fun onDestroy() {
        super.onDestroy()
        // Unregisters BroadcastReceiver when app is destroyed.
        this.unregisterReceiver(receiver)
    }

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

    public override fun onStart() {
        super.onStart()

        // Gets the user's network preference settings
        val 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.
    fun updateConnectedFlags() {
        val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

        val activeInfo: NetworkInfo? = connMgr.activeNetworkInfo
        if (activeInfo?.isConnected == true) {
            wifiConnected = activeInfo.type == ConnectivityManager.TYPE_WIFI
            mobileConnected = activeInfo.type == ConnectivityManager.TYPE_MOBILE
        } else {
            wifiConnected = false
            mobileConnected = false
        }
    }

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

    companion object {

        const val WIFI = "Wi-Fi"
        const val ANY = "Any"
        const val SO_URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort;=newest"

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

        // The user's current network preference setting.
        var sPref: String? = null
    }
...

}

Java

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

}

偵測連線變更

本章最後一個環節是 BroadcastReceiver 子類別 NetworkReceiver。當裝置的網路連線有所變更時,NetworkReceiver 會攔截 CONNECTIVITY_ACTION 動作,並判斷網路連線狀態,然後相應地標記 wifiConnectedmobileConnected 的值為 true/false。結果是,在 NetworkActivity.refreshDisplay 的值設定為 true 的情況下,使用者再次使用應用程式時,應用程式只會下載最新動態消息,並更新畫面。

設定不需要呼叫的 BroadcastReceiver,可能會造成系統資源負擔。範例應用程式會將 BroadcastReceiverNetworkReceiver 註冊於 onCreate(),並從 onDestroy() 取消註冊。這種做法比在資訊清單中宣告 <receiver> 更輕鬆。在資訊清單中宣告 <receiver> 時,即便您已有數周時間未啟用應用程式,它仍可以隨時喚醒您的應用程式。在主要活動中註冊或取消註冊 NetworkReceiver,即可確保在使用者未使用應用程式期間,應用程式不會被喚醒。如果您在資訊清單中宣告 <receiver>,並且確知其用途,那麼可以使用 setComponentEnabledSetting() 來視情況啟用或停用應用程式。

以下是 NetworkReceiver

Kotlin

class NetworkReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val conn = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val networkInfo: NetworkInfo? = conn.activeNetworkInfo

        // 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 == sPref && networkInfo?.type == 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 == 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()
        }
    }
}

Java

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