Kotlin Flow 冷流
协程:Flow
1、Flow是什么?
- 处理异步事件流
- 可取消:通过取消协程取消Flow
- 组合操作符:复杂逻辑处理
- 缓冲和背压:发送和接收时用不同速度处理,实现流量控制、避免数据丢失
2、传统事件处理方案:同步、sequence、异步delay
// 1、同步:fun getList() = listOf(100, 200, 300, 400, 500, 600)val job = GlobalScope.launch {getList().forEach{println(it)}}job.join()// 2、异步: 在SequenceScope中,禁止自己调用挂起,除了库内部的函数yield可以挂起fun getSequcence() = sequence{for(item in 0..1000){// 不允许使用 delayyield(item) // 可以做到协程间切换}}val job2 = GlobalScope.launch {getSequcence().forEach {println(it)}}job2.join()// 3、异步: 挂起,但没有协作suspend fun getSuspendSequcence(): List<Int> {delay(1000)return listOf(100, 200, 300, 400, 500, 600)}val job3 = GlobalScope.launch {getSuspendSequcence().forEach {println(it)}}job3.join()
flow
1、flow的作用
- RxJava和Flow完全一样
- 替代LiveData
// 1、类似Observablesuspend fun getFlow() = flow{for(item in 1..8){// 发射emit(item)}}val job = GlobalScope.launch {// 2、类似RxJava消费,subscribe === Observer消费getFlow().collect{println(it)}}job.join()
2、getFlow()可以不用suspend修饰,更自由
fun getFlow() = flow{for(item in 1..8){// 发射emit(item)}}
替换LiveData
1、Flow可以完全替换LiveData ===> LiveData
// Step 1 : 网络请求
fun fetchData() = flow{for(item in 1..100){emit("get Json String = $item")}
}
// Step 2 : ViewModel抛弃LiveData使用flow
class MyViewModel: ViewModel(){val dataFlow: Flow<String> = fetchData()
}// Step 3 : 订阅,并且collect返回数据
class MyFragment: Fragment(){val viewModel: MyViewModel by viewModels()override fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)lifecycleScope.launch {viewModel.dataFlow.collect{// 在Lifecycle的CoroutineScope中,订阅冷流println(it)}}}
}
flowOn
1、Kotlin的flowOn替代了subscribeOn, 对上游进行了切换 ====> RxJava
- 不再需要observeOn,在需要的线程collect即可
fun main() = runBlocking<Unit> { // 顶级协程launch(Dispatchers.Default) {NetworkRequest.uploadRequestAction().flowOn(Dispatchers.IO) // flow运行在IO线程池,默认是main。替换了subscribeOn
// .observeOn(Dispatchers.Default) // 不再需要observeOn,在需要的线程collect即可.collect {println("$it%") // 显示下载进度}}// flowOn是给上游还是下游切换?// 都是给上游
}object NetworkRequest {fun uploadRequestAction() = flow {println("uploadRequestAction thread:${Thread.currentThread().name}")for (item in 1..100) {delay(100)emit(item) // 反馈文件上传进度}}
}
冷流
1、flow和RxJava都是冷流
发射源简化
1、简化发射源 ===> 高阶函数
- 一切对象、函数都可以
toFlow转换为flow
// 1、无参非挂起函数 toFlow
private fun<T> (()->T).toFlow() = flow{emit(invoke())
}// 使用:val r: () -> String = ::getFlowValuer.toFlow().collect { println(it) }// 2、String toFlow
//private fun String.toFlow() = flow{
// emit(this@toFlow) // this@toFlow有markdown错误
//}// 使用:"String".toFlow().collect { println(it) }// 3、无参挂起函数
private fun <OUTPUT> (suspend () -> OUTPUT).toFlow() = flow {emit(invoke())
}// 使用:::getFlowValueSuspend.toFlow().collect { println(it) }// 4、所有集合toFlow
private fun <E> Iterable<E>.toFlow() = flow {this@toFlow.forEach { emit(it) }
}// 使用:listOf(1, 2, 3, 4, 5, 6).toFlow().collect { println(it) }setOf(100, 200, 300, 400, 500, 600).toFlow().collect { println(it) }// 5、sequence的toFlow
private fun <T> Sequence<T>.toFlow() = flow {this@toFlow.forEach { emit(it) }
}// 使用sequence {yield("Derry1")yield("Derry2")yield("Derry3")}.toFlow().collect { println(it) }// 6、Array系列处理
//private fun <T> Array<T>.toFlow() = flow {
// // this@toFlow.forEach { emit(it) }
// repeat(this@toFlow.size) {
// emit(this@toFlow[it])
// }
//}
//
//private fun IntArray.toFlow() = flow {
// for (i in this@toFlow) {
// emit(i)
// }
//}
//
//private fun LongArray.toFlow() = flow {
// for (i in this@toFlow) {
// emit(i)
// }
//}// 7、Range
// 注意第4步,就已经覆盖Range的情况
private fun IntRange.toFlow() = flow {this@toFlow.forEach { emit(it) }
}private fun LongRange.toFlow() = flow {this@toFlow.forEach { emit(it) }
}
vararg和flowOf
1、可变参数实现单个数据或者多个数据都可以转为flow
private fun <T> flows(vararg value: T) = flow{value.forEach {emit(it)}
}
使用
flows("Hello").collect{ println(it) }flows(1,2,3,4,5).collect{ println(it) }
2、使用官方的flowOf
flowOf("Hello").collect{ println(it) }flowOf(1,2,3,4,5).collect{ println(it) }
withContext
1、协程中上游不可以使用withContext,只能使用flowOn
- 上下文保存机制
- 使用withContext会报错
launchIn
1、launchIn的作用
- 发射区域flowOn
- 收集区域launchIn:选择下游协程,需要用onEach打印数据
// 发射源区域
fun getFlowValue() =listOf(100, 200, 300, 400, 500, 600).asFlow().onEach { delay(2000) }.flowOn(Dispatchers.Default)
// 收集消费区域val job = getFlowValue().onEach { println("thread:${Thread.currentThread().name} $it") }.launchIn(CoroutineScope(Dispatchers.IO + CoroutineName("自定义协程"))) // 打开水龙头job.join() // 需要等待执行完成,不然外面main执行结束了。
输出结果
thread:DefaultDispatcher-worker-3 @自定义协程#2 100
thread:DefaultDispatcher-worker-1 @自定义协程#2 200
thread:DefaultDispatcher-worker-1 @自定义协程#2 300
thread:DefaultDispatcher-worker-1 @自定义协程#2 400
thread:DefaultDispatcher-worker-1 @自定义协程#2 500
thread:DefaultDispatcher-worker-1 @自定义协程#2 600
cancellable
1、协程取消,会导致Flow管道流也会取消。每次都delay 1000,可以正确检测异常
fun getFlow() = flow {(1..10).forEach { emit(it) }
}.onEach { delay(1000) }
getFlow().collect {println(it)if (it == 5) cancel()}
输出结果
1
2
3
4
5
Exception in thread "main" kotlinx.coroutines.JobCancellationException
检测
2、cancellable:取消不及时,速度太快了,增加监测机制
(1..10).asFlow().collect {println(it)if (it == 5) cancel()}// 会输出1~10,才抛出异常(1..10).asFlow().cancellable().collect {println(it)if (it == 5) cancel()}// 可以正确捕获到
背压
buffer
1、背压是什么?
- 数据产生速度 >>> 数据消费速度,消耗过多时间
- 可能OOM
// 数据过多,会导致消费不来
fun getFlow() = flow {(1..10).forEach {delay(500L)emit(it) // 一秒钟发射一个 一秒钟发射一个 ....println("生成了:$it thread:${Thread.currentThread().name}")}
}// 消费慢val t = measureTimeMillis {getFlow().collect {delay(1000L)println("消费了:$it thread:${Thread.currentThread().name}")}}println("上游 下游 共 消耗:$t 时间")// 共消耗15495ms
// 都在一个线程处理,按顺序,放一个取一个
2、buffer:设立缓冲区,减少背压的数量【解决办法一】
fun getFlow() = flow {(1..10).forEach {delay(500L)emit(it) // 一秒钟发射一个 一秒钟发射一个 ....println("生成了:$it thread:${Thread.currentThread().name}")}
}.buffer(100) // 设置缓冲区,减少 背压// 共消耗11272ms
3、flowOn(Dispatchers.IO):另一个线程处理【解决办法二】
- 可以和buffer一起使用
fun getFlow() = flow {(1..10).forEach {delay(500L)emit(it) // 一秒钟发射一个 一秒钟发射一个 ....println("生成了:$it thread:${Thread.currentThread().name}")}
}.buffer(100) // 设置缓冲区,减少 背压.flowOn(Dispatchers.IO)
// 共消耗11001ms
conflate
1、conflate作用:只消费当前认为最新的值,会丢失部分信息
val t = measureTimeMillis {getFlow().conflate().collect{delay(1000L)println("消费了:$it thread:${Thread.currentThread().name}")}}println("上游 下游 共 消耗:$t 时间")
// 共消耗7303ms
collectLatest
1、collectLatest:只收集最新值,速度大幅度提升
val t = measureTimeMillis {getFlow().collectLatest {delay(1000L)println("消费了:$it thread:${Thread.currentThread().name}")}}println("上游 下游 共 消耗:$t 时间")
// 共消耗6869ms
transform
1、transform将上游数据转换后交给下游 ====> LiveData
listOf(100, 200, 300, 400, 500, 600).asFlow().transform {this.emit("你好啊数字$it")}.collect { println(it) }
take
1、take限制发送的长度,只要前面几个
listOf(100, 200, 300, 400, 500, 600).asFlow().take(4).collect { println(it) }
2、自定义take
- 对Flow扩展,调用collect收集结果
- 用结果构造出flow
fun <INPUT> Flow<INPUT>.myTake(number:Int):Flow<INPUT>{require(number > 0){"Request element count 0 show be positive"}return flow {var i = 0collect{// collect收集的n个数据,构造了flow{}if(i++ < number){return@collect emit(it)}}}
}
reduce
末端操作符:适合累加
- reduce参数p1 = 上一次运算返回的最后一行
- 下面代码实现:1+2+3+4+…+100 = 5050
val r = (1..100).asFlow().reduce { p1, p2 ->val result = p1 + p2result}println(r)
fliter:过滤
(100..200).toFlow().filter { it % 50 == 0 }.map { "map result:$it" }.collect{ println(it) }
zip
1、zip合并Flow
fun getNames() = listOf("杜子腾", "史珍香", "刘奋").asFlow().onEach { delay(1000) }
fun getAges() = arrayOf(30, 40, 50).asFlow().onEach { delay(2000) }// 合并 组合 操作符 zipgetNames().zip(getAges()) { p1, p2 ->"name:$p1, age:$p2"}.collect {println(it)}输出:
name:杜子腾, age:30
name:史珍香, age:40
name:刘奋, age:50
2、zip合并的两个Flow数据长度不一样会怎么办?
fun getNames() = listOf("杜子腾", "史珍香", "刘奋").asFlow().onEach { delay(1000) }
fun getAges() = arrayOf(30, 40, 50, 60, 70).asFlow().onEach { delay(2000) }zip之后输出结果:会抛弃不匹配的信息60、70
name:杜子腾, age:30
name:史珍香, age:40
name:刘奋, age:50
map
转换
flatmap
1、flatMapxxx作用是展平
- 不展平相当于 Flow嵌套,如:
Flow<Flow<String>> - 需要两次收集:
collect { it.collect { a -> println(a)} }手动展平
// 不展平相当于 Flow嵌套,如:Flow<Flow<String>>
// 这里发送两次事件,属于Flow
fun runWork(inputValue:Int) = flow {emit("$inputValue 号员工开始工作了")delay(1000L)emit("$inputValue 号员工结束工作了")
}(1..6).asFlow().onEach { delay(1000L)}.map { runWork(it) } // Flow<Flow<String>> // Flow嵌套.collect { it.collect { a -> println(a)} }
// 展平 操作符 flatMapgetNumbers().onEach { delay(1000L)}
// .flatMap { } // 已经废弃.flatMapConcat { runWork(it) }// .flatMapMerge { runWork(it) }// .flatMapLatest { runWork(it) }.collect { println(it) }
2、flatMapConcat:拼接,常用
3、flatMapMerge
4、flatMapLatest
merge
1、flow合并,执行,并且获得结果
- 数据请求函数
data class Home(val info1: String, val info2: String)data class HomeRequestResponseResultData(val code: Int, val msg: String, val home: Home)// 请求本地加载首页数据
fun CoroutineScope.getHomeLocalData(userName: String) = async (Dispatchers.IO) {delay(3000)Home("数据1...", "数据1...")
}// 请求网络服务器加载首页数据
fun CoroutineScope.getHomeRemoteData(userName: String) = async (Dispatchers.IO) {delay(6000)Home("数据3...", "数据4...")
}
- map + merge,合并Flow,collect触发冷流
// 流程// 1.把多个函数 拿过来// 2.组装成协程// 3.包装成FLow// 4.Flow合并 得到 结果coroutineScope {val r = listOf(::getHomeLocalData, ::getHomeRemoteData) // 1.把多个函数 拿过来.map {it("Derry用户") //it.call("Derry用户") 需要引入Kotlin反射 2.组装成协程,调用}.map {flow { emit(it.await()) }// 3.包装成FLow}val r2 = r.merge() // 4.Flow合并 得到 结果r2.collect { println(it) }}
异常
catch
捕获上游的异常
- 用声明式
flow {listOf(100).forEach { value ->emit(value)throw KotlinNullPointerException("上游抛出了异常")}}.catch {println("e:$it")emit(200)}.onEach { delay(1000L) }.collect { println(it) }
- Flow是流式的,catch不能捕获下游的异常
onCompletion
1、Flow正常结束,声明式
getNumbers().onCompletion { println("协程Flow结束了") }.collect{println(it)}
2、onCompletion来捕获异常结束:上游和下游都可以
// 上游getNumbers2().onCompletion {if (it != null) { // 非正常结束 是异常结束println("上游 发生了异常 $it")}}.catch { println("被catch到了 上游 发生了异常 $it") } // .catch是能 捕获到 上游 抛出的异常, 异常的传递过程.collect { println(it) }
3、异常总结
- 上游的异常抛出,可以使用 声明式
- 下游的异常抛出,可以使用 命令式
- onCompletion(声明式) 上游 与 下游 的异常信息,都能够知道 能够得到
- onCompletion(声明式) 正常的结束 还是 异常的结束,都能知道
- finally 能够知道正常的结束(命令式)
相关文章:
Kotlin Flow 冷流
协程:Flow 1、Flow是什么? 处理异步事件流可取消:通过取消协程取消Flow组合操作符:复杂逻辑处理缓冲和背压:发送和接收时用不同速度处理,实现流量控制、避免数据丢失 2、传统事件处理方案:同…...
Android Socket使用TCP协议实现手机投屏
本节主要通过实战来了解Socket在TCP/IP协议中充当的是一个什么角色,有什么作用。通过Socket使用TCP协议实现局域网内手机A充当服务端,手机B充当客户端,手机B连接手机A,手机A获取屏幕数据转化为Bitmap,通过Socket传递个…...
【云原生,k8s】Helm应用包管理器介绍
目录 一、为什么需要Helm? (一)Helm介绍 (二)Helm有3个重要概念: (三)Helm特点 二、Helm V3变化 (一)架构变化 (二)自动创建名…...
两个内网之间的linux服务器如何互相登录?快解析内网穿透
如果两个内网之间的linux服务器需要互相登录,或需要互相访问内网某个端口,担忧没有公网IP,可以使用的方法有 ngrok, 但并不方便,我们只需两条 SSH 命令即可。 SSH 内网端口转发实战SSH 内网端口转发实战 先给出本文主角&…...
sql server 存储过程 set ansi_nulls set quoted_identifier,out 、output
SQL-92 标准要求在对空值(NULL) 进行等于 () 或不等于 (<>) 比较时取值为 FALSE。 当 SET ANSI_NULLS 为 ON 时,即使 column_name 中包含空值,使用 WHERE column_name NULL 的 SELECT 语句仍返回零行。即使 column_name 中包含非空值,…...
1046:判断一个数能否同时被3和5整除
【题目描述】 判断一个数n 能否同时被3和5整除,如果能同时被3和5整除输出YES,否则输出NO。 【输入】 输入一行,包含一个整数n。( -1,000,000 < n < 1,000,000) 【输出】 输出一行,如果能同时被3…...
优漫动游零基础如何学习好UI设计
智能时代的来临,很多企业都越来越注重用户体验这一块,想要有一个吸引用户的好页面,UI设计师岗位不可或缺,如今越来越多的人想要学习UI设计技术,那么对于零基础小白如何学习好UI设计呢? 零基础小白如何学习好UI设计…...
Android岗位技能实训室建设方案
一 、系统概述 Android岗位技能作为新一代信息技术的重点和促进信息消费的核心产业,已成为我国转变信息服务业的发展新热点:成为信息通信领域发展最快、市场潜力最大的业务领域。互联网尤其是移动互联网,以其巨大的信息交换能力和快速渗透能力…...
Mysql系列:Mysql5.7编译安装--系统环境:Centos7 / CentOS9 Stream
Mysql系列:Mysql5.7编译安装 系统环境:Centos7 / CentOS9 Stream 1:下载mysql源码包 https://dev.mysql.com/downloads/mysql/5.7.htmldownloads 选择MySQL Community Server>source_code>Generic Linux (Architecture Independent)…...
Docker容器与虚拟化技术:Dockerfile部署LNMP
目录 一、理论 1.LNMP架构 2.背景 3.Dockerfile部署LNMP 3.构建Nginx镜像 4.构建MySQL容器 5.构建PHP镜像 6.启动 wordpress 服务 二、实验 1.环境准备 2.构建Nginx镜像 3.构建MySQL容器 4.构建PHP镜像 5.启动 wordpress 服务 三、问题 1.构建nginx镜像报错 …...
elementUI date-picker 日期格式转为 2023/08/08格式
<el-form-item label"基线日期:" prop"baselineDate"><el-date-pickertype"date"v-model"form.baselineDate"placeholder"选择日期"format"yyyy/MM/dd"change"(date, type) > changeTime(date, …...
生成式 AI 在泛娱乐行业的应用场景实践 – 助力风格化视频内容创作
感谢大家阅读《生成式 AI 行业解决方案指南》系列博客,全系列分为 4 篇,将为大家系统地介绍生成式 AI 解决方案指南及其在电商、游戏、泛娱乐行业中的典型场景及应用实践。目录如下: 《生成式 AI 行业解决方案指南与部署指南》《生成式 AI 在…...
elementPlus——图标引入+批量注册全局组件——基础积累
因为我们要根据路由配置对应的图标,也要为了后续方便更改。因此我们将所有的图标注册为全局组件。(使用之前将分页器以及矢量图注册全局组件的自定义插件)(所有图标全局注册的方法element-plus文档中已给出) 全局注册…...
国标GB28181安防视频平台EasyGBS显示状态正常,却无法播放该如何解决?
国标GB28181视频平台EasyGBS是基于国标GB/T28181协议的行业内安防视频流媒体能力平台,可实现的视频功能包括:实时监控直播、录像、检索与回看、语音对讲、云存储、告警、平台级联等功能。国标GB28181视频监控平台部署简单、可拓展性强,支持将…...
TIOVX:opencv的Mat类图像零拷贝转为openvx的vx_image格式,通过Not节点无效果问题记录
问题描述 代码中,创建了一个opencv的Mat图像(并打印了所有的像素值),然后通过vxCreateImageFromHandle函数将Mat图像转为了vx_image图像(通过映射的方式打印了所有的像素值,通过日志可以看出与之前打印相同)。然后创建graph,将其作…...
变压器故障诊断(python代码,逻辑回归/SVM/KNN三种方法同时使用,有详细中文注释)
视频效果:变压器三种方法下故障诊断Python代码_哔哩哔哩_bilibili代码运行要求:tensorflow版本>2.4.0,Python>3.6.0即可,无需修改数据路径。 1.数据集介绍: 采集数据的设备照片 变压器在电力系统中扮演着非常重要的角色。…...
ASEMI探索整流桥GBU814的独特优势和应用领域
编辑-Z 整流桥GBU814在众多电子元件中独树一帜,可在多种设备中发挥其重要作用。作为一款集高效性能和可靠稳定性于一身的整流桥,GBU814已在全球范围内赢得了广泛的好评。在这篇文章中,我们将详细介绍GBU814整流桥的优势和应用领域。 让我们首…...
js脚本自动化之葫芦娃
什么是葫芦娃? 贵州特产平台(扶贫助农平台)有很多,但都大同小异,就连模样都像一个娘生的,所以戏称为葫芦娃平台 #小程序://航旅黔购/1nkYlNRVzm0Gg9x #小程序://贵旅优品/7zz6mtnSVgDfyqa #小程序://新联惠购/ibFdsuhWqIbczEd #小程序://贵盐黔品/u2TgExCUdkavrFe #小程…...
从零基础到精通IT:探索高效学习路径与成功案例
文章目录 导语:第一步:明确学习目标与方向选择适合的IT方向设定具体的学习目标咨询和调研 第二步:系统学习基础知识选择适合的编程语言学习数据结构和算法掌握操作系统和计算机网络基础 第三步:实践项目锻炼技能选择合适的项目编写…...
2023.8.8巨人网络数据开发工程师面试复盘
1 概述 问题一览 总体感觉良好,通过面试官的介绍可知这个岗位偏向离线数仓。 1.自我介绍 2.询问了其中一段实习经历 3.讲下你说用过的Linux命令 4.讲下HIVE的内部表和外部表有什么不同 *5.讲下你使用过的Hive函数(好好在复习下多准备几个吧)…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...
多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...
【分享】推荐一些办公小工具
1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由:大部分的转换软件需要收费,要么功能不齐全,而开会员又用不了几次浪费钱,借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...
排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...
【Redis】笔记|第8节|大厂高并发缓存架构实战与优化
缓存架构 代码结构 代码详情 功能点: 多级缓存,先查本地缓存,再查Redis,最后才查数据库热点数据重建逻辑使用分布式锁,二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
Caliper 负载(Workload)详细解析
Caliper 负载(Workload)详细解析 负载(Workload)是 Caliper 性能测试的核心部分,它定义了测试期间要执行的具体合约调用行为和交易模式。下面我将全面深入地讲解负载的各个方面。 一、负载模块基本结构 一个典型的负载模块(如 workload.js)包含以下基本结构: use strict;/…...
uniapp 集成腾讯云 IM 富媒体消息(地理位置/文件)
UniApp 集成腾讯云 IM 富媒体消息全攻略(地理位置/文件) 一、功能实现原理 腾讯云 IM 通过 消息扩展机制 支持富媒体类型,核心实现方式: 标准消息类型:直接使用 SDK 内置类型(文件、图片等)自…...
