Volley源码分析

目录:
一、前言
二、基础层
   2.1 缓存模块
        2.1.1 Http缓存协议
        2.1.2 存储设计
        2.1.3 缓存总结
   2.2日志记录
        2.2.1 概念
        2.2.2 实例
        2.2.3 详细分析
           2.2.3.1日志开关
           2.2.3.2 格式化日志信息
   2.3异常模块
         2.3.1 类继承结构
   2.4 重试机制
三、网络层
   3.1.Http请求流程简介
   3.2.网络请求和响应对象封装
      3.2.1.Request
      3.2.2 FIFO优先级队列,请求排序
      3.2.3 Request中的抽象方法
      3.2.4 Response
   3.3网络请求引擎 
       3.3.1 HttpStack里的HttpURLConnection、HttpClient
       3.3.2.OkHttp
   3.4网络请求代理者
       3.4.1 NetWork的performRequest()方法工作流程图
四、控制层
    4.1 Volley内部请求的工作流程
       4.1.1 如何调度网络请求队列
       4.1.2 如何取消网络请求
    4.2 类的详细解析
        4.2.1 线程类CacheDispatcher的内部工作流程图
        4.2.2 线程类NetworkDispatcher的内部工作流程图
        4.2.3 ExecutorDelivery的内部工作原理
五、应用层
      5.1 使用StringRequest发起一次请求
六、总结

   
前言

本文是一篇日常学习总结性的文章,笔者通过分析经典网络框架Volley的源码,望以巩固Android网络框架中常见的多线程、IO、设计模式、SDK开发等方面的技术。

我相信当前80%安卓开发者使用的网络框架是Retrofit,但是我可以说99%开发者都没有没听过Volley,因为它的设计思想是史诗级的、经典的、值得反复阅读的。

在标准的网络协议中,前人把网络分不同层次进行开发,每一层分别负责不同的通信功能,同时每层之间能简单交互,业内比较规范的说法叫做“高内聚,低耦合”。比如 TCP/IP,是一组不同层次上的多个协议的组合。如下图:

TCP/IP协议簇的四个层次

所以,从上得到启示,按照分层的原则,逐层拨开Volley源码的面纱。

分层解耦

笔者认为Volley网络框架有如下层次结构:

volley架构.png

二、基础层

我们先看基础层,它上层都会需要依赖它这一层。它又好比一个建筑的地基,保证了整个框架的健壮性。

在基础层,我们能学习到:
1.缓存数据模块:通用的缓存策略。
2.日志模块:灵活地对事件打点。
3.异常模块:统一的异常处理。
4.重试机制:网络条件差情况下能提供重新请求的功能。

2.1 缓存模块
2.1.1 Volley的缓存机制

遵循HTTP缓存协议需自备梯子:)
概括的说http缓存协议利用http请求头中的cache-control和eTag字段进行是否缓存,缓存时间的判断。
主要过程如下图:

最佳 Cache-Control 策略.png

详细流程请看上面提到的链接。

2.1.2 存储设计
  • Cache接口:
    Cache接口,它制定了缓存的基本数据结构,即一个Key-Value键值对。其中Entry类作为值,它封装了一个HTTP响应数据,对应下图:
HTTP请求得到的响应头.png

其中两个重要的方法:

public Entry get(String key);//得到一个缓存对象
public void put(String key, Entry entry);//保存一个缓存对象
  • DiskBasedCache
    它是一个基础磁盘缓存类,其数据结构表如下:
DiskBasedCache属性.png

从表可知:
1.我们可以配置缓存的根目录,用于获取缓存文件对象和文件路径。
2.字段最大可用缓存和可用缓存容量来判断手机sdcard是否可以继续保存缓存。

  • CacheHeader :
    CacheHeader的一个内部类,用来读取和写入文件大小和缓存时间的一个头信息帮助类,除了文件实体内容。

