Do you really understand the "epic" Volley source code thoroughly?

Keywords: network Android SDK shell

Catalog:
I. Preface
 II. Foundation Layer
   2.1 Cache Module
        2.1.1 Http Caching Protocol
        2.1.2 Storage Design
        2.1.3 Cache Summary
   2.2 Log Record
        2.2.1 Concept
        2.2.2 Example
        2.2.3 Detailed analysis
           2.2.3.1 Log Switch
           2.2.3.2 Format log information
   2.3 exception module
         Class 2.3.1 Inheritance Structure
   2.4 Retry mechanism
 3. Network Layer
   3.1. Brief introduction of Http request process
   3.2. Network Request and Response Object Encapsulation
      3.2.1.Request
      3.2.2 FIFO Priority Queue, Request Sorting
      3.2.3 Abstract Method in Request
      3.2.4 Response
   3.3 Network Request Engine 
       3.3.1 HttpURLConnection, HttpClient in HttpStack
       3.3.2.OkHttp
   3.4 Network Request Agent
       3.4.1 NetWork Performance Request () Method Workflow Diagram
 IV. Control Layer
    4.1 Volley internal request workflow
       4.1.1 How to Schedule Network Request Queue
       4.1.2 How to Cancel Network Request
    Detailed Analysis of Class 4.2
        4.2.1 Internal workflow diagram of thread class CacheDispatcher
        4.2.2 Internal Workflow Diagram of Thread Class NetworkDispatcher
        4.2.3 Executor Delivery's Internal Working Principle
 5. Application Layer
      5.1 Use StringRequest to initiate a request
 VI. SUMMARY
Preface

This article is a summary of daily learning. By analyzing the source code of Volley, the author hopes to consolidate the common technologies of multithreading, IO, design pattern and SDK development in Android network framework.

I believe that 80% of Android developers currently use Retrofit as their network framework, but I can say that 99% of Android developers have never heard of Volley because its design idea is epic, classic and worth reading repeatedly.

In the standard network protocol, the predecessors developed the network at different levels. Each layer is responsible for different communication functions. At the same time, each layer can interact easily. The industry standard term is "high cohesion, low coupling". For example, TCP/IP is a combination of multiple protocols at different levels. As follows:


Four Hierarchies of TCP/IP Protocol Cluster

So, get inspiration from above, according to the principle of layering, pull the veil of Volley source code layer by layer.

Layered decoupling

The author thinks that the Volley network framework has the following hierarchical structure:


volley Architecture. png

II. Foundation Layer

Let's look at the basic layer first, and the upper layer will depend on it. It is like the foundation of a building, which guarantees the robustness of the whole frame.

At the basic level, we can learn:
1. Cache data module: a general caching strategy.
2. Logging module: Flexible event management.
3. Exception module: Unified exception handling.
4. Retry mechanism: In the case of poor network conditions, it can provide the function of re-request.

2.1 Cache Module
2.1.1 Volley Caching Mechanism

follow HTTP Caching Protocol Self-provided ladder:)
In summary, http cache protocol uses cache-control and eTag fields in http request header to judge whether or not to cache and cache time.
The main process is as follows:


Best Cache-Control Strategy.png


See the link above for details.

2.1.2 Storage Design
  • Cache interface:
    The Cache interface, which establishes the basic data structure of the cache, is a Key-Value pair. The Entry class serves as a Value, encapsulating an HTTP response data, corresponding to the following figure:

Response header for HTTP request. png

Two important methods are:

public Entry get(String key);//Get a cached object
public void put(String key, Entry entry);//Save a cached object
  • DiskBasedCache
    It is a basic disk cache class with the following data structure tables:

DiskBasedCache attribute. png


It can be seen from the table that:
1. We can configure the cached root directory to get the cached file objects and file paths.
2. The maximum available caching and available caching capacity of the field determines whether the mobile phone sdcard can continue to save the cache.

  • CacheHeader :
    An internal class of CacheHeader that reads and writes a header information help class for file size and cache time, except for file entity content.

DiskBasedCache method:

  • initialize(): Initialize DiskBasedCache by traversing the root directory file.
      @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. Determine whether the cache root directory exists or not, and create a new one if it does not exist.
    2. Traverse the cache directory.
    3. Read the header information of the file in turn, and store all the cached header information in mEntries (read from disk to memory).
  • getFileNameForKey(): Gets the hash value of the file name. Divide the file into two parts and take hash values separately, then merge them.

    Note: The only file name is obtained here, and the usual method of encrypting the file name with MD5 is not adopted. I think it is more efficient to encrypt the hash file name with string hash than with 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); get the cached object according to the key. The code snippet is as follows:
