普通视图

发现新文章,点击刷新页面。
昨天 — 2025年10月27日掘金 iOS

iOS底层原理:KVC分析

2025年10月27日 17:22

KVC是什么

KVC全称**Key-Value Coding,俗称键值编码。它是一种通过字符串描述符而不是通过调用访问方法或者直接使用实例变量的非直接的访问对象属性的机制。在iOS中,NSObject、NSArray、NSDictionary**等类使用这种机制并采用分类的形式为自身拓展了KVC的能力。常用的Api如下:

- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;

点方法进去:

1.png

2.png

3.png

我们可以看到,它是在 Foundation 框架下,关于**NSObject、NSArray、NSDictionary等的一个NSKeyValueCoding**的分类。其实这些内容在苹果的官方文档中解释的很清楚,小伙伴们可以自行查阅。Key-Value Coding Programming Guide

KVC设值和取值

KVC的使用我们都不陌生,那么KVC在内部操作时是以怎样的顺序来寻找key的呢?接下来我们就来探索一下。

取值 valueForKey:

先看下苹果官方文档的Setter步骤:

4.png

5.png

国际惯例,翻译一下:

  1. 在实例中搜索找到的第一个名称为get<Key><key>is<Key>、或的访问器方法_<key>,按该顺序。如果找到,则调用它并使用结果继续执行步骤 5。否则继续下一步。

  2. 如果没有找到简单的访问器方法,则在实例中搜索名称与模式countOf<Key>objectIn<Key>AtIndex:(对应于NSArray类定义的原始方法)和<key>AtIndexes:(对应于NSArray方法objectsAtIndexes:)的方法。 如果找到这些中的第一个和其他两个中的至少一个,则创建一个响应所有NSArray方法的集合代理对象并返回该对象。否则,继续执行步骤 3。 代理对象随后将任何NSArray接收到的一些组合的消息countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:消息给键-值编码创建它兼容的对象。如果原始对象还实现了一个可选的方法,其名称类似于get<Key>:range:,则代理对象也会在适当的时候使用它。实际上,与键值编码兼容的对象一起工作的代理对象允许底层属性表现得好像它是NSArray,即使它不是。

  3. 如果没有找到简单的访问方法或阵列访问方法组,寻找一个三重的方法命名countOf<Key>enumeratorOf<Key>和memberOf<Key>:(对应于由所定义的原始的方法NSSet类)。 如果找到所有三个方法,则创建一个响应所有NSSet方法的集合代理对象并返回该对象。否则,继续执行步骤 4。 此代理对象随后将任何NSSet接收到的一些组合信息countOf<Key>enumeratorOf<Key>memberOf<Key>:消息以创建它的对象。实际上,与键值编码兼容的对象一起工作的代理对象允许底层属性表现得好像它是NSSet,即使它不是。

  4. 如果发现收集的访问方法没有简单的存取方法或者组,如果接收器的类方法accessInstanceVariablesDirectly返回YES,搜索名为实例变量_<key>_is<Key><key>,或者is<Key>,按照这个顺序。如果找到,直接获取实例变量的值并进行步骤5,否则进行步骤6。

  5. 如果检索到的属性值是一个对象指针,只需返回结果即可。 如果该值是 支持的标量类型NSNumber,则将其存储在一个NSNumber实例中并返回该实例。 如果结果是NSNumber不支持的标量类型,则转换为NSValue对象并返回。

  6. 如果所有其他方法都失败,请调用valueForUndefinedKey:,默认情况下,这会引发异常,但 的子类NSObject可能会提供特定于键的行为。

这里推荐一个翻译工具DeepL,个人觉得还是挺准挺好用的。好了,废话不多说,我们顺着这个步骤来验证一下。

首先按照文档中的顺序**get<Key>** -> <key> -> is<Key> -> _<key> 进行调试验证即可,如图:

6.png

7.png

如果以上方法都没有,且**accessInstanceVariablesDirectly为YES时,则按照_<key>** -> _is<Key> -> <key> -> **is<Key>**取值,如图:

8.png

9.png

10.png

11.png

以上取值顺序依次注释代码进行验证即可,如果都没有的话,就会调用**valueForUndefinedKey:**这个方法,最终抛出异常。

设值 setValue:forKey:

先看下苹果官方文档的**Setter**步骤:

12.png

翻译如下:

  1. 按顺序寻找名为set<Key>:_set<Key>的第一个访问器。如果找到了,就用输入值(或根据需要解开的值)来调用它,然后完成。

  2. 如果没有找到简单的访问器,并且如果类方法accessInstanceVariablesDirectly返回YES,那么寻找一个名称为_<key>_is<Key><key>is<Key>的实例变量,依次进行。如果找到了,直接用输入值(或解开的值)设置变量,然后完成。

  3. 一旦发现没有访问器或实例变量,就调用setValue:forUndefinedKey:。这默认会引发一个异常,但NSObject的子类可以提供特定于键的行为。

首先来验证:set<Key>: ->_set<Key>:

13.png

14.png

15.png

从上面getter取值顺序我们可以看到有**get<Key>** -> <key> -> is<Key> -> _<key>四个方法,而此处的setter只有set<Key>:_set<Key>:两个方法。显然,对setter的第一步骤有所怀疑,那就探索一下,实现setIs<Key>_setIs<Key>,如图:

16.png

17.png

经过代码验证,这里确实会走**setIs<Key>:,而_setIs<Key>:**没有调用。所以苹果文档这里的描述并不全面。

当**set<Key>:** -> _set<Key>: ->setIs<Key>都没有实现的时候, 并且当accessInstanceVariablesDirectly设置为YES时 获取实例变量的顺序为顺序查找名称为_<key> -> _is<Key> -> <key> -> is<Key>,如下图:

18.png

19.png

20.png

21.png

根据上面的流程,梳理一张**setValue:forKey:**的流程图:

22.png

KVC自定义实现

清楚了KVC的设值和取值流程,我们就可以自定义一下它的实现过程,代码如下:

- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
   
    // KVC 自定义
    // 1: 判断什么 key
    if (key == nil || key.length == 0) {
        return;
    }
    
    // 2: setter set<Key>: or _set<Key>,
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self lg_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4: 间接变量
    // 获取 ivar -> 遍历 containsObjct -
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        // 4.2 获取相应的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 对相应的 ivar 设置值
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:_isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:key]) {
       Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }
    
    // 5:如果找不到相关实例
    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
    
}


- (nullable id)lg_valueForKey:(NSString *)key{
    
    // 1:刷选key 判断非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
        if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
            int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i<num-1; i++) {
                num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            }
            for (int j = 0; j<num; j++) {
                id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                [mArray addObject:objc];
            }
            return mArray;
        }
    }
#pragma clang diagnostic pop
    
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4.找相关实例变量进行赋值
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // _name -> _isName -> name -> isName
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }

    return @"";
}


#pragma mark - 相关方法
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
 
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

- (id)performSelectorWithMethodName:(NSString *)methodName{
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}

- (NSMutableArray *)getIvarListName{
    
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}

iOS底层原理:Method Swizzling原理和注意事项

2025年10月27日 17:21

Method Swizzling 是什么?

Method Swizzling的含义是方法交换,其核心内容是使用**runtime api**在运行时将一个方法的实现替换成另一个方法的实现。我们利用它可以替换系统或者我们自定义类的方法实现,进而达到我们的特殊目的,这就是我们常说的iOS黑魔法。

本文Demo地址:Github-JQMethodSwizzling

Method Swizzling 原理

OC方法在底层是通过**方法编号SEL函数实现IMP一一对应进行关联的。打个比方,OC类好比一本书,SEL就像是书中的目录,IMP**相当于每条目录所对应的页码。关系如图所示:

1.png

方法交换的代码如下:

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swiSEL);
    method_exchangeImplementations(oriMethod, swiMethod);

交换后的关系如图所示:

2.png

从上述方法交换可以看出:

  • 交换的是两者的方法实现,也就是说调用**oriSEL方法时,最终走的方法实现是swiIMP;调用swiSEL方法时,最终走的方法实现是oriIMP**。
  • 由此可见,在进行方法交换操作时,如果交换代码调用了两次或多次(2的倍数),就会导致方法实现又交换了回去,相当于交换了个寂寞,所以交换代码建议放在单例下进行来保证方法交换的有效性。

方法交换在使用中的递归调用分析

首先,我们来创建一个**JQStudent类,类中有两个实例方法,jq_studentInstanceMethod studentInstanceMethod;然后,在load方法中对两个方法进行交换;最后,jq_studentInstanceMethod 的实现中再次调用jq_studentInstanceMethod **方法。

代码实现如下图:

3.png

我们看到,这里会在**jq_studentInstanceMethod **方法中再次调用该方法,会不会引起递归调用呢?

运行结果如下图:

4.png

从运行结果看,并没有引起递归。这是因为进行方法交换后,在执行**[st studentInstanceMethod]时,实际上找到的是jq_studentInstanceMethod 的方法实现,而jq_studentInstanceMethod 方法实现中又执行[self jq_studentInstanceMethod],同样是因为方法交换,此时jq_studentInstanceMethod的方法实现也已经指向了studentInstanceMethod,所以并不会引起递归调用。相反,如果我们在jq_studentInstanceMethod 方法中调用了[self studentInstanceMethod]**才是会引起递归调用的,小伙伴们一定要注意!!! 流程如下图:

5.png

在实际的开发中,我们常采用这种方式对业务流程中的一些关键方法进行方法交换(俗称hook),从而达到不影响业务流程的情况下完成一些信息的收集工作,而这种方式则被称为**AOPAspect Oriented Programming,面向切面编程)。AOP是一种编程的思想,区别于OOPObject Oriented Programming,面向对象编程)。其实OOPAOP都是一种编程的思想,只不过OOP编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元。而AOP**则是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性。

方法交换的坑点和分析

坑点一:交换父类的方法

我们还在刚才的Demo中来演示,现在有一个**JQStudent类了,再创建一个JQPerson类,让JQStudent继承JQPerson,在JQPerson类添加一个实例方法personInstanceMethod,在JQStudent类的load方法中将jq_studentInstanceMethod 方法和父类中的personInstanceMethod**方法进行交换。

实现代码如下图:

6.png

7.png

运行结果如下图:

8.png

从上面的结果可以看到:

  • 子类**JQStudent对象调用父类JQPerson的方法personInstanceMethod**,消息发送会通过方法查找从而找到父类方法并调用。
  • 但是此时父类**JQPerson中的方法personInstanceMethod对应的方法实现已经被交换成了子类JQStudentjq_studentInstanceMethod,因此会执行子类的jq_studentInstanceMethod**方法实现。
  • 同理,此时子类中调用**jq_studentInstanceMethod方法,会执行父类的personInstanceMethod**方法实现。

这样看起来好像没有什么问题啊!紧接着,我们再使用父类**JQPerson对象调用一下personInstanceMethod**方法,如下图:

9.png

啪、啪、啪,报错了!!!我们来分析下什么原因,

  • 首先,父类调换用**personInstanceMethod方法会执行子类中的jq_studentInstanceMethod**方法实现。
  • 然后又调用了**jq_studentInstanceMethod方法,但是,此时的调用者是JQPerson对象,父类JQPerson中并没有jq_studentInstanceMethod**方法实现。所以因方法找不到而报错。

出了问题,我们来解决以下,将交换方式换成下面这种:

10.png

11.png

再看运行结果:

12.png

此时,我们的运行不报错了,而且**JQStudent对象调用父类的personInstanceMethod方法,确实走了方法交换后的流程,JQPerson对象也正常的调用了personInstanceMethod**方法,互不影响。为什么呢? 原因是:

  1. 在方法交换前,先尝试给本类添加一下**oriSEL方法,方法实现为swiMethod**;
  2. 如果添加成功则返回**YES,代表本类中原本没有oriSEL的方法实现;接着,再将父类的方法实现oriMethod替换给本类的swiSEL**;
  3. 添加失败则返回**NO,代表本类中已有oriSEL**的方法实现,进行正常的方法交换即可。

坑点二:交换的父类中并没有实现的方法

如果要交换的父类方法并没有实现呢?直接看下运行结果:

13.png

14.png

什么情况?我的天,递归了!!!为什么呢?我们断点调试一下,看图解释:

15.png

16.png

从上面这些坑中,我们可以得出一些结论:

  • 方法交换要遵循功能单一原则,也就是说本类交换本类中的方法,不能影响父类,否则会影响父类和兄弟姐妹的行为(方法);
  • 即使要交换父类的方法,也要在本类中实现(重写)父类的方法;
  • 本类或父类交换的方法实现不存在,要给本类添加这个方法实现,否则会出现递归调用

基于以上特点,我封装一个更好的方法交换方式,请看以下代码实现:

17.png

运行结果如下:

18.png

苹果 Swift 安卓SDK上线,用一套 Swift 代码开发安卓 App 成为可能!

作者 iOS研究院
2025年10月27日 17:10

背景

10 月 24 日,Swift 官网通过博文宣布,以 Nightly 预览版形式推出首个适用于谷歌安卓系统的 Swift SDK。这一举措标志着 Swift 语言正式突破 iOS 生态边界,向安卓平台迈进。

官方推文.png

回溯今年 6 月,就曾报道 Swift 成立安卓工作组的消息,其核心目标是推动开发者使用 Swift 开发安卓应用。此次 SDK 的发布,意味着该工作组的推进工作取得实质性成果,也让 Swift 跨平台生态建设迈出关键一步。

安卓工作组

Swift 官方在博文中引用开发者 Joannis Orlandos 的观点,强调这一里程碑并非偶然。它既是 Android 工作组数月集中攻坚的结果,也离不开社区多年来的自发贡献。其最终目的是打破不同平台间的技术壁垒,为整个移动生态的创新提速。

目前,该 SDK 预览版已对所有开发者开放,获取方式灵活多样。开发者既可通过最新的 Windows 安装程序直接获取,也能在 Linux 或 macOS 系统上单独下载使用。

为降低上手门槛,官方同步配套了丰富资源。其中包含一份详尽的入门指南,清晰分步讲解环境依赖、软件包与开发包的安装配置流程;同时在 GitHub 上提供多个示例项目,覆盖不同应用场景,供开发者参考学习。

除支持从零搭建新应用外,该 SDK 还为特定需求开发者提供支持。对于希望将 Swift 代码集成到现有 Android 项目的开发者,可通过 swift-java 项目探索 Swift 与 Java 的互操作性,实现两种语言在同一项目中的混合编程。

关于未来规划,Swift 安卓工作组正起草一份规划文件,以明确后续开发方向。同时,工作组也积极邀请开发者在 Swift 官方论坛分享使用体验与建议,共同推动 SDK 完善。

简单来说:

Swift对跨平台的兼容,对于iOS从业来说是一件利好的大事情。不论是从技术栈还是从就业面。

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

相关推荐

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

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

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

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

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

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

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

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

iOS底层原理:OC对象底层探索之alloc初探

2025年10月27日 16:05

0-0.png

iOS开发的小伙伴们对 [XXX alloc] init] 都不陌生,可以说 allocinit 贯穿我们整个的开发过程中。那么在OC对象的底层,到底做了哪些操作呢?今天我们就来探索一下 alloc 底层的工作流程。

一、抛砖引玉

我们先来看一下下面这张图中的测试代码和打印结果: 1.png

从上面的打印结果来看,p、p1、p2对象的内存地址是一样的,但是p、p1、p2对象的指针地址(&p、&p1、&p2)是不同的。而**pNew对象的内存地址和指针地址和p、p1、p2都不一样,很显然,pNew**属于拥有另一块内存空间的另一个对象了。 由此我们暂时得出结论:

  • **p、p1、p2**对象的指针地址是不同的, 但是他们都指向同一内存空间;
  • alloc 可以开辟内存空间,而 init 不会;
  • p、p1、p2对象的指针地址&p > &p1 > &p2 > &pNew,说明栈区是由高到低连续开辟的
  • p 、pNew对象的内存地址p < pNew,说明堆区是由低到高开辟内存的

结合堆栈的知识 ,我画了下面👇这张图,帮助大家理解。

2.png

二、准备工作

通过上面我们可以发现,对象内存地址是通过 alloc 创建,我们看一下 alloc 是怎么实现的。 点击 alloc 方法进入 NSObject.h: 2.1.png

2.2.png

进入**NSObject.h,我们再点击跳转,发现跳转不进去了,也就看不到alloc**的实现了。难道我们就只能停在这里?就只能在外面蹭一蹭了吗? NO,下面来介绍一下探索底层的三种方法,方便我们在探索底层源码的时候能够顺利的跟对方法(函数)的一个执行流程。

第一种:添加符合断点方式
  • 在工程中选择断点 --> 点击左下角"+" --> Symbolic Breakpoint

3.png

  • 比如我这里想知道**alloc源码位置, 那么就输入alloc**

4.png

  • 然后运行, 我们发现**alloc**的符号断点非常多,到底哪个才是我们想要的呢?

5.png

  • 接着我们还需要在想要执行的代码处增加一个普通断点,比如我们这里在**JQPersonalloc处打上一个断点,然后将alloc**符号断点先禁用

6.png

  • 运行程序,首先来到我们的普通断点**[JQPerson alloc]处,然后我们将符号断点alloc**启用,点击断点操作按钮进入下一步

7.png

8.png

