普通视图

发现新文章,点击刷新页面。
昨天以前首页

最便宜智能戒指:不充电、不联网、不 AI,只卖 500 块

作者 马扶摇
2025年12月12日 15:34

Apple Watch 作为一款可穿戴智能设备,最大的槽点就是需要频繁的充电。

能够养成习惯还好,但如果充电和你的作息打架,那它就会变成每天睡前一个很恼人的环节。

▲ 图|Apple Community

这个时候,如果有这么一款可穿戴产品:轻巧便携、防水防尘,而且永远不需要充电——你把它戴在手上,哪怕连续使用一整年都不用考虑续航。你会对此感兴趣吗?

听起来是不是有点像《赛博朋克 2077》里才有的黑科技?如果你是智能穿戴的老用户,听到这种「反常识」的设定,大概率会猜到一个名字:Pebble。

没错,那个曾经的智能手表鼻祖、极客心中的白月光,真的带着这么一款产品回来了。这个仿佛自带核能电池的可穿戴产品,就是 Pebble Index 01 戒指:

▲ 图|Repebble

早在 Apple Watch 定义了所谓「智能手表」品类之前,Pebble 就已经是智能穿戴设备的第一批玩家了。

2012 年,苹果入场智能穿戴设备的两年前,Pebble 就凭借电子墨水屏、超长续航和开放的生态系统,在 Kickstarter 上创造了目标 10 万融资 1026 万美元的「众筹神话」。

虽然 Pebble 后来由于 Apple Watch 和 Android Wear 两家巨头的入场而逐渐难以招架,最终于 2016 年被 Fitbit 收购,但在很多经历过 Pebble Classic 时代的老用户心中,那种代表性的极简和实用主义精神从未消失。

▲ 图|Wikipedia

而这一次,Pebble 的创始人 Eric Migicovsky 再次出山,但它的目光已经不在成熟的智能手表市场,而是做了一枚戒指。

更重要的是,在这个 AI 群魔乱舞的时候,Pebble 想做的不是一枚 AI 伴侣。

Pebble Index 01 的外观看上去和普通的戒指几乎没有区别,甚至第一眼都很难看出这是一个电子产品,反而像是一枚精致的、略带工业风的装饰指环。

▲ 图|Repebble

Pebble Index 01 目前一共推出了三种配色,分别为抛光银(Polished Silver)、抛光金(Polished Gold)以及哑光黑(Matte Black),共有 6-13 号八种尺寸,单从外观就已经值回售价了。

兼顾装饰和智能属性的「指环类」智能产品并非没有先例,但千万别把 Pebble Index 01 和 Oura Ring 或三星 Galaxy Ring 之类主打随身健康的智能戒指搞混了——

Pebble Index 01 没有任何健康监测功能,不能测心率、不能看睡眠,甚至不能用来记步。根据 Migicovski 的介绍,它存在的唯一目的,是充当你大脑的「外置内存」。

▲ 图|Gizmodo

因此,Pebble Index 01 的设计逻辑简直可以用「简单粗暴」来形容:没有屏幕和震动马达,只有一个按钮、一颗小 LED 灯和一个麦克风,简洁程度不输当年的初代 iPhone。

与之相呼应的是,Pebble Index 01 的用法也有且仅有一种

当你脑海中突然闪过一个灵感、想起一件急事,或者单纯想记录一个无厘头的地狱笑话时,既不需要掏出手机,也不用喊智能助手。只需要抬起手、按住戒指上的按钮,把要记录的东西说出来,就结束了。

▲ Youtube|Eric Migicovski

但是极简的用法并不代表这是个简陋的产品。比如对于语音交互的隐私问题,Pebble 就指出:

Pebble Index 01 的麦克风只有按住按钮时才会通电,无法后台启动;所有的录音会通过蓝牙加密传到手机,利用 Pebble app 的本地 AI 模型进行转文字和分类工作,全程无需联网、也不需要订阅费

当然,它能做的也并不只是单纯记录,你也可以给它下指令,要求定闹钟、添加日程和设置待办等等,这些指令都会通过 Pebble app 分发到手机上。

▲ 使用的就是连接 Pebble 手表的 app

此外,Pebble Index 01 也内置了有限的存储功能,可以在连接丢失时保存最长 5 分钟的录音,重新连接上手机之后再传输给 Pebble app。

甚至由于 Pebble Index 01 是个开源产品,你不一定非得用 Pebble 自己的 app,用它来连接和指挥 Siri 一样是可行的,唯一的缺点就是 Apple Intelligence 可能没那么智能

此外,有别于其他所有智能穿戴品牌,Pebble 给 Index 01 设置了一个惊为天人的功能:它不需要充电。

但这并不是什么外星科技,Pebble Index 01 内置了一颗不可更换的氧化银助听器电池,在每天记录 10-20 次、每次录音 5-10 秒的强度下,这块微型电池可以支撑两年。

▲ Youtube|Eric Migicovski

这就意味着你可以像戴普通戒指一样用 Pebble Index 01,无论洗漱、运动甚至泡澡都不用摘。当 Pebble Index 01 电量耗尽的时候,它会在 app 推送通知,提醒你重新下单并且把旧戒指寄给 Pebble 回收——

是的,从这个使用方法来看,Pebble Index 01 其实是一个「一次性产品」

但如果你经历过 Apple Watch 半路没电的窘况,Pebble 这种「刻意设计」的一次性也不失为一种解决方案。

更重要的是,Pebble Index 01 的售价仅为 75 美元(约合人民币 530 元),相当于用不到一单 648 的价格,换来一个能用两年的实体版「闪念胶囊」或者「小布记忆」,一定会有人对此感兴趣的。

只不过,我们也必须认识到 Pebble Index 01 面临的挑战——这是一款过于具体的产品,它解决的痛点非常聚焦和细碎,甚至可以说是从 Migicovsky 个人遇到的问题而开发出的解决方案

而对于大多数习惯了智能手表的人们来说,花几百块钱买一个「录音按钮」,并且为了它专门培养一套「按戒指说话」的新肌肉记忆,这门槛属实不低。

在语音助手已经无处不在的今天,Pebble Index 01 能否凭借其独特的「离线感」和「实体按钮感」打动足够多的人,还有待市场检验。

但能看到 Pebble 的名字再次出现在市场新闻里,本身就是一件让人高兴的事。

▲ 图|Repebble

目前,Pebble Index 01 仍处在 75 美元的预售阶段,正式开售后会上调至 99 美元,可以在 Pebble 官网上下单,发货则要等到 2026 年 3 月前后。

总的来说,Pebble Index 01 是一款非常有趣、有性格的产品,不仅延续了 Pebble 一贯的极客浪漫,更是真的敢于在功能上做减法、在体验上做加法。

这种大胆且好玩的设计理念,我们已经很久没有从科技厂牌的产品中见到了。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


Swift 迭代三巨头(下集):Sequence、Collection 与 Iterator 深度狂飙

2025年12月10日 10:12

在这里插入图片描述

🛡️ 第一重防线:破解 AsyncSequence 的错误恢复漏洞

异步序列的 “错误恢复漏洞”,本质是没搞懂AsyncSequence的错误传播规则 —— 就像 F1 赛车的刹车系统没校准,一踩就抱死,一松就失控。

next()抛出错误时,默认会直接终止迭代,可如果粗暴重试,又会导致 “重复接收元素”;如果不重试,又会丢失关键数据。

在本堂F1调教课中,您将学到如下内容:

  • 🛡️ 第一重防线:破解 AsyncSequence 的错误恢复漏洞
  • 错误传播的 “底层逻辑”
  • 精准重试:给迭代器加 “记忆功能”
  • 实战效果:再也不重复,再也不丢失
  • 🛡️ 第二重防线:给 SafeRingBuffer 加 “数据校验锁”
  • 校验逻辑:哈希值是 “数据身份证”
  • 升级后的 SafeRingBuffer(核心校验代码)
  • 效果:“迭代异常兽” 的篡改彻底失效
  • 🏆 终局:组合拳粉碎 “迭代异常兽”
  • 完整流程代码(终局方案)
  • 剧情收尾:赛道恢复平静,迭代真相揭晓
  • 📝 终极总结:Swift 迭代的 “黄金法则”

艾拉和杰西要做的,就是找到 “精准重试” 的平衡点。

在这里插入图片描述


错误传播的 “底层逻辑”

先搞懂一个关键:AsyncSequence的错误是 “终止性的”—— 一旦next()抛出错误,整个迭代就会停止,就像赛车引擎爆缸后再也没法前进。比如传感器断连抛出SensorDisconnectErrorfor try await循环会立刻跳出,进入catch块,后续的元素再也接收不到。

