最近有空在看《App研发录》一书,良心之作。书中第一部分第二章节讲了不少关于网络底层封装的知识,看后觉得学到了不少干货。
索性自己也动手完成了一个非常轻量级的网络请求框架,从该书中获得了不少帮助。特此记录,回顾一下思路,整理收获。OK,一起来看。
就如书中所言,通常我们可以通过AsyncTask来进行网络请求的处理。而不少网络请求框架的底层也正是基于AsyncTask来进行封装的。
显然AsyncTask有很多优点,使用也十分便捷。但它肯定同样也存在缺点:即我们无法灵活控制其内部的线程池;无法取消请求等。
无法取消一个请求的情况是指:假设我们在Activity-A中有10个请求需要执行。那么可能因为网络条件等原因出现一种情况:
即用户已经通过某种操作从Activity-A跳转至B,这时B中也有网络请求需要执行。但Activity虽然已经跳转,而在其中发出的请求仍会继续进行。
那么,Activity-B中的请求就会因为等待Activity-A中的请求执行完毕而陷入阻塞。从而造成一种无限“拥堵”的情况。
我们自然需要避免之前的这些情况。所以我们的框架将采取原生 的ThreadPoolExecutor + Runnble + Handler + HttpUrlConnection实现。
我们先通过一张图,来看一看完成后的框架其最终的结构是怎么样的:
现在我们一次来分析一下它们的作用,由于篇幅的原因,这里不会一一贴出源码,该框架的github地址如下,有兴趣的朋友可以对应一看:
https://github.com/RawnHwang/PacHttpClient
现在我们首先肯定就是用一用它,先爽一下。然后简单的挨个分析一下框架中的每个类都扮演了什么角色。
PacHttpClientConfig config =
new PacHttpClientConfig(getApplicationContext())
.corePoolZie( ? )
.maxPoolSize( ? )
.keepAliveTime( ? )
.timeUnit( ? )
.blockingQueue( ? );
PacHttpClient.init(config);
而因为我们本身在框架里也设置过默认的线程池配置信息,所以我们也可以使用另一种更加偷懒的初始化方式:
PacHttpClientConfig config = new PacHttpClientConfig(getApplicationContext()); PacHttpClient.init(config);
/*HttpRequest request = */ PacHttpClient.invokeRequest(this, "testGet", null, new RequestCallback() {
@Override
public void onSuccess(String content) {
Log.d("TestMyFrameWork", "请求成功");
}
@Override
public void onFail(String errorMessage) {
Log.d("TestMyFrameWork", "请求失败");
}
});
运行程序,我们查看相关的日志打印,证明我们确实已经成功的与服务器进行了一次GET通信:
要模拟中断的情况也简单,我们只需要在Servlet服务器通过让线程休眠5秒来模拟实际情况中的读取数据的过程。也就是说:
当我们该次HTTP请求与服务器建立起链接后,read inputstrem的过程会经过5秒的时间。我们就在这个时间内,中断本次请求。
现在,我们在之前的代码的基础上,加上如下一句中断请求的代码:
PacHttpClient.cancelDesignatedRequest(this,request);
我们再次运行程序,经过耐心的等待,会发现不会得到任何相关的输出信息。因为请求的确已经被我们中断了。
for (int i = 0; i < 10; i++) { // PacHttpClient.invokeRequest()..... }
再次运行程序,我们会发现如下的输出情况:
这种情况我们是可以预料得到的,因为我们在框架中为线程池设置的核心池的默认大小为5,所以每次自然只会有5个线程来执行请求。
而当有请求执行完毕后,则会从阻塞队列中取出新的请求来执行。那么注意了,也就是说我们在Activity发出10个请求后:
有5个请求会率先开始执行,另外5个将会进入阻塞队列中等待。那么,我们也就可以测试我们的框架中的另一个方法了。
这时,我们可能会希望用两种方式应对。第一种就是,我们希望已经开始执行的请求继续。但将还没执行的请求中断。而第二种就像我们说过的:
假设我们跳转后的Activity也有请求需要执行,那么受之前的界面中的请求影响,所以我们希望中断跳转之前的Activity中的所有请求,包括正在执行的。
那么,这个时候就开心了。因为我们在自己的请求框架中已经对于这些情况做了封装。所以我们能很容易就能实现这种需求。
首先,我们来看看中断未执行的请求怎么样发生。我们在之前的代码的基础上加上如下代码:
PacHttpClient.cancelBlockingRequest(this);
然后,我们观察日志信息发现,还未来得及执行的5条请求的确是被取消了:
好的,现在修改如下的代码。这样做的目的在于:我们虽然发起了10个请求,但我们希望只要有某一个请求执行完毕,就取消剩余所有的请求(包括正在执行的)
public void onSuccess(String content) {
Log.d("TestMyFrameWork", "请求成功");
PacHttpClient.cancelAllRequest(MainActivity.this);
}
根据日志信息,我们可以验证我们的确实现了我们的目的:
由此,我们就可以针对于一些情况做出应对了。以我们说的跳转Activity希望取消请求而言,我们只需要在适合的声明周期调用对应的方法就搞定了。
简单的爽了一下,现在来简单分析下整个框架的构成。首先来说,当我们项目中的http-api越来越多,那么将这些url信息存放在代码中肯定是很不爽的。
那么,就像《App研发录》一书中推荐的一样,我们可以在xml目录下新建一个xml文件单独来管理我们的api。我们暂时将格式设定如下:
<?xml version="1.0" encoding="utf-8"?>
<url>
<Node expires="0" //缓存时间 key="testGet" //api key netType="GET" //请求方式 url="http://192.168.2.100:8080/LocalServer/hello.api"//api url />
</url>
现在有了存放url的xml文件。那么,对应的我们就需要一个类来解析xml文件,获取到相关的请求信息;并将读取到的信息存放进一个实体类以供使用。
URLEntity.java
class URLEntity {
private String key; //apiKey
private long expires; //缓存时间
private HttpRequest.RequestType netType; //请求方式(GET or POST)
private String url; //url
//相关的setter/getter
}
URLConfigManager.java
关于这个其实类没什么好说的,所做的工作就是解析xml文件,并将读取的信息存放进URLEntity对象。唯一值得注意的一点是:
如果每次读取url都从xml文件进行解析,肯定影响效率。所以我们在初次读取时,一次性将所有url读进内存中的map存放,以后就直接从map中读取。
RequestThreadPool.java
我们说过框架将采取原生的RequestThreadPool实现,该类实际就是对线程池的一个封装。并提供相关的操作线程池的方法。
class RequestThreadPool {
// 封装的线程池
private static ThreadPoolExecutor pool;
/** * 根据配置信息初始化线程池 */
static void init(){
PacHttpClientConfig config = PacHttpClient.config;
pool = new ThreadPoolExecutor(config.corePoolZie,
config.maxPoolSize, config.keepAliveTime,
config.timeUnit, config.blockingQueue);
}
/** * 执行任务 * @param r */
public static void execute(final Runnable r) {}
/** * 清空阻塞队列 */
static void removeAllTask() {}
/** * 从阻塞队列中删除指定任务 * @param obj * @return */
static boolean removeTaskFromQueue(final Object obj) {}
/** * 获取阻塞队列 * @return */
static BlockingQueue<Runnable> getQuene(){}
/** * 关闭,并等待任务执行完成,不接受新任务 */
static void shutdown() {}
/** * 关闭,立即关闭,并挂起所有正在执行的线程,不接受新任务 */
static void shutdownRightnow() {}
}
HttpRequest.java
这可以说是最关键的一个类了,在这个类当中,我们通过HttpUrlConncetion完成对请求的实际封装。
public class HttpRequest implements Runnable {
//some code...
@Override
public void run() {
// 判断请求类型
switch (urlInfo.getNetType()) {
case GET:
// 类型为HTTP-GET时,将请求参数组装到URL链接字符串上
String trulyURL;
if (params != null && !params.isEmpty()) {
StringBuilder urlBuilder = new StringBuilder(urlInfo.getUrl());
urlBuilder.append("?").append(convertParam2String());
trulyURL = urlBuilder.toString();
} else {
trulyURL = urlInfo.getUrl();
}
// 正式发送GET请求到服务器
sendHttpGetToServer(trulyURL);
break;
case POST:
// 发送POST请求到服务器
sendHttpPostToServer(urlInfo.getUrl());
break;
default:
break;
}
}
/** * 发起GET请求 * * @param url */
private void sendHttpGetToServer(String url) {
try {
mURL = new URL(url);
mConnection = (HttpURLConnection) mURL.openConnection();
// 连接服务器的超时时长
mConnection.setConnectTimeout(5000);
// 从服务器读取数据的超时时长
mConnection.setReadTimeout(8000);
if (mConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
// 如果未设置请求中断,则进行读取数据的工作
if (!interrupted) {
// read content from response..
final String result = readFromResponse(mConnection.getInputStream());
// call back
if (callback != null) {
handler.post(new Runnable() {
@Override
public void run() {
callback.onSuccess(result);
}
});
}
} else { // 中断请求
return;
}
} else {
handleNetworkError("网络异常");
}
} catch (MalformedURLException e) {
handleNetworkError("网络异常");
} catch (IOException e) {
handleNetworkError("网络异常");
} finally {
hostManager.requests.remove(this);
}
}
// some code....
/** * 中断请求 */
void disconnect() {
// 设置标志位
interrupted = true;
// 如果当前请求正处于与服务器连接状态下,则断开连接
if (mConnection != null)
mConnection.disconnect();
}
}
我们保留了部分关键代码,其实该类的核心工作从上面的代码基本上能够得以体现。
我们这里关注的重点放在“取消”请求。“取消”的情况实际上大体可以分为三种:
RequestManager.java
因为我们知道一个activity通常肯定不会只有一个请求需要执行。所以,我们需要一个对象来管理activity中的所有请求。
class RequestManager {
ArrayList<HttpRequest> requests;
public RequestManager() {
requests = new ArrayList<>();
}
/** * 无参数调用 */
public HttpRequest createRequest(URLEntity url, RequestCallback requestCallback) {
return createRequest(url, null, requestCallback);
}
/** * 有参数调用 */
public HttpRequest createRequest(URLEntity url, List<RequestParameter> params, RequestCallback requestCallback) {
HttpRequest request = new HttpRequest(this, url, params, requestCallback);
addRequest(request);
return request;
}
/** * 添加Request到列表 */
public void addRequest(final HttpRequest request) {
requests.add(request);
}
/** * 取消所有的网络请求(包括正在执行的) */
public void cancelAllRequest() {
BlockingQueue queue = RequestThreadPool.getQuene();
for (int i = requests.size() - 1; i >= 0; i--) {
HttpRequest request = requests.get(i);
if (queue.contains(request)) {
queue.remove(request);
} else {
request.disconnect();
}
}
requests.clear();
}
/** * 取消未执行的网络请求 */
public void cancelBlockingRequest() {
// 取交集(即取出那些在线程池的阻塞队列中等待执行的请求)
List<HttpRequest> intersection = (List<HttpRequest>) requests.clone();
intersection.retainAll(RequestThreadPool.getQuene());
// 分别删除
RequestThreadPool.getQuene().removeAll(intersection);
requests.removeAll(intersection);
}
/** * 取消指定的网络请求 */
public void cancelDesignatedRequest(HttpRequest request) {
if (!RequestThreadPool.removeTaskFromQueue(request)) {
request.disconnect();
}
}
}
RequestParameter.java
这个类也很简单,就是对请求参数做一个封装,简单的来说就是封装请求参数的键值对。
RequestCallback.java
很显然,通常我们都会根据请求从服务器返回的数据来执行一些操作。所以,我们还需要一个回调接口:
public interface RequestCallback
{
void onSuccess(String content);
void onFail(String errorMessage);
}
PacHttpClient.java
实际上,现在我们已经万事俱备了。但我们不希望框架的使用者直接接触我们底层封装的这些类。所以我们来提供一个共有的调用类。
这个类的工作很简单,就是提供一些共有的方法供用户调用,来完成发起请求,中断请求,关闭线程池等操作。
该类中还有一个关键的变量managerMap。我们说了,每个activity都需要自己的RequestManager来管理自身的所有请求。
这个意义在于,调用者在Activity执行响应的请求操作时,只需要传入自身this对象,我们就能够找到对应的请求进行操作。
// 存放每个Activity对应的RequestManager
static Map<Activity, RequestManager> managerMap;
当然,我们需要为我们的框架提供一个酷酷的名字。因为读书的时候就是一位伟大的已故HipHop大神2pac的脑残粉,所以就叫PacHttpClient吧。
PacHttpClientConfig.java
我们还可以支持让用户来自己定制关于网络框架的一些相关信息,目前这里主要是提供对于线程池的配置信息以及context的设置。
ImageLoader.getInstance.init(config);
上面这样类似的代码一定很熟悉吧,我们这里定义的此类也是提供同样的效果。
好了,就总结到这里了。
PS:显然这个小小的框架还非常的不成熟,距离能够在实际开发中使用的程度还非常远,重点旨在提供一个思路,请多多指教!。