Android Media3 ExoPlayer 使用和源码解析

2,418 阅读11分钟

开始

简介

首先介绍一下今天的主角,Media3 ExoPlayer 是 Google 提供的一个 Android 媒体播放器组件,支持视频和音频文件的处理。并且提供了对各种媒体格式的支持,包括 MP4、MP3、WebM、M4A、MPEG-TS 和 AAC 等。

比较

在介绍 ExoPlayer 之前,我们先把老朋友 MediaPalyer 请出来对比一下。相对于 ExoPlayer ,开发者更为熟知的怕是 MediaPalyer,MediaPalyer 作为 Google 内置的媒体播放组件,API 简单且容易上手。对于 ExoPlayer 和 MediaPalyer 在项目中如何进行选择,不妨先看看各自的优缺点。

ExoPlayer

ExoPlayer 优点

  1. 自适应流支持:ExoPlayer 支持动态自适应流技术,如 DASH(Dynamic Adaptive Streaming over HTTP)和 SmoothStreaming,这意味着它能够根据网络状况自动调整视频质量,提供更流畅的观看体验;
  2. 高兼容性:除了支持常见的视频和音频格式(如 MP4, MP3, WebM, M4A, MPEG-TS, AAC 等),ExoPlayer 还额外支持 DASH 和 HLS(HTTP Live Streaming)等流媒体协议;
  3. 支持更新:ExoPlayer 可以跟随应用统一升级,避免因系统版本不同而导致的兼容性问题。

ExoPlayer 缺点

  1. 视频硬解依赖:ExoPlayer 主要依赖硬件解码,可能导致在某些设备上因硬件不支持特定编码格式而无法播放视频;
  2. 资源消耗和效率:由于数据流的请求和内存缓存在 Java 层实现,可能会受到虚拟机限制,影响内存效率和性能;
  3. 架构设计复杂:ExoPlayer 的架构相对复杂,不易上手。

MediaPlayer

MediaPlayer 优点

  1. 系统内置:MediaPlayer 是 Android 系统内置的播放器,无需配置依赖;
  2. 上手简单:MediaPlayer 的 API 相对简单,容易上手。

MediaPlayer 缺点

  1. 功能简单:相比于 ExoPlayer,MediaPlayer 支持的格式和协议较少,特别是不支持 DASH 和 SmoothStreaming 等自适应流技术;
  2. 版本更新依赖系统:MediaPlayer 作为系统组件,其功能和 bug 修复依赖于系统更新,开发者无法独立控制其版本,可能导致在旧系统上遇到已知问题。

所以,综合上述优缺点来讲。播放本地音视频时,推荐使用 MediaPlayer,其他业务场景推荐使用 ExoPlayer。

使用

接下来会用一个简单的 demo 进行示例,教大家如何快速集成和使用 ExoPlayer。

API介绍

在使用之前,先给大家罗列一下 ExoPlayer 常用的 API。

方法描述
prepare()准备播放器开始播放,是异步方法,播放器在准备完成后会改变其状态
setMediaItem(MediaItem mediaItem))设置播放器的媒体资源,需要穿入一个MediaItem对象,这个对象包含了媒体的URI和其他的元数据
play()开始或者恢复播放,如果播放器还没有准备好,调用这个方法会使得播放器在准备完成后自动开始播放
pause()暂停播放
isPlaying()检查播放器是否正在播放
getCurrentPosition()获取播放器当前的播放位置,单位是毫秒
getDuration()获取当前媒体的总时长,单位是毫秒
seekTo(long positionMs)播放器的播放位置跳转到指定的位置
addListener(Player.Listener listener)添加一个监听,可以监听接收播放器的各种事件,例如播放状态的改变,播放错误等
setVideoSurfaceHolder()设置播放器的视频输出的SurfaceHolder

集成 ExoPlayer API

接下来在 build.gradle 中集成依赖库,其中 DASH 播放支持和 UI 组件库按需进行集成即可。

implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.exoplayer.dash)
implementation(libs.androidx.media3.ui)

如果尚未启用 Java 8,这里需要开启 Java 8 支持。

compileOptions {
  targetCompatibility JavaVersion.VERSION_1_8
}

创建 ExoPlayer 播放器

第一步,创建 ExoPlayer 实例。

val exoPlayer = ExoPlayer.Builder(this).build()

第二步,设置媒体资源,这里使用本地视频资源。

val mediaItem = MediaItem.Builder()
    .setUri("/storage/emulated/0/Movies/VID_20240522_064453.mp4")
    .build()
exoPlayer.setMediaItem(mediaItem)

第三步,预加载视频资源。

exoPlayer.prepare()

第四步,绑定 SurfaceHolder,这里使用的是 SurfaceView。

surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
    override fun surfaceCreated(holder: SurfaceHolder) {
        exoPlayer.setVideoSurfaceHolder(holder)
    }
    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
    }
    override fun surfaceDestroyed(holder: SurfaceHolder) {
        exoPlayer.clearVideoSurface()
    }
})

第五步,开始播放媒体资源。

exoPlayer.play()

ExoPlayer 播放视频

播放视频的效果如下,视频有点 low ,请大家忽略。

11.gif

源码剖析

通过上述 demo 让大家对 ExoPlayer 的有个简单和初步的了解,代码还是比较简单。接下来我会对 ExoPlayer 常用方法的源码实现进行分析,从中抽丝剥茧,内容比较多请大家跟着我的思路一起分析下去。

ExoPlayer build()

首先来看 build() 方法,我们通过建造者模式创建出 ExoPlayer,拿到 ExoPlayer 实例进行播放相关处理,下面看一下内部是如何实现的。

androidx.media3.exoplayer.ExoPlayer

ExoPlayer 类是一个接口,定义播放相关的方法。

public interface ExoPlayer extends Player {
    ... 
    
    public ExoPlayer build() {
      checkState(!buildCalled);
      buildCalled = true;
      // 这里通过ExoPlayerImpl来实现
      return new ExoPlayerImpl(/* builder= */ this, /* wrappingPlayer= */ null);
    }
    
    ...
}

androidx.media3.exoplayer.ExoPlayerImpl

ExoPlayerImpl 类是 ExoPlayer 接口的默认实现。它是 ExoPlayer 库的核心类,负责管理和协调各种组件(如渲染器、媒体源、轨道选择器等)以实现媒体播放。

public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer) {
  ...
    // 渲染器用来处理媒体数据,包括视频和音频
    renderers =
          builder
              .renderersFactorySupplier
              .get()
              .createRenderers(...);
  ...
  // ExoPlayerImpl的内部类,封装一部分基础业务
  internalPlayer =
          new ExoPlayerImplInternal(
              renderers,
              trackSelector,
              emptyTrackSelectorResult,
              builder.loadControlSupplier.get(),
              bandwidthMeter,
              repeatMode,
              shuffleModeEnabled,
              analyticsCollector,
              seekParameters,
              builder.livePlaybackSpeedControl,
              builder.releaseTimeoutMs,
              pauseAtEndOfMediaItems,
              applicationLooper,
              clock,
              playbackInfoUpdateListener,
              playerId,
              builder.playbackLooper);
  ...            
}

上述代码比较简单,不用做过多阐述。需要关注的是构造方法里的 renderers 和 internalPlayer,后面会详细进行介绍。

ExoPlayer setMediaItem()

setMediaItem() 是用来设置媒体资源,这个方法接收一个 MediaItem 对象作为参数。MediaItem 对象作为媒体数据载体,包含有媒体的 URI、媒体的元数据等。我们在创建 MediaItem 对象时,至少需要提供媒体的 URI。

androidx.media3.common.Player

Player 类定义播放相关功能,包括开启播放、暂停播放、播放到指定位置,这里我们继续看 play() 方法。