看这个 “踩坑示例”:

// 错误示范:粗暴重试导致重复数据
Task {
    do {
        for try await data in SensorSequence() {
            sensorBuffer.enqueue(data)
            print("接收数据:\(data)")
        }
    } catch is SensorDisconnectError {
        // 断连后直接重试,却没记录“已接收的元素ID”
        print("传感器断连,重试中...")
        await retrySensorSequence() // 重试时会重新接收之前已处理的元素
    }
}

这段代码的问题在于:重试时会生成全新的异步迭代器,它不知道之前已经接收过哪些元素,导致 “旧数据重复入队”—— 这正是 “迭代异常兽” 想要的结果。

精准重试:给迭代器加 “记忆功能”

杰西的解决方案是:自定义一个RetryableSensorSequence,给异步迭代器加 “元素 ID 记忆”,重试时跳过已处理的元素,就像赛车在维修后重回赛道,能精准接上之前的位置继续跑。

在这里插入图片描述

// 带“记忆功能”的可重试异步序列
struct RetryableSensorSequence: AsyncSequence {
    typealias Element = SensorData // 传感器数据(含唯一ID和校验码)
    typealias AsyncIterator = RetryableSensorIterator
    
    private let baseSequence: SensorSequence // 原始传感器序列
    private var processedIDs: Set<String> = [] // 记录已处理的元素ID(防重复)
    private let maxRetries: Int = 3 // 最大重试次数(避免无限循环)
    private var currentRetry: Int = 0 // 当前重试次数

    init(baseSequence: SensorSequence) {
        self.baseSequence = baseSequence
    }

    func makeAsyncIterator() -> RetryableSensorIterator {
        RetryableSensorIterator(
            baseIterator: baseSequence.makeAsyncIterator(),
            processedIDs: &processedIDs,
            maxRetries: maxRetries,
            currentRetry: &currentRetry
        )
    }

    // 带记忆功能的异步迭代器
    struct RetryableSensorIterator: AsyncIteratorProtocol {
        typealias Element = SensorData
        
        private var baseIterator: SensorSequence.AsyncIterator
        private var processedIDs: inout Set<String> // 引用外部的已处理ID集合
        private let maxRetries: Int
        private var currentRetry: inout Int

        mutating func next() async throws -> SensorData? {
            do {
                guard let data = try await baseIterator.next() else {
                    return nil // 序列正常结束
                }
                // 关键:检查元素ID是否已处理,避免重复
                guard !processedIDs.contains(data.id) else {
                    return try await next() // 跳过重复元素,继续获取下一个
                }
                processedIDs.insert(data.id) // 记录已处理的ID
                return data
            } catch is SensorDisconnectError {
                // 达到最大重试次数,抛出最终错误
                guard currentRetry < maxRetries else {
                    currentRetry = 0 // 重置重试次数,方便后续复用
                    throw error // 重试失败,终止迭代
                }
                currentRetry += 1
                print("第\(currentRetry)次重试传感器连接...")
                // 重建基础迭代器(重新连接传感器)
                self.baseIterator = SensorSequence().makeAsyncIterator()
                // 递归调用next(),继续迭代(不重复已处理元素)
                return try await next()
            } catch {
                // 其他错误(比如数据格式错误),直接抛出
                throw error
            }
        }
    }
}

实战效果:再也不重复,再也不丢失

艾拉把这个 “带记忆的序列” 集成到系统后,效果立竿见影:

// 正确姿势:用可重试序列消费传感器数据
Task {
    do {
        let retryableSequence = RetryableSensorSequence(baseSequence: SensorSequence())
        for try await data in retryableSequence {
            sensorBuffer.enqueue(data)
            print("安全接收数据:\(data)(ID:\(data.id))")
        }
    } catch {
        print("最终失败:\(error),已触发备用传感器")
    }
}

当传感器断连时,序列会自动重试(最多 3 次),且重试后绝不会重复接收旧数据 —— 因为processedIDs会牢牢记住 “哪些数据已经处理过”。就像赛车在维修区快速换胎后,能精准回到赛道的正确位置,既不落后,也不跑偏。

在这里插入图片描述

🛡️ 第二重防线:给 SafeRingBuffer 加 “数据校验锁”

解决了重复问题,下一个目标是 “数据篡改”——“迭代异常兽” 会修改传感器数据的校验码,让错误数据混入缓冲区。杰西的方案是:给SafeRingBuffer升级,在 “入队” 和 “访问” 时双重校验数据完整性,就像给赛车装 “指纹锁”,不是自己人的数据,一概不让进。

校验逻辑:哈希值是 “数据身份证”

在这里插入图片描述

传感器数据会自带一个checksum(哈希值),计算规则是 “数据内容 + 时间戳” 的 MD5 值。SafeRingBuffer在入队时要验证这个哈希值,不匹配就拒绝入队;在访问时再二次校验,确保数据没被篡改。

升级后的 SafeRingBuffer(核心校验代码)

struct SafeRingBuffer<Element: DataVerifiable>: Collection {
    // 新增约束:Element必须遵守DataVerifiable协议(有校验能力)
    private var storage: [Element?]
    private var head = 0
    private var tail = 0
    private(set) var count = 0
    private let lock = NSLock()

    // 入队时校验:篡改的数据直接拒之门外
    mutating func enqueue(_ element: Element) throws {
        lock.lock()
        defer { lock.unlock() }

        // 关键:验证数据哈希值,不匹配则抛出“数据篡改错误”
        guard element.verifyChecksum() else {
            throw DataTamperingError.invalidChecksum(
                "数据校验失败,ID:\(element.id),可能被篡改"
            )
        }

        storage[tail] = element
        tail = (tail + 1) % storage.count

        if count == storage.count {
            head = (head + 1) % storage.count
        } else {
            count += 1
        }
    }

    // 下标访问时二次校验:防止缓冲区内部数据被篡改
    subscript(position: Index) -> Element {
        lock.lock()
        defer { lock.unlock() }

        precondition((0..<count).contains(position), "索引超出范围")
        let actualPosition = (head + position) % storage.count
        guard let element = storage[actualPosition] else {
            preconditionFailure("缓冲区数据丢失,位置:\(actualPosition)")
        }

        // 二次校验:确保数据在缓冲区中没被篡改
        precondition(element.verifyChecksum(), "缓冲区数据被篡改,ID:\(element.id)")
        return element
    }
}

// 数据校验协议:所有需要校验的数据都要遵守
protocol DataVerifiable {
    var id: String { get } // 唯一ID
    var checksum: String { get } // 数据哈希值
    // 校验方法:计算当前数据的哈希值,和自带的checksum对比
    func verifyChecksum() -> Bool
}

// 传感器数据实现校验协议
extension SensorData: DataVerifiable {
    func verifyChecksum() -> Bool {
        // 计算“内容+时间戳”的MD5哈希值(真实项目中建议用更安全的SHA256)
        let calculatedChecksum = "\(content)-\(timestamp)".md5()
        return calculatedChecksum == self.checksum
    }
}

// 自定义错误:数据篡改错误
enum DataTamperingError: Error {
    case invalidChecksum(String)
}

在这里插入图片描述

效果:“迭代异常兽” 的篡改彻底失效

当 “迭代异常兽” 试图把篡改后的传感器数据(校验码不匹配)入队时,enqueue会直接抛出DataTamperingError,错误数据连缓冲区的门都进不了;就算它想偷偷修改缓冲区里的数据,subscript访问时的二次校验也会触发preconditionFailure,立刻暴露问题。

艾拉测试时故意注入一条篡改数据,系统瞬间弹出警告:“DataTamperingError:数据校验失败,ID:sensor_123,可能被篡改”—— 就像赛车的防盗系统检测到非法入侵,立刻锁死引擎,让 “小偷” 无从下手。

🏆 终局:组合拳粉碎 “迭代异常兽”

解决了错误恢复和数据篡改,艾拉和杰西打出最后一套 “组合拳”:用RetryableSensorSequence处理异步错误,用SafeRingBuffer做数据缓存和校验,再配合一个 “监控 Task” 实时监控序列状态 —— 三者联动,形成无死角的防御网。

在这里插入图片描述

完整流程代码(终局方案)

// 1. 创建带校验的环形缓冲区(容量10,只存合法数据)
var verifiedBuffer = SafeRingBuffer<SensorData>(capacity: 10)

// 2. 创建可重试的传感器序列(防断连、防重复)
let sensorSequence = SensorSequence()
let retryableSequence = RetryableSensorSequence(baseSequence: sensorSequence)