/**
     * 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. Match the header information cached in memory from the map mEntries, and return null if not.
2. Find the cache file File on disk according to key.
3. Parse the file content data bytes into the cache object Entry and return them.

Summary: First read the header information, then read the main content of the file, and finally merge the two.

  • clear(); Clear cached files under thread security conditions: traverse all files under the root directory and delete them. The code snippet is as follows:
    public synchronized void clear() {
       File[] files = mRootDirectory.listFiles();
         if (files != null) {
             for (File file : files) {
                 file.delete(); //Delete files from disk
             }
         }
         mEntries.clear(); //Clean up memory
         mTotalSize = 0;//Reinitialize the total size
         VolleyLog.d("Cache cleared.");
    }
  • put(String key, Entry entry); save a cache file under the precondition of thread security. The code snippet is as follows:
    @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. First, determine whether the local cache capacity is sufficient or not, then traverse to delete the old files until the capacity is sufficient to stop.
    2. Write Entry's header information into the local file
    3. Write Entry entity data into local files
    4. CacheHeader, which gets header information from Entry, is cached into memory Map.
2.1.3 Cache Summary

So far, the caching module has been analyzed, and there are several techniques worth learning.
1. Save the file into two parts: memory and disk. Memory holds header information (file size, expiration time, etc.) while disk holds all data of the file. In this way, the memory can be read first to determine whether the header information is legitimate, and if it is legitimate, it can read the file data locally, otherwise it will be abandoned, which can reduce the time-consuming file reading time and improve efficiency.
2. On the premise of file information security, the method of dividing the file name string into two hash es can replace the md5 encryption method.

2.2 Log Record
2.2.1 Concept

Logging is mainly used to record events that occur during the running of a program in order to understand system activities and diagnose problems. It is very important to understand the trajectory of complex systems.
From Wikipedia, we can see that log plays an indispensable role in SDK development or component development. It is easy to debug and ensure the stability of the development system.

2.2.2 Example

After understanding the concept of logging, let's look at how the Volley framework records logs:
First, the client initiates a network request:

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

Then we look at the information output from the console 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 Detailed Code Analysis

We can see that in the first section of the code, the logs. D (TAG, "") in the android.util.log package is not used to print information. Where are these logs printed? It is not difficult to find that these log records are printed by Volley Log, an internal class within the Volley framework.

In addition, from the output information of the console, we know that the log points out some key information: the name of the event, the start time of the event, the thread ID, the total time consumed by the event, and the content of the event. That is "what, where, when". By looking at the console log, we can track a network request completely and improve the efficiency of debug.

Let's take a look at where the internal code first initiated the printing instructions:
In the Request class:

                    ~~~~~
// 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");
                }

The above code means that when a request object is scheduled, it uses request.addMarker("something") to do "event dotting", which records the entire life cycle of a network request.
The internal code of request.addMarker("something") is:

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

The mEventLog in the above code is an instance of VolleyLog's internal class MarkerLog.
When you get here, you may be confused about what MarkerLog is and what Volley Log is. Why do we need to customize Volley Log after we have android.util.Log?

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

    In the above code, by encapsulating the android.util.log class, the VolleyLog class has two advantages that we should learn from:

    2.2.3.1 Log Switch

boolean DEBUG=Log.isLoggable(TAG,VERBOSE) is a system-level method. Log.isLoggable() calls the underlying native code to determine whether the current logging system can print logs above VERBOSE level, which is not possible by default. So if the Volley framework is posted online with our app, by default we can't see the log information we printed before, which can reduce the performance consumption caused by log output.
So how do you return to true during debugging? Simply, we just need to enter commands at the terminal:

adb shell setprop log.tag.Volley VERBOSE

Running app again, we can see the log information printed by Volley.

2.2.3.2 Format log information

Experienced students should know that sometimes the console logs have no rules, which is not conducive to reading, so we can unify the output format, especially when debugging in multi-threads, we can also print out the thread id, which is more conducive to tracking problems. So let's see how to print the thread method stack log:

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

The above line of code is used to get the stack trajectory of the method call under the current thread and return it to an array.
For instance:

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

The stack trajectory of the method call of the above code should be:
1.fetch();
2.VolleyLog.d();
3.buildmessage();

So we understand new Throwable().fillInStackTrace().getStackTrace();
Role. As for why we started reading from the subscript of i = 2, it's because we went through two methods, d() and buildMessage(), while we focused on the thread id of fetch() method and its caller className.
Finally, we can format the string format:

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

In this way, we can get the following format in the console:

[1] MarkerLog.finish: (+1   ) [155] cache-hit-expired
2.3 exception module

If you know about Http requests, you should know that the requesting network will encounter different types of error responses, such as request password error, request timeout, network address resolution error, server error and so on. Volley wrote a class Volley Error for handling network anomalies in a unified way. It also expanded subclasses such as Network Error, AuthFailure Error and so on. The following is the complete class diagram:

Exception Inheritance Structure Diagram

Exceptional inheritance structure. png

Here we analyze a category AuthFailure Error, code:

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

AuthFailure Error is an exception thrown when handling user authentication requests. It has a member variable mResolutionIntent. When an exception occurs, we can use mResolutionIntent to display a prompt dialog box or other custom methods to handle the authentication exception.
Other anomalies are the same, so we will not explain them here.

So under what logic are so many exception classes thrown? Here's a pass. In the network layer, we will explain how to deal with different types of exceptions in a unified way.

2.4 Retry mechanism

The retry mechanism is to initiate network requests by the network framework when the network requests fail or time out.

The interface class RetryPolicy defines the retry main protocol:


volleyError.png

It defines three main points of the network retry mechanism:
1. Timeout (when to initiate a retry request)
2. Number of retries (after several retries, stop retries if they fail)
3. Initiate retry commands with possible thrown exceptions

According to the above three requirements, Volley provides a default implementation class internally:
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. MCurrent TimeoutMs represent the network request time connection range, beyond which re-request
  2. Number of retries completed by mCurrentRetryCount
  3. Maximum number of retries allowed by mMaxNumRetries
    DefaultRetryPolicy's constructor initializes default timeouts and times for us by default.
    Let's focus on the retry (error) method.
    mCurrentRetryCount++; the number of requests per retry is increased by 1
    mCurrentTimeoutMs + = Allowable timeout threshold also increases a certain coefficient level
    Finally, if hasAttemptRemaining exceeds the maximum number of retries, the exception is thrown and handled by the application layer developer.
summary

In addition to the DefaultRetryPolicy provided by the system, we can customize the retry strategy ourselves. As long as the RetryPlicy interface is implemented, the magnitude of mCurrent TimeoutMs value and the number of retries can be customized. For example, we can retry more times under sufficient power, no matter what, in order to achieve the ultimate goal of "tailoring measures to local conditions".

3. Network Layer

After analyzing the basic layer, let's look at the network layer. It will use the Cache class, log class and exception class in the basic layer. The main function of the network layer is to execute network requests.

3.1 Http Request Process Brief Introduction

http request. png
3.2 Network Request and Response Object Encapsulation
3.2.1 Request

The Volley framework encapsulates the header information required by Http requests into Request objects. In addition, the Request class also handles request sorting, notification transmitter dispatching request response data, and response error listener.
Request data structure:


request basic property. png
3.2.2 FIFO Priority Queue, Request Sorting

3.2.2.1 Priorities are first marked with enumerated values:

  /**
     * 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 Rewrite the compareTo function:

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

Then, in the control layer, we can execute the request in FIFO order.

The abstract method in 3.2.3 Request
 /**
     * 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);

The above two methods are handed over to the upper layer, the application layer. According to the annotations, we can see that they are respectively to parse the network response data for specific types and distribute the response data to the upper layer.

3.2.4 Response

After analyzing Request above, let's take a look at the network response class Response.
Response's role:
Response does not encapsulate network response data (Network Resonse class encapsulates network response data), it is mainly used to parse the network response data response to the upper listener, it plays the role of 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 Network Request Engine
3.3.1 HttpStack

As Volley's lowest network request engine, it encapsulates an http request.

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

There are two implementation classes for HttpStack:
1.HurlStack, the internal network engine is HttpURLConnection
2.HttpClientStack, the internal network engine is HttpClient.
So the code that initiates the network request is handled by these two implementation classes. The choice of network engine depends on the android version.

3.3.2OkHttp

Since httpStack is a network engine, we can use it OkHttp This better-performing network engine replaces it. See its official website for details.

3.4. Network Request Agent

Network Engine. png

We can imagine that before and after the execution of the request network, we need a protocol to handle the request header, and to handle the response status code and the response exception. At this time, we give httpstack to NetWork layer to handle the business logic before and after the request.
NetWork's performance Request () method workflow diagram:


network.png
  • Key Codes of Basic Network
public class BasicNetwork implements Network {
    protected final HttpStack mHttpStack;
 @Override
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
        while (true) {  //Loop execution, which is used to fail reconnection
          addCacheHeaders(headers, request.getCacheEntry());
             ....
~~~~~~~~~ Before executing the request
          httpResponse = mHttpStack.performRequest(request, headers);
~~~~~~~~~   After executing the request    
           return new NetworkResponse();
                   }     
            }  catch (ConnectTimeoutException e) {
                attemptRetryOnException("connection", request, new TimeoutError());
            } catch (IOException e) {   
                throw new NetworkError(networkResponse);
             }
    }

From the above basic network code, the network has gone through four steps:

1.mHttpStack.preformRequest() reads the request header information, such as cache flags, request parameters, etc. before executing the network request.

2. After executing the network request, we read the status code of the status code request, which is used to determine whether the request is successful, whether the cache needs to be updated, and so on. If successful, we return the NetWorkResponse object.

NetWorkResponse: It acts as a network data carrier to wrap the data requested by the network.

3. If a connection timeout occurs, we will reconnect the network request:

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

Because the preformRequest() method is a while loop, the loop exits by throwing exceptions or retrying requests more than the maximum number of times.

4. If a network request error occurs, then we throw the corresponding exception and terminate the loop. Here we correspond to the exception mechanism at the base level of the first part. The exception derivatives we defined are now useful:

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

Control Layer (Core)

So far, we have completed all the basic work of network requests, but the core of Volley's design idea starts here.

In the control layer, Volley designers solved the following problems.
1. How to schedule the network request queue?
2. How to cancel the network request?

4.1 Workflow of Volley Request

Before answering these questions, let's look at Volley's workflow chart:


volley-request.png


In the above figure, queue scheduling occurs mainly in the RequestQueue class.

4.1.1 How to schedule the network request queue?

When requestQueue.start() is called, RequestQueue will start a thread for caching and a thread pool for processing network requests. When you put the request object into the queue, it will be first received by a cache thread. If the cache hits, the corresponding response data will be parsed in the cache thread, and the parsed data will be sent to the main thread.

If the request is not hit by the cache, it will be taken over by the network queue. Then the first available network thread in the thread pool will take the request out of the queue, execute HTTP processing, parse the original response data in the worker thread, store the response in the cache, and send the parsed response data to the main thread.

It should be noted that you can initiate a request in any thread, and some time-consuming IO operations or parsing data operations will be performed in sub-threads (worker threads), but the resulting network response data will still be sent to the main thread.

4.1.2 How to cancel network requests?

To cancel a request, we can call the cancel() method of the Request object. Once the cancellation request is completed, Volley ensures that you will not receive the response processing after the request is received. This function means that you can cancel pending requests in the activity's onStop() callback method, and you don't have to check getActivity () = null in your response processing, or process requests in some recovery scenarios, such as onSave Instance State () callback.
In order to make better use of this behavior, you should carefully track all initiated requests so that you can cancel them at the right time.

Example: You can set a tag for each request so that you can cancel all requests that set the tag. For example, if you can tag an instance of Activity as a tag for all requests, you can call requestQueue.cancelAll(this) to cancel all requests in the onStop() callback. Similarly, on the viewPager page, you can set the tab name of the viewPager for the "Get Thumbnails" request, so that when the viewPager changes the tab, you can cancel the hidden view requests separately, and the requests for the new tab page will not be stopped.

Detailed Analysis of Class 4.2
4.2.1 Internal workflow diagram of thread class CacheDispatcher:

cacheDispacher.png
4.2.2 Thread class NetworkDispatcher's internal workflow diagram:

networkDispatcher.png
4.2.3 The internal working principle of Executor Delivery:

Within Executor Delivery, a handler is called through the mResponsePost object to dispatch the response data to the main thread:

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

5. Application Layer

5.1 Use StringRequest to initiate a request

As we learned earlier, Request has two abstract functions that need to be overridden at the upper level:

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

abstract protected void deliverResponse(T response);

Let's see how the StringRequest class is overridden:

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

The parameter reponse.data is encoded in utf-8 to get a String, and a wrapped Resonse < String > object is returned.

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

Here is the trigger response callback.

If we want to get a JsonOject, or a specific entity class, we can rewrite the parseNetworkResponse() function. This function is called in netWork.proformRequest().

If we want to do something else (such as displaying the progress bar) before the UI thread gets the response data, we can rewrite the delivery Response () function. This function is called in ResponseDeliver.

So let's look at the complete process of launching a request in the UI thread:

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

Simply explain the following steps:
1. Construct a stringRequest.
2. Implementing error monitoring and successful response callback functions
3. Set tag to request to cancel.
4. Give stringRquest to the controller RequestQueue to schedule and execute itself.

VI. SUMMARY

So far, the Volley source framework has been analyzed and meditated for a moment.
By completing this blog, I learned how to build a network framework step by step. The follow-up plan is a comparative analysis of retrofit network framework design ideas. In addition, I think if we do picture framework, breakpoint file multi-threaded download, Voley has a good reference value for my design ideas.

Anyway, please give more advice and encouragement to the readers.

Posted by robert_w_ark on Wed, 12 Jun 2019 13:13:57 -0700