DiskBasedCache的方法:

  • initialize():初始化DiskBasedCache,通过遍历根目录文件初始化DiskBasedCache。
    @Override
    public synchronized void initialize() {
        if (!mRootDirectory.exists()) {     -----1
            if (!mRootDirectory.mkdirs()) {
                VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
            }
            return;
        }
        File[] files = mRootDirectory.listFiles();  ------2
       ...
        for (File file : files) {  
            BufferedInputStream fis = null;
            try {
                fis = new BufferedInputStream(new FileInputStream(file));
                CacheHeader entry = CacheHeader.readHeader(fis);
                entry.size = file.length();
                putEntry(entry.key, entry);       --------3
            } catch (IOException e) {
             ...
    }

1.判断缓存根目录是否存在,不存在则新建。
2.遍历缓存目录。
3.依次读取文件的头信息,把所有已缓存的文件头信息以key存入mEntries中(从磁盘读到内存中)。

  • getFileNameForKey():得到文件名的hash值。把文件一分为二分别取hash值,后合并。

注意:这里得到唯一的文件名,并没有采取常见的md5加密文件名的办法,我认为这是考虑了字符串hash比md5加密哈希文件名时间效率更高。

    private String getFilenameForKey(String key) {
        int firstHalfLength = key.length() / 2;
        String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
        localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
        return localFilename;  
    }
  • get(String key); 根据key获取缓存对象。代码片段如下:
/**
     * Returns the cache entry with the specified key if it exists, null otherwise.
     */
    @Override
    public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get(key);   ------1
        // if the entry does not exist, return.
        if (entry == null) {
            return null;
        }
        File file = getFileForKey(key);     ------2
        CountingInputStream cis = null;
        try {
            cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
            CacheHeader.readHeader(cis); // eat header
            byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
            return entry.toCacheEntry(data);     ------3
        } catch (IOException e) {
         ...
         ...
        } finally {
                    cis.close();
    }

1.从mEntries这个map中匹配缓存在内存中的头信息,没有则返回null。
2.根据key查找磁盘中的缓存文件File。
3.将文件内容data字节解析到缓存对象Entry并返回。

总结:先读取文件头信息,然后读取文件主体内容,最后两者合并。

  • clear(); 在线程安全前提条件下,清除缓存文件:遍历根目录下面的所有文件,并删除。代码片段如下:
  public synchronized void clear() {
      File[] files = mRootDirectory.listFiles();
        if (files != null) {
            for (File file : files) {
                file.delete(); //删除磁盘中的文件
            }
        }
        mEntries.clear(); //清理内存
        mTotalSize = 0;//重新初始化总大小
        VolleyLog.d("Cache cleared.");
}
  • put(String key, Entry entry); 在线程安全前提条件下,保存一个缓存文件。代码片段如下:
 @Override
    public synchronized void put(String key, Entry entry) {
        pruneIfNeeded(entry.data.length);   //         ---------1
        File file = getFileForKey(key);
        try {
            BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
            CacheHeader e = new CacheHeader(key, entry);
            boolean success = e.writeHeader(fos);-----------2
            if (!success) {
                fos.close();
                VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
                throw new IOException();
            }
            fos.write(entry.data);  -------------3
            fos.close();
            putEntry(key, e);    ----------------4
            return;
        } catch (IOException e) {
        }
        boolean deleted = file.delete();
        if (!deleted) {
            VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
        }
    }

1.首先,判断本地缓存容量是否足够,不够则遍历删除旧文件,直到容量足够时停止。
2.把Entry的头信息写入本地文件中
3.把Entry的实体数据写入本地文件中
4.把从Entry得到头信息CacheHeader缓存到内存Map中。

2.1.3缓存总结

至此,缓存模块分析完毕,其中有几点技巧值得我们学习:
1.把文件保存分成内存和磁盘两部分。内存保存头信息(文件的大小、过期时间等),磁盘则保存文件的全部数据。这样每次读取缓存时可以先读内存判断头信息是否合法,若合法则去本地读取文件数据,否则放弃,这样可以减少耗时的文件读取时间,提升效率。
2.在文件信息安全的前提下,可以根据文件名字符串一分为二hash的办法,取代md5加密办法。

2.2日志记录
2.2.1 概念

日志记录主要用于程序运行期间发生的事件,以便于了解系统活动和诊断问题。它对于了解复杂系统的活动轨迹至关重要。
这段话摘自维基百科,可见日志记录在SDK开发或组件开发中扮演着不可或缺的角色,它可便于调试,保证开发系统的稳定性。

2.2.2 实例

了解了日志记录的概念后,那么我们看看Volley框架是如何记录日志:
首先客户端先发起一个网络请求:

String url = "http://www.fulaan.com/forum/fPostActivityAll.do?sortType=2&page=1&classify=-1&cream=-1&gtTime=0&postSection=&pageSize=25&inSet=1";
            StringRequest stringRequest = new StringRequest(Request.Method.GET, url, new Response.Listener<String>() {
                @Override
                public void onResponse(String response) {
                    mResultView.setText(response);
                    stopProgress();
                }
            }, new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    stopProgress();
                    showToast(error.getMessage());
                }
            });