// 3. 主Task:消费序列,存入缓冲区
let mainTask = Task {
    do {
        for try await data in retryableSequence {
            do {
                try verifiedBuffer.enqueue(data)
                print("成功入队:ID=\(data.id),转速=\(data.engineRPM)转")
                // 实时更新仪表盘(只传合法数据)
                await dashboard.update(with: verifiedBuffer[verifiedBuffer.count - 1])
            } catch DataTamperingError.invalidChecksum(let message) {
                print("拦截篡改数据:\(message)")
                // 触发警报,记录日志
                await alertSystem.triggerLevel(.high, message: message)
            }
        }
    } catch {
        print("迭代终止:\(error)")
        // 重试失败,切换到备用传感器
        await switchToBackupSensor()
    }
}

// 4. 监控Task:实时检查缓冲区状态,防止异常
let monitorTask = Task {
    while !Task.isCancelled {
        guard verifiedBuffer.count > 0 else {
            print("警告:缓冲区为空,可能传感器无数据")
            await alertSystem.triggerLevel(.low, message: "缓冲区空")
            try await Task.sleep(nanoseconds: 1_000_000_000)
            continue
        }
        // 每1秒检查一次最新数据的时效性(防止数据过期)
        let latestData = verifiedBuffer[verifiedBuffer.count - 1]
        if Date().timeIntervalSince(latestData.timestamp) > 5 {
            print("警告:最新数据已过期,可能序列卡顿")
            await alertSystem.triggerLevel(.medium, message: "数据过期")
        }
        try await Task.sleep(nanoseconds: 1_000_000_000)
    }
}

剧情收尾:赛道恢复平静,迭代真相揭晓

当这套组合拳部署完成后,赛车仪表盘的转速数据瞬间稳定下来 ——10000 转、10020 转、9980 转,每一个数字都精准跳动,控制室的警报声渐渐平息。艾拉看着屏幕上 “所有传感器正常” 的绿色提示,长舒一口气:“‘迭代异常兽’被打跑了?”

杰西笑着点开日志,里面全是 “拦截篡改数据”“重试成功” 的记录:“不是打跑,是它再也没法钻漏洞了。你看 ——AsyncSequence 的核心是‘错误可控’,Collection 的核心是‘数据可信’,迭代器的核心是‘状态独立’,只要守住这三个核心,再狡猾的问题也能解决。”

在这里插入图片描述

突然,仪表盘弹出一条新消息:“检测到外部干扰源已断开连接”——“迭代异常兽” 彻底消失了。

赛道上,F1 赛车重新加速,引擎的轰鸣声再次变得均匀有力;屏幕前,艾拉和杰西相视一笑,他们不仅修复了系统,更摸清了 Swift 迭代的 “底层逻辑”。

📝 终极总结:Swift 迭代的 “黄金法则”

  1. Sequence 是 “入门契约”:只承诺 “能迭代一次”,适合懒加载、生成器场景,像 F1 的 “练习赛”—— 灵活但不追求稳定。
  2. Collection 是 “进阶契约”:多轮迭代、索引访问、数据稳定,适合需要反复操作的数据,像 F1 的 “正赛”—— 稳定且高效。
  3. AsyncSequence 是 “异步契约”:支持暂停、抛错,适合数据流场景,但要注意 “错误终止性” 和 “重试防重复”,像 F1 的 “夜间赛”—— 更复杂,但有专属的应对策略。
  4. 迭代器的 “值语义优先”:尽量用 struct 实现迭代器,避免共享可变状态,就像赛车的 “独立操控系统”—— 互不干扰,安全可控。

在这里插入图片描述

最后记住:Swift 的迭代看似简单,实则是 “协议驱动” 的精妙设计。当你写下for item in list时,背后是 Sequence、Collection、Iterator 的协同作战 —— 就像 F1 赛车的引擎、刹车、底盘完美配合,才能跑出最快的速度,也才能写出最稳定、最高效的代码。

在这里插入图片描述

那么,宝子们看到这里学到了吗?

感谢观赏,下次我们再会吧!8-)

Swift 迭代三巨头(中集):Sequence、Collection 与 Iterator 深度狂飙

2025年12月10日 10:10

在这里插入图片描述

🏁 AsyncSequence 迭代界的 “涡轮增压引擎”

如果说同步 Sequence 是 “自然吸气引擎”,那 AsyncSequence 就是为异步场景量身打造的 “涡轮增压引擎”。

它能从容应对 “元素不是现成的” 场景,比如赛车传感器的实时数据流、网络请求的分批响应,甚至是文件的逐行读取。

在本堂F1调教课中,您将学到如下内容:

  • 🏁 AsyncSequence 迭代界的 “涡轮增压引擎”
  • 异步协议的 “核心蓝图”
  • 实战:用 AsyncStream “驯服” 异步数据流
  • 🛑 异步迭代的 “紧急刹车”:取消机制
  • 消费异步序列:for await 的 “正确姿势”
  • 主动取消:Task.checkCancellation () 的 “保命操作”
  • 🔧 自定义 Collection 进阶:打造 “抗崩溃缓存器”
  • 升级版 RingBuffer:补上 “安全漏洞”
  • 升级点解析:为什么这么改?
  • 实战:用 SafeRingBuffer 缓存异步传感器数据
  • ⚠️ 中集收尾:“迭代异常兽” 的新阴谋

核心秘诀在于:它允许迭代器的next()方法 “暂停等待” 和 “抛出错误”,完美适配现代 APP 的异步需求。

在这里插入图片描述


异步协议的 “核心蓝图”

AsyncSequence 和 AsyncIteratorProtocol 的结构,和同步版本 “神似但更强大”,就像 F1 赛车的升级版底盘 —— 保留经典设计,却强化了抗冲击能力:

// 异步序列的核心协议,相当于异步迭代的“赛道规则”
public protocol AsyncSequence {
    associatedtype Element // 迭代的元素类型(比如传感器的温度值、转速值)
    associatedtype AsyncIterator: AsyncIteratorProtocol where AsyncIterator.Element == Element

    // 生成异步迭代器,每次调用都返回“全新的异步操控器”
    func makeAsyncIterator() -> AsyncIterator
}

// 异步迭代器协议,迭代的“动力输出核心”
public protocol AsyncIteratorProtocol {
    associatedtype Element
    // 关键升级:async(可暂停)+ throws(可抛错)
    mutating func next() async throws -> Element?
}

在这里插入图片描述

和同步迭代器最大的不同在于next()方法的修饰符:

  • async:表示这个方法可能 “暂停等待”—— 就像赛车在维修区等待加油,等元素准备好再继续前进;
  • throws:表示可能抛出错误 —— 比如传感器突然断开连接,能直接把 “故障信号” 传递给上层,避免系统 “瞎跑”。

在这里插入图片描述

实战:用 AsyncStream “驯服” 异步数据流

大多数时候,我们不用从头实现 AsyncSequence,而是用AsyncStream—— 它就像 “异步序列的快捷组装工具”,能轻松把回调式 API(比如传感器的observe方法)转换成优雅的异步序列。

比如要处理赛车的进度更新数据流,用 AsyncStream 能让代码 “清爽到飞起”:

// 生成赛车进度的异步流(比如从0%到100%的加载进度)
func makeProgressStream() -> AsyncStream<Double> {
    // continuation:异步序列的“控制中枢”,负责发送元素、结束序列
    AsyncStream { continuation in
        // 1. 监听传感器的进度更新(回调式API)
        let observerToken = progressManager.observe { currentFraction in
            // 发送当前进度值(比如0.1、0.2...1.0)
            continuation.yield(currentFraction)
            // 进度到100%时,关闭序列(避免内存泄漏!)
            if currentFraction == 1.0 {
                continuation.finish()
            }
        }

        // 2. 序列终止时的“收尾操作”(比如取消监听,释放资源)
        continuation.onTermination = { _ in
            progressManager.removeObserver(observerToken)
        }
    }
}

这段代码的精髓在于continuation的 “双向控制”:既能主动发送元素(yield),又能在结束 / 取消时清理资源(onTermination)—— 就像赛车的 “智能中控”,既能控制加速,又能在紧急时切断动力。


🛑 异步迭代的 “紧急刹车”:取消机制

异步任务最怕 “失控”—— 比如用户已经退出页面,传感器数据流还在后台跑,不仅浪费资源,还可能触发 “迭代异常兽” 设下的 “内存泄漏陷阱”。Swift 的取消机制,就是异步迭代的 “紧急刹车系统”,能让失控的任务 “及时停稳”。

在这里插入图片描述

