Develop>Training(20)---执行网络操作

官方链接:https://developer.android.com/training/basics/network-ops/index.html
这节课讲解了网络连接参与一些基础的任务,监测网络连接(包括网络改变),让用户可以控制App的网络使用情况。这节课也描述了怎样解析和使用XML数据。
这节课包含了一个应用程序的例子,介绍了怎样执行常见的网络操作。
你可以在GitHub上找到代码示例,使用它作为你应用程序的可复用代码。
https://github.com/googlesamples/android-BasicNetworking/
通过这节课,你将掌握创建应用程序和构建基本模块的能力,在网络流量很小的时候,它可以高效的下载内容和解析数据。
注意:Transmitting Network Data Using Volley
是关于Volley的信息,一个HTTP请求的依赖库,可以让网络请求变的更容易、更快。Volley的GitHub地址:https://github.com/google/volley。Volley可以很方便的帮你提高应用程序在网络请求操作方面的性能。

连接到网络

为了应用程序可以执行网络操作,你的清单文件必须包含下面的权限:

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

1. 设计安全的网络通信
在应用程序添加网络功能之前,你要确保在网络传输的时候,应用程序数据和信息的安全。以下几个就是网络安全的最佳实战:

  • 最小化网络传输个人数据和敏感用户信息。
  • 应用程序通过SSL发送网络传输。
  • 考虑创建一个网络安全的配置network security configuration允许应用程序信任自定义的CA证书,或者约束为系统的CA证书,它可以保证通信的时候是安全的。

关于更多的网络安全的原理,可以参考networking security tips

2. 选择一个HTTP客户端
大多数Android应用程序的网络连接都是使用HTTP来发送和接收的。Android平台包含了HttpsURLConnection
客户端,支持TLS,流上传和下载,配置超时,IPv6,连接池。

3. 网络操作运行在子线程
为了避免创建一个反应迟钝的UI界面,不能在UI线程中执行网络操作。默认的,Android 3.0(API 11)或者更高,要求执行网络操作必须在非主线程中,如果你没有这样做,将会抛出一个NetworkOnMainThreadException异常。

下面的Activity代码片段使用了一个无界面的Fragment封装了异步的网络操作。之后,你将使用这个Fragment的实现类NetworkFragment完成操作。Activity需要实现DownloadCallback接口,当连接状态变化或者需要更新UI时,Fragment就可以回调给Activity。

public interface DownloadCallback<T> {

    interface Progress {
        int ERROR = -1;
        int CONNECT_SUCCESS = 0;
        int GET_INPUT_STREAM_SUCCESS = 1;
        int PROCESS_INPUT_STREAM_IN_PROGRESS = 2;
        int PROCESS_INPUT_STREAM_SUCCESS = 3;
    }

    /**
     * Indicates that the callback handler needs to update its appearance or information based on
     * the result of the task. Expected to be called from the main thread.
     */
    void updateFromDownload(T result);

    /**
     * Get the device's active network status in the form of a NetworkInfo object.
     */
    NetworkInfo getActiveNetworkInfo();

    /**
     * Indicate to callback handler any progress update.
     * @param progressCode must be one of the constants defined in DownloadCallback.Progress.
     * @param percentComplete must be 0-100.
     */
    void onProgressUpdate(int progressCode, int percentComplete);

    /**
     * Indicates that the download operation has finished. This method is called even if the
     * download hasn't completed successfully.
     */
    void finishDownloading();
}

public class MainActivity extends FragmentActivity implements DownloadCallback {

    ...

    // Keep a reference to the NetworkFragment, which owns the AsyncTask object
    // that is used to execute network ops.
    private NetworkFragment mNetworkFragment;

    // Boolean telling us whether a download is in progress, so we don't trigger overlapping
    // downloads with consecutive button clicks.
    private boolean mDownloading = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mNetworkFragment = NetworkFragment.getInstance(getSupportFragmentManager(), "https://www.google.com");
    }

    private void startDownload() {
        if (!mDownloading && mNetworkFragment != null) {
            // Execute the async download.
            mNetworkFragment.startDownload();
            mDownloading = true;
        }
    }

    @Override
    public void updateFromDownload(String result) {
        // Update your UI here based on result of download.
    }

    @Override
    public NetworkInfo getActiveNetworkInfo() {
        ConnectivityManager connectivityManager =
                (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
        return networkInfo;
    }

    @Override
    public void onProgressUpdate(int progressCode, int percentComplete) {
        switch(progressCode) {
            // You can add UI behavior for progress updates here.
            case Progress.ERROR:
                ...
                break;
            case Progress.CONNECT_SUCCESS:
                ...
                break;
            case Progress.GET_INPUT_STREAM_SUCCESS:
                ...
                break;
            case Progress.PROCESS_INPUT_STREAM_IN_PROGRESS:
                ...
                break;
            case Progress.PROCESS_INPUT_STREAM_SUCCESS:
                ...
                break;
        }
    }

    @Override
    public void finishDownloading() {
        mDownloading = false;
        if (mNetworkFragment != null) {
            mNetworkFragment.cancelDownload();
        }
    }

}

