阅读视图

发现新文章,点击刷新页面。

iOS26适配指南之通知

介绍

  • UIKit 带来了强类型通知,为通知系统带来了期待已久的类型与并发安全性。
  • 不再使用基于字符串的标识符以及通过userInfo字典传递数据,该种方式存在线程安全、拼写错误、类型转换等问题。
  • 通过全新的并发安全通知协议NotificationCenter.MainActorMessage(主线程消息)与NotificationCenter.AsyncMessage(异步消息)可以做到编译时检测,从而提前发现潜在的问题。
  • 与老版本的通知系统完全兼容。

使用

  • 代码。
import UIKit

public class NotificationSubject {
    static let shared = NotificationSubject()
}

// MARK: - 消息类型
public struct CustomMainActorMessage: NotificationCenter.MainActorMessage {
    // 发送者
    public typealias Subject = NotificationSubject
    public static var name: Notification.Name {
        .init("CustomMainActorMessage")
    }

    // 数据
    let info: String
}

class ViewController: UIViewController {
    lazy var label: UILabel = {
        let label = UILabel()
        label.frame = CGRect(x: 0, y: 0, width: 300, height: 60)
        label.textAlignment = .center
        label.text = "暂无通知消息"
        label.center = view.center
        return label
    }()

    var mainActorMessageToken: NotificationCenter.ObservationToken!

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(label)
        // 监听通知
        mainActorMessageToken = NotificationCenter.default.addObserver(of: NotificationSubject.shared, for: CustomMainActorMessage.self) { message in
            self.label.text = "\(message.info)"
        }
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // 发送通知
        NotificationCenter.default.post(CustomMainActorMessage(info: "you have a new message"), subject: NotificationSubject.shared)
    }

    deinit {
        NotificationCenter.default.removeObserver(mainActorMessageToken)
    }
}
  • 效果。

通知.gif

学会在Cursor中使用Rules生成代码后可以躺平了吗?

 

嗨,我是辉哥,一个致力于使用AI技术搞副业的超级个体

之前我们给DeepWrite(内容多平台分发工具)总结出了一套完整的Rules规则,最终的期望是让Cursor帮我们在集成其他平台时,可以解决大部分重复的工作,同时代码风格也要遵循我们项目规范,便于后续维护

拆分后的规则回顾

之前我们将一个大规则,按照功能边界进行了拆分,并且补充了一个导航的文件readme-rule.mdc:


    
    
    
  
    
    
    
  ---
description: "项目开发规范"
globs: 
alwaysApply: false
---
# DeepWrite 项目开发规范

## 概述

本规范定义了DeepWrite项目的开发标准和最佳实践,旨在确保代码质量、一致性和可维护性。规范文件按照不同关注点进行拆分,便于查找和维护。

## 规则文件结构

.cursor/rules/
├── readme-rule.mdc              # 当前文件:总体概述和指引
├── coding-standards.mdc         # 代码风格、命名规范和编码标准
├── network-layers.mdc           # API架构、网络请求与响应规范
├── platform-adapter.mdc         # 平台适配器架构与实现规范
└── platform-settings.mdc        # 平台设置配置与UI实现规范

## 规则文件说明

### coding-standards.mdc
定义了项目的基本编码规范,包括代码格式、命名约定、注释标准等。该规则适用于所有源代码文件,是其他规则的基础。

### network-layers.mdc
规定了四层网络架构设计,包括API接口、服务层、适配器层和数据模型层的职责与交互方式。该规则适用于所有网络请求相关代码。

### platform-adapter.mdc
详细说明了平台适配器的设计原则、接口规范、错误处理和生命周期管理。该规则适用于平台集成相关代码,特别是`src/platforms`目录。

### platform-settings.mdc
定义了平台设置的实现标准,包括配置结构、UI组件、验证逻辑和存储管理。该规则适用于平台设置相关代码。

## 如何使用这些规则

1. **开发新功能时**:首先阅读`coding-standards.mdc`了解基本编码规范
2. **实现API请求时**:参考`network-layers.mdc`了解网络架构设计
3. **开发平台适配器时**:参考`platform-adapter.mdc`了解适配器实现规范
4. **配置平台设置时**:参考`platform-settings.mdc`了解设置实现标准

## 规则维护

1. 规则文件应保持聚焦,每个文件专注于一个特定领域
2. 避免规则之间的重复,必要时使用引用关系
3. 随着项目发展,定期更新规则以反映最新的开发实践
4. 规则更新需要经过团队审核

## 适用范围


这些规则适用于DeepWrite项目的所有代码,尤其是核心功能模块和平台集成相关代码。每个规则文件开头的`globs`属性定义了具体的适用文件范围。 

使用Rules集成新的平台

由于项目已经实现了公众号、头条、知乎的分发逻辑,现在我们通过Rules让Cursor实现稀土掘金的集成

实现过程中会有一些关键的信息,没有正确定位到文件位置,或者缺失一些文件的内容更新。我们通过新的会话进行上下文的补充

最后这种问题解决完成后,可以继续迭代我们的规则内容,提升后续其他平台的集成代码质量

总的来说,都是一些小问题或者简单的报错,复制出来扔给Cursor后,可以直接修复

同时,在这个过程中,需要检查之前所定义好的Rule,是否可以进行完善,照着这个思路进行下去即可

下面说一个在这个过程中遇到的棘手问题,Cursor反复很多次修复最终才解决,这个过程也值得思考与总结

掘金平台图片上传问题

掘金平台对于图片上传跟之前的都不同,比较复杂。Cursor自己生成的代码都有问题,基本上都是api过期,我通过@web指令实时搜索最新api也无法解决。可能是外网的一些资料关于这部分知识比较缺失导致的,所以这部分只能人工来实现

通过平台图片上传接口的抓包,发现其使用的是字节跳动提供的图片处理和存储服务ImageX,这个过程会涉及复杂的AWS V4签名算法,该签名算法严格要求报文的参数格式符合规范才能通过

这部分的实现我尝试使用了Claude3.7+GPT4.1的反复实现与修改,最终才解决。不过还是可以提供一些具体的思路。将涉及到的4个接口,正确的报文与响应抓取出来,放置到文档中进行记录,然后让Cursor帮我们分析实现思路,后续再修复问题调试参数的时候,尽可能让其参考文档的报文进行比对修

总结

整体来看,通过rules我感觉解决了70%左右的工作量,我们使用AI的同时,也要能够接受它的不完美。但是我们要做的是,针对我们的项目,可以形成一套自己的方法论,来提升AI的正确率与解决问题的效率

比如上述AI无法处理最新的api,我通过抓包提供给其最新的接口报文后,Cusor实现的代码同样一直会报错,反反复复修补不正确。此时多轮上下文的信息非常膨胀,可能已经无法理解我们正确的报文数据了。后续我将正确的接口报文形成文档,每次修复问题都让其与文档接口进行比对后再修复,这样确实更加高效

这里也可以说明另一点,我们可以将项目中关键的知识内容文档化保存,在对话过程中,充分利用好窗口上下文的内容,毕竟这个直接决定AI回答的质量好于坏

</section><p style="font-size: 0px; line-height: 0; margin: 0px;">&nbsp;</p>

在TypeScript中装饰器有哪些应用场景?

# TypeScript装饰器应用场景详解

## 1. 类装饰器
```typescript
function logClass(target: Function) {
  console.log(`Class ${target.name} was defined`);
}

@logClass
class MyClass {
  // 类实现
}

应用场景:

  • 类注册(如Angular的@Component)
  • 类扩展(添加元数据)
  • 类替换(修改构造函数)

2. 方法装饰器

function logMethod(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${propertyKey} with args: ${args}`);
    return originalMethod.apply(this, args);
  };
}

class Calculator {
  @logMethod
  add(a: number, b: number) {
    return a + b;
  }
}

应用场景:

  • 日志记录
  • 性能监控
  • 权限控制
  • 缓存处理

3. 属性装饰器

function format(formatString: string) {
  return function(target: any, propertyKey: string) {
    let value = target[propertyKey];
    
    const getter = () => value;
    const setter = (newVal: string) => {
      value = newVal.replace(/(\d{4})(\d{2})(\d{2})/, formatString);
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter
    });
  };
}

class User {
  @format('$1-$2-$3')
  birthDate: string;
}

应用场景:

  • 数据格式化
  • 数据验证
  • 属性监听
  • 依赖注入

4. 参数装饰器

function validateParam(
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  // 存储参数验证信息
  const validations = Reflect.getMetadata('validations', target) || [];
  validations.push({
    method: propertyKey,
    paramIndex: parameterIndex,
    validator: (value: any) => value > 0
  });
  Reflect.defineMetadata('validations', validations, target);
}

class MathService {
  sqrt(@validateParam value: number) {
    return Math.sqrt(value);
  }
}

应用场景:

  • 参数验证
  • 依赖注入(如Angular)
  • 参数转换

5. 访问器装饰器

function configurable(value: boolean) {
  return function(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.configurable = value;
  };
}

class Point {
  private _x: number;
  private _y: number;
  
  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }
  
  @configurable(false)
  get x() { return this._x; }
  
  @configurable(true)
  get y() { return this._y; }
}

应用场景:

  • 访问控制
  • 属性配置
  • 计算属性缓存

6. 装饰器工厂

function unit(unitName: string) {
  return function(target: any, propertyKey: string) {
    const units = Reflect.getMetadata('units', target) || {};
    units[propertyKey] = unitName;
    Reflect.defineMetadata('units', units, target);
  };
}

class Physics {
  @unit('m/s²')
  acceleration: number;
  
  @unit('kg')
  mass: number;
}

应用场景:

  • 带参数的装饰器
  • 元数据标记
  • 配置驱动

7. 元数据反射

import 'reflect-metadata';

function entity(name: string) {
  return function(target: Function) {
    Reflect.defineMetadata('entity', name, target);
  };
}

@entity('user')
class User {
  // 类实现
}

// 获取元数据
const entityName = Reflect.getMetadata('entity', User);

应用场景:

  • ORM映射
  • API文档生成
  • 序列化配置

8. 组合装饰器

function first() {
  console.log('first(): factory evaluated');
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('first(): called');
  };
}