消费异步序列:for await 的 “正确姿势”

要消费 AsyncSequence,得用for await(无错误)或for try await(有错误),编译器会自动帮我们处理 “暂停等待” 和 “循环推进”,就像赛车的 “自动换挡系统”:

// 消费进度流:用for await“等待并处理每一个进度值”
Task {
    do {
        // 循环等待进度更新,直到序列结束(continuation.finish()被调用)
        for await fraction in makeProgressStream() {
            print("当前进度:\(fraction * 100)%") // 输出:10%、20%...100%
        }
        print("进度更新完成!")
    } catch {
        print("处理失败:\(error)") // 捕获可能抛出的错误(比如传感器断开)
    }
}

如果序列的next()方法会抛错(比如网络请求失败),就必须用for try await,并在do-catch里处理错误 —— 这步绝不能省!否则错误会直接导致 Task 崩溃,就像赛车没装 “防撞栏”,一撞就报废。

在这里插入图片描述

主动取消:Task.checkCancellation () 的 “保命操作”

光靠外部取消还不够,异步迭代器内部得 “知道该停”。比如一个 “轮询传感器” 的迭代器,如果不检查取消状态,就算 Task 被取消,它还会继续跑 —— 这正是 “迭代异常兽” 最喜欢的漏洞。

解决办法是在next()里调用Task.checkCancellation(),主动 “检查刹车信号”:

// 轮询赛车传感器的异步迭代器
struct SensorPollingIterator: AsyncIteratorProtocol {
    typealias Element = SensorData // 传感器数据模型(比如转速、温度)
    
    mutating func next() async throws -> SensorData? {
        // 关键:每次迭代前检查“是否需要取消”,发现取消就抛错终止
        try Task.checkCancellation()
        
        // 模拟轮询:等待1秒后获取传感器数据(真实场景是调用硬件API)
        let data = await sensorManager.fetchLatestData()
        return data
    }
}

// 对应的异步序列
struct SensorSequence: AsyncSequence {
    typealias Element = SensorData
    typealias AsyncIterator = SensorPollingIterator
    
    func makeAsyncIterator() -> SensorPollingIterator {
        SensorPollingIterator()
    }
}

当 Task 被取消时,Task.checkCancellation()会抛出CancellationError,直接终止for await循环 —— 就像赛车的 “紧急熄火开关”,一旦触发,立刻停稳,不给 “迭代异常兽” 留任何可乘之机。

在这里插入图片描述

艾拉就曾踩过这个坑:之前没加checkCancellation(),导致用户退出页面后,传感器迭代器还在后台跑,内存直接飙到 200MB。加上这行代码后,内存泄漏问题 “迎刃而解”,杰西调侃道:“这行代码比 F1 的刹车盘还管用,一踩就停!”


🔧 自定义 Collection 进阶:打造 “抗崩溃缓存器”

上集我们实现了基础版RingBuffer(环形缓冲区),但面对异步数据流,它还不够 “强”—— 比如多线程访问会崩溃,缓存满了会覆盖旧数据却没预警。这集我们要给它 “升级加固”,打造成能应对异步场景的 “抗崩溃缓存器”,用来暂存赛车的传感器数据。

升级版 RingBuffer:补上 “安全漏洞”

先看优化后的代码,关键升级点都加了注释:

struct SafeRingBuffer<Element>: Collection {
    // 存储底层数据:用可选类型,因为要区分“空槽”和“有值”
    private var storage: [Element?]
    // 头指针:指向第一个有值元素的位置(类似队列的“出队口”)
    private var head = 0
    // 尾指针:指向第一个空槽的位置(类似队列的“入队口”)
    private var tail = 0
    // 当前元素个数:单独维护,避免遍历计算(提升性能)
    private(set) var count = 0
    // 线程安全锁:解决多线程访问的“竞态条件”(异步场景必备!)
    private let lock = NSLock()

    // 初始化:指定缓冲区容量(一旦创建,容量固定,避免动态扩容的性能损耗)
    init(capacity: Int) {
        precondition(capacity > 0, "容量必须大于0!否则缓冲区无法存储数据")
        storage = Array(repeating: nil, count: capacity)
    }

    // 入队:添加元素到缓冲区(核心操作)
    mutating func enqueue(_ element: Element) {
        lock.lock()
        defer { lock.unlock() } // 确保锁一定会释放,避免死锁

        // 1. 存储元素到尾指针位置
        storage[tail] = element
        // 2. 尾指针后移,超过容量就“绕回”开头(环形的关键!)
        tail = (tail + 1) % storage.count

        // 3. 处理“缓冲区满”的情况:满了就移动头指针,覆盖最旧的元素
        if count == storage.count {
            head = (head + 1) % storage.count
        } else {
            count += 1
        }
    }

    // 出队:移除并返回最旧的元素(可选操作,增强实用性)
    mutating func dequeue() -> Element? {
        lock.lock()
        defer { lock.unlock() }

        guard count > 0 else { return nil } // 空缓冲区,返回nil
        let element = storage[head]
        storage[head] = nil // 清空位置,避免内存泄漏
        head = (head + 1) % storage.count
        count -= 1
        return element
    }

    // MARK: - 遵守Collection协议的必备实现
    typealias Index = Int

    var startIndex: Index { 0 }
    var endIndex: Index { count }

    // 获取下一个索引(必须确保不越界)
    func index(after i: Index) -> Index {
        precondition(i < endIndex, "索引超出范围!不能超过endIndex")
        return i + 1
    }

    // 下标访问:通过“逻辑索引”获取元素(核心映射逻辑)
    subscript(position: Index) -> Element {
        lock.lock()
        defer { lock.unlock() }

        // 1. 检查逻辑索引是否合法(防呆设计,避免越界访问)
        precondition((0..<count).contains(position), "索引\(position)超出缓冲区范围(0..<\(count))")
        // 2. 关键:把“逻辑索引”映射到“实际存储位置”(环形的核心算法)
        let actualPosition = (head + position) % storage.count
        // 3. 强制解包:因为前面已经检查过合法性,这里一定有值
        return storage[actualPosition]!
    }
}

在这里插入图片描述

升级点解析:为什么这么改?

  1. 线程安全锁(NSLock):异步场景下,可能有多个 Task 同时调用enqueuesubscript,不加锁会导致 “头指针和尾指针混乱”—— 比如一个 Task 在写tail,另一个在读head,结果就是数据错乱。加锁后,这些操作会 “排队执行”,就像 F1 赛车按顺序进维修区,互不干扰。
  2. dequeue 方法:基础版只有入队,升级版增加出队,让缓冲区更像 “可控队列”,能主动清理旧数据,避免无用数据占用内存。
  3. 更严格的 precondition:每个关键操作都加了 “防呆检查”,比如索引越界、容量为 0 等,一旦出现错误会立刻崩溃(而非默默返回错误数据),方便我们快速定位问题 —— 就像赛车的 “故障诊断系统”,早发现早修复。

在这里插入图片描述

实战:用 SafeRingBuffer 缓存异步传感器数据

艾拉把这个 “安全环形缓冲区” 集成到了赛车数据系统里,用来暂存传感器的异步数据:

// 1. 创建容量为10的缓冲区(缓存最近10条传感器数据)
var sensorBuffer = SafeRingBuffer<SensorData>(capacity: 10)

// 2. 消费异步传感器序列,把数据存入缓冲区
Task {
    do {
        for try await data in SensorSequence() {
            sensorBuffer.enqueue(data)
            print("缓存数据:\(data),当前缓存数:\(sensorBuffer.count)")
        }
    } catch {
        print("传感器序列出错:\(error)")
    }
}

// 3. 另一个Task:从缓冲区读取数据,显示到仪表盘
Task {
    while !Task.isCancelled {
        if let latestData = sensorBuffer.dequeue() {
            dashboard.update(with: latestData) // 更新仪表盘
        }
        try await Task.sleep(nanoseconds: 1_000_000_000) // 每秒读一次
    }
}

这套组合拳下来,传感器数据的 “接收 - 缓存 - 展示” 流程变得 “稳如泰山”—— 就算传感器数据突发暴涨,缓冲区也能 “吞得下、吐得出”,再也不会出现之前的卡顿或崩溃。

在这里插入图片描述


⚠️ 中集收尾:“迭代异常兽” 的新阴谋

就在艾拉和杰西以为 “异步赛道” 已经安全时,新的危机突然爆发 —— 仪表盘显示的赛车转速数据 “忽高忽低”,明明传感器传来的是 10000 转,仪表盘却偶尔显示 15000 转。杰西调出日志,发现SafeRingBuffersubscript访问时,偶尔会返回 “重复数据”。

