为了执行网络操作,必须先在清单文件中添加如下权限:
一、设计安全的网络通信
最大限度地减少传输用户的敏感数据。
使用 SSL 协议发送网络数据。
考虑创建网络安全配置。
二、在子线程中引入网络操作
为了避免造成 UI 无响应,请不要在 UI 线程上执行网络操作。Android 3.0(API级别11)及更高版本要求在 UI 线程以外的线程上执行网络操作,如果没有,则会抛出 NetworkOnMainThreadException
。
以下 Activity 代码段使用 Fragment 封装异步网络操作,还实现了 DownloadCallback
接口,允许 fragment 在需要获取连接状态或者需要将更新发送回 UI 时返回到 Activity。
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;
}
}
}
DownloadCallback 接口至少包含以下内容:
public interface DownloadCallback {
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();
}
现在,将 DownloadCallback 接口方法的实现添加进 Activity:
@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();
}
}
三、实现无 UI Fragment 来封装网络操作
NetworkFragment 默认运行在 UI 线程上,它使用 AsyncTask 执行网络操作。NetworkFragment 是无界面的,因为它没有引用任何 UI 元素。它仅用于封装逻辑和处理生命周期事件,让父 Activity 更新 UI。
使用 AsyncTask 的子类时必须小心,如果 AsyncTask 在完成其后台工作之前所引用的 Activity 销毁了,则会发生内存泄漏。为了确保不会发生这种情况,在 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);
}
}
...
}
现在,实现一个 AsyncTask 的子类作为 Fragment 的私有内部类:
/**
* Implementation of AsyncTask designed to fetch data from the network.
*/
private class DownloadTask extends AsyncTask {
private DownloadCallback mCallback;
DownloadTask(DownloadCallback callback) {
setCallback(callback);
}
void setCallback(DownloadCallback 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) {
}
...
}
四、使用 HttpsUrlConnection 获取数据
以下代码段使用 HttpsURLConnection API 来完成 downloadUrl() 方法:
/**
* 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;
}
五、将 InputStream 转换为字符串
InputStream 是可读的字节源。一旦获得 InputStream,通常将其解码或转换为目标数据类型。例如,如果正在下载图像数据,则可以如下解码并显示:
InputStream is = null;
...
Bitmap bitmap = BitmapFactory.decodeStream(is);
ImageView imageView = (ImageView) findViewById(R.id.image_view);
imageView.setImageBitmap(bitmap);
在上面显示的示例中,InputStream 表示响应体的文本。
以下是将 InputStream 转换为字符串的方式,以便 Activity 可以在 UI 中显示它:
/**
* 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 线程上),如果设备未连接到 Internet,则取消该任务。
然后该 AsyncTask 的 doInBackground() 方法在后台线程上运行并调用 downloadUrl() 方法。
该 downloadUrl() 方法将 URL 字符串作为参数,并使用 HttpsURLConnection 对象来获取 Web 内容作为 InputStream。
InputStream 被传递到 readStream() 方法,将其转换为字符串。
最后,AsyncTask 的 onPostExecute() 方法在 UI 线程上运行,并使用 DownloadCallback 将结果发送回用户界面。
六、设备配置更改
如果用户在后台线程运行时更改设备配置(例如将屏幕旋转 90 度),则 Activity 会销毁并重新创建自身,导致重新执行 onCreate() 并引用新的 NetworkFragment。因此,原来的 AsyncTask 无法更新 UI,在后台线程上完成的工作将被浪费。
因此需要保留原来的 Fragment 并确保重构的 Activity 引用它。为此,对代码进行以下修改:
首先,NetworkFragment 应该在它的 onCreate() 方法中调用 setRetainInstance(true)
方法:
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
...
// Retain this Fragment across configuration changes in the host Activity.
setRetainInstance(true);
}
然后,修改静态 getInstance() 方法中初始化 NetworkFragment 的方式:
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;
}
现在可以成功从互联网上提取数据了。