4. 实现一个无界面的Fragment来封装网络操作
NetworkFragment默认是运行在UI线程之中的,但它使用AsyncTask执行网络操作运行在后台线程之中。这个Fragment是无界面的,因为它没有任何引用和UI元素。相反,它只用于封装逻辑和处理生命周期事件,使Activity更新UI。

当使用AsyncTask子类来运行网络操作,你必须谨慎,不能造成内存泄漏,在AsyncTask完成后台任务之前,Activity销毁的时候还被AsyncTask引用着,就会造成内存泄漏。为了不发生这些事情,我们可以在Fragment的onDetach() 方法中清除Activity的任何引用:

/**
 * Implementation of headless Fragment that runs an AsyncTask to fetch data from the network.
 */
public class NetworkFragment extends Fragment {
    public static final String TAG = "NetworkFragment";

    private static final String URL_KEY = "UrlKey";

    private DownloadCallback mCallback;
    private DownloadTask mDownloadTask;
    private String mUrlString;

    /**
     * Static initializer for NetworkFragment that sets the URL of the host it will be downloading
     * from.
     */
    public static NetworkFragment getInstance(FragmentManager fragmentManager, String url) {
        NetworkFragment networkFragment = new NetworkFragment();
        Bundle args = new Bundle();
        args.putString(URL_KEY, url);
        networkFragment.setArguments(args);
        fragmentManager.beginTransaction().add(networkFragment, TAG).commit();
        return networkFragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mUrlString = getArguments().getString(URL_KEY);
        ...
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        // Host Activity will handle callbacks from task.
        mCallback = (DownloadCallback) context;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        // Clear reference to host Activity to avoid memory leak.
        mCallback = null;
    }

    @Override
    public void onDestroy() {
        // Cancel task when Fragment is destroyed.
        cancelDownload();
        super.onDestroy();
    }

    /**
     * Start non-blocking execution of DownloadTask.
     */
    public void startDownload() {
        cancelDownload();
        mDownloadTask = new DownloadTask();
        mDownloadTask.execute(mUrlString);
    }

    /**
     * Cancel (and interrupt if necessary) any ongoing DownloadTask execution.
     */
    public void cancelDownload() {
        if (mDownloadTask != null) {
            mDownloadTask.cancel(true);
        }
    }

    ...
}

/**
 * Implementation of AsyncTask designed to fetch data from the network.
 */
private class DownloadTask extends AsyncTask<String, Integer, DownloadTask.Result> {

    private DownloadCallback<String> mCallback;

    DownloadTask(DownloadCallback<String> callback) {
        setCallback(callback);
    }

    void setCallback(DownloadCallback<String> callback) {
        mCallback = callback;
    }

     /**
     * Wrapper class that serves as a union of a result value and an exception. When the download
     * task has completed, either the result value or exception can be a non-null value.
     * This allows you to pass exceptions to the UI thread that were thrown during doInBackground().
     */
    static class Result {
        public String mResultValue;
        public Exception mException;
        public Result(String resultValue) {
            mResultValue = resultValue;
        }
        public Result(Exception exception) {
            mException = exception;
        }
    }

    /**
     * Cancel background network operation if we do not have network connectivity.
     */
    @Override
    protected void onPreExecute() {
        if (mCallback != null) {
            NetworkInfo networkInfo = mCallback.getActiveNetworkInfo();
            if (networkInfo == null || !networkInfo.isConnected() ||
                    (networkInfo.getType() != ConnectivityManager.TYPE_WIFI
                            && networkInfo.getType() != ConnectivityManager.TYPE_MOBILE)) {
                // If no connectivity, cancel task and update Callback with null data.
                mCallback.updateFromDownload(null);
                cancel(true);
            }
        }
    }