Volley.addQueue(stringRequest);

然后我们看控制台logcat输出的信息:

07-03 16:09:43.350 11447-11518/com.mani.volleydemo V/Volley: [155] CacheDispatcher.run: start new dispatcher
07-03 16:09:45.310 11447-11519/com.mani.volleydemo D/Volley: [156] BasicNetwork.logSlowRequests: HTTP response for request=<[ ] http://www.fulaan.com/forum/fPostActivityAll.do?sortType=2&page=1&classify=-1&cream=-1&gtTime=0&postSection=&pageSize=25&inSet=1 0xd41c847b NORMAL 1> [lifetime=793], [size=187392], [rc=200], [retryCount=0]
07-03 16:09:45.546 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (1032 ms) [ ] http://www.fulaan.com/forum/fPostActivityAll.do?sortType=2&page=1&classify=-1&cream=-1&gtTime=0&postSection=&pageSize=25&inSet=1 0xd41c847b NORMAL 1
07-03 16:09:45.547 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+0   ) [ 1] add-to-queue
07-03 16:09:45.547 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+1   ) [155] cache-queue-take
07-03 16:09:45.549 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+1   ) [155] cache-hit-expired
07-03 16:09:45.549 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+0   ) [156] network-queue-take
07-03 16:09:45.549 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+794 ) [156] network-http-complete
07-03 16:09:45.551 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+4   ) [156] network-parse-complete
07-03 16:09:45.551 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+2   ) [156] network-cache-written
07-03 16:09:45.551 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+0   ) [156] post-response
07-03 16:09:45.551 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+230 ) [ 1] done
2.2.3 详细代码分析

我们可以看到在第一段代码中并没有使用android.util.log包中的Log.d(TAG,"")来打印信息,那这些日志是从哪里打印的呢?不难发现,这些日志记录是由Volley框架内部类VolleyLog打印出来的。

另外,从控制台的输出信息,我们了解到日志打点了一些关键信息:事件名称、事件发生的开始时间、线程ID、事件消耗总时间、事件内容。即“what 、where 、when”。这样我们通过看控制台日志,就能完整地跟踪一个网络请求,提升debug的效率。

先不急,我们看看内部代码在哪里首先发起了打印的指令:
在Request类中:

                    ~~~~~
// Perform the network request.
                   ~~~~~
                request.addMarker("network-http-complete");
                // If the server returned 304 AND we delivered a response already,
                // we're done -- don't deliver a second identical response.
                if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                    request.finish("not-modified");
                    continue;
                }
                // Parse the response here on the worker thread.
                Response<?> response = request.parseNetworkResponse(networkResponse);
                request.addMarker("network-parse-complete");
                ~~~~~
                if (request.shouldCache() && response.cacheEntry != null) {
                ~~~~~
                    request.addMarker("network-cache-written");
                }

上面的代码的意思是:当一个request对象被调度时,使用request.addMarker("something")的方式用来进行“事件打点”,它记录一个网络请求整个生命周期。
其中request.addMarker("something")内部代码是:

   /**
     * Adds an event to this request's event log; for debugging.
     */
    public void addMarker(String tag) {
        if (MarkerLog.ENABLED) {
            mEventLog.add(tag, Thread.currentThread().getId());
        }
    }

上述代码中mEventLog是VolleyLog的一个内部类MarkerLog的实例。
到了这里大家可能会迷糊MarkerLog又是啥,VolleyLog是啥。为什么我们有了android.util.Log这个Log类后还需要自己定制VolleyLog呢?

  • VolleyLog