void setMediaItem(MediaItem mediaItem)

androidx.media3.exoplayer.ExoPlayer

void setMediaItem(MediaItem mediaItem);

androidx.media3.common.BasePlayer

BasePlayer 是一个抽象类,实现了 Player 接口,我们继续往下看。

@Override
public final void setMediaItem(MediaItem mediaItem) {
  // 这里将单条媒体数据放入集合中
  setMediaItems(ImmutableList.of(mediaItem));
}

@Override
public final void setMediaItems(List<MediaItem> mediaItems) {
  setMediaItems(mediaItems, /* resetPosition= */ true);
}
  

androidx.media3.exoplayer.ExoPlayerImpl

通过下面方法可知,ExoPlayer 同时支持设置多条媒体数据。

@Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
  // 这里进行线程安全校验
  verifyApplicationThread();
  setMediaSources(createMediaSources(mediaItems), resetPosition);
}


@Override
public void setMediaSources(List<MediaSource> mediaSources, boolean resetPosition) {
  verifyApplicationThread();
  // 继续调用方法进行传参
  setMediaSourcesInternal(
      mediaSources,
      /* startWindowIndex= */ C.INDEX_UNSET,
      /* startPositionMs= */ C.TIME_UNSET,
      /* resetToDefaultPosition= */ resetPosition);
}


