ExoPlayer架构详解与源码分析(13)——TeeDataSource和CacheDataSource

系列文章目录


前言

书接上回,继续贴下上文的蜘蛛网

铺垫了那么多的基础,本篇终于可以来分析下CacheDataSource,上篇重点讲完了图的Cache的下半部分,而将Cache和CacheDataSource关联起来的是一个叫TeeDataSource特殊DataSource。

TeeDataSource

这是一个3通阀门一样的DataSource,有一个输入端2个输出端,也可以理解为在正常的输入和输出端中间又接了一个输出端,类似于下图

upstream也是一个DataSource,一般是上游的原始数据源,如OkHttpDataSource获取的网络源,dataSink参照上篇ExoPlayer架构详解与源码分析(12)------Cache,这些参数在TeeDataSource初始化时设置的。 看下代码实现

java 复制代码
  @Override
  //打开数据源
  public long open(DataSpec dataSpec) throws IOException {
    bytesRemaining = upstream.open(dataSpec);//打开上游数据源
    if (bytesRemaining == 0) {
      return 0;
    }
    if (dataSpec.length == C.LENGTH_UNSET && bytesRemaining != C.LENGTH_UNSET) {
      // 根据实际长度裁剪dataSpec,保证传给dataSink长度正确
      dataSpec = dataSpec.subrange(0, bytesRemaining);
    }
    dataSinkNeedsClosing = true;
    dataSink.open(dataSpec);//dataSink打开流,主要就是通过cache startFile打开缓存文件获取到流
    return bytesRemaining;
  }
  
  @Override
  //读取数据
  @Override
  public int read(byte[] buffer, int offset, int length) throws IOException {
    if (bytesRemaining == 0) {
      return C.RESULT_END_OF_INPUT;
    }
    //从上游读取数据
    int bytesRead = upstream.read(buffer, offset, length);
    if (bytesRead > 0) {
      // 在数据返回前,先通过dataSink将数据写入到上面打开的缓存文件流里
      dataSink.write(buffer, offset, bytesRead);
      if (bytesRemaining != C.LENGTH_UNSET) {
        bytesRemaining -= bytesRead;
      }
    }
    //输出数据
    return bytesRead;
  }

可以看到TeeDataSource实现很简单,TeeDataSource就是为了数据边读边缓存而实现的,只要是读取过的数据都会缓存到文件里,至于在什么时候使用TeeDataSource,这就要讲到本篇的重点CacheDataSource了。

CacheDataSource

读取和写入Cache的DataSource 。如果可能的话,请求会从缓存中获取。当数据未缓存时,会从上游DataSource请求数据并将其写入缓存。 CacheDataSource,主要包含3个DataSource

  • upstreamDataSource 上游原始数据的源,如果此时播放的是一个网络的URL文件,这个upstreamDataSource可能就是OkHttpDataSource,无需缓存或者当前已经在缓存时,就会使用此源。
  • cacheWriteDataSource 缓存写入源,默认的是TeeDataSource,通过它在数据读取时缓存到文件,当前资源未缓存且需要缓存时,就会使用此源。
  • cacheReadDataSource 缓存数据读取源,当前播放的数据已经被缓存时,就会通过这个源将缓存的数据读取出来,正常数据都是缓存在本地文件中的所以这里一般都是FileDataSource,当前资源已经查询到缓存时,就会使用此源。

上面几个源的关系可以在源码的openNextSource方法中看出来。

接下来看源码实现:

java 复制代码
private CacheDataSource(
      Cache cache,
      @Nullable DataSource upstreamDataSource,
      DataSource cacheReadDataSource,
      @Nullable DataSink cacheWriteDataSink,
      @Nullable CacheKeyFactory cacheKeyFactory,
      @Flags int flags,
      @Nullable PriorityTaskManager upstreamPriorityTaskManager,
      int upstreamPriority,
      @Nullable EventListener eventListener) {
    this.cache = cache;
    //用于读取缓存的DataSource,只要是缓存在文件里这里一般都是FileDataSource
    this.cacheReadDataSource = cacheReadDataSource;
    //cacheKeyFactory 用于生成CacheContent的资源key,默认就是取DataSpec.key,没有则获取播放文件的URL
    this.cacheKeyFactory = cacheKeyFactory != null ? cacheKeyFactory : CacheKeyFactory.DEFAULT;
    //是否在缓存时阻塞
    this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0;
    this.ignoreCacheOnError = (flags & FLAG_IGNORE_CACHE_ON_ERROR) != 0;
    this.ignoreCacheForUnsetLengthRequests =
        (flags & FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS) != 0;
    if (upstreamDataSource != null) {
      if (upstreamPriorityTaskManager != null) {
        upstreamDataSource =
            new PriorityDataSource(
                upstreamDataSource, upstreamPriorityTaskManager, upstreamPriority);
      }
      this.upstreamDataSource = upstreamDataSource;
      this.cacheWriteDataSource =
          cacheWriteDataSink != null//创建TeeDataSource作为cacheWriteDataSource 
              ? new TeeDataSource(upstreamDataSource, cacheWriteDataSink)
              : null;
    } else {
      this.upstreamDataSource = PlaceholderDataSource.INSTANCE;
      this.cacheWriteDataSource = null;
    }
    this.eventListener = eventListener;
  }
  
  @Override
  public long open(DataSpec dataSpec) throws IOException {
    try {
      String key = cacheKeyFactory.buildCacheKey(dataSpec);//根据dataSpec获取资源的key,一般为URL
      DataSpec requestDataSpec = dataSpec.buildUpon().setKey(key).build();//DataSpec设置key
      this.requestDataSpec = requestDataSpec;
      //如果之前有缓存,这里会通过当前的key查询索引中的资源,获取缓存里的最终跳转的真实URL
      actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ requestDataSpec.uri);
      readPosition = dataSpec.position;//更新当前的读取位置

      int reason = shouldIgnoreCacheForRequest(dataSpec);
      currentRequestIgnoresCache = reason != CACHE_NOT_IGNORED;
      if (currentRequestIgnoresCache) {//是否忽略缓存
        notifyCacheIgnored(reason);
      }

      if (currentRequestIgnoresCache) {
        bytesRemaining = C.LENGTH_UNSET;
      } else {
        //获取保存在索引文件中的资源Metadata中的总长度
        bytesRemaining = ContentMetadata.getContentLength(cache.getContentMetadata(key));
        if (bytesRemaining != C.LENGTH_UNSET) {
          bytesRemaining -= dataSpec.position;//总长度-当前位置=剩余数据长度
          if (bytesRemaining < 0) {//如果为负数说明当前位置已经超出资源的最大长度
            throw new DataSourceException(
                PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE);
          }
        }
      }
      if (dataSpec.length != C.LENGTH_UNSET) {
        bytesRemaining =//根据dataSpec.length更新bytesRemaining 
            bytesRemaining == C.LENGTH_UNSET
                ? dataSpec.length
                : min(bytesRemaining, dataSpec.length);
      }
      if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
        openNextSource(requestDataSpec, false);//重点看这个方法
      }
      return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : bytesRemaining;
    } catch (Throwable e) {
      handleBeforeThrow(e);
      throw e;
    }
  }
  
  private void openNextSource(DataSpec requestDataSpec, boolean checkCache) throws IOException {
    @Nullable CacheSpan nextSpan;
    String key = castNonNull(requestDataSpec.key);
    if (currentRequestIgnoresCache) {//如果忽略缓存,后面直接打开上游DataSource
      nextSpan = null;
    } else if (blockOnCache) {//通常下载器才会设置为true
    //需要阻塞获取调用startReadWrite,获取占位的HoleSpan,这里锁定了当前资源的剩余所有数据段
    //后续DataSkin会将HoleSpan锁定的区域,切分出多个文件对于多个CacheSpan添加到索引中
      try {
        nextSpan = cache.startReadWrite(key, readPosition, bytesRemaining);
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new InterruptedIOException();
      }
    } else {//无阻塞获取,正常播放主要通过startReadWriteNonBlocking获取
      nextSpan = cache.startReadWriteNonBlocking(key, readPosition, bytesRemaining);
    }

    DataSpec nextDataSpec;//下一个DataSpec 
    DataSource nextDataSource;//下一个DataSource 
    if (nextSpan == null) {
      //nextSpan == null2种可能,第一种是忽略缓存,第二种因为startReadWriteNonBlocking获取到了一个空的nextSpan,说明当前位置的缓存被锁定了,可能正在写入缓存
      //这个时候播放器是不能等待数据缓存完成的(下载器可以),会直接打开上游的数据源获取数据,保证正常播放
      nextDataSource = upstreamDataSource;//OkHttpDataSource
      nextDataSpec =
          requestDataSpec.buildUpon().setPosition(readPosition).setLength(bytesRemaining).build();
    } else if (nextSpan.isCached) {
      // 当前position数据已经被缓存了
      //直接获取缓存的文件使用FileDataSource打开源获取数据
      Uri fileUri = Uri.fromFile(castNonNull(nextSpan.file));
      long filePositionOffset = nextSpan.position;
      long positionInFile = readPosition - filePositionOffset;
      long length = nextSpan.length - positionInFile;
      if (bytesRemaining != C.LENGTH_UNSET) {
        length = min(length, bytesRemaining);
      }
      nextDataSpec =
          requestDataSpec
              .buildUpon()
              .setUri(fileUri)
              .setUriPositionOffset(filePositionOffset)
              .setPosition(positionInFile)
              .setLength(length)
              .build();
      nextDataSource = cacheReadDataSource;//FileDataSource
    } else {
      // 数据没有缓存,且没有阻塞,说明当前是第一次获取这块数据,且没有其他现场正在获取
      //这个时候打开TeeDataSource,使用其中的DataSink在数据读取时缓存到文件中
      long length;
      if (nextSpan.isOpenEnded()) {
        length = bytesRemaining;
      } else {
        length = nextSpan.length;
        if (bytesRemaining != C.LENGTH_UNSET) {
          length = min(length, bytesRemaining);
        }
      }
      nextDataSpec =
          requestDataSpec.buildUpon().setPosition(readPosition).setLength(length).build();
      if (cacheWriteDataSource != null) {
        nextDataSource = cacheWriteDataSource;//TeeDataSource
      } else {
        nextDataSource = upstreamDataSource;
        cache.releaseHoleSpan(nextSpan);
        nextSpan = null;
      }
    }

    //如果上面的某些情况导致了一开始一直忽略缓存直接从上游获取数据,
    //这个时候在读取超过MIN_READ_BEFORE_CHECKING_CACHE(100kb)的数据时,会再次调用openNextSource
    //检测此时的缓存是否可用,是否需要切换数据源
    checkCachePosition =
        !currentRequestIgnoresCache && nextDataSource == upstreamDataSource
            ? readPosition + MIN_READ_BEFORE_CHECKING_CACHE
            : Long.MAX_VALUE;
    if (checkCache) {
      //确保上次的DataSource为upstreamDataSource
      Assertions.checkState(isBypassingCache());
      if (nextDataSource == upstreamDataSource) {
        // 如果本次获取到的还是upstreamDataSource,继续使用原来的upstreamDataSource,不做任何操作直接返回
        return;
      }
      //到这里说明本次获取的数据源已经不是upstreamDataSource,需要关闭当前的数据源,切换到本次新获取的数据源
      // 关闭上一个源,准备打开下一个源
      try {
        closeCurrentSource();
      } catch (Throwable e) {
        if (castNonNull(nextSpan).isHoleSpan()) {
          // 异常后一定要释放锁定的数据段
          cache.releaseHoleSpan(nextSpan);
        }
        throw e;
      }
    }

    if (nextSpan != null && nextSpan.isHoleSpan()) {
      //当前为待写入数据,这里记录,在写入完成需要cache.releaseHoleSpan
      currentHoleSpan = nextSpan;
    }
    currentDataSource = nextDataSource;//更新当前数据源
    currentDataSpec = nextDataSpec;
    currentDataSourceBytesRead = 0;
    //调用上面获取的DataSource打开数据源
    long resolvedLength = nextDataSource.open(nextDataSpec);

    // 更新数据的总长度和真实的URI到数据源的Metadata里,最终会将Metadata记录到索引文件中
    ContentMetadataMutations mutations = new ContentMetadataMutations();
    if (nextDataSpec.length == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) {
      bytesRemaining = resolvedLength;
      ContentMetadataMutations.setContentLength(mutations, readPosition + bytesRemaining);
    }
    if (isReadingFromUpstream()) {
      actualUri = nextDataSource.getUri();
      boolean isRedirected = !requestDataSpec.uri.equals(actualUri);
      ContentMetadataMutations.setRedirectedUri(mutations, isRedirected ? actualUri : null);
    }
    if (isWritingToCache()) {//当前使用的是TeeDataSource,说明需要写入缓存
      cache.applyContentMetadataMutations(key, mutations);
    }
  }
  private void closeCurrentSource() throws IOException {
    if (currentDataSource == null) {
      return;
    }
    try {
    //关闭当前数据源
      currentDataSource.close();
    } finally {
      currentDataSpec = null;
      currentDataSource = null;
      if (currentHoleSpan != null) {
      //释放HoleSpan
        cache.releaseHoleSpan(currentHoleSpan);
        currentHoleSpan = null;
      }
    }
  }
  
  @Override
  //读取数据
  public int read(byte[] buffer, int offset, int length) throws IOException {
    if (length == 0) {
      return 0;
    }
    if (bytesRemaining == 0) {
      return C.RESULT_END_OF_INPUT;
    }
    DataSpec requestDataSpec = checkNotNull(this.requestDataSpec);
    DataSpec currentDataSpec = checkNotNull(this.currentDataSpec);
    try {
      if (readPosition >= checkCachePosition) {//判断当前是否需要切换数据源
        openNextSource(requestDataSpec, true);
      }
      //使用当前的DataSource读取数据
      int bytesRead = checkNotNull(currentDataSource).read(buffer, offset, length);
      if (bytesRead != C.RESULT_END_OF_INPUT) {//没有结束
        if (isReadingFromCache()) {
          totalCachedBytesRead += bytesRead;
        }
        readPosition += bytesRead;
        currentDataSourceBytesRead += bytesRead;
        if (bytesRemaining != C.LENGTH_UNSET) {
          bytesRemaining -= bytesRead;
        }
      } else if (isReadingFromUpstream()
          && (currentDataSpec.length == C.LENGTH_UNSET
              || currentDataSourceBytesRead < currentDataSpec.length)) {
        // 已经冲上游数据源读取到了结束位置,而currentDataSpec并没有限制长度,这个时候的readPosition就相当于结束位置,将它作为长度更新到资源的Metadata中
        setNoBytesRemainingAndMaybeStoreLength(castNonNull(requestDataSpec.key));
      } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
        //虽然读取到结束位置,但还有剩余,如多个缓存文件的情况,尝试再次打开数据源读取下个缓存的文件
        closeCurrentSource();
        openNextSource(requestDataSpec, false);
        return read(buffer, offset, length);
      }
      return bytesRead;
    } catch (Throwable e) {
      handleBeforeThrow(e);
      throw e;
    }
  }