到这里,我们可以看到**alloc方法在libobjc.A.dylib**库中(ps:libobjc.A.dylib是objc的系统库,感兴趣的小伙伴可以去苹果开源官网Open Source下载查看,注意:Open Source上下载下来的源码是不能直接编译和调试的,想要下载的objc源码可编译调试的小伙伴可以移步到我之前的文章iOS底层原理(一):苹果开源 objc4-818 源码项目的编译和调试

第二种: 断点 + step into方式
  • 我们先在要执行的代码打上断点,运行项目,来到断点位置

9.png

  • 然后按住control键,点击setp into一步一步查找,会看到如下结果

10.png

  • 最后再添加**objc_alloc符号断点,点击Continue program execution**继续执行

11.png

这里我们可以看到,断点进入了**libobjc.A.dylib中的objc_alloc函数,由此可知alloc方法的源码在libobjc.A.dylib**库中。

第三种: 汇编跟进方式
  • 首先,我们还是先在要执行的代码打上断点

12.png

  • 然后在Xcode菜单栏找到 Debug ==> Debug Workflow ==> Always Show Disassembly并选中(这里是启用汇编进行调试)

13.png

  • 运行项目,来到如下图的断点处

14.png

  • 我们可以看到当前断点下面两行处,有个**callq xxxx; symbol stub for objc_alloc,接着我们再添加一个objc_alloc符号断点, 点击Continue program execution继续执行(ps:这里解释一下:callq是汇编中的一个指令,代表这个这里即将要调用一个方法,symbol stub for objc_alloc翻译过来是objc_alloc的符号存根,也就是说objc_alloc**是要调用的方法名)

15.png 好了,到此底层探索的三种方式就介绍完了,接下来我们步入正题吧!

三、alloc源码探索

好的,有了上面的探索方法,我们现在就拿 objc 源码项目来探索 alloc 的底层实现吧。 首先,打开之前编译好的 objc4-818.2 项目,需要的小伙伴可以参考我之前文章**iOS底层原理(一):苹果开源 objc4-818 源码项目的编译和调试,到 Open Source 上下载源码自行编译,不想麻烦的也可以直接去 GitHub 上下载:JQObjc4-818.2BuildDebug。 然后,找到 JQObjcBuildDemo 目录下创建一个JQPerson类。然后在main.m**中添加如下代码:

16.png注意: 这里 16、17行 分别有个断点,后面会用到!!!

我们从上面的底层探索方式中可以看到:[JQPerson alloc]在底层libobjc.A.dylib库中执行的objc_alloc方法,接下来我就来验证一下。

第1步:alloc 和 objc_alloc
    1. 点击**alloc跳转到 objc 的源码中,搜索一下objc_alloc,然后分别在allocobjc_alloc**处打上断点

17.png18.png

    1. 然后,先将源码中**allocobjc_alloc**处的断点禁用,运行项目来到main.m中的断点处

19.png

    1. 接着,启用源码中**allocobjc_alloc处的断点,点击下一步,这时会发现:断点来到了objc_alloc**处

20.png 这就验证了我们前面所讲的,alloc方法在底层libobjc.A.dylib库中执行的objc_alloc方法

    1. 再次点击下一步,惊奇的发现:断点来到了**alloc**方法处

21.png 那么为什么**[JQPerson alloc]在底层会先走objc_alloc方法,再走alloc方法呢?按照我们在 objc 源码中看到的方法调用流程,应该是[JQPerson alloc] => alloc**呀?

为了验证这个问题,我们需要请出YYDS(永远滴神):llvm源码(是苹果开源的系统级别的源码),看一看苹果是不是在这里面做了什么骚操作。llvm-project下载地址

第2步:llvm-project 底层分析

由于 llvm-project 项目比较大,这里我们用 VSCode 打开

    1. 首先,我们全局搜索一下**alloc或者OMF_alloc:,来到tryGenerateSpecializedMessageSend**方法,这个方法在 CGObjC.cpp 文件中

21-1.png

我们主要看3号位置的方法解释,这里我翻译了一下,大家可以自行去看,这是苹果对性能的一个优化。主要意思就是:objc在运行时提供了快捷入口,这些入口比普通的消息发送速度更快,如果运行支持所需要的入口的话,这个方法就会调用并返回结果,否则返回None,调用者自己生成一个消息发送。

    1. 知道了**tryGenerateSpecializedMessageSend的作用,接着我再来看一下tryGenerateSpecializedMessageSend方法的调用情况,搜索tryGenerateSpecializedMessageSend,来到GeneratePossiblySpecializedMessageSend**

21-2.png

这个方法是运行时在底层的入口,所有的消息发送都会走这里。从代码可以看出,如果**tryGenerateSpecializedMessageSend方法返回None,这里判断为false,就会走GenerateMessageSend方法,也就是调用者自己生成一个普通的msgSend**。

    1. 然后,我们深入到**tryGenerateSpecializedMessageSend方法中,看看alloc是怎么被执行成了objc_alloc。这里看一下tryGenerateSpecializedMessageSend方法中4号位置的代码,这里有个条件判断 if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc") ,如果成立,就会走EmitObjCAlloc**方法,搜索一下,进去看一下

21-3.png

可以看到**EmitObjCAlloc方法这里生成了一个objc_alloc的入口(ObjCEntrypoints),包装为emitObjCValueOperation被返回执行,并且llvm对此做一个标记存在Selector中,而Selector则记录在SelectorTable**中

21-4.png21-5.png21-6.png21-7.png21-8.png

由此可以验证:[JQPerson alloc]在底层会先走到objc_alloc

    1. objc_alloc第一次调用callAlloc方法,会执行msgSend(cls, @selector(alloc))(ps:这个第3步 callAlloc中会讲,这里知道一下,先把llvm这个流程讲完)。 此时llvm底层还是会走tryGenerateSpecializedMessageSend,此时,由于已经标记了**allocSelector,不会再走*if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")*这个判断中的代码,最终返回None。然后由GenerateMessageSend**走普通的消息发送。
第3步:callAlloc

好了,allocobjc_alloc的调用清晰了。接着,我们来看一下最核心的方法callAlloc

    1. objc 源码中我们可以看到**objc_alloc方法中调用了callAlloc**

22.png

    1. 我们观察一下**callAlloc中的代码,会发现这个方法的最后一行(1937行)对传入的 cls 做了一次消息发送,发送的消息名称正是alloc,这似乎可以解释上面走完objc_alloc方法后,又走到alloc**的现象。但是我们还需要打断点,走一下流程来验证。

23.png

    1. 经断点调试,执行**objc_alloc后,callAlloc确实走到了发送alloc消息这一行,也就是[JQPerson alloc] => objc_alloc => callAlloc**

24.png

    1. 继续走断点,我们会发现执行流程为:[JQPerson alloc] => objc_alloc => callAlloc => alloc => _objc_rootAlloc => callAlloc =>_objc_rootAllocWithZone

25.png26.png

    1. 当前我们已经走完了 main.m 中的16行,也就是**(JQPerson)p1alloc**,此时断点会来到17行 (JQPerson)p2alloc
    1. 继续走源码断点,会发现执行流程为:[JQPerson alloc] => objc_alloc => callAlloc => _objc_rootAllocWithZone

27.png28.png29.png

这里我们就会奇怪,为什么**JQPerson类再次alloc时,就直接走到*if (fastpath(!cls->ISA()->hasCustomAWZ()))***条件判断中的代码了呢?

    1. 那我们就来看一下***if (fastpath(!cls->ISA()->hasCustomAWZ()))这句判断到底执行了什么? 进入到if (fastpath(!cls->ISA()->hasCustomAWZ()))***源码中看一下

30.png31.png32.png33.png

由以上源码可以看出: a. 当**JQPerson类第一次调用alloc方法时,底层会先调用objc_alloc,此时callAlloc被第一次调用,callAlloc内部通过当前clsISA返回一个Class对象; b. 紧接着会去判断当前ClasscacheFAST_CACHE_HAS_DEFAULT_AWZ(存储在元类metaclass中,记录着cache中是否已经缓存了alloc/allocWithZone:方法的实现)这个标志位的值是否为真,由于是第一次执行,没有缓存,所以cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ)取出来的值是false,前面加个,变成了truecallAllocif (fastpath(!cls->ISA()->hasCustomAWZ()))又加了个,所以值为false; c. 然后走到了if (allocWithZone),由于objc_alloc方法中allocWithZone参数传值为false,所以走到了(objc_msgSend)(cls, @selector(alloc))。然后,callAlloc被第二次调用,由于执行过了alloc方法,所以此时有了alloc的方法缓存,所以if (fastpath(!cls->ISA()->hasCustomAWZ()))判断为true,执行_objc_rootAllocWithZone。 d. 最后就是 main.m 中第17行JQPerson类第二次调用alloc方法,此时由于JQPerson类的cache中已经有了缓存,FAST_CACHE_HAS_DEFAULT_AWZ这个标志位的值为真,也就是if (fastpath(!cls->ISA()->hasCustomAWZ()))这个条件为真,所以,会直接执行_objc_rootAllocWithZone**。 下面我画一下流程图,帮助小伙们理解一下:

[JQPerson alloc]流程图新.png

另外,这里我附一张**NSObject alloc]**的流程图,有兴趣的小伙们可以去试一试:

[NSObject alloc]流程图.png

这里**NSObject alloc]只走了一遍callAlloc方法,猜测原因是:系统对 NSObject 做了优化,提前给cache**添加了缓存。

好了,**alloc的底层探索今天先写到这里。下面一篇文章我们将探索一下alloc**开辟内存空间相关的源码。敬请期待吧!!!

不要在 SwiftUI 中使用 .onAppear() 进行异步(Async)工作——这就是它导致你的 App 出现 Bug 的原因。

作者 JarvanMo
2025年10月27日 09:40

欢迎关注我的微信公众号:OpenFlutter,感恩

这个问题让我付出了沉重的代价——我的 SwiftUI App 每隔几秒就会随机重新加载数据

起初,我以为是我的 API 出了问题。

接着,我责怪我的 @State 变量。

然后是 Combine。

再后来是 CoreData。

有那么一刻,我甚至迁怒于 Xcode(说实话,它有时确实该背锅)。

但真正的罪魁祸首比我想象的要简单得多,也隐蔽得多:

那个看起来无辜的 .onAppear()。

像许多从 UIKit 转向 SwiftUI 的 iOS 开发者一样,我在任何地方都使用了 .onAppear()。

它感觉像是发起异步工作的自然之所——获取数据、加载图片、与 CoreData 同步以及启动后台更新。

它曾经运行得完美无缺……直到它失灵了。

突然间,我的 API 调用开始触发两次。

列表会闪烁。

有些视图会不停刷新。

最奇怪的是什么?它只是偶尔发生——这种“测不准错误(Heisenbug)”在你开启 Xcode 屏幕录制时又无法重现。

事实证明,SwiftUI 中的 .onAppear() 的含义和你想象的并不一样


1. .onAppear() 并非 viewDidLoad

当你从 UIKit 转来时,你期望获得某些生命周期保证。

viewDidLoad 只运行一次。viewWillAppear 在视图即将出现时每次都会运行。

你可以预测这些时刻。

然而,SwiftUI 是一个完全不同的野兽。

SwiftUI 视图是结构体(structs),而不是类。

根据状态如何变化、哪个父视图触发了重新渲染,或者 SwiftUI 如何优化视图层级,它们可以被多次重新创建。

这意味着你的 .onAppear() 可以一遍又一遍地触发——不只是在视图第一次出现时,而是每当 SwiftUI 觉得需要重新附加(reattaching)该视图时

示例:

struct UserListView: View {
    @State private var users: [User] = []

    var body: some View {
        List(users) { user in
            Text(user.name)
        }
        .onAppear {
            Task {
                await loadUsers()
            }
        }
    }

    func loadUsers() async {
        try? await Task.sleep(nanoseconds: 2_000_000_000)
        users = ["John", "Ava", "Noah"].map { User(name: $0) }
    }
}

看起来没问题,对吧?

然而,如果任何父视图发生了变化——比如一个筛选器(filter)、导航状态,或是一个绑定(binding)更新——SwiftUI 就可以**重新创建(recreate)**这个视图。

然后 .onAppear() 就会再次触发

现在你的 loadUsers() 就会运行多次。

  • 如果这是一个 API 调用,你将反复访问服务器
  • 如果这是 CoreData 操作,你将触发不必要的获取(fetches)
  • 如果是 UI 状态更新,你就会看到闪烁和重置

这一切都仅仅是因为 SwiftUI 认为它只是在重新渲染一个结构体。它并不知道你在里面进行了异步工作。


2. 在 .onAppear() 中进行异步工作是危险的

让我们看看当你将 .onAppear()Task 混用时,究竟会发生什么:

.onAppear {
    Task {
        await loadData()
    }
}

乍一看,这似乎是无害的

但这里有一个微妙的问题:

这个异步 Task 并没有绑定到(tied to)你的视图的生命周期。

因此,即使视图消失了(比如用户导航离开了),这个 Task 仍然在后台运行。

当它最终完成时,它会尝试更新一个 @State 变量……而这个变量可能已经不存在了。

这就是你最终遇到奇怪的运行时崩溃(runtime crashes)的原因,例如:

Publishing changes from background threads is not allowed

或者

Fatal error: Modifying state after view is gone

这些错误并非随机出现。它们是.onAppear() 内部启动的孤立异步任务所导致的直接后果

你只是在没有意识到的情况下制造了竞态条件(race condition)


3. SwiftUI 的正确做法:改用 .task { }

苹果公司知道这是一个问题。

因此,他们在 iOS 15 中引入了 .task { }。

乍一看,它和 .onAppear() 很像,但区别巨大。

.task { } 是专门为异步工作而设计的。

它会在视图消失时自动取消你的任务。

这意味着,如果用户导航离开或视图被销毁,SwiftUI 会安全地取消你的异步调用——没有内存泄漏,也没有僵尸更新(zombie updates)。

让我们用正确的方法重写之前的示例:


struct UserListView: View {
    @State private var users: [User] = []

    var body: some View {
        List(users) { user in
            Text(user.name)
        }
        .task {
            await loadUsers()
        }
    }

    func loadUsers() async {
        try? await Task.sleep(nanoseconds: 2_000_000_000)
        users = ["John", "Ava", "Noah"].map { User(name: $0) }
    }
}

现在,它的行为就完全符合你的预期

  • 任务在每次视图出现时只运行一次
  • 如果视图消失,SwiftUI 会自动取消它。
  • 无需手动管理任务的生命周期。
  • 没有“幽灵任务”(Ghost tasks)。
  • 没有重复加载。
  • 没有竞态条件。

4. 但是等等——为什么 .task 如此有效?

因为 SwiftUI 在内部将其绑定到了视图的“身份”(identity)

每个 SwiftUI 视图都有一个唯一的身份,这个身份决定了它何时处于“活动”状态。

当该身份发生变化时(例如,不同的 id、新的状态或导航事件),SwiftUI 就会取消任何与其相关的 .task。

这就是支持 .task(id:) 工作的机制,这是一个更高级的版本,它允许你控制任务何时重启

.task(id: user.id) {
    await fetchProfile(for: user)
}

因此,每当 user.id 发生变化时,你的异步任务就会重新启动

如果 user.id 没有变化,任务就会保持稳定——不会有重复的获取

这对于像分页列表依赖于选择的动态视图等复杂 UI 来说,是极其有用的。


5. .onAppear() 仍有意义的场景

公平地说,.onAppear() 并非一无是处。 它只是有不同的用途

.onAppear() 非常适合用于:

  • 同步状态更新
  • 动画触发
  • 日志记录或分析事件
  • 不涉及 await 或长时间操作的 UI 更改

例如:

.onAppear {
    isVisible = true
    analytics.log("UserList visible")
}

这样做完全没问题。

没有异步工作,没有外部依赖,自然也没有问题。只要你的代码中出现了 await,就应该把它移出 .onAppear()


6. 幽灵重载问题(The Phantom Reload Problem)

误用 .onAppear() 最令人沮丧的副作用之一发生在列表中。

想象一下这种情况:

ForEach(users) { user in
    UserRow(user: user)
        .onAppear {
            Task {
                await fetchProfilePicture(for: user)
            }
        }
}

这看起来无害——在每个用户行出现时获取他们的个人资料图片。

但在实际操作中,当你滚动时,SwiftUI 会回收(recycles)视图

因此,随着单元格不断出现和消失,.onAppear() 会被一遍又一遍地触发

恭喜你,你刚刚制造了一场后台网络风暴(background network storm)

修复方法:

改用 .task(id:),或者在视图层级的更高层级预取(prefetch)你的数据

ForEach(users) { user in
    UserRow(user: user)
        .task(id: user.id) {
            await fetchProfilePicture(for: user)
        }
}

现在,每个用户的图片获取任务都绑定到了它的身份(identity)。 当视图消失时,SwiftUI 会取消该任务。 这样你就避免了所有那些重复的获取


7. 真实世界的生产环境示例

我曾经为一个基于 SwiftUI 的电子商务应用工作,它有一个标签栏(tab bar)。 “首页”标签有一个仪表板视图,该视图在启动时需要获取多个 API 数据——促销信息、用户数据、购物车数量等。

代码看起来是这样的:

struct HomeView: View {
    @State private var data: HomeData?
    var body: some View {
        VStack {
            if let data {
                HomeDashboard(data: data)
            } else {
                ProgressView()
            }
        }
        .onAppear {
            Task {
                await fetchHomeData()
            }
        }
    }
}

在开发过程中,一切似乎都很正常。

但在生产环境中,用户发现应用运行缓慢

网络日志显示,每当他们切换标签时,就会出现重复的请求(duplicate requests)

为什么?

因为 SwiftUI 在底层会根据内存和导航状态**销毁并重新创建(destroys and recreates)**标签视图。

每次重新创建都会触发 .onAppear(),从而启动一个新的异步任务——即使数据已经加载完毕。

在改用 .task { } 之后,这个问题一夜之间就消失了


8. 调试技巧:打印生命周期事件

如果你不确定你的视图出现了多少次,可以试试这个快速技巧:

.onAppear { print("✅ Appeared!") }
.onDisappear { print("❌ Disappeared!") }

你会对这些事件触发的频率感到震惊——有时甚至当你只是在同级视图之间导航,或者切换更高层级的状态时,它们也会触发。

那一刻你就会意识到 .onAppear() 对异步工作来说有多么危险


9. 额外技巧:将 .task.refreshable 结合使用 ✨

当你处理数据获取时,这种组合简直就是纯粹的 SwiftUI 黄金搭档

struct ArticleList: View {
    @State private var articles: [Article] = []
var body: some View {
        List(articles) { article in
            Text(article.title)
        }
        .task { await loadArticles() }
        .refreshable { await loadArticles() }
    }
    func loadArticles() async {
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        articles = ["SwiftUI", "Concurrency", "Combine"].map { Article(title: $0) }
    }
}

这为你带来了:

  • 安全的初始加载
  • 轻松实现下拉刷新(pull-to-refresh)
  • 自动任务取消
  • 简洁的、声明式的语法
  • 无需过度思考

10. 经验法则

这是最简单的记忆方法:

如果你的函数包含 await,那么它就不属于 .onAppear()

就是这样。

  • .onAppear() = 用于轻量级、同步的 UI 触发器。
  • .task { } = 用于异步的、可取消的、并绑定到视图生命周期的工作。

11. 针对旧版 iOS 怎么办?

如果你需要支持 iOS 14 或更早的版本.task { } 是不可用的

在这种情况下,你仍然可以使 .onAppear() 变得安全——只需手动添加取消逻辑

示例:

struct LegacyView: View {
    @State private var task: Task<Void, Never>?
    var body: some View {
        VStack {
            Text("Legacy Async Work")
        }
        .onAppear {
            task = Task {
                await loadData()
            }
        }
        .onDisappear {
            task?.cancel()
        }
    }
    func loadData() async {
        try? await Task.sleep(nanoseconds: 2_000_000_000)
    }
}

不像之前那么优雅,但它能让你的异步工作保持在控制之下


12. 吸取的教训

这段经历教会了我这些:

  • SwiftUI 视图是短暂的(ephemeral)——把它们视为快照,而不是屏幕。
  • .onAppear() 可以(而且将会)多次触发——不要依赖它进行一次性的设置。
  • 异步工作需要是可取消的(cancelable)——.task { } 免费为你提供了这一点。
  • 除非你确切知道何时会触发,否则不要在视图结构体内部放置副作用(side effects)
  • 如果你看到随机的重载或闪烁,首先检查你的 .onAppear() 调用

13. 我的最终看法

如果你的 SwiftUI App 随机重新加载数据, 如果你的 API 调用触发了两次, 如果你的加载指示器无故闪烁—— 不要想太多。 检查你的 .onAppear()

在大多数情况下,用 .task { } 替换它会立即修复 90% 的这些问题

SwiftUI 提供了正确的工具;你只需要将它们用于其预期的目的

因为 .onAppear() 并没有坏——它只是不适合承担异步逻辑的重担

结语(Final Thoughts)

我曾经以为 .onAppear() 是无害的。 直到它悄无声息地让我的 SwiftUI App 看起来不稳定且不可预测

一旦我用 .task 替换了它,一切都豁然开朗——无论是字面上还是象征意义上。 UI 停止了闪烁。 API 停止了过度触发。 我的异步代码第一次感觉真正属于 SwiftUI 的世界。

所以,如果你正在与随机重载、奇怪的时序问题或无形的后台任务作斗争——不用再找了。 你很可能用错了 .onAppear()

基于 Metal 的 iOS 全景视频播放器

作者 linghugoogle
2025年10月26日 23:09

项目简介

PNPlayer 是一个基于 Metal 框架开发的 iOS 全景视频播放器,支持 360° 全景视频播放和直观的手势控制。与传统的视频播放器不同,PNPlayer 能够让用户通过手势自由旋转视角,仿佛置身于视频场景之中,带来极具沉浸感的观看体验。

项目地址:github.com/linghugoogl…,欢迎 Star 和 Fork!

核心功能特性

PNPlayer 具备以下关键功能:

  • 🎥 高质量全景视频播放,支持 360° 全视角浏览
  • 🎮 流畅的手势控制,通过拖拽实现视角旋转
  • ⏯️ 完整的播放控制界面,包括播放 / 暂停、进度调节
  • 🔊 音频同步播放,提供完整的音视频体验
  • 🔄 实时渲染优化,确保流畅的播放体验

技术实现亮点

PNPlayer 采用了先进的技术栈和架构设计,值得开发者关注:

1. 底层技术栈

  • 渲染引擎:采用 Metal + Metal Shading Language,充分利用 iOS 设备的 GPU 性能
  • 视频处理:基于 AVFoundation + AVPlayer,实现高效的视频解码和播放控制

2. 视频播放流程

PNPlayer 设计了高效的视频数据处理流水线:

视频文件 → AVPlayerAVPlayerItemVideoOutput → CVPixelBuffer → MTLTexture

核心处理由 VideoTextureManager 负责,通过 AVPlayerItemVideoOutput 提取视频帧数据,再通过 CVMetalTextureCache 将像素缓冲区转换为 Metal 纹理,最终交由 GPU 渲染。

3. 全景渲染机制

全景渲染的核心在于将平面视频映射到球面几何体:

球面几何体 → 顶点着色器 → 纹理映射 → 片段着色器 → 屏幕输出
  • SphereGeometry 生成高精度球面网格(包含顶点和 UV 坐标)
  • 顶点着色器应用 MVP 矩阵变换,实现 3D 空间定位
  • 视频纹理通过 UV 坐标精确映射到球面
  • 相机控制器处理用户手势,实时更新视角旋转

4. 着色器核心代码

Metal 着色器是实现全景渲染的关键:

// 顶点着色器:3D 坐标变换
vertex VertexOut vertex_main(VertexIn in [[stage_in]],
                            constant Uniforms& uniforms [[buffer(1)]]) {
    out.position = uniforms.modelViewProjectionMatrix * float4(in.position, 1.0);
    out.texCoord = in.texCoord;
    return out;
}

// 片段着色器:纹理采样
fragment float4 fragment_main(VertexOut in [[stage_in]],
                             texture2d<float> colorTexture [[texture(0)]],
                             sampler colorSampler [[sampler(0)]]) {
    return colorTexture.sample(colorSampler, in.texCoord);
}
昨天以前掘金 iOS

Swift 下标(Subscripts)详解:从基础到进阶的完整指南

作者 unravel2025
2025年10月26日 23:18

什么是下标

官方一句话:“类、结构体、枚举可以用下标(subscripts)快速访问集合、列表、序列中的元素,而无需再写专门的存取方法。”

换句话说:someArray[index]someDictionary[key] 这种“中括号”语法糖,就是下标。

你自己写的类型也能拥有这种“中括号”魔法。

下标语法 101

subscript(index: Int) -> ReturnType {
    get {
        // 返回与 index 对应的值
    }
    set(newValue) {   // 可省略 (newValue)
        // 用 newValue 保存
    }
}

要点速记:

  1. 用关键字 subscript 开头,不是 func
  2. 参数列表可以是任意个数、任意类型。
  3. 返回值也可以是任意类型。
  4. 可以是只读(省略 set),也可以是读写。
  5. 不支持 inout 参数。

只读下标:最简单的入门示例

需求:做一个“n 乘法表”结构体,通过 table[6] 直接得到 n * 6

struct TimesTable {
    let multiplier: Int
    
    // 只读下标:省略了 get 关键字
    subscript(index: Int) -> Int {
        multiplier * index
    }
}