在这里插入图片描述

“不对劲,” 艾拉皱眉,“我们加了锁,逻辑索引也没问题,怎么会出现重复?” 她盯着代码看了半天,突然发现AsyncSequencenext()方法在 “抛出错误后,居然没有清理已发送的元素”—— 这意味着,当传感器短暂断连又重连时,迭代器会 “重复发送上一次的元素”,而缓冲区没做 “去重” 处理,导致仪表盘数据错乱。

原来 “迭代异常兽” 根本没离开,它只是换了个招数 —— 利用异步序列的 “错误恢复漏洞”,制造数据重复,试图干扰赛车手的判断。而更可怕的是,杰西在日志里发现了 “数据篡改” 的痕迹:有几条传感器数据的 “校验码不匹配”,这说明 “迭代异常兽” 不仅要制造混乱,还要篡改核心数据,让赛车失控!

在这里插入图片描述

下一集,艾拉和杰西将直面最凶险的挑战:破解异步序列的 “错误恢复漏洞”,给SafeRingBuffer加上 “数据校验” 功能,彻底粉碎 “迭代异常兽” 的阴谋。而这场对决的关键,就藏在AsyncSequence的 “错误传播机制” 和 Collection 的 “数据一致性保障” 里 —— 他们能成功吗?

让我们拭目以待!

Swift 迭代三巨头(上集):Sequence、Collection 与 Iterator 深度狂飙

2025年12月10日 10:08

在这里插入图片描述

🏁 引子:赛道惊魂!迭代引擎的致命故障

赛道上的引擎轰鸣震耳欲聋,天才 Swift 工程师艾拉紧盯着赛车数据面板,额角的冷汗浸透了队服 —— 连续三次,实时处理赛车传感器数据的系统在迭代时突然宕机,就像一辆顶级 F1 赛车在蒙扎赛道的直道上突然爆胎。

资深架构师杰西拍了拍她的肩,递过闪烁代码的电脑:“问题不在硬件,而在 Swift 迭代的核心协议 ——Sequence 和 Collection 的契约规则,我们被它们表面的简单给骗了。”

在本堂F1调教课中,您将学到如下内容:

  • 🏁 引子:赛道惊魂!迭代引擎的致命故障
  • 📌 揭秘 Sequence:迭代界的 “起步引擎”
  • 核心协议架构
  • 迭代器为何偏爱 struct?
  • Sequence 的两大关键特性
  • 实战案例:stride 的 “赛道表演”
  • 🏎️ Collection 登场:给迭代装上 “稳定尾翼”
  • Collection 的核心承诺
  • 核心协议定义
  • 注意事项:Set 与 Dictionary 的 “特殊规则”
  • 🔧 for...in 的真相:赛道下的机械核心
  • 语法糖脱糖:暴露底层逻辑
  • 自定义 Sequence 实战:倒计时器
  • 致命陷阱:迭代时修改集合(赛道上改零件!)
  • 安全方案:让迭代与修改 “分道扬镳”
  • 🚦 上集收尾:异步赛道的终极挑战

这对搭档即将掀起一场针对 Swift 迭代底层的 “狂飙对决”,而他们的对手,是潜伏在代码深处、专门制造崩溃的 “迭代异常兽”。

在这里插入图片描述


📌 揭秘 Sequence:迭代界的 “起步引擎”

Sequence 是 Swift 迭代体系的 “最小作战单位”,它的契约如同 F1 赛车的起步规则 ——“只要有人需要迭代器,它就会交出一个能持续输出元素直到耗尽的家伙”。

这个规则看似简单,却暗藏玄机,是所有迭代操作的基石。

核心协议架构

要成为 Sequence 的 “合格选手”,必须遵守两大硬性要求:

  1. 定义两个关联类型:Element(迭代的元素类型)和Iterator(迭代器类型)。
  2. 实现makeIterator()方法,每次调用都返回一个全新的迭代器。
public protocol Sequence {
    associatedtype Element // 迭代的元素类型
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element // 关联的迭代器类型

    func makeIterator() -> Iterator // 生成新迭代器的核心方法
}

而迭代器本身必须遵循IteratorProtocol,暴露一个带mutating修饰的next()方法 —— 这是迭代的 “动力输出轴”:

public protocol IteratorProtocol {
    associatedtype Element
    mutating func next() -> Element? // 每次调用返回下一个元素,耗尽则返回nil
}

在这里插入图片描述

迭代器为何偏爱 struct?

你会发现绝大多数迭代器都用 struct 实现,这绝非偶然,而是 Swift 的 “精妙设计”:

  • next()mutating方法,值类型(struct)的迭代器能直接更新自身状态(比如当前位置),无需额外同步操作。
  • 复制迭代器时会得到一个 “独立副本”,就像给赛车复制了一套完全相同的操控系统,两个迭代器各自推进、互不干扰,彻底避免了共享可变状态的 “赛道事故”。

虽然类也能实现 IteratorProtocol,但值语义天生契合迭代器的 “单次推进、独立可控” 契约,就像 F1 赛车的专属定制部件,远比通用部件更靠谱。

Sequence 的两大关键特性

  1. 单遍迭代特性:Sequence 只承诺 “能迭代一次”,就像 F1 赛道的单圈比赛,跑完就结束。有些迭代器是 “一次性消耗品”,用完后永远返回 nil,比如懒加载 I/O 流、生成器式 API。
  2. 每次生成新迭代器makeIterator()每次调用都该返回全新实例。就像每次赛车出发前都要重置状态,确保每轮迭代都是 “干净起步”,避免多轮循环互相干扰。

在这里插入图片描述

实战案例:stride 的 “赛道表演”

stride(from:to:by:)是 Sequence 的典型代表,它无需分配数组,就能像赛车按固定间距前进一样,生成算术序列:

// 从0度到360度,每30度取一个值,像赛车按固定路线过弯
for angle in stride(from: 0, through: 360, by: 30) {
    print(angle) // 输出:0、30、60...360
}

这个 Sequence 不会在内存中存储所有角度值,而是每次调用next()时动态生成 —— 就像赛车实时响应赛道指令,而非提前规划所有路线。这正是 Sequence 的核心魅力:按需生成,高效灵活。


在这里插入图片描述

🏎️ Collection 登场:给迭代装上 “稳定尾翼”

Sequence 虽然灵活,但 “单遍迭代” 的特性在很多场景下就像赛车没有尾翼 —— 缺乏稳定性。

这时,Collection 闪亮登场,它在 Sequence 的基础上增加了三大 “硬核保障”,让迭代像 F1 赛车在赛道上疾驰一样稳如泰山。

Collection 的核心承诺

  1. 支持多轮迭代:无论迭代多少次,结果都一致(只要集合本身不被修改)。
  2. 稳定的顺序:元素的迭代顺序固定(除非集合文档明确说明顺序可变)。
  3. 索引与计数:支持索引访问、下标操作和 count 属性,能随时掌握 “赛道进度”。

Swift 中的 Array、Dictionary、Set 都是 Collection 的忠实拥护者,它们凭借这些特性成为日常开发的 “主力赛车”。

在这里插入图片描述

核心协议定义

public protocol Collection: Sequence {
    associatedtype Index: Comparable // 可比较的索引类型

    var startIndex: Index { get } // 起始索引(赛道起点)
    var endIndex: Index { get } // 结束索引(赛道终点)

    func index(after i: Index) -> Index // 获取下一个索引(下一个弯道)
    subscript(position: Index) -> Element { get } // 下标访问元素(精准定位赛道位置)
}

这些要求解锁了大量优化:map可以提前分配恰好的存储空间,count无需遍历整个集合就能获取 —— 就像赛车的空气动力学设计,让每一次操作都更高效。

注意事项:Set 与 Dictionary 的 “特殊规则”

虽然 Set 和 Dictionary 都遵循 Collection,但它们的迭代顺序可能在修改后变化。这就像赛车在不同赛道条件下的路线调整,协议本身不承诺顺序稳定,如果你需要固定顺序,一定要选择明确标注 “顺序不变” 的集合类型(比如 Array)。

在这里插入图片描述


🔧 for...in 的真相:赛道下的机械核心

我们天天用for item in list,就像赛车手天天踩油门,却很少有人知道底层的 “传动系统” 是如何工作的。其实,这个看似简单的语法糖,背后藏着 Swift 编译器的 “精妙操作”。

语法糖脱糖:暴露底层逻辑

for item in container的本质的是以下代码的简化版,这就是迭代的 “核心机械结构”:

