Android频道播放系统架构设计文档
1. 系统架构全景图
1.1 模块化分层架构
┌───────────────────────────────────────┐
│ app模块 │
│ • 应用入口点 │
│ • 全局导航控制 │
├───────────────────────────────────────┤
│ feature-player模块 │
│ • 播放界面(Compose) │
│ • 播放控制逻辑 │
├───────────────────────────────────────┤
│ feature-menu模块 │
│ • 动态菜单系统 │
│ • 用户偏好界面 │
├───────────────────────────────────────┤
│ core-ui模块 │
│ • 共享Compose组件 │
│ • 主题/样式系统 │
├───────────────────────────────────────┤
│ core-data模块 │
│ • 数据仓库(Repository) │
│ • 本地持久化(Room/DataStore) │
├───────────────────────────────────────┤
│ core-player模块 │
│ • ExoPlayer扩展 │
│ • WebView播放器封装 │
└───────────────────────────────────────┘
1.2 技术栈矩阵
技术领域 | 核心技术组件 |
---|---|
UI框架 | Jetpack Compose, Material3, Leanback (TV) |
异步处理 | Kotlin Coroutines, Flow, Channel |
依赖注入 | Hilt |
本地存储 | Room, DataStore, SharedPreferences |
网络 | Retrofit, OkHttp, Glide |
播放引擎 | ExoPlayer, Android WebView, Media3 |
调试工具 | LeakCanary, Chucker, Timber |
测试 | JUnit5, MockK, Espresso, Compose Testing |
2. UI层深度设计
2.1 播放界面组件树(Compose实现)
@Composable
fun PlayerScreen(
viewModel: PlayerViewModel = hiltViewModel()
) {
val playerState by viewModel.playerState.collectAsState()
Box(modifier = Modifier.fillMaxSize()) {
// 视频渲染层
VideoSurface(
modifier = Modifier.fillMaxSize(),
player = viewModel.player
)
// 叠加控制层
AnimatedVisibility(
visible = playerState.controlsVisible,
enter = fadeIn(),
exit = fadeOut()
) {
PlayerControls(
state = playerState,
onPlayPause = { viewModel.togglePlayPause() },
onSeek = { viewModel.seekTo(it) }
)
}
// 悬浮菜单按钮
FloatingMenuButton(
onClick = { viewModel.toggleMenu() }
)
}
}
2.2 多端UI适配方案
移动端菜单实现:
@Composable
fun MobileMenu(
menuState: MenuState,
onAction: (MenuAction) -> Unit
) {
ModalBottomSheet(
onDismissRequest = { onAction(MenuAction.Dismiss) }
) {
LazyColumn {
items(menuState.items) { item ->
when (item) {
is MenuItem.Expandable -> ExpandableMenuItem(item, onAction)
is MenuItem.Toggle -> ToggleMenuItem(item, onAction)
is MenuItem.Selectable -> SelectableMenuItem(item, onAction)
}
}
}
}
}
TV端菜单实现:
@Composable
fun TvMenu(
menuState: MenuState,
modifier: Modifier = Modifier
) {
val focusRequester = remember { FocusRequester() }
Box(modifier = modifier.background(MaterialTheme.colorScheme.surface)) {
LazyRow(
modifier = Modifier.focusRequester(focusRequester),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(menuState.items) { item ->
TvMenuItem(
item = item,
modifier = Modifier
.focusable()
.onFocusChanged { focus ->
if (focus.isFocused) {
viewModel.onMenuItemFocused(item.id)
}
}
)
}
}
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
3. 业务逻辑层精要设计
3.1 状态管理核心
class PlayerViewModel @Inject constructor(
private val playerAdapter: PlayerAdapter,
private val prefRepository: PreferenceRepository
) : ViewModel() {
private val _playerState = MutableStateFlow(PlayerState())
val playerState: StateFlow<PlayerState> = _playerState.asStateFlow()
init {
observePlayerEvents()
}
private fun observePlayerEvents() {
playerAdapter.addListener(object : PlayerListener {
override fun onPositionChanged(position: Long) {
_playerState.update { it.copy(currentPosition = position) }
}
override fun onPlaybackStateChanged(playing: Boolean) {
_playerState.update { it.copy(isPlaying = playing) }
}
})
}
fun togglePlayPause() {
if (playerState.value.isPlaying) {
playerAdapter.pause()
} else {
playerAdapter.play()
}
}
}
3.2 依赖注入配置
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun providePlayerAdapter(
context: Context,
@ApplicationScope scope: CoroutineScope
): PlayerAdapter {
return ExoPlayerAdapter(context, scope)
}
@Provides
@Singleton
fun provideChannelRepository(
localDataSource: ChannelLocalDataSource,
remoteDataSource: ChannelRemoteDataSource
): ChannelRepository {
return ChannelRepositoryImpl(localDataSource, remoteDataSource)
}
}
4. 数据层完整实现
4.1 数据流架构
graph TD
A[UI层] -->|观察| B(ViewModel)
B -->|调用| C[Repository]
C -->|本地数据| D[(Room数据库)]
C -->|远程数据| E[API Service]
D -->|缓存策略| F[DataStore]
E -->|网络请求| G[OkHttp]
4.2 数据库实体与DAO
@Entity(tableName = "channels")
data class ChannelEntity(
@PrimaryKey val id: String,
val name: String,
val url: String,
val type: String,
val lastPlayed: Long = 0,
val isFavorite: Boolean = false
)
@Dao
interface ChannelDao {
@Query("SELECT * FROM channels WHERE isFavorite = 1 ORDER BY lastPlayed DESC")
fun observeFavorites(): Flow<List<ChannelEntity>>
@Upsert
suspend fun upsert(channel: ChannelEntity)
@Query("UPDATE channels SET lastPlayed = :timestamp WHERE id = :channelId")
suspend fun updateLastPlayed(channelId: String, timestamp: Long)
}
4.3 仓库模式实现
class ChannelRepositoryImpl @Inject constructor(
private val local: ChannelLocalDataSource,
private val remote: ChannelRemoteDataSource
) : ChannelRepository {
override fun getFeaturedChannels(): Flow<List<Channel>> {
return local.getCachedChannels().combine(
remote.fetchFeaturedChannels()
) { cached, remote ->
if (cached.isEmpty()) remote else cached
}.onEach { channels ->
local.cacheChannels(channels)
}
}
}
5. 播放引擎层终极设计
5.1 ExoPlayer高级配置
class ExoPlayerAdapter @Inject constructor(
context: Context,
@ApplicationScope private val scope: CoroutineScope
) : PlayerAdapter {
private val player: ExoPlayer by lazy {
ExoPlayer.Builder(context)
.setTrackSelector(DefaultTrackSelector(context).apply {
parameters = buildUponParameters()
.setMaxVideoSizeSd()
.setPreferredAudioLanguage("zh")
.build()
})
.setLoadControl(DefaultLoadControl.Builder()
.setBufferDurationsMs(
MIN_BUFFER_MS,
MAX_BUFFER_MS,
PLAYBACK_BUFFER_MS,
REBUFFER_BUFFER_MS
)
.build())
.build()
}
private val bandwidthMeter = DefaultBandwidthMeter.Builder(context).build()
override fun loadMedia(url: String) {
val mediaItem = MediaItem.fromUri(url).buildUpon()
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()
player.setMediaItem(mediaItem)
player.prepare()
}
}
5.2 WebView播放器封装
class WebPlayerAdapter @Inject constructor(
context: Context
) : PlayerAdapter {
private val webView: WebView = WebView(context).apply {
settings.apply {
javaScriptEnabled = true
mediaPlaybackRequiresUserGesture = false
domStorageEnabled = true
}
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
injectPlayerScripts()
}
}
}
private fun injectPlayerScripts() {
val autoPlayScript = """
document.querySelector('video')?.play()
document.querySelector('video')?.requestFullscreen()
""".trimIndent()
webView.evaluateJavascript(autoPlayScript, null)
}
}
6. 性能优化全方案
6.1 内存优化策略
场景 | 优化措施 |
---|---|
后台播放 | 启用音频焦点管理,释放视频解码器 |
低内存设备 | 降低默认分辨率(480P),减少同时缓存的项目数 |
WebView实例 | 使用对象池限制最大实例数(3个),超出时销毁最久未使用的实例 |
图片加载 | 使用Glide with downsampling,启用硬件位图 |
播放器切换 | 预初始化备用播放器,但保持暂停状态 |
6.2 播放预加载机制
class PreloadManager @Inject constructor(
private val playerPool: PlayerPool,
private val repo: ChannelRepository
) {
private val preloadScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun schedulePreload(currentChannelId: String) {
preloadScope.launch {
val relatedChannels = repo.getRelatedChannels(currentChannelId)
playerPool.preparePlayers(relatedChannels.take(2).map { it.url })
}
}
fun cancelAll() {
preloadScope.coroutineContext.cancelChildren()
}
}
7. 安全与隐私保障
7.1 安全配置矩阵
安全领域 | 实施措施 |
---|---|
网络通信 | 强制HTTPS,证书锁定(Certificate Pinning) |
数据存储 | 敏感信息使用AndroidKeyStore加密 |
WebView安全 | 禁用文件访问,设置安全CSP策略 |
DRM支持 | Widevine Level1(支持硬件级内容保护) |
用户隐私 | 遵循GDPR,提供数据清除选项 |
7.2 安全代码示例
// 网络通信安全配置
fun createOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.sslSocketFactory(createSSLSocketFactory(), trustManager)
.hostnameVerifier { hostname, session ->
hostname == "api.trusted.com"
}
.addInterceptor(ChuckerInterceptor(context))
.build()
}
// WebView安全配置
private fun setupWebViewSecurity(webView: WebView) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
webView.settings.allowFileAccess = false
}
WebSettings.setWebContentsDebuggingEnabled(false)
webView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
}
8. 测试体系完整方案
8.1 单元测试套件
class PlayerViewModelTest {
@Test
fun `togglePlayPause should invert play state`() = runTest {
// Given
val mockPlayer = mockk<PlayerAdapter>()
val viewModel = PlayerViewModel(mockPlayer, mockk())
// When
viewModel.togglePlayPause()
// Then
verify { mockPlayer.play() }
}
}
8.2 UI测试用例
@HiltAndroidTest
class PlayerScreenTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Test
fun shouldShowControlsWhenVideoPlaying() {
composeTestRule.setContent {
PlayerScreen(viewModel = FakePlayerViewModel())
}
composeTestRule.onNodeWithTag("video_surface")
.performClick()
composeTestRule.onNodeWithTag("play_button")
.assertIsDisplayed()
}
}
9. 持续交付流水线
9.1 多环境配置
flavorDimensions "environment"
productFlavors {
dev {
dimension "environment"
applicationIdSuffix ".dev"
resValue "string", "app_name", "Player Dev"
}
staging {
dimension "environment"
applicationIdSuffix ".staging"
resValue "string", "app_name", "Player Staging"
}
prod {
dimension "environment"
resValue "string", "app_name", "Player"
}
}
9.2 CI/CD流程
name: CI Pipeline
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: ./gradlew assembleDebug lintDebug
- run: ./gradlew testDebugUnitTest
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: ./gradlew assembleProdRelease
- uses: fastlane/github-action@v1
with:
lane: 'deploy_to_firebase'
10. 扩展性设计
10.1 插件系统架构
interface PlayerPlugin {
fun onAttach(player: PlayerAdapter)
fun onDetach()
fun handleEvent(event: PlayerEvent): Boolean
}
class PlayerPluginManager @Inject constructor() {
private val plugins = mutableListOf<PlayerPlugin>()
fun registerPlugin(plugin: PlayerPlugin) {
plugins.add(plugin)
}
fun dispatchEvent(event: PlayerEvent) {
plugins.forEach { it.handleEvent(event) }
}
}
10.2 动态功能模块
dynamicFeatures = [
':features:downloads',
':features:cast',
':features:comments'
]
本终极版设计文档提供了从宏观架构到代码级别的完整指导方案,具有以下核心优势:
- 模块化设计:物理模块与逻辑分层完美对应,支持团队并行开发
- 现代化技术栈:全面采用Jetpack组件和Kotlin协程等现代Android开发技术
- 极致性能优化:从内存管理到播放预加载的全方位性能考量
- 企业级安全:涵盖网络安全、数据存储安全和内容保护的多层防护
- 完备的质量保障:包含单元测试、UI测试和CI/CD的完整质量体系
- 未来可扩展性:插件系统和动态功能模块设计确保长期可维护性
建议开发团队根据实际业务需求进行适当裁剪,并在实施过程中持续优化各模块实现细节。