let threeTimesTable = TimesTable(multiplier: 3)
print(threeTimesTable[6])   // 输出 18

注意:

  • 没有 set,外界只能读不能写。
  • 如果写成 threeTimesTable[6] = 100 会直接编译报错。

可读写下标:让下标也能“赋值”

需求:自己封装一个“固定长度”的数组,禁止越界。

struct SafeArray<Element> {
    private var storage: [Element]
    
    init(repeating: Element, count: Int) {
        storage = Array(repeating: repeating, count: count)
    }
    
    // 可读可写下标
    subscript(index: Int) -> Element {
        get {
            // 越界直接崩溃,提前暴露问题
            precondition(index >= 0 && index < storage.count,
                         "Index \(index) out of range 0..<\(storage.count)")
            return storage[index]
        }
        set {
            precondition(index >= 0 && index < storage.count,
                         "Index \(index) out of range 0..<\(storage.count)")
            storage[index] = newValue
        }
    }
}

var sa = SafeArray(repeating: 0, count: 5)
sa[2] = 10
print(sa[2])   // 10
// sa[7] = 1   // 运行时触发 precondition 失败

多参数、多维度:二维矩阵实战

struct Matrix {
    let rows: Int, cols: Int
    private var grid: [Double]
    
    init(rows: Int, cols: Int) {
        self.rows = rows
        self.cols = cols
        grid = Array(repeating: 0.0, count: rows * cols)
    }
    
    // 二维下标
    subscript(row: Int, col: Int) -> Double {
        get {
            precondition(indexIsValid(row: row, col: col))
            return grid[row * cols + col]
        }
        set {
            precondition(indexIsValid(row: row, col: col))
            grid[row * cols + col] = newValue
        }
    }
    
    func indexIsValid(row: Int, col: Int) -> Bool {
        row >= 0 && row < rows && col >= 0 && col < cols
    }
    
    // 调试打印:方便看扁平化结果
    func debug() {
        print("grid = \(grid)")
    }
}

var m = Matrix(rows: 2, cols: 2)
m[0, 1] = 1.5
m[1, 0] = 3.2
m.debug()          // grid = [0.0, 1.5, 3.2, 0.0]

下标重载:一个类型多个“中括号”

下标也能像函数一样“重载”:参数类型或数量不同即可。

struct MultiSub {
    // 1. 通过 Int 索引
    subscript(i: Int) -> String {
        "Int 下标:\(i)"
    }
    
    // 2. 通过 String 索引
    subscript(s: String) -> String {
        "String 下标:\(s)"
    }
    
    // 3. 两个参数
    subscript(x: Int, y: Int) -> String {
        "二维:(\(x), \(y))"
    }
}

let box = MultiSub()
print(box[5])          // Int 下标:5
print(box["hello"])    // String 下标:hello
print(box[2, 3])       // 二维:(2, 3)

类型下标(static / class):不依赖实例也能用[]

实例下标必须“先有一个对象”;类型下标直接挂在类型上,用法类似 Type[key]

enum AppTheme {
    case light, dark
    
    // 类型下标:根据字符串返回颜色
    static subscript(name: String) -> UInt32? {
        switch name {
        case "background":
            return 0xFFFFFF
        case "text":
            return 0x000000
        default:
            return nil
        }
    }
}

// 无需实例
let bgColor = AppTheme["background"]   // 0xFFFFFF
  • 结构体/枚举用 static subscript
  • 类如果想让子类可覆写,用 class subscript

常见陷阱与调试技巧

  1. 越界问题

    Matrixprecondition 在运行时断言,开发阶段建议全开,生产环境可换成 guard + 抛出错误。

  2. 性能陷阱

    下标语法糖容易隐藏复杂逻辑。若 get 里做大量计算,会让“一行代码”拖慢整体。必要时加缓存或改用方法。

  3. 与函数歧义

    下标不支持 inout,也不能 throws。若需要这些能力,请定义成方法。

  4. 可读性

    滥用多参数下标会降低可读性。建议保持“语义直观”,例如 matrix[row, col] 很直观,但 foo[a, b, c, d] 就要谨慎。

总结

核心 3 句话:

  1. 下标 = “中括号”语法糖,语法像计算属性。
  2. 参数、返回值随便定,可重载,不支持 inout。
  3. 实例下标最常用,类型下标适合“全局字典”式场景。

扩展场景:自定义 JSON、缓存、稀疏矩阵

(1)“SwiftyJSON”式下标

struct JSON {
    private var raw: Any
    
    init(_ raw: Any) { self.raw = raw }
    
    // 允许 json["user"]["name"] 一路点下去
    subscript(key: String) -> JSON {
        guard let dict = raw as? [String: Any],
              let value = dict[key] else { return JSON(NSNull()) }
        return JSON(value)
    }
    
    // 支持数组
    subscript(index: Int) -> JSON {
        guard let arr = raw as? [Any], index < arr.count else { return JSON(NSNull()) }
        return JSON(arr[index])
    }
    
    var stringValue: String? { raw as? String }
}

let json = JSON(["user": ["name": "Alice"]])
if let name = json["user"]["name"].stringValue {
    print(name)   // Alice
}

(2)LRU 缓存下标

final class Cache<Key: Hashable, Value> {
    private var lru = NSCache<NSString, AnyObject>()
    
    subscript(key: Key) -> Value? {
        get { lru.object(forKey: "\(key)" as NSString) as? Value }
        set {
            if let v = newValue { lru.setObject(v as AnyObject, forKey: "\(key)" as NSString) }
            else { lru.removeObject(forKey: "\(key)" as NSString) }
        }
    }
}

(3)稀疏矩阵

当 99% 都是 0 时,可用字典存“非零下标”:

struct SparseMatrix {
    private var storage: [String: Double] = [:]
    
    subscript(row: Int, col: Int) -> Double {
        get { storage["\(row),\(col)"] ?? 0.0 }
        set {
            if newValue == 0.0 {
                storage.removeValue(forKey: "\(row),\(col)")
            } else {
                storage["\(row),\(col)"] = newValue
            }
        }
    }
}

下标 × 属性包装器:让“语法糖”再甜一点

从 Swift 5.1 开始,Property Wrapper 把“存储逻辑”抽成了可复用的注解;

把“下标”与“属性包装器”放在一起,可以做出带访问钩子的数组/字典,而调用端仍然只用一对中括号。

场景:线程安全数组

需求:

  • 对外像普通数组一样用 [] 读写;
  • 内部用 DispatchQueue 做同步锁;
  • 编译期自动注入,无需手写 queue.async {}

实现思路:

  1. @propertyWrapper 做一个泛型 ThreadSafeBox
  2. wrappedValue 里用 queue.sync { ... } 实现读写;
  3. 再给它一个下标投影(projectedValue 返回自身),让外部能用 $ 语法拿到“带下标的实例”。

代码:

import Foundation

@propertyWrapper
class ThreadSafeArray<Element> {
    private var storage: [Element] = []
    private let queue = DispatchQueue(label: "sync.array", attributes: .concurrent)
    
    init(wrappedValue: [Element]) {
        self.wrappedValue = wrappedValue
    }
    
    var wrappedValue: [Element] {
        get { queue.sync { storage } }
        set { queue.async(flags: .barrier) { self.storage = newValue } }
    }
    
    // 对外暴露“自身”做下标
    var projectedValue: ThreadSafeArray { self }
    
    // 下标:读,写
    subscript(index: Int) -> Element {
        get { queue.sync { storage[index] } }
        set { queue.async(flags: .barrier) { self.storage[index] = newValue } }
    }
    
    // 方便扩展 append、count 等
    func append(_ element: Element) {
        queue.async(flags: .barrier) { self.storage.append(element) }
    }
    
    var count: Int {
        queue.sync { storage.count }
    }
}

/* ============ 使用端 ============== */
class ViewModel {
    // 看似普通数组,实则线程安全
    @ThreadSafeArray var numbers: [Int] = []
    
    func demo() {
        // 1. 直接读写(走 wrappedValue)
        numbers = [1, 2, 3]
        
        // 2. 用下标(走 projectedValue)
        $numbers[0] = 99
        print($numbers[0])   // 99
        
        // 3. 并发安全
        DispatchQueue.concurrentPerform(iterations: 1000) { i in
            $numbers.append(i)
        }
        print("final count:", $numbers.count) // 1003
    }
}
let vm = ViewModel()
vm.demo()

关键点:

  • projectedValue 返回 self,于是 $numbers 就能继续用 []
  • 读写分离:读用 sync,写用 async(flags: .barrier),多读单写模型。
  • 对调用者完全透明,看起来像普通数组,却自带锁。

场景:@UserDefault 下标版

系统自带的 UserDefaults 标准写法:

UserDefaults.standard.set(true, forKey: "darkMode")

我们可以把“键”做成下标,并且用属性包装器自动同步:

@propertyWrapper
class UserDefault<T> {
    let key: String
    let defaultValue: T
    
    init(key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }
    
    var wrappedValue: T {
        get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }
    
    // 允许 $ 语法直接取下标
    var projectedValue: UserDefault { self }
    
    // 下标:支持动态 key
    subscript(_ suffix: String) -> T {
        get {
            let fullKey = "\(key)_\(suffix)"
            return UserDefaults.standard.object(forKey: fullKey) as? T ?? defaultValue
        }
        set {
            let fullKey = "\(key)_\(suffix)"
            UserDefaults.standard.set(newValue, forKey: fullKey)
        }
    }
}

/* ============ 使用端 ============== */
@MainActor
struct Settings {
    @UserDefault(key: "darkMode", defaultValue: false)
    static var darkMode: Bool
    
    // 投影后支持 $darkMode["iPhone"] 这种动态 key
    static func demo() {
        darkMode = true
        print(darkMode)                    // true
        
        $darkMode["iPad"] = false
        print($darkMode["iPad"])           // false
    }
}

Settings.demo()

下标与 Result Builder 的化学反应

SwiftUI 的 ViewBuilder 让大家见识到“DSL”之美;

其实我们自己也能用 Result Builder + 下标 做出“声明式语法”。

示例:做一个极简版 JSON DSL,支持这种写法:

let obj = JSON {
    "user" {
        "name" <<< "Alice"
        "age"  <<< 25
    }
}

实现要点:

  1. @resultBuilderJSONBuilder
  2. <<< 运算符把 key ~ value 塞进字典;
  3. JSON 支持 subscript(key: String) -> JSONNode 实现嵌套。
@resultBuilder
enum JSONBuilder {
    static func buildBlock(_ components: JSONNode...) -> JSONNode {
        JSONNode(children: components)
    }
}

struct JSONNode {
    enum Value {
        case string(String)
        case number(Double)
        case object([String: JSONNode])
    }
    var value: Value?
    var children: [JSONNode] = []
}

struct JSON {
    private var root: JSONNode = JSONNode()
    
    init(@JSONBuilder content: () -> JSONNode) {
        root = content()
    }
    
    // 关键下标:支持链式嵌套
    subscript(key: String) -> JSONNode {
        get {
            guard case .object(let dict) = root.value,
                  let node = dict[key] else { return JSONNode() }
            return node
        }
        set {
            if case .object(var dict) = root.value {
                dict[key] = newValue
                root.value = .object(dict)
            } else {
                root.value = .object([key: newValue])
            }
        }
    }
}

infix operator <<< : AssignmentPrecedence
func <<< (lhs: JSONNode, rhs: Any) {
    // 简化版:把 rhs 转成 JSONNode 并赋值
}

性能深度剖析:下标真的“零成本”吗?

  1. 纯“转发”下标(直接读数组)

    • 经编译器优化后,与手动裸数组访问无差别;
    • -O -whole-module-optimization 下会内联。
  2. preconditionsync 锁的下标

    • 运行时多了一次函数调用 + 条件判断;

    • 若放在热点循环,建议:

      a. 把“裸指针”提取到循环外;

      b. 或者用 withUnsafeBufferPointer 一次性处理。

  3. 多参数下标

    • 调用约定与多参函数相同,不会额外装箱;
    • 但泛型参数过多时可能触发 specialization 爆炸,注意模块划分。

Flutter插件与包的本质差异

作者 CodingFisher
2025年10月26日 07:44

原文:xuanhu.info/projects/it…

Flutter插件与包的本质差异

1 核心概念定义

1.1 Flutter包(Package)

  • 纯Dart实现:仅包含Dart语言编写的逻辑代码
  • 跨平台特性:不依赖任何原生平台(Android/iOS)API
  • 典型用例
    // 日期格式化工具包示例
    class DateFormatter {
      static String formatDateTime(DateTime dt) {
        return '${dt.year}-${dt.month}-${dt.day}'; 
      }
    }
    

1.2 Flutter插件(Plugin)

  • 混合架构:包含Dart接口 + 平台特定实现
  • 平台通道:通过MethodChannel进行通信
    // Dart端调用原生摄像头
    final cameraPlugin = CameraPlugin();
    final imagePath = await cameraPlugin.takePicture(); 
    
  • 原生层实现
    // Android端Java实现
    public class CameraPlugin implements MethodCallHandler {
      @Override
      public void onMethodCall(MethodCall call, Result result) {
        if (call.method.equals("takePicture")) {
          dispatchTakePictureIntent(result); // 启动相机
        }
      }
    }
    

2 底层通信机制剖析

2.1 平台通道(Platform Channel)工作原理

sequenceDiagram
    Flutter->>Native: 调用方法 (MethodCall)
    Native->>Native: 执行原生操作
    Native->>Flutter: 返回结果 (Result)

2.2 数据类型映射表

Dart类型 Android类型 iOS类型
int java.lang.Integer NSNumber
double java.lang.Double NSNumber
String java.lang.String NSString
Uint8List byte[] FlutterStandardTypedData

3 实战场景对比

3.1 何时使用纯包

  • UI组件库:如pub.dev/packages/fl…
  • 业务逻辑封装:JWT令牌解析工具
  • 状态管理:Riverpod状态管理库
// 纯Dart包实现状态管理
final authProvider = Provider<User>((ref) {
  return User.fromToken(JWTParser.parse(token));
});

3.2 何时必须用插件

// 蓝牙插件使用示例
FlutterBlue flutterBlue = FlutterBlue.instance;
flutterBlue.scan().listen((scanResult) {
  print('发现设备: ${scanResult.device.name}');
});

4 混合开发进阶技巧

4.1 FFI(外部函数接口)替代方案

import 'dart:ffi';

typedef NativeAddFunc = Int32 Function(Int32, Int32);

final dylib = DynamicLibrary.open('libmath.dylib');
final add = dylib.lookupFunction<NativeAddFunc, NativeAddFunc>('add');

void main() {
  print('3+5=${add(3, 5)}'); // 直接调用C函数
}

4.2 插件性能优化策略

  • 批处理调用:减少平台通道通信次数
  • 二进制传输:使用ByteData代替Base64
  • 后台线程:耗时操作脱离UI线程
// 图像处理优化示例
final Isolate isolate = await Isolate.spawn(_processImage, imageData);

static void _processImage(Uint8List data) {
  // 在独立isolate中处理
  final result = applyFilters(data); 
  Isolate.exit(result);
}

5 企业级项目架构

5.1 分层架构设计

lib/
├── domain/       # 业务逻辑层(纯Dart包)
├── infrastructure/ # 基础设施层
│   ├── api_client.dart # 网络请求(纯Dart)
│   └── sensors.dart    # 传感器(插件)
└── presentation/ # UI层

5.2 联邦插件(Federated Plugins)

graph TD
    A[接口包] --> B[Android实现包]
    A --> C[iOS实现包]
    A --> D[Web实现包]

6 版本兼容性管理

6.1 多平台支持矩阵

插件版本 Flutter SDK Android API iOS版本
1.x >=2.0 21+ 11+
2.x >=3.3 24+ 13+

6.2 依赖冲突解决方案

dependency_overrides:
  plugin_core: 1.2.3 # 强制统一核心版本

总结

::: tabs#platform

@tab 核心差异

  • :纯Dart逻辑复用,适用于UI组件/工具类
  • 插件:平台桥接器,用于硬件/系统服务访问

@tab 选型决策树

flowchart TD
    A[需要访问硬件/系统API?] -->|是| B[使用插件]
    A -->|否| C[开发纯Dart包]

@tab 未来演进

  • WebAssembly支持:Dart与Rust的FFI深度整合
  • 宏编程:Dart 3元编程简化插件开发
  • 统一渲染引擎:减少平台特定代码需求

::: tip 最佳实践建议
1. **优先纯包架构**:90%业务逻辑应保持平台无关
2. **插件轻量化**:原生层仅做必要代理
3. **性能监控**:使用`devtools`检查通道调用耗时
4. **联邦插件策略**:支持多平台渐进增强
:::