    /**
     * Defines work to perform on the background thread.
     */
    @Override
    protected DownloadTask.Result doInBackground(String... urls) {
        Result result = null;
        if (!isCancelled() && urls != null && urls.length > 0) {
            String urlString = urls[0];
            try {
                URL url = new URL(urlString);
                String resultString = downloadUrl(url);
                if (resultString != null) {
                    result = new Result(resultString);
                } else {
                    throw new IOException("No response received.");
                }
            } catch(Exception e) {
                result = new Result(e);
            }
        }
        return result;
    }

    /**
     * Updates the DownloadCallback with the result.
     */
    @Override
    protected void onPostExecute(Result result) {
        if (result != null && mCallback != null) {
            if (result.mException != null) {
                mCallback.updateFromDownload(result.mException.getMessage());
            } else if (result.mResultValue != null) {
                mCallback.updateFromDownload(result.mResultValue);
            }
            mCallback.finishDownloading();
        }
    }

    /**
     * Override to add special behavior for cancelled AsyncTask.
     */
    @Override
    protected void onCancelled(Result result) {
    }
    ...
}

5. 使用HttpsUrlConnection获取数据
在上面的代码片段中,doInBackground()方法运行在后台线程之中。downloadUrl()给一个URL,然后使用HTTP GET发起请求。连接一旦建立,使用getInputStream()方法从InputStream中获取数据。

/**
 * Given a URL, sets up a connection and gets the HTTP response body from the server.
 * If the network request is successful, it returns the response body in String form. Otherwise,
 * it will throw an IOException.
 */
private String downloadUrl(URL url) throws IOException {
    InputStream stream = null;
    HttpsURLConnection connection = null;
    String result = null;
    try {
        connection = (HttpsURLConnection) url.openConnection();
        // Timeout for reading InputStream arbitrarily set to 3000ms.
        connection.setReadTimeout(3000);
        // Timeout for connection.connect() arbitrarily set to 3000ms.
        connection.setConnectTimeout(3000);
        // For this use case, set HTTP method to GET.
        connection.setRequestMethod("GET");
        // Already true by default but setting just in case; needs to be true since this request
        // is carrying an input (response) body.
        connection.setDoInput(true);
        // Open communications link (network traffic occurs here).
        connection.connect();
        publishProgress(DownloadCallback.Progress.CONNECT_SUCCESS);
        int responseCode = connection.getResponseCode();
        if (responseCode != HttpsURLConnection.HTTP_OK) {
            throw new IOException("HTTP error code: " + responseCode);
        }
        // Retrieve the response body as an InputStream.
        stream = connection.getInputStream();
        publishProgress(DownloadCallback.Progress.GET_INPUT_STREAM_SUCCESS, 0);
        if (stream != null) {
            // Converts Stream to String with max length of 500.
            result = readStream(stream, 500);
        }
    } finally {
        // Close Stream and disconnect HTTPS connection.
        if (stream != null) {
            stream.close();
        }
        if (connection != null) {
            connection.disconnect();
        }
    }
    return result;
}

注意: getResponseCode()方法会返回一个连接状态码,这是很有用的关于连接的额外信息。200表示连接成功。

6. 把InputStream转换成一个String
InputStream是一个可读的字节源,一旦得到一个InputStream,将解码或转换成目标数据类型是很常见的。例如,如果你下载的是一张图片,你可以用下面的方法解码并显示。

InputStream is = null;
...
Bitmap bitmap = BitmapFactory.decodeStream(is);
ImageView imageView = (ImageView) findViewById(R.id.image_view);
imageView.setImageBitmap(bitmap);

下面的代码InputStream表示为一个文本响应体,把InputStream转换为一个String,并在Activity上显示出来。

/**
 * Converts the contents of an InputStream to a String.
 */
public String readStream(InputStream stream, int maxReadSize)
        throws IOException, UnsupportedEncodingException {
    Reader reader = null;
    reader = new InputStreamReader(stream, "UTF-8");
    char[] rawBuffer = new char[maxReadSize];
    int readSize;
    StringBuffer buffer = new StringBuffer();
    while (((readSize = reader.read(rawBuffer)) != -1) && maxReadSize > 0) {
        if (readSize > maxReadSize) {
            readSize = maxReadSize;
        }
        buffer.append(rawBuffer, 0, readSize);
        maxReadSize -= readSize;
    }
    return buffer.toString();
}

