2.1 网络低层封装
很多公司和团队都是用AsyncTask来封装网络底层,因为这个类非常好用,内部封装了很多好用的方法,但缺点是可扩展性不高。
本节将介绍一种自定义的网络底层框架,我们可以基于这个框架随心所欲地增加自定义的逻辑,完成很多高级功能。
让我们先从MobileAPI网络请求格式入手。
2.1.1 网络请求的格式
对于网络请求,我们一般定义为GET和POST即可,GET为请求数据,POST为修改数据(增删改)。
1.Request格式
所有的MobileAPI都可以写作http://www.xxx.com/aaaa.api 的形式。
·对于GET,我们可以写作:http://www.xxx.com/aaaa.api?k1=va&k2=v2 的形式,也就是说,把key-value这样的键值对存放在URL上。之所以这样设计,是为了更方便地定义数据缓存。我们尽量使GET的参数都是string、int这样的简单类型。
·对于POST,我们将key-value这样的键值对存放在Form表单中,进行提交。POST经常会提交大量数据,所以有些键值对要定义成集合或者复杂的自定义实体,这时我们就需要将这样的值转换为JSON字符串进行提交,由App传递到MobileAPI后,再将JSON字符串转换为对应的实体。
上述介绍只是一家之言,不同公司有不同的实现方式,这取决于服务器端的设计。
2.Response格式
我们一般使用JSON作为MobileAPI返回的结果。最规范的JSON数据返回格式如下。
JSON数据格式1:
{ "isError" : true, "errorType" : 1, "errorMessage" : "网络异常", "result" : "" }
JSON数据格式2:
{ "isError" : false, "errorType" : 0, "errorMessage" : "", "result" : { "cinemaId" : 1, "cinemaName" : "星美" } }
这里,isError是调用MobileAPI成功与否,errorType是错误类型(如果成功则为0),errorMessage是错误消息(如果成功则为空),result是成功请求返回的数据结果(如果失败则返回空)。
既然所有的JSON都返回isError、errorType、errorMessage、result这4个字段,我们不妨定义一个Response实体类,作为所有JSON实体的最外层,代码如下所示:
public class Response { private boolean error; private int errorType; // 1为 Cookie失效 private String errorMessage; private String result; public boolean hasError() { return error; } public void setError(boolean hasError) { this.error = hasError; } public String getErrorMessage() { return errorMessage; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } public String getResult() { return result; } public void setResult(String result) { this.result = result; } public int getErrorType() { return errorType; } public void setErrorType(int errorType) { this.errorType = errorType; } }
如果成功返回了数据,数据会存放在result字段中,映射为Response实体的result属性。
上面的JSON数据返回的是一笔影院数据,如果返回的result是很多影院的数据集合,那么就要把result解析为相应的实体集合,如下所示:
{ "isError" : false, "errorType" : 0, "errorMessage" : "", "result" : [ {"cinemaId" : 1, "cinemaName" : "星美"}, {"cinemaId" : 2, "cinemaName" : "万达"} ] }
2.1.2 AsyncTask的使用和缺点
对AsyncTask的封装属于网络底层的技术,所以AsyncTask应该封装在AndroidLib类库中,而不是具体的项目里。
对网络异常的分类,也就是Response类中的errorType字段,分析如下:
·一种是请求发送到MobileAPI,MobileAPI执行过程中发现的异常,这时候要自定义错误类型,也就是errorType,比如说1是Cookie过期,2是第三方支付平台不能连接,等等,这些已知的错误都是大于0的整数,因接口不同而各自定义不同。
·另一种是在App访问MobileAPI接口时发生的异常,有可能App自身网络不稳定,有可能因为网络传输不好导致返回了空值,这些异常情况我们都标记为负数。
基于上述分析,AsyncTask的doInBackground方法复写为:
@Override protected Response doInBackground(String… url) { return getResponseFromURL(url[0]); } private Response getResponseFromURL(String url) { Response response = new Response(); HttpGet get = new HttpGet(url); String strResponse = null; try { HttpParams httpParameters = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(httpParameters, 8000); HttpClient httpClient = new DefaultHttpClient(httpParameters); HttpResponse httpResponse = httpClient.execute(get); if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { strResponse = EntityUtils.toString(httpResponse.getEntity()); } } catch (Exception e) { response.setErrorType(-1); response.setError(true); response.setErrorMessage(e.getMessage()); } if (strResponse == null) { response.setErrorType(-1); response.setError(true); response.setErrorMessage("网络异常,返回空值"); } else { strResponse = "{'isError':false,'errorType':0,'errorMessage':'', 'result':{'city':'北京','cityid':'101010100','temp':'17', 'WD':'西南风','WS':'2级','SD':'54%','WSE':'2','time':'23:15', 'isRadar':'1','Radar':'JC_RADAR_AZ9010_JB','njd':'暂无实况','qy':'1016'}}"; response = JSON.parseObject(strResponse, Response.class); } return response; }
相应的,在AsyncTask的onPostExecute方法中,我们要对错误类型进行分类,从而进一步回调:
public abstract class RequestAsyncTask extends AsyncTask<String, Void, Response> { public abstract void onSuccess(String content); public abstract void onFail(String errorMessage); @Override protected void onPreExecute() { } @Override protected void onPostExecute(Response response) { if(response.hasError()) { onFail(response.getErrorMessage()); } else { onSuccess(response.getResult()); } }
目前我们只定义了onSuccess和onFail两个回调函数,将网络返回值简单地分为成功与失败两种情况。在2.4.3节,我们将详细介绍如何在网络底层封装对Cookie过期时的异常处理。
在相应的Activity页面,调用AysncTask如下所示:
protected void loadData() { String url = "http://www.weather.com.cn/data/sk/101010100.html"; RequestAsyncTask task = new RequestAsyncTask() { @Override public void onSuccess(String content) { // 第2种写法,基于fastJSON WeatherEntity weatherEntity = JSON.parseObject(content, WeatherEntity.class); WeatherInfo weatherInfo = weatherEntity.getWeatherInfo(); if (weatherInfo != null) { tvCity.setText(weatherInfo.getCity()); tvCityId.setText(weatherInfo.getCityid()); } } @Override public void onFail(String errorMessage) { new AlertDialog.Builder(WeatherByFastJsonActivity.this) .setTitle("出错啦").setMessage(errorMessage) .setPositiveButton("确定", null).show(); } }; task.execute(url); }
网上关于如何使用AsyncTask的文章不胜枚举,大家都在欣赏它的优点,却忽略了它的致命缺点,那就是不能灵活控制其内部的线程池。
线程池里面的每个线程存放的都是MobileAPI的调用请求,而AsyncTask中又没有暴露出取消这些请求的方法,也就是我们熟知的CancelRequest方法,所以,一旦从A页面跳转到B页面,那么在A页面发起的MobileAPI请求,如果还没有返回,并不会被取消。
对于一款频繁调用MobileAPI的应用类App而言,最严重的情况发生在首页到二级页面的跳转,因为在首页会调用十几个MobileAPI接口,视网络情况而定,如果是WiFi,应该很快就能请求到数据,不会产生积压,但如果是3G或者2G,那么请求就会花费很长时间,而我们在这期间就跳转到二级页面,而这个二级页面也会调用MobileAPI接口,那么将得不到任何结果,因为首页的请求还在排队处理中,之前的那十几个MobileAPI接口的数据还都遥遥无期在线程池里排队呢,就更不要说当前页面这个请求了。
如果你不信,我们可以做个试验。记录每次MobileAPI请求发起和接收数据的时间点,你会看到,在迅速进入二级页面后,首页的十几个MobileAPI请求只有发起时间并没有返回时间,说明它们还在处理过程中,都被堵塞了。
2.1.3 使用原生的ThreadPoolExecutor+Runnable+Handler
既然AsyncTask有诸多问题,那么退而求其次,使用ThreadPoolExecutor+Runnable+Handler的原生方式,对网络底层进行封装。
接下来我将介绍一个非常轻量级的网络底层框架。它由以下9个类组成,如图2-1所示。
图2-1 轻量级的网络底层框架
图中只列出了8个,还有1个RemoteService类,位于YoungHeart项目的engine包中。下面分别介绍。
1.UrlConfigManager和URLData
我们把App所要调用的所有MobileAPI接口的信息都放在url.xml文件中,如下所示:
<?xml version="1.0" encoding="UTF-8"?> <url> <Node Key="getWeatherInfo" Expires="300" NetType="get" Url="http://www.weather.com.cn/data/sk/101010100.html" /> <Node Key="login" Expires="0" NetType="post" Url="http://www.weather.com.cn/data/login.api" /> </url>
在使用上,通过UrlConfigManager的findURL方法,在上述xml文件中找到当前MobileAPI调用的节点,其中每一个MobileAPI接口都对应一个URLData实体,如下所示:
public class URLData { private String key; private long expires; private String netType; private String url;
目前我们只用到key、url和netType这3个属性。expires是用来做数据缓存的,我们2.2节会介绍到它的作用。
这样,发起一次MobileAPI网络请求的所有数据就都准备好了。
2.RemoteService和RequestCallback、RequestParameter
这里的3个类是暴露给App用来调用MobileAPI接口的,举个例子,在WeatherBy-FastJsonActivity中的调用形式如下:
@Override protected void loadData() { weatherCallback = new RequestCallback() { @Override public void onSuccess(String content) { WeatherInfo weatherInfo = JSON.parseObject(content, WeatherInfo.class); if (weatherInfo != null) { tvCity.setText(weatherInfo.getCity()); tvCityId.setText(weatherInfo.getCityid()); } } @Override public void onFail(String errorMessage) { new AlertDialog.Builder(WeatherByFastJsonActivity.this) .setTitle("出错啦").setMessage(errorMessage) .setPositiveButton("确定", null).show(); } }; ArrayList<RequestParameter> params = new ArrayList<RequestParameter>(); RequestParameter rp1 = new RequestParameter("cityId", "111"); RequestParameter rp2 = new RequestParameter("cityName", "Beijing"); params.add(rp1); params.add(rp2); RemoteService.getInstance().invoke(this, "getWeatherInfo", params, weatherCallback); }
对上述方法介绍如下:
·RequestCallback是回调,目前有onSuccess和onFail两种。
·RequestParameter是用来传递调用MobileAPI接口所需参数的键值对的。我们原本可以使用HashMap<String,String>这样的数据结构,但是HashMap比较耗费内存,虽然它的查找速度是o(1),而对于MobileAPI接口的参数而言,数据一般不会太多,查找速度快体现不出优势来,所以我们使用ArrayList<RequestParameter>这样的数据结构。
·RemoteService这个单例是用来发起请求的,它会创建一个request,并将其添加到RequestManager中,然后放到DefaultThreadPool的一个线程中去执行这个request。
3.RequestManager
RequestManager这个集合类是用于取消请求(cancelRequest)的。因为每次发起请求,都会把为此创建的request添加到RequestManager中,所以RequestManager保存了全部request。
从ActivityA跳转到ActivityB,为了不产生阻塞,要取消ActivityA中的所有未完成的请求。这时候就需要RequestManager的cancelRequest方法出力了,它会遍历之前保存的所有request,不管三七二十一,全部终止。如下所示:
public void cancelRequest() { if ((requestList != null) && (requestList.size() > 0)) { for (final HttpRequest request : requestList) { if (request.getRequest() != null) { try { request.getRequest().abort(); requestList.remove(request.getRequest()); } catch (final UnsupportedOperationException e) { e.printStackTrace(); } } } } }
我们在BaseActivity中,会保持对RequestManager的一个引用,这样在onDestroy和onPause的时候,执行RequestManager的cancelRequest方法就可以取消所有未完成的请求了:
public abstract class BaseActivity extends Activity { // 请求列表管理器 protected RequestManager requestManager = null; protected void onDestroy() { // 在activity销毁的同时设置停止请求,停止线程请求回调 if (requestManager != null) { requestManager.cancelRequest(); } super.onDestroy(); } protected void onPause() { // 在activity停止的同时设置停止请求,停止线程请求回调 if (requestManager != null) { requestManager.cancelRequest(); } super.onPause(); } public RequestManager getRequestManager() { return requestManager; } }
4.DefaultThreadPool
DefaultThreadPool只是对ThreadPoolExecutor和ArrayBlockingQueue的简单封装。我们可以认为它就是一个线程池,每发起一次请求(runnable),就由线程池分配一个新的线程来执行该请求。
网上关于ThreadPoolExecutor和ArrayBlockingQueue的文章不胜枚举,请大家自行参阅。线程池不是本书的重点,这里不再花篇幅去讨论。
5.HttpRequest
HttpRequest是发起Http请求的地方,它实现了Runnable,从而让DefaultThreadPool可以分配新的线程来执行它,所以,所有的请求逻辑都在Runnable接口的run方法中,其中:
·对于get形式的MobileAPI接口,它会把从上层传递进来的ArrayList<RequestParameter>,解析为urlk1=v1&k2=v2这样的形式。
·对于post格式的MobileAPI接口,它会把从上层传递进来的ArrayList<RequestParameter>,转为BasicNameValuePair的形式,放到表单中进行提交。
需要注意的是,因为我们把每个HttpRequest都放在了新的子线程上执行,所以回调RequestCallback的onSuccess方法时,不能直接操作UI线程上的控件,所以我们在HttpRequest类中使用了Handler:
if (responseInJson.hasError()) { handleNetworkError(responseInJson.getErrorMessage()); } else { handler.post(new Runnable() { @Override public void run() { HttpRequest.this.requestCallback .onSuccess(responseInJson.getResult()); } }); }
这样就保证了RequestCallback的onSuccess方法是在UI线程上的,从而可以在Activity中编写这样的代码而不报错:
weatherCallback = new RequestCallback() { @Override public void onSuccess(String content) { WeatherInfo weatherInfo = JSON.parseObject(content, WeatherInfo.class); if (weatherInfo != null) { tvCity.setText(weatherInfo.getCity()); tvCityId.setText(weatherInfo.getCityid()); } }
Response这个类就不讨论了,本章前面已经介绍过了。
2.1.4 网络底层的一些优化工作
我们的网络底层越来越强大了,是否有意犹未尽的感受?接下来将完善这个框架,修复其中的一些瑕疵,如onFail的统一处理机制、UrlConfigManager的优化、ProgressBar的处理等。
1.onFail的统一处理机制
如果访问MobileAPI请求失败,我们一般希望只是在App上简单地弹出一个提示框,告诉用户网络有异常。
也就是说,对于每个在Activity中声明的RequestCallback实例而言,尽管每个onSuccess方法的处理逻辑各不相同,但每个onFail方法都是一样的逻辑和代码,如下所示:
weatherCallback = new RequestCallback() { @Override public void onSuccess(String content) { WeatherInfo weatherInfo = JSON.parseObject(content, WeatherInfo.class); if (weatherInfo != null) { tvCity.setText(weatherInfo.getCity()); tvCityId.setText(weatherInfo.getCityid()); } } @Override public void onFail(String errorMessage) { new AlertDialog.Builder(WeatherByFastJsonActivity.this) .setTitle("出错啦").setMessage(errorMessage) .setPositiveButton("确定", null).show(); } };
我不希望每次都编写同样的onFail方法,这会使程序很臃肿。于是在AppBaseActivity中写一个自定义类AbstractRequestCallback,如下所示:
public abstract class AppBaseActivity extends BaseActivity { public abstract class AbstractRequestCallback implements RequestCallback { public abstract void onSuccess(String content); public void onFail(String errorMessage) { new AlertDialog.Builder(AppBaseActivity.this) .setTitle("出错啦").setMessage(errorMessage) .setPositiveButton("确定", null).show(); } } }
那么我们的weatherRequestCallback的实例化就可以改写如下,可以看到,不再需要重写onFail方法:
weatherCallback = new AbstractRequestCallback() { @Override public void onSuccess(String content) { WeatherInfo weatherInfo = JSON.parseObject(content, WeatherInfo.class); if (weatherInfo != null) { tvCity.setText(weatherInfo.getCity()); tvCityId.setText(weatherInfo.getCityid()); } } };
当然,如果有些MobileAPI接口在返回错误时需要App特殊处理,比如重启App或者啥都不做,我们只需要在实例化AbstractRequestCallback时,重写onFail方法即可,如下所示。重写的onFail方法是一个空方法,表示出错时啥都不做:
weatherCallback = new AbstractRequestCallback() { @Override public void onSuccess(String content) { WeatherInfo weatherInfo = JSON.parseObject(content, WeatherInfo.class); if (weatherInfo != null) { tvCity.setText(weatherInfo.getCity()); tvCityId.setText(weatherInfo.getCityid()); } } @Override public void onFail(String errorMessage) { // 重启App或者啥都不做 } };
2.UrlConfigManager的优化
在UrlConfigManager的实现上,我们采取的策略是每发起一次MobileAPI请求,都会读取url.xml文件,把符合这次MobileAPI接口调用的参数取出来。
在一个大量调用MobileAPI的App中,这样的设计会造成频繁读xml文件,性能很差。于是我们对其进行改造,在App启动时,一次性将url.xml文件都读取到内存,把所有的UrlData实体保存在一个集合中,然后每次调用MobileAPI接口,直接从内存的这个集合中查找。考虑到内存中的数据会被回收,所以上述这个集合一旦为空,我们要从url.xml中再次读取。
基于上述方案,我们对UrlConfigManager的findUrl方法进行改造:
public static URLData findURL(final Activity activity, final String findKey) { // 如果urlList还没有数据(第一次) // 或者被回收了,那么(重新)加载xml if (urlList == null || urlList.isEmpty()) fetchUrlDataFromXml(activity); for (URLData data : urlList) { if (findKey.equals(data.getKey())) { return data; } } return null; }
其中,fetchUrlDataFromXml方法就不多说了,它的工作就是把xml的数据都搬到内存集合urlList中。
3.不是每个请求都需要回调的
有些时候,我们调用一个MobileAPI接口,并不需要知道调用成功与否以及返回结果是什么,比如向MobileAPI发送打点统计数据。那就是说,我们不需要回调函数了,那么代码可以写为:
void loadAPIData3() { ArrayList<RequestParameter> params = new ArrayList<RequestParameter>(); RequestParameter rp1 = new RequestParameter("cityId", "111"); RequestParameter rp2 = new RequestParameter("cityName", "Beijing"); params.add(rp1); params.add(rp2); RemoteService.getInstance() .invoke(this, "getWeatherInfo", params, null); }
我们将空的RequestCallback传给HttpRequest,那么在HttpRequest处理请求返回的结果时,就需要添加HttpRequest是否为空的判断,不为空,才会处理返回结果;否则,发起MobileAPI请求后什么都不做。
有以下两个地方需要修改:
1)处理请求时:
response = httpClient.execute(request); if ((requestCallback != null)) { // 获取状态 final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == HttpStatus.SC_OK) { final ByteArrayOutputStream content = new ByteArrayOutputStream();
2)遇到异常,是否要回调onFail方法:
public void handleNetworkError(final String errorMsg) { if ((requestCallback != null)) { handler.post(new Runnable() { @Override public void run() { HttpRequest.this.requestCallback .onFail(errorMsg); } }); } }
4.ProgressBar的处理
在调用MobileAPI的时候,会显示进度条ProgressBar,直到返回结果到onSuccess或onFail回调方法,ProgressBar才会消失。
由于App要保持风格统一,所以所有页面的ProgressBar应该长得一样。那么我们就可以将其定义在AppBaseActivity中,如下所示:
public abstract class AppBaseActivity extends BaseActivity { protected ProgressDialog dlg; public abstract class AbstractRequestCallback implements RequestCallback { public abstract void onSuccess(String content); public void onFail(String errorMessage) { dlg.dismiss(); new AlertDialog.Builder(AppBaseActivity.this) .setTitle("出错啦").setMessage(errorMessage) .setPositiveButton("确定", null).show(); } } }
在使用的时候,在开始调用MobileAPI的地方,执行show方法;在onSuccess和onFail方法的开始,执行dismiss方法:
@Override protected void loadData() { dlg = Utils.createProgressDialog(this, this.getString(R.string.str_loading)); dlg.show(); loadAPIData1(); } void loadAPIData1() { weatherCallback = new AbstractRequestCallback() { @Override public void onSuccess(String content) { dlg.dismiss(); WeatherInfo weatherInfo = JSON.parseObject(content, WeatherInfo.class); if (weatherInfo != null) {
不要把Dialog的show方法和dismiss方法封装到网络底层。网络底层的调用经常是在子线程执行的,子线程是不能操作Dialog、Toast和控件的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论