function second() {

OC底层原理二:OC对象的分类(实例对象、类对象、元类对象)

一、实例对象(instance object)

实例对象是使用类alloc出来的对象,每次调用alloc都会产生新的实例对象。

  1. 创建方法 NSObject* obj = [[NSObject alloc] init]; obj就是实例对象

  2. 存储信息 isa指针、其他成员变量的值(isa指针也是成员变量,实例对象只存储变量的值,而成员变量的类型和名称等信息保存在类对象)

二、类对象(class object)

  1. 获取方法 需导入#import <objc/runtime.h>

类对象的class方法返回self,所以[[NSObject class] class]方法返回的依然是类对象

实例对象的class方法,实际调用了object_getClass方法。 object_getClass方法:获取该对象的isa指针,如果传入实例对象,则返回类对象;如果传入类对象,则返回元类对象。

  1. 类型 类对象是一种结构体

  2. 存储信息 isa指针、superclass指针、属性列表、对象方法(或者叫实例方法)列表、协议列表、成员变量信息列表(类对象存储成员变量的类型和名称等信息,而变量的值存储在实例对象)等:

  3. 疑问 为什么实例方法、成员变量信息放在类对象中,而不是保存在实例对象中? 答: 因为这些信息是相对固定的,保存一份即可; 并且由实例对象调用,所以保存在类对象中; 如果保存在实例对象中,每创建一个实例对象,信息重复,不利于内存优化。

  4. 内存地址 一个类的类对象在内存中只有一份,打印上面类对象的地址,结果都一样:

三、元类对象(meta-class object)

  1. 获取方法 元类对象的获取方法:需导入#import <objc/runtime.h>

  2. 类型 和类对象一样,元类对象也是objc_class结构体

  3. 存储信息 isa指针、superclass指针、类方法等 由于元类对象和类对象的类型都是Class,所以它们的村粗结构是一样的,只是内容不一样而已。

  4. 内存地址 一个类的元类对象在内存中只有一份

  5. 验证元类对象的方法 class_isMetaClass(obj)

滑动窗口协议

滑动窗口协议是什么? 让我用一个超级简单的生活例子来解释,忘掉所有代码,我们来讲个故事。

故事:小明给小红寄送一套漫画书

假设小明要给小红寄一套10本的限量版漫画,每本书都按顺序编号(1到10)。

方式一:最笨的“发一等一”法 (Stop-and-Wait)

  1. 小明把第1本漫画打包,寄出去。
  2. 然后他就在家死等,什么也不干,直到小红收到后,打个电话说:“第1本收到了!”
  3. 小明听到后,才把第2本打包,寄出去。
  4. 再等小红的电话......

缺点:效率极低!小明大部分时间都在等待,快递员也一直在来回跑冤枉路。这就好比你发一条WebSocket消息,必须等服务器回复ACK,才能发下一条。


方式二:引入“滑动窗口”协议

小明觉得太慢了,他想了个新办法。他家门口有个小货架,这个货架最多只能放4本书(这就是我们的 窗口大小 Window Size = 4)。

新规则如下:

  1. 批量发送:小明一口气把第1、2、3、4本漫画全打包好,放到货架上,然后让快递员一次性全部拉走。现在,这4本“在路上”的书,就是所谓的“飞行中(In-Flight)的消息”。
  2. 等待确认:小明在自己的记事本上记下:1, 2, 3, 4已发。
  3. 窗口滑动
    • 过了一会儿,小红打来电话:“第1本收到啦!”
    • 小明在记事本上把“1”划掉。好极了!货架上空出了一个位置!
    • 他立刻把第5本漫画放到货架上,让快递员拉走。
    • 现在,他“在路上”的书变成了 2, 3, 4, 5。你看,这个“4本书的窗口”是不是像向右滑动了一格?

这个过程就叫滑动窗口。它允许小明连续发送一定数量的消息而无需等待,大大提高了效率。


解答你的核心困惑

现在,我们来处理最关键的“丢包”和“去重”问题。

1. 如果丢失了就只重传丢失的那一个消息?

这取决于小明(发送方)和小红(接收方)的“智商”,也就是协议的具体实现方式。主要有两种:

A) 比较“笨”但简单的策略 (Go-Back-N / 回退N帧)

假设第2本漫画在路上被快递弄丢了。

  • 小红的反应:她收到了第1本,然后满心期待第2本。结果,快递员送来了第3本!小红很固执,她说:“不行!我必须按顺序收,没收到第2本,后面的我都不要!” 于是她把第3本、第4本都拒收了。她会一直打电话给小明说:“我只收到了第1本哦!”(意思是“我下一个想要的是第2本”)。
  • 小明的反应:他一直没等到“第2本收到”的电话(超时了)。他就明白了:“第2本肯定出事了!” 他会怎么做?他会把从第2本开始的所有书(2, 3, 4, 5...)全部重新寄一遍!

这就是你在我们之前那个Demo里看到的逻辑。 它的优点是接收方(服务器)的逻辑非常简单,只需要判断来的序号是不是我想要的就行。缺点是浪费带宽,因为明明第3、4本可能已经到了,却被无情地重传了。

B) 比较“聪明”但复杂的策略 (Selective Repeat / 选择性重传)

还是第2本丢了。

  • 小红的反应:她收到了第1本。然后等来了第3本。她想:“虽然第2本没到,但我先把第3本收下,找个空地方放着。” 然后第4本也到了,她也收下,也找地方放着。她会打电话告诉小明:“我收到了第1、3、4本!”
  • 小明的反应:他收到了1、3、4的确认,但在本子上一看,唯独第2本迟迟没有消息(超时了)。他明白了:“哦,原来只是第2本丢了!” 于是,他只把第2本重新寄过去
  • 小红收到补发的第2本后,把它和之前收到的3、4本按顺序整理好。完美!

总结你的第一个问题:

是的,最理想的情况下是只重传丢失的那一个。但这需要发送方和接收方都实现更复杂的逻辑(选择性重传)。在很多简单的实现中(比如我们那个Demo),为了简化逻辑,会采用“回退N帧”的策略,即从丢失的那个开始,后面的全部重传。

2. 服务端去重还是怎么着啊?

必须去重!你说到点子上了!

这是接收方(小红/服务器)一项非常重要的任务。

场景:假设小明因为超时,重传了第2本书。但其实原来的第2本书只是路上堵车,晚到了而已。

  • 小红的反应:
    1. 她先收到了那个“迟到”的第2本书。她很高兴,确认收入,并打电话给小明:“第2本收到啦!”
    2. 过了不久,快递员又送来一本第2本书(这是小明重传的)。
    3. 小红一看手里的记事本:“咦?第2本我不是已经收过了吗?” 她会直接把这本重复的书扔掉(或者退回),这就是去重。
    4. 为了保险起见,她可能会再给小明打个电话:“第2本我收到了哈!”(因为她怕上次的确认电话,小明也没接到)。

所以,接收方必须根据消息的序列号(Sequence Number)来判断这条消息是不是处理过,从而丢弃所有重复的消息。

总结一下

概念 大白话解释 你的问题解答
滑动窗口 一次可以发送的最大“未确认”消息数。像一个货架,装满了就得等确认才能上新货。 是批量发送,但这个“批量”的大小是受窗口限制的。
滑动 收到一个确认(ACK),窗口就空出一个位置,可以发下一条新消息了。 -
丢包重传 在规定时间内没收到确认,就认为丢了,要重发。 可以只重传丢失的那个(复杂方案),也可以从丢失的那个开始全部重传(简单方案)。
去重 接收方根据序列号,识别并丢弃已经收到过的重复消息。 对,服务端(接收方)必须做去重。 这是保证数据正确性的关键。

希望这个故事能让你彻底明白滑动窗口协议的精髓!它就是用一种巧妙的方式,在“效率”(批量发送)和“可靠性”(确认、重传、去重)之间找到了一个绝佳的平衡点。

僵尸对象与野指针的区别和联系

僵尸对象(Zombie Object)与野指针(Dangling Pointer)的区别和联系


定义

  • 僵尸对象(Zombie Object) 指的是对象已经被释放,但程序仍尝试访问该对象的内存区域。在 Objective-C 中,开启 Zombie 模式后,释放的对象会变成“僵尸”,用于捕获对已释放对象的访问,便于调试。
  • 野指针(Dangling Pointer) 指指针变量指向的内存已经被释放或无效,但指针本身没有被置空,继续使用该指针会导致未定义行为。

区别

方面 僵尸对象(Zombie Object) 野指针(Dangling Pointer)
本质 已释放对象被替换成特殊的“僵尸”对象,捕获访问 指针指向已释放或无效内存,未被置空
出现环境 通常在 Objective-C 的 Zombie 模式下出现 任何语言中都可能出现
表现 访问僵尸对象时会触发崩溃,便于调试 访问野指针导致未定义行为,可能崩溃或数据错误
调试工具 Instruments Zombies 工具 静态分析工具、内存检测工具如 AddressSanitizer
防范措施 不访问已释放对象,开启 Zombie 模式辅助调试 释放后将指针置 nil/nullptr,避免悬挂指针

联系

  • 僵尸对象是野指针问题的一种表现形式,是通过特殊机制(Zombie 模式)帮助发现野指针访问。
  • 两者都源于访问已释放的内存,都会导致程序崩溃或异常。
  • 解决方法都强调释放后指针清理和避免访问已释放内存。

示例代码

野指针示例(C 语言)

#include <stdio.h>
#include <stdlib.h>

void dangling_pointer_example() {
    int *ptr = (int *)malloc(sizeof(int));
    *ptr = 42;
    free(ptr);
    // ptr 仍然指向已释放内存,成为野指针
    printf("Value: %d\n", *ptr); // 未定义行为,可能崩溃或打印垃圾值
}

僵尸对象示例(Objective-C)

@interface MyObject : NSObject
@end

@implementation MyObject
- (void)dealloc {
    NSLog(@"MyObject dealloc");
}
@end

void zombie_example() {
    MyObject *obj = [[MyObject alloc] init];
    [obj release]; // 释放对象
    // 访问已释放对象,若开启 Zombie 模式,此处会捕获异常
    [obj description]; // EXC_BAD_ACCESS 或 Zombie 捕获错误
}

总结

  • 野指针 是指向无效内存的指针,是一种常见的内存错误。
  • 僵尸对象 是 Objective-C 中为调试野指针访问而产生的特殊对象。
  • 通过合理管理内存和指针,避免访问已释放对象,可以防止这类问题。

内存泄漏和僵尸对象的区别与联系

内存泄漏(Memory Leak)和僵尸对象(Zombie Object)的区别与联系


内存泄漏 vs 僵尸对象