CacheDataSource可以处理3种状态的数据打开读取:

  1. 正在缓存 当前资源无需缓存,或者当前资源正在被其他线程缓存时,此时即使调用了startReadWriteNonBlocking会返回null,表明当前资源未缓存完成,且被锁定。此时播放器为了保证正常播放会直接使用upstreamDataSource 来获取数据,但是每当获取了100kb的数据时就会,再次去startReadWriteNonBlocking判断当前部分资源是否已经缓存,如果获取到已缓存或者未缓存,此时会切换数据源,使用cacheWriteDataSource 或者cacheReadDataSource来获取数据。同时不再判读是否切换数据源。

  2. 未缓存 当前资源没有缓存,此时调用startReadWriteNonBlocking返回一个HoleSpan,同时还会锁定这段数据,方式其他线程缓存。此时播放器会使用cacheWriteDataSource 也就是TeeDataSource获取数据。打开TeeDataSource同时会使用CacheDataSink打开缓存的文件句柄用于后续的写入,CacheDataSink内部通过SimpleCache。startFile获取文件路径。当开始读取数据的时候,TeeDataSource会将upstreamDataSource 中读取的数据先通过CacheDataSink写入到上面获取的文件里,注意如果CacheDataSink设置了分段存储,这个过程会多次startFile,commitFile,将整个资源分成多个缓存文件保存。最后关闭源时CacheDataSink会调用SimpleCache commitFile,创建一个对应于缓存的文件CacheSpan,首先存入内存的CachedContentIndex,最后将内存的CachedContentIndex同步到文件系统的索引文件。

  3. 已缓存 当前资源已经缓存,此时调用startReadWriteNonBlocking返回一个已缓存的CacheSpan,isCached=true。CacheSpan中包含了缓存文件的路径,这种情况直接使用cacheReadDataSource 也就是FileDataSource去获取缓存文件的数据。

总结

到这里整个DataSource部分就全部结束了,下一篇计划通过ProgressiveMediaPeriod,把SampleQueue,Loder和DataSource贯穿,面向整体讲下他们之间如何协调运作的。


版权声明 ©

本文为作者山雨楼原创文章

转载请注明出处

原创不易,觉得有用的话,收藏转发点赞支持

相关推荐
无极程序员39 分钟前
PHP常量
android·ide·android studio
58沈剑1 小时前
80后聊架构:架构设计中两个重要指标,延时与吞吐量(Latency vs Throughput) | 架构师之路...
架构
萌面小侠Plus2 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机
慢慢成长的码农2 小时前
Android Profiler 内存分析
android
大风起兮云飞扬丶2 小时前
Android——多线程、线程通信、handler机制
android
L72562 小时前
Android的Handler
android
清风徐来辽2 小时前
Android HandlerThread 基础
android
HerayChen3 小时前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野3 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing11233 小时前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机