/**
 * Logging helper class.
 * <p/>
 * to see Volley logs call:<br/>
 * {@code <android-sdk>/platform-tools/adb shell setprop log.tag.Volley VERBOSE}
 */
public class VolleyLog {
    public static String TAG = "Volley";

    public static boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);

    /**
     * Customize the log tag for your application, so that other apps
     * using Volley don't mix their logs with yours.
     * <br />
     * Enable the log property for your tag before starting your app:
     * <br />
     * {@code adb shell setprop log.tag.<tag>}
     */
    public static void setTag(String tag) {
        d("Changing log tag to %s", tag);
        TAG = tag;

        // Reinitialize the DEBUG "constant"
        DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
    }  

    public static void e(String format, Object... args) {
        Log.e(TAG, buildMessage(format, args));
    }

    /**
     * Formats the caller's provided message and prepends useful info like
     * calling thread ID and method name.
     */
    private static String buildMessage(String format, Object... args) {
        String msg = (args == null) ? format : String.format(Locale.US, format, args);
        StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace();

        String caller = "<unknown>";
        // Walk up the stack looking for the first caller outside of VolleyLog.
        // It will be at least two frames up, so start there.
        for (int i = 2; i < trace.length; i++) {
            Class<?> clazz = trace[i].getClass();
            if (!clazz.equals(VolleyLog.class)) {
                String callingClass = trace[i].getClassName();
                callingClass = callingClass.substring(callingClass.lastIndexOf('.') + 1);
                callingClass = callingClass.substring(callingClass.lastIndexOf('$') + 1);

                caller = callingClass + "." + trace[i].getMethodName();
                break;
            }
        }
        return String.format(Locale.US, "[%d] %s: %s",
                Thread.currentThread().getId(), caller, msg);
    }

上述代码中,通过对android.util.log类的封装,VolleyLog类有两个优势地方值得我们学习:

2.2.3.1 日志开关

boolean DEBUG=Log.isLoggable(TAG,VERBOSE)这个是系统级别方法,Log.isLoggable()会去调用底层native代码判断当前日志系统是否能打印VERBOSE以上级别的日志,默认情况下是不可以的。所以如果Volley框架随我们的app发布到了线上,默认我们看不到之前我们打印的日志信息,这样可以减少日志输出带来的性能消耗。
那么怎么在调试阶段返回为true呢? 很简单,我们只需要在终端输入命令:

adb shell setprop log.tag.Volley VERBOSE 

这样再次运行app,我们就能看到Volley打印的日志信息了。

2.2.3.2 格式化日志信息

有经验的同学应该知道控制台有时候很多日志没有规则,不利于阅读,所以我们可以统一输出的格式特别是在多线程中调试时,我们还可以打印出线程id,这样更有利于我们跟踪问题。那么我们看看是如何打印线程方法栈日志的:

 StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace();

上面一行代码用来得到当前线程下方法调用的栈轨迹,并返回在一个数组中。
举个例子:

 public void fetch(){
     VolleyLog.d("hello"); 
}

上述代码的方法调用的栈轨迹轨迹应该是:
1.fetch();
2.VolleyLog.d();
3.buildmessage();

这样我们就明白了 new Throwable().fillInStackTrace().getStackTrace();
的作用。至于为什么是从i=2的下标开始读起,是因为会经历d()、buildMessage()这两个方法,而我们关注的是fetch()方法的线程id,和它的调用者className。
最后我们可以格式化字符串格式:

String.format(Locale.US, "[%d] %s: %s",Thread.currentThread().getId(), caller, msg);

这样我们就能在控制台得到如下的格式:

[1] MarkerLog.finish: (+1   ) [155] cache-hit-expired
2.3异常模块

如果了解Http请求,那么应该知道请求网络会碰到不同类型的错误响应,如请求密码错误、请求超时、网络地址解析错误、服务器错误等等,Volley为了统一处理网络异常写了一个基类VolleyError,它又拓展了如NetworkError、AuthFailureError等子类,下面是完整类图:

异常继承结构图
异常继承结构.png

这里就分析一个类别 AuthFailureError,代码:

/**
 * Error indicating that there was an authentication failure when performing a Request.
 */
@SuppressWarnings("serial")
public class AuthFailureError extends VolleyError {
    /** An intent that can be used to resolve this exception. (Brings up the password dialog.) */
    private Intent mResolutionIntent;

    public AuthFailureError() { }

    public Intent getResolutionIntent() {
        return mResolutionIntent;
    }
    @Override
    public String getMessage() {
        if (mResolutionIntent != null) {
            return "User needs to (re)enter credentials.";
        }
        return super.getMessage();
    }

AuthFailureError是用来处理用户认证请求时抛出的异常,它有个成员变量mResolutionIntent,当发生异常时,我们可以通过mResolutionIntent去显示一个提示对话框,或者其他自定义方法来处理认证异常。
其他异常类同理,这里不做讲解。

那么这么多异常类分别是在什么逻辑下抛出呢?这里卖个关子。在网络层的时候我们将具体讲解如何统一处理不同类别异常。

2.4 重试机制

重试机制是为了在网络请求失败或者超时的情况下,由网络框架主动重新发起的网络请求。

接口类RetryPolicy定义了重试主要协议:

volleyError.png

它定义了网络重试机制的三个主要点:
1.超时时间(什么时候发起重试请求)
2.重试次数 (重试几次后,如果失败则停止重试)
3.发起重试命令,同时携带可能抛出的异常

根据上面三点要求,Volley内部提供了一个默认实现类:
DefaultRetryPolicy:

/**
 * Default retry policy for requests.
 */
public class DefaultRetryPolicy implements RetryPolicy {
    /** The current timeout in milliseconds. */
    private int mCurrentTimeoutMs;

    /** The current retry count. */
    private int mCurrentRetryCount;

    /** The maximum number of attempts. */
    private final int mMaxNumRetries;

    /** The backoff multiplier for the policy. */
    private final float mBackoffMultiplier;

    /** The default socket timeout in milliseconds */
    public static final int DEFAULT_TIMEOUT_MS = 2500;

    /** The default number of retries */
    public static final int DEFAULT_MAX_RETRIES = 1;

    /** The default backoff multiplier */
    public static final float DEFAULT_BACKOFF_MULT = 1f;

    /**
     * Constructs a new retry policy using the default timeouts.
     */
    public DefaultRetryPolicy() {
        this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
    }

    /**
     * Constructs a new retry policy.
     * @param initialTimeoutMs The initial timeout for the policy.
     * @param maxNumRetries The maximum number of retries.
     * @param backoffMultiplier Backoff multiplier for the policy.
     */
    public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
        mCurrentTimeoutMs = initialTimeoutMs;
        mMaxNumRetries = maxNumRetries;
        mBackoffMultiplier = backoffMultiplier;
    }

    /**
     * Returns the current timeout.
     */
    @Override
    public int getCurrentTimeout() {
        return mCurrentTimeoutMs;
    }

    /**
     * Returns the current retry count.
     */
    @Override
    public int getCurrentRetryCount() {
        return mCurrentRetryCount;
    }

    /**
     * Returns the backoff multiplier for the policy.
     */
    public float getBackoffMultiplier() {
        return mBackoffMultiplier;
    }

    /**
     * Prepares for the next retry by applying a backoff to the timeout.
     * @param error The error code of the last attempt.
     */
    @Override
    public void retry(VolleyError error) throws VolleyError {
        mCurrentRetryCount++;
        mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
        if (!hasAttemptRemaining()) {
            throw error;
        }
    }

    /**
     * Returns true if this policy has attempts remaining, false otherwise.
     */
    protected boolean hasAttemptRemaining() {
        return mCurrentRetryCount <= mMaxNumRetries;
    }
}
  1. mCurrentTimeoutMs代表网络请求时间连接范围,超出范围则重新请求
  2. mCurrentRetryCount已经完成的重试次数
  3. mMaxNumRetries最大允许的重试次数
    DefaultRetryPolicy的构造函数为我们默认初始化默认的超时次数和时间。
    我们重点看看retry(error)方法,
    mCurrentRetryCount++;每次重试请求则数量加1
    mCurrentTimeoutMs += 允许超时时间阀值也增加一定系数级别
    最后,hasAttemptRemaining如果超出最大重试次数,则抛出异常交给应用层开发者处理。