方面 内存泄漏(Memory Leak) 僵尸对象(Zombie Object)
定义 程序中已不再使用的对象没有被释放,仍占用内存 对象已经被释放,但程序仍尝试访问该对象的内存区域
本质原因 对象的引用计数没有归零,导致内存无法回收 访问已释放对象的指针,导致野指针访问
表现形式 内存持续增长,最终可能导致内存耗尽 程序崩溃,通常是 EXC_BAD_ACCESS 或访问违规错误
发生时机 运行过程中未正确释放对象 释放对象后继续访问该对象
调试方式 使用 Instruments 的 Leaks 工具、静态分析等 开启 Zombie 模式,使用 Instruments 的 Zombies 工具
解决方法 及时释放对象,避免循环引用,管理好对象生命周期 不访问已释放对象,修正野指针,确保对象访问安全

联系

  • 内存泄漏和僵尸对象都与内存管理相关,都是因对象生命周期管理不当引起的问题。

  • 两者都是内存错误,但表现和后果不同:

    • 内存泄漏是“没释放”,导致内存浪费。
    • 僵尸对象是“访问已释放”,导致程序崩溃。
  • 有时内存泄漏会导致程序长期占用大量内存,影响性能;僵尸对象通常导致崩溃,影响稳定性。

  • 在调试过程中,二者常常结合使用不同工具定位问题。


简单比喻

  • 内存泄漏:就像你租了一个房间,但搬走后忘了退租,房间一直占着,别人用不了。
  • 僵尸对象:就像你退了房,但钥匙还在别人手里,别人试图进房间发现房间已经不存在了,导致混乱。

示例代码

  • 内存泄漏示例(NSTimer 循环引用)

    @interface MyClass ()
    @property (nonatomic, strong) NSTimer *timer;
    @end
    
    @implementation MyClass
    
    - (void)startTimer {
        // NSTimer 强引用 self,self 又强引用 timer,形成循环引用
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0                                                  target:self                                                selector:@selector(timerFired)                                                userInfo:nil                                                 repeats:YES];
    }
    
    - (void)timerFired {
        NSLog(@"Timer fired");
    }
    
    - (void)dealloc {
        [self.timer invalidate]; 
        NSLog(@"MyClass dealloc");
    }
    
    @end
    
  • 僵尸对象示例(访问已释放对象)

    interface MyObject : NSObject
    @end
    
    @implementation MyObject
    - (void)dealloc {
        NSLog(@"MyObject dealloc");
    }
    @end
    
    void testZombie() {
        MyObject *obj = [[MyObject alloc] init];
        [obj release]; // 手动释放对象(非 ARC 环境下)
        // 下面访问已释放的对象,导致僵尸对象崩溃
        [obj description]; // EXC_BAD_ACCESS 错误
    }
    

IM数据在iOSApp端存储、读取方案

iOS社交应用设计的IM数据存储、读取及会话管理方案,结合高性能、安全与可扩展性,分为六个核心模块:


📚 一、存储架构设计:三级分层策略

  1. 本地存储层(高频访问)

    • 数据库选型:采用 SQLite + FMDB,轻量且支持复杂查询,优于Core Data在批量写入场景的性能。

    • 表结构设计

      • messages 表:消息ID(主键)、会话ID、发送者ID、内容、类型(文本/图片等)、时间戳、状态(已发送/已读等)。
      • conversations 表:会话ID、最后消息ID、未读计数、会话类型(单聊/群聊)。
    • 加密机制:集成 SQLCipher 对数据库文件加密,密钥存储于Keychain并绑定生物识别。

  2. 云端存储层(持久化与漫游)

    • 冷热数据分离

      • 热数据(7天内):Redis存储离线消息,支持高并发读取(写扩散模型,每人独立收件箱)。
      • 冷数据(历史消息):HBase按会话ID分片存储(读扩散模型),节省存储空间。
    • 文件存储:图片/视频等大文件上传至对象存储(如AWS S3),仅保留URL在数据库。

  3. 内存缓存层(加速访问)

    • NSCache + LRU策略:缓存最近会话列表及前20条消息,淘汰久未访问数据。
    • 预加载机制:滑动消息列表时,异步加载下一屏内容,避免滚动卡顿。

二、数据读写优化:高性能实践

  1. 写入策略

    • 批量事务处理:单次写入多条消息时,使用FMDB事务包裹,减少I/O次数(例:插入100条消息仅1次磁盘写入)。
    • 事务的ACID特性
      SQLite默认每次执行SQL语句都会开启一个独立事务(隐式事务),导致频繁的磁盘I/O。而显式事务(BEGIN TRANSACTION)将多个操作合并为一个原子单元:
  • 原子性:所有操作要么全部提交(COMMIT),要么全部回滚(ROLLBACK)。
  • 性能提升:减少磁盘写入次数,将N次操作合并为1次I/O,降低系统开销
  • 通过包裹多个executeUpdate操作,将批量插入合并为单次事务提交
    • 异步队列:通过FMDatabaseQueue串行化数据库操作,防止多线程竞争。
  1. 读取优化

    • 索引设计:为会话ID时间戳创建复合索引,加速消息按会话分页查询。
    • 分页加载:每次拉取20条消息,通过WHERE timestamp < last_msg_time LIMIT 20避免内存溢出。
    • 懒加载资源:图片消息先加载缩略图(<50KB),点击后下载原图)。

💬 三、会话管理:状态同步与更新

  1. 会话同步机制

    • 增量更新:客户端启动时,仅拉取本地最后更新时间戳后的新会话(减少网络传输)。
    • 未读计数聚合:服务端计算未读数,客户端本地更新时通过事务保证一致性。
  2. 消息状态流转

    graph LR
        A[消息发送] --> B{服务器接收成功?}
        B -->|是| C[更新状态为“已发送”]
        B -->|否| D[重试3次后标记“失败”]
        C --> E{接收方在线?}
        E -->|是| F[推送并标记“已送达”]
        E -->|否| G[存入Redis离线队列]
        F --> H[接收方阅读后标记“已读”]
    
    • 状态回执:接收方阅读后,通过WebSocket实时回传已读状态。

🔒 四、安全与隐私合规

  1. 数据传输安全

    • 端到端加密:敏感消息(如支付凭证)使用Signal协议加密,服务器仅中转密文。
    • SSL Pinning:防止中间人攻击,校验证书指纹。
  2. 本地数据防护

    • 敏感信息隔离:用户身份Token存储于Keychain,消息解密密钥通过生物认证后获取。
    • 沙盒限制:禁止IM数据共享至App Group,防止其他应用读取。

⚙️ 五、性能与扩展性增强

  1. 弱网适配

    • 消息QoS分级:文本消息优先传输,图片/视频在WiFi下自动下载。
    • 断点续传:大文件分片上传,记录分片MD5校验完整性。
  2. 万人群聊优化

    • 读写分离

      场景 策略 优势
      新消息写入 写扩散(每人收件箱) 读性能高,延迟<100ms
      历史消息拉取 读扩散(按会话存储) 存储成本降低70%
    • 流量控制:群消息仅推送摘要(如“3条新消息”),点击后加载详情。


♻️ 六、备份与灾难恢复

  1. 用户级备份

    • iCloud同步:加密的SQLite数据库自动同步至iCloud,支持换机恢复。
    • 增量备份:仅上传新增/修改的消息,节省用户流量。
  2. 数据恢复流程

    • 本地恢复:从iCloud下载备份文件,通过FMDB导入。
    • 服务端漫游:用户重装App后,从HBase拉取最近6个月历史消息。

💎 方案优势总结

  1. 性能指标

    • 消息读取延迟:<50ms(本地缓存命中时)
    • 群消息吞吐量:支持万级并发。
  2. 成本控制:冷热存储分离降低云端费用70%。

  3. 安全合规:满足GDPR及《》要求。

推荐技术栈:FMDB(SQLite封装)+ WebSocket(实时通信)+ Redis/HBase(云端存储) + iCloud(备份)。此方案已在微信、环信等亿级应用中验证,可支撑社交场景下IM全链路需求。

swift 基础

1. struct 和 class 的区别

类型区别

struct: 值类型

  • 赋值或传递时是值拷贝(深拷贝),独立内存空间
  • 修改副本不会影响原实例

class:应用类型

  • 赋值或者传递时是引用指针的拷贝,共享同一内存空间
  • 修改任一引用会影响所有指向该实例的变量

内存管理

struct:编译器自动管理,栈上,比较高效 class:内存通过ARC管理,堆上分配。可能引发循环引用

继承与多态

struct:不支持继承,但可以通过protocol实现多态 class:支持单继承,可通过override重写方法/属性

可变性

struct:默认不可变,如需修改方法内的属性,需标记为mutaing

//在值类型的实例方法中修改实例属性值,需要在方法前加 mutaing

struct Point2 {
    var x=0.0, y=0.0
    mutating func moveBy(dx:Double, dy:Double) {
//        x += dx
//        y += dy
        //或者直接为 self 赋值
        self = Point2(x: x+dx, y: y+dy)
    }
}

class:始终可变,无需额外关键字

初始化

struct:自动生成成员初始化器 对于struct,如果没有自定义初始化方法,会有一个默认的,为所有属性赋值的初始化方法:

//对于 struct,如果属性没有默认值,初始化的时候,会有一个默认初始化方法,可以为所有属性赋值
struct Size {
    var width:Double
    var height:Double
}
var size = Size(width: 100.0, height: 200.0)

如果有自定义了初始化方法,那么不能再调用默认的初始化方法了

struct Size {
    var width:Double
    var height:Double
    init() { // 自定义了初始化方法
        width = 200.0
        height = 300.0
    }
}
// 如果为 value type 提供了自定义初始化方法,那么就不能调用默认的初始化方法了
// 不能使用 var size = Size(width: 100.0, height: 200.0)
var size = Size()

class:需手动定义初始化器,若继承父类需处理super.init()

使用场景

优先选择struct:

  1. 数据简单,无需继承
  2. 线程安全

优先选择class:

  1. 需共享或者修改同一实例
  2. 需继承或类型检查

map, filter, reduce 作用

  1. map:对集合中每个元素进行转换,返回一个新的集合,可用于元素转换,替换for循环
let numbers = [1, 2, 3, 4]
let newArr = numbers.map { n in
    return n * 2
}
print(newArr) // [2,4,6,8]

let newArr2 = numbers.map { $0 * 2 }
print(newArr2)
  1. filter: 筛选符合条件的元素,返回一个新集合。可用于过滤无效数据,搜索匹配,替代if+for循环
let strs = ["fasdf", "werw", "fvvf", "32e124faf", "mmkjbnj"]
let res1 = strs.filter { str in
    return str.count > 6
}
print(res1) // ["32e124faf", "mmkjbnj"]

