直播技术-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) //注入任务

@UiThread

fun 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_PRIORITY

    const val HEARTBEAT = HIGH_PRIORITY - 1000L

}



object OnP0 {

    //viewModel

    const val INIT_P1 = HIGH_PRIORITY

    const val INIT_PLAYERDATA = HIGH_PRIORITY - 1000L



    //View

    const val INIT_PLAY = HIGH_PRIORITY - 2000L



}



object OnP1 {

    const val PLAYER_VIEWMODEL = HIGH_PRIORITY

    const val USER_VIEWMODEL = HIGH_PRIORITY - 1000L

    const val SOCKET_VIEWMODEL = HIGH_PRIORITY - 2000L

    const val INTERACTION_VIEWMODEL = HIGH_PRIORITY - 3000L

    const val GIFT_VIEWMODEL = HIGH_PRIORITY - 4000L

    const val BASIC_VIEWMODEL = HIGH_PRIORITY - 5000L

    const val SPPLAYER_VIEWMODEL = HIGH_PRIORITY - 6000L

    const val OPERATION_VIEWMODEL = HIGH_PRIORITY - 7000L

    const val ANIM_VIEWMODEL = HIGH_PRIORITY - 8000L

    const val SKIN_VIEWMODEL = HIGH_PRIORITY - 9000L

    const val OPERATION_VIEWMODELV3 = HIGH_PRIORITY - 10000L

    const val VOICE_VIEWMODEL = HIGH_PRIORITY - 11000L

    const val SUPERCHAT_VIEWMODEL = HIGH_PRIORITY - 12000L

    const val LPLSP_VIEWMODEL = HIGH_PRIORITY - 13000L

    const val INTERACTIONPANEL_VIEWMODEL = HIGH_PRIORITY - 14000L

    const val QUESTION_VIEWMODEL = HIGH_PRIORITY - 15000L



    //分发 其他Basic数据

    const val DISPATCH_BASIC_DATA = HIGH_PRIORITY - 16000L

    const 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: String

    get() = "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 = startTs

    assembly.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 = endTs

    assembly.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: String

