直播技术-Android基础框架

目录

(一)直播间架构

(二)核心任务调度机制

(1)复制从滑动直播间加载流程

(2)核心任务调度机制-代码设计

(3)核心任务调度机制-接入指南

(三)直播间数据和通道能力建设

(1)BiDataReporter-业务埋点能力

现状

目标

埋点能力建设

代码样例

(2)BiDataReporter-业务埋点规划

埋点通道规范

埋点数据规范

(3)DataStore - 数据隔离和访问能力

背景

现有数据来源

改造流程

(4)DataStore - 数据隔离和访问规范

DataStore概览

数据分层

基于核心任务调度机制的挂载点的业务,使用Task提供的数据集

(5)RxBus - Event通信能力

使用现状

解决方案

(6)RxBus - Event通信规范

事件定义

举例:

(四)动态层级管理器

(1)问题

(2)目标

(3)过程

(4)代码设计

新增分区

新增层级视图

(5)创建动态层级视图

对布局和控件的管理

实体view被加载的时机

定义view支持的屏幕模式和各屏幕的适配

LiveData的订阅方法

(五)直播间业务数据改造

直播间中的数据

1、数据的分类

1.1、环境数据

1.2、埋点数据

1.3、业务数据

2、直播间内数据的来源和流向

3、业务的启动和数据维护

DataStore的背景

数据真正的承载方

业务服务的改变

4、不同业务交互场景下的数据流向分析

业务场景和数据的关系

两个业务间的交互场景

两个业务以上的交互场景

(六)直播业务改造整理

LiveRoomActivityV3逻辑改造

业务分层

业务的组件化


(一)直播间架构

(二)核心任务调度机制

(1)复制从滑动直播间加载流程

(2)核心任务调度机制-代码设计

