返回介绍

2.1 网络低层封装

发布于 2024-08-17 23:46:12 字数 17360 浏览 0 评论 0 收藏 0

很多公司和团队都是用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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文