let res2 = strs.filter { $0.count > 6 }
print(res2)
  1. reduce:将集合的所有元素合并成一个值,如求和,字符串拼接,替代for循环+累加变量
let result = array.reduce(initialValue) { partialResult, element -> T in
    // 累积计算
    return updatedPartialResult
}

let nums = [1,2,3,4]
// 0 是初始值
let sum = nums.reduce(0) { partialResult, n in
    return partialResult + n
}
print(sum)

let sum1 = nums.reduce(0, +)
print(sum1)

let strs = ["hello", "world", "shanghai"]
let sum2 = strs.reduce("") { $0 + $1 }
print(sum2)

string和NSString的区别和联系

string可通过定义时的let或var决定,NSString不可变 string值类型,NSString引用类型 string编码方式UTF-8/UTF-16,自动处理Unicode string具有赋值时拷贝(Copy-on-Write)的特性,NSString传递指针

互相转换

let str = "hello"
let nstr = str as NSString
let nstr2 = NSString(string: "world")

let nstr3: NSString = "wwww"
let swiftStr = nstr3 as String
let swiftstr2 = String(nstr3)

swift中associatedtype 的作用

When defining a protocol, it’s sometimes useful to declare one or more associated types as part of the protocol’s definition. An associated type gives a placeholder name to a type that’s used as part of the protocol. The actual type to use for that associated type isn’t specified until the protocol is adopted. Associated types are specified with the associatedtype keyword

associatedtype主要作用是让protocol支持泛型 如下,定义一个协议,有一个属性和两个方法

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    func getItem(at index: Int) -> Item
}

Item是一个类型占位符,具体类型由实现协议的结构体或类来定义

下面的结构体实现了该协议,并且指定Item的类型为Int

struct IntContainer: Container {
    typealias Item = Int // 可以省略,swift会根据类型推断Item的具体类型
    private var items:[Int] = []
    mutating func append(_ item: Int) {
        items.append(item)
    }

    func getItem(at index: Int) -> Int {
        return items[index]
    }
}

又一个实现该协议的struct,这次指定Item为String

struct StringContainer: Container {
    private var items:[String] = []
    mutating func append(_ item: String) {
        items.append(item)
    }

    func getItem(at index: Int) -> String {
        return items[index]
    }
}

分别初始化两个结构体,可以看到可以有不同的类型

var intContainer = IntContainer()
intContainer.append(12)
intContainer.append(13)
print(intContainer.getItem(at: 0))

var stringContainer = StringContainer()
stringContainer.append("hello")
stringContainer.append("world")
print(stringContainer.getItem(at: 0) + " " + stringContainer.getItem(at: 1))

open和public的区别

