Media3 你的小电影是如何被缓存的?SimpleCache视频缓存分析

795 阅读5分钟

前言

YouTube Android 版用的也是ExoPlayer( Media3) 。看图片,可以看出来,缓存家族成员类不算少了,大约20个类。主要是靠大哥 SimpleCache 控制。让我们来看看!它是如何提供一个缓存和确保一个缓存的可用的。

屏幕截图 2024-04-27 235931.png

核心类用途
CacheDataSink负责缓存文件的写入
CacheDataSource缓存数据源
CachedContentIndex缓存文件内容索引
CacheFileMetadataIndex缓存文件数据索引
LeastRecentlyUsedCacheEvictor缓存LRU算法
SimpleCache缓存控制主体

先从缓存数据库表的设计入手

CachedContentIndex

这个表只有三个字段

  • id 自增值
  • key 缓存文件源地址
  • metadata 视频类型

屏幕截图 2024-04-28 224043.png

CacheFileMetadataIndex

这个表也有三个字段

  • name 定义缓存文件的名称(id + "." + position + "." + timestamp + SUFFI),注意文件name,前面的数字,这个name会缓存在TreeSet中,需要排序。缓存的文件是使用这个名称。

  • length 缓存文件的长度

  • last_touch_timestamp 缓存文件上一次访问的时间(可用于LRU算法)

屏幕截图 2024-04-28 223955.png

缓存的使用方式

只需要三个参数

1.缓存文件夹

2.缓存算法,缓存大小

3.储存的数据库提供者


val cache = SimpleCache(this.cacheDir, LeastRecentlyUsedCacheEvictor(1024*1024*1024L), StandaloneDatabaseProvider(this))

initialize() 缓存初始化

核心是创建缓存文件夹,扫描缓存文件,加载索引到内存中。

  • 1,创建缓存文件夹
  • 2,加载缓存文件
  • 3,加载缓存文件UID 标识,没有就创建一个
private void initialize() {
  if (!cacheDir.exists()) {
    try {
      createCacheDirectories(cacheDir);
    } catch (CacheException e) {
      initializationException = e;
      return;
    }
  }

  @Nullable File[] files = cacheDir.listFiles();
  if (files == null) {
    String message = "Failed to list cache directory files: " + cacheDir;
    Log.e(TAG, message);
    initializationException = new CacheException(message);
    return;
  }

  uid = loadUid(files);
  if (uid == UID_UNSET) {
    try {
      uid = createUid(cacheDir);
    } catch (IOException e) {
      String message = "Failed to create cache UID: " + cacheDir;
      Log.e(TAG, message, e);
      initializationException = new CacheException(message, e);
      return;
    }
  }

  try {
    contentIndex.initialize(uid);
    if (fileIndex != null) {
      fileIndex.initialize(uid);
      Map<String, CacheFileMetadata> fileMetadata = fileIndex.getAll();
      loadDirectory(cacheDir, /* isRoot= */ true, files, fileMetadata);
      fileIndex.removeAll(fileMetadata.keySet());
    } else {
      loadDirectory(cacheDir, /* isRoot= */ true, files, /* fileMetadata= */ null);
    }
  } catch (IOException e) {
    String message = "Failed to initialize cache indices: " + cacheDir;
    Log.e(TAG, message, e);
    initializationException = new CacheException(message, e);
    return;
  }

  contentIndex.removeEmpty();
  try {
    contentIndex.store();
  } catch (IOException e) {
    Log.e(TAG, "Storing index file failed", e);
  }
}

获取缓存

1,CachedContent不存在,创建长度占位

2,CachedContent 存在,长度一致

3,CachedContent 存在,还没有缓存


private SimpleCacheSpan getSpan(String key, long position, long length) {
  @Nullable CachedContent cachedContent = contentIndex.get(key);
  if (cachedContent == null) {
    return SimpleCacheSpan.createHole(key, position, length);
  }
  
  //确保缓存是null 或者缓存是可用
  while (true) {
    SimpleCacheSpan span = cachedContent.getSpan(position, length);
    if (span.isCached && Assertions.checkNotNull(span.file).length() != span.length) {
      //todo:移除length不匹配的缓存
      removeStaleSpans();
      continue;
    }
    return span;
  }
}

添加缓存

1,添加缓存

2,累计缓存大小

3,回调接口

private void addSpan(SimpleCacheSpan span) {
  contentIndex.getOrAdd(span.key).addSpan(span);
  totalSpace += span.length;
  notifySpanAdded(span);
}

移除一个缓存文件

private void removeSpanInternal(CacheSpan span) {
  @Nullable CachedContent cachedContent = contentIndex.get(span.key);
  if (cachedContent == null || !cachedContent.removeSpan(span)) {
    return;
  }
  totalSpace -= span.length;
  if (fileIndex != null) {
    String fileName = Assertions.checkNotNull(span.file).getName();
    try {
      fileIndex.remove(fileName);
    } catch (IOException e) {
      // This will leave a stale entry in the file index. It will be removed next time the cache
      // is initialized.
      Log.w(TAG, "Failed to remove file index entry for: " + fileName);
    }
  }
  contentIndex.maybeRemove(cachedContent.key);
  notifySpanRemoved(span);
}

移除文件length不匹配的。

private void removeStaleSpans() {
 
  ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>();
  for (CachedContent cachedContent : contentIndex.getAll()) {
    for (CacheSpan span : cachedContent.getSpans()) {
      if (Assertions.checkNotNull(span.file).length() != span.length) {
        spansToBeRemoved.add(span);
      }
    }
  }
  for (int i = 0; i < spansToBeRemoved.size(); i++) {
    removeSpanInternal(spansToBeRemoved.get(i));
  }
}