下面是代码中的事件顺序:

  • 从Activity开始构建一个NetworkFragment并通过指定一个特别的URL。
  • 当用户触发了Activity的downloadData()方法,NetworkFragment就会执行DownloadTask。
  • AsyncTask的onPreExecute()方法,该方法是运行在UI线程之中的,如果设备没有连接到网络,需要取消任务。
  • AsyncTask的doInBackground()方法运行在后台线程之中,然后调用downloadUrl()方法。
  • downloadUrl()把一个URL字符串作为参数,使用HttpsURLConnection对象从web上获取数据。
  • InputStream通过readStream()方法,将流转化为String。
  • 最后,一旦后台线程工作完成,AsyncTask的onPostExecute()方法是运行在UI线程之中的,使用DownloadCallback把结果发送给上层的UI。

7. 健壮的配置变化
到目前为止,你已经成功的完成了Activity的网络操作。但是,如果用户决定改变设备的配置(比如旋转屏幕90度),这时候doInBackground()正在运行,Activity销毁并重新创建它自己,将会重新运行onCreate()方法,重新创建一个NetworkFragment引用。因此,AsyncTask就会有一个原始的NetworkFragment对象,它会持有DownloadCallback对象,就会有一个原始Activity的引用,但是不再更新UI了。因此,后台线程完成的网络工作将被浪费。

不停的制造配置变化,你确保每次重建Activity的时候,保留原始的Fragment。要做到这些,按照下面的修改代码:

//  NetworkFragment中的onCreate()方法需要添加setRetainInstance(true)
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    // Retain this Fragment across configuration changes in the host Activity.
    setRetainInstance(true);
}

public static NetworkFragment getInstance(FragmentManager fragmentManager, String url) {
    // Recover NetworkFragment in case we are re-creating the Activity due to a config change.
    // This is necessary because NetworkFragment might have a task that began running before
    // the config change occurred and has not finished yet.
    // The NetworkFragment is recoverable because it calls setRetainInstance(true).
    NetworkFragment networkFragment = (NetworkFragment) fragmentManager
            .findFragmentByTag(NetworkFragment.TAG);
    if (networkFragment == null) {
        networkFragment = new NetworkFragment();
        Bundle args = new Bundle();
        args.putString(URL_KEY, url);
        networkFragment.setArguments(args);
        fragmentManager.beginTransaction().add(networkFragment, TAG).commit();
    }
    return networkFragment;
}

现在你的App能成功的从网上获取数据了。
注意:还有一些其他的后台线程管理工具可以帮助你实现相同的目标。随着应用程序复杂性的增加,你可能会发现其他工具更适合你的应用程序。而值得研究的是IntentService,而不是AsyncTask。

管理网络使用

这节课将教你编写出的应用程序可以细粒度的控制网络使用资源。如果你的应用程序执行了大量的网络操作,你应该提供一个”用户设置”,允许用户控制应用程序的数习惯,比如多久同步App数据一次,是否只有在Wi-Fi才执行上传下载操作,是否使用漫游数据,等等。有了这些控制,当App接近这些阈值时,用户就可能更少的访问后台数据了,因为它们可以严格的控制App的数据使用。

为了学习更多的App关于网络的使用,想了解一段时间内网络的类型和数据量,可以参考Inspect Network Traffic with Network Profiler。对于大部分编写出App的指南,是关于下载和网络连接对机器的电池续航影响降到最低,请参考Optimizing Battery LifeTransferring Data Without Draining the Battery

1. 检查设备的网络连接
一个设备可以有多种网络连接。本课程的重点是使用Wi-Fi或移动网络连接。对于可能有的网络类型的完整列表,请看ConnectivityManager

Wi-Fi通常是很快的。此外,移动数据是按量计算的,非常贵。一个常见的做法是App在Wi-Fi可用的情况下,获取大量的数据。

在执行网络操作之前,一个好的做法是先检查网络的连接状态。除此之外,它能够防止App不小心错误的使用了音乐播放器。如果网络不可用,App应该很优雅的做出回应。对于检查网络连接,你通常可以使用下面几个类:

  • ConnectivityManager:回答关于网络连接状态的请求。网络状态发生改变也会通知App。
  • NetworkInfo:描述给定网络状态的接口类型(移动或者Wi-Fi)

下面的代码片段测试的是移动还是Wi-Fi网络。它确定这些接口是否可用(网络是否已经连接)或者是否已经连接(网络是否存在或者是否已经建立套接字连接)

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

注意:判断网络连接不应该用”available”,你应该用 isConnected() 方法来检查网络连接,isConnected()可用处理奇怪的移动网络,飞行模式或者限制后台数据。

检查网络接口是否可用可用用一种更加简单的方法。getActiveNetworkInfo() 方法返回 NetworkInfo实例对象,表示可以找到的第一个连接的网络接口,为null表示网络接口未连接。

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