//直播间流程控制管理器
interface LiveRoomFlowManager {
@UiThread
fun registerTask(task: LiveRoomFlowTask) //注入任务@UiThreadfun dispatchTask(roomStatus: LiveRoomStatus) // 执行卡点开始事件fun destroy() //直播间销毁事件}//配置优先级管理中心
object LiveRoomFlowConfig {
const val LOW_PRIORITY = 1000L
const val DEFAULT_PRIORITY = 10000L
const val HIGH_PRIORITY = 1000000L
const val HIGHEST_PRIORITY = Long.MAX_VALUE//单个卡点优先级单独管理
object OnCreate {
const val INIT_PARAMS = HIGHEST_PRIORITY
const val INIT_IJK = HIGHEST_PRIORITY - 1000L
const val INIT_ITRACKER = HIGHEST_PRIORITY - 2000L
const val INIT_ROOM_VIEW = HIGHEST_PRIORITY - 3000L
const val INIT_VIEWMODEL = HIGHEST_PRIORITY - 4000L
const val INIT_PLAYERFRAGMENT = HIGHEST_PRIORITY - 5000L
const val INIT_CONFIGANDPCU = HIGHEST_PRIORITY - 6000L
}object OnResume {const val COMMON = HIGH_PRIORITYconst val HEARTBEAT = HIGH_PRIORITY - 1000L}object OnP0 {//viewModelconst val INIT_P1 = HIGH_PRIORITYconst val INIT_PLAYERDATA = HIGH_PRIORITY - 1000L//Viewconst val INIT_PLAY = HIGH_PRIORITY - 2000L}object OnP1 {const val PLAYER_VIEWMODEL = HIGH_PRIORITYconst val USER_VIEWMODEL = HIGH_PRIORITY - 1000Lconst val SOCKET_VIEWMODEL = HIGH_PRIORITY - 2000Lconst val INTERACTION_VIEWMODEL = HIGH_PRIORITY - 3000Lconst val GIFT_VIEWMODEL = HIGH_PRIORITY - 4000Lconst val BASIC_VIEWMODEL = HIGH_PRIORITY - 5000Lconst val SPPLAYER_VIEWMODEL = HIGH_PRIORITY - 6000Lconst val OPERATION_VIEWMODEL = HIGH_PRIORITY - 7000Lconst val ANIM_VIEWMODEL = HIGH_PRIORITY - 8000Lconst val SKIN_VIEWMODEL = HIGH_PRIORITY - 9000Lconst val OPERATION_VIEWMODELV3 = HIGH_PRIORITY - 10000Lconst val VOICE_VIEWMODEL = HIGH_PRIORITY - 11000Lconst val SUPERCHAT_VIEWMODEL = HIGH_PRIORITY - 12000Lconst val LPLSP_VIEWMODEL = HIGH_PRIORITY - 13000Lconst val INTERACTIONPANEL_VIEWMODEL = HIGH_PRIORITY - 14000Lconst val QUESTION_VIEWMODEL = HIGH_PRIORITY - 15000L//分发 其他Basic数据const val DISPATCH_BASIC_DATA = HIGH_PRIORITY - 16000Lconst val DISPATCH_ROOM_INFO_COMPLETE = HIGH_PRIORITY - 17000L}}//每个卡点可以挂载N个任务
sealed class LiveRoomFlowTask(val task: () -> Unit) {
abstract val tag: String //标签 别名
abstract val rule: LiveRoomFlowRule //规则 默认按优先级排序
abstract val liveRoomStatus: LiveRoomStatus // 挂载点
open val isSticky: Boolean = false //是否支持粘性消息
}//数据记录相关,记录相关数据 埋点上报
class LiveRoomFlowTrace(val hashCode: Int) : LiveLogger {override val logTag: Stringget() = "LiveRoomFlowTrace"private val initTs = SystemClock.elapsedRealtime()private val reportMap = LinkedHashMap<String, LiveRoomStatusAssembly>()init {logDebug { "init onCreate ms: $initTs hashCode: $hashCode" }}fun recordRoomStatusStart(liveRoomStatus: LiveRoomStatus, startTs: Long) {val assembly = reportMap[liveRoomStatus.tag]?: LiveRoomStatusAssembly(ArrayList(), liveRoomStatus).also {reportMap[liveRoomStatus.tag] = it}assembly.startTs = startTsassembly.tasks.clear()}fun recordTask(liveRoomFlowTask: LiveRoomFlowTask, costTs: Long, exception: String? = null) {val assembly = reportMap[liveRoomFlowTask.liveRoomStatus.tag]?: LiveRoomStatusAssembly(ArrayList(), liveRoomFlowTask.liveRoomStatus).also {reportMap[liveRoomFlowTask.liveRoomStatus.tag] = it}assembly.tasks.add(LiveRoomTaskCost(liveRoomFlowTask.tag, costTs, exception))}fun recordRoomStatusEnd(liveRoomStatus: LiveRoomStatus, endTs: Long) {val assembly = reportMap[liveRoomStatus.tag]?: LiveRoomStatusAssembly(ArrayList(), liveRoomStatus).also {reportMap[liveRoomStatus.tag] = it}assembly.endTs = endTsassembly.totalCostTs = assembly.endTs - assembly.startTs}fun logRoomStatus(roomStatus: LiveRoomStatus) {reportMap[roomStatus.tag]?.let {logDebug { JSON.toJSONString(it) }}}fun reportMap() {HandlerThreads.getHandler(HandlerThreads.THREAD_REPORT).post {LiveReporter.reportTech(EVENT_ID, generateReportInfo(), sampler = { true }, force = false)}}private fun generateReportInfo(): Map<String, String> {return HashMap<String, String>().apply {put("room_flow_info", JSON.toJSONString(reportMap))}}companion object {const val EVENT_ID = "live.room-flow.info.track"}}class LiveRoomStatusAssembly(val tasks: ArrayList, val liveRoomStatus: LiveRoomStatus, var totalCostTs: Long = 0L, var startTs: Long = 0, var endTs: Long = 0)data class LiveRoomTaskCost(val tag: String, val costTs: Long, val exception: String? = null)//管理器实现类,实现内部所有功能
class LiveRoomFlowManagerImpl(private val hashCode: Int) : LiveRoomFlowManager, LiveLogger {override val logTag: Stringget() = "LiveRoomFlowManager"private val mRoomTrace = LiveRoomFlowTrace(hashCode)private val mTasks = mutableMapOf<LiveRoomStatus, LinkedList<LiveRoomFlowTask>>()private val mComparator = Comparator<LiveRoomFlowTask> { o1, o2 -> o2.rule.priority.compareTo(o1.rule.priority) }private var mCurrentRoomStatus: Int = LiveRoomStatus.ON_NONE.priorityprivate var isDestroyCalled = falseoverride fun dispatchTask(roomStatus: LiveRoomStatus) {if (isDestroyCalled) returnmCurrentRoomStatus = mCurrentRoomStatus or roomStatus.prioritymRoomTrace.recordRoomStatusStart(roomStatus, SystemClock.elapsedRealtime())mTasks[roomStatus]?.forEach {if (isDestroyCalled) returnexecuteTask(it)}mRoomTrace.recordRoomStatusEnd(roomStatus, SystemClock.elapsedRealtime())mRoomTrace.logRoomStatus(roomStatus)}override fun registerTask(task: LiveRoomFlowTask) {if (isDestroyCalled) returnval queue = mTasks[task.liveRoomStatus] ?: LinkedList<LiveRoomFlowTask>().also {mTasks[task.liveRoomStatus] = it}if (queue.contains(task)) {return}setupRuleForTask(task, queue)queue.add(task)Collections.sort(queue, mComparator)if (task.isSticky && hasRoomStatus(task.liveRoomStatus)) {executeTask(task)}}private fun setupRuleForTask(liveRoomFlowTask: LiveRoomFlowTask, queue: LinkedList<LiveRoomFlowTask>) {val rule = liveRoomFlowTask.ruleif (rule.type == LiveRoomFlowRule.Type.PRIORITY) returnif (rule.type == LiveRoomFlowRule.Type.ALL_TOP) {liveRoomFlowTask.rule.priority = HIGHEST_PRIORITYreturn}val targetTask = getIndexByTag(rule.target, queue) ?: returnif (rule.type == LiveRoomFlowRule.Type.ABOVE) {liveRoomFlowTask.rule.priority = targetTask.rule.priority + 1}if (rule.type == LiveRoomFlowRule.Type.BELOW) {liveRoomFlowTask.rule.priority = targetTask.rule.priority - 1}}private fun getIndexByTag(target: String, queue: LinkedList<LiveRoomFlowTask>): LiveRoomFlowTask? {queue.forEachIndexed { _, liveRoomTask ->if (target == liveRoomTask.tag) {return liveRoomTask}}return null}private fun hasRoomStatus(liveRoomStatus: LiveRoomStatus) = liveRoomStatus.priority and mCurrentRoomStatus == liveRoomStatus.priorityprivate fun executeTask(flowTask: LiveRoomFlowTask) {val startTs = SystemClock.elapsedRealtime()try {flowTask.task()val endTs = SystemClock.elapsedRealtime()mRoomTrace.recordTask(flowTask, endTs - startTs)} catch (e: Exception) {val endTs = SystemClock.elapsedRealtime()mRoomTrace.recordTask(flowTask, endTs - startTs, e.message)}}override fun destroy() {isDestroyCalled = truemTasks.clear()mRoomTrace.reportMap()}}enum class LiveRoomStatus(val tag: String, val priority: Int) {
ON_NONE(“NONE”, ON_NONE_VAL),
ON_CREATE(“ON_CREATE”, ON_CREATE_VAL),
ON_RESUME(“ON_RESUME”, ON_RESUME_VAL),
ON_P0(“ON_P0”, ON_P0_VAL),
ON_P1(“ON_P1”, ON_P1_VAL)
}class LiveRoomFlowRule(val target: String = "", val type: Type = Type.PRIORITY) {var priority: Long = DEFAULT_PRIORITYinit {when (type) {Type.ABOVE, Type.BELOW, Type.PRIORITY -> UnitType.ALL_TOP -> priority = HIGHEST_PRIORITY}}enum class Type {ABOVE, BELOW, ALL_TOP, PRIORITY}companion object {const val ON_NONE_VAL = 0x000const val ON_CREATE_VAL = 0x001const val ON_RESUME_VAL = 0x002const val ON_P0_VAL = 0x004const val ON_P1_VAL = 0x008fun generatePriorityRule(priority: Long): LiveRoomFlowRule {return LiveRoomFlowRule().apply { this.priority = priority }}}}//直播间流程控制管理器
interface LiveRoomFlowManager {
@UiThread
fun registerTask(task: LiveRoomFlowTask) //注入任务@UiThreadfun dispatchTask(roomStatus: LiveRoomStatus) // 执行卡点开始事件fun destroy() //直播间销毁事件}//配置优先级管理中心
object LiveRoomFlowConfig {
const val LOW_PRIORITY = 1000L
const val DEFAULT_PRIORITY = 10000L
const val HIGH_PRIORITY = 1000000L
const val HIGHEST_PRIORITY = Long.MAX_VALUE//单个卡点优先级单独管理
object OnCreate {
const val INIT_PARAMS = HIGHEST_PRIORITY
const val INIT_IJK = HIGHEST_PRIORITY - 1000L
const val INIT_ITRACKER = HIGHEST_PRIORITY - 2000L
const val INIT_ROOM_VIEW = HIGHEST_PRIORITY - 3000L
const val INIT_VIEWMODEL = HIGHEST_PRIORITY - 4000L
const val INIT_PLAYERFRAGMENT = HIGHEST_PRIORITY - 5000L
const val INIT_CONFIGANDPCU = HIGHEST_PRIORITY - 6000L
}object OnResume {const val COMMON = HIGH_PRIORITYconst val HEARTBEAT = HIGH_PRIORITY - 1000L}object OnP0 {//viewModelconst val INIT_P1 = HIGH_PRIORITYconst val INIT_PLAYERDATA = HIGH_PRIORITY - 1000L//Viewconst val INIT_PLAY = HIGH_PRIORITY - 2000L}object OnP1 {const val PLAYER_VIEWMODEL = HIGH_PRIORITYconst val USER_VIEWMODEL = HIGH_PRIORITY - 1000Lconst val SOCKET_VIEWMODEL = HIGH_PRIORITY - 2000Lconst val INTERACTION_VIEWMODEL = HIGH_PRIORITY - 3000Lconst val GIFT_VIEWMODEL = HIGH_PRIORITY - 4000Lconst val BASIC_VIEWMODEL = HIGH_PRIORITY - 5000Lconst val SPPLAYER_VIEWMODEL = HIGH_PRIORITY - 6000Lconst val OPERATION_VIEWMODEL = HIGH_PRIORITY - 7000Lconst val ANIM_VIEWMODEL = HIGH_PRIORITY - 8000Lconst val SKIN_VIEWMODEL = HIGH_PRIORITY - 9000Lconst val OPERATION_VIEWMODELV3 = HIGH_PRIORITY - 10000Lconst val VOICE_VIEWMODEL = HIGH_PRIORITY - 11000Lconst val SUPERCHAT_VIEWMODEL = HIGH_PRIORITY - 12000Lconst val LPLSP_VIEWMODEL = HIGH_PRIORITY - 13000Lconst val INTERACTIONPANEL_VIEWMODEL = HIGH_PRIORITY - 14000Lconst val QUESTION_VIEWMODEL = HIGH_PRIORITY - 15000L//分发 其他Basic数据const val DISPATCH_BASIC_DATA = HIGH_PRIORITY - 16000Lconst val DISPATCH_ROOM_INFO_COMPLETE = HIGH_PRIORITY - 17000L}}//每个卡点可以挂载N个任务
sealed class LiveRoomFlowTask(val task: () -> Unit) {
abstract val tag: String //标签 别名
abstract val rule: LiveRoomFlowRule //规则 默认按优先级排序
abstract val liveRoomStatus: LiveRoomStatus // 挂载点
open val isSticky: Boolean = false //是否支持粘性消息
}//数据记录相关,记录相关数据 埋点上报
class LiveRoomFlowTrace(val hashCode: Int) : LiveLogger {override val logTag: Stringget() = "LiveRoomFlowTrace"private val initTs = SystemClock.elapsedRealtime()private val reportMap = LinkedHashMap<String, LiveRoomStatusAssembly>()init {logDebug { "init onCreate ms: $initTs hashCode: $hashCode" }}fun recordRoomStatusStart(liveRoomStatus: LiveRoomStatus, startTs: Long) {val assembly = reportMap[liveRoomStatus.tag]?: LiveRoomStatusAssembly(ArrayList(), liveRoomStatus).also {reportMap[liveRoomStatus.tag] = it}assembly.startTs = startTsassembly.tasks.clear()}fun recordTask(liveRoomFlowTask: LiveRoomFlowTask, costTs: Long, exception: String? = null) {val assembly = reportMap[liveRoomFlowTask.liveRoomStatus.tag]?: LiveRoomStatusAssembly(ArrayList(), liveRoomFlowTask.liveRoomStatus).also {reportMap[liveRoomFlowTask.liveRoomStatus.tag] = it}assembly.tasks.add(LiveRoomTaskCost(liveRoomFlowTask.tag, costTs, exception))}fun recordRoomStatusEnd(liveRoomStatus: LiveRoomStatus, endTs: Long) {val assembly = reportMap[liveRoomStatus.tag]?: LiveRoomStatusAssembly(ArrayList(), liveRoomStatus).also {reportMap[liveRoomStatus.tag] = it}assembly.endTs = endTsassembly.totalCostTs = assembly.endTs - assembly.startTs}fun logRoomStatus(roomStatus: LiveRoomStatus) {reportMap[roomStatus.tag]?.let {logDebug { JSON.toJSONString(it) }}}fun reportMap() {HandlerThreads.getHandler(HandlerThreads.THREAD_REPORT).post {LiveReporter.reportTech(EVENT_ID, generateReportInfo(), sampler = { true }, force = false)}}private fun generateReportInfo(): Map<String, String> {return HashMap<String, String>().apply {put("room_flow_info", JSON.toJSONString(reportMap))}}companion object {const val EVENT_ID = "live.room-flow.info.track"}}class LiveRoomStatusAssembly(val tasks: ArrayList, val liveRoomStatus: LiveRoomStatus, var totalCostTs: Long = 0L, var startTs: Long = 0, var endTs: Long = 0)data class LiveRoomTaskCost(val tag: String, val costTs: Long, val exception: String? = null)//管理器实现类,实现内部所有功能
class LiveRoomFlowManagerImpl(private val hashCode: Int) : LiveRoomFlowManager, LiveLogger {override val logTag: Stringget() = "LiveRoomFlowManager"private val mRoomTrace = LiveRoomFlowTrace(hashCode)private val mTasks = mutableMapOf<LiveRoomStatus, LinkedList<LiveRoomFlowTask>>()private val mComparator = Comparator<LiveRoomFlowTask> { o1, o2 -> o2.rule.priority.compareTo(o1.rule.priority) }private var mCurrentRoomStatus: Int = LiveRoomStatus.ON_NONE.priorityprivate var isDestroyCalled = falseoverride fun dispatchTask(roomStatus: LiveRoomStatus) {if (isDestroyCalled) returnmCurrentRoomStatus = mCurrentRoomStatus or roomStatus.prioritymRoomTrace.recordRoomStatusStart(roomStatus, SystemClock.elapsedRealtime())mTasks[roomStatus]?.forEach {if (isDestroyCalled) returnexecuteTask(it)}mRoomTrace.recordRoomStatusEnd(roomStatus, SystemClock.elapsedRealtime())mRoomTrace.logRoomStatus(roomStatus)}override fun registerTask(task: LiveRoomFlowTask) {if (isDestroyCalled) returnval queue = mTasks[task.liveRoomStatus] ?: LinkedList<LiveRoomFlowTask>().also {mTasks[task.liveRoomStatus] = it}if (queue.contains(task)) {return}setupRuleForTask(task, queue)queue.add(task)Collections.sort(queue, mComparator)if (task.isSticky && hasRoomStatus(task.liveRoomStatus)) {executeTask(task)}}private fun setupRuleForTask(liveRoomFlowTask: LiveRoomFlowTask, queue: LinkedList<LiveRoomFlowTask>) {val rule = liveRoomFlowTask.ruleif (rule.type == LiveRoomFlowRule.Type.PRIORITY) returnif (rule.type == LiveRoomFlowRule.Type.ALL_TOP) {liveRoomFlowTask.rule.priority = HIGHEST_PRIORITYreturn}val targetTask = getIndexByTag(rule.target, queue) ?: returnif (rule.type == LiveRoomFlowRule.Type.ABOVE) {liveRoomFlowTask.rule.priority = targetTask.rule.priority + 1}if (rule.type == LiveRoomFlowRule.Type.BELOW) {liveRoomFlowTask.rule.priority = targetTask.rule.priority - 1}}private fun getIndexByTag(target: String, queue: LinkedList<LiveRoomFlowTask>): LiveRoomFlowTask? {queue.forEachIndexed { _, liveRoomTask ->if (target == liveRoomTask.tag) {return liveRoomTask}}return null}private fun hasRoomStatus(liveRoomStatus: LiveRoomStatus) = liveRoomStatus.priority and mCurrentRoomStatus == liveRoomStatus.priorityprivate fun executeTask(flowTask: LiveRoomFlowTask) {val startTs = SystemClock.elapsedRealtime()try {flowTask.task()val endTs = SystemClock.elapsedRealtime()mRoomTrace.recordTask(flowTask, endTs - startTs)} catch (e: Exception) {val endTs = SystemClock.elapsedRealtime()mRoomTrace.recordTask(flowTask, endTs - startTs, e.message)}}override fun destroy() {isDestroyCalled = truemTasks.clear()mRoomTrace.reportMap()}}enum class LiveRoomStatus(val tag: String, val priority: Int) {
ON_NONE(“NONE”, ON_NONE_VAL),
ON_CREATE(“ON_CREATE”, ON_CREATE_VAL),
ON_RESUME(“ON_RESUME”, ON_RESUME_VAL),
ON_P0(“ON_P0”, ON_P0_VAL),
ON_P1(“ON_P1”, ON_P1_VAL)
}class LiveRoomFlowRule(val target: String = "", val type: Type = Type.PRIORITY) {var priority: Long = DEFAULT_PRIORITYinit {when (type) {Type.ABOVE, Type.BELOW, Type.PRIORITY -> UnitType.ALL_TOP -> priority = HIGHEST_PRIORITY}}enum class Type {ABOVE, BELOW, ALL_TOP, PRIORITY}companion object {const val ON_NONE_VAL = 0x000const val ON_CREATE_VAL = 0x001const val ON_RESUME_VAL = 0x002const val ON_P0_VAL = 0x004const val ON_P1_VAL = 0x008fun generatePriorityRule(priority: Long): LiveRoomFlowRule {return LiveRoomFlowRule().apply { this.priority = priority }}}
}

(3)核心任务调度机制-接入指南

//首先确定LiveRoomFlowConfig 中确定要挂载的点,目前有 4种ON_CREATE("ON_CREATE", ON_CREATE_VAL), // ON_CREATE 时机
ON_RESUME("ON_RESUME", ON_RESUME_VAL), // ON_RESUME 时机
ON_P0("ON_P0", ON_P0_VAL), // p0接口请求结束
ON_P1("ON_P1", ON_P1_VAL) //p1接口请求结束
//确认挂载点之后,在确认要执行的顺序优先级,写在固定的Object 下面,数值越大优先级越高object LiveRoomFlowConfig {const val LOW_PRIORITY = 1000Lconst val DEFAULT_PRIORITY = 10000Lconst val HIGH_PRIORITY = 1000000Lconst val HIGHEST_PRIORITY = Long.MAX_VALUE
​object OnCreate {const val INIT_PARAMS = HIGHEST_PRIORITYconst val INIT_IJK = HIGHEST_PRIORITY - 1000Lconst val INIT_ITRACKER = HIGHEST_PRIORITY - 2000Lconst val INIT_ROOM_VIEW = HIGHEST_PRIORITY - 3000Lconst val INIT_VIEWMODEL = HIGHEST_PRIORITY - 4000Lconst val INIT_PLAYERFRAGMENT = HIGHEST_PRIORITY - 5000Lconst val INIT_CONFIGANDPCU = HIGHEST_PRIORITY - 6000L}
​object OnResume {const val COMMON = HIGH_PRIORITYconst val HEARTBEAT = HIGH_PRIORITY - 1000L}
​object OnP0 {//viewModelconst val INIT_P1 = HIGH_PRIORITYconst val INIT_PLAYERDATA = HIGH_PRIORITY - 1000L
​//Viewconst val INIT_PLAY = HIGH_PRIORITY - 2000L
​}
​object OnP1 {const val PLAYER_VIEWMODEL = HIGH_PRIORITYconst val USER_VIEWMODEL = HIGH_PRIORITY - 1000Lconst val SOCKET_VIEWMODEL = HIGH_PRIORITY - 2000Lconst val INTERACTION_VIEWMODEL = HIGH_PRIORITY - 3000Lconst val GIFT_VIEWMODEL = HIGH_PRIORITY - 4000Lconst val BASIC_VIEWMODEL = HIGH_PRIORITY - 5000Lconst val SPPLAYER_VIEWMODEL = HIGH_PRIORITY - 6000Lconst val OPERATION_VIEWMODEL = HIGH_PRIORITY - 7000Lconst val ANIM_VIEWMODEL = HIGH_PRIORITY - 8000Lconst val SKIN_VIEWMODEL = HIGH_PRIORITY - 9000Lconst val OPERATION_VIEWMODELV3 = HIGH_PRIORITY - 10000Lconst val VOICE_VIEWMODEL = HIGH_PRIORITY - 11000Lconst val SUPERCHAT_VIEWMODEL = HIGH_PRIORITY - 12000Lconst val LPLSP_VIEWMODEL = HIGH_PRIORITY - 13000Lconst val INTERACTIONPANEL_VIEWMODEL = HIGH_PRIORITY - 14000Lconst val QUESTION_VIEWMODEL = HIGH_PRIORITY - 15000L
​//分发 其他Basic数据const val DISPATCH_BASIC_DATA = HIGH_PRIORITY - 16000Lconst val DISPATCH_ROOM_INFO_COMPLETE = HIGH_PRIORITY - 17000L}
}
//创建一个Task 继承自 AbstractLiveRoomFlowTask类型, 或者使用快捷创建方式sealed class LiveRoomFlowTask(val task: () -> Unit) {abstract val tag: String //tag标记abstract val rule: LiveRoomFlowRule //规则,推荐优先级处理方式abstract val liveRoomStatus: LiveRoomStatus //挂载点open val isSticky: Boolean = false //是否支持粘性
}
​
internal abstract class AbstractLiveRoomFlowTask(val status: LiveRoomStatus, task: () -> Unit) : LiveRoomFlowTask(task) {override val liveRoomStatus: LiveRoomStatusget() = status
}
​
fun createFlowTask(tag: String, status: LiveRoomStatus, task: () -> Unit): LiveRoomFlowTask {return createFlowTask(tag, status, LiveRoomFlowConfig.DEFAULT_PRIORITY, false, task)
}
​
fun createFlowTask(tag: String, status: LiveRoomStatus, priority: Long, task: () -> Unit): LiveRoomFlowTask {return createFlowTask(tag, status, priority, false, task)
}
​
fun createFlowTask(tag: String, status: LiveRoomStatus, priority: Long, isSticky: Boolean, task: () -> Unit): LiveRoomFlowTask {return object : AbstractLiveRoomFlowTask(status, task) {override val tag: Stringget() = tagoverride val rule: LiveRoomFlowRuleget() = LiveRoomFlowRule.generatePriorityRule(priority)
​override val isSticky: Booleanget() = isSticky}
}
//最后通过registerTask方法注入taskfun LiveRoomFlowManager.registerOnCreateTask(tag: String, priority: Long, task: () -> Unit) {registerTask(createFlowTask(tag, LiveRoomStatus.ON_CREATE, priority, task))
}
​
fun LiveRoomFlowManager.registerOnResumeTask(tag: String, priority: Long, task: () -> Unit) {registerTask(createFlowTask(tag, LiveRoomStatus.ON_RESUME, priority, task))
}
​
fun LiveRoomFlowManager.registerOnP0Task(tag: String, priority: Long, task: () -> Unit) {registerTask(createFlowTask(tag, LiveRoomStatus.ON_P0, priority, task))
}
​
fun LiveRoomFlowManager.registerOnP1Task(tag: String, priority: Long, task: () -> Unit) {registerTask(createFlowTask(tag, LiveRoomStatus.ON_P1, priority, task))
}

(三)直播间数据和通道能力建设

(1)BiDataReporter-业务埋点能力

随着直播间业务埋点需求和复杂度不断增加,统一建设埋点能力和埋点数据规范成为我们必要的工作,此文档中核心描述直播间业务埋点能力建设。

现状

目前直播技术已建设埋点数据能力,直播间架构未对此进行合理设计,较多业务埋点数据存在公用埋点,且公共埋点无抽象和封装,简单依赖Kotlin扩展解决问题,导致直播间架构层次依赖关系复杂。

目标

建设直播间埋点数据能力与组装数据规范,拆解清楚直播间架构层级依赖关系。

埋点能力建设

定义直播间业务埋点接口LiveRoomReporter,支持如下:

  1. 业务点击与曝光埋点;
  2. 技术事件埋点;
  3. 系统事件埋点;
  4. 自定义类型埋点;

LiveRoomReporter接口实现类LiveRoomReporterImpl生命周期绑定LiveRoomContext,且内部依赖DataStoreManager,以便完成公共参数默认组装,组装方式参见 埋点数据组装规范。

LiveRoomReporter的服务对象:

  1. Domain;
  2. ViewModel;
  3. 可以获得RoomContext的对象;

代码样例

//注入实现代码:

class LiveRoomContext private constructor(val flowManager: LiveRoomFlowManager,

                                          val dataStoreManager: ILiveRoomDataStoreManager,

                                          val socketManager: ISocketManager

) : LiveLogger {

    val roomReporter: LiveRoomReporter = LiveRoomReporterImpl(dataStoreManager)

}

//接口定义代码:

/**

 * 房间页打点统一通道

 */

interface LiveRoomReporter {

    /**

     * 点击事件埋点

     * @param eventId       事件id

     * @param data          埋点参数组装

     */

    fun reportClick(eventId: String, data: LiveRoomReportData.() -> Unit)

    /**

     * 点击事件埋点

     * @param eventId       事件id

     * @param force         是否实时上传

     * @param data          埋点参数组装

     */

    fun reportClick(eventId: String, force: Boolean, data: LiveRoomReportData.() -> Unit)

    /**

     * 点击事件埋点

     * @param eventId       事件id

     * @param extension     埋点参数

     */

    fun reportClick(eventId: String, extension: Map<String, String> = HashMap())

    /**

     * 点击事件埋点

     * @param eventId       事件id

     * @param extension     埋点参数

     * @param force         是否实时上报

     */

    fun reportClick(eventId: String, extension: Map<String, String> = HashMap(), force: Boolean)

    /**

     * 曝光事件埋点

     * @param eventId       事件id

     * @param data          埋点参数组装

     */

    fun reportExposure(eventId: String, data: LiveRoomReportData.() -> Unit)

    /**

     * 曝光事件埋点

     * @param eventId       事件id

     * @param force         是否实时上报

     * @param data          埋点参数组装

     */

    fun reportExposure(eventId: String, force: Boolean, data: LiveRoomReportData.() -> Unit)

    /**

     * 曝光事件埋点

     * @param eventId       事件id

     * @param extension     埋点参数

     */

    fun reportExposure(eventId: String, extension: Map<String, String> = HashMap())

    /**

     * 曝光事件埋点

     * @param eventId       事件id

     * @param extension     埋点参数

     * @param force         是否实时上报

     */

    fun reportExposure(eventId: String, extension: Map<String, String> = HashMap(), force: Boolean)

    /**

     * 技术埋点

     * @param eventId       事件id

     * @param pageType      页面类型:PageType.NATIVE, PageType.H5

     * @param sampler       是否采样

     * @param data          埋点数据组装

     */

    fun reportTech(eventId: String, @PageType.All pageType: Int = PageType.NATIVE, sampler: () -> Boolean, data: LiveRoomReportData.() -> Unit)

    /**

     * 技术埋点

     * @param eventId       事件id

     * @param pageType      页面类型:PageType.NATIVE, PageType.H5

     * @param sampler       是否采样

     * @param force         是否实时上报

     * @param data          埋点数据组装

     */

    fun reportTech(eventId: String, @PageType.All pageType: Int = PageType.NATIVE, sampler: () -> Boolean, force: Boolean, data: LiveRoomReportData.() -> Unit)

    /**

     * 系统事件埋点

     * @param eventId       事件id

     * @param data          埋点数据组装

     */

    fun reportSystem(eventId: String, data: LiveRoomReportData.() -> Unit)

    /**

     * 系统事件埋点

     * @param eventId       事件id

     * @param force         是否实时上报

     * @param data          埋点数据组装

     */

    fun reportSystem(eventId: String, force: Boolean, data: LiveRoomReportData.() -> Unit)

    /**

     * 自定义埋点

     * @param eventCategory 类型

     * @param eventId       事件id

     * @param data          埋点数据组装

     */

    fun reportCustom(@EventCategory.Custom eventCategory: Int, eventId: String, data: LiveRoomReportData.() -> Unit)

    /**

     * 自定义埋点

     * @param eventCategory 类型

     * @param eventId       事件id

     * @param force         是否实时上报

     * @param data          埋点数据组装

     */

    fun reportCustom(@EventCategory.Custom eventCategory: Int, eventId: String, force: Boolean, data: LiveRoomReportData.() -> Unit)

}

(2)BiDataReporter-业务埋点规划

埋点通道规范

总结如下:

  1. 直播间业务统一使用LIveRoomReporter接口完成业务埋点;
  2. 非直播间使用LiveReporter单例完成业务埋点。

埋点数据规范

总结如下:

  1. 直播间业务使用LiveRoomReportData完成数据组装;
  2. 非直播间业务使用LiveReportData完成数据组装。

(3)DataStore - 数据隔离和访问能力

背景

1、直播间在接入核心任务调度机制之前,很多核心业务数据访问时间需要在数据的获取后

2、为了解决上述问题使用了,许多业务数据被迫使用了LiveData的声明方式,用来通知数据已获取

3、后续的许多业务场景沿用了以上设计,但大部分只读数据并不需要通知的能力

现有数据来源

1、如上图所示,进入直播间后会按顺序经过以下阶段(后续可继续拓展):进入直播间 → P0接口请求成功 → P1接口请求成功 → 用户接口请求成功。

2、在设计DataStore之前,直播间数据是没有分层的(baseData、P0data、P1data、UserData),对各个阶段访问的数据也没有设置相应的限制和约束,直播间所有数据不管有没有获取到,都可以在一个大对象里通过属性访问到。

3、这样会出现如下问题:上一个阶段的任务在需要访问下一个阶段任务对应的数据时,下一个阶段对应的数据尚未被赋值,会导致该访问异常。

4、并且开发人员因为能够拿到该属性,会以为是有值可用的,往往在线上出了问题才会被发现使用的时机不对。例如:在进入直播间阶段,会访问p1接口中的X数据,然而,该数据尚未被赋值,所以该访问操作应当在p1接口请求成功后在执行,但是因为可以访问到该数据对应的属性,开发人员无法感知自己的调用时机是错误的。

改造流程

1、将LiveRoomData中有关进房间接口的数据迁移至LiveRoomDataStore中

2、去掉房间接口数据中LiveData的通知方式,改为流程控制中task注册方式

3、各task中任务只能使用task返回的该阶段数据

4、针对房间复杂业务数据做特殊处理

5、完善DataStore - 数据隔离和访问规范(下一节内容)

(4)DataStore - 数据隔离和访问规范

DataStore概览

业务中常用的属性和方法:

LiveRoomDataStore:播间核心数据源,持有并管理各阶段的数据

  • write(key: Key, value: Any) : 当有数据在接口下发后有修改操作,需要提供相关的key,并在write方法里注册

LiveRoomDataStoreManager:DataStore的管理类,提供各种数据相关的接口给业务方使用

  • baseData:提供安全的baseData,如果在onCreate前调用,debug模式下回抛出异常,release模式会提供保底数据并上报到星际平台
  • finalData:提供安全的finalData,如果在p1接口成功前调用,debug模式下回抛出异常,release模式会提供保底数据并上报到星际平台
  • getDataByStatus():提供安全的各阶段数据获取方法,用于获取各阶段数据
  • writeDataStoreValueByKey(key: Key, value: Any):提供安全的数据修改方法,仅在数据所属阶段后修改数据

ILiveRoomBaseData:房间基础数据集,包含房间外传入数据和进直播间生成的数据,feed模式下部分数据由推荐列表接口提供

LiveRoomP0Data:P0接口成功后提供的数据集,包含BaseData内容

LiveRoomP1Data:P1接口成功后提供的数据集,包含P0Data和BaseData内容


数据分层

在DataStroe机制中,对直播间数据进行了分层

如下图所示,进入直播间后会经过“进入直播间 → P0接口请求成功 → P1接口请求成功 → 用户接口请求成功”四个阶段(后续可继续拓展),各阶段数据的定义如下。

baseData:生成直播间所需要的基础信息,非接口返回而是直播间外带入的,包括房间id、房间跳转来源等;

P0Data:直播间的功能性信息,由进入直播间调用的第一个接口返回的数据生成;

P1Data:直播间的业务信息,各业务初始化所需数据集合;

UserData:用户基本信息,只有登录后才会存在

1、在上述阶段中会获取或创建不同的直播间数据,处在下个阶段的任务可以访问上一个阶段的数据,反之因为数据还不存在可能会导致业务异常。

2、DataStroe机制的建设解决了这个问题,业务在通过DataStroe访问各个阶段数据时,会判断当前所在的阶段,如果业务所处阶段已存在数据则可以访问,反之则无法访问。


基于核心任务调度机制的挂载点的业务,使用Task提供的数据集

@UiThread
fun registerOnP0Task(tag: String, priority: Long, task: (LiveRoomP0Data) -> Unit) {registerTask(FlowTaskFactory.create(tag, LiveRoomStatus.ON_P0, priority) {dataStoreManager.getDataByStatus<LiveRoomP0Data>()?.let { task(it) }})
}@UiThread
fun registerOnP1Task(tag: String, priority: Long, task: (LiveRoomP1Data) -> Unit) {registerTask(FlowTaskFactory.create(tag, LiveRoomStatus.ON_P1, priority) {dataStoreManager.getDataByStatus<LiveRoomP1Data>()?.let { task(it) }})
}@UiThread
fun registerOnUserInfoTask(tag: String, priority: Long, task: (BiliLiveRoomUserInfo) -> Unit) {registerTask(FlowTaskFactory.create(tag, LiveRoomStatus.ON_USERINFO, priority) {dataStoreManager.getDataByStatus<BiliLiveRoomUserInfo>()?.let { task(it) }})
}

调用时it来访问数据

(5)RxBus - Event通信能力

使用现状

1、直播间数据通道不统一;

2、目前通道事件管理混乱,无法统计;

3、发送事件时未校验该事件是否注册;

解决方案

1、定义基类Event,收拢事件;

2、记录所有注册的事件和线程类型,post没有注册事件时在debug抛出异常,线上接入数据告警通道;

3、从LiveRoomData迁移到LiveRoomContext中;

class LiveRoomContext(roomData: LiveRoomData, val liveRoomFlowManager: LiveRoomFlowManager) {// 用于主线程通信的RxDataval mainRxBus by lazy(LazyThreadSafetyMode.NONE) {RxBus.newInstance()}// 用于子线程通信的RxDataval serializedRxBus by lazy {RxBus.newInstance()}// 事件注册情况private val subscribedEventMap = mutableMapOf<Class<out Event>, ThreadType>()	fun post(event: Event, threadType: ThreadType = ThreadType.MAIN) {// 检测事件注册情况if (subscribedEventMap[event::class.java] != threadType) {if (BuildConfig.DEBUG) {throw IllegalArgumentException("RxBus has no event subscribed in $threadType")}logError { "RxBus has no event subscribed in $threadType" }}when (threadType) {ThreadType.MAIN -> postMainEvent(event)ThreadType.SERIALIZED -> serializedRxBus.post(event)}}fun <T : Event> subscribe(clazz: Class<T>, action: (T) -> Unit, threadType: ThreadType = ThreadType.MAIN) {subscribedEventMap[clazz] = threadTypewhen (threadType) {ThreadType.MAIN -> mainRxBus.registerNoinline(clazz).subscribe({...})ThreadType.SERIALIZED -> serializedRxBus.registerNoinline(clazz).onBackpressureDrop {...}}fun cleanUp() {mainRxBus.cleanUp()serializedRxBus.cleanUp()}...
}interface Eventenum class ThreadType {MAIN, SERIALIZED
}
方便使用这几个ViewModel的扩展方法还是保留@MainThread
fun LiveRoomBaseViewModel.postMainEvent(event: Event) {roomContext.post(event)
}inline fun <reified T : Event> LiveRoomBaseViewModel.subscribeMainEvent(crossinline action: (T) -> Unit) {roomContext.subscribe(action)
}@WorkerThread
fun LiveRoomBaseViewModel.postBackgroundEvent(event: Event) {roomContext.post(event, ThreadType.SERIALIZED)
}inline fun <reified T : Event> LiveRoomBaseViewModel.subscribeBackgroundEvent(crossinline action: (T) -> Unit) {roomContext.subscribe(action, ThreadType.SERIALIZED)
}

(6)RxBus - Event通信规范

事件定义

直播间所有事件都需要实现Event接口;

interface Event

class LiveRoomLoginEvent(val requestCode: Int) : Event
事件收拢路径
直播间内所有事件都需定义在如下目录中;

andruid/app/bililive/bililiveLiveVideoPlayer/src/main/kotlin/com/bilibili/bililive/videoliveplayer/ui/roomv3/base/events
事件分类
事件分类bussiness和common两个大类。然后再按具体业务划分到文件中。

举例:

events/bussiness/PK.kt

//pk 倒计时开始
class PKPreEvent(val pkID: Long, val pkStatus: Int, val pkPreEntity: PKPreEntity) : Event

//pk 开始
class PKStartEvent(val pkID: Long, val pkStatus: Int, val pkStartEntity: PKStartEntity) : Event

//pk 结束
class PKEndEvent(val pkID: Long, val pkStatus: Int, val pkEndEntity: PKEndEntity) : Event

//pk 结算
class PKSettleEvent(val pkID: Long, val pkStatus: Int, val pkSettleEntity: PKSettleEntity) : Event

(四)动态层级管理器

(1)问题

当前直播间所有View布局,在初始化的时候统一加载到内存里,存在很多资源浪费,同时影响首帧,首屏耗时。
直播间当前布局都在一个xml文件中,业务之间耦合,层级梳理不清晰。

(2)目标

  1. 支持层级动态,懒加载方式,节约内存压力,减少初始化耗时。
  2. 层级梳理,动态生成层级关系快照,基于当前层级关系,方便后续业务迭代
  3. 对业务优化,简化业务接入方式,开发业务无需考虑内部实现
  4. 对表现层重新划分,按照区域划分方式,方便查找相关业务

(3)过程

基于竖版直播间改造,接入层级管理系统,同时梳理层级关系。加载流程

(4)代码设计


新增分区
  1. 新增区域时需要在 HierarchyScope 添加新区名
  2. 每个分区都有一个 HierarchyViewContainer 用来添加层级视图,一个 HierarchyAdapter 用来管理视图的配置、添加和移除
  3. 层级视图里业务关心的生命周期和返回键拦截等需要 HierarchyViewContainer 来分发
class LiveRoomActivityV3 {private var mHierarchyContainer: HierarchyViewContainer? = nullprivate var mHierarchyAdapter: HierarchyAdapter = HierarchyAdapter(HierarchyScope.GLOBAL)override fun onCreate(savedInstanceState: Bundle?) {//...mHierarchyContainer = HierarchyViewContainer(this)mHierarchyContainer?.setAdapter(mHierarchyAdapter)//...}override fun onResume() {super.onResume()mHierarchyContainer.onResumeView(this)}override fun onPause() {super.onPause()mHierarchyContainer.onPauseView(this)}override fun onDestroy() {super.onDestroy()mHierarchyContainer.onDestroyView(this)}override fun onBackPressed() {if (mHierarchyContainer.onBackPressed()) {return}super.onBackPressed()}
}
新增层级视图

层级视图分为两类

  1. 区域初始化时通过配置构建,添加顺序决定来该视图的层级关系,继承自 HierarchyView
  2. 通过 HierarchyAdapter 动态添加,继承自 DialogHierarchyView

HierarchyViewHolder

  1. 每个 HierarchyView 都有一个对应的 HierarchyViewHolder 用于提供构建视图的能力和层级的信息
  2. HierarchyAdapter 不会暴露对层级视图的直接操作,添加、删除等操作的对象都是 HierarchyViewHolder
/**层级信息:
- tag : 层级的名称
- id : 层级的唯一标示,格式为tag_index,区域内新每添加一个相同tag的层级实例时index自增1
*/
class HierarchyAViewHolder : HierarchyViewHolder("layerA") {override fun createHierarchyView(context: Context, adapter: HierarchyAdapter): HierarchyView {return HierarchyAView(tag, adapter, context)}
}class HierarchyAView(tag: String, adapter: HierarchyAdapter, context: Context) : HierarchyView(tag, adapter, context) {override fun onCreateView(context: Context, id: String) {super.onCreateView(context, id)inflate(context, R.layout.layout_layer_a, this)}
}

HierarchyAdapter
外部对层级的操作均使用 HierarchyAdapter 进行

setViewHolderData 方法用来初始配置区域的初始层级视图


hierarchyAdapter.setViewHolderData(listOf(HierarchyAViewHolder(),HierarchyBViewHolder(),HierarchyCViewHolder(),HierarchyDViewHolder())
)

这里建议使用方建立一个 HierarchyScopeDefine 用来管理区域配置信息

hierarchyAdapter.setViewHolderData(HierarchyScopeDefine.Global().getHierarchyDefine()
)class HierarchyScopeDefine {interface ScopeDefine{fun getHierarchyDefine(): List<HierarchyViewHolder>}class Global : ScopeDefine{override fun getHierarchyDefine(): List<HierarchyViewHolder> {return listOf(HierarchyAViewHolder(),HierarchyBViewHolder(),HierarchyCViewHolder(),HierarchyDViewHolder())}}
}//createView 方法用来通过上述配置构建初始层级视图
hierarchyAdapter.createView(this)//addHierarchy 方法用来根据规则添加视图
hierarchyAdapter.addHierarchy(context,DialogBHierarchyViewHolder(),HierarchyRule("dialogBottom", HierarchyRule.Type.ABOVE))hierarchyAdapter.addHierarchy(context,DialogBHierarchyViewHolder(),HierarchyRule(type = HierarchyRule.Type.ALL_TOP))
  • removeHierarchyByTag 方法用来移除视图,移除所有符合tag类型的层级
  • removeHierarchyById 方法用来移除视图,根据id移除层级
  • snapshotDataHierarchy 方法用于打印adapter中数据的快照

(5)创建动态层级视图

新view需要继承LiveRoomBaseDynamicInflateView,并通过重写部分属性和方法来定制化实体view在层级容器中的位置:

class LiveRoomDemoView(activity: LiveRoomActivityV3, liveHierarchyManager: LiveHierarchyManager, lifecycleOwner: LifecycleOwner) : LiveRoomBaseDynamicInflateView(activity, liveHierarchyManager, lifecycleOwner) {override val layoutRes: Intget() = TODO("需要加载的XML")override val tag: Stringget() = TODO("TAG")override val priority: LiveRoomViewPriorityget() = TODO("优先级")override val defaultLayoutParams: LiveRoomViewLayoutParamsget() = TODO("层级布局")override val supportScreenMode: Intget() = TODO("支持的屏幕类型")/*** 默认View在 BUSINESS层 如果要改变所属的容器层,需要重写此方法*/override fun getScope() = HierarchyScope.BUSINESSoverride fun onViewCreate(view: View) {TODO("实体view被加载后的逻辑")}override fun onScreenModeChange(mode: PlayerScreenMode) {TODO("屏幕方向变化后的逻辑")}override fun onDestroyIfInflate() {TODO("如果实体view被加载,处理onDestroy的逻辑")}
}
对布局和控件的管理

1、每个虚拟view会管理一个属于自己的实体view,通过重写layoutRes属性来提供业务的布局文件

2、使用bindInflateView或者bindInflateOptionalView方法获取布局中的控件

3、在onViewCreate里对控件进行UI操作

4、对于某些只有在实体view加载时才处理的销毁逻辑,在onDestroyIfInflate里处理

实体view被加载的时机

1、只有在调用了inflateView()方法,实体view才会被加载

2、inflateView()一般不需要业务手动调用,大多数场景是由某个事件触发的,具体见下面的事件驱动型订阅LiveData

3、如果一个view是刚进入直播间就需要加载的可以手动调用inflateView()

4、可以通过isInflated字段判断实体view是否已经加载

定义view支持的屏幕模式和各屏幕的适配

1、直播间有三种屏幕模式,重写supportScreenMode来定义该view在哪些屏幕下可见,默认支持三种屏幕模式

2、在屏幕模式发生变化时的逻辑,可以在onScreenModeChange里处理

3、supportScreenMode决定了priority和defaultLayoutParams的设定,每种屏幕模式都有对应的优先级和布局样式

4、所有的层级优先级可以在LiveRoomHierarchyConfig里查看,图例见0015. Android 竖屏直播间改版三期-三屏View复用方案

例:

override val priority: LiveRoomViewPriority = LiveRoomViewPriority(thumbPriority = LiveRoomHierarchyConfig.ThumbBusiness.BUSINESS_PK,verticalPriority = LiveRoomHierarchyConfig.VerticalBusiness.BUSINESS_PK,horizontalPriority = LiveRoomHierarchyConfig.HorizontalBusiness.BUSINESS_PK
)override val defaultLayoutParams = LiveRoomViewLayoutParams(verticalLayoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT),thumbLayoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT),horizontalLayoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
LiveData的订阅方法

事件驱动型订阅
1、当前直播间内的大多数业务,在收到LiveData变化的通知后需要对实体view进行操作,换言之这部分操作需要在实体view加载好后执行

2、使用observerForInflateView订阅LiveData时,如果实体view没有加载,会先执行view的加载来保证后续的操作

3、对应的observerForever方法为observerForeverForInflateView

业务感知型订阅
1、另一种常见的订阅场景是,如果当前实体view已经被加载就执行后续操作

2、使用observerForActionIfInflated订阅LiveData时,会先检查当前的view是否加载,如果已经加载才会执行后续操作

3、对应的observerForever方法为observerForeverForActionIfInflated

特殊情况的处理
1、部分订阅场景不与view的加载有关,可以使用observerForNormal简化调用链

2、同时某些不是通过LiveData通知的场景需要加载view时,可以使用inflateView反复手动加载view

(五)直播间业务数据改造

直播间中的数据

1、数据的分类

1.1、环境数据

1、数据特点:多读多写,不属于任何业务,经常用来描述业务所处环境,例如:是否登录、屏幕状态、开关播状态、房间类型等

2、数据来源:globalDataService和roomDataService,业务方直接使用各自的ViewModel即可访问环境数据的最新值

1.2、埋点数据

1、数据特点:仅用于埋点传参,聚合类数据,公参基本由环境数据提供,不同埋点会带上不同业务独有的数据

2、数据来源:与埋点的时机和位置有关,业务方在埋点的地方获取环境数据和自己的业务数据,合成埋点所需要的数据集合,通常构建一个Map实例传递

1.3、业务数据

1、数据特点:每个数据对应唯一的业务方,由各个业务方维护,部分数据需要通过接口共享(提供给其他业务)

2、数据来源:房间聚合接口、socket广播、业务接口、用户动作等

2、直播间内数据的来源和流向

3、业务的启动和数据维护

DataStore的背景

1、直播间的初始数据由Intent、P0P1接口提供,业务的启动逻辑往往依赖这些数据以及这些接口返回的时机

2、原先DataStore的设计,是为了配合核心任务调度机制而存在的,保证了业务逻辑在某个任务中访问数据的安全性

3、这些初始数据大部分在初始化阶段后就不再使用,但还有一部分数据在业务运行阶段还会被修改和访问,为了提供非Task内访问数据的能力制定了复杂的DataStore - 数据隔离和访问规范

4、这些复杂的交互的根本原因是DataStore作为了数据的承载方:因为DataStore提供了初始值,所有用到这个值的业务都会向DataStore获取这个值,因此这个值在后续改变时只需要更新DataStore维护的这个值,所有用到这个值的业务再次向向DataStore获取这个值时,拿到的就是最新的值了

数据真正的承载方

1、数据的承载方应该与数据的描述有关,这个数据描述的是哪块业务,这块业务就应该承载这个数据,并负责后续的更新和向外提供,初始数据仅仅是这个数据的第一个赋值

2、如果某个数据描述的是整个直播间的基础状态信息,各个业务都需要使用该信息做逻辑判断,那么这个数据将不属于任何业务,他的承载方应该是房间的基础数据服务

3、将原先DataStore里的数据迁移到各个业务或基础数据服务中后,原本向DataStore获取这个值的地方将直接向该数据的承载方获取该值

业务服务的改变

  1. ILiveRoomBizService需要新的数据管理方式

  • 新增一个businessData,用来描述整个业务
  • 新增各个时机的数据更新方法用来更新businessData的数据
  • 新增startUp()方法作为服务启动的初始时机
  • 新增优先级机制,可以决定分各个服务发数据和startUp()执行的先后顺序
  1. LiveAppServiceManager提供获取全部房间业务服务的方法

  2. 在TaskManger的各个数据时机里进行数据分发,遍历serviceList调用响应时机的方法
  3. 在P1数据分发结束后,遍历serviceList调用startUp()方法,在或User数据分发结束结束后分发onLoginComplete()方法

4、不同业务交互场景下的数据流向分析

业务场景和数据的关系

  1. 一个业务的主体是由数据和逻辑处理组成的
  2. 简单的业务场景下,处理逻辑用到的数据一般都是归属于该业务自己的
  3. 实际情况中一个业务的逻辑往往会关心其他业务的数据,从而产生各种交互

两个业务间的交互场景

  1. 两个业务间最常见的交互:获取其他业务的数据、改变其他业务的数据
  2. 获取其他业务数据场景的设计相对比较统一,一般由数据维护方提供获取的能力
  3. 改变其他业务数据的场景就比较多元化,需要根据不同的业务场景制定: 
  • 当外部需要感知具体改动的数据是什么时,数据维护方可以提供更改某数据的能力供外部调用
  • 当一个业务在某个时机发生的某个变化有多个其他业务需要改变时,往往会描述为该业务发出了一个改变的通知,关心这个通知的其他业务接到通知后对维护的数据执行改变

两个业务以上的交互场景

  1. 获取其他业务数据场景的设计与两个业务的设计相同,不在赘述
  2. 关于在加入第三个业务时,该业务对于已有的数据流向来说,是事件的生产方还是消费方决定这次通知应该如何描述:
  • 如果新增的业务后有多个生产方,事件应该定义在消费事件的业务里,描述为消费方需要响应的事件
  • 如果新增的业务后有多个消费方,事件应该定义在生产事件的业务里,描述为生产方产生的事件

(六)直播业务改造整理

LiveRoomActivityV3逻辑改造

问题:

1、Activity的上千行代码中除了必要的View交互外大致分为三部分:

  1. 直播间加载流程OnCreate、OnResume、OnReset任务的处理
  2. P0接口加载成功、失败(包含加密)逻辑的处理
  3. 播放器相关逻辑

2、部分功能的管理者由Activity持有,导致VerticalView滑动流程需要反向调用Activity中的方法

3、落地直播间流程与滑动重置直播间Item流程统一出口处理导致逻辑不清晰

业务分层

 直播间引入上下滑功能后,现有业务没有分层设计,导致部分特殊生命周期业务组件的流程处理逻辑不清晰,拆分后如图:

1、绿色部分为房间维度业务,随直播间上下划出屏幕而重建

2、蓝色部分为滑动直播间中的滑动列表业务,该部分不随上下滑重建,但会随后续的直播间大刷重建

3、黄色部分为进直播间后就存在的业务,与Activity生命周期同步,大刷时仍需保持不变,随Activity的销毁而销毁

业务的组件化

目标:每一个模块都有自己独立的逻辑和UI,模块之间完全解耦

1、模块化的数据如何存储:数据需要分为各业务独自持有独自处理的部分(外部不可见)和共享出去的部分(外部可见)

2、不同业务间通讯和交互:各业务间不应该感知其他业务的存在,对于模块而言只有要做什么,各模块的数据变化的交互应该转换为各种输入和输出的行动指令

3、模块每一层处理该层的输入和输出:

  1. View:
    1. 输入:监听LiveData的数据变化、用户输入的UI事件
    2. 输出:调用ViewModel提供的功能接口
  2. ViewModel:
    1. 输入:监听持有的Service提供的callBack数据
    2. 输出:调用持有的Service提供的功能接口
  3. Service:
    1. 输入:Http的响应、监听Socket的广播、其他业务的数据变化
    2. 输出:Http的请求、共享数据变化(其他业务需要的数据)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/478400.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【es6】原生js在页面上画矩形添加选中状态高亮及显示调整大小控制框(三)

接上篇文章&#xff0c;这篇实现下选中当前元素显示调整大小的控制框&#xff0c;点击document取消元素的选中高亮状态效果。 实现效果 代码逻辑 动态生成控制按钮矩形,并设置响应的css // 动态添加一个调整位置的按钮addScaleBtn(target) {const w target.offsetWidth;con…

ArcGIS应用指南:ArcGIS制作局部放大地图

在地理信息系统&#xff08;GIS&#xff09;中&#xff0c;制作详细且美观的地图是一项重要的技能。地图制作不仅仅是简单地将地理数据可视化&#xff0c;还需要考虑地图的可读性和美观性。局部放大图是一种常见的地图设计技巧&#xff0c;用于展示特定区域的详细信息&#xff…

记录一些PostgreSQL操作

本文分享一些pg操作 查看版本 select version(); PostgreSQL 11.11 查看安装的插件 select * from pg_available_extensions; 查看分词效果 select ‘我爱北京天安门,天安门上太阳升’::tsvector; ‘天安门上太阳升’:2 ‘我爱北京天安门’:1select to_tsvector(‘我爱北京天…

RHCSA作业2

压缩 将整个 /etc 目录下的文件全部打包并用 gzip 压缩成/back/etcback.tar.gz [rootjyh ~]# cd /etc [rootjyh etc]# tar -czf etcback.tar.gz /etc tar: Removing leading / from member names tar: /etc/etcback.tar.gz: file changed as we read it [rootjyh etc]# ls使当…

大语言模型(LLM)安全:十大风险、影响和防御措施

一、什么是大语言模型&#xff08;LLM&#xff09;安全&#xff1f; 大语言模型&#xff08;LLM&#xff09;安全侧重于保护大型语言模型免受各种威胁&#xff0c;这些威胁可能会损害其功能、完整性和所处理的数据。这涉及实施措施来保护模型本身、它使用的数据以及支持它的基…

递推进阶与入门递归

一、递推进阶&#xff0c;勇攀高峰 昆虫繁殖 题目描述 科学家在热带森林中发现了一种特殊的昆虫&#xff0c;这种昆虫的繁殖能力很强。每对成虫过X个月产Y对卵&#xff0c;每对卵要过两个月长成成虫。假设每个成虫不死&#xff0c;第一个月只有一对成虫&#xff0c;且卵长成成虫…

深入浅出:JVM 的架构与运行机制

一、什么是JVM 1、什么是JDK、JRE、JVM JDK是 Java语言的软件开发工具包&#xff0c;也是整个java开发的核心&#xff0c;它包含了JRE和开发工具包JRE&#xff0c;Java运行环境&#xff0c;包含了JVM和Java的核心类库&#xff08;Java API&#xff09;JVM&#xff0c;Java虚拟…

极客大挑战2024wp

极客大挑战2024wp web 和misc 都没咋做出来&#xff0c;全靠pwn✌带飞 排名 密码学和re没做出几个&#xff0c;就不发了 web ez_pop 源代码 <?php Class SYC{public $starven;public function __call($name, $arguments){if(preg_match(/%|iconv|UCS|UTF|rot|quoted…

C++设计模式-策略模式-StrategyMethod

动机&#xff08;Motivation&#xff09; 在软件构建过程中&#xff0c;某些对象使用的算法可能多种多样&#xff0c;经常改变&#xff0c;如果将这些算法都编码到对象中&#xff0c;将会使对象变得异常复杂&#xff1b;而且有时候支持不使用的算法也是一个性能负担。 如何在运…

【初阶数据结构和算法】leetcode刷题之设计循环队列

文章目录 一、实现循环队列1.大致思路分析2.循环队列的结构定义和初始化结构定义初始化 3.循环队列的判空和判满判空和判满难点分析判空判满 4.循环队列的入队列和出队列入队列出队列 5.循环队列取队头和队尾元素取队头元素取队尾元素 6.循环队列的销毁7.最后题解源码 一、实现…

【网络通信】数据集合集!

本文将为您介绍经典、热门的数据集&#xff0c;希望对您在选择适合的数据集时有所帮助。 1 RITA 更新时间&#xff1a;2024-11-22 访问地址: GitHub 描述&#xff1a; RITA 是一个用于网络流量分析的开源框架。 该框架以 TSV 或 JSON 格式提取 Zeek 日志&#xff0c;目前支…

.net core MVC入门(一)

文章目录 项目地址一、环境配置1.1 安装EF core需要包1.2 配置数据库连接二、使用EF创建表2.1 整体流程梳理2.1 建表详细流程三、添加第一个视图3.1整体流程梳理3.1 添加视图,并显示在web里四、使用EF增加Catogory数据,并且读取数据到页面4.1整体流程梳理4.2 实现五、增加Cat…

蓝桥杯不知道叫什么题目

小蓝有一个整数&#xff0c;初始值为1&#xff0c;他可以花费一些代价对这个整数进行变换。 小蓝可以花贵1的代价将教数增加1。 小蓝可以花费3的代价将整数增加一个值,这个值是整数的数位中最大的那个(1到9) .小蓝可以花费10的代价将整数变为原来的2倍, 例如&#xff0c;如果整…

css效果

css炫彩流光圆环效果 <!DOCTYPE html> <html><head><meta charset"utf-8" /><title></title><style>*{margin: 0;padding: 0;}body{width: 100%;height: 100vh;}.container{position: relative;width: 100%;height: 100vh…

提供html2canvas+jsPDF将HTML页面以A4纸方式导出为PDF后,内容分页时存在截断的解决思路

前言 最近公司有个系统要做一个质量报告导出为PDF的需求&#xff0c;这个报表的内容是固定格式&#xff0c;但是不固定内容多少的&#xff0c;网上找了很多资料&#xff0c;没有很好的解决我的问题&#xff0c;pdfmakde、还有html2CanvasjsPDF以及Puppeteer无头浏览器的方案都不…

【C++动态规划 子集状态压缩】2002. 两个回文子序列长度的最大乘积|1869

本文涉及知识点 C动态规划 位运算、状态压缩、枚举子集汇总 LeetCode2002. 两个回文子序列长度的最大乘积 给你一个字符串 s &#xff0c;请你找到 s 中两个 不相交回文子序列 &#xff0c;使得它们长度的 乘积最大 。两个子序列在原字符串中如果没有任何相同下标的字符&…

鸿蒙NEXT开发案例:字数统计

【引言】 本文将通过一个具体的案例——“字数统计”组件&#xff0c;来探讨如何在鸿蒙NEXT框架下实现这一功能。此组件不仅能够统计用户输入文本中的汉字、中文标点、数字、以及英文字符的数量&#xff0c;还具有良好的用户界面设计&#xff0c;使用户能够直观地了解输入文本…

[极客大挑战 2019]BabySQL--详细解析

信息搜集 进入界面&#xff1a; 输入用户名为admin&#xff0c;密码随便输一个&#xff1a; 发现是GET传参&#xff0c;有username和password两个传参点。 我们测试一下password点位能不能注入&#xff1a; 单引号闭合报错&#xff0c;根据报错信息&#xff0c;我们可以判断…

C++《二叉搜索树》

在初阶数据结构中我学习了树基础的概念以及了解了顺序结构的二叉树——堆和链式结构二叉树该如何实现&#xff0c;那么接下来我们将进一步的学习二叉树&#xff0c;在此会先后学习到二叉搜索树、AVL树、红黑树&#xff1b;通过这些的学习将让我们更易于理解后面set、map、哈希等…

Apollo9.0源码部署(Nvidia显卡)

本文参照Apollo官方部署例程&#xff0c;进行修改。解决在部署期间遇到的网络不通、编译卡死、编译失败等问题。&#xff08;安装具有时效性&#xff0c;仅供参考&#xff09; 步骤1. 安装docker,显卡驱动、nvidia插件&#xff0c;此步骤可见专栏第一、二 节 步骤2. 拉取代…