深度揭秘!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;}// 从缓存中获取指定键的数据条目@Overridepublic 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);// 如果头部信息读取失败,删除文件并返回nullif (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) {}}}}// 将数据存入缓存@Overridepublic 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;}// 执行网络请求@Overridepublic 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和Executorpublic ExecutorDelivery(Handler mainThreadHandler) {mMainThreadHandler = mainThreadHandler;mExecutor = new Executor() {@Overridepublic void execute(Runnable command) {command.run();}};}// 分发正常响应@Overridepublic void postResponse(Request<?> request, Response<?> response) {postResponse(request, response, null);}// 分发响应,并可指定在响应分发后执行的Runnable任务@Overridepublic 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));}}// 分发错误响应@Overridepublic 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;}@Overridepublic 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)问题。特别是在一些配置较低的设备上,过大的线程池会使系统资源更加紧张