总结

除了系统提供的DefaultRetryPolicy,我们可以自己定制重试策略,只要是实现了RetryPlicy接口,mCurrentTimeoutMs值大小和重试次数可以自定义,例如,我们可以在电量充足的条件下重试次数更多,不管怎么样,以达到“因地制宜”为最终目的。

三、网络层

分析完基础层后,我们看看网络层,它将使用到基础层中Cache类和日志类、异常类,而网络层主要职能是执行网络请求。

3.1Http请求流程简介
http请求.png
3.2网络请求和响应对象封装
3.2.1 Request

Volley框架把Http请求需要的头信息封装到了Request对象中,另外,Request类还处理了请求排序、通知传送器派发请求响应的数据、响应错误监听器。
Request数据结构:

request基本属性.png
3.2.2FIFO优先级队列,请求排序

3.2.2.1 首先用枚举值标记优先级:

  /**
     * Priority values.  Requests will be processed from higher priorities to
     * lower priorities, in FIFO order.
     */
    public enum Priority {
        LOW,
        NORMAL,
        HIGH,
        IMMEDIATE
    }

3.2.2.2重写compareTo函数:

  /**
     * Our comparator sorts from high to low priority, and secondarily by
     * sequence number to provide FIFO ordering.
     */
    @Override
    public int compareTo(Request<T> other) {
        Priority left = this.getPriority();
        Priority right = other.getPriority();

        // High-priority requests are "lesser" so they are sorted to the front.
        // Equal priorities are sorted by sequence number to provide FIFO ordering.
        return left == right ?
                this.mSequence - other.mSequence :
                right.ordinal() - left.ordinal();
    }

这样,随后我们在控制层中,就可以按照先进先出的顺序执行请求。

3.2.3Request中的抽象方法
 /**
     * Subclasses must implement this to parse the raw network response
     * and return an appropriate response type. This method will be
     * called from a worker thread.  The response will not be delivered
     * if you return null.
     * @param response Response from the network
     * @return The parsed response, or null in the case of an error
     */
    abstract protected Response<T> parseNetworkResponse(NetworkResponse response);

 /**
     * Subclasses must implement this to perform delivery of the parsed
     * response to their listeners.  The given response is guaranteed to
     * be non-null; responses that fail to parse are not delivered.
     * @param response The parsed response returned by
     * {@link #parseNetworkResponse(NetworkResponse)}
     */
    abstract protected void deliverResponse(T response);

上面的两个方法交给上层---应用层去实现,根据注释可了解到它们分别是解析网络响应数据为具体类型、分发响应数据给上层。

3.2.4 Response

上面分析完Request后,我们来看看网络响应类Response。
Response的作用:
Response的并没有封装网络响应的数据(NetworkResonse类封装了网络响应数据),它主要是用来把网络响应解析完毕后的数据响应给上层监听器,它扮演Control的角色。

/**
 * Encapsulates a parsed response for delivery.
 *
 * @param <T> Parsed type of this response
 */
public class Response<T> {

    /** Callback interface for delivering parsed responses. */
    public interface Listener<T> {
        /** Called when a response is received. */
        public void onResponse(T response);
    }

    /** Callback interface for delivering error responses. */
    public interface ErrorListener {
        /**
         * Callback method that an error has been occurred with the
         * provided error code and optional user-readable message.
         */
        public void onErrorResponse(VolleyError error);
    }

    /** Returns a successful response containing the parsed result. */
    public static <T> Response<T> success(T result, Cache.Entry cacheEntry) {
        return new Response<T>(result, cacheEntry);
    }

    /**
     * Returns a failed response containing the given error code and an optional
     * localized message displayed to the user.
     */
    public static <T> Response<T> error(VolleyError error) {
        return new Response<T>(error);
    }

    /** Parsed response, or null in the case of error. */
    public final T result;

    /** Cache metadata for this response, or null in the case of error. */
    public final Cache.Entry cacheEntry;

    /** Detailed error information if <code>errorCode != OK</code>. */
    public final VolleyError error;