// 1. 生成集合的迭代器(给赛车装上变速箱)
var iterator = container.makeIterator()
// 2. 循环调用next(),直到返回nil(变速箱持续换挡,直到终点)
while let element = iterator.next() {
    print(element)
}

在这里插入图片描述

自定义 Sequence 实战:倒计时器

为了让你更直观理解,我们来打造一个 “倒计时 Sequence”,就像赛车起跑前的倒计时:

struct Countdown: Sequence {
    let start: Int // 倒计时起始值

    // 生成迭代器,每次调用都返回新实例
    func makeIterator() -> Iterator {
        Iterator(current: start)
    }

    // 嵌套迭代器结构体,遵循IteratorProtocol
    struct Iterator: IteratorProtocol {
        var current: Int // 当前倒计时值

        mutating func next() -> Int? {
            // 小于0则结束倒计时,返回nil
            guard current >= 0 else { return nil }
            // 先返回当前值,再自减(关键:延迟递减,确保起始值能被返回)
            defer { current -= 1 }
            return current
        }
    }
}

// 测试:从3开始倒计时
for number in Countdown(start: 3) {
    print(number) // 输出:3、2、1、0
}

运行这段代码时,编译器会自动将for...in转化为前面的while循环。由于迭代器是 struct(值类型),如果中途复制迭代器,两个副本会各自独立推进 —— 就像两辆完全相同的赛车,从同一位置出发,却能各自完成自己的赛程。

在这里插入图片描述

致命陷阱:迭代时修改集合(赛道上改零件!)

最容易触发 “迭代崩溃” 的操作,就是在循环中修改集合的底层存储。这就像赛车在高速行驶时突然更换轮胎,必然会失控翻车。

看这个典型的 “死亡代码”:

struct TodoItem {
    var title: String
    var isCompleted: Bool // 是否完成
}

var todoItems = [
    TodoItem(title: "发布技术博客", isCompleted: true),
    TodoItem(title: "录制播客", isCompleted: false),
    TodoItem(title: "审核PR", isCompleted: true),
]

// 错误示范:迭代时删除元素
for item in todoItems {
    if item.isCompleted,
       // 每次都扫描整个数组找索引,效率极低
       let index = todoItems.firstIndex(where: { $0.title == item.title }) {
        todoItems.remove(at: index) // ⚠️ 致命错误:迭代时集合被修改!
    }
}

这段代码会直接崩溃,原因很简单:数组迭代器假设底层存储 “稳定不变”,删除元素后,数组的内存布局发生偏移,迭代器就像失去方向的赛车,再也找不到下一个元素的位置。更糟的是,firstIndex每次都会扫描整个数组,让时间复杂度飙升到 O (n²),堪称 “性能灾难”。

在这里插入图片描述

安全方案:让迭代与修改 “分道扬镳”

解决这个问题的核心,就是让迭代和修改互不干扰,就像赛车比赛和维修工作分开进行:

  1. 使用 removeAll (where:):让集合自己管理迭代,高效安全:
todoItems.removeAll(where: \\.isCompleted) // 一次遍历完成删除,O(n)效率
  1. 创建过滤副本:保留原集合,生成新的过滤后集合:
let openTodos = todoItems.filter { !\$0.isCompleted } // 原集合不变,安全无风险

这两种方案都遵循了 “迭代不修改,修改不迭代” 的黄金法则,彻底避开了 “迭代异常兽” 设下的陷阱。


🚦 上集收尾:异步赛道的终极挑战

艾拉和杰西终于破解了同步迭代的核心密码,修复了赛车数据处理系统的崩溃问题。但他们还没来得及庆祝,新的警报又响了 —— 实时赛车传感器数据是异步流式传输的,传统的同步迭代根本无法应对。

在这里插入图片描述

Swift 并发体系中的AsyncSequence,就像迭代界的 “涡轮增压引擎”,能处理异步生成的元素,但它也藏着更棘手的 “延迟陷阱” 和 “取消难题”。

下一集,艾拉和杰西将深入异步迭代的 “死亡赛道”,解锁for await的终极用法,直面 “异步延迟魔” 的挑战。而 “迭代异常兽” 的真正阴谋 —— 利用内存泄漏摧毁整个赛车数据系统,也将浮出水面。他们能否凭借对 AsyncSequence 和自定义 Collection 的深刻理解,再次化险为夷?

UniApp H5 代理失效的终极替代方案

作者 Harry技术
2025年12月10日 10:05

UniApp H5 代理失效的终极替代方案

放弃 manifest.json,用 vite.config.js 配置(推荐,100% 生效)

在 UniApp(Vue3)H5 端配置代理不生效是高频问题,核心原因是 UniApp 对 H5 端 devServer 代理的解析规则、配置格式与纯 Vue CLI/Vite 项目存在差异,且容易忽略多环境、路径匹配、编译模式等细节。以下是针对性的排查和修复方案:

一、先确认核心前提(90% 问题出在这里)

UniApp 的 H5 端代理仅在 H5 开发模式(npm run dev:h5 下生效,且需满足:

  1. 配置文件是 manifest.json(根目录),且修改后必须重启 H5 开发服务(停止 dev:h5 后重新启动);
  2. 前端请求必须是相对路径(不能写 http://localhost:8080/dev-api/xxx 这类绝对路径,需直接写 /dev-api/xxx);
  3. 仅 H5 端生效,小程序 / APP 端不支持 devServer 代理(需用真机调试或配置跨域白名单)。

二、修正 manifest.json 代理配置格式(关键)

UniApp 对 h5.devServer.proxy 的配置格式有严格要求,你的配置看似正确,但需确认以下细节:

正确的 manifest.json 配置示例(JSON 格式严格)
{
  "vueVersion": "3",
  "h5": {
    "devServer": {
      "proxy": {
        "/dev-api": {
          "target": "http://192.168.31.24:9999",
          "changeOrigin": true,
          "pathRewrite": {
            "^/dev-api": ""
          },
          // 新增:UniApp 部分版本需显式开启 secure(非 HTTPS 目标设为 false)
          "secure": false,
          // 可选:开启日志,排查代理是否命中
          "logLevel": "debug"
        }
      },
      "https": false,
      // 可选:指定 H5 开发服务端口,避免端口冲突
      "port": 8080
    }
  }
}

三、代理不生效的高频排查点(逐一验证)

1. 检查请求路径是否为相对路径(最常见)

错误示例(绝对路径,不走代理):

// ❌ 绝对路径会绕过代理,直接请求 localhost:8080
axios.get('http://localhost:8080/dev-api/user/info')

正确示例(相对路径,触发代理):

// ✅ 相对路径,会被代理转发到 192.168.31.24:9999/user/info
axios.get('/dev-api/user/info')

// 或配置 axios baseURL(推荐)
const request = axios.create({
  baseURL: '/dev-api', // 开发环境
  timeout: 5000
})
request.get('/user/info')
2. 确认 UniApp 编译模式(Vue3 需匹配 Vite 规则)

UniApp Vue3 版本默认使用 Vite 编译,而 Vite 的代理配置中,pathRewrite 需替换为 rewrite(部分 UniApp 版本未兼容旧字段)。

修改为 Vite 兼容的代理配置:

{
  "h5": {
    "devServer": {
      "proxy": {
        "/dev-api": {
          "target": "http://192.168.31.24:9999",
          "changeOrigin": true,
          // Vite 中用 rewrite 替代 pathRewrite(UniApp Vue3 需此写法)
          "rewrite": "(path) => path.replace(/^/dev-api/, '')"
        }
      }
    }
  }
}
3. 检查后端是否限制跨域(代理生效但后端拦截)

即使代理配置正确,若后端未配置跨域允许,仍会报错:

  • 后端需添加跨域响应头(以 SpringBoot 为例):

    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOrigins("http://localhost:8080") // H5 开发地址
                    .allowedMethods("GET", "POST", "PUT", "DELETE")
                    .allowCredentials(true)
                    .maxAge(3600);
        }
    }
    
  • 若后端用 Nginx 代理,需添加:

    add_header Access-Control-Allow-Origin http://localhost:8080;
    add_header Access-Control-Allow-Credentials true;
    
4. 排查 UniApp 版本兼容问题

部分 UniApp 版本(如 3.0.0~3.2.0)存在 H5 代理解析 Bug,可:

  • 升级 UniApp 核心版本:

    npm update @dcloudio/uni-app @dcloudio/uni-cli-shared
    
  • 或降级到稳定版本(如 3.4.0+):

    npm install @dcloudio/uni-app@3.4.0
    
5. 验证代理是否命中(开启日志)