```dart
// 健康检查工具(同时使用包和插件)
void checkHealth() {
  final memory = SystemInfo.getMemoryUsage(); // 插件访问系统API
  final status = HealthAnalyzer.analyze(memory); // 纯Dart分析逻辑
  print('系统健康度: ${status.level}');
}

结论

技术选型

考量维度 包(Package) 插件(Plugin)
开发成本 ★★☆☆☆ ★★★★☆
跨平台一致性 ★★★★★ ★★★☆☆
系统能力访问
热重载支持 部分支持
空安全支持 100% 依赖原生实现

演进趋势预测

  1. 插件包融合:Dart FFI技术将缩小两者差异
  2. Wasm跨平台:WebAssembly可能替代部分原生插件
  3. AI代码生成:GPT工程自动生成平台通道代码

原文:xuanhu.info/projects/it…

Swift 中基础概念:「函数」与「方法」

作者 unravel2025
2025年10月25日 11:28

为什么要区分「函数」和「方法」

写 Swift 时,我们每天都在写 func

但同一个关键字,有时叫「函数」,有时又叫「方法」。

名字不同,背后其实是作用域与归属权的差异:

  • 函数(function)——独立存在,像一把瑞士军刀,谁都能拿来用。
  • 方法(method)——寄居在类型内部,能直接访问类型的数据,像家电的遥控器,只能操控指定品牌。

搞清这一点,再读苹果文档或第三方库源码,就不会迷糊。

函数:真正独立的代码块

  1. 定义与调用
// 函数定义:全局可用,不依赖任何类型
func greet(name: String) -> String {
    return "Hello, \(name)!"
}

// 调用
let msg = greet(name: "Alice")
print(msg)   // 输出:Hello, Alice!
  1. 生活化比喻

把函数想成微波炉:你把它搬到任何厨房(项目),它都能加热食物(完成任务)。

微波炉不隶属于某套房子(对象),完全独立。

  1. 再看一个无参无副作用的工具函数
/// 返回当前时间戳(秒)
func currentTimestamp() -> TimeInterval {
    return Date().timeIntervalSince1970
}

这种「纯工具」逻辑,做成函数最合适,因为任何类型都可能用得到。

方法:绑定在类型上的函数

方法 = 函数 + 上下文。

上下文就是类型(class/struct/enum)里的属性与其他方法。

  1. 实例方法(Instance Method)
struct Car {
    var speed: Int      // 属性

    // 实例方法:只能由具体 Car 实例调用
    func describe() -> String {
        return "The car is going \(speed) km/h."
    }
}

let myCar = Car(speed: 100)
print(myCar.describe())   // 输出:The car is going 100 km/h.

要点:

  • describe 写在 Car 内部,直接读取 speed
  • 如果脱离 Car 实例,describe 无法存在。
  1. 变异方法(Mutating Method)

值类型(struct/enum)默认不可修改自身属性,需要显式标记 mutating

struct Counter {
    var count = 0

    // 变异方法:可以修改属性
    mutating func increment() {
        count += 1
    }
}

var counter = Counter()
counter.increment()
print(counter.count)   // 输出:1

注意:

  1. 只有 值类型 才用 mutating;class 是引用类型,直接改即可。

  2. 调用者必须用 var 声明,不能用 let

  3. 类型方法(Type Method)

相当于其他语言的「静态方法」,由类型本身调用,不依赖实例。

class Car {
    // 类型属性
    static let defaultWheelCount = 4
    
    // 类型方法
    static func numberOfWheels() -> Int {
        return defaultWheelCount
    }
}

print(Car.numberOfWheels())   // 输出:4

使用场景:

  • 工厂方法(创建实例)
  • 与实例无关的全局配置或工具逻辑

函数与方法协同实战:跑步记录器

下面把「函数」与「方法」放在同一段业务里,感受它们如何各司其职。

// 1️⃣ 独立函数:公里转英里,任何模块都能用
func convertToMiles(kilometers: Double) -> Double {
    return kilometers * 0.621371
}

// 2️⃣ 类型:Runner
struct Runner {
    var name: String
    var distanceInKm: Double
    
    // 实例方法:生成专属报告
    func progressReport() -> String {
        // 直接调用上面的独立函数
        let miles = convertToMiles(kilometers: distanceInKm)
        return "\(name) 今天跑了 \(String(format: "%.2f", miles)) 英里"
    }
    
    // 变异方法:增加跑量
    mutating func runExtra(km: Double) {
        distanceInKm += km
    }
}

// 3️⃣ 使用
var emma = Runner(name: "Emma", distanceInKm: 5)
print(emma.progressReport())   // Emma 今天跑了 3.11 英里

emma.runExtra(km: 2)
print(emma.progressReport())   // Emma 今天跑了 4.35 英里

看到吗?

  • convertToMiles 是纯逻辑,与任何类型无关。
  • progressReport 需要 Runner 内部数据,所以做成实例方法。
  • 两者组合,代码既复用又内聚。

易错点小结

场景 正确做法 常见错误
struct 里想改属性 mutating 忘记写,编译报错
类型方法里用实例属性 ❌ 不能 static 当实例方法用
全局工具逻辑 写成函数 强行塞进某个类型,导致复用困难

理解 & 使用原则

  1. 优先写成函数:只要逻辑不依赖任何实例数据,就独立出来。

    方便单元测试、跨模块复用,也减少类型体积。

  2. 升阶为方法的时机:

    • 需要频繁访问类型内部私有属性
    • 需要多态(子类重写)
    • 需要链式调用(return self

    满足一条,再考虑搬进类型。

  3. 命名统一,作用域清晰:

    函数用「动词」开头,convertdownloadvalidate

    方法也用「动词」,但可省略主语,describeincrementsave

    让调用者一眼看出谁属于谁。

扩展场景:把知识用到实际项目

  1. 网络层
   // 独立函数,任何模型都能调
   func GET<T: Decodable>(_ url: URL) async throws -> T { ... }

   // 模型内部方法,自己知道怎么拼装 URL
   struct Article {
       let id: Int
       func detailURL() -> URL { ... }
   }
  1. SwiftUI 视图
   // 全局函数,做颜色插值
   func interpolateColor(from: Color, to: Color, percent: Double) -> Color { ... }

   // 视图的方法,负责自己的业务
   struct ProgressCircle: View {
       var progress: Double
       private func strokeColor() -> Color {
           return interpolateColor(from: .green, to: .red, percent: progress)
       }
   }
  1. 算法库

    快速排序、哈希函数等纯算法,一律写成函数,避免绑定具体类型。

结语

所有方法都是函数,但函数不一定是方法。

记住「是否依赖类型数据」这一把尺子,该独立就独立,该内聚就内聚。

当你把边界划清楚,代码会自然呈现出高内聚、低耦合的漂亮形态。

希望这篇笔记能帮你把「函数 vs 方法」彻底吃透,写出更 Swifty 的代码!

参考资料

Swift 官方语言指南 - Functions

Swift 官方语言指南 - Methods

iOS进阶1-combine

2025年10月24日 20:50

iOS 进阶:深入浅出 Swift Combine 框架

本文深入探讨 Apple 的响应式编程框架 Combine。通过核心概念解析、丰富代码示例和实战场景,系统介绍如何利用 Combine 优雅地处理异步事件和数据流,提升代码的可读性、可维护性。

一、Combine 框架概述

Combine 是 Apple 在 2019 年 WWDC 上推出的一个声明式响应式编程框架,专为 Swift 语言设计。它的核心思想是处理随时间变化的值或异步事件。想象一下诸如网络请求响应、用户界面输入、定时器事件等场景,这些都可以被建模为事件流,而 Combine 提供了一套统一的 API 来组合和转换这些流 。

在 SwiftUI 中,Combine 是数据驱动 UI 的基石(如 @Published 和 ObservableObject)。但它同样强大地适用于 UIKit 开发,用于简化复杂的异步操作和状态管理 。

核心概念三要素

Combine 的运作主要围绕三个核心角色,它们共同构成一条清晰的数据流水线:

核心概念 角色描述 现实世界类比
Publisher(发布者) 事件的源头,负责产出值。 报社
Subscriber(订阅者) 事件的终点,负责接收并消费值。 订户
Operator(操作符) 数据的处理站,位于 Publisher 和 Subscriber 之间,负责转换、过滤、组合值。 报社的编辑部门(校对、排版、内容整合)

一条完整的 Combine 链条工作流程如下:Publisher -> (零个或多个 Operator) -> Subscriber 

。接下来,我们详细看看每个组成部分。

二、核心组件详解

2.1 Publisher(发布者)

Publisher 是一个协议,它定义了一个能够发出一系列元素(Values)、完成事件(Completion)或错误(Failure)的类型。它有两种终止状态:正常完成(.finished)失败(.failure)。在收到终止事件后,数据流即结束 。

常用的内置 Publisher 包括:

  • **Just**: 发送一个值后立即完成。

    swift
    复制
    let publisher = Just("Hello, Combine!") // 输出类型是 String, 错误类型是 Never
    
  • **Fail**: 立即发送一个错误并终止。

  • **PassthroughSubject**: 一个可以手动发送值的 Subject(主题),不保留当前值。

  • **CurrentValueSubject**: 一个可以手动发送值的 Subject,并保存当前值的状态 。

  • **@Published**: 属性包装器,能将一个属性转换为 Publisher 。

2.2 Subscriber(订阅者)

Subscriber 是数据的消费者。Publisher 在有了 Subscriber 之后才会开始发送数据。Combine 提供了两种常用的内置 Subscriber:

  1. **sink**
    最通用的订阅者,它接收两个闭包:一个处理接收到的值(receiveValue),另一个处理完成事件(receiveCompletion)。

    swift
    复制
    [1, 2, 3, 4, 5].publisher // Publisher 发布数据
        .sink(
            receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("数据流正常结束")
                case .failure(let error):
                    print("数据流因错误结束: (error)")
                }
            },
            receiveValue: { value in
                print("接收到值: (value)") // 依次打印 1, 2, 3, 4, 5
            }
        )
    
  2. **assign**
    将接收到的值直接绑定到某个对象的某个属性上,用于更新 UI 非常方便。

    swift
    复制
    class MyViewController: UIViewController {
        @IBOutlet weak var nameLabel: UILabel!
        var cancellables = Set<AnyCancellable>()
        
        func viewDidLoad() {
            super.viewDidLoad()
            // $name 是一个 Publisher<String, Never>
            viewModel.$name
                .assign(to: .text, on: nameLabel) // 将值直接赋给 label 的 text 属性
                .store(in: &cancellables)
        }
    }
    

2.3 Operator(操作符)

操作符是 Combine 强大功能的体现,它们是 Publisher 协议上定义的方法,每个操作符都会返回一个新的 Publisher,从而允许进行链式调用。以下是一些常用类别:

操作符类别 代表操作符 功能说明 示例
转换 map 将接收到的值转换为另一种形式。 .map { $0.count } (将字符串流转换为整数流)
过滤 filter 只允许满足条件的值通过。 .filter { $0 > 10 } (只保留大于10的值)
错误处理 catch 捕获错误,并返回一个备用的 Publisher。 .catch { _ in return Just("Default Value") }
组合 combineLatest 组合多个 Publisher,当任何一个有新值时,发送所有 Publisher 最新值的元组。 用于表单验证,同时监听用户名和密码输入框。
时间控制 debounce 防抖,例如用于搜索框,在用户停止输入一段时间后才发送请求。 .debounce(for: .seconds(0.5), scheduler: RunLoop.main)

链式调用示例

swift
复制
// 一个综合使用操作符的示例
let cancellable = [1, 2, 3, 4, 5].publisher
    .filter { $0 % 2 == 0 } // 过滤偶数:2, 4
    .map { $0 * $0 }        // 转换平方:4, 16
    .sink { value in
        print(value) // 最终输出:4, 16
    }

三、实战应用场景

3.1 网络请求

Combine 能极大地简化网络请求的处理。URLSession 直接提供了 dataTaskPublisher 用于网络调用。

swift
复制
import Combine

struct User: Decodable {
    let name: String
}

func fetchUser(userId: String) -> AnyPublisher<User, Error> {
    guard let url = URL(string: "https://api.example.com/users/(userId)") else {
        return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
    }
    
    return URLSession.shared.dataTaskPublisher(for: url) // 1. 创建网络请求 Publisher
        .map(.data)                                   // 2. 提取数据部分
        .decode(type: User.self, decoder: JSONDecoder()) // 3. 解码 JSON
        .receive(on: DispatchQueue.main)              // 4. 切换到主线程更新 UI
        .eraseToAnyPublisher()                         // 5. 类型擦除,方便返回
}

// 使用
var cancellables = Set<AnyCancellable>()
fetchUser(userId: "123")
    .sink(receiveCompletion: { completion in
        if case .failure(let error) = completion {
            print("请求失败: (error)")
        }
    }, receiveValue: { user in
        print("获取到用户: (user.name)")
        // 在这里更新 UI
    })
    .store(in: &cancellables)

3.2 处理用户输入(UIKit)

使用 Combine 可以轻松响应 UIKit 控件的各种事件。

swift
复制
import Combine
import UIKit

class SearchViewController: UIViewController {
    @IBOutlet weak var searchTextField: UITextField!
    @IBOutlet weak var resultsLabel: UILabel!
    
    var viewModel = SearchViewModel()
    var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bindTextField()
    }
    
    private func bindTextField() {
        // 1. 创建文本变化的 Publisher
        let textPublisher = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: searchTextField)
            .compactMap { ($0.object as? UITextField)?.text } // 确保有文本
            .eraseToAnyPublisher()
        
        // 2. 将 Publisher 绑定到 ViewModel
        viewModel.performSearch(with: textPublisher)
        
        // 3. 订阅 ViewModel 的结果来更新 UI
        viewModel.$searchResults
            .receive(on: DispatchQueue.main)
            .assign(to: .text, on: resultsLabel)
            .store(in: &cancellables)
    }
}

class SearchViewModel: ObservableObject {
    @Published var searchResults: String = ""
    private var cancellables = Set<AnyCancellable>()
    
    func performSearch(with queryPublisher: AnyPublisher<String, Never>) {
        queryPublisher
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main) // 防抖,避免频繁请求
            .removeDuplicates() // 去除连续重复的输入
            .flatMap { query -> AnyPublisher<String, Never> in
                // 模拟网络搜索,例如返回 "Results for '(query)'"
                return Just("Results for '(query)'").eraseToAnyPublisher()
            }
            .assign(to: .searchResults, on: self)
            .store(in: &cancellables)
    }
}

3.3 状态管理与 SwiftUI 集成

在 SwiftUI 中,Combine 是无缝集成的。ObservableObject 协议和 @Published 属性包装器是核心。

swift
复制
import SwiftUI
import Combine

class CounterViewModel: ObservableObject {
    @Published var count: Int = 0 // 使用 @Published 标记,当其变化时会通知视图更新
    @Published var isEven: Bool = false
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 监听 count 的变化,自动推导 isEven 的状态
        $count
            .map { $0 % 2 == 0 }
            .assign(to: .isEven, on: self)
            .store(in: &cancellables)
    }
    
    func increment() {
        count += 1
    }
    
    func decrement() {
        count -= 1
    }
}

struct CounterView: View {
    @StateObject var viewModel = CounterViewModel() // StateObject 监听 ObservableObject 的变化
    
    var body: some View {
        VStack {
            Text("Count: (viewModel.count)")
            Text(viewModel.isEven ? "Even" : "Odd")
            Button("+1") {
                viewModel.increment()
            }
            Button("-1") {
                viewModel.decrement()
            }
        }
    }
}

四、内存管理与最佳实践

4.1 内存管理:AnyCancellable

当您调用 sink 或 assign 时,返回值是一个 AnyCancellable 实例。你必须强引用这个对象,否则订阅会立即被取消,数据流也会中断 。

标准做法是使用一个集合(通常是 Set<AnyCancellable>)来存储所有订阅。

swift
复制
class MyViewController: UIViewController {
    private var cancellables = Set<AnyCancellable>() // 存储订阅的集合
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        SomePublisher()
            .sink { ... }
            .store(in: &cancellables) // 关键:将订阅存储到集合中
    }
}

4.2 最佳实践建议

  1. 线程切换:使用 receive(on:) 操作符确保 UI 更新在主线程进行(DispatchQueue.main)。

  2. 错误处理:合理使用 catchretry 等操作符来优雅地处理可能发生的错误。

  3. 避免循环引用:在 sink 的闭包内使用 [weak self] 来避免循环引用,特别是在将值赋给 self 的属性时。

    swift
    复制
    .sink { [weak self] value in
        self?.updateUI(with: value)
    }
    
  4. 合理使用操作符:操作符虽好,但不宜过度嵌套,保持链式的可读性。

  5. 调试:使用 print() 操作符可以打印出事件流的发生,便于调试。

    swift
    复制
    .print("Debug Stream")
    .sink { ... }
    

总结

Combine 框架通过其声明式的语法,将复杂的异步代码转化为清晰、线性的数据流管道。虽然初学时有一定门槛,但一旦掌握,它将极大地提升你处理异步事件和状态管理的能力,使代码更健壮、更易维护。从简单的 UI 绑定到复杂的异步操作链,Combine 都是一个强大的工具。建议从简单的例子开始实践,逐步深入到更复杂的场景中。

希望这篇博客能为你打开 Combine 世界的大门!

KN:Kotlin 与 OC 交互

作者 Dashing
2025年10月24日 17:49

Kotlin Native (KN)

Kotlin/Native | Kotlin Documentation

KN是一种技术,可以将Kotlin的代码编译为原生的二进制库,无需依赖虚拟机即可运行,主要包括下面两部分的内容:

● 基于LLVM的Kotlin编译器的后端

● Kotlin标准库的原生实现

 

 

KN 是 Kotlin Multiplatform 的一部分,KN让 Kotlin 代码可以在更多的目标平台运行。

 

目标平台(Target Platform) 指代码编译后最终运行的硬件和操作系统环境

处理器指令集架构 x86、x86_64、arm32、arm64
二进制文件的组织规范 ELF(Linux)、Mach-O(macOS/iOS)、PE(Windows)
操作系统特有的接口 不同的系统调用接口(IO、多线程)

 

 

编译

image.png

将 Kotlin IR 转换为 LLVM IR,最终通过LLVM生成对应平台的可执行文件。

处理器指令集架构 以及 二进制文件格式 的差异被处理

 

 

Runtime

运行时库,抹平操作系统的差异,为Kotlin标准库提供底层功能实现

 

image.png

可以参考C/C++运行时库理解,可以自行搜索

 

 

编译器跟runtime是相互配合的,runtime 提供的能力,需要编译器来桥接,编译器也需要知道哪些功能可以怎么桥接才能完成编译。

 

 

Helloworld

创建一个 Hello.kt 文件,内容如下:

interface HelloAble {
    fun makeIt(): String
}
 
open class Hello(open var name: String) {
 
    var helloable: HelloAble? = null
 
    open fun greetPrefix(): String {
        return "Hello"
    }
 
    fun greet(): String {
        return ("${greetPrefix()} $name")
    }
 
    fun triggleInterface() {
        helloable?.makeIt()
    }
 
    companion object {
        fun createObj(): Hello {
            return Hello("luyu")
        }
    }
}
 
open class Helloworld(override var name: String) : Hello(name) {
    override fun greetPrefix(): String {
        val s = super.greetPrefix()
        return "$s world"
    }
}
 
fun main() {
    val h = Helloworld("cindy")
    println(h.greet())
    h.triggleInterface()
}

 

执行命令:

kotlinc-native Hello.kt -g -Xno-inline -p program -o Hello

● kotlinc-native:编译Kotlin文件

● -g:带调试信息

● -p program:编译为可执行程序,还可以编译为动态库,静态库,framework等

● -o Hello:输出文件名为Hello

 

在我目前的Mac电脑上,生成下面两个产物:

 

可以直接运行,不依赖虚拟机环境

 

 

启动过程

使用MachOView打开Hello.kexe,搜索main函数,发现函数符号名为kfun:#main(){},可以使用lldb调试看看:

查看堆栈:

需要先初始化runtime,创建参数

调到我们的main函数

 

对应的runtime源码

 

对象创建

main函数反编译结果

 

整体跟C++的机制比较像,成员方法编译为全局的C函数,对象实例是一个C结构体; 调用成员方法就是调用全局函数,并把对象作为第一个参数传入

 

通过 MachOView 可以看到编译后所有的对应的函数

 

 

我们打断点到init方法

清楚的看到有两个参数,第一个参数是this对象,第二个参数是name,分别通过x0寄存器跟x0寄存器 跟 x1 寄存器传递

 

 

我们给name传的值是字符串"cindy",验证下是否正确

● x/8gx $x1 读取 $x1 地址对应的内存中的内容,g:8个字节为一组 x:以16进制显示 重复 8 次

● x/8hx 0x1000b22c0 读取0x1000b22c0地址对应的内存中的内容 h:两个字节为一组 x:以16进制显示 重复 8 次 似乎看到了ASCII 吗

● 以字符串显示,发现了确实是cindy,Kotlin每个字符大小是两个字节

 

 

编译器已经把源码中的 "cindy" 字符串编译为了放在全局数据区的一个 String 对象(全局常量),C结构体定义如下

struct StringHeader {
    TypeInfo* typeInfoOrMeta_;    uint32_t count_;
    int32_t hashCode_;
    uint16_t flags_;
    alignas(KChar) char data_[];
}

 

x1是字符串"cindy",编译器生成的全局常量,x1是字符串"cindy",编译器生成的全局常量,x0是Helloworld对象吗?怎么来的?

 

可以看main函数中调用 init方法之前的一段代码

 

 

● 从数据段获取了Helloworld的类型的信息 kclass:Helloworld

● 调用 AllocInstance 分配内存

● 调用 UpdateStackRef 将分配好的内存的地址保存到栈上

 

类型信息

kclass:Helloworld 是一个地址,里面存的内容是什么?为什么创建对象需要它?

 

通过查看源码,对应的类型是 TypeInfo 的结构体,存储一个类的信息(类比与OC的类对象)

 

对应的定义如下(截图不完整):

● 这些信息本身都是在编译期的常量,编译器组织起来放在了数据段(不可变),运行时可以读取使用

● 很多跟OC类似的信息,父类型、实例大小、成员变量的便宜、遵守的协议等等

 

可以读取信息看下

有 3 个成员变量:objOffsetsCount_ = 3,便宜地址如下:

 

relativeName_ = Helloworld

 

看看ExtendedTypeInfo

三个变量的名字分别是:name helloable name

 

instanceSize_ = 0x20 占用32个字节

三个成员变量,三个指针,每个指针占用8个字节,这里为啥是32个字节?

 

对象

返回的对象使用ObjHeader来表示

 

所有的Kotlin对象,都有一个成员叫 typeInfoOrMeta_,指向自己的类对象 (类比OC中的isa指针)

 

小结

对象创建的流程大概如下:

● 编译器根据类的定义生成类信息(TypeInfo),并放在全局数据段(__DATA_CONST.__const)

● 代码中遇到类实例化,则将代码翻译为以下几步

● 从数据段取到类信息

● 调用 AllocInstance 分配内存,分配内存的大小 从 TypeInfo 中获取

● 并把前 8 个字节的内容填写为TypeInfo的地址

● 调用class的构造方法初始化成员变量

 

方法调用

初始化方法

会先调用父类的构造方法

 

父类设置name属性

 

子类设置name属性

 

 

greet方法

直接调用函数,并且把上面初始化的对象作为第一个参数传入

 

重载greetPrefix

查看函数,greetPrefix 有三个方法,除了父类与子类的实现个一个外,还有一个trampoline的辅助函数

打断点看到,第一个参数也是我们上面创建的对象实例

逻辑如下:

通过对象(读取前八个字节里的地址对应的内容)获取对应的类对象(TypeInfo)

类对象中有虚函数表,虚函数表里有函数地址,通过偏移读取到目标函数的地址,然后跳转到具体的函数

 

查看虚函数表

由于我们没有重载toString等方法的实现,所以虚函数表中对应的函数地址是父类的函数的地址。

 

 

与OC交互

重新将产物编译为 framework

kotlinc-native Hello.kt -g -Xno-inline -Xstatic-framework -p framework -o Hello

 

得到如下产物:

 

可以集成到 Xcode 中运行了

 

生成的头文件也都可以调用了

 

main函数

在二进制的符号表中搜索#main,发现多了一个包装函数

 

objc2kotlin_kfun:#main(){} 中会调用 kfun:#main(){}

 

objc2kotlin_kfun:#main(){} 函数不对应任何一句原代码,完全由编译器生成

objc2kotlin前缀表示 从objc调用kotlin的函数

 

kfun:#main(){} 的逻辑不变(创建对象,方法调用)

 

 

成员方法

查看二进制文件符号,发现每个成员方法也生成了对应的objc2kotlin前缀的函数,例如:

 

在KN编译代码中找到了"objc2kotlin"

编译器如何通过生成 objc2kotlin_ 前缀的方法?

 

objc2kotlin_xxx方法是在OC侧调用,所以其接受的参数以及返回值都是 OC 的对象

objc2kotlin_xxx 要"复用" xxx(KN kotlin 方法) 的逻辑

 

根据相关位置的代码以及编译结果看,大概的流程为以下三步:

 

1.  转换 OC对象 -> Kotlin对象(struct ObjHeader)

2.  调用kotlin函数,获得Kotlin对象的返回值

3.  将返回值转回 OC 对象(还有其他分支,暂不关心)

 

 

比如:

 

由于是init方法,返回值就是传入的self对象,所以不需要第三部转换,可以通过下面这个函数来看下返回值的情况

 

 

 

OC Kotlin 交互

Kotlin_ObjCExport_refFromObjC

查看源码可知是调用objc_msgSend,target是obj,SEL 是toKotlin:

 

 

任意一个对象能响应这个方法吗?

可以响应,runtime给NSObject添加了分类方法

 

当然对于 OC 的原生类有具体的处理:

 

还有一个特殊的KotlinBase的类,是 所有从Kotlin生成的OC的类的 基类

比如:HelloHello,是从Kotlin的Hello类生成的,其基类就是 KotlinBase

这个类有一个特殊的成员变量 refHolder

在toKotlin:中,返回值就是从refHolder中获取的,所以需要看下这个refHolder什么时候赋值的,其内容是什么?

 

情况1

● 94:分配内存,创建 OC 的实例对象(Oojb)

● 96:获取到HelloHello类对应的Kotlin Hello类的TypeInfo

○ OC的HelloHello 是 编译器根据 Hello 生成的,编译器跟runtime有办法获取到,具体获取方法暂不介绍

●  109:通过 Hello类的TypeInfo,创建一个Hello类的实例对象(Kobj),并把 Oobj 绑定到 Kobj 上

○ 绑定:通过Kobj可以轻松的(成本很低的)获取到Oobj

● 112:把 Kobj 放在 Oobj 的 refHolder 成员变量里

 

串一下这段代码后面发生的事情:

 

● alloc 过程:有两个分配内存的过程(创建了两个对象),OC 侧跟Kotlin侧个创建了一个对象,并且把两个对象绑定起来了(互相持有引用),通过其中任意一个对象,可以轻松获取到对侧的对象

● init 过程:给上述Alloc获取的对象发消息,SEL='initWithName',对应的实现是objc2kotlin_kfun:Hello#<init>(kotlin.String){}函数,函数中self对象跟NSString 通过 toKotlin: 转换为 Kotlin 侧的对象,并作为调用 kfun:Hello#<init>(kotlin.String){}函数的参数

 

问题

上述过程好像只是把Kobj初始化了,Oobj初始化成功了吗?

Oobj有成员变量吗?

 

output:

ivar count = 0; property count = 0; method count = 8;

 

其实HelloHello这个类没有实例变量,其父类有一个成员变量:refHolder

 

可以认为OC 侧的对象是 Kotlin侧对象的一个包装,自己不保存任何数据,对OC 侧做的操作(函数调用)都完全转发给Kotlin侧来实现

● OC函数是对Kotlin函数的包装

● OC对象是对Kotlin对象的包装

 

 

情况2

Kotlin_ObjCExport_refToRetainedObjC

● 462:获取Kobj绑定的Oobj

○ 之前绑定过,获取成功直接返回

● 下面的逻辑:需要走新建对象并绑定

○ 1. 通过TypeInfo获取到对应的OC class

○ 2. 调用 createRetainedWrapper 方法

● 分配内存,创建 Oobj

● 双向绑定

 

下面的情况属于这种情况,先创建Kotlin对象,然后创建OC对象

 

情况3

不是从Kotlin class 生成的OC 的类的实例对象转Kotlin对象

● MyObject 从Kotlin 的class生成的

● 如果有一个类集成了 'HelloHello',这个类也不是从Kotlin class 映射过来的

转Kotlin对象走Kotlin_ObjCExport_convertUnmappedObjCObject

动态生成一个TypeInfo,然后单向绑定,只有从Kotlin对象可以找到OC对象

 

动态生成一个TypeInfo里会根据父类,遵守的协议设置虚函数表(这些虚标是编译器提前为每个类型准备好的),对应的实现函数是 以kotlin2objc为前缀的这些函数:

 

这些函数是跟上面介绍的objc2kotlin的作用相反

● kotlin2objc函数 在Kotlin侧调用(绑定在TypeInfo的虚表中);objc2kotlin函数在OC侧调用,绑定在OC类对应的SEL上

● kotlin2objc 将参数转为OC对象,然后给OC对象发消息,获取返回值转为Kotlin对象

● objc2kotlin 将参数转为Kotlin对象,然后调用Kotlin的函数,获取返回值转为OC对象

 

 

 

背后的过程是

● OC_37

○ 创建 MyObject 的实例对象 O1

○ 调用 h1 的 setHelloable 的方法 参数是 h1(self) SEL O1 函数对应的实现是 objc2kotlin_kfun:Hello#<set-helloable>(HelloAble?){}

○ objc2kotlin_kfun:Hello#<set-helloable>(HelloAble?){} 将 h1 跟 O1 转为 对应的 Kotlin 对象 Kh1 KO1

○ h1 -> Kh1 的转换读取refHolder即可,因为 h1 是通过 createRetainedWrapper 方法创建的,创建的时候已经绑定了kotlin对象

○ O1 -> KO1 的过程: 通过 Kotlin_ObjCExport_convertUnmappedObjCObject实现,过程中会动态创建 TypeInfo,通过 O1 遵守的HelloHelloAble协议(OC的runtime可以获取到) 找到Kotlin侧 HelloAble 协议对应的虚表,并设置到动态创建的TypeInfo上,然后通过创建的TypeInfo创建一个 KO1 对象,

○ 然后将 KO1 设置到 Kh1 对应的成员变量上

● OC_38

○ 调用 h1 的 triggleInterface 方法,方法的实现是 objc2kotlin_kfun:Hello#triggleInterface(){}

○ 获取 h1 对应的 Kh1

○ 调用 kotlin 侧的 triggleInterface 方法

● Kotlin_18

○ 读取 Kh1 的成员变量helloable,读取到的对象是 KO1

○ 调用 KO1 makeIt 方法 kfun:HelloAble#makeIt(){}kotlin.String-trampoline -trampoline 后缀的方法是需要通过 TypeInfo 读虚表(此TypeInfo是上面动态创建的,创建的时候设置好了虚表)来确定调用最终的函数为kotlin2objc_kfun:HelloAble#makeIt(){}kotlin.String

○ kotlin2objc_kfun:HelloAble#makeIt(){}kotlin.String 中会把 KO1 转为 O1 对象,调用 O1 对象的 makeIt 方法,通过msgSend 实现,获取到返回值通过toKotlin: 方法转为Kotlin侧的对象

 

总结

● KN 处理 Kotlin 的逻辑整体跟C++的方式比较类似

● refHolder、toKotlin:、objc2kotlin_xx、kotlin2objc_xx 实现了与OC的交互

 

 

附录

objc2kotlin_xx 与 kotlin2objc_xx 方法如何绑定

● 每个class 每个 interface 编译器都会未其生成 TypeInfo

● 如果需要跟OC交互,TypeInfo中有个变量

● objc2kotlin_xx 与 kotlin2objc_xx 就存在这些列表里,runtime在运行时会根据类型匹配,动态绑定这些函数入口

● 在OC类的initialize方法中,会给OC类添加(通过 OC 的 runtime) directAdapters,classAdapters,implementedInterfaces_

● 在动态创建TypeInfo 的过程中,则会使用到kotlinVtable,kotlinItable,reverseAdapters

5.A.swift 使用指南

作者 JZXStudio
2025年10月24日 16:57

大家好,我是K哥。一名独立开发者,同时也是Swift开发框架【Aquarius】的作者,悦记爱寻车app的开发者。

Aquarius开发框架旨在帮助独立开发者和中小型团队,完成iOS App的快速实现与迭代。使用框架开发将给你带来简单、高效、易维护的编程体验。


Aquarius 是一个为 Swift 开发者打造的高效、轻量级开发框架,致力于提供简洁统一的 API 设计,帮助开发者快速构建高质量的 iOS 应用。本文将重点介绍其核心工具集 A.swift 的使用方法。

Aquarius 框架中的 A.swift(简称 A)是一个功能强大的工具箱,它将常用的开发功能封装为静态方法,覆盖了 UI 构建、颜色图片、数据存储、文件操作、事件管理、日志、内购等高频场景。使用 A,开发者可以告别繁琐的底层调用,大幅提升开发效率与代码整洁度。

一、A.swift 是什么?

A 是 Aquarius 框架中的核心便捷层,以命名空间(Namespace)的形式组织代码,提供了一系列静态属性和方法,让开发者能够通过类似 A.uiA.colorA.file 这样的语法快速调用功能模块。

其主要优势包括:

  • 统一入口:所有功能通过 A 访问,降低记忆成本
  • 类型安全:多数接口设计为类型安全或可选值,避免隐性崩溃
  • 功能完备:涵盖 UI、主题、存储、系统交互、日志、支付等常用场景
  • 现代并发支持:如 IAP 模块使用 async/await 封装,适配 Swift 并发编程

二、核心模块概览

下面我们简要介绍 A 中常用的子模块及其典型用途:

模块名 功能说明
A.ui 快速创建常用 UI 控件
A.color / A.image 主题色、系统图标与图片工具
A.userDefaults(_:) UserDefaults 便捷封装,支持 App Group
A.file 文件路径、目录与文件操作
A.calendarEvent 日历事件管理(基于 EventKit)
A.log 分级日志输出,支持 emoji 标识
A.iap 内购流程封装,基于 StoreKit 现代 API

三、实战演示:感受编码效率的飞跃

3.1 快速创建并添加按钮

传统方式

let button = UIButton(type: .system)
button.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)

使用A.swift

let button = A.ui.button
button.addTouchUpInsideBlock { [weak self] control in
    ...
}
let label = A.ui.label
let imageView = A.ui.imageView
let tableView = A.ui.tableView
...

告别不同UI控件创建方式的不同,统一UI控件创建方式。

3.2 使用主题色与系统图标

view.backgroundColor = A.color.blackColor
imageView.image = A.image.systemImage(systemName: "star.fill")

统一管理颜色与图标,轻松适配暗黑模式与主题切换。

3.3 读写 UserDefaults

//写入
A.userDefaults("group.com.jzx.app").forKey("username")
A.userDefaults("group.com.jzx.app").setValue("张三", forKey: "username")
//读取
let name: String? = A.userDefaults("group.com.jzx.app").getStringValue("username")

支持 App Group,并提供类型安全的读取接口。

3.4 文件操作

let path = A.file.pathFromDocuments("data/user.json")
if !A.file.isExist(path) {
    try? A.file.createFolder(at: "data")
}
// 写入文件...

封装常用文件操作方法,提升代码可读性。

3.5 创建日历事件

A.calendarEvent.add(title: "发布会", startDate: start, endDate: end) { result in
    switch result {
    case .success(let id): A.log.info("创建成功:\(id)")
    case .failure(let err): A.log.error("创建失败:\(err)")
    }
}

自动处理权限申请与事件添加,回调清晰。

3.6 分级日志

A.log.debug("用户点击按钮")
A.log.warning("网络请求超时")
A.log.error("解析失败:\(error)")

日志自带 emoji 和等级标识,调试更直观。

3.7 发起内购

Task {
    do {
        let products = try await A.iap.fetchProducts(["com.jzx.pro"])
        if let product = products.first {
            try await A.iap.purchase(product: product)
            A.log.info("购买成功")
        }
    } catch {
        A.log.error("购买失败:\(error)")
    }
}

基于现代 StoreKit API,支持 async/await,逻辑清晰。

四、最佳实践与注意事项

4.1 错误处理要到位

A 中多数可能出错的操作都会通过 Resultthrows 或可选值来表示失败,请务必处理这些情况,避免直接使用 try! 或强制解包。

4.2 权限管理不能忘

如使用 A.calendarEventA.reminderEvent,请确保已在 Info.plist 中添加相应权限说明,并在使用前检查授权状态。

4.3 线程安全需注意

涉及 UI 更新的操作请确保在主线程执行。A.iap 等异步方法已自动处理线程切换,但仍建议使用 MainActorDispatchQueue.main 更新界面。

4.4 结合 MVVM 架构

你可以在 ViewModel 中直接使用 A.fileA.userDefaultsA.iap 等模块,将平台相关代码与 UI 逻辑分离:

class SettingViewModel {
    func clearCache() {
        let cachePath = A.file.pathFromCaches("")
        try? A.file.removeItem(cachePath)
    }
}

五、总结

A.swift 作为 Aquarius 框架中的"瑞士军刀",极大地简化了 iOS 开发中常见的任务流程。无论是创建界面、管理数据、记录日志,还是处理内购和系统事件,A 都提供了简洁而强大的接口。

如果你正在寻找一个能提升开发效率、减少样板代码的 Swift 工具集,不妨试试 Aquarius 框架中的 A.swift


立即体验Aquarius:

第一步:探索资源

第二步:体验效果

  • 📱 下载示例APP悦记 | 爱寻车 - 感受真实项目中的流畅体验

第三步:沟通交流

🧩 iOS DiffableDataSource 死锁问题记录

作者 songgeb
2025年10月24日 16:49

本文提到的问题是实际项目中遇到的,但文章内容由ChatGPT完成,人工进行了review

🪪 错误信息

在使用 UITableViewDiffableDataSource / UICollectionViewDiffableDataSource 时,
调用 apply(_:animatingDifferences:completion:) 方法更新数据时出现了如下崩溃错误

Deadlock detected: calling this method on the main queue with outstanding async updates is not permitted and will deadlock. Please always submit updates either always on the main queue or always off the main queue

⚙️ 问题本质

  • 由于 apply() 本身是 异步执行 diff 计算与 UI 更新 的,如果在前一次 apply() 尚未完成时又调用了新的 apply(),UIKit 就会检测到潜在死锁并抛出上述错误。
  • “outstanding async updates” 表示仍在进行中的异步更新。 即上一次 diff 操作尚未完成,又发起了新的一次 diff

需要注意的是,虽然崩溃信息中提示是线程问题,但根据实际测试,即使所有调用都在主线程执行,也仍然可能发生此错误,因为 UIKit 内部的 diff 计算与视图更新是异步的。

错误代码以及错误原因如下所示:

import UIKit

class MyViewController: UIViewController {
    enum Section { case main }
    struct Item: Hashable { let id = UUID(); let title: String }

    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
    private var items: [Item] = [.init(title: "A")]

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionViewFlowLayout())
        view.addSubview(collectionView)
        collectionView.register(MyCell.self, forCellWithReuseIdentifier: "cell")

        dataSource = UICollectionViewDiffableDataSource<Section, Item>(
            collectionView: collectionView
        ) { [weak self] collectionView, indexPath, item in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCell
            cell.label.text = item.title

            // 第二次通过apply刷新列表,❌ 错误:cell 回调中再次触发 apply
            cell.onTap = {
                guard let self = self else { return }
                self.items.append(.init(title: "New"))
                self.applySnapshot() // 上一次 apply 未完成时调用,可能触发错误
            }
            return cell
        }
        // 第一次通过apply刷新列表
        applySnapshot()
    }

    private func applySnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}


🧭 解决思路

✅ 1. 防止重入(推荐)

使用标志位,确保同一时间只执行一次 apply(),并缓存后续请求:

private var isApplyingSnapshot = false
private var pendingSnapshot: NSDiffableDataSourceSnapshot<Section, Item>?

func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>) {
    guard !isApplyingSnapshot else {
        pendingSnapshot = snapshot
        return
    }
    isApplyingSnapshot = true

    dataSource.apply(snapshot, animatingDifferences: true) { [weak self] in
        guard let self = self else { return }
        self.isApplyingSnapshot = false
        if let next = self.pendingSnapshot {
            self.pendingSnapshot = nil
            self.applySnapshot(next)
        }
    }
}

并可以根据该思想封装一个防止重入的类

import UIKit

/// 一个安全的 DiffableDataSource 快照更新管理器
/// 支持自动排队多次 apply,防止死锁与丢帧
final class SafeDiffableApplier<Section: Hashable, Item: Hashable> {
    private let dataSource: UITableViewDiffableDataSource<Section, Item>
    private var isApplying = false
    private var queue: [QueuedSnapshot] = []

    private struct QueuedSnapshot {
        let snapshot: NSDiffableDataSourceSnapshot<Section, Item>
        let animatingDifferences: Bool
        let completion: (() -> Void)?
    }

    init(dataSource: UITableViewDiffableDataSource<Section, Item>) {
        self.dataSource = dataSource
    }

    /// 安全地应用快照(自动排队,避免死锁)
    func apply(
        _ snapshot: NSDiffableDataSourceSnapshot<Section, Item>,
        animatingDifferences: Bool = true,
        completion: (() -> Void)? = nil
    ) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            let task = QueuedSnapshot(snapshot: snapshot, animatingDifferences: animatingDifferences, completion: completion)
            self.queue.append(task)
            self.processNextIfNeeded()
        }
    }

    /// 按顺序依次执行队列中的快照更新
    private func processNextIfNeeded() {
        guard !isApplying, !queue.isEmpty else { return }
        isApplying = true

        let next = queue.removeFirst()
        dataSource.apply(next.snapshot, animatingDifferences: next.animatingDifferences) { [weak self] in
            guard let self = self else { return }
            next.completion?()
            self.isApplying = false
            self.processNextIfNeeded() // 递归继续下一个
        }
    }
}

✅ 2. 合并或节流更新

如果更新非常频繁,可以合并多次变化后再统一 apply()

func scheduleSnapshotUpdate() {
    pendingWorkItem?.cancel()
    let workItem = DispatchWorkItem { [weak self] in
        guard let self = self else { return }
        let snapshot = self.generateSnapshot()
        self.dataSource.apply(snapshot, animatingDifferences: true)
    }
    pendingWorkItem = workItem
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem)
}

🧠 总结

  • apply()异步 的,不可重复调用。
  • 错误提示的 “outstanding async updates” 即代表上一次 diff 尚未完成。
  • 必须串行化更新操作,或合并多次更新。
  • 仅仅在主线程调度(DispatchQueue.main.async) 并不能根本解决问题。

参考


《Flutter全栈开发实战指南:从零到高级》- 06 -常用布局组件

2025年10月24日 10:04

Flutter常用布局

1. 引言:为什么布局系统如此重要?

比方说你要装修一间房子:你需要规划每个房间的位置、大小,考虑家具的摆放,确保空间利用合理且美观。Flutter的布局系统就是你在数字世界中的"室内设计师",它决定了每个UI元素的位置、大小和相互关系。

一个好的布局应该具备:

  • 精确的元素定位
  • 兼容自适应屏幕
  • 渲染性能高效
  • 视觉层次美观

今天就带你详细介绍Flutter的常用布局,让你熟练掌握布局系统~~~

2. Container:万能的布局容器

它是最基础也是最强大的布局组件之一。 71bbb7f5f8f06c5f6fe93e939eee55b8.png

2.1 Container的基本用法

Container(
  width: 200,                    // 设置宽度
  height: 100,                   // 设置高度
  color: Colors.blue,            // 背景颜色
  child: Text('Hello Flutter'),  // 子组件
)

这就像给文字套上了一个蓝色的相框,简单直接。

2.2 Container的装饰功能

但Container的真正威力在于它的装饰能力:

Container(
  width: 200,
  height: 100,
  decoration: BoxDecoration(
    color: Colors.white,                     // 背景色
    borderRadius: BorderRadius.circular(16), // 圆角
    boxShadow: [                             // 阴影
      BoxShadow(
        color: Colors.black12,
        blurRadius: 10,
        offset: Offset(0, 4),
      ),
    ],
    border: Border.all(                    // 边框
      color: Colors.blue,
      width: 2,
    ),
    gradient: LinearGradient(              // 渐变背景
      colors: [Colors.blue, Colors.purple],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    ),
  ),
  child: Center(
    child: Text(
      '精美的容器',
      style: TextStyle(
        color: Colors.white,
        fontWeight: FontWeight.bold,
      ),
    ),
  ),
)

Container的核心属性:

  • width / height:控制尺寸
  • margin:外边距,与其他组件的距离
  • padding:内边距,内容与边框的距离
  • decoration:装饰效果(颜色、边框、阴影等)
  • constraints:尺寸约束

2.3 实际应用场景

下面以具体的实际开发场景为例,带大家深入了解Container组件

场景1:用户头像容器

Container(
  width: 80,
  height: 80,
  decoration: BoxDecoration(
    color: Colors.grey[200],
    borderRadius: BorderRadius.circular(40), // 圆形
    border: Border.all(color: Colors.blue, width: 2),
    image: DecorationImage(
      image: NetworkImage('https://example.com/avatar.jpg'),
      fit: BoxFit.cover,
    ),
  ),
)

场景2:消息气泡

Container(
  constraints: BoxConstraints(
    maxWidth: 250,  // 最大宽度限制
  ),
  padding: EdgeInsets.all(12),
  decoration: BoxDecoration(
    color: Colors.blue[50],
    borderRadius: BorderRadius.only(
      topLeft: Radius.circular(16),
      topRight: Radius.circular(16),
      bottomRight: Radius.circular(4),
    ),
  ),
  child: Text('这是一条消息内容'),
)

3. Padding和Margin

Padding和Margin就像人与人之间的安全距离,它们控制着组件之间的空间关系,但作用对象不同。

3.1 Padding:内部空间

Padding是组件内容与边框之间的距离,好比相框与照片之间的留白:

Container(
  color: Colors.blue,
  child: Padding(
    padding: EdgeInsets.all(16),  // 四周都留16像素的空白
    child: Text(
      '有呼吸空间的文字',
      style: TextStyle(color: Colors.white),
    ),
  ),
)

EdgeInsets的四种用法:

// 1. 统一间距
EdgeInsets.all(16)

// 2. 分别设置上下左右
EdgeInsets.fromLTRB(10, 20, 10, 20)

// 3. 设置水平和垂直
EdgeInsets.symmetric(horizontal: 10, vertical: 20)

// 4. 只设置一边
EdgeInsets.only(left: 10, top: 5)

3.2 Margin:外部安全距离

Margin是组件与其他组件之间的距离,就像两个人谈话时的舒适距离:

Container(
  width: 100,
  height: 100,
  color: Colors.red,
  margin: EdgeInsets.all(20),  // 四周都保持20像素的距离
  child: Text('我有外边距'),
)

3.3 实际应用:卡片布局

Container(
  margin: EdgeInsets.all(16),      // 卡片与其他组件的距离
  padding: EdgeInsets.all(20),     // 卡片内容与边框的距离
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [
      BoxShadow(
        color: Colors.black12,
        blurRadius: 8,
      ),
    ],
  ),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        '产品标题',
        style: TextStyle(
          fontSize: 18,
          fontWeight: FontWeight.bold,
        ),
      ),
      SizedBox(height: 8),         // 文字之间的间距
      Text('产品描述信息...'),
    ],
  ),
)

4. Row和Column:线性布局

Row是横向布局,Column是竖向布局。它们让组件按照线性方式排列,是使用频率最高的布局组件。

4.1 Row:水平排列

Row让子组件水平排列,就像我们生活中排队买票的人群: 97750a37af503adc2ed73b58323a389f.png

Row(
  children: [
    Icon(Icons.star, color: Colors.orange),
    Icon(Icons.star, color: Colors.orange),
    Icon(Icons.star, color: Colors.orange),
    Icon(Icons.star_border, color: Colors.grey),
    Icon(Icons.star_border, color: Colors.grey),
  ],
)

Row的核心属性:

  • mainAxisAlignment:主轴对齐方式(水平方向)
  • crossAxisAlignment:交叉轴对齐方式(垂直方向)
  • mainAxisSize:主轴尺寸

4.2 主轴对齐方式

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween, // 两端对齐,均匀分布
  children: [
    Container(width: 50, height: 50, color: Colors.red),
    Container(width: 50, height: 50, color: Colors.green),
    Container(width: 50, height: 50, color: Colors.blue),
  ],
)

MainAxisAlignment的选项:

  • start:左对齐
  • end:右对齐
  • center:居中对齐
  • spaceBetween:两端对齐,组件间隔相等
  • spaceAround:每个组件两侧间隔相等
  • spaceEvenly:组件间隔与边框间隔都相等

4.3 Column:垂直排列

Column让子组件垂直排列,就像叠放的一摞书籍: f397e34988953bbbdb8ad0d9a937f65d.png

Column(
  crossAxisAlignment: CrossAxisAlignment.start, // 左对齐
  children: [
    Text('标题', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
    SizedBox(height: 8),
    Text('副标题', style: TextStyle(fontSize: 16, color: Colors.grey)),
    SizedBox(height: 16),
    Text('内容描述...'),
  ],
)

4.4 实际应用:用户信息卡片

Row(
  children: [
    // 头像
    Container(
      width: 60,
      height: 60,
      decoration: BoxDecoration(
        color: Colors.grey[300],
        borderRadius: BorderRadius.circular(30),
      ),
      child: Icon(Icons.person, size: 30, color: Colors.grey[600]),
    ),
    
    // 间距
    SizedBox(width: 16),
    
    // 用户信息
    Expanded(  // 占据剩余空间
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('张小明', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          SizedBox(height: 4),
          Text('高级用户体验设计师', style: TextStyle(color: Colors.grey[600])),
          SizedBox(height: 4),
          Text('2小时前在线', style: TextStyle(color: Colors.green, fontSize: 12)),
        ],
      ),
    ),
    
    // 右侧图标
    Icon(Icons.chevron_right, color: Colors.grey[400]),
  ],
)

5. Flex和Expanded:弹性布局

举个例子:Flex和Expanded就像弹簧和橡皮筋,它们让布局具有弹性,能够根据可用空间自动调整。

5.1 Flex布局基础

Flex是Row和Column的父类,提供了更灵活的布局方式: f6851a8990489db85a134b5058f86981.png

Flex(
  direction: Axis.horizontal,  // 水平排列,相当于Row
  children: [
    // 子组件
  ],
)

5.2 Expanded:占据剩余空间

Expanded让子组件占据剩余空间,就像弹簧可以拉伸:

Row(
  children: [
    Container(
      width: 80,
      height: 50,
      color: Colors.red,
    ),
    Expanded(  // 占据剩余的所有水平空间
      child: Container(
        height: 50,
        color: Colors.blue,
        child: Center(child: Text('弹性区域')),
      ),
    ),
  ],
)

5.3 Flexible:尺寸控制

Flexible提供更精细的弹性控制:

Row(
  children: [
    Flexible(
      flex: 1,  // 权重为1
      child: Container(height: 50, color: Colors.red),
    ),
    Flexible(
      flex: 2,  // 权重为2,占据两倍的空间
      child: Container(height: 50, color: Colors.green),
    ),
    Flexible(
      flex: 1,  // 权重为1
      child: Container(height: 50, color: Colors.blue),
    ),
  ],
)

Flexible vs Expanded:

  • Expanded是Flexible(fit: FlexFit.tight)的简写
  • Flexible默认是FlexFit.loose,子组件可以选择不填满空间
  • Expanded强制子组件填满空间

5.4 实际应用:比例布局

Column(
  children: [
    // 标题栏
    Container(
      height: 60,
      color: Colors.blue,
      child: Center(child: Text('仪表盘', style: TextStyle(color: Colors.white))),
    ),
    
    // 内容区域(占据剩余空间)
    Expanded(
      child: Row(
        children: [
          // 侧边栏(固定宽度)
          Container(
            width: 200,
            color: Colors.grey[100],
            child: ListView(
              children: [
                ListTile(title: Text('菜单项1')),
                ListTile(title: Text('菜单项2')),
                ListTile(title: Text('菜单项3')),
              ],
            ),
          ),
          
          // 主内容区(占据剩余空间)
          Expanded(
            child: Container(
              color: Colors.white,
              child: Center(child: Text('主内容区域')),
            ),
          ),
        ],
      ),
    ),
    
    // 底部栏
    Container(
      height: 50,
      color: Colors.grey[800],
      child: Center(child: Text('版权所有 © 2024', style: TextStyle(color: Colors.white))),
    ),
  ],
)

6. Stack:层叠布局

Stack好比透明的幻灯片,可以让多个组件重叠在一起,组合出丰富的页面视觉效果。 f4cc635cca69f396ef656e89d5e5a1d0.png

6.1 Stack基础用法

Stack(
  children: [
    // 底层背景
    Container(
      width: 200,
      height: 200,
      color: Colors.blue,
    ),
    
    // 中间层
    Positioned(
      top: 20,
      left: 20,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.red,
      ),
    ),
    
    // 顶层
    Positioned(
      bottom: 20,
      right: 20,
      child: Container(
        width: 80,
        height: 80,
        color: Colors.green,
      ),
    ),
  ],
)

6.2 Positioned:精确定位

Positioned用于在Stack中精确定位子组件:

Positioned(
  top: 10,      // 距离顶部10像素
  left: 20,     // 距离左边20像素
  right: 30,    // 距离右边30像素
  bottom: 40,   // 距离底部40像素
  child: Container(color: Colors.orange),
)

6.3 Alignment:相对定位

除了Positioned,还可以使用Alignment进行相对定位:

Stack(
  alignment: Alignment.center,  // 所有子组件默认居中对齐
  children: [
    Container(width: 200, height: 200, color: Colors.blue),
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Align(  // 单独设置对齐方式
      alignment: Alignment.bottomRight,
      child: Container(
        width: 50,
        height: 50,
        color: Colors.green,
      ),
    ),
  ],
)

6.4 实际应用:用户头像徽章

Stack(
  children: [
    // 用户头像
    Container(
      width: 80,
      height: 80,
      decoration: BoxDecoration(
        color: Colors.grey[300],
        borderRadius: BorderRadius.circular(40),
      ),
      child: Icon(Icons.person, size: 40, color: Colors.grey[600]),
    ),
    
    // 在线状态指示器
    Positioned(
      bottom: 0,
      right: 0,
      child: Container(
        width: 20,
        height: 20,
        decoration: BoxDecoration(
          color: Colors.green,
          borderRadius: BorderRadius.circular(10),
          border: Border.all(color: Colors.white, width: 2),
        ),
      ),
    ),
    
    // VIP徽章
    Positioned(
      top: 0,
      right: 0,
      child: Container(
        padding: EdgeInsets.all(4),
        decoration: BoxDecoration(
          color: Colors.orange,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Text(
          'VIP',
          style: TextStyle(
            color: Colors.white,
            fontSize: 10,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    ),
  ],
)

7. 响应式布局设计:适配各种屏幕

响应式布局能够根据不同的屏幕尺寸自动调整布局,提升用户体验。

7.1 MediaQuery:获取屏幕信息

MediaQuery可以获取屏幕尺寸、方向等信息:

class ResponsiveLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 获取屏幕尺寸
    final screenWidth = MediaQuery.of(context).size.width;
    final screenHeight = MediaQuery.of(context).size.height;
    
    // 判断屏幕方向
    final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;
    
    return Container(
      width: screenWidth,
      height: screenHeight,
      color: Colors.grey[200],
      child: Center(
        child: Text(
          '屏幕尺寸: ${screenWidth.toInt()} × ${screenHeight.toInt()}\n'
          '方向: ${isPortrait ? '竖屏' : '横屏'}',
          textAlign: TextAlign.center,
        ),
      ),
    );
  }
}

7.2 LayoutBuilder:根据约束调整布局

LayoutBuilder可以根据父组件的约束动态调整布局:

LayoutBuilder(
  builder: (context, constraints) {
    // 根据可用宽度决定布局方式
    if (constraints.maxWidth > 600) {
      // 宽屏布局
      return Row(
        children: [
          Container(width: 200, color: Colors.blue, child: Text('侧边栏')),
          Expanded(child: Container(color: Colors.green, child: Text('主内容'))),
        ],
      );
    } else {
      // 窄屏布局
      return Column(
        children: [
          Container(height: 100, color: Colors.blue, child: Text('顶部导航')),
          Expanded(child: Container(color: Colors.green, child: Text('主内容'))),
        ],
      );
    }
  },
)

7.3 实际应用:响应式仪表盘

class ResponsiveDashboard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('响应式仪表盘')),
      body: LayoutBuilder(
        builder: (context, constraints) {
          final isWideScreen = constraints.maxWidth > 768;
          
          return Row(
            children: [
              // 侧边栏(在宽屏显示,窄屏隐藏)
              if (isWideScreen)
                Container(
                  width: 250,
                  color: Colors.grey[100],
                  child: ListView(
                    children: [
                      ListTile(title: Text('仪表盘')),
                      ListTile(title: Text('用户管理')),
                      ListTile(title: Text('数据分析')),
                      ListTile(title: Text('系统设置')),
                    ],
                  ),
                ),
              
              // 主内容区域
              Expanded(
                child: Container(
                  padding: EdgeInsets.all(16),
                  child: GridView.count(
                    // 根据屏幕宽度调整列数
                    crossAxisCount: isWideScreen ? 3 : 2,
                    crossAxisSpacing: 16,
                    mainAxisSpacing: 16,
                    children: [
                      _buildStatCard('用户数', '1,234', Colors.blue),
                      _buildStatCard('订单数', '567', Colors.green),
                      _buildStatCard('收入', '\$8,901', Colors.orange),
                      _buildStatCard('增长率', '12.3%', Colors.purple),
                      _buildStatCard('满意度', '98%', Colors.red),
                      _buildStatCard('活跃度', '87%', Colors.teal),
                    ],
                  ),
                ),
              ),
            ],
          );
        },
      ),
      
      // 窄屏时显示底部导航
      bottomNavigationBar: constraints.maxWidth <= 768 ? BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.dashboard), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.people), label: '用户'),
          BottomNavigationBarItem(icon: Icon(Icons.settings), label: '设置'),
        ],
      ) : null,
    );
  }
  
  Widget _buildStatCard(String title, String value, Color color) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
        boxShadow: [
          BoxShadow(
            color: Colors.black12,
            blurRadius: 6,
            offset: Offset(0, 2),
          ),
        ],
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            value,
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
              color: color,
            ),
          ),
          SizedBox(height: 8),
          Text(
            title,
            style: TextStyle(
              color: Colors.grey[600],
            ),
          ),
        ],
      ),
    );
  }
}

8. 实战案例:用户资料卡片页面

让我们把所有知识融合起来,创建一个完整的用户资料卡片:

class UserProfileCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(16),
      padding: EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black12,
            blurRadius: 10,
            offset: Offset(0, 4),
          ),
        ],
      ),
      child: Column(
        children: [
          // 头部:头像和基本信息
          Row(
            children: [
              // 头像区域(带徽章)
              Stack(
                children: [
                  Container(
                    width: 80,
                    height: 80,
                    decoration: BoxDecoration(
                      color: Colors.blue[100],
                      borderRadius: BorderRadius.circular(40),
                      image: DecorationImage(
                        image: NetworkImage('https://example.com/avatar.jpg'),
                        fit: BoxFit.cover,
                      ),
                    ),
                  ),
                  // 在线状态
                  Positioned(
                    bottom: 0,
                    right: 0,
                    child: Container(
                      width: 20,
                      height: 20,
                      decoration: BoxDecoration(
                        color: Colors.green,
                        borderRadius: BorderRadius.circular(10),
                        border: Border.all(color: Colors.white, width: 2),
                      ),
                    ),
                  ),
                ],
              ),
              
              // 间距
              SizedBox(width: 16),
              
              // 用户信息
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      '张小明',
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    SizedBox(height: 4),
                    Text(
                      '高级用户体验设计师',
                      style: TextStyle(
                        color: Colors.grey[600],
                      ),
                    ),
                    SizedBox(height: 8),
                    Row(
                      children: [
                        Icon(Icons.location_on, size: 16, color: Colors.grey),
                        SizedBox(width: 4),
                        Text(
                          '北京市海淀区',
                          style: TextStyle(fontSize: 12, color: Colors.grey),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
              
              // 更多操作按钮
              IconButton(
                icon: Icon(Icons.more_vert, color: Colors.grey),
                onPressed: () {},
              ),
            ],
          ),
          
          // 分隔线
          Padding(
            padding: EdgeInsets.symmetric(vertical: 16),
            child: Divider(height: 1, color: Colors.grey[300]),
          ),
          
          // 统计信息
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _buildStatItem('关注', '234'),
              _buildStatItem('粉丝', '1.2k'),
              _buildStatItem('作品', '56'),
              _buildStatItem('点赞', '3.4k'),
            ],
          ),
          
          // 分隔线
          Padding(
            padding: EdgeInsets.symmetric(vertical: 16),
            child: Divider(height: 1, color: Colors.grey[300]),
          ),
          
          // 操作按钮
          Row(
            children: [
              Expanded(
                child: ElevatedButton(
                  onPressed: () {},
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.blue,
                    foregroundColor: Colors.white,
                  ),
                  child: Text('关注'),
                ),
              ),
              SizedBox(width: 12),
              Expanded(
                child: OutlinedButton(
                  onPressed: () {},
                  child: Text('发消息'),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
  
  Widget _buildStatItem(String label, String value) {
    return Column(
      children: [
        Text(
          value,
          style: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
        ),
        SizedBox(height: 4),
        Text(
          label,
          style: TextStyle(
            fontSize: 12,
            color: Colors.grey[600],
          ),
        ),
      ],
    );
  }
}

9. 性能优化与最佳实践

9.1 布局性能优化

  1. 避免过度嵌套

    // ❌ 不好的做法:过度嵌套
    Container(
      child: Padding(
        padding: EdgeInsets.all(10),
        child: Container(
          child: Center(
            child: Text('Hello'),
          ),
        ),
      ),
    )
    
    // ✅ 好的做法:使用Container的padding属性
    Container(
      padding: EdgeInsets.all(10),
      child: Center(
        child: Text('Hello'),
      ),
    )
    
  2. 使用const构造函数

    // ✅ 好的做法:使用const
    const Text('静态文本')
    
    // ❌ 不好的做法:不使用const
    Text('静态文本')
    

9.2 代码优化

  1. 提取重复布局

    // 提取为独立组件
    Widget _buildListItem(String title, String subtitle) {
      return ListTile(
        title: Text(title),
        subtitle: Text(subtitle),
        trailing: Icon(Icons.chevron_right),
      );
    }
    
  2. 使用扩展方法

    extension PaddingExtension on Widget {
      Widget withPadding(EdgeInsets padding) {
        return Padding(padding: padding, child: this);
      }
    }
    
    // 使用
    Text('Hello').withPadding(EdgeInsets.all(16))
    

当然还有很多其他优化的点,这里就不一一介绍了,需要大家花时间去一步步摸索尝试~

10. 知识点总结

1e81305d8cf662284c242d04e69e3050.png

通过今天的学习,我们掌握了Flutter布局系统的核心概念:

1. 基础容器:

  • Container:万能的布局容器,支持装饰效果
  • Padding:控制内部间距
  • Margin:控制外部间距

2. 线性布局:

  • Row:水平排列组件
  • Column:垂直排列组件
  • Flex / Expanded:弹性布局,按比例分配空间

3. 层叠布局:

  • Stack:组件重叠布局
  • Positioned:在Stack中精确定位
  • Align:相对对齐定位

4. 响应式设计:

  • MediaQuery:获取屏幕信息
  • LayoutBuilder:根据约束动态布局
  • 断点设计和方向适配

重点:布局设计思维

  1. 从外到内:先确定整体结构,再细化内部组件
  2. 优先使用简单布局:能用Row/Column解决的问题不要用复杂布局
  3. 考虑扩展性:设计时要考虑不同屏幕尺寸和内容变化
  4. 性能意识:避免过度嵌套,合理使用const

写在最后的话

好的布局就像好的建筑,不仅要美观,更要实用和稳固。

布局设计是一个需要不断练习和实践的过程。多观察优秀的App界面,思考它们的布局方式,然后用自己的代码实现出来。很快你就会发现,面对任何UI设计稿,你都能轻松地用Flutter实现出来!

如果这篇教程对你有帮助,请给我点个赞 👍 支持一下! 有什么布局方面的疑问?欢迎在评论区留言讨论~Happy Coding! ✨

Swift 方法全解:实例方法、mutating 方法与类型方法一本通

作者 unravel2025
2025年10月24日 09:52

前言

官方文档已经把语法和规则写得足够严谨,但初学者常遇到三个卡点:

  1. 结构体/枚举居然也能定义方法?
  2. mutating 到底“变异”了什么?
  3. static 与 class 关键字在类型方法里的区别与实战意义。

方法(Method)到底是什么

一句话:方法是“挂在某个类型上的函数”。

  • 在 Swift 里,类(class)、结构体(struct)、枚举(enum)都能挂函数,有两大类,分别叫做实例方法或类型方法。

  • 与 C/Objective-C 不同,C 只有函数指针,Objective-C 只有类能定义方法;Swift 把“方法”能力下放到了值类型,带来了更灵活的建模方式。

实例方法(Instance Method)

  1. 定义与调用
class Counter1 {
    var count = 0
    
    // 实例方法:默认访问全部实例成员
    func increment() {
        count += 1
    }
    
    // 带参数的方法
    func increment(by amount: Int) {
        count += amount
    }
    
    func reset() {
        count = 0
    }
}

// 调用
let counter = Counter1()
counter.increment()          // 1
print(counter.count)
counter.increment(by: 5)     // 6
print(counter.count)
counter.reset()              // 0
print(counter.count)
  1. self 的隐式与显式
  • 不写 self:编译器默认你访问的是“当前实例”成员。
  • 必须写 self:局部变量/参数与属性重名时,用来消歧。
struct Point {
    var x = 0.0, y = 0.0
    
    func isToTheRightOf(x: Double) -> Bool {
        // 如果省略 self,x 会被当成参数 x
        return self.x > x
    }
}

值类型内部修改自身:mutating 实例方法

  1. 默认禁止修改

结构体/枚举是值类型,实例方法里不能改自己属性——除非加 mutating

  1. 加 mutating 后发生了什么
  • 方法被标记为“会改本体”,编译器会把调用处生成的 let 常量拦截掉。
  • 底层实现:方法拿到的是 inout self,可以整体替换。
struct Point: CustomStringConvertible {
    var description: String {
        "{ x: \(x), y: \(y)}"
    }
    
    var x = 0.0, y = 0.0
    
    // 移动自身
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
    
    // 更激进的写法:直接给 self 赋新实例
    mutating func teleport(toX x: Double, toY y: Double) {
        self = Point(x: x, y: y)
    }
}

var p = Point(x: 1, y: 1)
print(p)
p.moveBy(x: 2, y: 3)   // 现在 (3,4)
print(p)
p.teleport(toX: 0, toY: 0) // 整体替换为 (0,0)
print(p)

// 以下会编译错误
let fixedPoint = Point(x: 3, y: 3)
print(fixedPoint)
// fixedPoint.moveBy(x: 1, y: 1) // ❌
  1. 枚举也能 mutating
enum TriStateSwitch {
    case off, low, high
    
    mutating func next() {
        switch self {
        case .off: self = .low
        case .low: self = .high
        case .high: self = .off
        }
    }
}

var oven = TriStateSwitch.low
print(oven)
oven.next() // high
print(oven)
oven.next() // off
print(oven)

类型方法(Type Method)

  1. 关键字
  • static:类、结构体、枚举都能用;子类不能重写。
  • class:仅类能用;子类可 override。
  1. 调用方式

“类型名.方法名”,无需实例。

  1. 方法体内 self 指“类型自身”
class SomeClass {
    class func helloType() {
        print("Hello from \(self)") // 打印类名
    }
}
SomeClass.helloType()

完整实战:游戏关卡管理

下面把“类型属性 + 类型方法 + 实例方法”揉到一起,演示一个常用模式:

  • 类型层保存“全局状态”(最高解锁关卡)。
  • 实例层保存“个人状态”(当前玩家在第几关)。
struct LevelTracker {
    // 1. 类型属性:所有玩家共享
    nonisolated(unsafe) static var highestUnlockedLevel = 1
    
    // 2. 类型方法:解锁
    static func unlock(_ level: Int) {
        if level > highestUnlockedLevel {
            highestUnlockedLevel = level
        }
    }
    
    // 3. 类型方法:查询是否解锁
    static func isUnlocked(_ level: Int) -> Bool {
        return level <= highestUnlockedLevel
    }
    
    // 4. 实例属性:个人当前关卡
    var currentLevel = 1
    
    // 5. 实例方法(mutating):进阶到指定关
    @discardableResult
    mutating func advance(to level: Int) -> Bool {
        if LevelTracker.isUnlocked(level) {
            currentLevel = level
            return true
        }
        return false
    }
}

// 玩家类
class Player {
    let name: String
    var tracker = LevelTracker()
    
    init(name: String) {
        self.name = name
    }
    
    // 完成某关
    func complete(level: Int) {
        LevelTracker.unlock(level + 1)          // 全局解锁下一关
        tracker.advance(to: level + 1)          // 个人进度推进
    }
}

// 场景脚本
let player = Player(name: "Argyrios")
player.complete(level: 1)
print("最高解锁关卡:\(LevelTracker.highestUnlockedLevel)") // 2

// 第二个玩家想跳关
let player2 = Player(name: "Beto")
if player2.tracker.advance(to: 6) {
    print("直接跳到 6 成功")
} else {
    print("6 关尚未解锁") // 走进这里
}

易忘细节速查表

  1. mutating 只能用于 struct/enum;class 天生可变,不需要。
  2. static 与 class 区别:
    • 结构体/枚举只能用 static。
    • 类里 static = final class,不允许子类覆盖;class 允许覆盖。
  3. 在类型方法里调用同类类型方法/属性,可直接写名字,无需前缀类型。
  4. @discardableResult 用于“调用方可以不处理返回值”的场景,消除警告。

总结与实战扩展

  1. 方法不再“是类的专利”后,优先用 struct 建模数据,再根据需要升级成 class,可大幅降低引用类型带来的共享状态问题。
  2. mutating 让“值语义 + 链式调用”成为可能,例如:
extension Array where Element: Comparable {
    mutating func removeMin() -> Element? {
        guard let min = self.min() else { return nil }
        remove(at: firstIndex(of: min)!)
        return min
    }
}
var scores = [98, 67, 84]
while let min = scores.removeMin() {
    print("从低到高处理", min)
}
  1. 类型方法是做“全局状态但作用域受限”的利器:
  • App 配置中心(static 存储 + 类型方法读写)
  • 网络请求 stub 中心(type method 注册/注销 mock)
  • 工厂方法(class func makeDefaultXxx())
  1. 与协议组合

把 mutating 写进协议,让 struct/enum 也能提供“可变更”接口,而 class 实现时自动忽略 mutating:

protocol Resettable {
    mutating func reset()
}

Swift 嵌套类型:在复杂类型内部优雅地组织枚举、结构体与协议

作者 unravel2025
2025年10月24日 08:27

为什么要“嵌套”

在 Swift 中,我们经常会写一些“小工具”类型:

  • 只在某个类/结构体里用到的枚举
  • 仅服务于一条业务逻辑的辅助结构体
  • 与外部世界无关的私有协议

如果把它们全部写成顶层类型,会导致:

  1. 命名空间污染(Top-Level 名字过多)
  2. 可读性下降(“这个类型到底给谁用?”)
  3. 访问控制粒度变粗(想私有却不得不 public)

嵌套类型(Nested Types)正是为了解决这三个痛点:把“辅助类型”放进“主类型”内部,让代码的“作用域”与“视觉层次”保持一致。

语法一览:如何“套娃”

// 外层:主类型
struct BlackjackCard {
    
    // 嵌套枚举 ①
    enum Suit: Character {
        case spades   = "♠"
        case hearts   = "♥"
        case diamonds = "♦"
        case clubs    = "♣"
    }
    
    // 嵌套枚举 ②
    enum Rank: Int {
        case two = 2, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king, ace
        
        // 在枚举里再嵌套一个结构体 ③
        struct Values {
            let first: Int
            let second: Int?   // Ace 才有第二值
        }
        
        // 计算属性,返回嵌套结构体
        var values: Values {
            switch self {
            case .ace:
                return Values(first: 1, second: 11)
            case .jack, .queen, .king:
                return Values(first: 10, second: nil)
            default:                // 2...10
                return Values(first: self.rawValue, second: nil)
            }
        }
    }
    
    // 主类型自己的属性
    let rank: Rank
    let suit: Suit
    
    // 计算属性,拼接描述
    var description: String {
        let valueDesc = rank.values.second == nil ?
            "\(rank.values.first)" :
            "\(rank.values.first)\(rank.values.second!)"
        return "\(suit.rawValue)\(rank.rawValue)(点数 \(valueDesc))"
    }
}

知识点逐条拆解

  1. 嵌套深度不限

    上面 Values 结构体嵌套在 Rank 枚举里,Rank 又嵌套在 BlackjackCard 中,形成三级嵌套。只要你愿意,可以继续往下套。

  2. 名字自动带上“前缀”

    在外部使用时,编译器强制你加“外层名字.”前缀,天然起到命名空间隔离:

let color = BlackjackCard.Suit.hearts   // 不会和 Poker.Suit.hearts 冲突
  1. 访问控制可逐层细化

    如果 BlackjackCardpublic,而 Values 声明为 private,那么模块外部无法感知 Values 存在,实现细节被彻底隐藏。

  2. 成员构造器依旧生效

    因为 BlackjackCard 是结构体且未自定义 init,编译器仍会生成逐成员构造器:

let card = BlackjackCard(rank: .ace, suit: .spades)
print(card.description)   // ♠ace(点数 1 或 11)

注意:.ace.spades 可以省略前缀,因为 Swift 能根据形参类型推断出 RankSuit

再举三个日常开发场景

  1. UITableView 嵌套数据源
class SettingsViewController: UITableViewController {
    
    // 仅在本控制器里使用的模型
    private enum Section: Int, CaseIterable {
        case account, privacy, about
        
        var title: String {
            switch self {
            case .account: return "账号"
            case .privacy: return "隐私"
            case .about:   return "关于"
            }
        }
    }
    
    private typealias Row = (icon: UIImage?, text: String, action: () -> Void)
    
    private var data: [Section: [Row]] = [:]
}
  1. Network 嵌套错误
struct API {
    enum Error: Swift.Error {
        case invalidURL
        case httpStatus(code: Int)
        case decodeFailure(underlying: Swift.Error)
    }
    
    func request() async throws -> Model {
        guard let url = URL(string: "https://example.com") else {
            throw Error.invalidURL
        }
        ...
    }
}
  1. SwiftUI 嵌套模型
struct EmojiMemoryGame: View {
    
    // 仅在本 View 文件里使用
    private struct Card: Identifiable {
        let id = UUID()
        let emoji: String
        var isFaceUp = false
    }
    
    @State private var cards: [Card] = []
}

总结与最佳实践

  1. 命名空间 > 前缀

    与其写 BlackjackSuitBlackjackRank,不如直接嵌套,用 BlackjackCard.Suit 既简洁又清晰。

  2. 能 private 就 private

    把嵌套类型默认写成 private,直到外部真的需要再放宽权限,避免“泄露实现”。

  3. 不要“为了嵌套而嵌套”

    如果某个类型在多个业务模块出现,继续嵌套反而会增加引用成本,此时应提升为顶层 internalpublic

  4. typealias 搭配食用更佳

    当嵌套路径过长时,可在当前文件顶部 typealias CardSuit = BlackjackCard.Suit,既保留命名空间,又减少手指负担。

  5. 在 Swift Package 中作为“实现细节”

    公开接口只暴露最外层 public struct BlackjackCard,所有辅助枚举/结构体保持 internalprivate,后续迭代可随意重构而不破坏 SemVer。

Swift 类型转换实用指北:从 is / as 到 Any/AnyObject 的完整路线

作者 unravel2025
2025年10月24日 08:21

为什么要“类型转换”

Swift 是强类型语言,编译期就必须知道每个变量的真实类型。

但在面向对象、协议、泛型甚至混用 OC 的场景里,变量“静态类型”与“实际类型”常常不一致。

类型转换(Type Casting)就是用来:

  1. 检查“实际类型”到底是谁(is)
  2. 把“静态类型”当成别的类型来用(as / as? / as!)
  3. 处理“任意类型”这种黑盒(Any / AnyObject)

核心运算符速查表

运算符 返回类型 可能失败 用途示例
is Bool 不会崩溃 判断“是不是”某类型
as? 可选值 会返回nil 安全向下转型(失败不炸)
as! 非可选值 可能崩溃 强制向下转型(失败运行时错误)
as 原类型 不会失败 向上转型或桥接(OC↔Swift)

建立实验田:先搭一个类层级

// ① 根类:媒体条目
class MediaItem {
    let name: String
    init(name: String) { self.name = name }
}

// ② 子类:电影
class Movie: MediaItem {
    let director: String
    init(name: String, director: String) {
        self.director = director
        super.init(name: name)
    }
}

// ③ 子类:歌曲
class Song: MediaItem {
    let artist: String
    init(name: String, artist: String) {
        self.artist = artist
        super.init(name: name)
    }
}

// ④ 仓库:存放所有媒体
let library: [MediaItem] = [
    Movie(name: "卧虎藏龙", director: "李安"),
    Song(name: "青花瓷", artist: "周杰伦"),
    Movie(name: "星际穿越", director: "诺兰"),
    Song(name: "晴天", artist: "周杰伦"),
    Song(name: "夜曲", artist: "周杰伦")
]

虽然数组静态类型是[MediaItem],但运行时每个元素仍然是原来的MovieSong

想访问director/artist?先检查、再转换——这就是本文主题。

检查类型:is 的用法

需求:统计library里电影、歌曲各多少。

var movieCount = 0
var songCount  = 0

for item in library {
    if item is Movie { movieCount += 1 }
    if item is Song  { songCount  += 1 }
}

print("电影\(movieCount)部,歌曲\(songCount)首")
// 打印:电影2部,歌曲3首

要点

  1. is只回答“是/否”,不改动类型。
  2. 对协议也适用,例如item is CustomStringConvertible

向下转型:as? 与 as! 的抉择

需求:把每个元素的详细信息打印出来,需要访问子类独有属性。

for item in library {
    // 1. 先尝试当成电影
    if let movie = item as? Movie {
        print("电影:《\(movie.name)》——导演:\(movie.director)")
        continue
    }
    
    // 2. 再尝试当成歌曲
    if let song = item as? Song {
        print("歌曲:《\(song.name)》——歌手:\(song.artist)")
    }
}

输出: 电影:《卧虎藏龙》——导演:李安 歌曲:《青花瓷》——歌手:周杰伦 电影:《星际穿越》——导演:诺兰 歌曲:《晴天》——歌手:周杰伦 歌曲:《夜曲》——歌手:周杰伦

经验

  1. 不确定成功用as?+可选绑定,几乎不会错。
  2. 只有100%确定时才写as!,否则崩溃现场见:
let first = library[0] as! Song  // 运行时错误:Could not cast value of type 'Movie' to 'Song'

向上转型:as 的“隐形”场景

向上转是最安全的,因为子类一定能当父类用,Swift甚至允许省略as

let m: Movie = Movie(name: "哪吒", director: "饺子")
let item: MediaItem = m   // 编译器自动向上转

但在某些桥接场景必须显式写as

// NSArray 只能装 NSObject,Swift String 需要桥接
let ocArray: NSArray = ["A", "B", "C"] as NSArray

Any 与 AnyObject:万金油盒子

类型 能装什么 常见场景
Any 任何类型(含struct/enum/closure) JSON、脚本语言交互
AnyObject 任何class(含@objc协议) OC SDK、UITableView datasource

示例:把“完全不相干”的值塞进一个数组

var things = [Any]()
things.append(42)                       // Int
things.append(3.14)                     // Double
things.append("Hello")                  // String
things.append((2.0, 5.0))               // 元组
things.append(Movie(name: "哪吒", director: "饺子"))
things.append({ (name: String) -> String in "Hi, \(name)" }) // 闭包

怎么把这些值取出来?switch + 模式匹配最清晰:

for thing in things {
    switch thing {
    case let int as Int:
        print("整数值:\(int)")
    case let double as Double:
        print("小数值:\(double)")
    case let str as String:
        print("字符串:\(str)")
    case let (x, y) as (Double, Double):
        print("坐标:(\(x), \(y))")
    case let movie as Movie:
        print("任意盒里的电影:\(movie.name)")
    case let closure as (String) -> String:
        print("闭包返回:\(closure("Swift"))")
    default:
        print("未匹配到的类型")
    }
}

要点

  1. as模式可以一次性完成“类型检查+绑定”。
  2. 不要滥用Any/AnyObject,你会失去编译期检查,代码维护成本陡增。

常见踩坑与调试技巧

  1. “is”对协议要求苛刻
protocol Playable { }
extension Song: Playable { }

let s: MediaItem = Song(name: "x", artist: "y")
print(s is Playable) // true

之前的某个Swift版本,会将s推断为MediaItem,而MediaItem没实现Playable协议,所以返回false

从4.x之后s的实际类型是Sone,返回true

  1. JSON转字典后全成Any
let json: [String: Any] = ["age": 18]
let age = json["age"] as! Int + 1  // 万一服务器返回String就崩

as?+guard提前返回,或Codable一步到位。

  1. as!链式写法
let label = (view.subviews[0] as! UILabel).text as! String  // 两层强转,一层失败就崩

建议分步+可选绑定,或使用if let label = view.subviews.first as? UILabel

实战延伸:类型转换在架构中的身影

  1. MVVM差异加载

tableView同一cellForRow里根据item is HeaderItem / DetailItem画不同UI。
2. 路由/插件

URL路由把参数打包成[String: Any],各插件再as?取出自己关心的类型。
3. 单元测试

XCTAssertTrue(mock is MockNetworkClient)确保测试替身注入正确。
4. OC老SDK混编

UIViewController→自定义子类,as!前先用isKind(of:)(OC习惯)或is检查。

总结与私货

  1. 类型转换不是“黑科技”,它只是把运行时类型信息暴露给开发者。
  2. 优先用as?+可选绑定,让错误止步于nil;as!留给自己能写单元测试担保的场景。
  3. Any/AnyObject是“逃生舱”,一旦打开就等于对编译器说“相信我”。能不用就不用,实在要用就封装成明确的枚举或struct,把转换工作限制在最小作用域。
  4. 在团队Code Review里,见到as!可以强制要求写注释说明为什么不会崩;这是用制度换安全感。
  5. 如果业务里大量is/as泛滥,多半协议/泛型抽象得不够,可以考虑重构:
    • 用协议扩展把“差异化行为”做成多态,而不是if/else判断类型。
    • 用泛型把“运行时类型”提前到“编译期类型”,减少转换。

《Flutter全栈开发实战指南:从零到高级》- 05 - 基础组件实战:构建登录界面

2025年10月23日 17:48

手把手教你实现一个Flutter登录页面

嗨,各位Flutter爱好者!今天我要和大家分享一个超级实用的功能——用Flutter构建一个功能完整的登录界面。说实话,第一次接触Flutter时,看着那些组件列表也是一头雾水,但当真正动手做出第一个登录页面后,才发现原来一切都这么有趣!

登录界面就像餐厅的门面,直接影响用户的第一印象。今天,我们就一起来打造一个既美观又实用的"门面"!

我们要实现什么?

先来看看我们的目标——一个支持多种登录方式的登录界面:

含以下功能点:

  • 双登录方式:账号密码 + 手机验证码
  • 实时表单验证
  • 记住密码和自动登录
  • 验证码倒计时
  • 第三方登录(微信&QQ&微博)
  • 交互动画

是不是已经迫不及待了?别急,工欲善其事,必先利其器!!! 在开始搭建之前,我们先来熟悉一下Flutter的基础组件,这些组件就像乐高积木,每个都有独特的用途,组合起来就能创造奇迹!

一、Flutter基础组件

1.1 Text组件:不只是显示文字

Text组件就像聊天时的文字消息,不同的样式能传达不同的情感。让我给你展示几个实用的例子:

// 基础文本 - 就像普通的聊天消息
Text('你好,Flutter!')

// 带样式的文本 - 像加了特效的消息
Text(
  '欢迎回来!',
  style: TextStyle(
    fontSize: 24.0,              // 字体大小
    fontWeight: FontWeight.bold, // 字体粗细
    color: Colors.blue[800],     // 字体颜色
    letterSpacing: 1.2,          // 字母间距
  ),
)

// 富文本 - 像一条消息中有不同样式的部分
Text.rich(
  TextSpan(
    children: [
      TextSpan(
        text: '已有账号?',
        style: TextStyle(color: Colors.grey[600]),
      ),
      TextSpan(
        text: '立即登录',
        style: TextStyle(
          color: Colors.blue,
          fontWeight: FontWeight.bold,
        ),
      ),
    ],
  ),
)

实用技巧:

  • 文字超出时显示省略号:overflow: TextOverflow.ellipsis
  • 限制最多显示行数:maxLines: 2
  • 文字居中显示:textAlign: TextAlign.center

1.2 TextField组件:用户输入

TextField就像餐厅的点菜单,用户在上面写下需求,我们负责处理。来看看如何打造一个贴心的输入体验:

// 基础输入框
TextField(
  decoration: InputDecoration(
    labelText: '用户名',             // 标签文字
    hintText: '请输入用户名',        // 提示文字
    prefixIcon: Icon(Icons.person), // 前缀图标
  ),
)

// 密码输入框 - 带显示/隐藏切换
TextField(
  obscureText: true,  // 隐藏输入内容
  decoration: InputDecoration(
    labelText: '密码',
    prefixIcon: Icon(Icons.lock),
    suffixIcon: IconButton(    // 后缀图标按钮
      icon: Icon(Icons.visibility),
      onPressed: () {
        // 切换密码显示/隐藏
      },
    ),
  ),
)

// 带验证的输入框
TextFormField(
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '请输入内容';  // 验证失败时的提示
    }
    return null;  // 验证成功
  },
)

TextField的核心技能:

  • controller:管理输入内容
  • focusNode:跟踪输入焦点
  • keyboardType:为不同场景准备合适的键盘
  • onChanged:实时监听用户的每个输入

1.3 按钮组件:触发事件的开关

按钮就像电梯的按键,按下它就会带你到达想去的楼层。Flutter提供了多种类型按钮,每种都有其独有的特性:

// 1. ElevatedButton - 主要操作按钮(有立体感)
ElevatedButton(
  onPressed: () {
    print('按钮被点击了!');
  },
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.blue,      // 背景色
    foregroundColor: Colors.white,     // 文字颜色
    padding: EdgeInsets.all(16),       // 内边距
    shape: RoundedRectangleBorder(     // 形状
      borderRadius: BorderRadius.circular(12),
    ),
  ),
  child: Text('登录'),
)

// 2. TextButton - 次要操作按钮
TextButton(
  onPressed: () {
    print('忘记密码');
  },
  child: Text('忘记密码?'),
)

// 3. OutlinedButton - 边框按钮
OutlinedButton(
  onPressed: () {},
  child: Text('取消'),
  style: OutlinedButton.styleFrom(
    side: BorderSide(color: Colors.grey),
  ),
)

// 4. IconButton - 图标按钮
IconButton(
  onPressed: () {},
  icon: Icon(Icons.close),
  color: Colors.grey,
)

按钮状态管理很重要:

  • 加载时禁用按钮,防止重复提交
  • 根据表单验证结果控制按钮可用性
  • 提供视觉反馈,让用户知道操作已被接收

1.4 布局组件

布局组件就像房子的承重墙,它们决定了界面元素的排列方式。掌握它们,你就能轻松构建各种复杂布局:

// Container - 万能的容器
Container(
  width: 200,
  height: 100,
  margin: EdgeInsets.all(16),    // 外边距
  padding: EdgeInsets.all(20),   // 内边距
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [                 // 阴影效果
      BoxShadow(
        color: Colors.black12,
        blurRadius: 10,
      ),
    ],
  ),
  child: Text('内容'),
)

// Row - 水平排列
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Text('左边'),
    Text('右边'),
  ],
)

// Column - 垂直排列
Column(
  children: [
    Text('第一行'),
    SizedBox(height: 16),  // 间距组件
    Text('第二行'),
  ],
)

现在我们已经熟悉了基础组件,是时候开始真正的功能实战了!

二、功能实战:构建多功能登录页面

2.1 项目目录结构

在开始编码前,我们先规划好项目结构,就像建房子前先画好房体图纸一样:

lib/
├── main.dart                    # 应用入口
├── models/                      # 数据模型
│   ├── user_model.dart          # 用户模型
│   └── login_type.dart          # 登录类型
├── pages/                       # 页面文件
│   ├── login_page.dart          # 登录页面
│   ├── home_page.dart           # 首页
│   └── register_page.dart       # 注册页面
├── widgets/                     # 自定义组件
│   ├── login_tab_bar.dart       # 登录选项卡
│   ├── auth_text_field.dart     # 认证输入框
│   └── third_party_login.dart   # 第三方登录
├── services/                    # 服务层
│   └── auth_service.dart        # 认证服务
├── utils/                       # 工具类
│   └── validators.dart          # 表单验证
└── theme/                       # 主题配置
    └── app_theme.dart           # 应用主题

2.2 数据模型定义

我们先定义需要用到的数据模型:

// 登录类型枚举
enum LoginType {
  account,  // 账号密码登录
  phone,    // 手机验证码登录
}

// 用户数据模型
class User {
  final String id;
  final String name;
  final String email;
  final String phone;
  
  User({
    required this.id,
    required this.name,
    required this.email,
    required this.phone,
  });
}

2.3 实现登录页面

下面我将会带你一步步构建登录页面。

第一步:状态管理

首先,我们需要管理页面的各种状态,就像我们平时开车时要关注各项指标:

class _LoginPageState extends State<LoginPage> {
  // 登录方式状态
  LoginType _loginType = LoginType.account;
  
  // 文本控制器
  final TextEditingController _accountController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  final TextEditingController _phoneController = TextEditingController();
  final TextEditingController _smsController = TextEditingController();
  
  // 焦点管理
  final FocusNode _accountFocus = FocusNode();
  final FocusNode _passwordFocus = FocusNode();
  final FocusNode _phoneFocus = FocusNode();
  final FocusNode _smsFocus = FocusNode();
  
  // 状态变量
  bool _isLoading = false;
  bool _rememberPassword = true;
  bool _autoLogin = false;
  bool _isPasswordVisible = false;
  bool _isSmsLoading = false;
  int _smsCountdown = 0;
  
  // 错误信息
  String? _accountError;
  String? _passwordError;
  String? _phoneError;
  String? _smsError;
  
  // 表单Key
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  
  @override
  void initState() {
    super.initState();
    _loadSavedData();
  }
  
  void _loadSavedData() {
    // 从本地存储加载保存的账号
    if (_rememberPassword) {
      _accountController.text = 'user@example.com';
    }
  }
}
第二步:构建页面

接下来,我们构建页面的整体结构:

@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.grey[50],
    body: SafeArea(
      child: SingleChildScrollView(
        physics: BouncingScrollPhysics(),
        child: Container(
          padding: EdgeInsets.all(24),
          child: Form(
            key: _formKey,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _buildBackButton(),        // 返回按钮
                SizedBox(height: 20),
                _buildHeader(),            // 页面标题
                SizedBox(height: 40),
                _buildLoginTypeTab(),      // 登录方式切换
                SizedBox(height: 32),
                _buildDynamicForm(),       // 动态表单
                SizedBox(height: 24),
                _buildRememberSection(),   // 记住密码选项
                SizedBox(height: 32),
                _buildLoginButton(),       // 登录按钮
                SizedBox(height: 40),
                _buildThirdPartyLogin(),   // 第三方登录
                SizedBox(height: 24),
                _buildRegisterPrompt(),    // 注册提示
              ],
            ),
          ),
        ),
      ),
    ),
  );
}
第三步:构建各个组件

现在我们来逐一实现每个功能组件:

登录方式切换选项卡:

Widget _buildLoginTypeTab() {
  return Container(
    height: 48,
    decoration: BoxDecoration(
      color: Colors.grey[100],
      borderRadius: BorderRadius.circular(12),
    ),
    child: Row(
      children: [
        // 账号登录选项卡
        _buildTabItem(
          title: '账号登录',
          isSelected: _loginType == LoginType.account,
          onTap: () {
            setState(() {
              _loginType = LoginType.account;
            });
          },
        ),
        // 手机登录选项卡
        _buildTabItem(
          title: '手机登录',
          isSelected: _loginType == LoginType.phone,
          onTap: () {
            setState(() {
              _loginType = LoginType.phone;
            });
          },
        ),
      ],
    ),
  );
}

动态表单区域:

Widget _buildDynamicForm() {
  return AnimatedSwitcher(
    duration: Duration(milliseconds: 300),
    child: _loginType == LoginType.account
        ? _buildAccountForm()   // 账号登录表单
        : _buildPhoneForm(),    // 手机登录表单
  );
}

账号输入框组件:

Widget _buildAccountField() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text('邮箱/用户名'),
      SizedBox(height: 8),
      TextFormField(
        controller: _accountController,
        focusNode: _accountFocus,
        decoration: InputDecoration(
          hintText: '请输入邮箱或用户名',
          prefixIcon: Icon(Icons.person_outline),
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
          ),
          errorText: _accountError,
        ),
        onChanged: (value) {
          setState(() {
            _accountError = _validateAccount(value);
          });
        },
      ),
    ],
  );
}

登录按钮组件:

Widget _buildLoginButton() {
  bool isFormValid = _loginType == LoginType.account
      ? _accountError == null && _passwordError == null
      : _phoneError == null && _smsError == null;

  return SizedBox(
    width: double.infinity,
    height: 52,
    child: ElevatedButton(
      onPressed: isFormValid && !_isLoading ? _handleLogin : null,
      child: _isLoading
          ? CircularProgressIndicator()
          : Text('立即登录'),
    ),
  );
}
第四步:实现业务逻辑

表单验证:

String? _validateAccount(String? value) {
  if (value == null || value.isEmpty) {
    return '请输入账号';
  }
  final emailRegex = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
  if (!emailRegex.hasMatch(value)) {
    return '请输入有效的邮箱';
  }
  return null;
}

登录逻辑:

Future<void> _handleLogin() async {
  if (_isLoading) return;
  
  if (_formKey.currentState!.validate()) {
    setState(() {
      _isLoading = true;
    });
    
    try {
      User user;
      if (_loginType == LoginType.account) {
        user = await AuthService.loginWithAccount(
          account: _accountController.text,
          password: _passwordController.text,
        );
      } else {
        user = await AuthService.loginWithPhone(
          phone: _phoneController.text,
          smsCode: _smsController.text,
        );
      }
      await _handleLoginSuccess(user);
    } catch (error) {
      _handleLoginError(error);
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }
}

效果展示与总结

f1.png

f2.png 至此我们终于完成了一个功能完整的登录页面!让我们总结一下实现的功能:

实现功能点

  1. 双登录方式:用户可以在账号密码和手机验证码之间无缝切换
  2. 智能验证:实时表单验证,即时错误提示
  3. 用户体验:加载状态、错误提示、流畅动画
  4. 第三方登录:支持微信、QQ、微博登录
  5. 状态记忆:记住密码和自动登录选项

学到了什么?

通过这个项目,我们掌握了:

  • 组件使用:Text、TextField、Button等基础组件的深度使用
  • 状态管理:使用setState管理复杂的页面状态
  • 表单处理:实时验证和用户交互
  • 布局技巧:创建响应式和美观的界面布局
  • 业务逻辑:处理用户输入和API调用

最后的话

看到这里,你已经成功构建了一个完整的登录界面!这个登录页面只是开始,期待你能创造出更多更好的应用!

有什么问题或想法?欢迎在评论区留言讨论~, Happy Coding!✨

Swift 枚举完全指南——从基础语法到递归枚举的渐进式学习笔记

作者 unravel2025
2025年10月23日 08:31

前言

在 C/Objective-C 里,枚举只是一组别名整型;在 Swift 里,枚举被提升为“一等类型”(first-class type),可以拥有

  • 计算属性
  • 实例方法
  • 初始化器
  • 扩展、协议、泛型
  • 递归结构

因此,它不再只是“常量集合”,而是一种强大的建模工具。

基础语法:enum、case、点语法

// 1. 最简形式:不附带任何值
enum CompassPoint {
    case north
    case south
    case east
    case west
}

// 2. 单行多 case 写法
enum Planet {
    case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

// 3. 类型推断下的点语法
var direction = CompassPoint.north
direction = .west          // 类型已知,可省略前缀

与 switch 联用: 穷举检查

Swift 的 switch 必须覆盖所有 case,否则编译失败——这是“安全第一”的体现。

var direction = CompassPoint.north
switch direction {
case .north:
    print("Lots of planets have a north")
case .south:
    print("Watch out for penguins")
case .east, .west:          // 多 case 合并
    print("Where the sun rises or sets")
}
// 如果注释掉任意 case,编译器立即报错

遍历所有 case:CaseIterable 协议

只需加一句 : CaseIterable,编译器自动合成 allCases 数组。

enum Beverage: CaseIterable {
    case coffee, tea, juice
}
print("总共 \(Beverage.allCases.count) 种饮品")
for drink in Beverage.allCases {
    print("今天喝\(drink)")
}

关联值(Associated Values)

区别于原始值,关联值是把额外信息绑定到具体实例,而不是枚举定义本身。

enum Barcode {
    // UPC 一维码:四段数字
    case upc(Int, Int, Int, Int)
    // QR 二维码:任意长度字符串
    case qrCode(String)
}

// 创建实例时才真正携带值
var product = Barcode.upc(8, 85909, 51226, 3)
product = .qrCode("https://swift.org")

// switch 提取关联值
switch product {
    //case .upc(let numSystem, let manufacturer, let product, let check):
    // 简写:如果全是 let 或 var,可移到前面
case let .upc(numSystem, manufacturer, product, check):
    print("UPC: \(numSystem)-\(manufacturer)-\(product)-\(check)")
case .qrCode(let url):
    print("QR 内容: \(url)")
}

原始值(Raw Values)——“编译期就确定”

原始值与关联值互斥:

  • 原始值在定义时就写死,所有实例共用;
  • 关联值在创建时才给出,每个实例可以不同。
  1. 手动指定
enum ASCIIControl: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}
  1. 隐式自动递增 / 隐式字符串
enum PlanetInt: Int {
    case mercury = 1      // 显式从 1 开始
    case venus            // 隐式 2
    case earth            // 隐式 3
}

enum CompassString: String {
    case north            // 隐式 rawValue = "north"
    case south
}
  1. 通过 rawValue 初始化?返回的是可选值
enum PlanetInt: Int {
    case mercury = 1      // 显式从 1 开始
    case venus            // 隐式 2
    case earth            // 隐式 3
}

let possiblePlanet = PlanetInt(rawValue: 7)   // nil,因为没有第 7 颗行星
print(possiblePlanet) // nil
if let planet = PlanetInt(rawValue: 3) {
    print("第 3 颗行星是 \(planet)")   // earth
}

自定义构造器 / 计算属性 / 方法

枚举也能“长得像类”。

enum LightBulb {
    case on(brightness: Double)   // 关联值
    case off

    // 计算属性
    var isOn: Bool {
        switch self {
        case .on: return true
        case .off: return false
        }
    }

    // 实例方法
    mutating func toggle() {
        switch self {
        case .on(let b):
            self = .off
            print("从亮度 \(b) 关灯")
        case .off:
            self = .on(brightness: 1.0)
            print("开灯到默认亮度")
        }
    }
}

var bulb = LightBulb.on(brightness: 0.8)
bulb.toggle()   // 关灯
bulb.toggle()   // 开灯

递归枚举(Indirect Enumeration)

当枚举的关联值再次包含自身时,需要显式标记 indirect,让编译器插入间接层,避免无限嵌套导致内存无法布局。

// 方式 A:单个 case 递归
enum ArithmeticExpr {
    case number(Int)
    indirect case addition(ArithmeticExpr, ArithmeticExpr)
    indirect case multiplication(ArithmeticExpr, ArithmeticExpr)
}

// 方式 B:整个枚举全部 case 都递归
indirect enum Tree<T> {
    case leaf(T)
    case node(Tree<T>, Tree<T>)
}

构建与求值:把“(5 + 4) * 2”装进枚举

let five = ArithmeticExpr.number(5)
let four = ArithmeticExpr.number(4)
let two = ArithmeticExpr.number(2)

let sum = ArithmeticExpr.addition(five, four)
let product = ArithmeticExpr.multiplication(sum, two)

// 递归求值
func evaluate(_ expr: ArithmeticExpr) -> Int {
    switch expr {
    case .number(let value):
        return value
    case .addition(let left, let right):
        return evaluate(left) + evaluate(right)
    case .multiplication(let left, let right):
        return evaluate(left) * evaluate(right)
    }
}

print(evaluate(product))   // 18

实战 1:用枚举建模“JSON”

enum JSON {
    case string(String)
    case number(Double)
    case bool(Bool)
    case null
    case array([JSON])
    case dictionary([String: JSON])
}

let json: JSON = .dictionary([
    "name": .string("Swift"),
    "year": .number(2014),
    "awesome": .bool(true),
    "tags": .array([.string("iOS"), .string("macOS")])
])

优势:

  • 编译期保证类型组合合法;
  • 写解析/生成器时,switch 覆盖所有 case 即可,无需 if-else 层层判断。

实战 2:消除“字符串驱动”——网络请求路由

enum API {
    case login(user: String, pass: String)
    case userInfo(id: Int)
    case articleList(page: Int, pageSize: Int)
}

extension API {
    var host: String { "https://api.example.com" }
    
    var path: String {
        switch self {
        case .login: return "/login"
        case .userInfo(let id): return "/users/\(id)"
        case .articleList: return "/articles"
        }
    }
    
    var parameters: [String: Any] {
        switch self {
        case .login(let u, let p):
            return ["username": u, "password": p]
        case .userInfo:
            return [:]
        case .articleList(let page, let size):
            return ["page": page, "pageSize": size]
        }
    }
}

// 使用
let request = API.login(user: "alice", pass: "123456")
print("请求地址:\(request.host + request.path)")

好处:

  • 路由与参数封装在一起,外部无需硬编码字符串;
  • 新增接口只需再加一个 case,编译器会强制你补全 path & parameters。

性能与内存Tips

  1. 不带关联值的枚举 = 一个整型大小,最省内存。
  2. 关联值会占用更多空间,编译器会按最大 case 对齐;如果内存敏感,可用 indirect 将大数据挂到堆上。
  3. 原始值并不会额外占用存储,它只是编译期常量;运行时通过 rawValue 访问即可。
  4. 枚举是值类型,跨线程传递无需担心引用计数,但大体积关联值复制时要注意写时复制(CoW)开销。

给枚举加“泛型”——一个类型参数打通所有关联值

// 1. 泛型枚举:Success 与 Failure 的具体类型由使用方决定
enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

// 2. 网络层统一返回
enum APIError: Error { case timeout, invalidJSON }

func fetchUser(id: Int) -> Result<User, APIError> {
    ...
    return .success(user)
}

// 3. 调用方用 switch 就能拿到强类型的 User 或 APIError
let r = fetchUser(id: 1)
switch r {
case .success(let user):
    print(user.name)
case .failure(let error):
    print(error)
}

要点

  • 枚举可以带泛型参数,且每个 case 可使用不同参数。
  • Swift 标准库已内置 Result<Success, Failure>,无需自己写。

枚举也遵守协议——让“一组无关类型”共享行为

protocol Describable { var desc: String { get } }

enum IOAction: Describable {
    case read(path: String)
    case write(path: String, data: Data)
    
    var desc: String {
        switch self {
        case .read(let p):  return "读取 \(p)"
        case .write(let p, _): return "写入 \(p)"
        }
    }
}

let action = IOAction.write(path: "/tmp/a.txt", data: Data())
print(action.desc)

进阶:把枚举当成“小而美”的命名空间,里面再套结构体、类,一并遵守协议,可组合出非常灵活的对象图。

@unknown default —— 面向库作者的“向后兼容”保险

当模块使用 library evolutionBUILD_LIBRARY_FOR_DISTRIBUTION = YES)打开 resilient 构建时,公开枚举默认是“非冻结”的,未来可能新增 case。

客户端必须用 @unknown default: 兜底,否则升级库后会得到编译警告:

// 在 App 代码里
switch frameworkEnum {
case .oldCaseA: ...
case .oldCaseB: ...
@unknown default:        // 少了就会警告
    assertionFailure("请适配新版本 SDK")
}

冻结枚举(@frozen)则告诉编译器“以后绝对不会再加 case”,可以省略 @unknown default

System 框架里大量使用了该技巧,保证 Apple 加新枚举值时老 App 不会直接崩溃。

SwiftUI 视图工厂——用枚举消灭“字符串驱动”的 Navigation

enum Route: Hashable {
    case home
    case article(id: Int)
    case settings(debug: Bool)
}

@main
struct App: SwiftUI.App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .navigationDestination(for: Route.self) { route in
                    switch route {
                    case .home:         HomeView()
                    case .article(let id): ArticleView(id: id)
                    case .settings(let debug): SettingsView(debug: debug)
                    }
                }
        }
    }
}

优点

  • 路由表即枚举,强类型;
  • 新增 case 编译器会强制你补全对应视图;
  • 支持 NavigationStackpath 参数,可持久化/还原整棵导航树。

把枚举当“位掩码”——OptionSet 的本质仍是枚举

struct FilePermission: OptionSet {
    let rawValue: Int
    
    // 内部用静态枚举常量,对外却是结构体
    static let read   = FilePermission(rawValue: 1 << 0)
    static let write  = FilePermission(rawValue: 1 << 1)
    static let execute = FilePermission(rawValue: 1 << 2)
}

let rw: FilePermission = [.read, .write]
print(rw.rawValue)   // 3

为什么不用“纯枚举”?

  • 枚举无法表达“组合”语义;
  • OptionSet 协议要求 struct 以便支持按位或/与运算。

结论:需要位运算时,用结构体包一层 rawValue,而不是直接上枚举。

性能压测:100 万个关联值到底占多少内存?

测试模型

enum Node {
    case leaf(Int)
    indirect case node(Node, Node)
}

在 64 位下

  • leaf:实际 9 字节(1 字节区分 case + 8 字节 Int),但按 16 字节对齐。
  • node:额外存储两个指针(16 字节)+ 1 字节 tag → 24 字节对齐。

结论

  • 不带 indirect 的枚举=最省内存;
  • 大数据字段务必 indirect 挂到堆上,避免栈爆炸;
  • 如果 case 差异巨大,考虑“枚举 + 类”混合:枚举负责分派,类负责存数据。

什么时候该把“枚举”改回“结构体/类”

  1. case 数量会动态膨胀(如用户标签、城市字典)→ 用字典或数据库。
  2. 需要存储大量同质数据 → 结构体数组更合适。
  3. 需要继承/多态扩展 → 用协议 + 类/结构体。
  4. 需要弱引用、循环引用 → class + delegate 模式。

口诀:“有限状态用枚举,无限集合用集合;行为多态用协议,生命周期用类。”

一条龙完整示例:用枚举写个“小型正则表达式”引擎

indirect enum Regex {
    case literal(Character)
    case concatenation(Regex, Regex)
    case alternation(Regex, Regex)   // “或”
    case repetition(Regex)           // 闭包 *
}

// 匹配函数
extension Regex {
    func match(_ str: String) -> Bool {
        var idx = str.startIndex
        return matchHelper(str, &idx) && idx == str.endIndex
    }
    
    private func matchHelper(_ str: String, _ idx: inout String.Index) -> Bool {
        switch self {
        case .literal(let ch):
            guard idx < str.endIndex, str[idx] == ch else { return false }
            str.formIndex(after: &idx)
            return true
            
        case .concatenation(let left, let right):
            let tmp = idx
            return left.matchHelper(str, &idx) && right.matchHelper(str, &idx) || ({ idx = tmp; return false })()
            
        case .alternation(let left, let right):
            let tmp = idx
            return left.matchHelper(str, &idx) || ({ idx = tmp; return right.matchHelper(str, &idx) })()
            
        case .repetition(let r):
            let tmp = idx
            while r.matchHelper(str, &idx) { }
            return true
        }
    }
}

// 测试
let pattern = Regex.repetition(.alternation(.literal("a"), .literal("b")))
print(pattern.match("abba"))   // true

亮点

  • 纯值类型,线程安全;
  • 用枚举递归描述语法树,代码即文档;
  • 若需性能,可再包一层 JIT 或转成 NFA/DFA。

总结与扩展场景

  1. 枚举是值类型,但拥有近似类的能力。
  2. 关联值 = 运行期动态绑定;原始值 = 编译期静态绑定。
  3. switch 必须 exhaustive,借助 CaseIterable 可遍历。
  4. 可以写构造器、计算属性、方法、扩展、协议等
  5. 建模“有限状态 + 上下文”时,优先用枚举:
    • 播放器状态:.idle / .loading(url) / .playing(item, currentTime) / .paused(item, currentTime)
    • 订单状态:.unpaid(amount) / .paid(date) / .shipped(tracking) / .refunded(reason)
  6. 把“字符串魔法”改成枚举,可让编译器帮你检查漏掉的 case,减少运行时崩溃。
  7. 递归枚举天生适合表达树/表达式这类“自相似”结构,配合模式匹配写解释器极其清爽。
  8. 如果 case 太多(>100),可读性下降,可考虑:
    • 拆成多级枚举(namespace)
    • 用静态工厂方法隐藏细节
    • 改用结构体 + 协议,让“类型”退化为“数据”

checklist:如何写“优雅”的 Swift 枚举

☑ 名字首字母大写,case 小写。

☑ 先问自己“状态是否有限”,再决定用枚举还是字符串。

☑ 关联值 > 3 个字段就封装成结构体,保持 switch 整洁。

☑ 公开库一定要想好“未来会不会加 case”,决定 @frozen 与否。

☑ 超过 20 个 case 考虑分层:外层命名空间枚举,内层再拆。

☑ 需要 Codable 时,关联值枚举要自定义 init(from:) & encode(to:),否则编译器会报错。

☑ 最后写单元测试:把每个 case 都 switch 一遍,防止后续改挂。

❌
❌