    /** True if this response was a soft-expired one and a second one MAY be coming. */
    public boolean intermediate = false;

    /**
     * Returns whether this response is considered successful.
     */
    public boolean isSuccess() {
        return error == null;
    }


    private Response(T result, Cache.Entry cacheEntry) {
        this.result = result;
        this.cacheEntry = cacheEntry;
        this.error = null;
    }

    private Response(VolleyError error) {
        this.result = null;
        this.cacheEntry = null;
        this.error = error;
    }
}
3.3网络请求引擎
3.3.1 HttpStack

它作为Volley最底层的网络请求引擎,它封装了一次http请求。

public interface HttpStack {
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
        throws IOException, AuthFailureError;
}

HttpStack的实现类有两个:
1.HurlStack,内部使用的网络引擎是HttpURLConnection
2.HttpClientStack,,内部使用的网络引擎是 HttpClient
所以发起网络请求的代码就是这两个实现类去处理的。至于选择哪个网络引擎则根据android版本来定。

3.3.2OkHttp

既然httpStack是网络引擎,那么我们可以用OkHttp这个性能更好的网络引擎取代它,具体内容可参见其官网。

3.4.网络请求代理者
网络引擎.png

我们可以想象到在执行请求网络前后,我们需要一个处理请求头协议,和处理响应状态码、处理响应异常,这个时候我们把httpstack交给了NetWork这一层去处理请求前后的业务逻辑。
NetWork的performRequest()方法工作流程图:

network.png
  • BasicNetwork的关键代码
public class BasicNetwork implements Network {
    protected final HttpStack mHttpStack;
 @Override
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
        while (true) {  //循环执行,这里是用来失败重连
          addCacheHeaders(headers, request.getCacheEntry());
             ....
~~~~~~~~~ 执行请求前
          httpResponse = mHttpStack.performRequest(request, headers);
~~~~~~~~~   执行请求后    
           return new NetworkResponse();
                   }     
            }  catch (ConnectTimeoutException e) {
                attemptRetryOnException("connection", request, new TimeoutError());
            } catch (IOException e) {   
                throw new NetworkError(networkResponse);
             }
    }

从上述BasicNetwork的代码来看,network经历了四个步骤:

1.mHttpStack.preformRequest()执行网络请求前,读取请求头信息,如缓存标志,请求参数等。

2.在执行网络请求后,我们读取了statusCode请求状态码,它用来判断是否请求成功,是否需要更新缓存等。如果成功我们返回NetWorkResponse对象。

NetWorkResponse:它充当网络数据载体的角色,用来包裹网络请求得到的数据.

3.如果发生连接超时我们将重新连接网络请求:

while(true){
~~~~~~~~~~~~~~~~~~~~~
 attemptRetryOnException("connection", request, new TimeoutError());
~~~~~~~~~~~~~~~~~~~~~
}

因为preformRequest()方法里是一个while循环,通过抛出异常或者重试请求已经超过最大次数,循环才会退出。

4.如果是发生网络请求错误,那么我们则抛出对应的异常,中止循环,这里就对应了在第一部分基础层的异常机制,我们定义的那些异常派生类现在就派上用场了:

 throw new ServerError(networkResponse);
 throw new NetworkError(networkResponse);

四、控制层(核心)

至此,我们已经把网络请求需要做的基础工作已经全部做完,但Volley设计思想的核心从这里开始。

在控制层中Volley设计者解决了下面几个问题。
1.如何调度网络请求队列?
2.如何取消网络请求?

4.1 Volley请求的工作流程

回答这些问题之前我们看看Volley的工作流程图:

volley-request.png

上幅图中 ,队列的调度主要发生在RequestQueue类中。

4.1.1 如何调度网络请求队列?

当调用requestQueue.start()时,RequestQueue将会启动一个用于处理缓存的线程和一个处理网络请求的线程池。当你把request对象放入队列,它首先会被一个缓存线程给接收处理,如果缓存命中了,那么对应原先缓存好的响应数据将在缓存线程解析,解析完的数据将会派发到主线程。

