本文主要探讨如何利用Combine中Schedulers对任务执行进行管理。假定读者已经了解Combine的基本原理,想要进一步对Combine中任务调度进行详细了解。由于Schedulers与GCD(Grand Dispatch Queue),线程(thread),RunLoop都有关联,所以也需要读者有这方面的基础了解才能更好的读懂下文。
本文将会介绍以下几方面内容:
Scheduler是一个协议(protocol),定义了什么时候(when)和在什么地方(where)执行一个闭包,其中什么地方(where)意味着 runloop,dispatch queue,operator queue,三选一使用哪个;什么时候(when)意味着Combine事件流的虚拟时间,也就是Combine中Publisher,Operator,Subscriber中具体实现的上下文是执行在哪个Scheduler上。
你可能注意到了Scheduler的定义刻意避免了对线程的任何引用,这是因为你的代码具体执行在哪个线程,是由你选择的Scheduler决定的,也就是说Scheduler协议的具体实现(DispatchQueue,OperationQueue都是遵循Scheduler协议的)定义了任务调度,同时你需要注意Scheduler和线程并不是完全的对应关系,指定一个Scheduler后也可能在不同的线程上执行任务。
默认情况下,即不使用任何后续我们将要讲到的scheduler,Combine将会默认使用上游Publisher产生的线程发送到下游示例1
var cancellables = Set<AnyCancellable>()
let intSubject = PassthroughSubject<Int, Never>()
intSubject.sink(receiveValue: { value in
print(value)
print(Thread.current)
}).store(in: &cancellables)
intSubject.send(1)
DispatchQueue.global().async {
intSubject.send(2)
}
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.underlyingQueue = DispatchQueue(label: "com.donnywals.queue")
queue.addOperation {
intSubject.send(3)
}
输出
1
<NSThread: 0x600003b80780>{number = 1, name = main}
2
<NSThread: 0x600003b99300>{number = 6, name = (null)}
3
<NSThread: 0x600003bb2080>{number = 4, name = (null)}
sink的receiveValue被三次调用,每次被调用在不同线程,.send(1)
是在主线程发出的,此时sink输出也是在主线程,而send(2)
和send(3)
都是在子线程发出,则对应sink收到也是在子线程
我们经常需要在后台运行一些消耗资源的操作,这样当用户在界面上操作时,能够避免阻塞主线线程,不影响用户操作。Combine中提供了Schedulers对任务执行进行切换,主要通过两个操作符:subscribe(on:)
和receive(on:)
receive(on:)影响其所在位置下游的scheduler,即其后面的操作符是影响范围示例2
publisher
.operatorA
.receive(on:)
.operatorB
.sink
receive(on:)插在两个OperatorA,OperatorB之间,会影响OperatorB,影响sink函数,不会影响OperatorA,所以是影响其所在位置下游的scheduler
subscribe(on:)稍微复杂一些,如果你了解Comebine的Backpressure机制,会记得遵循Publisher协议需要实现receive
方法,遵循Subscription协议需要实现request
,cancel
方法,subscribe(on:)
就是用来控制上面提到的这些方法的切换调度;一般来说,subscribe(on:)
会影响一个Combine异步事件链条上的Publisher和Operator,但是不是一定,它取决于实现Publisher协议对象的具体内部实现,如果其内部实现指定了线程,将不会遵循外部subscribe(on:)
的设置,反之,如果没有指定一般会使用。
上面的描述可能还是让你一头雾水,下面将会使用具体的代码实例解释。
我们将Publisher分成两类进行分析,一类是自定义实现的Publisher,另一类是系统提供的Publisher。
完整示例3
//先创建一个ComputationSubscription(Subscription)和ExpensiveComputation(Publisher)
final class ComputationSubscription<Output>: Subscription {
private let duration: TimeInterval
private let sendCompletion: () -> Void
private let sendValue: (Output) -> Subscribers.Demand
private let finalValue: Output
private var cancelled = false
init(duration: TimeInterval, sendCompletion: @escaping () -> Void, sendValue: @escaping (Output) -> Subscribers.Demand, finalValue: Output) {
self.duration = duration
self.finalValue = finalValue
self.sendCompletion = sendCompletion
self.sendValue = sendValue
}
func request(_ demand: Subscribers.Demand) {
if !cancelled {
print("Beginning expensive computation on thread \(Thread.current.number)")
}
Thread.sleep(until: Date(timeIntervalSinceNow: duration))
if !cancelled {
print("Completed expensive computation on thread \(Thread.current.number)")
_ = self.sendValue(self.finalValue)
self.sendCompletion()
}
}
func cancel() {
cancelled = true
}
}
extension Publishers {
public struct ExpensiveComputation: Publisher {
public typealias Output = String
public typealias Failure = Never
public let duration: TimeInterval
public init(duration: TimeInterval) {
self.duration = duration
}
public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
Swift.print("ExpensiveComputation subscriber received on thread \(Thread.current.number)")
let subscription = ComputationSubscription(duration: duration,
sendCompletion: { subscriber.receive(completion: .finished) },
sendValue: { subscriber.receive($0) },
finalValue: "Computation complete")
subscriber.receive(subscription: subscription)
}
}
}
//--------------------------------------
//继续使用上面的代码,完成一个完整的订阅流程
// 1
let computationPublisher = Publishers.ExpensiveComputation(duration: 3)
// 2
let queue = DispatchQueue(label: "serial queue")
let currentThread = Thread.current.number
print("Start computation publisher on thread \(currentThread)")
let subscription = computationPublisher
.subscribe(on: queue) // 3
.map({ (val) -> String in
// print(val)
//4
print("Thread.current.number=\(Thread.current.number)")
return val
})
.receive(on: DispatchQueue.main) //5
.sink { value in
let thread = Thread.current.number
print("Received computation result on thread \(thread): '\(value)'")
}
输出
Start computation publisher on thread 1
ExpensiveComputation subscriber received on thread 6
Beginning expensive computation on thread 6
Completed expensive computation on thread 6
Thread.current.number=6
Received computation result on thread 1: 'Computation complete'
subscribe(on:)
的使用影响Publisher的线程使用,Operator也是一种Publisher,map中操作也是在子线程,.subscribe(on:)
的位置在map前或者后面,没有区别receive(on:)
用于控制其位置下游的scheduler,一般使用在sink前,用于Subscirber侧的线程控制,即sink中输出值的运行线程,但不仅仅限于此,如果receive(on:)
位置移动到map的前面,也会影响map的线程下面修改一下代码,调整receive(on:)
位置,移动到subscribe(on:)
后面,map前面
示例3-局部修改
// 1
let computationPublisher = Publishers.ExpensiveComputation(duration: 0)
// 2
let queue = DispatchQueue(label: "serial queue")
// 3
let currentThread = Thread.current.number
print("Start computation publisher on thread \(currentThread)")
let subscription = computationPublisher
.subscribe(on: queue)
.receive(on: DispatchQueue.main)
.map({ (val) -> String in
// print(val)
print("Thread.current.number=\(Thread.current.number)")
return val
})
.sink { value in
let thread = Thread.current.number
print("Received computation result on thread \(thread): '\(value)'")
}
输出
Start computation publisher on thread 1
ExpensiveComputation subscriber received on thread 6
Beginning expensive computation on thread 6
Completed expensive computation on thread 6
Thread.current.number=1
Received computation result on thread 1: 'Computation complete'
注意:map切到主线程执行,publisher仍然在子线程执行,receive(on:)
成功影响其下游的scheduler,map位于其下游,所以切到主线程,而Publisher被subscribe(on:)
控制还位于子线程
示例3-局部修改2
public struct ExpensiveComputation: Publisher {
public typealias Output = String
public typealias Failure = Never
public let duration: TimeInterval
public init(duration: TimeInterval) {
self.duration = duration
}
public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
DispatchQueue.main.async { //note
Swift.print("ExpensiveComputation subscriber received on thread \(Thread.current.number)")
let subscription = ComputationSubscription(duration: duration,
sendCompletion: { subscriber.receive(completion: .finished) },
sendValue: { subscriber.receive($0) },
finalValue: "Computation complete")
subscriber.receive(subscription: subscription)
}
}
}
这段代码唯一一处修改是Publisher的实现ExpensiveComputation中,receive
方法的内部实现,指定了运行在主线程。
此时再执行下面代码,注意subscribe
和receive
位置恢复到初始状态
let subscription = computationPublisher
.subscribe(on: queue) // 3
.map({ (val) -> String in
// print(val)
//4
print("Thread.current.number=\(Thread.current.number)")
return val
})
.receive(on: DispatchQueue.main) //5
.sink { value in
let thread = Thread.current.number
print("Received computation result on thread \(thread): '\(value)'")
}
输出
Start computation publisher on thread 1
ExpensiveComputation subscriber received on thread 1
Beginning expensive computation on thread 6
Completed expensive computation on thread 6
Thread.current.number=1
Received computation result on thread 1: 'Computation complete'
此时,map不再执行在subscribe指定的子线程,而是主线程,这是Publisher内部实现决定的;
下面继续介绍系统提供的Publisher,主要介绍PassthroughSubject和CurrentValueSubject;这里我们会频繁使用Thread.current
这个系统提供的方法,打印线程信息,比如
<NSThread: 0x600001a6c900>{number = 1, name = main}
就代表当前在线程1,线程的名字是main,也就是主线程,在playground中主线程number等于1,
示例4
let intSubject = PassthroughSubject<Int, Never>()
intSubject
.subscribe(on: DispatchQueue.global())
.sink(receiveValue: { value in
print(Thread.current)
})
.store(in: &cancellables)
intSubject.send(1)
intSubject.send(2)
intSubject.send(3)
输出
<NSThread: 0x600001a6c900>{number = 1, name = main}
<NSThread: 0x600001a6c900>{number = 1, name = main}
<NSThread: 0x600001a6c900>{number = 1, name = main}
subscribe切到子线程,但是sink并没有在子线程输出,而是仍然在主线程,开头"Combine的默认调度机制"小节已经证实是按subject.send所在线程,不受subscribe(on:)
影响
示例5
let intSubject = PassthroughSubject<Int, Never>()
intSubject
.map({ (num) -> Int in
print("map:\(Thread.current)")
return num + 1
})
.subscribe(on: DispatchQueue.global())
.sink(receiveValue: { value in
print("sink:\(Thread.current)")
})
.store(in: &cancellables)
// sleep(5)
intSubject.send(1)
intSubject.send(2)
intSubject.send(3)
如果运行在模拟器,没有输出,这是因为,由于subscribe切换执行到子线程异步执行,代码马上继续执行,当subscription建立前,send已经发出,所以没有任何输出;
如果把sleep(5)
的注释打开,由于延迟了足够的时间才执行send,这时sink已经执行完成,所以会有输出;
运行在Playground,输出
map:<NSThread: 0x600001f54440>{number = 1, name = main}
sink:<NSThread: 0x600001f54440>{number = 1, name = main}
map:<NSThread: 0x600001f54440>{number = 1, name = main}
sink:<NSThread: 0x600001f54440>{number = 1, name = main}
map:<NSThread: 0x600001f54440>{number = 1, name = main}
sink:<NSThread: 0x600001f54440>{number = 1, name = main}
发现PassthroughSubject不受subscribe(on:)
影响,取决于send所在线程
示例6
let intSubject = CurrentValueSubject<Int, Never>(0)
intSubject
.subscribe(on: DispatchQueue.global())
.map({ (num) -> Int in
print("num=\(num),map:\(Thread.current)")
return num + 1
})
.receive(on: DispatchQueue.global())
.sink(receiveValue: { value in
print("num=\(value),sink:\(Thread.current)")
})
.store(in: &cancellables)
sleep(5)
intSubject.send(1)
intSubject.send(2)
输出
num=0,map:<NSThread: 0x600001e48180>{number = 3, name = (null)}
num=1,sink:<NSThread: 0x600001e48180>{number = 3, name = (null)}
num=1,map:<NSThread: 0x600001e20900>{number = 1, name = main}
num=2,map:<NSThread: 0x600001e20900>{number = 1, name = main}
num=2,sink:<NSThread: 0x600001e2d340>{number = 6, name = (null)}
num=3,sink:<NSThread: 0x600001e48180>{number = 3, name = (null)}
重组一下,按map和sink配对输出,便于对照
num=0,map:<NSThread: 0x600001e48180>{number = 3, name = (null)}
num=1,sink:<NSThread: 0x600001e48180>{number = 3, name = (null)}
num=1,map:<NSThread: 0x600001e20900>{number = 1, name = main}
num=2,sink:<NSThread: 0x600001e2d340>{number = 6, name = (null)}
num=2,map:<NSThread: 0x600001e20900>{number = 1, name = main}
num=3,sink:<NSThread: 0x600001e48180>{number = 3, name = (null)}
发现CurrentValueSubject,初始值0按subscribe(on:)
执行调度,后续map所在线程send按发出线程,sink按receive(on:)
指定的sechduler
苹果提供了Scheduler协议的几个具体实现
schedule(after:)
),使用这个Scheduler将会遇到致命错误。下面针对每一种类型介绍如下:
let source = Timer
.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.scan(0) { counter, _ in counter + 1 }
source
//1
.map{ value -> Int in
print("1:\(Thread.current)")
return value
}
// 2
.receive(on: ImmediateScheduler.shared)
// 3
.map{ value -> Int in
print("2:\(Thread.current)")
return value
}
.eraseToAnyPublisher()
.sink { _ in
//print(out)
}
.store(in: &subscription)
部分输出摘录
1:<NSThread: 0x600000c2c2c0>{number = 1, name = main}
2:<NSThread: 0x600000c2c2c0>{number = 1, name = main}
1:<NSThread: 0x600000c2c2c0>{number = 1, name = main}
2:<NSThread: 0x600000c2c2c0>{number = 1, name = main}
这个例子中,为了明确知道Scheduler调度后,具体在哪个线程执行,在注释1和3处,加入了两个map,他们的闭包内部没有任何实际意义,只是用来打印线程,直接返回上游的结果;
由于ImmediateScheduler在当前线程执行,playground的main thread 是1,所以都在主线程执行
再上面代码上做一点修改代码,在注释1前面增加,receive(on: DispatchQueue.global())
source
//4
.receive(on: DispatchQueue.global())
//1
.map{ value -> Int in
print("1:\(Thread.current)")
return value
}
// 2
.receive(on: ImmediateScheduler.shared)
// 3
.map{ value -> Int in
print("2:\(Thread.current)")
return value
}
.eraseToAnyPublisher()
.sink { _ in
//print(out)
}
.store(in: &subscription)
输出
1:<NSThread: 0x6000018393c0>{number = 6, name = (null)}
2:<NSThread: 0x6000018393c0>{number = 6, name = (null)}
1:<NSThread: 0x600001829300>{number = 3, name = (null)}
2:<NSThread: 0x600001829300>{number = 3, name = (null)}
此时,Publisher不在运行在主线程,而是在global queue
RunLoop产生早于DispatchQueue,是在线程级别管理输入资源的方式,我们的应用程序的主线程默认有一个关联的Runloop,你也可以通过调用RunLoop.current获得Fundation框架提供的当前线程;现在Runloop使用场景更少,DispatchQueue更常用,但是特定场景下Runloop还是很有用,比如Timer执行在RunLoop上;
为了方便对比,我们继续利用ImmediateScheduler小节提供的代码示例,再其基础上做修改
var subscription = Set<AnyCancellable>()
let source = Timer
.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.scan(0) { counter, _ in counter + 1 }
source
//4
.receive(on: DispatchQueue.global())
//1
.map{ value -> Int in
print("1:\(Thread.current)")
return value
}
// 2
.receive(on: RunLoop.current)
// 3
.map{ value -> Int in
print("2:\(Thread.current)")
return value
}
.eraseToAnyPublisher()
.sink { _ in
//print(out)
}
.store(in: &subscription)
输出
1:<NSThread: 0x6000024e8680>{number = 5, name = (null)}
2:<NSThread: 0x6000024c8300>{number = 1, name = main}
1:<NSThread: 0x6000024e8680>{number = 5, name = (null)}
2:<NSThread: 0x6000024c8300>{number = 1, name = main}
这段代码注释2处,使用RunLoop.current
代替ImmediateScheduler.shared
,需要注意,RunLoop.current
是什么?它是与在调用时处于当前状态的线程相关联的RunLoop。由于从主线程调用程序,因此RunLoop.current
是主线程的RunLoop。
DispatchQueue遵循scheduler协议,通过向系统管理的调度队列提交任务,可以在多核硬件上并发执行代码;DispatchQueue可以是串行(默认)或并行;
注意:在使用subscribe(on:)、receive(on:)或任何其他接受调度程序参数的操作符时,绝不应该假定调度程序的线程每次都是相同的。
var subscriptions = Set<AnyCancellable>()
let serialQueue = DispatchQueue(label: "Serial queue")
let sourceQueue = DispatchQueue.main
// 1
let source = PassthroughSubject<Void, Never>()
// 2
let subscription = sourceQueue.schedule(after: sourceQueue.now,
interval: .seconds(1)) {
source.send()
}
source
.map{ _ in
print("1:\(Thread.current)")
}
.receive(on: serialQueue,options: DispatchQueue.SchedulerOptions(qos: .userInteractive))
.map{ _ in
print("2:\(Thread.current)")
}
.eraseToAnyPublisher()
.sink(receiveValue: { _ in
})
.store(in: &subscriptions)
输出
1:<NSThread: 0x600002490740>{number = 1, name = main}
2:<NSThread: 0x600002486340>{number = 6, name = (null)}
1:<NSThread: 0x600002490740>{number = 1, name = main}
2:<NSThread: 0x6000024a4780>{number = 4, name = (null)}
1:<NSThread: 0x600002490740>{number = 1, name = main}
2:<NSThread: 0x600002486340>{number = 6, name = (null)}
1:<NSThread: 0x600002490740>{number = 1, name = main}
2:<NSThread: 0x6000024a4780>{number = 4, name = (null)}
从输出结果可以看到DispatchQueue无法保证执行在哪个线程上,source发出后是指定在main,当切到serialQueue后,执行在一个串行队列serialQueue上,但是不能确定保持不变,这个例子中一会是6,一会是4;另外,你也会注意到DispatchQueue是唯一有option可设置的,有两种:
如果改动一下上面代码,把sourceQueue从DispatchQueue.main改成serialQueue,即前后切换的两个queue保持一致
let sourceQueue = serialQueue //DispatchQueue.main
输出
1:<NSThread: 0x60000308c080>{number = 3, name = (null)}
2:<NSThread: 0x60000308c080>{number = 3, name = (null)}
1:<NSThread: 0x600003085300>{number = 4, name = (null)}
2:<NSThread: 0x600003085300>{number = 4, name = (null)}
线程number就会一直保持不变,即一直在一个线程上执行
let queue = OperationQueue()
let subscription = (1...10).publisher
.receive(on: queue)
.sink { value in
print("Received \(value)")
}
输出
Received 1
thread = <NSThread: 0x600002af80c0>{number = 3, name = (null)}
Received 2
thread = <NSThread: 0x600002ac4f80>{number = 5, name = (null)}
Received 3
thread = <NSThread: 0x600002ac5480>{number = 7, name = (null)}
Received 7
thread = <NSThread: 0x600002ac4680>{number = 8, name = (null)}
Received 6
thread = <NSThread: 0x600002af4780>{number = 9, name = (null)}
OperationQueue在底层使用OperationQueue执行任务,所以不保证具体执行在哪个线程;另外OperationQueue的maxConcurrentOperationCount属性也很明确的可以并发执行,所以输出结果的顺序并不保证;
简单修改一下,增加
queue.maxConcurrentOperationCount = 1
由于设置了最大并行操作计数等于1,所以等价串行队列,此时结果就会按顺序输出
Received 1 on thread 3
Received 2 on thread 3
Received 3 on thread 3
Received 4 on thread 3
Received 5 on thread 4
Received 6 on thread 3
Received 7 on thread 3
Received 8 on thread 3
Received 9 on thread 3
Received 10 on thread 3
本篇中使用了大量代码讲解Combine中Schedulers的运行机制,当你需要执行一些耗时或资源消耗型操作,就需要考虑适当使用Schedulers进行合理的任务调度,避免阻塞主线程;通过定义Scheduler这个新的概念,提醒使用者调度的核心是合理的选择调度逻辑(主线程或子线程),但是不能具体指定在哪个线程;同时,苹果提供了Scheduler协议的多种具体实现,一般来说,更推荐使用DispatchQueue。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/gTmQX-_5l9rQjHDL2XwRhA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。