深度揭秘!Android Volley性能瓶颈全解析与源码级剖析
一、引言
在Android移动开发领域,Android Volley曾是开发者处理网络请求的热门选择。它以轻量级、易用性和内置缓存等特性,帮助开发者快速实现网络通信功能。然而,随着应用规模的扩大和用户对性能要求的不断提高,Volley在实际使用中逐渐暴露出一些性能瓶颈。这些瓶颈不仅影响应用的响应速度,还可能导致用户体验下降、资源浪费等问题。深入分析Volley的性能瓶颈,从源码层面探究其产生的原因,对于优化应用性能、提升用户体验具有重要意义。本文将围绕Android Volley的性能瓶颈展开全面、深入的源码级分析,揭开其性能问题背后的真相。
二、Volley架构基础回顾
在分析性能瓶颈之前,有必要先对Volley的基础架构进行回顾,这有助于我们更好地理解后续性能问题产生的根源。
2.1 核心组件
Volley主要由以下几个核心组件构成:
- RequestQueue(请求队列):请求队列是Volley的核心调度中心,负责管理和调度所有的网络请求。它包含两个子队列,分别是缓存队列(
mCacheQueue)和网络队列(mNetworkQueue)。
public class RequestQueue {
// 缓存队列,存储可缓存的网络请求
private final BlockingQueue<Request<?>> mCacheQueue = new LinkedBlockingQueue<>();
// 网络队列,存储需要进行网络请求的任务
private final BlockingQueue<Request<?>> mNetworkQueue = new LinkedBlockingQueue<>();
// 缓存对象,用于读写缓存数据
private final Cache mCache;
// 网络对象,负责实际的网络请求操作
private final Network mNetwork;
// 响应分发器,将处理后的响应数据分发到主线程
private final ResponseDelivery mDelivery;
// 缓存调度器线程
private CacheDispatcher mCacheDispatcher;
// 网络调度器线程数组
private NetworkDispatcher[] mNetworkDispatchers;
// 构造函数,初始化请求队列相关组件
public RequestQueue(Cache cache, Network network, int threadPoolSize, ResponseDelivery delivery) {
mCache = cache;
mNetwork = network;
mDelivery = delivery;
// 创建缓存调度器线程
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
// 创建并启动多个网络调度器线程
mNetworkDispatchers = new NetworkDispatcher[threadPoolSize];
for (int i = 0; i < threadPoolSize; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
mNetworkDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
// 将请求添加到请求队列中
public <T> Request<T> add(Request<T> request) {
// 设置请求所属的请求队列
request.setRequestQueue(this);
// 判断请求是否可缓存,决定放入哪个队列
if (request.shouldCache()) {
mCacheQueue.add(request);
} else {
mNetworkQueue.add(request);
}
return request;
}
// 启动请求队列,开始处理请求
public void start() {
mCacheDispatcher.start();
for (NetworkDispatcher networkDispatcher : mNetworkDispatchers) {
networkDispatcher.start();
}
}
}
- Cache(缓存):缓存模块负责存储和读取网络请求的响应数据,通过减少重复的网络请求来提高应用性能。Volley的缓存接口为
Cache,默认实现类是DiskBasedCache,基于文件系统实现缓存功能。
public class DiskBasedCache implements Cache {
// 缓存目录,用于存储缓存文件
private final File mRootDirectory;
// 缓存的最大容量(字节)
private final int mMaxCacheSizeInBytes;
// 当前缓存已使用的大小(字节)
private long mTotalSize = 0;
// 缓存条目映射表,通过缓存键快速查找缓存条目
private final Map<String, CacheHeader> mEntries = new LinkedHashMap<>(16, 0.75f, true);
// 构造函数,初始化缓存相关参数
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}
// 从缓存中获取指定键的数据条目
@Override
public Entry get(String key) {
// 根据缓存键生成对应的缓存文件
File file = getFileForKey(key);
// 如果文件不存在,说明缓存中没有该数据
if (!file.exists()) {
return null;
}
BufferedInputStream fis = null;
try {
// 打开文件输入流
fis = new BufferedInputStream(new FileInputStream(file));
// 读取缓存头部信息,获取元数据
CacheHeader header = CacheHeader.readHeader(fis);
// 如果头部信息读取失败,删除文件并返回null
if (header == null) {
VolleyLog.d("Cache header corruption for %s", file.getAbsolutePath());
file.delete();
return null;
}
// 读取缓存数据部分
byte[] data = streamToBytes(fis, (int) (file.length() - fis.available()));
// 创建缓存条目对象并填充数据
Entry entry = new Entry();
entry.data = data;
entry.etag = header.etag;
entry.softTtl = header.softTtl;
entry.ttl = header.ttl;
entry.serverDate = header.serverDate;
entry.responseHeaders = header.responseHeaders;
return entry;
} catch (IOException e) {
// 发生异常时,记录日志并删除文件
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
remove(key);
return null;
} finally {
// 关闭输入流
if (fis != null) {
try {
fis.close();
} catch (IOException ignored) {
}
}
}
}
// 将数据存入缓存
@Override
public void put(String key, Entry entry) {
// 检查缓存空间是否足够,不足则清理部分缓存
pruneIfNeeded(entry.data.length);
// 根据缓存键生成对应的缓存文件
File file = getFileForKey(key);
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try {
// 创建临时文件用于写入
File tmpFile = getTempFileForKey(key);
// 打开临时文件输出流
fos = new FileOutputStream(tmpFile);
bos = new BufferedOutputStream(fos);
// 写入缓存头部信息
CacheHeader header = new CacheHeader(key, entry);
header.writeHeader(bos);
// 写入缓存数据
bos.write(entry.data);
// 刷新输出流
bos.flush();
// 将临时文件重命名为正式的缓存文件
if (!tmpFile.renameTo(file)) {
VolleyLog.e("ERROR: rename failed, tmpFile=%s, file=%s", tmpFile.getAbsolutePath(), file.getAbsolutePath());
throw new IOException("Rename failed!");
}
// 更新缓存条目映射表和总大小
putEntry(key, header);
} catch (IOException e) {
// 发生异常时,删除文件并记录日志
if (file.exists()) {
if (!file.delete()) {
VolleyLog.e("Could not clean up file %s", file.getAbsolutePath());
}
}
VolleyLog.e("Failed to write cache entry for key=%s, filename=%s: %s", key, file.getAbsolutePath(), e.getMessage());
} finally {
// 关闭输出流
if (bos != null) {
try {
bos.close();
} catch (IOException ignored) {
}
} else if (fos != null) {
try {
fos.close();
} catch (IOException ignored) {
}
}
}
}
// 如果缓存空间不足,清理部分缓存
private void pruneIfNeeded(int neededSpace) {
// 如果当前缓存大小加上需要的空间超过最大缓存容量
if ((mTotalSize + neededSpace) > mMaxCacheSizeInBytes) {
// 计算目标缓存大小(最大容量的90%)
int targetSize = (int) (mMaxCacheSizeInBytes * 0.9f);
// 遍历缓存条目映射表,删除较旧的条目
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
while (iterator.hasNext() && mTotalSize + neededSpace > mMaxCacheSizeInBytes) {
Map.Entry<String, CacheHeader> entry = iterator.next();
CacheHeader e = entry.getValue();
// 删除对应的缓存文件
boolean deleted = e.file.delete();
// 更新已使用的缓存大小
if (deleted) {
mTotalSize -= e.size;
}
// 从映射表中移除该条目
iterator.remove();
}
// 如果清理后仍空间不足,记录错误
if (mTotalSize + neededSpace > mMaxCacheSizeInBytes) {
VolleyLog.e("Failed to clear space in cache");
}
}
}
// 其他辅助方法...
}
- Network(网络):网络模块负责执行实际的网络请求操作,从服务器获取数据。Volley的网络接口为
Network,默认实现类是BasicNetwork,基于HttpURLConnection或HttpClient实现网络请求。
public class BasicNetwork implements Network {
// HTTP堆栈对象,用于发送HTTP请求
private final HttpStack mHttpStack;
// 重试策略,用于处理请求失败时的重试逻辑
private final RetryPolicy mRetryPolicy;
// 构造函数,初始化HTTP堆栈和重试策略
public BasicNetwork(HttpStack httpStack) {
this(httpStack, new DefaultRetryPolicy());
}
public BasicNetwork(HttpStack httpStack, RetryPolicy retryPolicy) {
mHttpStack = httpStack;
mRetryPolicy = retryPolicy;
}
// 执行网络请求
@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
// 记录请求开始时间
long requestStart = SystemClock.elapsedRealtime();
while (true) {
try {
// 构建HTTP请求
HttpStack.HttpRequestStackEntry entry = mHttpStack.performRequest(request, new HashMap<String, String>());
// 获取HTTP响应
HttpResponse httpResponse = entry.response;
// 获取响应状态码
int statusCode = httpResponse.getStatusLine().getStatusCode();
// 读取响应内容
byte[] responseContents = EntityUtils.toByteArray(httpResponse.getEntity());
// 解析响应头部信息
Map<String, String> responseHeaders = entry.responseHeaders;
// 如果响应状态码在200 - 299之间,表示请求成功
if (statusCode >= 200 && statusCode < 300) {
return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
}
// 如果响应状态码为304,表示缓存未失效
if (statusCode == 304) {
return new NetworkResponse(statusCode, null, responseHeaders, true);
}
// 其他错误状态码,抛出异常
throw new IOException();
} catch (IOException e) {
// 处理请求失败的情况,根据重试策略进行重试
request.addMarker("network-http-attempt-failed");
if (!request.shouldRetryNetworkException(new VolleyError(e))) {
throw new VolleyError(e);
}
if (mRetryPolicy.hasAttemptRemaining()) {
try {
mRetryPolicy.retry(new VolleyError(e));
} catch (VolleyError retryError) {
throw retryError;
}
} else {
throw new VolleyError(e);
}
}
}
}
}
- ResponseDelivery(响应分发器):响应分发器负责将处理后的响应数据分发到主线程,以便更新UI或进行其他业务逻辑处理。
public class ExecutorDelivery implements ResponseDelivery {
// 主线程的Handler,用于在主线程执行任务
private final Handler mMainThreadHandler;
// 后台线程的Executor,用于执行非UI相关任务
private final Executor mExecutor;
// 构造函数,初始化Handler和Executor
public ExecutorDelivery(Handler mainThreadHandler) {
mMainThreadHandler = mainThreadHandler;
mExecutor = new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
};
}
// 分发正常响应
@Override
public void postResponse(Request<?> request, Response<?> response) {
postResponse(request, response, null);
}
// 分发响应,并可指定在响应分发后执行的Runnable任务
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
// 如果响应是中间响应(例如缓存过期但先返回旧数据)
if (response.intermediate) {
// 在后台线程执行响应分发
mExecutor.execute(new ResponseDeliveryRunnable(request, response, runnable));
} else {
// 在主线程执行响应分发
mMainThreadHandler.post(new ResponseDeliveryRunnable(request, response, runnable));
}
}
// 分发错误响应
@Override
public void postError(Request<?> request, VolleyError error) {
// 创建错误响应对象
Response<?> response = Response.error(error);
// 在主线程分发错误响应
mMainThreadHandler.post(new ResponseDeliveryRunnable(request, response, null));
}
// 响应分发的Runnable任务类
private class ResponseDeliveryRunnable implements Runnable {
private final Request<?> mRequest;
private final Response<?> mResponse;
private final Runnable mRunnable;
public ResponseDeliveryRunnable(Request<?> request, Response<?> response, Runnable runnable) {
mRequest = request;
mResponse = response;
mRunnable = runnable;
}
@Override
public void run() {
// 如果请求已被取消,直接结束
if (mRequest.isCanceled()) {
mRequest.finish("canceled-at-delivery");
return;
}
// 调用请求的响应监听器处理响应
mRequest.deliverResponse(mResponse.result);
// 如果有指定的Runnable任务,执行该任务
if (mRunnable != null) {
mRunnable.run();
}
// 标记请求已完成响应分发
mRequest.finish("done");
}
}
}
2.2 工作流程
Volley的工作流程如下:
- 开发者创建网络请求对象,并调用
RequestQueue.add(request)方法将请求添加到请求队列中。 - 请求队列根据请求的
shouldCache属性,将请求放入缓存队列(可缓存请求)或网络队列(不可缓存请求)。 - 缓存调度器
CacheDispatcher从缓存队列中取出请求,检查缓存中是否存在对应数据。若存在且未过期,则直接将数据返回;若不存在或已过期,则将请求转发到网络队列。 - 网络调度器
NetworkDispatcher从网络队列中取出请求,执行网络请求操作,从服务器获取数据。 - 网络请求成功后,将响应数据进行处理,并根据请求是否可缓存,决定是否更新缓存。
- 最后,响应分发器
ResponseDelivery将处理后的响应数据分发到主线程,完成整个网络请求流程。
了解Volley的架构和工作流程是分析其性能瓶颈的基础,接下来我们将深入探讨Volley在实际使用中存在的性能问题。
三、线程池与请求队列引发的性能瓶颈
3.1 固定线程池大小的局限性
Volley使用固定大小的线程池来处理网络请求,在RequestQueue的构造函数中进行初始化:
public RequestQueue(Cache cache, Network network, int threadPoolSize, ResponseDelivery delivery) {
// 省略其他参数初始化...
// 创建网络调度器线程数组,线程池大小固定
mNetworkDispatchers = new NetworkDispatcher[threadPoolSize];
for (int i = 0; i < threadPoolSize; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
mNetworkDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
这种固定线程池大小的设计在某些场景下会带来性能问题。当应用同时发起大量网络请求时,如果线程池大小设置过小,会导致请求在网络队列中大量堆积,等待时间过长,从而影响应用的响应速度。例如,在一个新闻类应用中,首页可能需要同时加载新闻列表、广告、推荐内容等多个数据,若线程池大小仅设置为默认的4个线程,这些请求可能无法及时得到处理,用户会感觉到页面加载缓慢。
另一方面,如果线程池大小设置过大,虽然可以加快请求处理速度,但会占用过多的系统资源,导致系统性能下降,甚至可能引发OOM(Out Of Memory)问题。特别是在一些配置较低的设备上,过大的线程池会使系统资源更加紧张