破解 Kotlin 协程(9) - Channel 篇

安卓原生开发为何是初创企业的生存基础设施 安卓原生开发是移动应用开发的基础技术路径,指基于Android SDK、直接调用系统API构建高性能、高可控性客户端的能力。其核心原理在于绕过Web容器或跨端中间层,实现对硬件传感器、后台服务、离线存储及厂商推送等底层能力的稳定访问。相比H5、小程序或跨平台方案,原生开发在弱网适配、消息送达率、启动性能和隐私合规等方面具备不可替代的技术价值。典型应用场景包括社区团购实时订单同步、智能硬件蓝牙联动、车载Android Auto集成、金融类App安全保活等强交互、低延迟、高可靠性需求领域。对于资源有限的初创企业 阅读详情

在这里插入图片描述

1. 认识 Channel

Channel 实际上就是一个队列,而且是并发安全的,它可以用来连接协程,实现不同协程的通信。废话不多说,直接看例子:

suspend fun main() {
    val channel = Channel<Int>()

    val producer = GlobalScope.launch {
        var i = 0
        while (true){
            channel.send(i++)
            delay(1000)
        }
    }

    val consumer = GlobalScope.launch {
        while(true){
            val element = channel.receive()
            Logger.debug(element)
        }
    }

    producer.join()
    consumer.join()
}

我们构造了两个协程,分别叫他们 producer 和 consumer,我们没有明确的指定调度器,所以他们的调度器都是默认的,在 Java 虚拟机上就是那个大家都很熟悉的线程池:他们可以运行在不同的线程上,当然也可以运行在同一个线程上。

例子的运行机制是,producer 当中每隔 1s 向 Channel 中发送一个数字,而 consumer 那边则是一直在读取 Channel 来获取这个数字并打印,我们能够发现这里发端是比收端慢的,在没有值可以读到的时候,receive 是挂起的,直到有新元素 send 过来——所以你知道了 receive 是一个挂起函数,那么 send 呢?

2. Channel 的容量

如果你自己去 IDE 写了这段代码,你会发现 send 也是挂起函数。额,发端为什么会挂起?想想我们以前熟知的 BlockingQueue,我们往里面添加元素的时候,元素在队列里实际上是占用了空间的,如果这个队列空间不足,那么再往里面添加的时候就是两种情况:1. 阻塞,等待队列腾出空间;2. 抛异常,拒绝添加元素。send 也会面临同样的问题,我们说 Channel 实际上就是一个队列嘛,队列不应该有缓冲区吗,那么这个缓冲区一旦满了,并且也一直没有人调用 receive 取走元素的话,send 不就挂起了嘛。那么接下来我们看下 Channel 的缓冲区的定义:

public fun <E> Channel(capacity: Int = RENDEZVOUS): Channel<E> =
    when (capacity) {
        RENDEZVOUS -> RendezvousChannel()
        UNLIMITED -> LinkedListChannel()
        CONFLATED -> ConflatedChannel()
        else -> ArrayChannel(capacity)
    }

我们构造 Channel 的时候调用了一个叫 Channel 的函数,hmm,这玩意儿确实不是它的构造器,在 Kotlin 当中我们可以随便定义一个顶级函数跟某些类名一样来伪装成构造器,这本质上就是个工厂方法。

类似的还有 String,不信你去试试

它有一个参数叫 capacity,指定缓冲区的容量,默认值 RENDEZVOUS 就是 0,这个词本意就是描述“不见不散”的场景,所以你不来 receive,我这 send 就一直搁这儿挂起等着。换句话说,我们开头的例子里面,如果 consumer 不 receive,producer 里面的第一个 send 就给挂起了:

val producer = GlobalScope.launch {
    var i = 0
    while (true){
        i++ //为了方便输出日志,我们将自增放到前面
        Logger.debug("before send $i")
        channel.send(i)
        Logger.debug("before after $i")
        delay(1000)
    }
}

val consumer = GlobalScope.launch {
    while(true){
        delay(2000) //receive 之前延迟 2s
        val element = channel.receive()
        Logger.debug(element)
    }
}

我们故意让收端的节奏放慢,你就会发现,send 总是会挂起,直到 receive 之后才会继续往下执行:

07:11:23:119 [DefaultDispatcher-worker-2 @coroutine#1]  before send 1
07:11:24:845 [DefaultDispatcher-worker-2 @coroutine#2]  1
07:11:24:846 [DefaultDispatcher-worker-2 @coroutine#1]  before after 1
07:11:25:849 [DefaultDispatcher-worker-4 @coroutine#1]  before send 2
07:11:26:850 [DefaultDispatcher-worker-2 @coroutine#2]  2
07:11:26:850 [DefaultDispatcher-worker-3 @coroutine#1]  before after 2

UNLIMITED 比较好理解,来者不拒,从它给出的实现 LinkedListChannel 来看,这一点也与我们的 LinkedBlockingQueue 有异曲同工之妙。

CONFLATED,这个词是合并的意思,跟 inflate 是同一个词根,con- 前缀表示反着来,那是不是说我发了个 1、2、3、4、5 那边收的时候就会收到一个 [1,2,3,4,5] 的集合呢?毕竟字面意思是合并嘛。但实际上这个的效果是只保留最后一个元素,不是合并,应该是置换,换句话说,这个类型的 Channel 有一个元素大小的缓冲区,但每次有新元素过来,都会用新的替换旧的,也就是说我发了个 1、2、3、4、5 之后收端才接收的话,就只能收到 5 了。

剩下的就是 ArrayChannel 了,它接收一个值作为缓冲区容量的大小,这也比较类似于 ArrayBlockingQueue。

3. 迭代 Channel

前面我们在发送和读取 Channel 的时候用了 while(true),因为我们想要去不断的进行读写操作, Channel 本身实际上也有点儿像序列,可以一个一个读,所以我们在读取的时候也可以直接获取一个 Channel 的 iterator:

val consumer = GlobalScope.launch {
    val iterator = channel.iterator()
    while(iterator.hasNext()){ // 挂起点
        val element = iterator.next()
        Logger.debug(element)
        delay(2000)
    }
}

那么这个时候,iterator.hasNext() 是挂起函数,在判断是否有下一个元素的时候实际上就需要去 Channel 当中读取元素了。

这个写法自然可以简化成 for each:

val consumer = GlobalScope.launch {
    for (element in channel) {
        Logger.debug(element)
        delay(2000)
    }
}

4. produce 和 actor

前面我们在协程外部定义 Channel,并在协程当中访问它,实现了一个简单的生产-消费者的示例,那么有没有便捷的办法构造生产者 和消费者呢?

val receiveChannel: ReceiveChannel<Int> = GlobalScope.produce {
    while(true){
        delay(1000)
        send(2)
    }
}

我们可以通过 produce 这个方法启动一个生产者协程,并返回一个 ReceiveChannel,其他协程就可以拿着这个 Channel 来接收数据了。反过来,我们可以用 actor 启动一个消费者协程:

val sendChannel: SendChannel<Int> = GlobalScope.actor<Int> {
    while(true){
        val element = receive()
    }
}

ReceiveChannel 和 SendChannel 都是 Channel 的父接口,前者定义了 receive,后者定义了 send,Channel 也因此既可以 receive 又可以 send。

produce 和 actor 与 launch 一样都被称作“协程启动器”。通过这两个协程的启动器启动的协程也自然的与返回的 Channel 绑定到了一起,因此 Channel 的关闭也会在协程结束时自动完成,以 produce 为例,它构造出了一个 ProducerCoroutine 的对象:

internal open class ProducerCoroutine<E>(
    parentContext: CoroutineContext, channel: Channel<E>
) : ChannelCoroutine<E>(parentContext, channel, active = true), ProducerScope<E> {
    ...
    override fun onCompleted(value: Unit) {
        _channel.close() // 协程完成时
    }

    override fun onCancelled(cause: Throwable, handled: Boolean) {
        val processed = _channel.close(cause) // 协程取消时
        if (!processed && !handled) handleCoroutineException(context, cause)
    }
}

注意到在协程完成和取消的方法调用中,对应的 _channel 都会被关闭。

这样看上去还是挺有用的。不过截止目前这俩 API produce 和 actor 都没有稳定,前者仍被标记为 ExperimentalCoroutinesApi,后者则标记为 ObsoleteCoroutinesApi,这就比较尴尬了,明摆着不让用嘛。actor 的文档中提到的 issue 的讨论也说明相比基于 Actor 模型的并发框架,Kotlin 协程提供的这个 actor API 也不过就是提供了一个 SendChannel 的返回值而已。当然,协程的负责人也有实现一套更复杂的 Actor 的想法,只是这一段时间的高优明显是 Flow——这货从协程框架的 v1.2 开始公测,到协程 v1.3 就稳定,真是神速,我们后面的文章会介绍它。

虽然 produce 没有被标记为 ObsoleteCoroutinesApi,显然它作为 actor 的另一半,不可能单独转正的,这俩 API 我的建议是看看就好了。

5. Channel 的关闭

前我们提到了 produce 和 actor 返回的 Channel 都会伴随着对应的协程执行完毕而关闭。哦,原来 Channel 还有一个关闭的概念。

Channel 和我们后面的文章即将要探讨的 Flow 不同,它是在线的,是一个热数据源,换句话说就是有想要收数据,就要有人在对面给他发,就像发微信一样。既然这样,就难免曲终人散,对于一个 Channel,如果我们调用了它的 close,它会立即停止接受新元素,也就是说这时候它的 isClosedForSend 会立即返回 true,而由于 Channel 缓冲区的存在,这时候可能还有一些元素没有被处理完,所以要等所有的元素都被读取之后 isClosedForReceive 才会返回 true。

val channel = Channel<Int>(3)

val producer = GlobalScope.launch {
    List(5){
        channel.send(it)
        Logger.debug("send $it")
    }
    channel.close()
    Logger.debug("close channel. ClosedForSend = ${channel.isClosedForSend} ClosedForReceive = ${channel.isClosedForReceive}")
}

val consumer = GlobalScope.launch {
    for (element in channel) {
        Logger.debug("receive: $element")
        delay(1000)
    }

    Logger.debug("After Consuming. ClosedForSend = ${channel.isClosedForSend} ClosedForReceive = ${channel.isClosedForReceive}")
}

我们把例子稍作修改,开了一个缓冲区大小为 3 的 Channel,在 producer 协程里面快速的发送元素出去,发送5个之后关闭 Channel,而在 consumer 协程当中每秒读取一个, 结果如下:

11:05:20:678 [DefaultDispatcher-worker-1]  send 0
11:05:20:678 [DefaultDispatcher-worker-3]  receive: 0
11:05:20:678 [DefaultDispatcher-worker-1]  send 1
11:05:20:678 [DefaultDispatcher-worker-1]  send 2
11:05:20:678 [DefaultDispatcher-worker-1]  send 3
11:05:21:688 [DefaultDispatcher-worker-3]  receive: 1
11:05:21:688 [DefaultDispatcher-worker-3]  send 4
11:05:21:689 [DefaultDispatcher-worker-3]  close channel. ClosedForSend =true ClosedForReceive = false
11:05:22:693 [DefaultDispatcher-worker-3]  receive: 2
11:05:23:694 [DefaultDispatcher-worker-4]  receive: 3
11:05:24:698 [DefaultDispatcher-worker-4]  receive: 4
11:05:25:700 [DefaultDispatcher-worker-4]  After Consuming. ClosedForSend =true ClosedForReceive = true

下面我们来探讨下 Channel 关闭的意义。

一说起关闭,我们就容易想到 IO,如果不关闭可能造成资源泄露,那么 Channel 的关闭是个什么概念呢?我们前面提到过, Channel 其实内部的资源就是个缓冲区,这个东西本质上就是个线性表,就是一块儿内存,所以如果我们开了一个 Channel 而不去关闭它,其实也不会造成什么资源泄露,发端如果自己已经发完,它就可以不理会这个 Channel 了。嗯,看上去好像没什么问题是吧?

But,这时候在接收端就比较尴尬了,它不知道会不会有数据发过来,如果 Channel 是微信,那么接收端打开微信的窗口可能一直看到的是『对方正在输入』,然后它就一直这样了,孤独终老。所以这里的关闭更多像是一种约定:

女:咱俩没戏,你别傻等了。 男:哦。(您的消息未发送成功)

那么 Channel 的关闭究竟应该有谁来处理呢?正常的通信,如果是单向的,就好比领导讲话,讲完都会说『我讲完了』,你不能在领导还没讲完的时候就说『我听完了』,所以单向通信的情况比较推荐由发端处理关闭;而对于双向通信的情况,就要考虑协商了,双向通信从技术上两端是对等的,但业务场景下通常来说不是,建议由主导的一方处理关闭。

还有一些复杂的情况,前面我们看到的例子都是一对一的收发,其实还有一对多,多对多的情况,这种也仍然存在主导一方, Channel 的生命周期最好由主导方来维护。官方文档给出的扇入(fan-in)和扇出(fan-out),其实就是这种情况。

扇入和扇出的概念可能大家不是很熟悉,网上的说法不是很通俗,大家就想象它是一把折扇,折扇的边射向圆心就是扇入,这种情况圆心如果是通信的一端,那它就是接收方,如果是一个函数,那它就是被调用方。扇入越大,说明模块的复用程度越高,以函数为例,如果一个函数被调用的次数越多,那说明复用的程度越高。扇出就是反过来的情况,描述的是复杂度高的情形,例如一个 Model,负责调用网络模块、数据库、文件等很多模块。

6. BroadcastChannel

前面提到了一对多的情形,从数据处理的本身来讲,虽然有多个接收端,同一个元素只会被一个接收端读到。广播则不然,多个接收端不存在互斥行为。

直接创建 broadcastChannel 的方法跟普通的 Channel 似乎也没什么太多的不一样:

val broadcastChannel = broadcastChannel<Int>(5)

如果要订阅,那么只需要调用:

val receiveChannel = broadcastChannel.openSubscription()

这样我们就得到了一个 ReceiveChannel,获取订阅的消息,只需要调用它的 receive。

我们看一个完整一点儿的例子,例子中我们在发端发送 1 - 5,并启动 3 个协程同时接收广播:

val producer = GlobalScope.launch {
    List(5) {
        broadcastChannel.send(it)
        Logger.debug("send $it")
    }
    channel.close()
}

List(3) { index ->
    GlobalScope.launch {
        val receiveChannel = broadcast.openSubscription()
        for (element in receiveChannel) {
            Logger.debug("[$index] receive: $element")
            delay(1000)
        }
    }
}.forEach { it.join() }

producer.join()

输出结果如下:

12:34:59:656 [DefaultDispatcher-worker-6]  [2] receive: 0
12:34:59:656 [DefaultDispatcher-worker-3]  [1] receive: 0
12:34:59:656 [DefaultDispatcher-worker-5]  [0] receive: 0
12:34:59:656 [DefaultDispatcher-worker-7]  send 0
12:34:59:657 [DefaultDispatcher-worker-7]  send 1
12:34:59:658 [DefaultDispatcher-worker-7]  send 2
12:35:00:664 [DefaultDispatcher-worker-3]  [0] receive: 1
12:35:00:664 [DefaultDispatcher-worker-5]  [1] receive: 1
12:35:00:664 [DefaultDispatcher-worker-6]  [2] receive: 1
12:35:00:664 [DefaultDispatcher-worker-8]  send 3
12:35:01:669 [DefaultDispatcher-worker-8]  [0] receive: 2
12:35:01:669 [DefaultDispatcher-worker-3]  [1] receive: 2
12:35:01:669 [DefaultDispatcher-worker-6]  [2] receive: 2
12:35:01:669 [DefaultDispatcher-worker-8]  send 4
12:35:02:674 [DefaultDispatcher-worker-8]  [0] receive: 3
12:35:02:674 [DefaultDispatcher-worker-7]  [1] receive: 3
12:35:02:675 [DefaultDispatcher-worker-3]  [2] receive: 3
12:35:03:678 [DefaultDispatcher-worker-8]  [1] receive: 4
12:35:03:678 [DefaultDispatcher-worker-3]  [0] receive: 4
12:35:03:678 [DefaultDispatcher-worker-1]  [2] receive: 4

这里请大家重点关注每一个收端协程都可以读取到每一个元素。

日志顺序不能非常直观的反映数据的读写顺序,如果大家自己再次运行,顺序上可能也有出入。

除了直接创建以外,我们也可以直接用前面定义的普通的 Channel 来做个转换:

val channel = Channel<Int>()
val broadcast = channel.broadcast(3)

其中,参数表示缓冲区的大小。

实际上这里得到的这个 broadcastChannel 可以认为与原 Channel 是级联关系,这个扩展方法的源码其实很清晰的为我们展示了这一点:

fun <E> ReceiveChannel<E>.broadcast(
    capacity: Int = 1,
    start: CoroutineStart = CoroutineStart.LAZY
): broadcastChannel<E> =
    GlobalScope.broadcast(Dispatchers.Unconfined, capacity = capacity, start = start, onCompletion = consumes()) {
        for (e in this@broadcast) {  //这实际上就是在读取原 Channel
            send(e)
        }
    }

哦~原来对于 BroadcastChannel,官方也提供类似于 produce 和 actor 的方式,我们可以通过 CoroutineScope.broadcast 来直接启动一个协程,并返回一个 BroadcastChannel。

需要注意的是,从原始的 Channel 转换到 BroadcastChannel 其实就是对原 Channel 的一个读取操作,如果还有其他协程也在读这个原始的 Channel,那么会与 BroadcastChannel 产生互斥关系。

另外, BroadcastChannel 相关的 API 大部分被标记为 ExperimentalCoroutinesApi,后续也许还会有调整。

7. Channel 版本的序列生成器

前面的文章我们讲到过 Sequence,它的生成器是基于标准库的协程的 API 实现的,实际上 Channel 本身也可以用来生成序列,例如:

val channel = GlobalScope.produce(Dispatchers.Unconfined) {
    Logger.debug("A")
    send(1)
    Logger.debug("B")
    send(2)
    Logger.debug("Done")
}

for (item in channel) {
    Logger.debug("Got $item")
}

有了前面的基础这个就很容易看懂了, produce 创建的协程返回了一个缓冲区大小为 0 的 Channel,为了问题描述起来比较容易,我们传入了一个 Dispatchers.Unconfined 调度器,意味着协程会立即在当前协程执行到第一个挂起点,所以会立即输出 A 并在 send(1) 处挂起,直到后面的 for 循环读到第一个值时,实际上就是 channel 的 iterator 的 hasNext 方法的调用,这个 hasNext 方法会检查是否有下一个元素,是一个挂起函数,在检查的过程中就会让前面启动的协程从 send(1) 挂起的位置继续执行,因此会看到日志 B 输出,然后再挂起到 send(2) 这里,这时候 hasNext 结束挂起,for 循环终于输出第一个元素,依次类推。输出结果如下:

22:33:56:073 [main @coroutine#1]  A
22:33:56:172 [main @coroutine#1]  B
22:33:56:173 [main]  Got 1
22:33:56:173 [main @coroutine#1]  Done
22:33:56:176 [main]  Got 2

我们看到 B 居然比 Got1 先输出,同样, Done 也比 Got2 先输出,这个看上去比较不符合直觉,不过挂起恢复的执行顺序确实如此,关键点就是我们前面提到的 hasNext 方法会挂起并触发了协程内部从挂起点继续执行的操作。如果你选择了其他调度器,当然也会有其他合理的结果输出。不管怎么样,我们体验了一把用 Channel 模拟 sequence。如果类似的代码换作 sequence,是这样的:

val sequence = sequence {
    Logger.debug("A")
    yield(1)
    Logger.debug("B")
    yield(2)
    Logger.debug("Done")
}

Logger.debug("before sequence")

for (item in sequence) {
    Logger.debug("Got $item")
}

sequence 的执行顺序要直观的多,它没有调度器的概念,而且 sequence 的 iterator 的 hasNext 和 next 都不是挂起函数,在 hasNext 的时候同样会触发元素的查找,这时候就会触发 sequence 内部逻辑的执行,因此这次实际上是先触发了 hasNext 才会输出 A, yield 把 1 传出来作为 sequence 的第一个元素,这样就会有 Got 1 这样的输出,完整输出如下:

22:33:55:600 [main]  A
22:33:55:603 [main]  Got 1
22:33:55:604 [main]  B
22:33:55:604 [main]  Got 2
22:33:55:604 [main]  Done

sequence 本质上就是基于标准库的协程 API 实现的,没有上层协程框架的作用域以及 Job 这样的概念。

所以我们可以在 Channel 的例子里面切换不同的调度器来生成元素,例如:

val channel = GlobalScope.produce(Dispatchers.Unconfined) {
    Logger.debug(1)
    send(1)
    withContext(Dispatchers.IO){
        Logger.debug(2)
        send(2)
    }
    Logger.debug("Done")
}

sequence 就不行了。

当然,单纯的用 Channel 当做序列生成器来使用有点儿小题大做,这里更多的是告诉大家存在这样的可能性,大家在将来遇到合适的场景时,就可以灵活运用了。

8. Channel 的内部结构

前面我们提到 sequence 无法享受更上层的协程框架概念下的各种能力,还有一点 sequence 显然不是线程安全的,而 Channel 可以在并发场景下使用。

Channel 内部结构我们主要说下缓冲区分别是链表和数组的版本。链表版本的定义主要是在 AbstractSendChannel 当中:

internal abstract class AbstractSendChannel<E> : SendChannel<E> {
    protected val queue = LockFreeLinkedListHead()
    ...    
}

LockFreeLinkedListHead 本身其实就是一个双向链表的节点,实际上 Channel 把它首尾相连成为了循环链表,而这个 queque 就是哨兵(sentinel)节点。有新的元素添加时,就在 queue 的前面插入,实际上就相当于在整个队列的最后插入元素了。

它所谓的 LockFree 在 Java 虚拟机上其实是通过原子读写来实现的, 对于链表来说,需要修改的无非就是前后节点的引用:

public actual open class LockFreeLinkedListNode {
    private val _next = atomic<Any>(this) // Node | Removed | OpDescriptor
    private val _prev = atomic<Any>(this) // Node | Removed
    ...   
}

它的实现基于一篇论文中提到的无锁链表的实现,由于 CAS 原子操作通常只能修改一个引用,对于需要原子同时修改前后节点引用的情形是不适用的,例如单链表插入节点时需要修改两个引用,分别是操作节点的前一个节点的 next 和自己的 next,即 Head -> A -> B -> C 在 A 、B 之间插件 X 时会需要先修改 X -> B 再修改 A -> X,如果这个过程中 A 被删除,那么可能的结果是 X 一并被删除,得到的链表是 Head -> B -> C。

这个无锁链表的实现通过引入 prev 来辅助解决这个问题,即在 A 被删除的问题发生的同时,其实我们是可以做到 X.next = B,X.prev = A 的,这时候判断如果 A 已经被移除了,那么 B.prev 本来是 A,结果就变成了 Head,这时候就可以将 X.prev 再次赋值为 B.prev 来修复,当然这个过程稍稍有些复杂,有兴趣的同学也可以参考 LockFreeLinkedListNode 在 Jvm 上的实现。

而对于数组版本, ArrayChannel 就相对粗暴了,内部就是一个数组:

//如果缓冲区大小大于 8,会先分配大小为 8 的数组,在后续进行扩容
private var buffer: Array<Any?> = arrayOfNulls<Any?>(min(capacity, 8))

对这个数组读写时则直接用了一个 ReentrantLock 进行加锁。

这里是不是有优化的空间呢?其实对于数组的元素,我们同样可以进行 CAS 读写,如果大家有兴趣,可以参考下 ConcurrentHashMap 的实现,JDK 7 的实现中对于段数组的读写采用了 UnSafe 的 CAS 读写,JDK 1.8 直接干掉了分段,对于桶的读写也采用了 UnSafe 的 CAS。

协程在 Js 和 Native 上的实现就要简单得多,因为它们的协程都只是在单线程上运行,基本不需要处理并发问题。

9. 小结

Channel 的出现,应该说为协程注入了灵魂。每一个独立的协程不再是孤独的个体, Channel 可以让他们更加方便的协作起来。实际上 Channel 的概念并不是 Kotlin 原创的,且不说 Golang 里面的 channel,就说 Java NIO 当中也存在 Channel 这样的概念,其实这时候大家很容易就应该想到多路复用,多路复用的时候我们还能像前面那样简单的挂起吗?或者不挂起我们该怎么办呢?且看下回分解。

Kotlin 协程学习资料可以扫描下方二维码免费领取!

《史上最详Android版kotlin协程入门进阶实战》

第一章 Kotlin协程的基础介绍

            ● 协程是什么

            ● 什么是Job 、Deferred 、协程作用域

            ● Kotlin协程的基础用法

img

第二章 kotlin协程的关键知识点初步讲解

            ● 协程调度器

            ● 协程上下文

            ● 协程启动模式

            ● 协程作用域

            ● 挂起函数

img

第三章 kotlin协程的异常处理

            ● 协程异常的产生流程

            ● 协程的异常处理

img

第四章 kotlin协程在Android中的基础应用

            ● Android使用kotlin协程

            ● 在Activity与Framgent中使用协程

            ● ViewModel中使用协程

            ● 其他环境下使用协程

img

第五章 kotlin协程的网络请求封装

            ● 协程的常用环境

            ● 协程在网络请求下的封装及使用

            ● 高阶函数方式

            ● 多状态函数返回值方式
Python逆向央视频Ckey算法:实现直播流自动下载的完整实战 在移动应用安全与数据交互领域,参数签名与身份验证是保障通信安全的核心机制。其基本原理是通过特定算法,将请求参数、时间戳、设备标识等元素组合加密,生成唯一的签名凭证(如Ckey),服务器借此验证请求的合法性。这项技术对于构建安全的客户端-服务器架构至关重要,能有效防止数据篡改和未授权访问。从技术价值看,理解并复现签名算法,不仅是爬虫与自动化工具开发中的关键突破点,也是深入学习现代App加密通信、逆向工程及协议分析的绝佳实践。典型的应用场景包括安全研究、自动化测试以及合规的媒体内容备份等。本文以央视频直播流下载 阅读详情

相关推荐

移动端电话功能开发全解析:从系统拨号到VoIP的技术选型与实现

在移动应用开发中,实现电话呼叫功能是一个常见但涉及多维度考量的技术点。其核心原理主要分为两类:基于运营商网络的传统电路交换通话,以及基于互联网传输的VoIP(Voice over IP)分组交换通话。前者通过系统API(如Android的Intent或iOS的URL Scheme)唤起拨号界面,实现简单且无需处理语音流;后者则依赖音视频编解码和网络传输技术,能实现更丰富的应用内通话场景,但复杂度更高。从技术价值看,正确的选型能平衡功能需求、开发成本、用户体验与平台合规性。在具体应用场景中,无论是简单的客服联

weixin_30378623的博客 348

【Kotlin学习】协程的基本概念(2)

学习于《Kotlin进阶实战》,本文讲解了协程的上下文和调度,协程的作用域和Channel机制,并用简单的实例展示

PanicLin的博客 1332

安卓APK加固实战:腾讯乐固核心原理与避坑指南

在移动应用开发领域,应用安全是保障业务稳定和用户信任的基石。其核心原理在于通过技术手段对应用进行保护,防止逆向工程和恶意篡改。这不仅能保护知识产权和核心算法,更是维护应用商店信誉和用户数据安全的关键技术。在众多防护方案中,代码虚拟化(VMP)和资源完整性校验构成了纵深防御体系的核心。代码虚拟化通过将关键字节码转换为自定义指令集,极大增加了静态分析和动态调试的难度;而完整性校验则确保应用在运行前未被篡改。这些技术对于金融支付、核心算法保护等高安全需求场景具有重要价值。本文以腾讯移动应用安全(乐固)为例,深入解

weixin_34396103的博客 609

P7阿里大佬教你破解 Kotlin 协程(10) - Select 篇

在协程当中,Select 的语义与 Java NIO 或者 Unix 的 IO 多路复用类似,它的存在使得我们可以轻松实现 1 拖 N,实现哪个先来就处理哪个。尽管 Select 和 Channel 比起标准库的协程 API 已经更接近业务开发了,不过个人认为它们仍属于相对底层的 API 封装,在实践当中多数情况下也可以使用 Flow API 来解决。而这个 Flow API,完全就是响应式编程的协程版 API,我们简直可以照着 RxJava 来学习它,所以我们下一篇再见吧~~~,这样反而将问题复杂化了。

Android_XG的博客 183

IDE黄昏不是终结,而是AI驱动开发会话的黎明

现代软件开发正从‘项目为中心’转向‘问题为中心’的范式,其核心是开发会话(Development Session)——一种以意图驱动、跨环境调度、语义深度理解的新型交互模式。这一转变由代码复杂度爆炸、多语言协同瓶颈与开发者注意力碎片化共同推动,技术底座依赖PSI树解析、AST锁定与LSP增强等深层代码语义能力。其价值在于将IDE从代码容器升维为开发意图翻译器,支撑CI失败归因、跨端调试协同、Git上下文感知等高阶工程场景。JetBrains AI Assistant与Fleet协议正是这一范式的典型实践,标

weixin_34341117的博客 430

【信息科学与工程学】计算机科学与自动化——第三百零四篇 高性能系统开发指南01

高并发:限流、熔断、降级高性能:红包拆分、多级缓存、大Key/热Key拆分、Pipeline、线程池、RocketMQ零丢失、幂等、分布式锁、集群缩容、分批入库海量数据处理:日志分表分片、跨表查询、冷热分层、大数据引擎服务器选型系统安全:网关安全、服务器安全、云存储安全、风控、监控、测试数据一致性:缓存与数据库一致性、大厂方案、蓝绿回滚、流量一致性、流程一致性项目与团队管理:流程问题、管理问题(风险、成本、人员、文档、培训)稳定性建设:上线三板斧、线上五步闭环、混沌工程、容量规划。

weixin_49199313的博客 251

高级Android开发工程师核心能力与面试指南

Android开发作为移动端核心技术栈,其工程师能力模型已从基础功能实现演进为系统级架构设计。理解Android Framework核心机制如Binder、Handler等IPC通信原理,是构建高性能应用的基础。Kotlin以其78%的采用率成为现代Android开发的首选语言,与Java形成技术纵深。在工程实践层面,模块化架构设计与Flutter跨平台技术整合成为提升开发效率的关键。性能优化涉及启动速度、内存管理等全链路解决方案,需结合CI/CD持续集成体系实现工程化落地。对于高级开发者而言,深入ART虚

weixin_34319374的博客 395

EBSD分析工具-jtex.不需要再用付费的channel5 了,一个软件解决

EBSD分析工具-jtex.不需要再用付费的channel5 了,一个软件解决

Flutter逆向工程实战:使用B(l)utter从libapp.so提取Dart代码与字符串

在移动应用开发领域,Flutter作为跨平台框架,其编译产物libapp.so文件包含了应用的核心逻辑。理解二进制文件逆向分析原理,对于安全审计、竞品研究和漏洞挖掘具有重要技术价值。通过解析ELF文件格式、定位Dart代码段、还原序列化数据结构,可以深入理解应用内部机制。B(l)utter工具专门针对Flutter逆向工程设计,能够高效提取Dart字节码、类结构信息和字符串资源,广泛应用于移动安全评估和代码审计场景。本文聚焦于Flutter应用逆向分析,详细演示如何通过B(l)utter工具链从libapp

weixin_33904756的博客 692

inSSIDer4破解版,亲测可用

inSSIDer 通过inSSIDer搜索无线热点,我们可以看到每个热点的MAC地址、网络名称(SSID)、无线信号强度、使用的信道、加密方式、无线传输速率和网络类型等主要信息,非常的全面。此外,在这些基本信息的下方,我们还可以查看每个时间段每个无线热点的信号强度和稳定性(纵坐标:信号强度,横坐标:时间段),其中纵坐标越高,表明信号强度越强,而横坐标越平滑,则表明无线信号越稳定。 2.4GHz频段信道使用情况 查看2.4GHz频段信道使用情况,这是inSSIDer非常有亮点的一个功能(纵坐标:信号强度,横坐标:14信道)。在这里我们不仅可以看到每个无线热点所占用的无线信道,还能看到该热点的信号强度。此时,信号强度强,占用信道不拥挤的无线热点就是你的选择。 5GHz频段信道使用情况 查看5GHz频段信道使用情况,这是inSSIDer为广大用户提前准备的前卫功能,显示效果与2.4GHz频段相同。相信在双频段无线网络普及时,inSSIDer将同样为你带来选择。

inssider 4.2免安装绿化破解版

inssider 4.2免安装绿化破解版,最新版本,免安装,已注册,查看WIFI信息利器,

Android应用上架遇阻?深度解析代码相似度检测与破解之道

在Android应用开发与上架过程中,代码相似度检测是开发者常遇的技术挑战,其核心原理基于静态分析与特征比对。系统通过分析应用的代码结构、资源文件指纹、第三方库烙印及原生库特征等多维度信息,构建应用的特征图谱以识别潜在的“马甲包”或“套壳应用”。这项技术的价值在于维护应用商店生态,打击批量生产的低质应用,保护原创开发者的权益。然而,在实际应用中,由于大量应用集成相同的流行框架(如Flutter、React Native)或行业通用SDK(如友盟统计、腾讯Bugly),常导致“误伤”正经原创应用。因此,开发者

weixin_30875157的博客 370

Channel5 EBSD分析软件下载与安装教程

资源下载链接为: https://pan.quark.cn/s/d9ef5828b597 EBSD分析软件Channel 5简介 EBSD(电子背散射衍射)是一种在扫描电子显微镜(SEM)中用于分析材料微观结构的技术,能够提供晶体取向分布等重要信息,广泛应用于材料科学、地质学等研究领域。Channel 5是由英国Oxford Instruments公司开发的一款专门用于EBSD数据处理和纹理分析的软件。本文将介绍Channel 5的下载安装过程,并探讨其在EBSD分析中的主要功能和应用。 一、Channel 5的下载与安装 Channel 5的安装文件通常以压缩包形式提供,例如“EBSD分析软件——Channel5下载安装教程.zip”。用户可以使用WinRAR或7-Zip等解压缩工具将其解压。解压后,会找到一个名为“Channel 5 安装教程使用教程.docx”的文档,其中详细介绍了软件的下载和安装步骤。此外,压缩包中可能还包含“Windows XP Professional下载地址.txt”,这表明Channel 5可能对操作系统有特定要求。尽管Windows XP已经过时,但某些实验室可能仍在使用。需要注意的是,最新版本的软件可能不支持较旧的操作系统,因此用户在安装前应检查软件的系统需求,确保操作系统兼容。安装过程通常包括运行安装程序、接受许可协议、选择安装路径等步骤。安装完成后,可能需要重启计算机以确保软件正常运行。 二、Channel 5的功能与应用 Channel 5为EBSD数据分析提供了一整套工具,涵盖数据导入、图像显示、晶体取向计算、纹理分析以及图形输出等功能,以下是其主要功能特点: 数据导入与预处理:Channel 5能够读取多种格式的EBSD数据,如MTEX、HKL、TSL等,并支持数据校正和滤波功能,可有效去除噪声和伪晶,确保数据的准确性和可靠性。

3层安全防线:为TV应用构建坚不可摧的隐私保护体系

在智能电视生态中,我们面临着一个独特的挑战:客厅作为家庭共享空间,设备却承载着个人化内容。当家人轮流使用电视时,如何确保个人观看记录不被窥探?当设备连接网络时,如何防止敏感数据泄露?这些问题正是TV应用安全设计的核心命题。 ## 问题场景:当客厅变成隐私战场 想象一下这样的场景:你在电视上观看的私人内容,第二天被家人无意间看到;或者电视应用中的设置被孩子随意更改。更严重的是,如果应用存储了用户

gitblog_00715的博客 1040

ICCV 2025 | 哈工大、清华等提出首个双曲学习框架:HLFormer,革新局部相关视频检索

离原点越远,代表的语义越具体(部分)。现有方法大多在传统的欧几里得空间中对视频和文本进行建模,但这往往会导致对视频内在层次结构的错误表示,从而影响检索的准确性。通过UMAP对学习到的帧嵌入进行可视化(如下图),可以观察到,来自同一时刻的视频帧在嵌入空间中聚集在一起,清晰地展示了模型对时序关系的捕捉能力。下图的双曲空间可视化进一步证实了模型的层次建模能力:离原点越近的点,代表的语义层级越高、粒度越粗糙,反之亦然。:既然文本查询描述的是视频的一部分,那么在几何表示上,文本的嵌入向量应该从属于视频的嵌入向量。

我爱计算机视觉 1083

3D识别建模 — 基于ReconstructMe和EAGLE EYES SCANNER的实现

3D实体识别建模,作为3D打印反向工程的一个重要环节,今天就让我们利用ReconstructMe和EAGLE EYES SCANNER来了解一下。 第一步、软件和硬件准备就绪 ReconstructMe下载并安装 http://reconstructme.net/?wpdmdl=160 EAGLE EYES SCANNER直接将相应USB插入电脑接口即OK,之后在    中点击搜索“设备管理...

qq_40013290的博客 882

3步构建TV应用安全防护体系:my-tv密码保护实战指南

在智能电视应用开发中,如何保护用户隐私和观看数据是一个常见的技术挑战。my-tv项目通过原生加密库和配置管理机制,为电视直播软件提供了完整的安全解决方案。本文将深入解析该项目的安全防护实现,帮助开发者快速掌握TV应用密码保护的核心技术。 ## 智能电视应用面临的安全挑战 TV应用通常运行在家庭共享设备上,这意味着多个用户可能访问同一设备。没有适当的安全防护,用户的观看历史、频道偏好和个人设置都

gitblog_00774的博客 708

阿里云一键登录Java后端实战:5分钟搞定号码认证服务集成(附完整代码)

本文详细介绍了如何在Java后端项目中快速集成阿里云号码认证服务(PNVS),实现高效、安全的一键登录功能。文章从核心架构、安全机制解析入手,提供了完整的Maven依赖配置、服务端环境准备、核心业务逻辑实现(包括Token验证、用户处理、异常降级)以及生产环境下的性能优化、监控与部署方案,并附有可直接使用的完整代码示例。

p1q2r的博客 848

深入解析并发与并行:从CPU多核到高并发编程实战

并发与并行是提升计算效率的核心概念,它们共同构成了现代高性能计算的基础。并发关注任务的组织方式,通过时间片轮转等技术,使多个任务在单核上“看起来”同时执行,旨在最大化系统吞吐量;并行则依赖于多核CPU等硬件支持,实现任务的物理同时执行,以缩短单个任务的完成时间。理解其原理对于优化程序性能至关重要,尤其在处理高并发请求或海量数据时。本文结合多线程编程、CPU缓存一致性等热词,探讨了如何通过合理的任务分解与同步机制,避免竞态条件与死锁,从而在Web服务器、大数据分析等应用场景中实现高效并发与并行处理,最终提升整

weixin_34280237的博客 374

面向企业级应用的 SAP UI5 troubleshooting 全景指南:从日志与调试到性能与可测试性

本指南系统化介绍了SAP UI5应用排错的完整流程和工具链。从基础的浏览器开发者工具使用,到SAP UI5专用调试工具(Technical Information Dialog、Support Assistant、Diagnostics Window),再到性能优化和日志管理,提供了一套工程化的解决方案。重点推荐组合使用UI5 Inspector扩展、Test Recorder测试工具以及性能测量模块,并强调将常见问题整理成团队知识库。通过制度化的排错步骤和工具组合,开发者可以快速定位、复现和修复问题,将调

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 286

基于STM32的智能婴儿床设计(蓝牙通信+摄像头监控)_315

本项目设计并实现了一款基于STM32微控制器的智能婴儿床系统,通过多种传感器和智能控制模块为家长提供更便捷和高效的婴儿照护解决方案。系统包括婴儿哭泣检测、湿度监测、空气质量检测、温度监控等多项功能,并通过蓝牙与手机APP进行数据传输和远程控制。婴儿床内置摇摇床和安抚音乐播放功能,能够在婴儿哭泣时自动启动摇摇床并播放安抚音乐。此外,系统还支持摄像头远程监控,让家长实时查看婴儿的状态。整个系统采用STM32F103RCT6作为核心控制单元,结合声音传感器、湿度传感器、MQ135空气质量传感器。

865

Flutter for Android开发详细配置开创建项目

Windows开发环境下使用 Flutter语言开发android 注:使用Flutter Sdk也要使用android sdk, 和kotlin语言一样(对话框工具,Dialog集合工具,引用了V7包的AlertDialog 使用的 kotlin_version = '1.3.10’写的: https://github.com/Jay-YaoJie/KotlinDialogs) 安装An...

唯美,不是唯一 787

手把手教你学Simulink--基于多能源系统协同控制的复杂场景实例:氢燃料电池+锂电池混合动力电动汽车能量管理仿真

本文介绍了基于Simulink的氢燃料电池与锂电池混合动力电动汽车能量管理系统的建模与仿真方法。系统包含驾驶循环输入、燃料电池子系统、锂电池子系统、能量管理、电机驱动等模块,通过协同控制优化能源分配。详细说明了从模型创建到各子系统设置的7个步骤,并提供了MATLAB代码示例。该仿真可评估不同工况下的系统响应速度、能量利用效率和电池寿命等性能指标,为混合动力汽车的能量管理策略设计和优化提供参考。

MHD0815的博客 239
上一篇: 看完这篇华为的 Android 面试题和八股文,这个金三银四不愁了
下一篇: 从阿里外包到华为OD的Android程序员
不秃头de程序猿
博客等级 码龄5年 4089粉丝 1478原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值