特性 public open
模块外可见性 ✅ 其他模块可以访问 ✅ 其他模块可以访问
模块外可继承 ❌ 其他模块不能继承该类 ✅ 其他模块可以继承该类(open class
模块外可重写 ❌ 其他模块不能重写方法/属性 ✅ 其他模块可以重写方法/属性(open func
适用场景 暴露接口但不允许外部修改 允许外部继承或重写(如框架设计)

Optional是用什么实现的?

enum,有两个case: .some(value) .none

定义静态方法时,staitc 和 class 有什么区别?

都用于定义类型级别的方法或者属性,但是关键区别在于是否允许子类重写

特性 static class
适用范围 类、结构体、枚举、协议 仅类(class
是否允许重写 ❌ 不可被子类重写 ✅ 允许子类重写(需加 override
语义 强调静态不可变性 强调类的可继承性

协议中必须用static

何时用static?

  1. 定义工具方法或者常量
  2. 需要跨类型使用
  3. 禁止子类修改

何时用class?

  1. 设计可扩展的类层次结构
  2. 需要子类提供特定实现

如果想禁止重写class方法,可配合final使用:

class Parent {
    final class func cannotOverride() {} // 子类不能重写
}

weak和assign区别

weak:修饰对象类型,不会增加引用计数,对象释放后自动设置为nil,代理用weak,可防止循环引用。 assign:修饰基本数据类型,修饰对象时可能导致野指针。

weak修饰的属性自动置为nil 的原理

weak是Runtime维护了一个hash(哈希)表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。

weak_table: 全局管理弱引用的哈希表 weak var obj = someObj

key: 被弱引用的对象的地址,这里是指 &someObj 作用:通过地址快速定位到该对象的所有弱引用记录

value:weak_entry_t结构体 类型:weak_entry_t,存储一个对象的所有弱引用信息

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;  // Key 的副本(被弱引用的对象)
    union {
        struct {
            weak_referrer_t *referrers;  // 动态数组:存储所有 weak 变量的地址
            uintptr_t num_refs;          // 数组长度
        };
        struct {
            weak_referrer_t  inline_referrers[4]; // 内联数组(优化少量 weak 引用)
        };
    };
};

referrers存储的是变量obj的地址,如:[&obj],如果有多个weak修饰,那么如:[&obj1, &obj2] 内存关系图:

image.png

weak引用插入weak_table_t的过程: 当执行 weak var obj = someObject 时:

  1. 计算 someObject 地址的哈希值:
    hash = hash_pointer(someObject)
  2. 通过 hash & mask 计算数组索引 index
  3. 检查 weak_entries[index]
    • 若为空,初始化一个新的 weak_entry_t,存入 referent = someObject 和 referrers = [&obj]
    • 若已存在且 referent == someObject,将 &obj 追加到 referrers 数组。
    • 若发生哈希冲突(referent != someObject),线性探测下一个位置(开放寻址法)

什么时候用copy修饰符

修饰NSString,NSArray,NSDictionary等不可变类型的时候 原因:使用copy修饰的时候,赋值的时候,实际保存的是值的一个不可变副本,不会被外界无意修改。 NSString,NSArray,NSDictionary有对应的可变类型,如果用strong修饰,得到的实际是可变对象,外界修改时会同步改变属性的值

不手动指定Autoreleasepool的前提下,一个AutoreleasePool对象在什么时候释放?

所有autorelease对象都由主线程的RunLoop创建的@autoreleasepool来管理。

每一个Autoreleasepool都是由一系列的AutoreleasePage组成的,数据结构为双向链表,AutoreleasePage是节点

AutoreleasePage

类似一个栈结构

image.png

初始化的时候,调用objc_autoreleasePoolPush,把一个POOL_SENTINELpush到自动释放池的栈顶,并且返回这个POOL_SENTINEL对象。 POP时,向自动释放池中的对象发送release消息,直到第一个POOL_SENTINEL

Autoreleasepool和RunLoop的关系

主线程的RunLoop中注册了两个observer

  • 一个监听kCFRunLoopEntry事件,调用push
  • 第二个observer,监听kCFRunLoopBeforeWaiting事件,调用pop,push。监听kCFRunLoopBeforeExit事件,调用pop。 总结就是runloop会自动创建和销毁Autoreleasepool。

RunLoop

juejin.cn/post/684490…

运行循环,在程序运行中做一些事情,如接收和处理消息,休眠等待 主要是靠内部的事件循环来实现

事件循环

  • 没有消息时休眠,避免占用资源
  • 有消息时唤醒线程
  • 用户态到内核态到转换

RunLoop mode

多种mode起到屏蔽效果,运行在mode1模式的时候,无法处理mode2中的事件。

RunLoop和线程的关系

  • 一一对应
  • 如果没有Runloop,线程执行完任务就会退出,Runloop会在第一次获取它时创建

子线程创建RunLoop过程

  1. 获取当前线程的RunLoop
  2. 添加source/port
  3. run

为什么block可以修改使用__block修饰的局部变量?

使用__block修饰的局部变量,会在底层被编译成一个结构体,结构体中引用的有该变量的地址,在block中会通过地址访问该结构体,然后通过结构体更改该变量的值。

如何解决NSTimer和self的循环引用?

  1. 中间代理
  2. iOS 10+ 提供了 scheduledTimerWithTimeInterval:repeats:block:,结合 weakSelf
__weak typeof(self) weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    [weakSelf timerFired]; // 必须检查 weakSelf 是否存活
}];
  1. gcd timer

dispatch_barrier_async作用

主要起到线程同步的作用,只能作用在自定义的并发队列

  • 立即返回
    dispatch_barrier_async 在提交任务后会 立即返回,不会阻塞当前线程(异步提交)。

  • 屏障块的执行时机

    • 当屏障块到达 私有并发队列的队首 时,不会立即执行
    • 队列会 等待当前正在执行的所有并发任务完成,之后 屏障块单独执行
    • 屏障块执行期间,队列 暂停执行其他任务,直到屏障块完成。
  • 屏障块之后的任务
    在屏障块之后提交的任务,必须等待屏障块执行完毕才会开始执行

举例: 在barrier之前有3个并发任务A,B,C。barrier中加入了任务D,后续还有任务E,F。 那么任务执行如下:

  • A,B,C并发执行
  • D不会立即执行,要等到A,B,C都执行完毕后,D才开始执行。
  • D执行完毕后,E,F开始执行。

Objective-C的消息处理流程

www.jianshu.com/p/220841172…

iOS 事件响应链

juejin.cn/post/689451…

Objective-c中的nil,NULL,Nil,NSNULL

nil

OC中nil 是一个宏,定义如下:

#define nil ((id)0)
NSString *str = nil; //实际会被转为
NSString *str = ((id)0); // 即 str = 0

它表示 将 0 强制转换为 id 类型(Objective-C 对象的通用指针类型)。

Objective-C 中,对象指针的本质是 C 指针,而 0(或 NULL)是 C 语言中表示空指针的标准方式。这里提供两个验证方式:

1、nil 和 0 的等价性:

图片

2、指针的底层值:

图片

nilNULL 和 0 在指针层面是相同的,以下几行代码都不会报错

int *a = nil;
int *b = NULL;
NSString *c = NULL;
NSString *c0;
NSString *e = Nil;

为什么 Objective-C 要用 nil 而不是直接写 0

语义清晰,nil 明确表示“空对象指针”,而 0 可能被误解为整数值,与 C 语言的 NULL 区分,NULL 用于 C 指针(如 int*),nil 用于 Objective-C 对象。但是从上述例子中也可以看到NULL和nil可以混用

Nil

Nil 表示指向 Objective-C 类(Class)的空指针。它是一个宏,定义为 (Class)0。可以说跟nil是一模两样了

用于 类的空指针,通常较少使用:

Class someClass = Nil;
if (someClass == Nil) {    
    NSLog(@"Class is Nil");
}

与 nil 类似,但用于 类对象(Class)  而不是实例对象。

NULL

NULL 是 C 语言标准的空指针,表示指向任何类型的空指针。通常定义为 (void *)0

int *ptr = NULL;
if (ptr == NULL) {    
    NSLog(@"ptr is NULL");
}

适用于 C 语言层面的指针(如 int*char*)。

在 Objective-C 中,优先使用 nil 而不是 NULL

NSNull

在 NSArray 或 NSDictionary 中,nil 不能直接存储(因为 nil 表示列表结束),可以用 NSNull 占位

NSNull 是一个单例对象, 它只有一个单例方法:+[NSNull null],用于表示集合(如 NSArrayNSDictionary)中的空值

它不是指针,而是一个 真正的 Objective-C 对象

iOS 二维码扫描组件推荐:zcscan,一款轻量级可定制的扫码工具库!

🚀 iOS 二维码扫描组件推荐:zcscan,一款轻量级可定制的扫码工具库!

在日常 App 开发中,扫码早已成为一个非常常见的需求,比如登录、分享、支付、设备配对等等。而系统自带的 AVCapture 使用复杂、UI 样式千篇一律,不利于快速接入和个性化定制。

今天给大家推荐一款开源的 Swift 扫码组件库 👉 zcscan,支持二维码扫描 + 相册选择,并且提供了丰富的 UI 自定义能力。


🌟 项目亮点

  • ✅ 基于 Swift 构建,轻量且易集成
  • ✅ 支持自定义扫码界面样式(线条、按钮、图标等)
  • ✅ 支持选择相册图片进行二维码识别
  • ✅ 支持 present 和 push 两种方式展示扫码页
  • ✅ CocoaPods 和 SwiftPM 均支持
  • ✅ 默认样式已足够美观,开箱即用

📦 安装方式

1️⃣ CocoaPods

pod 'zcscan'

终端执行:

pod install

2️⃣ Swift Package Manager(SPM)

  • Xcode 菜单栏选择:File > Add Packages...
  • 输入地址:github.com/ZClee128/zc…
  • 也可以在 Package.swift 中添加:
.package(url: "https://github.com/ZClee128/zcscan.git", from: "1.0.0")

⚡️ 快速上手

✅ 导入模块

import zcscan

✅ 方式一:默认 present/push 扫码页面

let vc = ZCScanViewController.present(fromVC: self, albumClickBlock: nil, resultBlock: { link in
    print("扫描结果:\(link)")
})

✅ 方式二:push + 自定义相册选择行为

let vc = ZCScanViewController.push(fromVC: self, albumClickBlock: { selectPhoto in
    let picker = ZLPhotoPicker()
    picker.selectImageBlock = { results, _ in
        if let img = results.first?.image {
            selectPhoto(img)
        }
    }
    picker.cancelBlock = {
        // 用户取消选择
    }
    picker.showPhotoLibrary(sender: self)
}, resultBlock: { link in
    print("扫描结果:\(link)")
})

🎨 UI 高度可定制

zcscan 暴露了所有必要的 UI 属性,可以根据你的品牌样式灵活设置:

let config = ZCScanManager.shared.conifg
config.selectQrcodeBtnImage = UIImage(named: "qrcode_arrow")
config.scanninglineImage = UIImage(named: "scan_line")

📷 示例效果图

IMG_1651.PNG

IMG_1650.PNG

📚 项目地址 & Demo

GitHub 地址:github.com/ZClee128/zc…

示例工程:Example/ViewController.swift

欢迎大家 Star ⭐️ 一下!


🧑‍💻 作者信息

作者:ZClee

Email:876231865@qq.com


📄 License

zcscan 使用 MIT 开源协议,免费商用,放心食用。


如果你觉得这个库不错,欢迎点赞收藏 ⭐️,转发给更多有需要的朋友!

如你需要二维码扫码、照片识别、界面自定义,zcscan 都能助你一臂之力!


Appstore开始新一轮备案号审查,看看你的产品被下架了么?

背景

最近Appstore出现大面积产品下架和重新上架的情况,正常来讲下架的产品通常都是违规导致的3.2f,也有其他几种非常规现象。比如,开发者账号到期未续费或者申诉成功解救

但是最近这批下架重新上架的面积及其普遍,加上粉丝留言。基本可以锁定是苹果主动下架了中国大陆区

在Appstore后将看到如下情况:

下架截图.png

App是啥?

自从新赛季开启App备案之后,苹果陆续同步了关于中国大陆区的校验工作,甚至在Appstore后台新增了专属于中国大陆区的ICP备案号填写。

其实App备案本身和域名备案是一样的,主要是确保开发者主体真实有效,约束大多数合规开发者的君子协议

Appstore后台的备案号也经常有Bug,所以才会导致这种局面。

应该做什么

对于中国大陆区的开发者来说,无论是个人还是公司主体都应该积极备案。如果说产品名称不确定的情况下,可以在提审时候不销售中国大陆区,同时着手备案相关事宜。这样既能保证产品的一个正常迭代,又能不受政策影响

不该做什么

既然可以备案名称而且不需要提供额外的软著或者其他证明材料,这时候就会有人抖机灵,耍小聪明。

首先就是备案竞品名称,抢注热品牌词,其实这种做法毫无意义。对备案的开发者来说,白瞎了一个备案服务号100元,以及一周左右的备案时间

其次备案顺利到手,也无法完成Appstore上架的需求。同时真正的品牌词持有者依旧可以正常备案,丝毫不受影响

特别说明:不要妄图乱填备案号的方式欺骗审核人员,已经有同行因为此问题触发了3.2f的彩蛋。所以,还是要老老实实做人,本本分分开发。

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

相关推荐

# Pingpong和连连的平替,让AppStore收款无需新增持有人。

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

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

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

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

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

知识星球

更多Appstore咨询问题,请关注知识星球。「提供1v1上架指导,帮助开发者解决Appstore的疑难杂症,助力每一位开发者!」

Swift 单元测试突破口:如何优雅测试私有方法?

这里每天分享一个 iOS 的新知识,快来关注我吧

image.png

前言

在编写单元测试时,我们通常希望测试代码的业务逻辑。常用的测试编写格式是 GWT(Given-When-Then),其中在 "When" 步骤中调用我们想要测试的代码。然而,如果你曾写过测试用例,你可能遇到过无法测试私有方法的问题。

这是因为私有属性或方法的作用域仅限于其所在的类,因此我们无法在测试类中访问这些属性或方法。

例如,如果我们有一个类,其中包含一个私有方法,如下所示:

class SomeClass {
    private func somePrivateMethod() {
        // 一些逻辑代码
    }
}

尝试在单元测试中调用 somePrivateMethod,会发现无法访问,并会产生编译错误:“'somePrivateMethod' 由于 'private' 保护级别而无法访问”,这很容易理解。

class SomeClassTests: XCTestCase {
    func testSomePrivateMethod() {
        let testTarget = SomeClass()
        testTarget.somePrivateMethod() // 错误:无法访问私有方法
    }
}

那么我们该如何解决这个问题呢?如果 somePrivateMethod 包含一些业务逻辑,单元测试一定要能够覆盖,那么我们就必须找到其他可行的方法。

方法一:改变访问级别

一种方法是将这些方法的访问级别改为非私有,但这会将这些方法暴露给其他代码,显然这不是一个理想的方案。

public class SomeClass {
    public func somePrivateMethod() {
        // 一些逻辑代码
    }
}

方法二:使用 TestHooks

TestHooks 可以派上用场了。我们可以创建测试钩子并利用它们来访问私有方法和属性,以进行单元测试。TestHooks 只是我们的类持有的一个钩子集,通过这些钩子可以提供对私有方法和属性的访问。

创建 TestHooks 时需要注意的几点:

  1. TestHooks 是在我们希望访问其私有方法或属性的类的扩展中创建的(在我们的例子中是 SomeClass),因为只有这样钩子才能访问那些属性或方法。

  2. 我们想访问的每个属性或方法都需要一个钩子。

  3. 建议将钩子扩展放在 DEBUG 宏中,以避免误用。

实现 TestHook:

以下是 SomeClass 的 TestHook 实现示例:

#if DEBUG    // 在 debug 宏下添加以避免误用,并避免在发布环境中暴露私有方法
extension SomeClass {    // 在类的扩展中编写,我们希望访问其私有方法
    var testHooks: TestHooks {      // testHooks 的实例,通过它我们将在单元测试中访问私有方法
        TestHooks(target: self)      // 使用 self 初始化以访问 self 的私有方法
    }
    struct TestHooks {    // TestHooks 结构体,其中包含我们希望访问的所有属性和方法的钩子
        var target: SomeClass    // 需要访问其私有方法的目标
        func somePrivateMethod() {    // 暴露方法的钩子
            target.somePrivateMethod()    // 暴露该方法
        }
    }
}
#endif

这样一来,我们可以在单元测试文件中通过 testHooks 访问 somePrivateMethod

class SomeClassTests: XCTestCase {
    func testSomePrivateMethod() {
        let testTarget = SomeClass()
        testTarget.testHooks.somePrivateMethod() // 通过 testHooks 访问私有方法
    }
}

结尾

通过这种方式,我们可以在不改变代码结构的情况下,合理地测试私有方法。

TestHooks 提供了一种在测试环境中访问私有方法的途径,同时在发布环境中保持代码的封装性。这是一种在不破坏类封装原则的情况下进行单元测试的有效方法。

希望这能帮助到大家更好地进行 Swift 的单元测试,你对这种方式有什么看法呢?是否还有更好的方案分享,欢迎在评论区留言讨论。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

Swift组件:使用第三方静态库

最近在做的项目,使用了组件化方案,组件都是用Swift编写。在做用户组件的时候,用到了微信登录SDK,该SDK是一个使用OC编写的静态库,文件只有.a和.h文件,通过CocoaPods引入到项目后,尝试导入,结果报错找不到该库。

为什么找不到静态库❓

因为OC没有暴露头文件给Swift。

在常规Swift项目中,创建OC文件时Xcode会自动帮我们创建桥接文件Xx-Bridging-Header.h,供Swift使用OC代码,同时也会创建Xx-Swift.h文件,供OC使用Swift代码,以达到混合开发的目的。

但是,组件是不允许使用桥接文件的。

没有桥接文件怎么办❓

我们来看下Cocoapods的做法。 比如在Swift组件中导入OC库MJRefresh。 导入后会看到在该库的Support Files文件夹中多了MJRefresh-umbrella.h、MJRefresh.modulemap等桥接文件。

其中MJRefresh-umbrella.h文件导入了所有OC头文件:

#import "MJRefreshAutoFooter.h"
#import "MJRefreshBackFooter.h"
#import "MJRefreshComponent.h"
...

MJRefresh.modulemap文件声明了Swift可以导入的库名MJRefresh以及头文件MJRefresh-umbrella.h:

framework module MJRefresh {
  umbrella header "MJRefresh-umbrella.h"
  export *
  module * { export * }
}

这一套下来,Swift就知道可以导入哪个OC库,里边有哪些OC类可以用。

问题来了🤔为什么CocoaPods导入微信开源库没有自动生成上面说的这些文件?

因为微信开源库提供的文件不是源码,而是打包好的静态库,CocoaPods不会为静态库创建这些文件。

我们要想办法让它创建桥接文件。

怎样做才能让CocoaPods生成桥接文件❓

其中一个思路是把SDK包装成一个本地组件!

我们看下微信开源库有哪些文件:

libWechatOpenSDK.a
WechatAuthSDK.h
WXApi.h
WXApiObject.h

第一步,创建文件夹和配置文件,目录如下:

WechatOpenSDK

  • WechatOpenSDK
    • libWechatOpenSDK.a
    • WechatAuthSDK.h
    • WXApi.h
    • WXApiObject.h
  • WechatOpenSDK.podspec

第二步,填写配置内容:

Pod::Spec.new do |s|
  s.name         = 'WechatOpenSDK'
  s.version      = '2.0.4'
  s.summary      = '本地集成的微信 OpenSDK'
  s.homepage     = 'https://open.weixin.qq.com'
  s.license      = { :type => 'Commercial' }
  s.author       = { 'WeChat' => 'wechat@tencent.com' }
  s.source       = { :path => '.' }

  s.ios.deployment_target = '9.0'
  s.swift_version = '5.0'
  s.requires_arc = true

  # 声明是静态库
  s.static_framework = true
  
  s.source_files         = 'WechatOpenSDK/*'
  s.vendored_libraries   = 'WechatOpenSDK/*.a'
  
  # SDK依赖
  s.libraries = 'c++'}
  