private void setMediaSourcesInternal(
    List<MediaSource> mediaSources,
    int startWindowIndex,
    long startPositionMs,
    boolean resetToDefaultPosition) {
  ...
  // 据给定的时间线和期间位置信息,创建一个新的PlaybackInfo实例
  PlaybackInfo newPlaybackInfo =
      maskTimelineAndPosition(
          playbackInfo,
          timeline,
  ...
  // 继续设置播放器的媒体源列表
  internalPlayer.setMediaSources(
      holders, startWindowIndex, Util.msToUs(startPositionMs), shuffleOrder);
  ...
}

androidx.media3.exoplayer.ExoPlayerImplInternal

public void setMediaSources(
    List<MediaSourceList.MediaSourceHolder> mediaSources,
    int windowIndex,
    long positionUs,
    ShuffleOrder shuffleOrder) {
  // 发送Handler消息  
  handler
      .obtainMessage(
          MSG_SET_MEDIA_SOURCES,
          new MediaSourceListUpdateMessage(mediaSources, shuffleOrder, windowIndex, positionUs))
      .sendToTarget();
}


@Override
public boolean handleMessage(Message msg) {
  try {
    switch (msg.what) {
      // 接收Handler消息
      case MSG_SET_MEDIA_SOURCES:
        setMediaItemsInternal((MediaSourceListUpdateMessage) msg.obj);
        break;
        
        ...
        
}


private void setMediaItemsInternal(MediaSourceListUpdateMessage mediaSourceListUpdateMessage)
    throws ExoPlaybackException {
  playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
  if (mediaSourceListUpdateMessage.windowIndex != C.INDEX_UNSET) {
    // 刷新播放窗口索引和播放位置
    pendingInitialSeekPosition =
        new SeekPosition(
            new PlaylistTimeline(
                mediaSourceListUpdateMessage.mediaSourceHolders,
                mediaSourceListUpdateMessage.shuffleOrder),
            mediaSourceListUpdateMessage.windowIndex,
            mediaSourceListUpdateMessage.positionUs);
  }
  // 更新播放器的媒体资源列表
  Timeline timeline =
      mediaSourceList.setMediaSources(
          mediaSourceListUpdateMessage.mediaSourceHolders,
          mediaSourceListUpdateMessage.shuffleOrder);
  // 处理已更新的媒体资源列表        
  handleMediaSourceListInfoRefreshed(timeline, /* isSourceRefresh= */ false);
}

ExoPlayer prepare()

prepare() 方法在 ExoPlayer 中的作用是准备播放器开始播放媒体。当设置了媒体资源后,需要调用 prepare() 方法来初始化播放器和媒体数据。这个过程包括加载媒体数据、准备解码器等。

在调用 prepare() 方法后,ExoPlayer 会将其状态从空闲(IDLE)状态转变为缓冲(BUFFERING)状态。在缓冲状态,ExoPlayer 会尝试读取媒体数据并填充缓冲区。当缓冲区数据足够后,ExoPlayer 会将状态转变为准备好(READY)状态,此时可以开始播放媒体。

androidx.media3.common.Player

void prepare();

androidx.media3.exoplayer.ExoPlayerImpl

@Override
public void prepare() {
  // 这里进行线程安全校验
  verifyApplicationThread();
  boolean playWhenReady = getPlayWhenReady();
  @AudioFocusManager.PlayerCommand
  int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING);
  updatePlayWhenReady(
      playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand));
  // 当前播放状态不是 Player.STATE_IDLE 就会返回
  if (playbackInfo.playbackState != Player.STATE_IDLE) {
    return;
  }
  ...
  // 这里调用 prepare 方法,其实现类是 ExoPlayerImplInternal
  internalPlayer.prepare();
  ...
}

androidx.media3.exoplayer.ExoPlayerImplInternal

ExoPlayerImplInternal 是 ExoPlayer 的核心控制类,进一步封装和实现 ExoPlayer 的内部行为。主要是协调各个组件的工作,实现流畅的媒体播放。


public void prepare() {
  // 发送 Handler 消息 MSG_PREPARE 
  handler.obtainMessage(MSG_PREPARE).sendToTarget();
}


public boolean handleMessage(Message msg) {
  try {
    switch (msg.what) {
      case MSG_PREPARE:
        // 接收 Handler 消息进行处理
        prepareInternal();
        break;
        ...
}        
        
               
private void prepareInternal() {
  ...
  // 当播放准备完成时,这里发送 MSG_DO_SOME_WORK 消息开始进行资源的处理。
  handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}


public boolean handleMessage(Message msg) {
  try {
    switch (msg.what) {
      case MSG_DO_SOME_WORK:
        // 重要方法,后续会讲到
        doSomeWork();
        break;
      ...
}

ExoPlayer play()

play() 方法顾名思义是用开启播放或恢复播放,也是本文源码分析的重点,看看源码中是如何实现的。

androidx.media3.common.Player

void play();

androidx.media3.common.BasePlayer

@Override
public final void play() {
  // 设置准备状态为true,一切准备就绪
  setPlayWhenReady(true);
}

androidx.media3.common.Player

看了一圈,您猜怎么着,又回来了。Player 是一个接口类,我们继续往下看,寻找实现 setPlayWhenReady 方的类。

void setPlayWhenReady(boolean playWhenReady);

androidx.media3.exoplayer.ExoPlayerImpl

@Override
public void setPlayWhenReady(boolean playWhenReady) {
  // 这里进行线程安全校验
  verifyApplicationThread();
  // 根据播放器状态,放弃或请求音频焦点。
  @AudioFocusManager.PlayerCommand
  int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState());
  // 更新状态
  updatePlayWhenReady(
      playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand));
}
private void updatePlayWhenReady(
    boolean playWhenReady,
    @AudioFocusManager.PlayerCommand int playerCommand,
    @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
  playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY;
  @PlaybackSuppressionReason
  int playbackSuppressionReason = computePlaybackSuppressionReason(playWhenReady, playerCommand);
  // 当前状态已经是ready,则直接返回
  if (playbackInfo.playWhenReady == playWhenReady
      && playbackInfo.playbackSuppressionReason == playbackSuppressionReason) {
    return;
  }
  // 更新状态
  updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
      playWhenReady, playWhenReadyChangeReason, playbackSuppressionReason);
}
private void updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
    boolean playWhenReady,
    @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
    @PlaybackSuppressionReason int playbackSuppressionReason) {
  pendingOperationAcks++;
  
  // 在改变播放状态之前,需要对播放进度进行复制或则估算
  PlaybackInfo newPlaybackInfo =
      this.playbackInfo.sleepingForOffload
          ? this.playbackInfo.copyWithEstimatedPosition()
          : this.playbackInfo;
          
  // 复制并保存播放信息,其中包含有关播放是否应在准备就绪时继续的新信息。
  newPlaybackInfo =
      newPlaybackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason);
      
  // 发送 Handler 消息要准备播放了    
  internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason);
  updatePlaybackInfo(
      newPlaybackInfo,
      /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
      playWhenReadyChangeReason,
      /* positionDiscontinuity= */ false,
      /* ignored */ DISCONTINUITY_REASON_INTERNAL,
      /* ignored */ C.TIME_UNSET,
      /* ignored */ C.INDEX_UNSET,
      /* repeatCurrentMediaItem= */ false);
}

