Android VideoCache缓存框架分析
一、概述
AndroidVideoCache是一个视频缓存框架,支持边下载边播放。
基本原理:使用本地代理代替直接根据url请求网络服务。
1.首先在本地新建一个服务(ServerSocket),监听客户端的接入,一旦有客户端接入就新建一个Socket来维持客户端和服务端之间的通讯。
2.转换url,在客户端请求的时候使用proxy.getProxyUrl(url)把网络url转为本地url,调用这个url的时候会和本地的代理服务ServerSocket连接(也就是第一步)
3.根据第二步,连接成功后创建一个HttpProxyCacheServerClients客户端,并执行request方法。
4.创建HttpProxyCache类,并执行其processRequest方法回复数据。
5.判断文件是否下载完成,如果没有下载完成,则单独开启一个线程进行异步下载(readSourceAsync)下载的数据会存入缓存。之后从缓存中读取数据(cache.read),也就是是播放器只读缓存数据
二、代码示例分析
入口是HttpProxyCacheServer.java类。
1.在Application中初始化HttpProxyCacheServer类,并建立本地代理服务
public static HttpProxyCacheServer getProxy(Context context) { App app = (App) context.getApplicationContext(); return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy; } private HttpProxyCacheServer newProxy() { return new HttpProxyCacheServer.Builder(this) .cacheDirectory(Utils.getVideoCacheDir(this)) .build(); }
在HttpProxyCacheServer的构造方法中会做一系列的初始化动作及新建一个代理服务,代码如下
private HttpProxyCacheServer(Config config) { this.config = checkNotNull(config); try { InetAddress inetAddress = InetAddress.getByName(PROXY_HOST); /**ServerSocket(port,backlog,inetAddress)参数说明: * port:第一个参数为端口号,如果填写0,则系统自动会分配一个端口号 * backlog:第二个参数:最多同时可支持多少个链接 * inetAddress:主机地址 * */ this.serverSocket = new ServerSocket(0, 8, inetAddress); //获取随机分配的端口号 this.port = serverSocket.getLocalPort(); IgnoreHostProxySelector.install(PROXY_HOST, port); /**信号量:其目的是为了让waitConnectionThread的run方法先执行*/ CountDownLatch startSignal = new CountDownLatch(1); /**创建并开启一个一直等待客户端连接的线程*/ this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal)); this.waitConnectionThread.start(); startSignal.await(); // freeze thread, wait for server starts //创建一个ping实例 this.pinger = new Pinger(PROXY_HOST, port); LOG.info("Proxy cache server started. Is it alive? " + isAlive()); } catch (IOException | InterruptedException e) { socketProcessor.shutdown(); throw new IllegalStateException("Error starting local proxy server", e); } }
其中上述代码:startSignal信号量是用来确保WaitRequestRunnable类的run方法先执行,WaitRequestsRunnable线程使用来开启代理服务,并等待接受客户端的链接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** * 一直等待客户端连接的线程 */ private final class WaitRequestsRunnable implements Runnable { private final CountDownLatch startSignal; public WaitRequestsRunnable(CountDownLatch startSignal) { this .startSignal = startSignal; } @Override public void run() { startSignal.countDown(); //确保run方法先执行 waitForRequest(); } } |
通过waitForRequest()等待客户端的接入,并将创建好的客户端Socket放入socketProcessor线程池中执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /** * 死循环通过serverSocket.accept()一直阻塞等待客户端连接 */ private void waitForRequest() { try { while (!Thread.currentThread().isInterrupted()) { //一直等待有客户端连接进来 Socket socket = serverSocket.accept(); LOG.info( "Accept new socket " + socket); //连接进来之后立马提交给线程池执行 socketProcessor.submit( new SocketProcessorRunnable(socket)); } } catch (IOException e) { onError( new ProxyCacheException( "Error during waiting connection" , e)); } } |
到这里初始化这块已经结束了。首先ServerSocket服务建立好了,其次接受客户端连接的线程也创建好了,最后接收客户端Socket后会放入线程池中执行。
2.接下来就是客户端请求服务端,代码如下:
private void startVideo() { HttpProxyCacheServer proxy = App.getProxy(getActivity()); proxy.registerCacheListener(this, url); //将url转换为代理url String proxyUrl = proxy.getProxyUrl(url); Log.d(LOG_TAG, "Use proxy url " + proxyUrl + " instead of original url " + url); videoView.setVideoPath(proxyUrl); videoView.start(); }
在上述代码中最主要的是proxy.getProxyUrl(url)方法,此方法是把网络url编码为本地的url,然后videoView.setVideoPath()设置本地请求路径,之后通过videoView.start()方法通过本地路径请求服务,此请求会到本地代理服务ServerSocket中。即会放到SocketProcessorRunnable类中执行。
请求到这里算是结束了,接下来会看一下请求后的操作(这块是最主要的)
3.客户端和服务端连通成功后的操作。其实这里会分成两块,一块是播放器和本地代理服务,另一块是本地代理服务和真正的云端服务。
调用客户端HttpProxyCacheServerClients的processRequest方法创建一个HttpProxyCache类,利用该类的processRequest方法发起(本地)网络请求
1 2 3 4 5 6 7 8 9 | public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException { startProcessRequest(); try { clientsCount.incrementAndGet(); proxyCache.processRequest(request, socket); } finally { finishProcessRequest(); } } |
封装响应头并给客户端回复消息
public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException { OutputStream out = new BufferedOutputStream(socket.getOutputStream()); String responseHeaders = newResponseHeaders(request); out.write(responseHeaders.getBytes("UTF-8"));//给客户端回复消息 long offset = request.rangeOffset; if (isUseCache(request)) { responseWithCache(out, offset); } else { responseWithoutCache(out, offset); } }
1 2 3 4 5 6 7 8 9 | private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int readBytes; while ((readBytes = read(buffer, offset, buffer.length)) != -1) { out.write(buffer, 0, readBytes); offset += readBytes; } out.flush(); } |
关键代码为read方法。read方法的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public int read(byte[] buffer, long offset, int length) throws ProxyCacheException { ProxyCacheUtils.assertBuffer(buffer, offset, length); while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) { readSourceAsync(); waitForSourceData(); checkReadSourceErrorsCount(); } int read = cache.read(buffer, offset, length); if (cache.isCompleted() && percentsAvailable != 100) { percentsAvailable = 100; onCachePercentsAvailableChanged(100); } return read; } |
此方法做了两件主要的事情:1.检测文件是否已经下载完成(readSourceAsync();),如果没有下载完成就接着下载,如果下载完成就跳过,代码如下:
1 2 3 4 5 6 7 | private synchronized void readSourceAsync() throws ProxyCacheException { boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED; if (!stopped && !cache.isCompleted() && !readingInProgress) { sourceReaderThread = new Thread( new SourceReaderRunnable(), "Source reader for " + source); sourceReaderThread.start(); } } |
在SourceReaderRunnable中会调用run方法执行readSource方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | private void readSource() { long sourceAvailable = -1; long offset = 0; try { offset = cache.available(); source.open(offset); sourceAvailable = source.length(); byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE]; int readBytes; while ((readBytes = source.read(buffer)) != -1) { synchronized (stopLock) { if (isStopped()) { return ; } cache.append(buffer, readBytes); } offset += readBytes; notifyNewCacheDataAvailable(offset, sourceAvailable); } tryComplete(); onSourceRead(); } catch (Throwable e) { readSourceErrorsCount.incrementAndGet(); onError(e); } finally { closeSource(); notifyNewCacheDataAvailable(offset, sourceAvailable); } } |
此方法的作用是接着上次没下载完的文件继续下载,下载后写入缓存文件,并实时通知客户端下载进度
read方法做的第二件事情就是从缓存中读取数据给客户端
1 2 3 4 5 6 7 8 9 10 | @Override public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException { try { dataFile.seek(offset); return dataFile.read(buffer, 0, length); } catch (IOException e) { String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]" ; throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e); } } |
总结:到此AndroidVideoCache的源代码已经分析完毕了。其最主要的流程便是:代理服务器连接真是的网络url并把文件下载到本地cache,客户端连接本地代理服务器,并从本地cache中拿到数据并播放。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库