缓存唯一标识uid

屏幕截图 2024-05-07 232208.png

加载缓存唯一标识uid

加载缓存唯一标识uid,没有设置情况下,会调用createUid 创建一个。

private static long loadUid(File[] files) {
  for (File file : files) {
    String fileName = file.getName();
    if (fileName.endsWith(UID_FILE_SUFFIX)) {
      try {
        return parseUid(fileName);
      } catch (NumberFormatException e) {
        file.delete();
      }
    }
  }
  return UID_UNSET;
}

创建缓存文件唯一标识uid

1,生成非负UID

2, 保存为文件

3,返回Uid

private static long createUid(File directory) throws IOException {
  // 生成非负UID。
  long uid = new SecureRandom().nextLong();
  uid = uid == Long.MIN_VALUE ? 0 : Math.abs(uid);
  // 保存为文件
  String hexUid = Long.toString(uid, /* radix= */ 16);
  File hexUidFile = new File(directory, hexUid + UID_FILE_SUFFIX);
  if (!hexUidFile.createNewFile()) {
    // False means that the file already exists, so this should never happen.
    throw new IOException("Failed to create UID file: " + hexUidFile);
  }
  return uid;
}

缓存文件夹,标记上锁

给缓存文件夹,标记上锁,避免同时写同一个文件夹

  • lockFolder 给缓存文件夹,标记上锁,避免同时写同一个文件夹

  • unlockFolder 移除缓存文件夹,即标记解锁

private static synchronized boolean lockFolder(File cacheDir) {
  return lockedCacheDirs.add(cacheDir.getAbsoluteFile());
}

private static synchronized void unlockFolder(File cacheDir) {
  lockedCacheDirs.remove(cacheDir.getAbsoluteFile());
}

无阻塞获取缓存

  • 1,获取缓存基本信息
  • 2,如果缓存已缓存,就修改缓存访问时间
  • 3,没有缓存,如果上锁了,返回 span.
  • 4,缓存不存在,也没有上锁,返回null
public synchronized CacheSpan startReadWriteNonBlocking(String key, long position, long length)
    throws CacheException {
  Assertions.checkState(!released);
  checkInitialization();

  SimpleCacheSpan span = getSpan(key, position, length);

  if (span.isCached) {
    // Read case.
    return touchSpan(key, span);
  }

  CachedContent cachedContent = contentIndex.getOrAdd(key);
  if (cachedContent.lockRange(position, span.length)) {
    // Write case.
    return span;
  }

  // Lock not available.
  return null;
}

阻塞获取缓存

代码核心还是调用 startReadWriteNonBlocking实现,就是多了阻塞。从 CacheDataSource 中,可以看出 openNextSource 看出 当 blockOnCache为true时候,会调用这个方法。FLAG_BLOCK_ON_CACHE: 如果缓存被锁定,不管数据是否被缓存,都从上游读取。 下载器会使用到这个 缓存flag

public synchronized CacheSpan startReadWrite(String key, long position, long length)
    throws InterruptedException, CacheException {
  Assertions.checkState(!released);
  checkInitialization();
  while (true) {
    CacheSpan span = startReadWriteNonBlocking(key, position, length);
    if (span != null) {
      return span;
    } else {
      ......
      wait();
    }
  }
}


提交缓存文件

1,创建一个SimpleCacheSpan

2,检查span是否与设置的内容长度冲突

3,写入数据库,缓存索引(缓存到文件夹下的文件使用的是这里的name)

4,添加到内存缓存中

5,写入缓存文件索引到数据库

6,唤醒线程,startReadWrite 中的等待被唤醒。

public synchronized void commitFile(File file, long length) throws CacheException {

  ......
  SimpleCacheSpan span =
      Assertions.checkNotNull(SimpleCacheSpan.createCacheEntry(file, length, contentIndex));
  CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(span.key));
  Assertions.checkState(cachedContent.isFullyLocked(span.position, span.length));

  long contentLength = ContentMetadata.getContentLength(cachedContent.getMetadata());
  if (contentLength != C.LENGTH_UNSET) {
    Assertions.checkState((span.position + span.length) <= contentLength);
  }

  if (fileIndex != null) {
    String fileName = file.getName();
    try {
      //写入数据库
      fileIndex.set(fileName, span.length, span.lastTouchTimestamp);
    } catch (IOException e) {
      throw new CacheException(e);
    }
  }
  //添加到内存中
  addSpan(span);
  try {

    //写入缓存文件索引到数据库
    contentIndex.store();
  } catch (IOException e) {
    throw new CacheException(e);
  }
  notifyAll();
}

释放缓存坑位

释放从startReadWrite获得的CacheSpan,该CacheSpan对应于缓存中一个坑位。在CacheDataSource 里面调用,有三处地方,分别是 cacheWriteDataSource 不等于null,抛出异常时,在 closeCurrentSource中调用。

@Override
public synchronized void releaseHoleSpan(CacheSpan holeSpan) {
Assertions.checkState(!released);
CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(holeSpan.key));
cachedContent.unlockRange(holeSpan.position);
contentIndex.maybeRemove(cachedContent.key);
notifyAll();
}

下一篇我们详细分析一下 CacheDataSource 吧!打通缓存数据源的获取

参考资料

ExoPlayer架构详解与源码分析(12)——Cache

juejin.cn/post/734993…

本人知识有限,如有描述错误之处,望虎正。

你的赞就像冬日暖阳,温暖心窝。