devServer 中添加 logLevel: "debug",重启 H5 服务后,查看终端日志:

  • 若日志中出现 [proxy] proxying request /dev-api/user/info to http://192.168.31.24:9999,说明代理已命中;
  • 若无此日志,说明路径匹配失败(检查请求路径是否以 /dev-api 开头)。

四、UniApp H5 代理失效的终极替代方案

若以上配置仍不生效,可绕过 manifest.json,直接修改 Vite 配置(UniApp Vue3 基于 Vite 编译):

  1. 在项目根目录创建 vite.config.js(UniApp 会自动识别);
  2. 写入 Vite 原生代理配置:
import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';

export default defineConfig({
  plugins: [uni()],
  server: {
    // 直接配置 Vite 代理(优先级高于 manifest.json)
    proxy: {
      '/dev-api': {
        target: 'http://192.168.31.24:9999',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/dev-api/, ''),
        logLevel: 'debug'
      }
    },
    // 确保端口与 manifest.json 一致
    port: 8080,
    https: false
  }
});
  1. 重启 npm run dev:h5,此方案优先级高于 manifest.json 中的 h5.devServer,几乎 100% 生效。

五、小程序 / APP 端的替代方案(重要)

UniApp 的 devServer 代理仅对 H5 端生效,小程序 / APP 端无法使用:

  • 小程序端:需在微信开发者工具中配置 “不校验合法域名”(仅调试),或在微信公众平台添加后端域名白名单;
  • APP 端:无需跨域(APP 无同源策略),直接请求后端真实地址 http://192.168.31.24:9999/xxx 即可。

总结

UniApp H5 端代理不生效的核心修复步骤:

  1. 确保请求为相对路径/dev-api/xxx);
  2. 重启 H5 开发服务(修改配置后必须重启);
  3. 优先使用 vite.config.js 配置 Vite 原生代理(替代 manifest.json);
  4. 验证后端跨域配置,避免代理生效但后端拦截。

按以上步骤配置后,请求 /dev-api/user/info 会被正确代理到 http://192.168.31.24:9999/user/info(移除 /dev-api 前缀)。

放弃 manifest.json,用 vite.config.js 配置(推荐,100% 生效)

UniApp Vue3 底层基于 Vite 编译,直接在 vite.config.js 中写 Vite 原生代理配置(支持 JS 函数),优先级高于 manifest.json,且不会有格式兼容问题:

1. 根目录创建 / 修改 vite.config.js
import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';

export default defineConfig({
  plugins: [uni()],
  server: {
    // H5 开发服务器代理配置(Vite 原生语法,支持函数)
    proxy: {
      '/dev-api': {
        target: 'http://192.168.31.24:9999',
        changeOrigin: true,
        // Vite 原生 rewrite 配置(函数形式,正确写法)
        rewrite: (path) => path.replace(/^/dev-api/, ''), 
        secure: false, // 非 HTTPS 目标需设为 false
        logLevel: 'debug' // 开启日志,便于排查
      }
    },
    port: 8080, // 与 manifest.json 保持一致(可选)
    https: false
  }
});
2. 清理 manifest.json 中的无效代理配置

manifest.jsonh5.devServer.proxy 部分删除(避免冲突),保留必要配置即可:

{
  "vueVersion": "3",
  "h5": {
    "devServer": {
      "https": false,
      "port": 8080 // 仅保留端口/HTTPS 配置,代理交给 vite.config.js
    }
  }
}
3. 重启 H5 开发服务

停止当前 npm run dev:h5,重新执行命令,代理即可生效。

方案二:强制适配 manifest.json(仅 JSON 格式,兼容旧版本)

如果必须在 manifest.json 中配置,需避开 JS 函数,改用 UniApp 兼容的 字符串替换规则(部分 UniApp 版本支持 pathRewrite 字符串格式):

{
  "vueVersion": "3",
  "h5": {
    "devServer": {
      "proxy": {
        "/dev-api": {
          "target": "http://192.168.31.24:9999",
          "changeOrigin": true,
          // 改用 pathRewrite(JSON 字符串格式,UniApp 可解析)
          "pathRewrite": {
            "^/dev-api": ""
          },
          "secure": false
        }
      },
      "https": false
    }
  }
}
关键说明:
  • manifest.json 是纯 JSON 文件,只能写键值对 / 字符串 / 数字 / 布尔值,不能写函数、正则表达式(/^/dev-api/ 这种正则在 JSON 中会被解析为字符串,部分版本 UniApp 能兼容);
  • 此方案仅适配 UniApp 对 pathRewrite 的兼容解析,若仍报错,优先用方案一(vite.config.js)。

报错原因补充

你之前写的 "rewrite": "(path) => path.replace(/^/dev-api/, '')" 存在两个问题:

  1. JSON 中无法解析箭头函数,最终 rewrite 是字符串类型,而非函数,导致 UniApp/Vite 执行 opts.rewrite(path) 时报错;
  2. 正则表达式写法错误:/^/dev-api/ 应为 /^/dev-api/(JSON 中需转义反斜杠,或在 JS 中直接写)。

验证是否生效

重启 npm run dev:h5 后,前端发送请求:

// 示例:用 axios 发送请求
axios.get('/dev-api/user/info')

查看终端日志(开启 logLevel: 'debug' 后),若出现:

[proxy] proxying request /dev-api/user/info to http://192.168.31.24:9999/user/info

说明代理已成功移除 /dev-api 前缀,配置生效。

最终建议

UniApp Vue3 项目优先使用 vite.config.js 配置代理:

  • 兼容 Vite 原生语法,支持函数 / 正则,无格式限制;
  • 避开 manifest.json 的 JSON 格式约束;
  • 配置逻辑与纯 Vite 项目一致,维护成本更低。

听说你毕业很多年了?那么来做题吧🦶

作者 _大学牲
2025年12月9日 17:06

序言

自别学宫,岁月如狗,撒腿狂奔,不知昔日学渣今何在?
左持键盘,右捏鼠标,微仰其首,竟在屏幕镜中显容颜!
心中微叹,曾几何时,提笔杀题,犹如天上人间太岁神。
知你想念,故此今日,鄙人不才,出题小侠登场献丑了。

起因

截屏2025-12-09 14.33.41.png 在这一篇《从 0 到上架:用 Flutter 一天做一款功德木鱼》文章中,我的 木鱼APP 最终陨落了,究其原因就是这种 APP 在  商店中太,如果你要想成功上架,无异于要脱胎换骨。

后面有时间了,我打算将其重铸为 修仙敲木鱼,通过积攒鱼力,突破秩序枷锁,成就 无上木鱼大道


因此,我吸取失败的教训,着力于开发一款比较 独特的APP ,结合这个AI大时代的背景,这款AI智能 出题侠 就应运而生了。最后总算是不辜负我的努力,成功上架了。
接下来就向大家说说 它的故事 吧。

截屏2025-12-09 14.44.38.png

实践

一. 准备阶段

1.流程设计

flowchart TD

  %% 启动与登录
  A[启动页] --> B[无感登录]
  B --> C[进入导航页]

  %% 主壳导航
  C --> H[首页]
  C --> R[记录]
  C --> T[统计]
  C --> P[我的]

  %% 首页出题 → 记录
  H --> H1[输入主题/高级设置]
  H1 --> H2[生成题目]
  H2 --> H3[提示后台生成]
  H3 --> R

  %% 记录 → 答题/详情
  R --> R1{记录状态}
  R1 -->|进行中| Q[进入答题页]
  R1 -->|已完成| RD[记录详情]
  RD --> E[秒懂百科]

  %% 答题流程
  Q --> Q1[作答 / 提交]
  Q1 --> Q2[保存成绩]
  Q2 --> R

  %% 统计页
  T --> T1[刷新统计数据]

  %% 我的页
  P --> P1[设置/关于]
  P --> P2[隐私政策]
  P --> P3[注销]
  P3 --> |确认后| P4[清除 token / 返回未登录状态]

2. 素材获取

截屏2025-12-09 15.22.28.png

App的 logo 和其中的 插图,我都是用的 Doubao-Seedream-4.0 生成的,一次效果不行就多生成几次,最终还是能得到相对满意的结果。

到我写文章的时候,已经有了 Doubao-Seedream-4.5,大家可以去体验体验。

截屏2025-12-09 15.27.20.png

二. 开发阶段

1. 前端

前端毫无争议的使用的是 Flutter,毕竟要是以后发行 Android 也是非常方便的,无需重新开发。再结合 Trae,我只需要在口头上指点指点,那是开发的又快又稳,非常的轻松加愉快。

无须多言,这就是赛博口嗨程序员!🫡

截屏2025-12-09 15.44.31.png