    get() = "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.priority



private var isDestroyCalled = false



override fun dispatchTask(roomStatus: LiveRoomStatus) {

    if (isDestroyCalled) return

    mCurrentRoomStatus = mCurrentRoomStatus or roomStatus.priority

    mRoomTrace.recordRoomStatusStart(roomStatus, SystemClock.elapsedRealtime())

    mTasks[roomStatus]?.forEach {

        if (isDestroyCalled) return

        executeTask(it)

    }

    mRoomTrace.recordRoomStatusEnd(roomStatus, SystemClock.elapsedRealtime())

    mRoomTrace.logRoomStatus(roomStatus)

}



override fun registerTask(task: LiveRoomFlowTask) {

    if (isDestroyCalled) return

    val 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.rule

    if (rule.type == LiveRoomFlowRule.Type.PRIORITY) return

    if (rule.type == LiveRoomFlowRule.Type.ALL_TOP) {

        liveRoomFlowTask.rule.priority = HIGHEST_PRIORITY

        return

    }

    val targetTask = getIndexByTag(rule.target, queue) ?: return

    if (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.priority



private 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 = true

    mTasks.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_PRIORITY



init {

    when (type) {

        Type.ABOVE, Type.BELOW, Type.PRIORITY -> Unit

        Type.ALL_TOP -> priority = HIGHEST_PRIORITY

    }

}



enum class Type {

    ABOVE, BELOW, ALL_TOP, PRIORITY

}



companion object {

    const val ON_NONE_VAL = 0x000

    const val ON_CREATE_VAL = 0x001

    const val ON_RESUME_VAL = 0x002

    const val ON_P0_VAL = 0x004

    const val ON_P1_VAL = 0x008

    fun generatePriorityRule(priority: Long): LiveRoomFlowRule {

        return LiveRoomFlowRule().apply { this.priority = priority }

    }

}

}

//直播间流程控制管理器
interface LiveRoomFlowManager {
@UiThread
fun registerTask(task: LiveRoomFlowTask) //注入任务

@UiThread

fun 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_PRIORITY

    const val HEARTBEAT = HIGH_PRIORITY - 1000L

}



object OnP0 {

    //viewModel

    const val INIT_P1 = HIGH_PRIORITY

    const val INIT_PLAYERDATA = HIGH_PRIORITY - 1000L



    //View

    const val INIT_PLAY = HIGH_PRIORITY - 2000L



}



object OnP1 {

    const val PLAYER_VIEWMODEL = HIGH_PRIORITY

    const val USER_VIEWMODEL = HIGH_PRIORITY - 1000L

    const val SOCKET_VIEWMODEL = HIGH_PRIORITY - 2000L

    const val INTERACTION_VIEWMODEL = HIGH_PRIORITY - 3000L

    const val GIFT_VIEWMODEL = HIGH_PRIORITY - 4000L

    const val BASIC_VIEWMODEL = HIGH_PRIORITY - 5000L

    const val SPPLAYER_VIEWMODEL = HIGH_PRIORITY - 6000L

    const val OPERATION_VIEWMODEL = HIGH_PRIORITY - 7000L

    const val ANIM_VIEWMODEL = HIGH_PRIORITY - 8000L

    const val SKIN_VIEWMODEL = HIGH_PRIORITY - 9000L

    const val OPERATION_VIEWMODELV3 = HIGH_PRIORITY - 10000L

    const val VOICE_VIEWMODEL = HIGH_PRIORITY - 11000L

    const val SUPERCHAT_VIEWMODEL = HIGH_PRIORITY - 12000L

    const val LPLSP_VIEWMODEL = HIGH_PRIORITY - 13000L

    const val INTERACTIONPANEL_VIEWMODEL = HIGH_PRIORITY - 14000L

    const val QUESTION_VIEWMODEL = HIGH_PRIORITY - 15000L



    //分发 其他Basic数据

    const val DISPATCH_BASIC_DATA = HIGH_PRIORITY - 16000L

    const 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: String

    get() = "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 = startTs

    assembly.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 = endTs

    assembly.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: String

    get() = "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.priority

private var isDestroyCalled = false


override fun dispatchTask(roomStatus: LiveRoomStatus) {

    if (isDestroyCalled) return

    mCurrentRoomStatus = mCurrentRoomStatus or roomStatus.priority

    mRoomTrace.recordRoomStatusStart(roomStatus, SystemClock.elapsedRealtime())

    mTasks[roomStatus]?.forEach {

        if (isDestroyCalled) return

        executeTask(it)

    }

    mRoomTrace.recordRoomStatusEnd(roomStatus, SystemClock.elapsedRealtime())

    mRoomTrace.logRoomStatus(roomStatus)

}


override fun registerTask(task: LiveRoomFlowTask) {

    if (isDestroyCalled) return

    val 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.rule

    if (rule.type == LiveRoomFlowRule.Type.PRIORITY) return

    if (rule.type == LiveRoomFlowRule.Type.ALL_TOP) {

        liveRoomFlowTask.rule.priority = HIGHEST_PRIORITY

        return

    }

    val targetTask = getIndexByTag(rule.target, queue) ?: return

    if (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.priority



private 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 = true

    mTasks.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_PRIORITY



init {

    when (type) {

        Type.ABOVE, Type.BELOW, Type.PRIORITY -> Unit

        Type.ALL_TOP -> priority = HIGHEST_PRIORITY

    }

}


enum class Type {

    ABOVE, BELOW, ALL_TOP, PRIORITY

}


companion object {

    const val ON_NONE_VAL = 0x000

    const val ON_CREATE_VAL = 0x001

    const val ON_RESUME_VAL = 0x002

    const val ON_P0_VAL = 0x004

    const val ON_P1_VAL = 0x008

    fun 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 = 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_PRIORITY
        const val HEARTBEAT = HIGH_PRIORITY - 1000L
    }
​
    object OnP0 {
        //viewModel
        const val INIT_P1 = HIGH_PRIORITY
        const val INIT_PLAYERDATA = HIGH_PRIORITY - 1000L
​
        //View
        const val INIT_PLAY = HIGH_PRIORITY - 2000L
​
    }
​
    object OnP1 {
        const val PLAYER_VIEWMODEL = HIGH_PRIORITY
        const val USER_VIEWMODEL = HIGH_PRIORITY - 1000L
        const val SOCKET_VIEWMODEL = HIGH_PRIORITY - 2000L
        const val INTERACTION_VIEWMODEL = HIGH_PRIORITY - 3000L
        const val GIFT_VIEWMODEL = HIGH_PRIORITY - 4000L
        const val BASIC_VIEWMODEL = HIGH_PRIORITY - 5000L
        const val SPPLAYER_VIEWMODEL = HIGH_PRIORITY - 6000L
        const val OPERATION_VIEWMODEL = HIGH_PRIORITY - 7000L
        const val ANIM_VIEWMODEL = HIGH_PRIORITY - 8000L
        const val SKIN_VIEWMODEL = HIGH_PRIORITY - 9000L
        const val OPERATION_VIEWMODELV3 = HIGH_PRIORITY - 10000L
        const val VOICE_VIEWMODEL = HIGH_PRIORITY - 11000L
        const val SUPERCHAT_VIEWMODEL = HIGH_PRIORITY - 12000L
        const val LPLSP_VIEWMODEL = HIGH_PRIORITY - 13000L
        const val INTERACTIONPANEL_VIEWMODEL = HIGH_PRIORITY - 14000L
        const val QUESTION_VIEWMODEL = HIGH_PRIORITY - 15000L
​
        //分发 其他Basic数据
        const val DISPATCH_BASIC_DATA = HIGH_PRIORITY - 16000L
        const 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: LiveRoomStatus
        get() = 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: String
            get() = tag
        override val rule: LiveRoomFlowRule
            get() = LiveRoomFlowRule.generatePriorityRule(priority)
​
        override val isSticky: Boolean
            get() = isSticky
    }
}
//最后通过registerTask方法注入task

fun 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) {
    // 用于主线程通信的RxData
    val mainRxBus by lazy(LazyThreadSafetyMode.NONE) {
        RxBus.newInstance()
    }

    // 用于子线程通信的RxData
    val 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] = threadType
        when (threadType) {
            ThreadType.MAIN -> mainRxBus.registerNoinline(clazz).subscribe({
                ...
            })
            ThreadType.SERIALIZED -> serializedRxBus.registerNoinline(clazz)
                    .onBackpressureDrop {
                    ...
        }
    }

    fun cleanUp() {
        mainRxBus.cleanUp()
        serializedRxBus.cleanUp()
    }
	...
}

interface Event

enum 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? = null
    private 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: Int
        get() = TODO("需要加载的XML")
 
    override val tag: String
        get() = TODO("TAG")
 
    override val priority: LiveRoomViewPriority
        get() = TODO("优先级")
 
    override val defaultLayoutParams: LiveRoomViewLayoutParams
        get() = TODO("层级布局")
 
    override val supportScreenMode: Int
        get() = TODO("支持的屏幕类型")
 
    /**
     * 默认View在 BUSINESS层 如果要改变所属的容器层,需要重写此方法
     */
    override fun getScope() = HierarchyScope.BUSINESS
 
    override 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的请求、共享数据变化(其他业务需要的数据)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

『 Linux 』数据链路层 - MAC帧/以太网帧

文章目录 MAC帧/以太网帧局域网的通信原理 MAC帧/以太网帧 MAC帧也叫做以太网帧,通常情况下MAC帧也是一个更广义的语术,用来描述数据链路层,即OSI模型的第二层的一种数据帧格式,这种格式包括其他如WI-FI,令牌环,帧中继等数据链路层所使用的数据帧; 以太网帧是具体使用的一种MAC…

LightRAG开源了…结合本地ollama实现股票数据接口Akshare智能问答

LightRAG是由香港大学研究团队推出的一种检索增强生成&#xff08;Retrieval-Augmented Generation, RAG&#xff09;系统。该系统通过整合图结构索引和双层检索机制&#xff0c;显著提升了大型语言模型在信息检索中的准确性和效率。LightRAG 不仅能够捕捉实体间的复杂依赖关系…

LabVIEW引用类型转换问题

一、问题描述 在LabVIEW中&#xff0c;refnum&#xff08;引用编号&#xff09;用于引用各种资源&#xff0c;如文件、队列、控件等。这些引用是与具体类型相关的&#xff0c;通常情况下&#xff0c;LabVIEW会根据引用的类型自动进行处理。然而&#xff0c;当不同类型的引用需…

Redis五大基本类型——Set集合命令详解(命令用法详解+思维导图详解)

目录 一、Set集合类型介绍 二、常见命令 1、SADD 2、SMEMBERS 3、SISMEMBER 4、SCARD 5、SRANDMEMBER 6、SPOP 7、SMOVE 8、SREM ​编辑 9、集合间操作 &#xff08;1&#xff09;SINTER &#xff08;2&#xff09;SINTERSTORE &#xff08;3&#xff09;SUNION…

HTMLCSS:彩色灵动气泡效果

效果演示 这段代码是一个HTML文档&#xff0c;包含了内联的CSS样式&#xff0c;用于创建一个具有动画效果的网页背景&#xff0c;其中包含多个彩色浮动的气泡元素。 HTML <div class"container"><div class"bubble"><span></spa…

[工具分享] 根据Excel数据根据Word文档模板,批量创建生成Word文档并重命名,方便快速查找打印

前几天交楼的小姐姐要多份Word文档合同打印给客户&#xff0c;那么100份就需要修改100次 上面好多都是模板的制式文件&#xff0c;里面的部分数据都是要根据实际值来变动的&#xff0c; 那么有没有快速的方法来操作呢&#xff0c;还是只能一个个手动的改&#xff0c;又容易出…

《硬件架构的艺术》笔记(五):低功耗设计

介绍 能量以热量形式消耗&#xff0c;温度升高芯片失效率也会增加&#xff0c;增加散热片或风扇会增加整体重量和成本&#xff0c;在SoC级别对功耗进行控制就可以减少甚至可能消除掉这些开支&#xff0c;产品也更小更便宜更可靠。本章描述了减少动态功耗和静态功耗的各种技术。…

【Linux学习】【Ubuntu入门】2-3 make工具和makefile引入

1.使用命令新建三个.c文件vi main.c&#xff0c;vi input.c&#xff0c;vi caclcu.c&#xff0c;两个.h文件vi input.h&#xff0c;vi caclcu.h 2.vi Makefile&#xff1a;新建Makefile文件&#xff0c;输入一下内容 注意&#xff1a;命令列表中每条命令前用TAB键&#xff0c;不…

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

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

Otter 安装流程

优质博文&#xff1a;IT-BLOG-CN 一、背景 随着公司的发展&#xff0c;订单库的数据目前已达到千万级别&#xff0c;需要进行分表分库&#xff0c;就需要对数据进行迁移&#xff0c;我们使用了otter&#xff0c;这里简单整理下&#xff0c;otter 的安装过程&#xff0c;希望对…

#Java-常用API-BigInteger、BigDecima、正则表达式

1.BigInteger BigInteger可以表示非常大范围的整数&#xff0c;理论上来说无限大 a.构造方法 构造方法说明public BigInteger(int num, Random rnd)获取随机大整数,范围 : [0 ~ 2的num次方 - 1]public BigInteger(String val)获取指定的大整数public BigInteger(String val,…

C++设计模式-中介者模式

动机(Motivation) 多个对象相互关联的情况&#xff0c;对象之间常常会维持一种复杂的引用关系&#xff0c;如果遇到一些需求的更改&#xff0c;这种直接的引用关系将面临不断的变化。在这种情况下&#xff0c;可以使用一种”中介对象“来管理对象间的关联关系&#xff0c;避免…

cursor 使用经验分享

cursor 是一款基于 VSCode 的 AI 优先编辑器&#xff0c;如果你还没有体验过它&#xff0c;我强烈建议看完这篇&#xff0c;因为 cursor 真的在改变编程游戏的规则&#xff0c;在了解 cursor 前&#xff0c;可以先思考一下我们日常在编程环节都做哪些事情&#xff0c;我日常开发…

Linux系统编程之进程基础知识

概述 在Linux系统中&#xff0c;进程是指一个正在运行的程序实例。每个进程都有一个唯一的进程标识符&#xff0c;即PID&#xff0c;操作系统通过这个PID来唯一识别和管理各个进程。进程不仅仅是程序代码的运行实例&#xff0c;它还包含了程序运行时所需的各种资源&#xff0c;…

LabVIEW发动机热磨合试验台

在汽车发动机的研发和质量控制中&#xff0c;发动机热磨合试验是关键环节。它能够检验发动机在实际运行条件下的性能&#xff0c;及时发现异响、振动、漏油等潜在问题。通过搭建基于LabVIEW的高效测试平台&#xff0c;可以显著提高发动机的可靠性和使用寿命。下面介绍LabVIEW开…

【C++】C++11新特性详解:可变参数模板与emplace系列的应用

C语法相关知识点可以通过点击以下链接进行学习一起加油&#xff01;命名空间缺省参数与函数重载C相关特性类和对象-上篇类和对象-中篇类和对象-下篇日期类C/C内存管理模板初阶String使用String模拟实现Vector使用及其模拟实现List使用及其模拟实现容器适配器Stack与QueuePriori…

js+jquery实现经典推箱子游戏

纯前端项目&#xff0c;只使用html,css,js,jquery实现经典推箱子游戏&#xff0c;直接下载本地双击index.html即可运行体验。 游戏展示 开始界面 完成游戏 代码展示

【Pytest+Yaml+Allure】实现接口自动化测试框架

一、框架思想 requestsyamlpytestallure实现接口自动化框架。结合数据驱动和分层思想&#xff0c;将代码与数据分离&#xff0c;易维护&#xff0c;易上手。使用yaml编写编写测试用例&#xff0c;利用requests库发送请求&#xff0c;使用pytest管理用例&#xff0c;allure生成…

[HarmonyOS] 解决HMRouter路由地址无法抽取的问题

解决HMRouter路由地址无法抽取的问题 背景 最近开始学习HarmonyOS开发&#xff0c;搭建项目的时候采用了 HMRouter 路由框架&#xff0c;在项目里使用到路由跳转&#xff0c;官方链接在这&#xff1a; https://gitee.com/hadss/hmrouter/blob/master/HMRouterLibrary/README…

ElasticSearch学习了解笔记

搜索引擎的原理&#xff1a; 1、查询分析&#xff08;自然语言处理&#xff09;理解用户需求 2、分词技术 3、关键词搜索匹配 4、搜索排序 lucence Lucene 是一个成熟的权威检索库 Elasticsearch 的搜索原理简单过程是&#xff0c;索引系统通过扫描文章中的每一个词&#xff…