查询更细粒度的网络状态,请使用 NetworkInfo.DetailedState,但这不是必须的。

2. 管理网络使用
你可以实现一个设置Activity,让用户能够控制App的网络使用资源。例如:

  • 允许用户上传视频,当设备连接Wi-Fi的时候。
  • 你可以同步或者不同步,视具体情况而定,例如网络可用性、时间间隔等。

为了编写可以控制网络访问和网络管理,你的 manifest 文件必须配置 permissions 和 intent filters。这个 manifest 必须包括下面的权限:

你要声明 intent filter 为 ACTION_MANAGE_NETWORK_USAGE 表明你的App定义了一个Activity,提供了控制网络数据的选项。 ACTION_MANAGE_NETWORK_USAGE 显示设置用于管理网络数据对于一个指定的App。当你的App有一个设置Activity,允许用户控制网络使用,你就需要给这个Activity申明该intent filter。

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

3. 实现一个偏好Activity
正如上面的manifest文件那样,SettingsActivity有一个 intent filter 为 ACTION_MANAGE_NETWORK_USAGE。SettingsActivity是PreferenceActivity
的子类,它显示一个首选项屏幕,允许用户指定以下内容:

  • 是否为每个XML条目显示简介,或者为每个条目显示一个链接。
  • 是否可以下载XML简介,是任何网络连接可用,或者仅当Wi-Fi可用时。
    这里写图片描述这里写图片描述

SettingsActivity需要实现 OnSharedPreferenceChangeListener 接口,当用户改变首选项时,会回调 onSharedPreferenceChanged(),然后设置 refreshDisplay 为true。

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

4. 回应配置变化
当用户改变了配置的时候,这对App的行为是很重要的。下面的代码片段,用户在 onStart() 方法检查
首选项配置。

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

}

5. 检测网络变化
最后,NetworkReceiver是 BroadcastReceiver 的子类。当设备网络发生变化的时候,NetworkReceiver就会拦截 CONNECTIVITY_ACTION 动作,它决定了网络的状态,因为可以设置 wifiConnected 和 mobileConnected 变量的值。结果当用户下一次返回到App的时候,如果NetworkActivity.refreshDisplay变量设置为true,App就会下载最新的数据并更新显示。

设置一个广播是对系统资源的浪费。该App在onCreate() 方法中注册了NetworkReceiver 广播,在 onDestroy() 取消注册。这比在manifest中注册是更轻量级的。当在manifest文件中申明的时候,它能够在任何时候唤醒App,甚至几个礼拜没有运行了。通过在Activity中注册和注销NetworkReceiver 广播,可以确保当用户离开App的时候不会被唤醒。当你确定你需要在manifest中注册广播的时候,你可以通过 setComponentEnabledSetting() 启用或者禁用它。

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

优化网络数据使用

在智能手机的使用过程中,花费的数据流量很可能会超过设备本身。用户能够启用设备上的数据保护,以优化设备上的数据使用,让流量变得更少。此功能在漫游时、周期账单或小的预付费数据包中尤其有用。

当用户在Settings中启动了”数据保护”,系统会防止后台数据使用,在前台也减少App得数据量。用户可以指定一个App白名单,运行这些App在”数据保护”打开得情况下可以访问后台数据。

Android 7.0(API 24)继承了 ConnectivityManager API,给App提供了这些方法 retrieve the user’s Data Saver preferencesmonitor preference changes 。App会检查用户是否启用了数据保护程序,并努力限制前台和后台数据的使用,这被认为是很好做法。

1. 检查数据保护应用程序得首选项
在Android 7.0(API 24),ConnectivityManager API 决定了App是否使用了数据使用限制。根据 getRestrictBackgroundStatus() 返回值来判断:

  • RESTRICT_BACKGROUND_STATUS_DISABLED:数据保护程序被禁用。
  • RESTRICT_BACKGROUND_STATUS_ENABLED:数据保护程序可以使用。App努力限制前台的数据使用,并优雅地处理后台数据使用。
  • RESTRICT_BACKGROUND_STATUS_WHITELISTED:数据保护程序可以使用,但App在白名单之中。App仍然应该努力限制前台和后台数据的使用。

一个好的做法是,当App使用计量的网络时,应该限制网络数据的使用,甚至数据保护程序被禁用或者在白名单之中,参考下面代码的做法:

ConnectivityManager connMgr = (ConnectivityManager)
        getSystemService(Context.CONNECTIVITY_SERVICE);