end

注意:s.source_files需把.a文件也包括在内,否则CocoaPods不会生成桥接文件。

到这,本地组件就做好了。

本地组件怎么用❓

1、在配置文件中依赖该组件:

s.dependency 'WechatOpenSDK'

2、在Podfile文件中指定该组件的路径:

pod 'WechatOpenSDK', :path => '../WechatOpenSDK'

然后执行pod install。

仔细看WechatOpenSDK组件的Support Files文件夹中多出很多文件,包括WechatOpenSDK-umbrella.h和WechatOpenSDK.modulemap,至此,我们成功把OC静态库暴露给Swift🏅。

接下来导入WechatOpenSDK模块即可使用:

import WechatOpenSDK

其他

🧐问题一:组件为什么不能使用桥接文件?

  1. Bridging-Header 只对主 Target 有效

Bridging-Header 是通过主 App 的 Build Settings 中 Objective-C Bridging Header 设置路径生效的。 这个设置不会自动传递给子模块(比如 Pod 或 Framework)。

  1. Swift 模块之间是隔离的

Swift Framework 被设计为 模块化的独立单元,不能通过 Bridging-Header 暴露 OC 给 Swift。 如果允许使用 Bridging-Header,模块之间的隔离性就被破坏了,会出现冲突、依赖混乱等问题。

🧐问题二:为什么 CocoaPods 对静态库没有自动生成 umbrella header (umbrella.h) 和 module.modulemap 文件?

Umbrella Header 和 modulemap 是为动态 Framework(modular framework)服务的,而不是为传统的静态库(.a)服务的。

  1. 静态库(Static Library)本身不支持模块化特性

静态库(.a 文件)是预编译的二进制文件,不具备模块边界、命名空间,本质上只能通过头文件路径手动 #import 来暴露接口。所以CocoaPods 默认不会为 .a 文件创建模块相关的 umbrella header 或 modulemap。

  1. CocoaPods 默认是非模块化集成方式

CocoaPods 默认行为(不启用 use_frameworks! 或 modular_headers):不开启模块化编译,不生成 module.modulemap,不要求每个 Pod 是一个独立模块。这样做的原因是为了兼容所有 Objective-C 传统项目(非 Swift 项目)。

🧐问题三:为什么这样配置组件后CocoaPods就会生成桥接文件?

因为:

  1. s.source_files 包含了 .h 文件,CocoaPods 知道要暴露公共头文件。
  2. 没有禁用 modular headers,CocoaPods 默认生成 module 支持。
  3. s.static_framework = true 或 use_frameworks!,CocoaPods 会以 framework 方式组织代码。
  4. Pod 是给 Swift 使用,CocoaPods 会自动生成 module.modulemap 和 umbrella.h 以供 Swift import 使用。

参考:gitee.com/style_tende…

iOS 使用 Objective-C 实现基于 Wi-Fi 的 Socket 通信(TCP)

在iOS开发中,除了蓝牙通信(BLE),Wi-Fi网络下的Socket 通信同样常用于设备互联,比如智能家居、安防系统、工业控制等应用场景。

本篇文章将以Objective-C为语言,完整讲解我在iOS开发工程中如何使用 GCDAsyncSocket 第三方库,在 iOS设备上通过Wi-Fi与局域网设备建立 TCP Socket 通信连接,并实现数据的收发。

使用的框架

导入第三方库 GCDAsyncSocket

我们使用 CocoaAsyncSocket 项目中的 GCDAsyncSocket 作为TCP Socket的封装库。

安装方式:

使用 CocoaPods 安装:

pod 'CocoaAsyncSocket'

或手动集成:

可以自行从GitHub下载,详细教程可以从GitHub官网查看说明。

通信原理简介

  • 基于 TCP 协议的通信是可靠连接
  • iOS 作为客户端,主动连接服务器(通常是局域网中的嵌入式设备)
  • 通过 IP 和端口完成连接,发送/接收数据使用 NSData

创建 SocketManager 类(Objective-C)

我们将通信逻辑封装在 SocketManager 单例类中,方便统一管理。

SocketManager.h


#import <Foundation/Foundation.h>
#import "GCDAsyncSocket.h"

NS_ASSUME_NONNULL_BEGIN

@interface SocketManager : NSObject <GCDAsyncSocketDelegate>

@property (nonatomic, strong) GCDAsyncSocket *socket;

/// 单例
+ (instancetype)sharedManager;

/// 连接服务器
- (void)connectToHost:(NSString *)host port:(uint16_t)port;

/// 发送数据
- (void)sendData:(NSData *)data;

/// 断开连接
- (void)disconnect;

@end

NS_ASSUME_NONNULL_END

SocketManager.m

#import "SocketManager.h"

@implementation SocketManager

+ (instancetype)sharedManager {
    static SocketManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[SocketManager alloc] init];
    });
    return manager;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    }
    return self;
}

#pragma mark - 连接服务器

- (void)connectToHost:(NSString *)host port:(uint16_t)port {
    if ([self.socket isConnected]) {
        [self.socket disconnect];
    }
    NSError *error = nil;
    BOOL result = [self.socket connectToHost:host onPort:port error:&error];
    if (!result || error) {
        NSLog(@"连接失败:%@", error.localizedDescription);
    } else {
        NSLog(@"正在连接 %@:%d", host, port);
    }
}

#pragma mark - 发送数据

- (void)sendData:(NSData *)data {
    if ([self.socket isConnected]) {
        [self.socket writeData:data withTimeout:-1 tag:0];
    } else {
        NSLog(@"Socket未连接,无法发送");
    }
}

#pragma mark - 断开连接

- (void)disconnect {
    [self.socket disconnect];
}

#pragma mark - GCDAsyncSocketDelegate

/// 连接成功回调
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    NSLog(@"连接成功:%@:%d", host, port);
    [sock readDataWithTimeout:-1 tag:0]; // 准备接收数据
}

/// 接收到数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    NSString *receivedStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"收到数据:%@", receivedStr);
    [sock readDataWithTimeout:-1 tag:0]; // 继续读取
}

/// 数据发送成功
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
    NSLog(@"数据发送成功");
}

/// 断开连接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err {
    NSLog(@"连接断开:%@", err ? err.localizedDescription : @"正常断开");
}

@end

示例调用代码


NSString *ip = @"192.168.1.100"; // 设备IP
uint16_t port = 8080; // 端口

[[SocketManager sharedManager] connectToHost:ip port:port];

// 构建发送命令
NSString *cmd = @"A1B2C3D4";
NSData *cmdData = [ParseDataTool transToDataWithString:cmd];
[[SocketManager sharedManager] sendData:cmdData];

我在工作中常见问题与建议

1. 连接不上设备?

  • 检查设备是否和 iOS 在同一局域网
  • 检查端口是否开放(用 PC 尝试连接测试)
  • 如果是虚拟机或开发板,请关闭防火墙或开启Socket服务

2. 收不到数据?

  • 检查服务器是否主动发送数据
  • 请确保调用了 readDataWithTimeout:tag: 读取回包

3. 断开频繁?

  • TCP需要持续心跳或定时交互,避免NAT路由断流
  • 可设置定时器每隔 30 秒发送一次PING包

实用拓展建议

  • 建议使用我之前的ParseDataTool 工具类来处理十六进制命令、异或校验等底层数据
  • 若需处理大数据或文件传输,请考虑分片 + 回包确认机制
  • 若需加密通信,可结合TLS或AES加密库使用

若有说错的地方,恳请大家指正。相互学习,一起进步~~

掌握生死时速:苹果应用加急审核全攻略!

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

image.png

在应用开发的过程中,总会遇到一些突发情况,比如应用版本在上线后出现重大 bug 或崩溃,严重影响了大部分用户的使用体验。这种情况下,及时响应和修复问题至关重要,不仅可以减少用户的不良体验,还能防止应用评分的下降。

然而,即便你已经找到了问题的根源,并准备好了解决方案,想要快速发布更新版本,也并非完全由你决定。

因为首先苹果是不允许热更新的,每个版本发布都必须经过苹果的审核,其次审核是否通过完全取决于苹果,因此加快审核流程就显得尤为重要。

但很多 iOS 开发者可能不知道,其实可以通过“加急审核”来告知 App Review 团队有紧急更新需要审核,从而加快审核速度。

本文将详细介绍如何通过 Apple 开发者门户请求加急审核。

如何请求加急审核

