上次分析了一下Volley的源码,今天又看了一遍Volley的源码,受益匪浅。上次分析的很不到位,Volley框架有很多需要学习的地方,光凭一篇blog肯定是分析不完的。更何况里面还有许多思想在里面需要我进行学习。
首先回顾一下Request类,这是一个抽象类。Volley支持8种http请求方式:GET,POST,PUT,DELETE,HEAD,OPTIONS,TRACE,PATCH。
还可以看到Request构造方法中包含了请求方法,请求url,error监听接口。
/**
* Base class for all network requests.
*
* @param <T> The type of parsed response this request expects.
*/
public abstract class Request<T> implements Comparable<Request<T>>{
...
/**
* Supported request methods.
*/
public interface Method {
int DEPRECATED_GET_OR_POST = -1;
int GET = 0;
int POST = 1;
int PUT = 2;
int DELETE = 3;
int HEAD = 4;
int OPTIONS = 5;
int TRACE = 6;
int PATCH = 7;
}
...
public Request(int method, String url, Response.ErrorListener listener) {
mMethod = method;
mUrl = url;
mIdentifier = createIdentifier(method, url);
mErrorListener = listener;
setRetryPolicy(new DefaultRetryPolicy());
mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
}
}
我们通过构建一个Request类的非抽象子类对象(StringRequest、JsonRequest、ImageRequest或自定义),并将其加入到RequestQueue中完成一次网络请求。
这是一个抽象类,所以必然有需要子类继承的抽象方法。以下是子类必须重写的两个方法。
/**
* 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);
下面就拿ImageRequest类举个例子,来看一下ImageRequest源码:
可以看到,ImageRequest的构造方法中默认调用的是父类构造方法并默认调用GET获取图片。
public class ImageRequest extends Request<Bitmap> {
...
/**
* Creates a new image request, decoding to a maximum specified width and
* height. If both width and height are zero, the image will be decoded to
* its natural size. If one of the two is nonzero, that dimension will be
* clamped and the other one will be set to preserve the image's aspect
* ratio. If both width and height are nonzero, the image will be decoded to
* be fit in the rectangle of dimensions width x height while keeping its
* aspect ratio.
*
* @param url URL of the image
* @param listener Listener to receive the decoded bitmap
* @param maxWidth Maximum width to decode this bitmap to, or zero for none
* @param maxHeight Maximum height to decode this bitmap to, or zero for
* none
* @param scaleType The ImageViews ScaleType used to calculate the needed image size.
* @param decodeConfig Format to decode the bitmap to
* @param errorListener Error listener, or null to ignore errors
*/
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
ScaleType scaleType, Config decodeConfig, Response.ErrorListener errorListener) {
super(Method.GET, url, errorListener);
setRetryPolicy(
new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT));
mListener = listener;
mDecodeConfig = decodeConfig;
mMaxWidth = maxWidth;
mMaxHeight = maxHeight;
mScaleType = scaleType;
}
}
下面是ImageRequest实现Request抽象类的两个抽象方法:
protected Response parseNetworkResponse(NetworkResponse response)
protected void deliverResponse(Bitmap response)
分别用于解析从网络或缓存中获取的图片数据;将解析后得到的bitmap传递给监听接口以便于bitmap进行操作。
首先来看parseNetworkResponse方法中调用了doParse(response)方法,这里是主要是对从网络或缓存中获取的bitmap原始数据进行解析的具体实现。
@Override
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
// Serialize all decode on a global lock to reduce concurrent heap usage.
synchronized (sDecodeLock) {
try {
return doParse(response);
} catch (OutOfMemoryError e) {
VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
return Response.error(new ParseError(e));
}
}
}
从注释中就可以看到,这里是真正的parseNetworkResponse的实现,对字节数组数据解码,这既是broken out for readability。
如果不规定MaxWidth和MaxHeight大小的话即(mMaxWidht==0&&mMaxHeight==0),直接通过BitmapFactory进行解码,不对解码后的图片进行尺寸调整。因为没有大小限制,所以无需调整啊。
否则通过decodeOptions存放解码后bitmap的真实大小,然后通过getResizedDimension()方法依据MaxWidth与MaxHeight得到我们期望的尺寸大小。
然后通过findBestSampleSize()方法得到期望的采样率并赋给decodeOptions.inSampleSize。
然后根据期望的采样率对字节数组进行decode获得bitmap。如果此时得到的bitmap尺寸仍然大于允许的最大尺寸的话,通过createScaledBitmap()得到期望尺寸的bitmap,然后把原先不合适的bitmap回收(如果不回收小心内存溢出)。如果尺寸合适就直接使用bitmap。
/**
* The real guts of parseNetworkResponse. Broken out for readability.
*/
private Response<Bitmap> doParse(NetworkResponse response) {
byte[] data = response.data;
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
Bitmap bitmap = null;
if (mMaxWidth == 0 && mMaxHeight == 0) {
decodeOptions.inPreferredConfig = mDecodeConfig;
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
} else {
// If we have to resize this image, first get the natural bounds.
decodeOptions.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
int actualWidth = decodeOptions.outWidth;
int actualHeight = decodeOptions.outHeight;
// Then compute the dimensions we would ideally like to decode to.
int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
actualWidth, actualHeight, mScaleType);
int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
actualHeight, actualWidth, mScaleType);
// Decode to the nearest power of two scaling factor.
decodeOptions.inJustDecodeBounds = false;
// TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
// decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
decodeOptions.inSampleSize =
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
Bitmap tempBitmap =
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
// If necessary, scale down to the maximal acceptable size.
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
tempBitmap.getHeight() > desiredHeight)) {
bitmap = Bitmap.createScaledBitmap(tempBitmap,
desiredWidth, desiredHeight, true);
tempBitmap.recycle();
} else {
bitmap = tempBitmap;
}
}
if (bitmap == null) {
return Response.error(new ParseError(response));
} else {
return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
}
}
下面再来分析deliverResponse(Bitmap response)方法,这是把已经解析并调整好尺寸的bitmap作为传入参数。
在这个方法中,只是简单的调用了监听接口的onResponse方法。其实这就是我们在使用volley框架时,对获得的bitmap进行处理的方法。
@Override
protected void deliverResponse(Bitmap response) {
mListener.onResponse(response);
}
我们都知道Volley适合传输数据量小的数据,这又是为什么呢?
那就要看一下调用newRequestQueue()生成一个请求队列对象的时候内部发生了什么。
这里我只贴出了部分代码,最开始当我们创建一个请求队列对象的时候,就会创建一个cacheDir文件夹。
再看后面的代码,调用了newRequestQueue()构造方法,其中传入的参数是DiskBasedCache!看到这里我最先想到既然是利用的磁盘缓存怎么会适合传输数据量小的数据呢?想知道原因就还得继续深入DiskBasedCache去查看它的实现。
/**
* Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
* You may set a maximum size of the disk cache in bytes.
*
* @param context A {@link Context} to use for creating the cache dir.
* @param stack An {@link HttpStack} to use for the network, or null for default.
* @param maxDiskCacheBytes the maximum size of the disk cache, in bytes. Use -1 for default size.
* @return A started {@link RequestQueue} instance.
*/
public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
...
if (maxDiskCacheBytes <= -1)
{
// No maximum size specified
queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
}
else
{
// Disk cache size specified
queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
}
queue.start();
return queue;
}
先前创建的cacheDir在DiskBasedCache中作为rootDirectory,然后会进行初始化。就是扫描rootDirectory文件夹所有的缓存文件并将其存入内存中。
那么一开始肯定是没有这个rootDirectory的,所以会进行mkdir。
public class DiskBasedCache implements Cache {
/**
* Constructs an instance of the DiskBasedCache at the specified directory.
* @param rootDirectory The root directory of the cache.
* @param maxCacheSizeInBytes The maximum size of the cache in bytes.
*/
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}
...
/**
* Initializes the DiskBasedCache by scanning for all files currently in the
* specified root directory. Creates the root directory if necessary.
*/
@Override
public synchronized void initialize() {
if (!mRootDirectory.exists()) {
//检查是否创建根目录
if (!mRootDirectory.mkdirs()) {
VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
}
return;
}
//扫描根目录下所有缓存文件
File[] files = mRootDirectory.listFiles();
if (files == null) {
return;
}
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);
} catch (IOException e) {
if (file != null) {
file.delete();
}
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException ignored) { }
}
}
}
}
将数据存入缓存中。先将数据存入缓存,先检查缓存是否会满,如果会满就先删除部分缓存数据,然后再新创建缓存文件
/**
* Puts the entry with the specified key into the cache.
*/
@Override
public synchronized void put(String key, Entry entry) {
//检查缓存是否会满,会的话则删除缓存中部分数据
pruneIfNeeded(entry.data.length);
//根据key创建新的缓存文件
File file = getFileForKey(key);
try {
//将缓存数据写入缓存文件
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
CacheHeader e = new CacheHeader(key, entry);
boolean success = e.writeHeader(fos);
if (!success) {
fos.close();
VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
throw new IOException();
}
fos.write(entry.data);
fos.close();
putEntry(key, e);
return;
} catch (IOException e) {
}
boolean deleted = file.delete();
if (!deleted) {
VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
}
}
从缓存中得到数据,先从CacheHeader中得到数据,然后从文件中读取缓存数据内容。
/**
* 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);
// if the entry does not exist, return.
if (entry == null) {
return null;
}
File file = getFileForKey(key);
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);
} catch (IOException e) {
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
remove(key);
return null;
} catch (NegativeArraySizeException e) {
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
remove(key);
return null;
} finally {
if (cis != null) {
try {
cis.close();
} catch (IOException ioe) {
return null;
}
}
}
}
所以综上所述,尽管是把缓存写到了文件中,但是最后仍然写入了内存当中。内存中无法存放数据量大的内容,自然volley适合传输数据量小的内容。
在volley框架中还封装了ImageLoader,不过volley并未对ImageLoader中的ImageCache进行实现,只是定义了一个ImageCache接口:
/**
* Simple cache adapter interface. If provided to the ImageLoader, it
* will be used as an L1 cache before dispatch to Volley. Implementations
* must not block. Implementation with an LruCache is recommended.
*/
public interface ImageCache {
public Bitmap getBitmap(String url);
public void putBitmap(String url, Bitmap bitmap);
}
volley推荐我们使用LruCache。
另外如果发生了多个相同的请求,volley中是如何处理的?
先得到request的url,将其相关信息存入hashmap中。volley中给我们提供了UrlRewriter的接口,定义了rewriteUrl(String originalUrl)。volley中是通过这个接口实现相同url进行复用,我们应该也可以自己实现当相同request传来时直接抛弃此request。
@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
String url = request.getUrl();
HashMap<String, String> map = new HashMap<String, String>();
map.putAll(request.getHeaders());
map.putAll(additionalHeaders);
if (mUrlRewriter != null) {
String rewritten = mUrlRewriter.rewriteUrl(url);
if (rewritten == null) {
throw new IOException("URL blocked by rewriter: " + url);
}
//复用相同request的url
url = rewritten;
}
URL parsedUrl = new URL(url);
HttpURLConnection connection = openConnection(parsedUrl, request);
for (String headerName : map.keySet()) {
connection.addRequestProperty(headerName, map.get(headerName));
}
...
}
/**
* An interface for transforming URLs before use.
*/
public interface UrlRewriter {
/**
* Returns a URL to use instead of the provided one, or null to indicate
* this URL should not be used at all.
*/
public String rewriteUrl(String originalUrl);
}