// Checks if the device is on a metered network
if (connMgr.isActiveNetworkMetered()) {
  // Checks user’s Data Saver settings.
  switch (connMgr.getRestrictBackgroundStatus()) {
    case RESTRICT_BACKGROUND_STATUS_ENABLED:
    // Background data usage is blocked for this app. Wherever possible,
    // the app should also use less data in the foreground.

    case RESTRICT_BACKGROUND_STATUS_WHITELISTED:
    // The app is whitelisted. Wherever possible,
    // the app should use less data in the foreground and background.

    case RESTRICT_BACKGROUND_STATUS_DISABLED:
    // Data Saver is disabled. Since the device is connected to a
    // metered network, the app should use less data wherever possible.
  }
} else {
  // The device is not on a metered network.
  // Use data as required to perform syncs, downloads, and updates.
}

2. 请求白名单权限
如果App需要在后台使用数据,它能够请求一个白名单权限,通过发送Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS Intent,包含一个URI,就是你App的包名,例如:package:MY_APP_ID。

发送Intent和URI给Settings App,显示你App的数据使用设置。用户能够决定App是否可以启动后台数据,在你发送这个Intent之前,一个很好的做法是是先询问用户,如果用户想启动后台数据就运行Settings App。

3. 监测数据保护程序改变的首选项
通过广播App能监测数据服务程序保护配置的变化,要监听ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED 动作,通过Context.registerReceiver() 方法动态监听广播,当一个应用程序接收到广播,它应该通过ConnectivityManager.getRestrictBackgroundStatus() 方法 check if the new Data Saver preferences affect its permissions
注意:只有当你通过Context.registerReceiver() 方法注册了广播,系统才会发送广播给你的App,App如果是在 manifest 注册的就会收不到广播。

4. 测试Android调试桥命令
Android Debug Bridge (ADB) 提供了一些命令可以用来测试App的数据保护程序。你能检查和配置网络权限,或者设置无限网络来测试App的网络计量情况。

  • $ adb shell dumpsys netpolicy:生成一份报告,包括当前所有的后台网络限制设置,当前白名单的包的UID,其他已知包的网络权限。
  • $ adb shell cmd netpolicy:显示一个完整的网络策略管理列表。
  • $ adb shell cmd netpolicy set restrict-background :开启或者禁用数据保护程序。
  • $ adb shell cmd netpolicy add restrict-background-whitelist :添加指定的程序到白名单,允许使用后台数据。
  • $ adb shell cmd netpolicy remove restrict-background-whitelist :在数据保护程序开启的情况下,从白名单中移除指定程序。
  • $ adb shell cmd netpolicy list wifi-networks:列出所有Wi-Fi网络,显示它们是否是计量的。
  • $ adb shell cmd netpolicy set metered-network true:设置Wi-Fi与指定的SSID作为计量,允许你在不是计量网络的情况下模拟计量网络。

解析XML数据

可扩展标记语言(XML)是一组机器可读形式编码文档。XML是网络上共享数据的常用格式。网站频繁地更新他们的内容,比如一条新闻或者博客,提供XML的数据以便外部程序可以跟上内容的变化。App连接网络上传或者解析XML数据是很常见的任务。这节课就告诉我们怎么解析XML文档和使用数据。

1. 选择一个解析器
我们推荐 XmlPullParser 解析器,在Android上它解析XML是高效的、方便的。在历史上,Android实现过两个接口:

两种选择都不错。这节课使用的是 ExpatPullParser via Xml.newPullParser()

2. 分析标签
第一步就是解析标签,那些你感兴趣的标签。解析器能够提取这些字段的数据,忽略其他字段。下面的片段是解析一个示例App,每个发送到 StackOverflow.com 的文章都出现在 entry 标签中,在其中还嵌套了几个标签。

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" ...">
<title type="text">newest questions tagged android - Stack Overflow</title>
...
    <entry>
    ...
    </entry>
    <entry>
        <id>http://stackoverflow.com/q/9439999</id>
        <re:rank scheme="http://stackoverflow.com">0</re:rank>
        <title type="text">Where is my data file?</title>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="android"/>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="file"/>
        <author>
            <name>cliff2310</name>
            <uri>http://stackoverflow.com/users/1128925</uri>
        </author>
        <link rel="alternate" href="http://stackoverflow.com/questions/9439999/where-is-my-data-file" />
        <published>2012-02-25T00:30:54Z</published>
        <updated>2012-02-25T00:30:54Z</updated>
        <summary type="html">
            <p>I have an Application that requires a data file...</p>

        </summary>
    </entry>
    <entry>
    ...
    </entry>