2. 后端

后端就是,世界上最好的编程语言 JAVA 了,毕竟 SpringBoot 可太香了,我也是亲自上手。

2.1 依赖概览
<!-- ✅ 核心 LangChain4j 依赖 -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>${langchain4j.version}</version>
</dependency>

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
   <version>4.12.0</version>
</dependency>

<!-- Validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- Spring Aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- HuTool -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>${hutool.version}</version>
</dependency>

<!-- MyBatis-Plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>

<!-- MyBatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>${mybatis-spring-boot-starter.version}</version>
</dependency>

<!-- MyBatis-PageHelper -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>${pagehelper.version}</version>
</dependency>

<!-- Sa-Token -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot3-starter</artifactId>
    <version>${sa-token.version}</version>
</dependency>

<!-- Sa-Token 整合 RedisTemplate -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis-template</artifactId>
    <version>1.42.0</version>
</dependency>

<!-- 提供 Redis 连接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<!-- Knife4j -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.8.6</version>
</dependency>
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.4.0</version>
</dependency>

<!-- 短信验证码 -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-auth</artifactId>
    <version>0.2.0-beta</version>
</dependency>

<!-- 阿里云短信服务 SDK -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>dysmsapi20170525</artifactId>
    <version>2.0.24</version> <!-- 使用最新版 -->
</dependency>

      ......

这依赖一添加,满满的安全感:

  • 数据库:我有MyBatis。
  • AI:我有LangChain4j。
  • 登录鉴权:我有Sa-Token。
  • ......
2.2 接口限流

要说到项目中最需要重点关注的部分,接口限流 无疑排在首位。无论是短信发送接口,还是调用 AI 的接口,一旦被恶意刷取或滥用,都可能导致资源耗尽、费用爆炸💥。

因此,本项目采用 注解 + AOP + Redis 的方式,构建了一套 轻量级、可配置、低侵入 的接口限流方案,在不影响业务代码结构的前提下,对高风险接口进行有效保护,确保系统在高并发场景下依然稳定可控。


代码示例:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {

    /**
     * 限流 key 的前缀(唯一标识一个限流维度)
     */
    String key();

    /**
     * 时间窗口,单位秒
     */
    long window() default 60;

    /**
     * 时间窗口内允许的最大次数
     */
    int limit() default 10;

    /**
     * 是否按 IP 维度区分限流
     */
    boolean perIp() default false;

    /**
     * 是否按用户维度区分限流
     */
    boolean perUser() default false;

    /**
     * 自定义提示信息
     */
    String message() default "请求过于频繁,请稍后再试";
}
@Slf4j
@Aspect
@Component
public class RateLimitAspect {

    @Resource
    private RateLimitRedisUtil rateLimitRedisUtil;

    @Around("@annotation(org.dxs.problemman.annotation.RateLimit)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RateLimit rateLimit = method.getAnnotation(RateLimit.class);

        String key = buildKey(rateLimit);

        boolean allowed = rateLimitRedisUtil.tryAcquire(
                key, rateLimit.limit(), rateLimit.window());

        if (!allowed) {
            log.warn("限流触发:key={}, limit={}, window={}s", key, rateLimit.limit(), rateLimit.window());
            throw new RateLimitException(rateLimit.message());
        }

        return joinPoint.proceed();
    }

    private String buildKey(RateLimit rateLimit) {
        StringBuilder key = new StringBuilder("ratelimit:").append(rateLimit.key());
        if (rateLimit.perIp()) {
            String ip = IpUtils.getIpAddress();
            key.append(":").append(ip);
        }

        if (rateLimit.perUser()) {
            String userId = StpUtil.getLoginIdAsString();
            key.append(":").append(userId);
        }
        return key.toString();
    }
}

在使用上,开发者只需在需要保护的接口方法上添加 @RateLimit 注解,即可声明该接口的限流规则。通过 key 区分不同业务场景,并可按需开启 IP 维度用户维度 的限流控制,从而精确限制单一来源或单一用户的访问频率。

@NotLogin
@PostMapping("/sms")
@RateLimit(key = "sms", limit = 200, window = 3600, message = "短信调用太频繁,请1小时后再试")
public AjaxResult<String> sms(@Validated @RequestBody PhoneDTO dto) {
    return AjaxResult.success(loginService.sms(dto));

}
@PostMapping("/generate")
@RateLimit(key = "generate", limit = 3, perUser = true, window = 3600*24, message = "每人每天仅可体验三次!")
@Operation(summary = "依据条件,生成题目")
public AjaxResult<Object> generate(@Validated @RequestBody GenerateRequestDTO dto) throws IOException, InterruptedException {
    questionService.generate(dto);
    return AjaxResult.success();
}

请求进入时,AOP 切面会拦截带有 @RateLimit 注解的方法,根据注解配置动态构建限流 Key,并交由 Redis 进行原子计数校验;若在指定时间窗口内超过访问上限,则直接中断请求并返回友好的限流提示,同时记录告警日志,便于后续排查与监控。

限流 Key 的结构统一为:

ratelimit:{业务key}:{ip}:{userId}

通过 Redis 过期机制自然形成时间窗口,既保证了并发场景下的准确性,也避免了额外的清理成本。

三. 上架备案

1.前提

截屏2025-12-08 21.51.13.png

截屏2025-12-08 21.51.51.png

想要备案上架,域名和服务器是必不可少的。

  • 域名:你是在手机上,其实不需要啥好域名,因为大家根本看不见,十几块钱一年就行了。
  • 服务器:花了三四百买个轻量级服务器就行了。

2. 阿里云备案

截屏2025-12-08 22.02.14.png

截屏2025-12-08 22.06.22.png

💡 小建议
像这里阿里云备案和获取管局审核,可以先行一步,在app开发完之前就可以提交了。
因为管局审核是要2-3周的,有可能我们的小APP开发好了,备案号都没有下来。

3. 苹果商店上架

截屏2025-12-09 14.44.38.png

信息这里按部就班,按照提示,一点点填写完成就行了,没啥特别的。

踩坑总结

  • 测试账号:你的APP中只要有登录模块,就一定要提供测试账号,就算你纯手机号登录也不行,必须提供测试账号。
  • 注销功能:苹果商店硬性要求,必须要有 注销功能,但其实也没那么严格,你只要UI显示是那么回事就行,就当 退出登录 功能去做就行了。

4. 预览图制作推荐

截屏2025-12-09 16.47.09.png截屏2025-12-09 16.46.11.png

注意是需要订阅付费的,要是有什么更好的,希望评论告知。😂

展望

在后续规划的新功能中,将以大学期末考试复习作为典型应用场景进行设计。通常在期末阶段,老师都会给出明确的考试范围、复习大纲以及相关资料文档,而临阵磨枪的学生往往面临资料繁多、重点分散、不知从何下手的问题。

针对这一痛点,用户可以将老师提供的复习文档直接导入 App,系统会基于 AI 对内容进行自动解析与归纳,将零散的文本信息整理为思维导图形式的知识图谱,清晰呈现各章节与知识点之间的层级与关联关系。

在此基础上,用户可围绕任意知识节点一键生成对应题目,用于针对性复习与自测,做到哪里薄弱练哪里。通过文档 → 知识图谱 → 题目练习 的闭环方式,帮助用户更高效地理解重点内容,提升期末复习的针对性与整体效率。

46a1b81cde50407982da18d76b651dcf.gif

😭 作为大学毕业生的深彻感悟。

支持

ScreenRecording_12-08-2025 22-10-28_1.gif

AppStore 搜索 出题侠 即可,每个用户每天可免费使用三次。

感谢大家的支持与反馈。🙏

苹果开发者后台叕挂了,P0级别的报错!

作者 iOS研究院
2025年12月9日 11:40

背景

AppStore的开发者后台,今天早上8点开始又摆烂了。只要进入Apps页面,点击App详情自动触发退出功能。

dad0d392646571955680b365a73d7c42.jpg

还有同行直接直接开启502状态模式

ScreenShot_2025-12-09_112425_505.png

对于这种严重使用线上用户体验的操作,P0级别的事故当之无愧。

当然,这已经不是今年第一次了摆烂了。上一次摆烂的时候是开发者传包之后,出现了不发邮件或者邮件错乱的情况

大致情况为等待审核邮件已经收到了,未能收到状态更新为准备提交的邮件。

导致部分开发者误以为自己忘记了点击提交审核按钮。

希望果子除了对开发者严格之外,也严律利己。不要给开发者增加游戏难度。

目前苹果后台已经恢复正常,开发者可以自行进行提交审核的操作。

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

❌
❌