要请求加急审核,你需要前往 developer.apple.com 并登录到你的开发者账号。登录的账号必须具备管理需要请求加急审核的应用的权限。

image.png

  1. 滚动到主页底部,点击“联系我们”链接。

image.png 2. 从列表中选择“应用审核”类别。

Image

  1. 选择“请求加急应用审核”选项。

Image

  1. 点击“联系应用审核团队”。

Image

  1. 填写表单,告知审核团队你希望加快处理的应用信息。

Image

需要在表单中提供的信息包括:

  • 你希望向应用审核团队提出的请求类型:在我们这种情况下,选择“请求加急审核”选项。

  • 请求加急审核的人员姓名。

  • 请求加急审核的人员邮箱。

  • 拥有该应用的组织名称。

  • 应用名称。

  • 需要加急审核的版本平台。

完成以上步骤后,点击最后的“Send”按钮,将请求提交给应用审核团队。

正常情况下,几个小时后会收到苹果审核的结果。

注意事项

虽然加急审核是开发者工具箱中的一个强大工具,是在紧急情况下将应用快速交到用户手中的最佳方式,但它应仅在特殊情况下使用。正如 Apple 在审核表单中提到的:

如果你面临紧急情况,比如修复关键 bug 或发布应用以配合某个事件,可以通过填写此表单请求加急审核。

同时需要注意,太多次的加急审核请求可能会导致 Apple 对你的反感,然后可能会导致对你之后的加急请求不予理会,因此要谨慎使用。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

Swift 6.2 中的 `@concurrent`

核心概念

@concurrent 是 Swift 6.2 引入的新特性,用于明确标记需要卸载到全局执行器(后台线程)的函数。它与 nonisolated(nonsending) 共同构成 Swift 并发模型的改进,旨在解决以下问题:

  1. 行为统一
    消除异步/同步函数在隔离行为上的不一致性
  2. 显式意图
    明确标识需要并发执行的代码
  3. 简化复杂度
    减少不必要的并发隔离域

关键机制解析

1. nonisolated(nonsending)(统一行为)

// 始终在调用者的执行器上运行
nonisolated(nonsending) func decode<T: Decodable>(_ data: Data) async throws -> T
版本 行为差异
Swift 6.1 异步函数 → 全局执行器
Swift 6.1 同步函数 → 调用者执行器
Swift 6.2 统一在调用者执行器运行

2. @concurrent(显式卸载)

// 明确卸载到全局执行器
@concurrent func decode<T: Decodable>(_ data: Data) async throws -> T
特性 说明
自动标记 nonisolated 无需额外声明
创建新隔离域 要求状态实现 Sendable
使用限制 不能与显式隔离声明(如 @MainActor)共存

何时使用 @concurrent

适用场景

class Networking {
    // 主线程安全的网络请求
    func loadData(from url: URL) async throws -> Data { ... }
    
    // 耗时解码 → 适合 @concurrent
    @concurrent func decode<T: Decodable>(_ data: Data) async throws -> T {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    }
    
    func getFeed() async throws -> Feed {
        let data = try await loadData(from: Feed.endpoint)
        // 避免阻塞调用者线程
        let feed: Feed = try await decode(data)
        return feed
    }
}

使用原则

  1. 精准定位
    仅标记实际需要并发的函数(如 CPU 密集型任务)
  2. 避免过度使用
    减少不必要的隔离域和 Sendable 约束
  3. 性能优化
    解决特定性能瓶颈(如大数据量解码)

总结对比表

特性 nonisolated (旧) nonisolated(nonsending) @concurrent
执行位置 异步→全局/同步→调用者 始终在调用者执行器 全局执行器
隔离域 可能创建新隔离域 不创建新隔离域 创建新隔离域
状态要求 潜在需要 Sendable 无特殊要求 必须 Sendable
使用场景 兼容旧版 默认推荐 显式并发需求
代码可读性 意图模糊 行为明确 意图明确

在Swift中运行Silero VAD

最近又开始学习Swift了,前段时间在AI的帮助下做了一个可以和大模型聊天的软件,当时VAD的功能很头痛,搜了下有一个付费的Cobra VAD,另外就只有靠音频能量判断了,这种方式不准。

最近做的东西又有VAD需求了,研究了很久后可以在Swift里跑Silero VAD了,直接把代码丢出来。

由于我不知道如何把ONNX模型转成Core ML的,官方ONNX Runtime只有Pods的包,我用的是另一个Swift Packags版本的ONNX Runtime,用Pods的包要把import OnnxRuntimeBindings换一下。

//
//  SileroVAD.swift
//  Real-time Captions
//
//  Created by yu on 2025/6/30.
//

import AVFoundation
import Foundation
import OnnxRuntimeBindings

/// 说话起止事件回调
protocol SileroVADDelegate: AnyObject {
    /// 检测到"开始说话"
    /// - Parameter probability: 触发时那一帧的 VAD 概率
    func vadDidStartSpeech(probability: Float)

    /// 检测到"结束说话"
    /// - Parameter probability: 触发时那一帧的 VAD 概率
    func vadDidEndSpeech(probability: Float)
}

final class SileroVAD {
    // MARK: - 可调参数

    public struct Config {
        /// 进入说话的高阈值
        public var threshold: Float = 0.5
        /// 退出说话的低阈值(自动与 threshold 保持 0.15 差值)
        public var negThreshold: Float { max(threshold - 0.15, 0.01) }
        /// 连续多长时间高于 threshold 才算"开始说话"(秒)
        public var startSecs: Float = 0.20
        /// 连续多长时间低于 negThreshold 才算"结束说话"(秒)
        public var stopSecs: Float = 0.80
        /// 采样率,仅支持 8 kHz / 16 kHz
        public var sampleRate: Int = 16000

        public init() {}
    }

    // MARK: - 内部状态

    private enum VADState {
        case silence // 静音状态
        case speechCandidate // 可能开始说话
        case speech // 正在说话
        case silenceCandidate // 可能结束说话
    }

    private enum VADError: Error {
        case modelLoadFailed(String)
        case invalidAudioFormat(String)
        case inferenceError(String)
        case tensorCreationFailed(String)
    }

    // MARK: - 核心属性

    private let session: ORTSession
    private var state: ORTValue
    private let config: Config
    public weak var delegate: SileroVADDelegate?

    // 状态机相关
    private var vadState: VADState = .silence
    private var speechFrameCount = 0
    private var silenceFrameCount = 0
    private var lastProbability: Float = 0.0

    // 阈值(基于配置计算的帧数)
    private let speechFrameThreshold: Int
    private let silenceFrameThreshold: Int

    // 音频缓冲
    private var sampleBuffer: [Float] = []
    private let bufferSize = 512

    // MARK: - 公有方法

    public init(config: Config = Config(), delegate: SileroVADDelegate? = nil) {
        self.config = config
        self.delegate = delegate

        // 计算帧数阈值(基于配置动态计算窗口时长)
        let windowDurationSecs = Float(bufferSize) / Float(config.sampleRate)
        speechFrameThreshold = Int(config.startSecs / windowDurationSecs)
        silenceFrameThreshold = Int(config.stopSecs / windowDurationSecs)

        guard let modelPath = Bundle.main.path(forResource: "silero_vad", ofType: "onnx") else {
            fatalError("SileroVAD: Model file not found in bundle")
        }

        do {
            let env = try ORTEnv(loggingLevel: .warning)
            let sessionOptions = try ORTSessionOptions()

            // 性能优化配置
            try sessionOptions.setGraphOptimizationLevel(.all)
            try sessionOptions.setIntraOpNumThreads(Int32(ProcessInfo.processInfo.processorCount))

            // 尝试启用Core ML硬件加速
            do {
                let coreMLOptions = ORTCoreMLExecutionProviderOptions()
                try sessionOptions.appendCoreMLExecutionProvider(with: coreMLOptions)
                print("SileroVAD: Using Core ML Execution Provider (Neural Engine/NPU)")
            } catch {
                print("SileroVAD: Using optimized CPU execution with \(ProcessInfo.processInfo.processorCount) cores")
            }

            session = try ORTSession(env: env, modelPath: modelPath, sessionOptions: sessionOptions)

        } catch {
            fatalError("SileroVAD: Failed to create ONNX session: \(error)")
        }

        // 初始化RNN状态 (shape: 2, 1, 128)
        let stateData = Array(repeating: Float(0.0), count: 2 * 1 * 128)
        do {
            state = try ORTValue(tensorData: NSMutableData(data: Data(bytes: stateData, count: stateData.count * 4)),
                                 elementType: .float,
                                 shape: [2, 1, 128])
        } catch {
            fatalError("SileroVAD: Failed to create initial state tensor: \(error)")
        }
    }

    /// 输入音频样本,自动处理状态检测
    public func feed(_ samples: [Float]) {
        sampleBuffer.append(contentsOf: samples)

        // 当有足够样本时自动检测
        while sampleBuffer.count >= bufferSize {
            if let probability = performDetection() {
                updateVADState(probability: probability)
            }
        }
    }

    /// 重置内部状态机 & RNN 隐状态
    public func reset() {
        // 重置状态机
        vadState = .silence
        speechFrameCount = 0
        silenceFrameCount = 0
        lastProbability = 0.0

        // 清空缓冲区
        sampleBuffer.removeAll()

        // 重置RNN状态
        let stateData = Array(repeating: Float(0.0), count: 2 * 1 * 128)
        do {
            state = try ORTValue(tensorData: NSMutableData(data: Data(bytes: stateData, count: stateData.count * 4)),
                                 elementType: .float,
                                 shape: [2, 1, 128])
        } catch {
            print("SileroVAD: Failed to reset state tensor: \(error)")
        }
    }

    // MARK: - 私有方法

    private func performDetection() -> Float? {
        guard sampleBuffer.count >= bufferSize else {
            return nil
        }

        // 取出一个窗口的样本
        let vadInput = Array(sampleBuffer.prefix(bufferSize))
        sampleBuffer.removeFirst(bufferSize)

        do {
            let probability = try runInference(audioData: vadInput)
            lastProbability = probability
            return probability
        } catch {
            print("SileroVAD: Detection error: \(error)")
            return nil
        }
    }