...
</feed>

这个示例App提取了entry 标签的内容,还有它嵌套的 title,link,和 summary 标签。

3. 实例化解析器
下一步就是实例化解析器,然后启动解析过程。下面的代码片段,实例化一个解析器但是没有处理命名空间,提供了 InputStream 作为输入,通过调用 nextTag() 和 readFeed() 开始处理解析,提取出App感兴趣的数据。

public class StackOverflowXmlParser {
    // We don't use namespaces
    private static final String ns = null;

    public List parse(InputStream in) throws XmlPullParserException, IOException {
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
            parser.setInput(in, null);
            parser.nextTag();
            return readFeed(parser);
        } finally {
            in.close();
        }
    }
 ...
}

4. 读取标签
readFeed() 做实际的处理标签的工作,它把标记为 “entry” 的作为处理的开始点,循环调用。如果没有 entry 标签,它就会跳过。一旦进入循环处理,readFeed() 方法就是返回一个包含 entry 的 List 列表。

private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException {
    List entries = new ArrayList();

    parser.require(XmlPullParser.START_TAG, ns, "feed");
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        // Starts by looking for the entry tag
        if (name.equals("entry")) {
            entries.add(readEntry(parser));
        } else {
            skip(parser);
        }
    }
    return entries;
}

5. 解析XML
下面就是解析XML数据的步骤:

  • Analyze the Feed 的描述中,App要明确想要的标签。下面的例子提取了entry 标签的数据,还有它嵌套的 title,link,和summary。
  • 创建下面的几个方法:

    • 一个读方法,你感兴趣的每个标签。例如,readEntry(),readTitle() 等等,通过输入流解析要读的标签。当遇到标签名为 entry,title,link 和 summary 时,对这些标签调用合适的方法。否则,就跳过标签。
    • 对于 title 和 summary 标签,调用 readText() 方法解析。该方法通过 parser.getText() 提取出标签的数据。
    • 对于 link 标签,解析器首先确定是不是感兴趣的类型,然后在提取数据。通过 parser.getAttributeValue() 提取链接的值。
    • 对于 entry 标签,调用 readEntry() 方法。该方法解析它嵌套的标签,并返回 Entry 对象。
public static class Entry {
    public final String title;
    public final String link;
    public final String summary;

    private Entry(String title, String summary, String link) {
        this.title = title;
        this.summary = summary;
        this.link = link;
    }
}

// Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off
// to their respective "read" methods for processing. Otherwise, skips the tag.
private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException {
    parser.require(XmlPullParser.START_TAG, ns, "entry");
    String title = null;
    String summary = null;
    String link = null;
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        if (name.equals("title")) {
            title = readTitle(parser);
        } else if (name.equals("summary")) {
            summary = readSummary(parser);
        } else if (name.equals("link")) {
            link = readLink(parser);
        } else {
            skip(parser);
        }
    }
    return new Entry(title, summary, link);
}

// Processes title tags in the feed.
private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "title");
    String title = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "title");
    return title;
}

// Processes link tags in the feed.
private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException {
    String link = "";
    parser.require(XmlPullParser.START_TAG, ns, "link");
    String tag = parser.getName();
    String relType = parser.getAttributeValue(null, "rel");
    if (tag.equals("link")) {
        if (relType.equals("alternate")){
            link = parser.getAttributeValue(null, "href");
            parser.nextTag();
        }
    }
    parser.require(XmlPullParser.END_TAG, ns, "link");
    return link;
}

// Processes summary tags in the feed.
private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "summary");
    String summary = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "summary");
    return summary;
}

// For the tags title and summary, extracts their text values.
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
    String result = "";
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.getText();
        parser.nextTag();
    }
    return result;
}
  ...
}

6. 跳过你不关心的标签
XML解析的第一步就是跳过你不感兴趣的标签。通过解析器的 skip() 方法:

private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
    if (parser.getEventType() != XmlPullParser.START_TAG) {
        throw new IllegalStateException();
    }
    int depth = 1;
    while (depth != 0) {
        switch (parser.next()) {
        case XmlPullParser.END_TAG:
            depth--;
            break;
        case XmlPullParser.START_TAG:
            depth++;
            break;
        }
    }
 }

下面解释下它时怎么工作的:

  • 如果当前没有 START_TAG ,它抛出异常。
  • 消费了START_TAG 等所有事件包括 END_TAG 。
  • 为了确保它能够停止在 END_TAG ,不能在第一个标签就遇到了原始的 START_TAG ,它要记录嵌套的深度。

