自己动手写一个轻量级的Android网络请求框架

最近有空在看《App研发录》一书,良心之作。书中第一部分第二章节讲了不少关于网络底层封装的知识,看后觉得学到了不少干货。
索性自己也动手完成了一个非常轻量级的网络请求框架,从该书中获得了不少帮助。特此记录,回顾一下思路,整理收获。OK,一起来看。

就如书中所言,通常我们可以通过AsyncTask来进行网络请求的处理。而不少网络请求框架的底层也正是基于AsyncTask来进行封装的。
显然AsyncTask有很多优点,使用也十分便捷。但它肯定同样也存在缺点:即我们无法灵活控制其内部的线程池;无法取消请求等。
无法取消一个请求的情况是指:假设我们在Activity-A中有10个请求需要执行。那么可能因为网络条件等原因出现一种情况:
即用户已经通过某种操作从Activity-A跳转至B,这时B中也有网络请求需要执行。但Activity虽然已经跳转,而在其中发出的请求仍会继续进行。
那么,Activity-B中的请求就会因为等待Activity-A中的请求执行完毕而陷入阻塞。从而造成一种无限“拥堵”的情况。
我们自然需要避免之前的这些情况。所以我们的框架将采取原生 的ThreadPoolExecutor + Runnble + Handler + HttpUrlConnection实现。

我们先通过一张图,来看一看完成后的框架其最终的结构是怎么样的:
自己动手写一个轻量级的Android网络请求框架_第1张图片
现在我们一次来分析一下它们的作用,由于篇幅的原因,这里不会一一贴出源码,该框架的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);
  • 好了,现在我们先来简单的发起一个GET请求。来体验一下框架的使用快感:
        /*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);

我们再次运行程序,经过耐心的等待,会发现不会得到任何相关的输出信息。因为请求的确已经被我们中断了。

  • 好的,现在我们来加大点力度。假设我们在一个Activity中发出10个请求,看看会发生什么。
        for (int i = 0; i < 10; i++) {
            // PacHttpClient.invokeRequest().....
        }

再次运行程序,我们会发现如下的输出情况:
自己动手写一个轻量级的Android网络请求框架_第2张图片
这种情况我们是可以预料得到的,因为我们在框架中为线程池设置的核心池的默认大小为5,所以每次自然只会有5个线程来执行请求。
而当有请求执行完毕后,则会从阻塞队列中取出新的请求来执行。那么注意了,也就是说我们在Activity发出10个请求后:
有5个请求会率先开始执行,另外5个将会进入阻塞队列中等待。那么,我们也就可以测试我们的框架中的另一个方法了。

  • 现在我们来测试另一种使用情况。假设我们在当前Activity发起了10个请求,但请求并未执行完毕,我们的Activity就跳转了。

这时,我们可能会希望用两种方式应对。第一种就是,我们希望已经开始执行的请求继续。但将还没执行的请求中断。而第二种就像我们说过的:
假设我们跳转后的Activity也有请求需要执行,那么受之前的界面中的请求影响,所以我们希望中断跳转之前的Activity中的所有请求,包括正在执行的。
那么,这个时候就开心了。因为我们在自己的请求框架中已经对于这些情况做了封装。所以我们能很容易就能实现这种需求。

首先,我们来看看中断未执行的请求怎么样发生。我们在之前的代码的基础上加上如下代码:

        PacHttpClient.cancelBlockingRequest(this);

然后,我们观察日志信息发现,还未来得及执行的5条请求的确是被取消了:
自己动手写一个轻量级的Android网络请求框架_第3张图片

好的,现在修改如下的代码。这样做的目的在于:我们虽然发起了10个请求,但我们希望只要有某一个请求执行完毕,就取消剩余所有的请求(包括正在执行的)

                public void onSuccess(String content) {
                    Log.d("TestMyFrameWork", "请求成功");
                    PacHttpClient.cancelAllRequest(MainActivity.this);
                }

根据日志信息,我们可以验证我们的确实现了我们的目的:

自己动手写一个轻量级的Android网络请求框架_第4张图片

由此,我们就可以针对于一些情况做出应对了。以我们说的跳转Activity希望取消请求而言,我们只需要在适合的声明周期调用对应的方法就搞定了。

  • 最后,PacHttpClient还有提供了另外两个公有方法:shutdown以及shutdownRightnow。顾名思义,就是关闭框架中封装的线程池的。
    例如,我们可以在退出应用的时候调用来关闭线程池释放资源。它们的不同就在于,shutdown虽然关闭线程池但会执行完当前线程池中剩余的任务。而shutdownRightnow则还会试图立刻结束当前剩余的线程。

简单的爽了一下,现在来简单分析下整个框架的构成。首先来说,当我们项目中的http-api越来越多,那么将这些url信息存放在代码中肯定是很不爽的。
那么,就像《App研发录》一书中推荐的一样,我们可以在xml目录下新建一个xml文件单独来管理我们的api。我们暂时将格式设定如下:


<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 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();
    }
}

我们保留了部分关键代码,其实该类的核心工作从上面的代码基本上能够得以体现。
我们这里关注的重点放在“取消”请求。“取消”的情况实际上大体可以分为三种:

  • 还未执行的请求,这种情况最易处理。因为未执行代表它现在处于线程池的阻塞队列中,我们只要把任务从阻塞队列中清楚就搞定了。
  • 另一种情况是该请求已经与服务器建立连接,这个时候,我们需要通过httpurlconnection.disconnect来中断连接。注意该方法会抛出IOException。
  • 还有另外一种可恶的状态,即可能mConnection的一些初始化工作已经执行了。但还未来及的从服务器read inputStream,线程便切换了。
    这个时候disconnect是没用的,因为现在实际根本就还没有与服务器建立连接。所以我们在从服务器读取数据的代码前加上了一个判断,
    通过标志位interrupted判断请求是否已经取消,从而决定是继续与服务器建立连接,read inputstream;还是直接中断任务。

RequestManager.java
因为我们知道一个activity通常肯定不会只有一个请求需要执行。所以,我们需要一个对象来管理activity中的所有请求。

class RequestManager {

    ArrayList requests;

    public RequestManager() {
        requests = new ArrayList<>();
    }

    /**
     * 无参数调用
     */
    public HttpRequest createRequest(URLEntity url, RequestCallback requestCallback) {
        return createRequest(url, null, requestCallback);
    }

    /**
     * 有参数调用
     */
    public HttpRequest createRequest(URLEntity url, List 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 intersection = (List) 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 managerMap;

当然,我们需要为我们的框架提供一个酷酷的名字。因为读书的时候就是一位伟大的已故HipHop大神2pac的脑残粉,所以就叫PacHttpClient吧。
PacHttpClientConfig.java
我们还可以支持让用户来自己定制关于网络框架的一些相关信息,目前这里主要是提供对于线程池的配置信息以及context的设置。

ImageLoader.getInstance.init(config);

上面这样类似的代码一定很熟悉吧,我们这里定义的此类也是提供同样的效果。

好了,就总结到这里了。
PS:显然这个小小的框架还非常的不成熟,距离能够在实际开发中使用的程度还非常远,重点旨在提供一个思路,请多多指教!。

你可能感兴趣的:(Android,杂记)