    private func runInference(audioData: [Float]) throws -> Float {
        guard audioData.count == 512 else {
            throw VADError.invalidAudioFormat("Audio data must be exactly 512 samples")
        }

        // 创建输入张量
        let inputTensor = try ORTValue(
            tensorData: NSMutableData(data: Data(bytes: audioData, count: audioData.count * 4)),
            elementType: .float,
            shape: [1, 512]
        )

        // 创建采样率张量
        var srData = Int64(config.sampleRate)
        let srTensor = try ORTValue(
            tensorData: NSMutableData(data: Data(bytes: &srData, count: 8)),
            elementType: .int64,
            shape: [1]
        )

        // 准备输入
        let inputs: [String: ORTValue] = [
            "input": inputTensor,
            "state": state,
            "sr": srTensor,
        ]

        // 执行推理
        let allOutputNames = try session.outputNames()
        let outputs = try session.run(withInputs: inputs, outputNames: Set(allOutputNames), runOptions: nil)

        // 提取结果
        guard let outputTensor = outputs["output"] else {
            throw VADError.inferenceError("Missing 'output' tensor")
        }

        guard let newStateTensor = outputs["stateN"] else {
            throw VADError.inferenceError("Missing 'stateN' tensor")
        }

        // 更新状态
        state = newStateTensor

        // 提取概率值
        let tensorData = try outputTensor.tensorData() as Data
        let probability = tensorData.withUnsafeBytes { bytes in
            bytes.load(as: Float.self)
        }

        return probability
    }

    private func updateVADState(probability: Float) {
        let isHighProbability = probability >= config.threshold
        let isLowProbability = probability <= config.negThreshold

        switch vadState {
        case .silence:
            if isHighProbability {
                vadState = .speechCandidate
                speechFrameCount = 1
                silenceFrameCount = 0
            }

        case .speechCandidate:
            if isHighProbability {
                speechFrameCount += 1
                if speechFrameCount >= speechFrameThreshold {
                    vadState = .speech
                    delegate?.vadDidStartSpeech(probability: probability)
                }
            } else {
                vadState = .silence
                speechFrameCount = 0
            }

        case .speech:
            if isLowProbability {
                vadState = .silenceCandidate
                silenceFrameCount = 1
                speechFrameCount = 0
            } else if isHighProbability {
                // 继续说话,重置静音计数
                silenceFrameCount = 0
            }

        case .silenceCandidate:
            if isLowProbability {
                silenceFrameCount += 1
                if silenceFrameCount >= silenceFrameThreshold {
                    vadState = .silence
                    delegate?.vadDidEndSpeech(probability: probability)
                }
            } else if isHighProbability {
                vadState = .speech
                silenceFrameCount = 0
            }
        }
    }
}

要下载模型silero_vad.onnx丢进项目。

当然这个代码也是Claude帮我写的。

Swift 的多平台策略,需要我们大家一起来建设 | 肘子的 Swift 周报 #091

issue91.webp

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

Swift 的多平台策略,需要我们大家一起来建设

继 2025 年 2 月 Swift 社区论坛发布关于启动 Android Community Workgroup 的消息数月后,Swift.org 于上周正式宣布成立官方 Android 工作组。这标志着由官方主导的 Swift 安卓平台支持正式启动,未来 Swift 开发者有望获得更完善的安卓适配工具链与开发体验。

不过,在欣喜之余,我们也应正视一个现实:对于绝大多数 Swift 开发者来说,长期以来的开发工作深度依赖苹果生态,日常所用 API 多与系统框架强耦合。尽管 Swift 社区和苹果已着手推进 Foundation 的纯 Swift 化改造,并陆续提供更多跨平台基础库,但这距离满足实际跨平台开发的需求仍有相当差距。

不久前,Swift Package Index 在原有对苹果平台和 Linux 的兼容性标识基础上,新增了对 Android 与 Wasm 平台的支持,侧面反映出社区对多平台适配的重视。我也借此机会让自己的两个库完成了对 Linux 的兼容。不过在适配过程中也深刻体会到,目前还缺乏一个便捷、统一的跨平台开发环境。虽然这两个库的适配较为简单,仅通过 GitHub Actions 就完成了编译测试和修复,但若将来需要支持更多平台,社区能否构建一个便利、安全的适配机制将变得至关重要。

近年来,Swift 在多平台战略上的推进明显提速,但若想真正成为跨平台开发者的主流选择,仅靠官方与苹果的努力还远远不够。我们每一位 Swift 开发者的参与同样不可或缺。Swift 越强大,Swift 开发者越受益。Swift 的多平台生态,需要我们共同建设!

前一期内容全部周报列表

原创

NotificationCenter.Message:Swift 6.2 并发安全通知的全新体验

NotificationCenter 作为 iOS 开发中的经典组件,为开发者提供了灵活的广播——订阅机制。然而,随着 Swift 并发模型的不断演进,传统基于字符串标识和 userInfo 字典的通知方式暴露出了诸多问题。为了彻底解决这些痛点,Swift 6.2 在 Foundation 中引入了全新的并发安全通知协议:NotificationCenter.MainActorMessageNotificationCenter.AsyncMessage。它们充分利用 Swift 的类型系统和并发隔离特性,让消息的发布与订阅在编译期就能得到验证,从根本上杜绝了“线程冲突”和“数据类型错误”等常见问题。

近期推荐

Xcode Coding Intelligence 逆向解析简报 (Reverse-Engineering Xcode's Coding Intelligence Prompt)

在 Xcode 26 中,苹果正式推出了备受期待的 AI 编码助手 —— Coding Intelligence。相较于市面上已有的 AI 编程工具,苹果在系统提示词(system prompt)的设计上是否有自己的哲学?Peter Friese 借助 Proxyman 对其进行了深入逆向分析。通过这些解析出的提示词内容,我们不仅可以了解 Coding Intelligence 的工作机制,也能窥见苹果对现代开发实践的倾向性,比如:强烈推荐使用 Swift Concurrency(async/await、actor)而非 Combine,测试建议使用 Swift Testing 框架与宏。这些设计细节,是苹果开发范式的重要指标。


SwiftUI 设计系统中的语义颜色设计 (SwiftUI Design System Considerations: Semantic Colors)

在构建 SwiftUI 设计系统 API 时,如何优雅地处理 语义颜色(Semantic Colors) 始终是一个令人头疼的问题。Magnus Jensen 在本文中系统梳理了常见方案的优缺点,并提出了一种基于宏(macro)的解决路径,力求实现 可读性强、类型安全、上下文感知 的色彩系统。如果你正打算为自己的 SwiftUI 项目设计一套结构清晰、可维护的风格体系,这篇文章值得一读。


iOS 内存效率指南系列 (Memory Efficiency in iOS)

随着项目复杂度的提升,开发者终将面对内存相关的问题:内存泄漏、系统警告,甚至因资源占用过高被系统强制终止。在这种情况下,如何诊断问题、控制内存占用,是对开发者经验与体系理解的深度考验。Anton Gubarenko 在两篇文章(内存优化篇)中,系统梳理了 iOS 应用内存使用的评估方式、诊断工具以及优化手段,构建出一套完整、实用的内存管理知识体系。


What is @concurrent in Swift 6.2?

从 Swift 最近的几个版本更新和 Xcode 26 的表现可以看出,Swift 团队正有意识地优化并发编程的开发体验。通过启用新的默认行为,开发者无需在一开始就理解所有细节,便能写出更安全的并发代码。@concurrent 的引入,正是这一策略下的产物之一。在 Donny Wals 的这篇文章中,他详细介绍了 @concurrent 的背景与用途。简单来说,@concurrent 是 Swift 6.2 引入的显式并发标记,主要用于在启用 NonIsolatedNonSendingByDefault 特性时,明确指定函数运行在全局执行器上,从而在需要时将工作负载转移到后台线程,避免阻塞调用者所在的 actor(如主线程)。

或许有人会质疑 Swift 是否又在“用新关键字补旧洞”,但从语言设计趋势来看,随着并发模型逐步完善,许多旧关键字的使用将逐渐被默认机制吸收、简化甚至隐藏。


Swift 与 Java 互操作 (Swift 6.2 Java interoperability in Practice)

Swift 与 Java 的互操作并非新鲜事物,但过往的解决方案往往过程复杂且容易出错。Swift 6.2 引入的 swift-java 包具有划时代意义——这是首次提供官方支持、与工具链深度集成、开发体验接近一等公民的互操作方案,标志着 Swift 和 Java 之间真正意义上的“无缝互通”正式到来。Artur Gruchała 通过一个完整的示例项目,详细演示了如何从 Swift 端调用 Java 方法、构建双语言协作的 CLI 应用,并深入分析了实际开发中容易踩坑的关键细节——特别是 classpath 配置等看似简单却至关重要的环节。


Kodeco 教程:迁移到 Swift 6 (Migrating to Swift 6 Tutorial)

Swift 6 引入了更严格的并发规则与更加结构化的编程范式。在迁移过程中,理解隔离域、Sendable 类型、默认行为,以及 @concurrent 的使用变得尤为重要。Audrey Tam 通过一个完整的 SwiftUI 示例项目(附项目源码),系统演示了从 Swift 5 迁移至 Swift 6.2 的全过程,涵盖 Xcode 设置、并发语义调整与数据隔离等核心环节,是一篇很具实用价值的迁移教程。


Modern Concurrency - Swift 6.2 Suite of Examples

如何在 async/await 中实现类似 Combine 的 throttle 操作?如何持续追踪 @Observable 属性的变化?如何构建支持多消费者的异步流?Lucas van Dongen 在这个开源项目中给出了系统性的实践示例。他汇集了 Swift 6.2 并发模型下的多种模式,演示了如何在实际项目中逐步替代 Combine,迁移到更现代、类型安全的并发范式。


是否升级应用的最低支持版本?(Considerations for New iOS Versions)

WWDC 25 中 Liquid Glass 的登场令人惊艳,但要同时支持两种视觉风格,对开发资源是一大考验。这也让很多开发者开始思考是否应放弃对旧系统的支持。David Smith 建议从两个角度判断:现有用户影响新用户流失。以他的 Widgetsmith 应用为例,当前仍有约 9% 的新增用户来自旧系统,一旦抬高最低支持版本将直接失去这部分潜在用户。他认为,只有当旧系统用户占比降至个位数时,再做版本升级才更合理——简化技术负担,不应以牺牲业务增长为代价

活动

AdventureX 25 游客指南

AdventureX 25 将于 2025 年 7 月 23 日至 27 日在杭州市湖畔创研中心与未来科技城学术交流中心举行。本指南包含活动行程介绍、参与方式、群聊福利、出行与住宿建议及注意事项等内容。不论你是来逛展、互动,还是寻找志同道合的伙伴,这份指南都将帮助你轻松规划行程~

往期内容

THANK YOU

如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

❌