上一篇 JSON学习 中介绍了 JSON 对象的结构,如何解析 JSON 数据,但是我们的 JSON 数据是本地已经转换的 String 类型,现在我们处理网上的 JSON 数据,用到了网上的操作,必然要进行网络的连接,讲到网络部分,有必然使用到多线程,所以本篇侧重于介绍多线程部分。
我们最初的 Android 学习中,默认都是在主线程( MainThread )进行操作的,主线程也叫做 UI 线程,一般的添加控件之类的操作都是主线程运行的,下面的图可以很好的说明,我们一般的绘图操作,点击按钮的操作,网络请求…都是在主线程上运行,但是这些操作的会被放到队列中,顺序执行,每次执行一个事件。
然而我们为什么要使用多线程,我们可能遇到过这样的情况,当我们访问一些 APP 的时候,在做一些网络请求的时候,界面会不动,当你点击界面上的按钮时,会出现是否停止响应的按钮。
为什么会出现这种情况?那是因为我们的网络请求很可能写到主线程上,当你进行网络请求时,应用可以需要一段时间去连接,获取,解析数据,但是同时你又多次按下了按钮,这些操作会出现到你的主线程队列上,但是应用并没有执行你的操作,以至于导致线程阻塞,一段时间后 Android 系统会显示上图的提示。
所以 Android 有个重要原则:不能把网络请求放到主线程 ,也就是不能阻塞 UI 线程,所以这个问题的解决方案就是使用多线程,让各自的操作到各自的线程中进行,网络访问一个线程,数据处理一个线程….这里我们只需要一个后台线程,用来处理网络请求。关于进程和线程可以访问官方链接
Android为了降低开发难度,提供了AsyncTask。AsyncTask就是一个封装过的后台任务类,就是异步任务。 先来介绍一下 AyncTask 的基本用法。AyncTask 是一个抽象类,要使用这个抽象类需要建立一个子类去继承它,在继承这个类的同时需要规定三个泛型参数,我们为了便于理解,我们把着三个泛型参数放到方法之后讲解,先来讲解 AyncTask 的四个方法:
onPreExcute(): UI 线程中调用 该方法在后台任务执行之前调用,一般用于界面的初始化操作,如:显示一个进度条。
doInBackground(Params…): 后台线程中调用 ,该方法用于处理耗时操作,操作一旦完成既可以通过 return 来返回执行结果。用过要在该方法中更新 UI 可以手动调用 publishProgres(Progress…) 方法来完成。
onProgressUpdate(Progress…): UI 线程中调用 利用该方法可以对 UI 进行相应的更新。
onPostExcute(Result): UI 线程中调用 在doInBackground(Params…)中返回的数据会作为参数传递到该方法中。 可以利用后台线程返回的参数进行 UI 操作。
现在我们在讲一讲 AyncTask 中的三个泛型参数,配合上述的方法,你会很快找到对应的规律和使用方法。 - Params:在执行 AyncTask 需要传入的参数,用于后台任务使用。对应 doInBackground() - Progress:后台任务执行时,如果需要在界面上显示当前的进度。对应onProgressUpdate() - Result:当任务完毕的情况下,如果需要对结果进行返回,则使用该返回值类型,对应方法onPostExcute()
>这个项目是获取网上地震数据并进行显示的应用。效果图如下:
先顶一个
public final class QueryUtils { private QueryUtils() { } //该方法进行网络连接,获取数据,解析数据 public static List<Info> fetchEarthquakeDatas(String requestUrl) throws MalformedURLException { /*调用方法处理正确的url*/ URL url = createUrl(requestUrl); /*定义一个json响应的变量*/ String jsonResponse = null; try { /*调用一个方法获取jsonResponse*/ jsonResponse = makeHttpRequest(url); } catch (IOException e) { e.printStackTrace(); } /*extractFeatureFromJson一个提取json数据的方法*/ List<Info> listItem = extractFeatureFromJson(jsonResponse); return listItem; } /*创建一个方法,处理传入的Url*/ private static URL createUrl(String stringUrl) { URL url = null; try { url = new URL(stringUrl); } catch (MalformedURLException e) { e.printStackTrace(); } return url; } /*创建一个方法,连接网络获取jsonResponse*/ private static String makeHttpRequest(URL url) throws IOException { String jsonResponse = ""; if (url == null) return jsonResponse; /*初始化网络连接*/ HttpURLConnection urlConnection = null; /*初始化输入流,因为返回的是一个字符串类型,所以要读取数据*/ InputStream inputStream = null; try { urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.setReadTimeout(10000); urlConnection.setConnectTimeout(15000); urlConnection.setRequestMethod("GET"); urlConnection.connect(); if (urlConnection.getResponseCode() == 200) { Log.i(TAG_LOG, "启动网络服务成功"); Log.v("MainActivity", String.valueOf(urlConnection.getResponseCode())); /*得到他的输入流,也就是读取*/ inputStream = urlConnection.getInputStream(); /*调用一个方法,读取数据*/ jsonResponse = readFromStream(inputStream); } else { Log.i(TAG_LOG, "启动网络服务失败"); Log.v("MainActivity", String.valueOf(urlConnection.getResponseCode())); } } catch (IOException e) { e.printStackTrace(); } finally { if (urlConnection != null) urlConnection.disconnect(); if (inputStream != null) { inputStream.close(); } } return jsonResponse; } /*创建一个方法读取输入流的数据*/ private static String readFromStream(InputStream inputStream) { /*字符串的拼接*/ StringBuilder output = new StringBuilder(); if (inputStream != null) { InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charset.forName("UTF-8")); BufferedReader reader = new BufferedReader(inputStreamReader); try { String line = reader.readLine(); while (line != null) { output.append(line); line = reader.readLine(); } } catch (IOException e) { e.printStackTrace(); } } /*如果输入流不为空,进行数据读取转换为字符串类型*/ return output.toString(); /*返回字符串*/ } /*创建一个提取json数据的方法*/ private static List<Info> extractFeatureFromJson(String earthquakeJSON) throws MalformedURLException { if (TextUtils.isEmpty(earthquakeJSON)) return null; /*因为返回类型是List<Info>,所以新建一个对象*/ List<Info> listItem = new ArrayList<Info>(); try { JSONObject root = new JSONObject(earthquakeJSON); JSONArray featureArray = root.getJSONArray("features"); for (int i = 0; i < featureArray.length(); i++) { JSONObject feature = featureArray.getJSONObject(i); JSONObject properties = feature.getJSONObject("properties"); Double mag = properties.getDouble("mag"); String place = properties.getString("place"); Long time = properties.getLong("time"); String urlString = properties.getString("url"); URL url = new URL(urlString); listItem.add(new Info(mag, place, time, url)); } } catch (JSONException e) { e.printStackTrace(); } return listItem; } }上面 QueryUtils 类 一个网络请求的类,我们需要使用 AsyncTask 内部类,调用我们这个网络访问类。
/*创建一个AsyncTask的内部类*/ private class EarthAsyncTask extends AsyncTask<String, Void, List<Info>> { @Override protected List<Info> doInBackground(String... urls) { Log.i(TAG_LOG, "doInBackground()"); if (urls.length < 1 || urls[0] == null) return null; List<Info> result = null; try { result = QueryUtils.fetchEarthquakeDatas(urls[0]); } catch (MalformedURLException e) { e.printStackTrace(); } return result; } @Override protected void onPostExecute(List<Info> result) { /* 清除之前地震数据的适配器*/ adapter.clear(); /* add与addALL的区别在于add方法即便加入一个list也只是保存其中一条,addAll 保存全部*/ if (result != null && !result.isEmpty()) adapter.addAll(result); } }当我们旋转屏幕的时候,会改变设备配置。设备配置是用来描述设备当前状态的一系列特征。当我们旋转屏幕时,我们重新创建了一个新的活动,相当于创建了两个 AsyncTask ,可能这个问题看起来没什么问题,我们都知道我们手机有内存限制,当我们频繁的旋转设备,就会造成大量的 AsyncTask 的资源占用,还有就是每次设备更改时,都会访问一次 Internet ,这不仅仅是造成不必要的流量丢失,还造成数据利用率极低。需要注意的,当 AsyncTask 数量增加时,Android 系统并不会自动释放不用的资源,因为 AsyncTask 是原始活动的的内部类,所以只有 AsyncTask 结束才会统一释放资源,如何处理上述所说的问题,这里就用到 Loader
关于 Loader 介绍,我们可以查看官方文档
在上述的项目中,我们如何加入 AsyncTaskLoader 来处理设备配置的问题? 首先,我们先创建一个子类继承 AsyncTaskLoader 并实现一个最重要的方法loadInBackground(),该方法和doInBackground()原理相同
其次,创建完这个 AsyncTaskLoader 的子类,我们需要用到一个 LoaderManager 顾名思义,它是管理我们 Loader 的类,要想让我们的类使用 Loader 就必须使用 LoaderManager.LoaderCallbacks 接口,这样 LoaderManager 就可以通知我们创建 加载器 Loader 。 并且实现三个方法
onCreateLoader() 当loadermanager调用initLoader()时, 首先检查指定的id是否存在,如果不存在才会触发该方法,通过该方法才能创建一个loaderonLoadFinished() 当一个加载器 完成了它的装载过程后被调用onLoaderReset() 当一个加载器 被重置而什其数据无效时被调用不多说直接贴代码
public class EarthquakeActivity extends AppCompatActivity implements android.app.LoaderManager.LoaderCallbacks<List<Info>> { private TextView emptyTextView; private InfoAdapter adapter; private static final String REQUEST_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&orderby=time&minmag=5&limit=10"; @Override protected void onCreate(Bundle savedInstanceState) { Log.i(TAG_LOG, "onCreate()"); super.onCreate(savedInstanceState); setContentView(R.layout.earthquake_activity); emptyTextView = (TextView) findViewById(R.id.empty_tv); /*获取listView*/ ListView earthquakeListView = (ListView) findViewById(R.id.list); /*将空视图和list进行挂接,实现当加载不出来时,显示提示文本*/ earthquakeListView.setEmptyView(emptyTextView); adapter = new InfoAdapter(this, new ArrayList<Info>()); /*绑定适配器*/ earthquakeListView.setAdapter(adapter); /*判断是否联网*/ ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetWork = cm.getActiveNetworkInfo(); if (activeNetWork != null && activeNetWork.isConnectedOrConnecting()) { /*初始化loader*/ getLoaderManager().initLoader(0, null, this); /*添加项目点击监听器*/ Log.i(TAG_LOG, "initLoader()"); } else { View loading_progress_Bar = findViewById(R.id.loading_progressbar); loading_progress_Bar.setVisibility(View.GONE); emptyTextView.setText("no Internet"); } earthquakeListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { /*查找单机的当前地震*/ Info currentInfo = (Info) adapter.getItem(i); /*获取uri对象???*/ Uri infoUri = Uri.parse(currentInfo.getURL().toString()); /*创建一个新的Intent*/ Intent intent = new Intent(Intent.ACTION_VIEW, infoUri); startActivity(intent); } }); /* 实例化AsyncTask 并执行*/ EarthAsyncTask task = new EarthAsyncTask(); task.execute(REQUEST_URL); } @Override public android.content.Loader<List<Info>> onCreateLoader(int i, Bundle bundle) { Log.i(TAG_LOG, "onCreateLoader()"); return new EarthquakeLoader(this, REQUEST_URL); } @Override public void onLoadFinished(android.content.Loader<List<Info>> loader, List<Info> infos) { Log.i(TAG_LOG, "onLoadFinished()"); adapter.clear(); /* 如果存在 {@link Earthquake} 的有效列表,则将其添加到适配器的 数据集。这将触发 ListView 执行更新。 add与addALL的区别在于add方法即便加入一个list也只是保存其中一条,addAll 保存全部*/ if (infos != null && !infos.isEmpty()) adapter.addAll(infos); View loading_progress_Bar = findViewById(R.id.loading_progressbar); loading_progress_Bar.setVisibility(View.GONE); emptyTextView.setText("no Text"); } @Override public void onLoaderReset(android.content.Loader<List<Info>> loader) { Log.i(TAG_LOG, "onLoaderReset()"); adapter.clear(); } /*创建一个AsyncTask的内部类*/ private class EarthAsyncTask extends AsyncTask<String, Void, List<Info>> { @Override protected List<Info> doInBackground(String... urls) { Log.i(TAG_LOG, "doInBackground()"); if (urls.length < 1 || urls[0] == null) return null; List<Info> result = null; try { Log.i(TAG_LOG, "调用doInBackground+fetchEarthquakeDatas"); result = QueryUtils.fetchEarthquakeDatas(urls[0]); } catch (MalformedURLException e) { e.printStackTrace(); } return result; } @Override protected void onPostExecute(List<Info> result) { /* 清除之前地震数据的适配器*/ Log.i(TAG_LOG, "onPostExecute()"); adapter.clear(); /* 如果存在 {@link Earthquake} 的有效列表,则将其添加到适配器的 数据集。这将触发 ListView 执行更新。 add与addALL的区别在于add方法即便加入一个list也只是保存其中一条,addAll 保存全部*/ if (result != null && !result.isEmpty()) adapter.addAll(result); } } }总结:花了一下午时间去整理,后面的内容可能不是很详细,但是这种概念的东西,大家都可以的百度的到,还有有些代码都做了相关的注释,一些 UI 上的东西,跟本篇讲的可能关系不大,主要是处理应用的交互性,比如:判断是否联网,当没有联网的情况下提示文本,还有在进行网络访问的时候,添加一个进度条提示,应用正在处理。希望能给大家带来帮助!