androidx.media3.exoplayer.ExoPlayerImplInternal

ExoPlayerImplInternal 类是 ExoPlayerImpl 的内部实现类,进一步封装和处理播放器的内部行为和状态管理。

public void setPlayWhenReady(
    boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) {
  // 发送Handler消息
  handler
      .obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, playbackSuppressionReason)
      .sendToTarget();
}


@Override
public boolean handleMessage(Message msg) {
  try {
    switch (msg.what) {
      ... 
      // 接收和处理准备播放的 Handler 消息
      case MSG_SET_PLAY_WHEN_READY:
        setPlayWhenReadyInternal(
            /* playWhenReady= */ msg.arg1 != 0,
            /* playbackSuppressionReason= */ msg.arg2,
            /* operationAck= */ true,
            Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
        break;
      ...
}


private void setPlayWhenReadyInternal(...)
    throws ExoPlaybackException {
  ...
  if (!shouldPlayWhenReady()) {
    // 未准备好播放,停止解码并更新当前播放进度
    stopRenderers();
    updatePlaybackPositions();
  } else {
    // 对当前播放状态进行校验
    if (playbackInfo.playbackState == Player.STATE_READY) { 
      // 更新 isRebuffering 值,该值是用来判断当前媒体播放是否正在重新缓冲中
      updateRebufferingState(
          /* isRebuffering= */ false, /* resetLastRebufferRealtimeMs= */ false);   
      // 开始播放计时    
      mediaClock.start();
      // 启动所有媒体渲染器
      startRenderers();
      // 发送执行渲染的 Handler 消息,最终调用 doSomeWork() 方法
      handler.sendEmptyMessage(MSG_DO_SOME_WORK);
    } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
      handler.sendEmptyMessage(MSG_DO_SOME_WORK);
    }
  }
}


private void startRenderers() throws ExoPlaybackException {
  ...
  for (int i = 0; i < renderers.length; i++) {
    if (trackSelectorResult.isRendererEnabled(i) && renderers[i].getState() == STATE_ENABLED) {
      // 启动媒体渲染器
      renderers[i].start();
    }
  }
}

接下来到本文的重头戏了,千呼万唤始出来!

private void doSomeWork() throws ExoPlaybackException, IOException {
  ...
  // 当前播放器状态已经完成准备
  if (playingPeriodHolder.prepared) {
    ...
    for (int i = 0; i < renderers.length; i++) {
      // 开始遍历渲染器集合
      Renderer renderer = renderers[i];
      if (!isRendererEnabled(renderer)) {
        continue;
      }
      // 开启渲染
      renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
      ...
    }
  } else {
    playingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
  }

  ...
}

androidx.media3.exoplayer.Renderer

Renderer 类是一个接口,定义了媒体渲染的功能方法作和生命周期。

public interface Renderer extends PlayerMessage.Target {

  /**
    * 逐步进行渲染
    
    * @param positionUs 当前媒体时间(以微秒为单位),在渲染循环的当前迭代开始时测量。
    * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} 以微秒为单位,
    * 在渲染循环当前迭代开始时测量。
    */
  void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException;

}

androidx.media3.exoplayer.mediacodec.MediaCodecRenderer

MediaCodecRenderer 是一个抽象类,使用 MediaCodec 来解码样本以进行渲染。这个类主要负责管理和协调媒体编解码器的生命周期,包括编解码器的初始化、配置、输入/输出缓冲区的处理、错误处理等。如果想要了解 MediaCodeC ,可以参考这篇文章

@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
  ...
  try {
    ...
    // 尝试进行进行初始化或者采用绕过解码模式
    maybeInitCodecOrBypass();
    if (bypassEnabled) {
      // 采用绕过解码模式
      TraceUtil.beginSection("bypassRender");
      while (bypassRender(positionUs, elapsedRealtimeUs)) {}
      TraceUtil.endSection();
    } else if (codec != null) {
      long renderStartTimeMs = getClock().elapsedRealtime();
      TraceUtil.beginSection("drainAndFeed");
      // 不断从解码器中取出数据,并进行处理
      while (drainOutputBuffer(positionUs, elapsedRealtimeUs)
          && shouldContinueRendering(renderStartTimeMs)) {}   
      // 将源数据交由解码器进行解码   
      while (feedInputBuffer() 
          && shouldContinueRendering(renderStartTimeMs)) {}
      TraceUtil.endSection();
    } else {
      ...
    }
    ...
  } catch (IllegalStateException e) {
    ...
  }
}

private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
    throws ExoPlaybackException {
  // 通过MediaCodecAdapter间接使用MediaCodeC
  MediaCodecAdapter codec = checkNotNull(this.codec);
  if (!hasOutputBuffer()) {
    int outputIndex;
    if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
      try {
        // 获取下一个可用的输出缓冲区索引
        outputIndex = codec.dequeueOutputBufferIndex(outputBufferInfo);
      } catch (IllegalStateException e) {
        processEndOfStream();
        if (outputStreamEnded) {
          // 发生异常时,及时释放MediaCodeC
          releaseCodec();
        }
        return false;
      }
    } else {
      outputIndex = codec.dequeueOutputBufferIndex(outputBufferInfo);
    }
  ...

  return false;
}


private boolean feedInputBuffer() throws ExoPlaybackException {
  ...
  MediaCodecAdapter codec = checkNotNull(this.codec);
  if (inputIndex < 0) {
    // 获取下一个可用的输入缓冲区索引
    inputIndex = codec.dequeueInputBufferIndex();
    // 没有可用的输入缓冲区
    if (inputIndex < 0) {
      return false;
    }
    // 获取缓冲区Buffer数据
    buffer.data = codec.getInputBuffer(inputIndex);
    buffer.clear();
  }
  ...

  @SampleStream.ReadDataResult int result;
  try {
    // 从当前buffer读取数据
    result = readSource(formatHolder, buffer, /* readFlags= */ 0);
  } catch (InsufficientCapacityException e) {
    ...
  }

  ...
  
  // 重置buffer状态
  resetInputBuffer();
  ...
  return true;
}

总结

未命名绘图.jpg

源码已经分析完了,相对来说还是比较枯燥和乏味的。不妨先看看 ExoPlayer 类图,通过类图熟悉类之间的关系,加深我们对源码的理解。

通过对 ExoPlayer 源码的分析,我们知道音视频的编解码是通过 MediaCodeC 来完成的。MediaCodec 是 Android 底层多媒体处理和编解码提供的一个 API,用于访问底层的多媒体硬件和软件编解码器,可以用于对音频和视频数据进行编码和解码。所以,了解和熟悉 MediaCodeC 对于音视频播放很重要。

最后,还是想说源码虽好,请勿贪杯。掌握源码其架构设计思想,才是看源码的初衷。由于内容比较多,有些地方或许存在纰漏,望大家不吝赐教!