7. 消费XML数据
App在AsyncTask中获取并解析了XML数据,这些处理要在主线程之外。当处理完成,要在主线程中更新UI。

下面的代码摘录,loadPage() 做了两件事:

  • 实例化了一个String的URL变量
  • 如果网络连接了,将执行 new DownloadXmlTask().execute(url) 。将实例化一个 DownloadXmlTask 对象,运行 execute() 方法,下载并解析数据,返回一个String结果显示在UI上。
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;
    public static String sPref = null;

    ...

    // Uses AsyncTask to download the XML feed from stackoverflow.com.
    public void loadPage() {

        if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) {
            new DownloadXmlTask().execute(URL);
        }
        else if ((sPref.equals(WIFI)) && (wifiConnected)) {
            new DownloadXmlTask().execute(URL);
        } else {
            // show error
        }
    }

DownloadXmlTask 是AsyncTask的子类,实现了父类的2个方法:

  • doInBackground() 它执行了loadXmlFromNetwork()方法,传入一个URL参数。 loadXmlFromNetwork() 方法获取并处理数据,当它完成时,返回一个String结果。
  • onPostExecute() 处理返回String的结果,并显示在UI上。
// Implementation of AsyncTask used to download XML feed from stackoverflow.com.
private class DownloadXmlTask extends AsyncTask<String, Void, String> {
    @Override
    protected String doInBackground(String... urls) {
        try {
            return loadXmlFromNetwork(urls[0]);
        } catch (IOException e) {
            return getResources().getString(R.string.connection_error);
        } catch (XmlPullParserException e) {
            return getResources().getString(R.string.xml_error);
        }
    }

    @Override
    protected void onPostExecute(String result) {
        setContentView(R.layout.main);
        // Displays the HTML string in the UI via a WebView
        WebView myWebView = (WebView) findViewById(R.id.webview);
        myWebView.loadData(result, "text/html", null);
    }
}

下面介绍loadXmlFromNetwork() 方法,它是由DownloadXmlTask类来执行的。

  • 实例化StackOverflowXmlParser对象。声明一个包含Entry 的List变量,它将用于保存从XML中提取出的数据。
  • 调用downloadUrl()方法,获取数据并返回一个InputStream。
  • 用 StackOverflowXmlParser 对象解析InputStream,并填充List对象。
  • 处理List对象,将获取的数据与HTML标签相结合。
  • 返回一个HTML String数据,并在主线程中更新显示UI。
// Uploads XML from stackoverflow.com, parses it, and combines it with
// HTML markup. Returns HTML string.
private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException {
    InputStream stream = null;
    // Instantiate the parser
    StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser();
    List<Entry> entries = null;
    String title = null;
    String url = null;
    String summary = null;
    Calendar rightNow = Calendar.getInstance();
    DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa");

    // Checks whether the user set the preference to include summary text
    SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    boolean pref = sharedPrefs.getBoolean("summaryPref", false);

    StringBuilder htmlString = new StringBuilder();
    htmlString.append("<h3>" + getResources().getString(R.string.page_title) + "</h3>");
    htmlString.append("<em>" + getResources().getString(R.string.updated) + " " +
            formatter.format(rightNow.getTime()) + "</em>");

    try {
        stream = downloadUrl(urlString);
        entries = stackOverflowXmlParser.parse(stream);
    // Makes sure that the InputStream is closed after the app is
    // finished using it.
    } finally {
        if (stream != null) {
            stream.close();
        }
     }

    // StackOverflowXmlParser returns a List (called "entries") of Entry objects.
    // Each Entry object represents a single post in the XML feed.
    // This section processes the entries list to combine each entry with HTML markup.
    // Each entry is displayed in the UI as a link that optionally includes
    // a text summary.
    for (Entry entry : entries) {
        htmlString.append("<p><a href='");
        htmlString.append(entry.link);
        htmlString.append("'>" + entry.title + "</a></p>");
        // If the user set the preference to include summary text,
        // adds it to the display.
        if (pref) {
            htmlString.append(entry.summary);
        }
    }
    return htmlString.toString();
}

// Given a string representation of a URL, sets up a connection and gets
// an input stream.
private InputStream downloadUrl(String urlString) throws IOException {
    URL url = new URL(urlString);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setReadTimeout(10000 /* milliseconds */);
    conn.setConnectTimeout(15000 /* milliseconds */);
    conn.setRequestMethod("GET");
    conn.setDoInput(true);
    // Starts the query
    conn.connect();
    return conn.getInputStream();
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值