如果request没有被缓存命中,那么它将会被网络队列(network queue)接管,然后线程池中第一个可用的网络线程将从队列中把request取出来,执行HTTP处理,在工作线程解析原始响应数据,并把响应存入缓存,然后把解析完毕的响应数据派发到主线程。

需要注意的是,你可以在任意线程发起一个请求,而其中一些耗时的IO操作或者解析数据操作将会在子线程(工作线程)执行,但是最后得到的网络响应数据还是会发送到主线程。

4.1.2 如何取消网络请求?

为了取消一个请求,我们可以调用Request对象的cancel()方法。一旦取消请求完成,Volley保证你一定不会收到的请求后的响应处理。这个功能意味着你可以在activity的onStop()回调方法中取消那些即将发送的(pending)请求,同时你不必在你的响应处理中检查getActivity() == null,或者一些恢复场景中再去处理请求,如onSaveInstanceState()回调中。
为了更好的利用这个行为,你应该谨慎地跟踪所有发起的请求,以便你可以在合适的时间取消它们。

Example:你可以给每个请求设置一个tag,这样你就能取消所有设置了这个tag的请求。例如,你能把Activity的实例作为所有请求的tag,那么在onStop()回调中你可以调用requestQueue.cancelAll(this)取消所有请求。相似的,在viewPager页面中,你能给“获得缩略图”请求设置一个viewPager的tab名称,这样后,在滑动viewPager改变tab的时候,你能单独取消那些已经隐藏的view的请求,新tab页面的请求则不会被停止。

4.2类的详细解析
4.2.1线程类CacheDispatcher的内部工作流程图:
cacheDispacher.png
4.2.2 线程类NetworkDispatcher的内部工作流程图:
networkDispatcher.png
4.2.3 ExecutorDelivery的内部工作原理:

ExecutorDelivery内部是通过mResponsePoster对象调用一个handler把响应数据派发到主线程:

 mResponsePoster = new Executor() {
            @Override
            public void execute(Runnable command) {
                handler.post(command);
            }
        };

五、应用层

5.1 使用StringRequest发起一次请求

前面我们了解到Request有两个需要上层重写的抽象函数:

abstract protected Response<T> parseNetworkResponse(NetworkResponse response);

abstract protected void deliverResponse(T response);

我们看看StringRequest类是如何重写的:

  @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
    }

把参数reponse.data用utf-8的编码方式得到一个String,并返回一个包装好的Resonse<String>对象。

  @Override
    protected void deliverResponse(String response) {
        mListener.onResponse(response);
    }

这里是触发响应回调。

如果我们想得到一个JsonOject,或者具体的实体类,那么我们重写parseNetworkResponse()函数即可。这个函数会在netWork.proformRequest()里面调用。

如果我们想在UI线程得到响应数据之前做些其他操作(如显示进度条),那么我们重写deliverResponse()函数即可。这个函数会在ResponseDeliver里面调用。

至此我们看看在UI线程我们发起一个请求的完整过程:

 String url = "http://www.fulaan.com/forum/fPostActivityAll.do?sortType=2&page=1&classify=-1&cream=-1&gtTime=0&postSection=&pageSize=25&inSet=1";
    StringRequest stringRequest = new StringRequest(url, new Response.Listener<String>() {
      @Override public void onResponse(String response) {
        Log.d(TAG, "onResponse: " + response);
      }
    }, new Response.ErrorListener() {
      @Override public void onErrorResponse(VolleyError error) {
        Log.d(TAG, "VolleyError: " + error);
      }
    });
    stringRequest.setTag(this);
    VolleyUtil.getInstance(this).addRequestQueue(stringRequest);

简单说明下步骤:
1.构造一个stringRequest,
2.实现错误监听和成功响应回调函数
3.给request设置tag,用于取消。
4.把stringRquest交给控制器RequestQueue去自行调度执行。

六、总结

至此,分析完了Volley源码框架,冥想片刻。
完成这篇博客,我学到了如何一步步构建一套网络框架。后续计划是对比分析retrofit网络框架设计思想,另外我想如果做图片框架、断点文件多线程下载等,Voley给我的设计思想都有很好的参考价值。

不管怎么样,还请读者多多指教与共勉~多谢!

推荐阅读更多精彩内容