尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

仿哔哩哔哩App开发:从Mock数据到ExoPlayer播放器

仿哔哩哔哩App开发:从Mock数据到ExoPlayer播放器 简介面向中级及以上Android开发者的Android仿哔哩哔哩B站源码工程目标是通过还原B站核心界面与功能帮助学习者掌握MVP架构、RecyclerView高性能列表、自定义View、Retrofit网络请求、Room数据库、Glide图片加载、EventBus通信、Dagger2依赖注入、LiveData与ViewModel生命周期管理、弹幕系统实现、RxJava响应式编程以及运行时权限处理等主流技术栈。压缩包大小约9.79MB源码目录结构清晰便于对照分析各模块。已有709人在CSDN学习下载适合希望从零搭建类似视频类App、深入理解大型项目架构设计与组件化拆分的开发者。通过学习该资源可直观看到各技术点在实际业务中的整合方式尤其弹幕渲染与列表性能优化部分具有较高参考价值。1. 仿哔哩哔哩源码的第一步先拆功能边界与数据面很多同学下载一套 Android 仿哔哩哔哩B站源码之后第一反应是打开 Android Studio 直接 build。结果八成报错报错集中在两个地方一是接口域名失效二是播放器初始化崩溃。跑不起来的原因往往不在 UI而在数据面和播放链路。另一件反直觉的事是这种仿站项目里真正值得花时间研究的不是哪个控件怎么摆放而是推荐流的数据编排、播放器的状态切换、以及没有后端的情况下怎么把 App 跑出真实效果。这篇博客按数据层、列表层、播放层的顺序拆目标是让你拿到的工程在一天内自己跑起来并且知道你改的每一处代码在仿站项目里意味着什么。适合已经有 Android 基础、想用完整项目练手或做毕业设计的开发者。2. 数据层三套接口加一个 Mock 拦截器让工程先跑起来2.1 先定功能边界首页、播放、评论三件事仿站项目最容易犯的错是功能越加越多最后把“我的”“直播”“番剧”“充电”全都堆进去。真实 B 站客户端的复杂度来自业务线而不是界面数量。做仿站只需要守住最小闭环首页推荐流 → 点击视频 → 播放页 → 看到评论区。这三件事覆盖了列表请求、视频寻址、评论分页三类高频网络场景做完之后你已经摸到这类 App 的骨架。模块划分常见做法是单 module 加包名分层不要一上来就拆多 module。App 规模没到几十人协作时多 module 只会拖慢编译。我一般按数据、列表、播放、通用四层分包data/网络接口、数据模型、Mock 数据ui/home/首页 Fragment、卡片类型、Adapterui/player/播放器 Activity、状态管理、进度条common/图片加载、工具类、BaseView2.2 三套核心接口的设计与参数仿站工程的数据层核心是三个接口推荐列表、播放地址、评论列表。接口路径和参数按 B 站公开接口的通用设计习惯来写重点放在参数语义上不要纠结于直连线上环境。线上 B 站接口普遍带 buvid、签名、wbi 鉴权字段这些不属于仿站学习范围抓包直连既不稳定也有合规风险。推荐列表接口interface BilibiliApi { GET(x/web-interface/wbi/index/top/feed/rcmd) suspend fun getRecommend( Query(ps) pageSize: Int 30, Query(fresh_type) freshType: Int 4, Query(platform) platform: String android, Query(mobi_app) mobiApp: String android ): RecommendResponse GET(x/player/playurl) suspend fun getPlayUrl( Query(bvid) bvid: String, Query(cid) cid: Long, Query(qn) qn: Int 64, Query(fnver) fnver: Int 0, Query(fnval) fnval: Int 4048 ): PlayUrlResponse GET(x/v2/reply) suspend fun getReplies( Query(type) type: Int 1, Query(oid) oid: Long, Query(pn) page: Int 1, Query(ps) pageSize: Int 20 ): ReplyResponse }接口语义说明ps控制每页条数推荐流一次 30 条足够超过 50 条滑动时内存压力会明显上升qn是清晰度档位16 对应 360P32 对应 480P64 对应 720P80 对应 1080P。播放页先请求 playurl 拿到视频流地址再交给播放器加载不要在首页把视频地址和列表数据一起返回流量和内存都扛不住。评论接口的oid是视频稿件 IDtype1表示视频评论这套参数语义在大部分 UGC 视频 App 里是通用的后面换别的项目也能迁移。2.3 没有后端也能跑OkHttp 拦截器做 Mock仿站工程跑不起来的最大原因是没有可用的后端数据。处理办法是给 OkHttp 加一个 Mock 拦截器按路径前缀返回本地 JSON这样不依赖网络也能完整走通列表和播放。class MockInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request chain.request() val path request.url.encodedPath val body when { path.contains(/recommend) - mockRecommendJson() path.contains(/playurl) - mockPlayUrlJson() path.contains(/reply) - mockReplyJson() else - return chain.proceed(request) } return Response.Builder() .request(request) .protocol(Protocol.HTTP_1_1) .code(200) .message(Mock) .body(body.toResponseBody(application/json; charsetutf-8.toMediaType())) .build() } }代码逻辑说明拦截器优先匹配 URL 路径命中后直接用本地 JSON 构造 Response 返回没命中的请求放行给真实网络。这意味着 Mock 数据和真实接口共用同一套数据模型切换环境时不用改 View 层代码。使用要点在 Retrofit 的 OkHttpClient 上通过addInterceptor注入 MockInterceptor。我一般用 BuildConfig 来控制开关buildFeatures { buildConfig true } defaultConfig { buildConfigField(boolean, MOCK_ENABLE, true) }Retrofit 构建时判断BuildConfig.MOCK_ENABLE为 true 就加 Mock 拦截器。这里有一个 Android Studio 里容易踩的坑AGP 8.0 之后buildConfig默认关闭不定申明buildFeatures.buildConfig true的话BuildConfig.MOCK_ENABLE编译直接报错。mock JSON 里视频地址用公开测试视频流不要用本地file://路径播放器对 Content URI 的支持在 7.0 之后有额外配置要求。3. 首页推荐流ItemViewType 多卡片与瀑布流的伸缩细节3.1 用 ItemViewType 组织卡片类型首页推荐流最常见的结构是双列瀑布流里面混着横图卡片、竖图卡片、带播放数的视频小卡和广告位。用 RecyclerView 实现时一个 Adapter 里用getItemViewType区分不同布局ViewHolder 分开写。enum class CardType { VIDEO_NORMAL, VIDEO_BANNER, AD_PLACEHOLDER } override fun getItemViewType(position: Int): Int { return when (items[position].type) { CardType.VIDEO_NORMAL - R.layout.item_video_normal CardType.VIDEO_BANNER - R.layout.item_video_banner CardType.AD_PLACEHOLDER - R.layout.item_ad_placeholder } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater LayoutInflater.from(parent.context) return when (viewType) { R.layout.item_video_normal - VideoNormalHolder(inflater.inflate(viewType, parent, false)) R.layout.item_video_banner - VideoBannerHolder(inflater.inflate(viewType, parent, false)) else - AdPlaceHolder(inflater.inflate(viewType, parent, false)) } }这里的核心点是viewType直接复用布局 ID省去一套类型映射表。很多仿站工程的 adapter 会写一个when把 type 值映射到布局 ID再写一个映射转回来纯属多余。类型多了以后维护两个映射关系很容易漏改。就我见过的项目里凡是 ViewType 出 bug 的多半是两个映射表不一致。3.2 瀑布流参数spanCount、gapStrategy、itemAnimator瀑布流布局用StaggeredGridLayoutManager实现但默认行为有三个地方需要调。val layoutManager StaggeredGridLayoutManager(2, VERTICAL).apply { gapStrategy StaggeredGridLayoutManager.GAP_HANDLING_NONE } recyclerView.layoutManager layoutManager recyclerView.itemAnimator null参数说明表格参数取值作用踩坑点spanCount2双列瀑布流竖屏主流设计平板或横屏建议改为 3否则卡片过宽gapStrategyGAP_HANDLING_NONE不自动调整第一列与第二列间隙默认值在加载更多时会把顶部已显示的卡片整体位移itemAnimatornull关闭默认动画瀑布流复用视图时 ImageView 尺寸变化会闪烁GAP_HANDLING_NONE不是永远正确。如果你的数据流里存在“刷新后整体替换”的操作用GAP_HANDLING_MOVE_TO_TOP_AND_CROP表现更好。但推荐流是增量加载用 NONE 最稳。3.3 点赞收藏的局部刷新DiffUtil 的 payload 用法瀑布流列表里频繁触发点赞、收藏、播放数 1 这类更新。如果整条数据notifyDataSetChanged()列表会闪烁并且所有卡片重新加载图片。正确做法是用DiffUtil的payload做精准刷新。override fun areItemsTheSame(oldItem: VideoCard, newItem: VideoCard): Boolean { return oldItem.bvid newItem.bvid } override fun areContentsTheSame(oldItem: VideoCard, newItem: VideoCard): Boolean { return oldItem.playCount newItem.playCount oldItem.likeCount newItem.likeCount oldItem.title newItem.title } override fun getChangePayload(oldItem: VideoCard, newItem: VideoCard): Any? { return if (oldItem.likeCount ! newItem.likeCount) like else null } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: ListAny) { if (payloads.contains(like)) { (holder as VideoNormalHolder).updateLikeCount(items[position].likeCount) return } super.onBindViewHolder(holder, position, payloads, payloads) }改动逻辑说明点赞回调触发后不要直接用notifyItemChanged(position)。先修改列表内存数据然后把变化交给DiffUtil计算getChangePayload只返回变化的最小字段onBindViewHolder收到 payload 后只更新对应 View。配合点赞的防重复点击请求失败时回滚数据再notifyItemChanged一次。3.4 分页加载提前缓存与 footer 状态首页推荐流的滑动体验瓶颈不在帧率而在用户滑到底部时才开始请求下一页。RecyclerView 有天然的预取机制但模拟的是 item 的 layout 预取不是网络预取。我一般用setExtraLayoutSpace做手动预加载recyclerView.setExtraLayoutSpace(屏幕高度 / 2)配合OnScrollListener判断最后一个可见 item 的位置当剩余 item 数量小于 5 时触发加载下一页。footer 状态用三个值LOADING、NO_MORE、ERROR分别对应加载中转圈、到底提示、失败重试。不要在onBindViewHolder里根据状态 inflate 布局footer 单独占一个 viewType避免列表滑动时反复创建布局。4. 视频播放链路ExoPlayer 参数、进度条与生命周期编排4.1 播放器选型为什么最后选 ExoPlayer仿站项目播放器选型基本是四个方向对比一下各自边界方案维护状态协议支持适用场景MediaPlayer系统自带HTTP 点播为主单视频播放无清晰度切换需求ExoPlayerGoogle 维护DASH、HLS、RTSP、SmoothStreaming仿站项目首选IjkPlayerB 站开源但已停止维护RTMP、HLS老视频项目新机型解码适配有坑阿里云播放器 / 腾讯播放器商用商业协议完整正式上线且预算充足的场景仿站工程我一般直接选 ExoPlayer也就是 Media3 官方库。IjkPlayer 虽然有 B 站背景但多年不更新Android 14 上硬件解码适配问题突出。ExoPlayer 对 Demo 视频流和 HLS 直播流的兼容性都够用能调的东西也更多后面要接清晰度、倍速、后台播放都有现成 API。4.2 ExoPlayer 初始化与 buffer 参数播放器初始化看起来简单真正影响体验的是LoadControl的缓冲策略。val loadControl DefaultLoadControl.Builder() .setBufferDurationsMs( 50_000L, // minBufferMs 50_000L, // maxBufferMs 2_500L, // bufferForPlaybackMs 5_000L // bufferForPlaybackAfterRebufferMs ) .build() exoPlayer ExoPlayer.Builder(context) .setLoadControl(loadControl) .build() playerView.player exoPlayer参数说明表格参数值作用minBufferMs50000缓冲至少 50 秒才开始播放网络抖动不易卡顿maxBufferMs50000最大缓冲 50 秒超过后停止继续下载bufferForPlaybackMs2500点播放后最多等 2.5 秒缓冲超时进入播放状态bufferForPlaybackAfterRebufferMs5000播放中卡顿后重新开始播放前再等 5 秒这几个参数直接影响弱网表现。minBufferMs调太大会导致用户点击视频后迟迟不出画面调太小则到网速波动时频繁 rebuffer。仿站项目不需要追求极端起播速度取上面的经验值即可。注意 Media3 的DefaultLoadControl.Builder和旧版com.google.android.exoplayer2构造方式几乎一致迁移成本很低。4.3 进度条与播放状态机Android 进度条的正确拖动方式仿 B 站视频详情页的 Android 进度条核心坑在拖动回调的触发频率。如果写在onProgressChanged里直接调player.seekTo()每帧都会触发 seek播放器会不断打断当前读取的数据块。binding.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { // 拖动中只做 UI 提示不触发 seek if (fromUser) { binding.currentTime.text formatDuration(progress.toLong()) } } override fun onStopTrackingTouch(seekBar: SeekBar?) { // 松手时才真正跳转到目标位置 exoPlayer?.seekTo(seekBar?.progress?.toLong() ?: 0L) } })这里的逻辑是onProgressChanged每毫秒都可能触发seekTo需要等待用户确认结束。进度条更新用播放器的setPlaybackParameters或 listener 回调驱动不要用Handler.postDelayed每秒轮询。如果是广告位视频或者卡片页自动播放的短视频进度条应改为不可拖动只读展示。4.4 生命周期绑定与释放播放器必须绑定 Activity 生命周期否则退出页面后声音还在播或者在屏幕旋转时崩溃。override fun onStart() { super.onStart() if (isVisible) exoPlayer?.play() } override fun onStop() { super.onStop() exoPlayer?.pause() } override fun onDestroy() { super.onDestroy() exoPlayer?.release() exoPlayer null }补充逻辑onPause里暂停播放onDestroy里必须release()并置空引用否则 Activity 被销毁后播放器线程仍然持有 SurfaceView导致内存泄漏。切换清晰度时调用setMediaItem(mediaItem, true)第二个参数resetPosition设为 true 会回到起点设 false 会保留当前进度。倍速播放直接setPlaybackSpeed(1.25f)恢复 1.0 倍时同样调用这个方法不需要重新 prepare。5. 交付前的冒烟检查用 Baseline Profile 和 LeakCanary 过一遍5.1 冷启动首帧的验证与优化仿站工程的首页往往包含大量图片和网络请求冷启动容易慢。用 Android Studio 自带的 Profiler 录制一次冷启动观察从点击应用图标到首帧绘制的时间。常见瓶颈有两个Application 初始化时同步创建了三方 SDK以及首页 RecyclerView 没等数据回来就直接暴露。优化首帧最有效的手段是 Baseline Profile。做法是在 Android Studio 中新建宏基准测试模块编写启动路径的遍历规则生成baseline-prof文件放到app/src/main/baselineProfiles目录下。这个文件让系统在安装阶段预编译热点代码首帧时间通常能快 15% 到 25%。注意每次大版本更新后重新生成一次 profile因为代码路径变化后旧 profile 的收益会下降。5.2 内存泄漏的三个高频检查点播放器相关页面跑一遍 LeakCanary关注三个位置ExoPlayer 的 release 是否在 onDestroy 中执行Adapter 里的回调是否注册到了外部对象图片库的请求是否在页面关闭时取消了。LeakCanary 3 以上版本不需要手动初始化只要在依赖里引入即可自动检测。debugImplementation com.squareup.leakcanary:leakcanary-android:2.14检查项工具通过标准冷启动首帧Android Studio Startup Profiler中端机 1.5 秒以内首页滑动帧率GPU 渲染模式或 Profiler掉帧率低于 5%播放页退出后内存LeakCanary无 Activity 泄漏瀑布流图片内存Android Studio Memory Profiler持续滑动时内存曲线平稳低端机验证时把 minSdk 调到 26 以下后反而要警惕的是动画开销GAP_HANDLING_NONE在这种机型上表现更明显。补上一点视频播放器在切到后台时如果要保持音频继续需要在 onStop 中判断是否开启了“后台播放”开关否则 onStop 一律 pause。本文还有配套的精品资源点击获取
返回列表