阅读视图

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

DNS域名解析:从入门到优化必备基础

前言

在当今互联网世界,域名就像我们生活中的地址,而DNS(Domain Name System)就是那个将地址翻译成具体位置的神奇系统。无论你是前端开发者、移动端工程师还是运维人员,理解DNS的工作机制都至关重要。本文将从基础概念开始,逐步深入解析DNS的方方面面,并结合实际开发中的优化技巧,让你彻底掌握域名解析的艺术。

一、DNS解析的基本流程

1.1 传统DNS解析过程

当你在浏览器中输入 www.example.com 并按下回车时,背后发生了什么?

用户输入域名 → 浏览器缓存 → 操作系统缓存 → 路由器缓存 → ISP DNS服务器 → 递归查询 → 返回IP地址

具体步骤:

  1. 浏览器缓存检查:现代浏览器会缓存DNS记录一段时间
  2. 操作系统缓存:如果浏览器没有缓存,系统会检查自己的DNS缓存
  3. 路由器缓存:家庭或办公路由器也可能缓存DNS记录
  4. ISP DNS服务器:互联网服务提供商的DNS服务器进行递归查询
  5. 递归查询过程
    • 根域名服务器(返回.com顶级域服务器地址)
    • 顶级域名服务器(返回example.com权威服务器地址)
    • 权威域名服务器(返回www.example.com的IP地址)

1.2 iOS应用中的DNS解析

在iOS开发中,当使用URLSession发起网络请求时:

// iOS默认使用系统DNS解析
let url = URL(string: "https://api.example.com")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
    // 处理响应
}
task.resume()

iOS系统会自动处理DNS解析,开发者通常无需关心具体过程。但从iOS 15开始,我们可以通过NWParametersexpiredDNSBehavior属性来控制DNS记录的过期行为:

import Network

let parameters = NWParameters.tcp
if #available(iOS 15.0, *) {
    // 配置DNS记录过期行为
    parameters.expiredDNSBehavior = .reloadFromOrigin
    // .allow: 允许使用过期记录(默认)
    // .reloadFromOrigin: 强制重新查询
    // .reloadFromOriginAndFailIfNotAvailable: 必须获取最新记录
}

二、网络请求的完整过程:DNS解析之后

DNS解析完成后,真正的网络通信才刚刚开始:

2.1 TCP连接建立(三次握手)

客户端 → 服务器: SYN (seq=x)
服务器 → 客户端: SYN-ACK (seq=y, ack=x+1)
客户端 → 服务器: ACK (seq=x+1, ack=y+1)

为什么重新连接也需要三次握手? 无论是首次连接还是重新连接,TCP都需要三次握手来确保:

  • 双方都能正常通信
  • 序列号同步
  • 防止旧的重复连接请求

2.2 IP网络选路

这个重要的步骤发生在DNS解析之后、建立TCP连接之前。数据包需要经过多个路由器(跳)才能到达目标服务器:

客户端 → 本地路由器 → ISP网络 → 互联网骨干网 → 目标服务器

优化空间

  • 使用CDN减少路由跳数
  • 部署Anycast技术自动路由到最近节点
  • 优化MTU避免数据包分片

2.3 TLS握手(HTTPS请求)

Client Hello → Server Hello → 证书验证 → 密钥交换 → 加密通信开始

TLS 1.3的优势

  • 减少握手步骤
  • 支持0-RTT(零往返时间)恢复会话
  • 更强的加密算法

2.4 HTTP协议演进

HTTP/1.1 → HTTP/2 → HTTP/3的改进:

特性 HTTP/1.1 HTTP/2 HTTP/3
多路复用 ❌ 不支持 ✅ 支持 ✅ 支持
头部压缩 ❌ 不支持 ✅ HPACK ✅ QPACK
传输协议 TCP TCP QUIC(UDP)
队头阻塞 连接级别 流级别 ❌ 无
连接迁移 ❌ 不支持 ❌ 不支持 ✅ 支持

三、性能优化实战

3.1 减少DNS解析时间

iOS中的DNS预解析

// HTML中的DNS预取(WebView场景)
let html = """
<!DOCTYPE html>
<html>
<head>
    <link rel="dns-prefetch" href="//cdn.example.com">
</head>
<body>...</body>
</html>
"""

// 或使用Network Framework进行预连接
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
    if path.status == .satisfied {
        // 网络可用时预连接
        let connection = NWConnection(host: "api.example.com", port: 443, using: .tls)
        connection.start(queue: .global())
    }
}

3.2 处理DNS解析失败

在Alamofire中判断DNS解析失败:

import Alamofire

extension AFError {
    var isDNSError: Bool {
        if case .sessionTaskFailed(let underlyingError) = self {
            if let urlError = underlyingError as? URLError {
                return urlError.code == .cannotFindHost || 
                       urlError.code == .dnsLookupFailed
            } else if let nsError = underlyingError as? NSError {
                return nsError.domain == NSURLErrorDomain && 
                      (nsError.code == NSURLErrorCannotFindHost || 
                       nsError.code == NSURLErrorDNSLookupFailed)
            }
        }
        return false
    }
}

// 使用示例
AF.request("https://api.example.com").response { response in
    if let error = response.error as? AFError, error.isDNSError {
        print("DNS解析失败,尝试备用方案")
        // 切换到备用域名或HTTPDNS
    }
}

3.3 使用HTTPDNS

HTTPDNS通过HTTP协议直接查询DNS,避免传统DNS的污染和劫持:

// 示例:使用阿里云HTTPDNS
func resolveWithHTTPDNS(domain: String, completion: @escaping (String?) -> Void) {
    let url = URL(string: "http://203.107.1.1/100000/d?host=\(domain)")!
    URLSession.shared.dataTask(with: url) { data, _, _ in
        if let data = data, let ip = String(data: data, encoding: .utf8) {
            completion(ip.trimmingCharacters(in: .whitespacesAndNewlines))
        } else {
            completion(nil)
        }
    }.resume()
}

// 使用解析的IP直接建立连接
resolveWithHTTPDNS(domain: "api.example.com") { ip in
    guard let ip = ip else { return }
    var request = URLRequest(url: URL(string: "https://\(ip)/endpoint")!)
    request.setValue("api.example.com", forHTTPHeaderField: "Host") // 关键:设置Host头部
    AF.request(request).response { response in
        // 处理响应
    }
}

四、高级主题:协议层面的优化

4.1 QUIC与HTTP/3

HTTP/3基于QUIC协议,带来了革命性的改进:

QUIC的核心特性

// QUIC解决了TCP的队头阻塞问题
// 传统TCP:一个数据包丢失会阻塞整个连接
// QUIC:每个流独立,丢包只影响当前流

// 在iOS中,HTTP/3会自动启用(如果服务器支持)
// 从iOS 15开始,URLSession默认支持HTTP/3
let configuration = URLSessionConfiguration.default
if #available(iOS 13.0, *) {
    // 允许使用"昂贵"的网络(如蜂窝数据)
    configuration.allowsExpensiveNetworkAccess = true
    
    // 允许使用"受限"的网络(如低数据模式)
    configuration.allowsConstrainedNetworkAccess = true
}
let session = URLSession(configuration: configuration)

4.2 队头阻塞问题详解

TCP的队头阻塞

# 假设发送了3个数据包
packets = ["Packet1", "Packet2", "Packet3"]

# 如果Packet2丢失
# 即使Packet3已到达,接收端也必须等待Packet2重传
# 这就是TCP层的队头阻塞

HTTP/2的队头阻塞

  • 虽然HTTP/2支持多路复用,但仍基于TCP
  • TCP层的丢包会影响所有HTTP/2流

HTTP/3的解决方案

  • 基于UDP,每个QUIC流独立
  • 一个流的丢包不会影响其他流

4.3 网络性能监控

监控DNS解析时间

import Foundation

class NetworkMonitor {
    func performRequestWithMetrics(urlString: String) {
        guard let url = URL(string: urlString) else { return }
        
        let configuration = URLSessionConfiguration.default
        let session = URLSession(configuration: configuration)
        
        let task = session.dataTask(with: url) { data, response, error in
            if let error = error {
                print("请求失败: \(error)")
                return
            }
            
            print("请求成功")
        }
        task.delegate = task.delegate // 保留引用以获取metrics
        // 监听任务完成
        if #available(iOS 10.0, *) {
            // 在任务完成后获取指标
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                self.printMetrics(for: task)
            }
        }
        
        task.resume()
    }
    
    @available(iOS 10.0, *)
    private func printMetrics(for task: URLSessionTask) {
        task.getMetrics { metrics in
            guard let metrics = metrics else { return }
            
            // 分析时间线
            let transactionMetrics = metrics.transactionMetrics
            
            for metric in transactionMetrics {
                print("=== 请求指标分析 ===")
                print("URL: \(metric.request.url?.absoluteString ?? "N/A")")
                
                // DNS查询时间
                if let domainLookupStart = metric.domainLookupStartDate,
                   let domainLookupEnd = metric.domainLookupEndDate {
                    let dnsTime = domainLookupEnd.timeIntervalSince(domainLookupStart)
                    print("DNS解析时间: \(String(format: "%.3f", dnsTime * 1000))ms")
                } else {
                    print("DNS解析时间: 使用缓存或无法测量")
                }
                
                // TCP握手时间
                if let connectStart = metric.connectStartDate,
                   let connectEnd = metric.connectEndDate {
                    let tcpTime = connectEnd.timeIntervalSince(connectStart)
                    print("TCP连接时间: \(String(format: "%.3f", tcpTime * 1000))ms")
                }
                
                // TLS握手时间
                if let secureStart = metric.secureConnectionStartDate,
                   let secureEnd = metric.secureConnectionEndDate {
                    let tlsTime = secureEnd.timeIntervalSince(secureStart)
                    print("TLS握手时间: \(String(format: "%.3f", tlsTime * 1000))ms")
                }
                
                // 总时间
                if let fetchStart = metric.fetchStartDate,
                   let responseEnd = metric.responseEndDate {
                    let totalTime = responseEnd.timeIntervalSince(fetchStart)
                    print("总请求时间: \(String(format: "%.3f", totalTime * 1000))ms")
                }
                
                // 网络协议
                print("网络协议: \(metric.networkProtocolType ?? "unknown")")
                print("是否代理连接: \(metric.isProxyConnection)")
                print("是否重用连接: \(metric.isReusedConnection)")
            }
        }
    }
}

// 使用示例
let monitor = NetworkMonitor()
monitor.performRequestWithMetrics(urlString: "https://httpbin.org/get")

五、移动端开发最佳实践

5.1 iOS中的网络优化

使用合适的缓存策略

let configuration = URLSessionConfiguration.default

// 设置根据情况合理的缓存策略
configuration.requestCachePolicy = .useProtocolCachePolicy
configuration.urlCache = URLCache(
    memoryCapacity: 50 * 1024 * 1024,  // 50MB内存缓存
    diskCapacity: 500 * 1024 * 1024,   // 500MB磁盘缓存
    diskPath: "CustomCache"
)

// 配置连接限制(iOS 11+)
if #available(iOS 11.0, *) {
    configuration.httpMaximumConnectionsPerHost = 6
}

处理网络切换

import Network

class NetworkManager {
    private let monitor = NWPathMonitor()
    private var currentPath: NWPath?
    
    func startMonitoring() {
        monitor.pathUpdateHandler = { [weak self] path in
            self?.currentPath = path
            
            if path.status == .satisfied {
                // 网络可用
                if path.usesInterfaceType(.wifi) {
                    print("切换到WiFi")
                } else if path.usesInterfaceType(.cellular) {
                    print("切换到蜂窝网络")
                }
                
                // 网络切换时清除DNS缓存
                self?.clearDNSCache()
            }
        }
        monitor.start(queue: .global())
    }
    
    private func clearDNSCache() {
        // 注意:iOS没有直接清除DNS缓存的API
        // 可以通过以下方式间接触发刷新:
        // 1. 重新创建URLSession
        // 2. 使用新的NWParameters
        // 3. 等待系统自动刷新(通常很快)
    }
}

5.2 错误处理与重试机制

智能重试策略

import Alamofire

final class NetworkService {
    private let session: Session
    
    init() {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = 30
        
        // 配置重试策略
        let retryPolicy = RetryPolicy(
            retryLimit: 3,
            exponentialBackoffBase: 2,
            exponentialBackoffScale: 0.5
        )
        
        session = Session(
            configuration: configuration,
            interceptor: retryPolicy
        )
    }
    
    func requestWithRetry(_ url: String) {
        session.request(url)
            .validate()
            .responseDecodable(of: ResponseType.self) { response in
                switch response.result {
                case .success(let data):
                    print("请求成功: \(data)")
                case .failure(let error):
                    if let afError = error.asAFError,
                       afError.isSessionTaskError,
                       let urlError = afError.underlyingError as? URLError {
                        
                        switch urlError.code {
                        case .cannotFindHost, .dnsLookupFailed:
                            print("DNS错误,尝试备用域名")
                            self.tryBackupDomain(url)
                        case .notConnectedToInternet:
                            print("网络未连接")
                        case .timedOut:
                            print("请求超时")
                        default:
                            print("其他网络错误: \(urlError)")
                        }
                    }
                }
            }
    }
    
    private func tryBackupDomain(_ originalUrl: String) {
        // 实现备用域名逻辑
        let backupUrl = originalUrl.replacingOccurrences(
            of: "api.example.com",
            with: "api-backup.example.com"
        )
        session.request(backupUrl).response { _ in }
    }
}

六、安全考量

6.1 DNS安全威胁

常见的DNS攻击

  1. DNS劫持:篡改DNS响应,指向恶意服务器
  2. DNS污染:缓存投毒,传播错误记录
  3. DNS放大攻击:利用DNS服务器进行DDoS

防护措施

// 使用HTTPS防止中间人攻击
let configuration = URLSessionConfiguration.default

// 启用ATS(App Transport Security)
// iOS默认要求HTTPS,可在Info.plist中配置例外
/*
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
    <key>NSExceptionDomains</key>
    <dict>
        <key>example.com</key>
        <dict>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>
*/

// 证书锁定(Certificate Pinning)
let serverTrustPolicies: [String: ServerTrustEvaluating] = [
    "api.example.com": PinnedCertificatesTrustEvaluator()
]

let session = Session(
    serverTrustManager: ServerTrustManager(evaluators: serverTrustPolicies)
)

6.2 隐私保护

减少DNS泄露

// 使用本地DNS解析
import dnssd

// 或使用加密的DNS(DNS over TLS/HTTPS)
let parameters = NWParameters.tls
if #available(iOS 14.0, *) {
    // 配置加密DNS
    let options = NWProtocolTLS.Options()
    // 设置DNS over TLS
}

总结

DNS域名解析是互联网通信的基石,理解其工作原理和优化策略对于构建高性能应用至关重要。从传统的递归查询到现代的HTTPDNS,从TCP的三次握手到QUIC的零往返连接,网络技术正在不断演进。

关键要点

  1. 理解完整流程:DNS解析只是开始,后续还有TCP握手、TLS协商等步骤
  2. 选择合适协议:根据场景选择HTTP/2或HTTP/3
  3. 实施智能优化:使用预解析、HTTPDNS、连接复用等技术
  4. 处理边界情况:网络切换、DNS失败、高延迟环境
  5. 重视安全隐私:防止DNS劫持,保护用户数据

通过本文的深入解析,希望你能掌握DNS域名解析的全貌,并在实际开发中应用这些优化技巧,打造更快、更稳定、更安全的网络应用。


下一篇预告:我们将深入探讨HTTP/3和QUIC协议,解析其如何彻底解决队头阻塞问题,以及在实际项目中的部署实践。

# 老司机 iOS 周报 #361 | 2025-12-29

ios-weekly
老司机 iOS 周报,只为你呈现有价值的信息。

你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。

文章

🐕 Exploring interactive snippet intents

@BluesJiang: 这篇文章主要探索了一下 App Intent 框架。苹果在 WWDC25 上引入了 App Intent 的可交能力,在 Widget、App Shortcut、Intent 中都可以使用。作者探索了这个 App Intent 的交互框架和编码逻辑,旨在了解这个交互框架可以做什么,不可以做什么,交互分范式是什么样的。
这个框架使用 SwiftUI 编码,但是交互逻辑与方式则有很大的不同,在 App Intent 框架下,不存在传统生命式框架下的状态和交互变化,甚至按钮的触发事件也不是直接的,而是间接通过注册的 Intent 来完成响应。
如果有需要在 App 外做即时响应的功能,可以考虑研究一下。

🐎 使用 "git mv" 命令记录 Git 中文件名的大小写更改

@含笑饮砒霜:这篇文章主要介绍了在 macOS 和 Windows 默认的大小写不敏感但保留大小写的文件系统中,直接修改文件名大小写时 Git 不会记录该名称变更,可能导致文件系统与 Git 存储的文件名不一致,进而引发后续使用(如跨大小写敏感文件系统、CI 打包)的问题,同时给出解决方案:使用 git mv 命令记录文件名大小写变更,若不便使用该命令,可通过 “先重命名为临时名称、再改为目标名称” 的两阶段提交方式实现同样效果。

🐎 Swift Configuration 1.0 released

@AidenRao:Swift Configuration 1.0 的正式发布。该项目旨在为 Swift 应用提供一套统一的配置管理方案,帮助开发者优雅地处理来自环境变量、配置文件乃至远程服务的各类配置项。通过它,我们可以告别过去分散繁琐的配置逻辑,以更清晰、安全和可维护的方式构建应用。

🐎 Using associated domains alternate mode during development

@DylanYang:作者向我们介绍了如何在调试 AASA(apple-app-site-association) 相关能力时,通过开发者模式使域名相关的改动可以即时的被同步到。开发者模式需要我们在对应域名上加上特定后缀,并且只对开发模式的签名文件生效。有调试相关能力需求的开发者可以参考一下。

🐢 Command Line Interface Guidelines

@zhangferry:这篇文章是一份开源的《命令行界面(CLI)设计指南》,核心目标是结合传统 UNIX 原则与现代需求,帮助开发者打造更易用、更友好的 CLI 程序。虽然现在 GUI 非常普及,但 CLI 以其灵活、稳定、跨平台的优势在很多场景(例如 DevOps)都在放光发热。所以了解如何更好的设计 CLI 仍有必要,以下是从文章内挑选的几条重要设计指南:

  • 基础规范:使用对应语言的命令行参数解析库,Swift 下是 swift-argument-parser;成功时返回 0,失败返回非 0;核心输出到 stdout(支持管道传递),日志,错误信息输出到 stderr(避免干扰管道)
  • 帮助和文档:默认运行无参数时显示简洁的帮助,-h/--help 对应完整的帮助说明。
  • 输出设计:人类可读最重要,如果为了人类可读破坏了机器可读,可以增加 --plain 参数输出机器可读内容,这有利于 grep、awk 工具的集成
  • 错误处理:避免冗余输出,核心错误应该放在末尾
  • 参数和标志:优先使用 flags,而不是依赖位置读参数;所有 flags 都提供短格式和长格式两种(-h/--help);危险操作增加一个保护措施:输入名称、--force 标志等
  • 健壮性与兼容性:及时响应用户的输入(100ms 以内),如果流程耗时增加进度反馈(进度条)
  • 环境变量:避免占用 POSIX 标准变量;本地用 .env 管理但不应把 .env 当做配置文件;不要使用环境变量存储密钥等重要信息,这样很容易泄漏,推荐通过文件或密钥管理服务

🐕 SwiftUI Group Still(?) Considered Harmful

@Damien:本文指出 SwiftUI 的 Group 会把修饰符“分发”给每个子视图,曾让 onAppear 被多次触发。onAppear/task 虽被苹果特殊处理,但文档未改,且自定义修饰符与在 List 内仍照分发。解决方案为:除非必须一次性给兄弟视图统一加修饰符,否则别用 Group,直接重复代码或拆视图更稳妥。

代码

🐢 SwiftAgents

@阿权:SwiftAgents 为 Swift 开发者提供了一套现代化、类型安全、并发友好的 AI Agent 开发框架,兼具强大的功能与优雅的 API 设计,适合在苹果全平台及 Linux 上构建下一代智能应用。

实现能力:

  • Agent 框架:支持 ReAct、PlanAndExecute、ToolCalling 等多种推理模式
  • 灵活内存系统:包含对话内存、滑动窗口、摘要记忆及可插拔持久化后端
  • 类型安全工具:通过 @Tool@Parameter 宏大幅减少样板代码
  • 多代理编排:支持监督者-工作者模式、并行执行与智能路由
  • 全平台支持:兼容 iOS 17+、macOS 14+、Linux(Ubuntu 22.04+)
  • 强并发安全:基于 Swift 6.2 的 Actor 隔离与 Sendable 类型
  • 可观测性与弹性:内置日志追踪、指标收集、重试策略与熔断器

适用场景:

  • 对话式 AI 助手
  • 自动化任务执行与决策流程
  • 多 Agent 协同分析系统
  • 需要持久化记忆与工具调用的复杂应用

🐕 XcodeBuildMCP 1.15.0 released

@Cooper Chen:XcodeBuildMCP 是一个基于 Model Context Protocol(MCP)的开源工具,将 Xcode 的构建、运行与模拟器能力以标准化接口暴露给 AI Agent,使其能够真正参与 iOS / macOS 的开发流程。开发者只需在首次调用时设置好 project、simulator 和 scheme,之后的每一次调用都可以直接复用配置,“一次设定,次次生效”。

这一设计显著降低了上下文和参数负担:

  • 上下文占用减少 24.5%(smaller context footprint)
  • 每次调用所需参数更少(fewer params per call)

对于依赖 AI 自动编译、跑测试、定位问题的场景而言,这意味着更低的 Token 消耗、更稳定的 Agent 行为,以及更高效的工具调用体验。XcodeBuildMCP 是连接 Xcode 与 AI 工作流的关键基础设施,尤其适合构建长期、可持续的智能开发系统。

音视频

🐕 CS193 Stanford 2025

@极速男孩:这是是斯坦福大学计算机科学系著名的公开课程 CS193p: Developing Applications for iOS(iOS 应用程序开发)。主要涵盖最新的 iOS SDK 特性。根据网站最新信息(Spring 2025 版本),内容包括 Xcode 的使用、SwiftUI 的视图与修饰符、Swift 类型系统、动画、数据持久化(SwiftData)以及多线程等。

内推

重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考

具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)

关注我们

我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参

同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom

说明

🚧 表示需某工具,🌟 表示编辑推荐

预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)

Flutter限制输入框只能输入中文,iOS拼音打不出来?

中文输入必踩的 Flutter 坑合集:iOS 拼音打不出来,其实是你 Formatter 写错了

如果你在 Flutter 里做过「只允许中文 / 中英文校验」,并且只在 iOS 上翻过车,那这篇文章大概率能帮你节省半天 Debug 时间。

这不是 iOS 的锅,也不是 Flutter 的 Bug,而是 TextInputFormatter 和中文输入法(IME)之间的理解偏差


一、血iOS 上拼音怎么都打不出来

常见反馈包括:

  • iOS 中文拼音键盘
  • 输入 bei jing
  • 键盘有拼音显示
  • 输入框内容完全不变
  • 无法选词、无法上屏

👉 Android 正常
👉 模拟器正常
👉 真机 iOS 不行

很多人第一反应是:
“Flutter 对中文支持不好?”

结论先行:不是。


二、罪魁祸首:TextInputFormatter 的「中文校验」

下面这种 Formatter,你一定写过或见过:

class NameInputFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    final chineseOnly = RegExp(r'^[\u4E00-\u9FFF]+$');

    if (newValue.text.isEmpty) return newValue;

    if (!chineseOnly.hasMatch(newValue.text)) {
      return oldValue; // 
    }

    return TextEditingValue(
      text: newValue.text,
      selection: TextSelection.collapsed(
        offset: newValue.text.length,
      ),
    );
  }
}

逻辑看起来非常合理:

  • 只允许中文
  • 非法字符直接回退

但在 iOS 上,这段代码等于封死了中文输入法的入口


三、核心原理:iOS 中文输入法有「组字阶段」

1️ composing 是什么?

iOS 拼音输入法的输入过程分为两步:

  1. 组字(composing)

    • 输入:bei
    • 输入框里是拼音(未确认)
  2. 提交

    • 选择「北」
    • 中文字符真正上屏

在组字阶段:

newValue.text == "bei"
newValue.composing.isCollapsed == false

"bei" 必然无法通过「只允许中文」的正则校验


2️ Formatter 提前“否决”了输入

当 Formatter 在 composing 阶段做了以下任意一件事:

  • return oldValue
  • 修改 text
  • 强制重置 selection

iOS 输入法就会认为:
「当前输入不合法,终止组字」

于是出现经典现象:

拼音能打,但永远无法选字


四、隐藏更深的坑:selection 会杀死输入法

很多 Formatter 里都有这行:

selection: TextSelection.collapsed(offset: text.length),

在普通输入下没问题,但在中文输入中:

  • selection 是 IME 状态的一部分
  • 每次重置 selection = 重启组字流程

哪怕你放行了拼音,也可能出现:

  • 候选词异常
  • 游标跳动
  • 输入体验极差

五、那为什么 Android 没这个问题?

这是一个非常关键、也最容易误判的点

Android 的行为差异

  • Android 输入法对 composing 的暴露不一致
  • 很多键盘在 字符提交后才触发 Formatter
  • 即使 composing 存在,也更“宽容”

结果就是:

错误的 Formatter 在 Android 上“看起来能用”

但这并不代表代码是对的,只是 Android 没那么严格

真相

Android 是侥幸没炸,iOS 是严格把问题暴露出来。


六、正确原则

1. composing 阶段必须放行

if (!newValue.composing.isCollapsed) {
  return newValue;
}

2. 校验只在 composing 结束后做

3. 不要无脑重置 selection

4. Formatter ≠ 表单最终校验


七、正确示例

下面是一个安全、可扩展、iOS / Android 双端稳定的 Formatter 示例:

class UniversityNameInputFormatter extends TextInputFormatter {
  UniversityNameInputFormatter({this.maxLength = 40});

  final int maxLength;

  static final RegExp _disallowed =
      RegExp(r'[^a-zA-Z0-9\u4E00-\u9FFF-\s]');
  static final RegExp _multiHyphen = RegExp(r'-{2,}');
  static final RegExp _leadingHyphen = RegExp(r'^-+');
  static final RegExp _trailingHyphen = RegExp(r'-+$');

  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    // iOS 中文拼音组字阶段
    if (!newValue.composing.isCollapsed) {
      return newValue;
    }

    var text = newValue.text;
    if (text.isEmpty) return newValue;

    text = text.replaceAll(_disallowed, '');
    text = text.replaceAll(_multiHyphen, '-');
    text = text.replaceAll(_leadingHyphen, '');
    text = text.replaceAll(_trailingHyphen, '');

    if (text.length > maxLength) {
      text = text.substring(0, maxLength);
    }

    if (text == newValue.text) return newValue;

    int clamp(int o) => o.clamp(0, text.length);

    return TextEditingValue(
      text: text,
      selection: TextSelection(
        baseOffset: clamp(newValue.selection.baseOffset),
        extentOffset: clamp(newValue.selection.extentOffset),
      ),
      composing: TextRange.empty,
    );
  }
}

八、中文输入必踩的 Flutter 坑合集(Checklist)

❌ 坑 1:Formatter 里直接做中文正则校验

后果:iOS 拼音无法输入

❌ 坑 2:忽略 newValue.composing

后果:IME 组字被打断

❌ 坑 3:每次都把 selection 移到末尾

后果:候选词异常、游标乱跳

❌ 坑 4:以为 Android 正常 = 代码正确

后果:iOS 真机翻车


九、一句话总结

TextInputFormatter 是 IME 输入流程的一部分,不是简单的字符串过滤器。

读《疯狂的尿酸》

《疯狂的尿酸》是一本关于健康的科普书,来自于美国医学博士:戴维·珀尔马特,他是一位畅销书作家,写过《谷物大脑》和《菌群大脑》。

什么是尿酸

正常人体中的尿酸,2/3 是内源性的。尿酸是嘌呤的代谢产物,而嘌呤是细胞的重要组成部分,可以用来合成 DNA 和 RNA,人类的细胞因为不停地在分裂和衰老,死亡的细胞在被处理的时候就会产生尿酸。

另外 1/3 的尿酸来自于外部摄入的食物,包括动物内脏,海鲜,啤酒等。

果糖是一种特别的糖,它虽然不会造成血糖上升,但是会在代谢的时候产生尿酸。

尿酸会促进脂肪的产生

因为高尿酸与肥胖相关性很高,为了研究他们之间的因果关系,人们发现了“尿酸氧化酶”。这是一种存在于大多数动物体内的酶,能够迅速将尿酸排出体外,但是我们的人类祖先在几百万年的进化过程中,产生这个酶的基因被破坏了,变成了“假基因”。这就使得我们人类血液中的尿酸含量是其他哺乳动物的 3-10 倍。

当远古时代的人类吃下果糖后,果糖会在代谢过程中产生尿酸,而尿酸会打开人体的“脂肪开关”,帮助人体把果糖转化为脂肪。“从水果到脂肪”的生理机制帮助古代的灵长类动物能够度过漫长的、食物匮乏的冬天。

果糖

果糖是所有天然的碳水化合物中最甜的一种,天然的果糖只存在于水果和蜂蜜中,所以人类摄入得很少。而且水果中富含膳食纤维,可以延缓果糖被吸收的速度;而水果中富含的维生素 C 还有降低尿酸及促进尿酸排出的功能,所以吃水果对果糖的提升是很低的,代谢产生的尿酸也很少。

纯葡萄糖和果糖都是单糖(糖的最简单形式),而蔗糖是葡萄糖和果糖的组合,是一种双糖(两个分子连接在一起)。蔗糖进入人体后在小肠被分解,释放果糖和葡萄糖,然后被吸收。

果葡糖浆是一种以果糖为主的糖浆制品,果糖占比约 55%,葡萄糖占比 42%。最早是 1957 年由美国生物化学家 理查德·O 马歇尔 和 厄尔·R 科伊 生产出来,他们创造了一种酶,可以通过化学方法使玉米糖浆中的葡萄糖的结构重新排列,将其转化为果糖。

果葡糖浆从 20 世纪 70 年代开始流行,主要是因为其甜度比蔗糖高,价格又比蔗糖低,所以逐渐取代了蔗糖。到了 1984 年,可口可乐和百事可乐也都把各自品牌的饮料从添加蔗糖改为添加果葡糖浆。

果糖的升糖指数是所有天然糖中最低的,这意味着它不会直接导致血糖升高,也就不会刺激胰岛素的分泌,所以在一段时间内,人们把果糖视为一种“更安全”和“健康”的糖。但后来人们发现,相比于葡萄糖参与能量生成,果糖则参与能量储存,所以更容易让人肥胖。

果糖的代谢过程

果糖和葡萄糖除了一些化学键不同,其他结构几乎完全一样。然后,正是这微小的差异使得它们的代谢过程完全不同。

葡萄糖代谢的第一步(葡萄糖的磷酸化)是在葡萄糖激酶催化下分解,分解所释放的 ATP 也会在细胞中维持稳定的水平。ATP(三磷酸腺苷)是人体能量的来源。

果糖的代谢与葡萄糖完全不同。果糖在进入人体后,会迅速被血液吸收,然后被运输到肝脏中进行代谢。在肝细胞内,果糖激酶会开始工作,做出包括消耗 ATP 在内的一系列事情。果糖会消耗 ATP 的过程会带来一些下游效应,它会导致血液中的尿酸水平快速上升。由于果糖消耗了 ATP,细胞会发出信号:我们的能量快用完了。这会促使身体减缓新陈代谢以减少静息能量消耗。

除了消耗能量外,果糖还会触发脂肪的生成过程:肝脏中的果糖代谢会直接导致脂肪的产生:主要是以甘油三酯的形式存在,这是人体中最常见的脂肪存在形式。

AMP 活化蛋白激酶

AMP 活化蛋白激酶被激活时,它会向你的身体发出“狩猎状况良好”(即食物充足)的信号,你的身体就会让自己从储存脂肪转换为燃烧脂肪,帮助身体保持良好的狩猎状态。

AMP 活化蛋白激酶还可以帮助身体减少葡萄糖生成。二甲双胍就利用了这一点来实现降血糖。

与AMP 活化蛋白激酶对应的,还有一种让身体储存脂肪的酶,叫做腺苷单磷酸脱氨酶 2。动物在准备冬眠的时候,就会激活腺苷单磷酸脱氨酶 2 用于储存脂肪;在冬眠的时候,则切换到AMP 活化蛋白激酶用于燃烧脂肪。

而果糖代谢过程产生的尿酸,就是这两种酶的调节剂,尿酸能够抑制AMP 活化蛋白激酶,同时激活腺苷单磷酸脱氨酶 2 。

断食

作者推荐大家可以尝试 24 小时的断食,即:24 小时内不吃任何东西,且大量饮水。如果正在服用药物,务必继续服用。

我也见过一种 16:8 的轻断食方法:即 16 小时断食,8 小时进食。通常时间设置为中午 12 点-下午 8 点,或者上午 10 点到晚 6 点。

小结

本书主要揭示了果糖和尿酸在人体代谢中的核心原理,让我们更加关注饮食和内分泌的健康。

深入理解 UITabBarController:代理方法与 ViewController 生命周期的执行顺序(含 UINavigationController 场景)

在 iOS 开发中,UITabBarController 是构建多 Tab 应用的标准容器。但很多开发者对以下问题仍模糊不清:

  • tabBarController:shouldSelectViewController:didSelectViewController: 到底在什么时候调用?
  • tabBarController.selectedViewController 何时发生变化
  • 如果每个 Tab 都是 UINavigationController,代理拿到的是谁?生命周期又由谁接收?
  • 如何准确获取“切换前”和“切换后”的业务控制器

本文将通过精确的时间线 + 状态快照 + 实战代码,彻底厘清 UITabBarController 在用户点击 Tab 时的完整执行链,助你写出精准、健壮的导航逻辑。


🔑 核心概念:selectedViewController 的变化时机

UITabBarController 有一个关键属性:

@property(nullable, nonatomic, weak) UIViewController *selectedViewController;

它的值不是在切换开始时变,而是在切换完成后才更新。这一点直接决定了你在代理中能拿到什么。

结论先行

  • shouldSelectViewController: 中,selectedViewController 仍是旧的控制器
  • didSelectViewController: 中,selectedViewController 已等于新控制器
  • 真正的赋值发生在 ViewController 转场完成之后、didSelect 调用之前

这个特性,是实现“记录切换来源”的关键!


📋 一、标准执行流程(从 Tab A → Tab B)

假设:

  • 当前选中的是 A 控制器
  • 用户点击 Tab,切换到 B 控制器
  • 所有 view 已加载(非首次)
  • tabBarController.delegate = self

✅ 完整执行顺序 + selectedViewController 快照

步骤 方法 / 事件 selectedViewController 的值 说明
1 shouldSelectViewController:(B) A 可拦截切换;此时 B 尚未激活
2 A.viewWillDisappear: A A 即将消失
3 B.viewWillAppear: A B 即将出现,但 TabBar 仍认为 A 是当前页
4 A.viewDidDisappear: A A 已消失
5 B.viewDidAppear: A B 已显示,但 selectedViewController 仍未更新
6 内部赋值 B UIKit 私有逻辑:selectedViewController = B
7 didSelectViewController:(B) B 切换完成,可安全使用新控制器

💡 重点:selectedViewController 的变更发生在 viewDidAppear: 之后、didSelect... 之前

这意味着:

  • 不能viewDidAppear: 中通过 tabBarController.selectedViewController 判断“是否刚被选中”(因为它还是旧的!)
  • 但你可以shouldSelect... 中通过 selectedViewController 获取“切换前”的控制器

📦 二、当 Tab 中是 UINavigationController 时

这是最常见架构:

TabBarController
├── UINavigationController (rootVC of Tab 0) → HomeVC
└── UINavigationController (rootVC of Tab 1) → ProfileVC

🔄 执行流程是否改变?

否!顺序完全一致,但对象类型不同:

阶段 普通 VC 场景 UINavigationController 场景
shouldSelect... 参数 HomeVC UINavigationController
selectedViewController HomeVC UINavigationController
生命周期接收者 HomeVC HomeVC(Nav 的 topViewController)
didSelect... 参数 HomeVC UINavigationController

✅ 示例:代理中的日志

- (BOOL)tabBarController:(UITabBarController *)tc shouldSelectViewController:(UIViewController *)toRoot {
    UIViewController *fromRoot = tc.selectedViewController; // 仍是旧的 Nav
    NSLog(@"[shouldSelect] from: %@, to: %@", 
          NSStringFromClass([fromRoot class]), 
          NSStringFromClass([toRoot class]));
    // 输出:from: UINavigationController, to: UINavigationController
    return YES;
}

而与此同时,HomeVC 会正常收到 viewWillAppear:,因为 UINavigationController 会自动将生命周期传递给其 topViewController。


🛠 三、如何正确获取“业务控制器”?

由于 delegate 拿到的是 root VC(可能是 Nav),我们需要“穿透”一层。

✅ 推荐工具方法:

- (UIViewController *)businessViewControllerFromTabRoot:(UIViewController *)root {
    if ([root isKindOfClass:[UINavigationController class]]) {
        return [(UINavigationController *)root topViewController];
    }
    return root;
}

应用于代理:

- (BOOL)tabBarController:(UITabBarController *)tc shouldSelectViewController:(UIViewController *)toRoot {
    UIViewController *fromBusiness = [self businessViewControllerFromTabRoot:tc.selectedViewController];
    UIViewController *toBusiness   = [self businessViewControllerFromTabRoot:toRoot];
    
    NSLog(@"从 %@ 切换到 %@", 
          NSStringFromClass([fromBusiness class]), 
          NSStringFromClass([toBusiness class]));
    
    // 缓存“切换前”用于 didSelect
    self.previousBusinessVC = fromBusiness;
    
    return YES;
}

- (void)tabBarController:(UITabBarController *)tc didSelectViewController:(UIViewController *)toRoot {
    UIViewController *toBusiness = [self businessViewControllerFromTabRoot:toRoot];
    UIViewController *fromBusiness = self.previousBusinessVC;
    
    [Analytics logTabSwitchFrom:fromBusiness to:toBusiness];
}

⚠️ 注意:不要在 didSelect... 中再读 tc.selectedViewController 来获取“from”,因为它已经是 to 了!


⚠️ 四、特殊场景深度解析

1. 重复点击当前 Tab(A → A)

  • shouldSelectViewController: 被调用,selectedViewController == toRoot
  • 不触发任何生命周期方法
  • 不调用 didSelectViewController:
  • 不会修改 selectedViewController(本来就是它)

✅ 用途:实现“回到顶部”、“刷新当前页”

- (BOOL)tabBarController:(UITabBarController *)tc shouldSelectViewController:(UIViewController *)vc {
    if (vc == tc.selectedViewController) {
        if ([vc isKindOfClass:[UINavigationController class]]) {
            [(UINavigationController *)vc popToRootViewControllerAnimated:YES];
        }
        return NO; // 语义上“不需要切换”
    }
    return YES;
}

2. 通过代码切换 Tab(如 selectedIndex = 1

  • 不触发任何 delegate 方法
  • ✅ 但会触发 ViewController 生命周期
  • selectedViewController 会立即更新

所以:delegate 方法仅由用户点击 TabBar 触发


📌 五、开发最佳实践总结

需求 推荐位置 注意事项
获取“切换前”的控制器 shouldSelect... 中读 selectedViewController 需解包 UINavigationController
获取“切换后”的控制器 didSelect... 的参数 或 selectedViewController 两者此时相等
刷新数据 业务 VC 的 viewWillAppear: 不要放 viewDidLoad
埋点 / 日志 didSelect... 确保切换已完成
拦截切换 shouldSelect... 返回 NO 可用于权限控制
处理重复点击 shouldSelect... 中判断 vc == selectedViewController 可手动触发逻辑

✅ 六、终极流程图(含状态快照)

用户点击 Tab B
        ↓
[Delegate] shouldSelectViewController:(B_root)
    → selectedViewController == A_root ✅
        ↓
[A_business] viewWillDisappear
[B_business] viewWillAppear
        ↓
[A_business] viewDidDisappear
[B_business] viewDidAppear
        ↓
UIKit 内部: tabBarController.selectedViewController = B_root
        ↓
[Delegate] didSelectViewController:(B_root)
    → selectedViewController == B_root ✅

🎯 记住三句话

  1. shouldSelect 看“过去”,didSelect 看“现在”
  2. 生命周期属于业务 VC,代理参数属于 Tab root VC
  3. selectedViewController 的变更,发生在转场结束之后

📚 结语

UITabBarController 的设计精巧而一致。只要理解 代理时机、selectedViewController 变化点、以及 UINavigationController 的穿透逻辑,你就能在任何复杂 Tab 架构中游刃有余。

希望本文能成为你处理 Tab 切换逻辑的“黄金参考”。


欢迎点赞、收藏、评论交流!如果你有更复杂的嵌套场景(如 Tab 内嵌 PageViewController),也欢迎留言讨论!

Swift——高阶函数(map、filter、reduce、forEach、sorted、contains……)

本文主要讲解 map、filter、reduce、forEach、sorted、contains 、 first(where:) / last(where:) 、firstIndex 和 lastIndex 、prefix( :) 和 dropFirst( :) 、 allSatisfy(_:) 、 lazy:延迟加载

一、map

map 函数,Swift 中最常用的高阶函数之一,核心作用是将集合中的每个元素按照指定规则转换,返回一个新的同类型集合,非常适合批量处理数组、字典等集合类型的元素。 map 就像一个 “转换器”:遍历集合中的每一个元素,把每个元素传入你定义的转换规则(闭包),然后将转换后的结果收集起来,形成一个新的集合返回。

  • 原集合不会被修改(纯函数特性)
  • 新集合的元素数量和原集合完全一致
  • 新集合的元素类型可以和原集合不同
    let prices = [100,200,300]
    let discountedPrices = prices.map{$0 * 10}
    print(discountedPrices) // [1000, 2000, 3000]
    let cast = ["Vivien", "Marlon", "Kim", "Karl"]
    let lowercaseNames = cast.map{$0.lowercased()}
    print(lowercaseNames) // ["vivien", "marlon", "kim", "karl"]
    let letterCounts = cast.map{$0.count}
    print(letterCounts)//  [6, 6, 3, 4]

二、 filter

filter 函数和 map 并列的核心高阶函数,filter的核心作用是根据指定条件筛选集合中的元素,返回符合条件的新集合,非常适合从数组、字典等集合中 “挑选” 需要的元素。 filter 就像一个 “筛选器”:遍历集合中的每一个元素,把每个元素传入你定义的判断条件(闭包),只有满足条件(闭包返回 true)的元素会被保留,最终返回一个包含所有符合条件元素的新集合。

  • 原集合不会被修改
  • 新集合的元素数量 ≤ 原集合
  • 新集合的元素类型和原集合完全一致
    // 示例1:筛选数字数组中的偶数
    let numbers = [1, 2, 3, 4, 5, 6, 7, 8]
    let evenNumbers = numbers.filter { number in
        return number % 2 == 0
    }
    print(evenNumbers) // 输出:[2, 4, 6, 8]
    // 简化写法
    let evenNumbersShort = numbers.filter { $0 % 2 == 0 }
    print(evenNumbersShort) // 输出:[2, 4, 6, 8]

    // 示例2:筛选字符串数组中长度大于5的元素
    let fruits = ["apple", "banana", "orange", "grape", "watermelon"]
    let longFruits = fruits.filter { $0.count > 5 }
    print(longFruits) // 输出:["banana", "orange", "watermelon"]
 // 示例3:筛选自定义对象数组(比如筛选年龄≥18的用户)
    struct User {
        let name: String
        let age: Int
    }
    let users = [
        User(name: "张三", age: 17),
        User(name: "李四", age: 20),
        User(name: "王五", age: 25)
    ]
    let adultUsers = users.filter { $0.age >= 18 }
    print(adultUsers.map { $0.name }) // 输出:["李四", "王五"]

三、reduce

reduce 核心作用是将集合中的所有元素 “归约”/“汇总” 成一个单一的值(比如求和、拼接字符串、计算总宽度、生成字典等),可以理解为把一组元素 “压缩” 成一个结果。

reduce 就像一个 “汇总器”:从一个初始值开始,遍历集合中的每一个元素,将当前元素与累计结果做指定运算,最终得到一个单一的汇总值。

  • 原集合不会被修改
  • 最终结果的类型可以和集合元素类型不同(比如数组元素是 Int,结果可以是 String;元素是 CGFloat,结果可以是 CGFloat
  • 核心逻辑:初始值 + 元素1 → 累计值1 + 元素2 → 累计值2 + ... → 最终结果
    // 示例1:数字数组求和(最基础用法)
    let numbers = [1, 2, 3, 4, 5]
    // 初始值为0,累计规则:累计值 + 当前元素
    let sum = numbers.reduce(0) { partialSum, number in
        return partialSum + number
    }
    // 简化写法
    let sumShort = numbers.reduce(0, +) // 直接用运算符简写,等价于上面的闭包
    print(sum) // 输出:15

    // 示例2:字符串数组合并成一个完整字符串
    let words = ["Hello", " ", "Swift", " ", "reduce!"]
    // 初始值为空字符串,累计规则:拼接字符串
    let sentence = words.reduce("") { $0 + $1 }
    print(sentence) // 输出:"Hello Swift reduce!"

    // 示例3:计算数组中最大值(初始值设为最小值)
    let scores = [85, 92, 78, 95, 88]
    let maxScore = scores.reduce(Int.min) { max($0, $1) }
    print(maxScore) // 输出:95

四、 forEach

forEach 函数,它是集合的基础遍历方法,核心作用是遍历集合中的每一个元素并执行指定操作,和传统的 for-in 循环功能类似,但写法更简洁,且是函数式编程风格的遍历方式。

forEach 就像一个 “遍历执行器”:按顺序遍历集合中的每一个元素,对每个元素执行你定义的闭包操作(比如打印、修改属性、调用方法等)。

  • 原集合不会被修改(除非你在闭包内主动修改元素的可变属性)
  • 没有返回值(Void),这是和 map/filter/reduce 最大的区别(后三者都返回新集合 / 值)
  • 无法用 break/continue 中断 / 跳过遍历(如需中断,建议用传统 for-in 循环)
// 示例1:遍历打印数组元素
let fruits = ["apple", "banana", "orange"]
fruits.forEach { fruit in
    print("水果:\(fruit)")
}
// 简化写法
fruits.forEach { print("水果:\($0)") }
   let numbers = [1, 2, 3, 4, 5]

    // 需求:遍历到3时停止
    // ❌ forEach 无法中断,会遍历所有元素
    numbers.forEach {
        if $0 == 3 {
            return // 仅跳过当前元素,不会中断整体遍历
        }
        print($0) // 输出:1,2,4,5
    }

    // ✅ for-in 可以中断
    for number in numbers {
        if number == 3 {
            break // 直接中断遍历
        }
        print(number) // 输出:1,2
    }

五、 sorted 排序

sorted 函数,它是集合中用于排序的核心高阶函数,核心作用是将集合中的元素按指定规则排序,返回一个新的有序集合(原集合保持不变)。

 // 示例1:数字数组默认排序(升序)
    let numbers = [5, 2, 9, 1, 7]
    let sortedNumbers = numbers.sorted()
    print(sortedNumbers) // 输出:[1, 2, 5, 7, 9]

    // 示例2:自定义降序排序
    let descendingNumbers = numbers.sorted { $0 > $1 }
    print(descendingNumbers) // 输出:[9, 7, 5, 2, 1]

    // 示例3:字符串数组排序(默认字母序,区分大小写)
    let fruits = ["banana", "Apple", "orange", "grape"]
    let sortedFruits = fruits.sorted()
    print(sortedFruits) // 输出:["Apple", "banana", "grape", "orange"]

    // 示例4:字符串忽略大小写排序(自定义规则)
    let caseInsensitiveFruits = fruits.sorted { $0.lowercased() < $1.lowercased() }
    print(caseInsensitiveFruits) // 输出:["Apple", "banana", "grape", "orange"](和上面结果一样,但逻辑更通用)

六、contains

contains 函数,它是集合用于判断 “是否包含指定元素 / 符合条件的元素” 的核心方法,核心作用是快速检查集合中是否存在目标元素或满足条件的元素,返回布尔值(true/false)。 contains 就像一个 “检测器”:遍历集合并检查是否存在符合要求的元素,无需手动遍历判断,代码更简洁。

  • 有两个常用变体:
    1. contains(_:):检查是否包含具体某个元素(要求元素遵循 Equatable 协议,如 Int、String、CGSize 等默认遵循)
    2. contains(where:):检查是否包含符合自定义条件的元素(更灵活,适用于复杂判断)
  • 返回值是 Booltrue= 包含,false= 不包含)
  • 原集合不会被修改
    // 示例1:检查是否包含具体数字
    let numbers = [1, 2, 3, 4, 5]
    let hasThree = numbers.contains(3)
    let hasTen = numbers.contains(10)
    print(hasThree) // 输出:true
    print(hasTen) // 输出:false

    // 示例2:检查是否包含具体字符串
    let fruits = ["apple", "banana", "orange"]
    let hasBanana = fruits.contains("banana")
    print(hasBanana) // 输出:true

    // 示例3:检查是否包含符合条件的元素(数字大于3)
    let hasGreaterThanThree = numbers.contains { $0 > 3 }
    print(hasGreaterThanThree) // 输出:true(4、5都满足)

    // 示例4:检查是否包含长度大于5的字符串
    let hasLongFruit = fruits.contains { $0.count > 5 }
    print(hasLongFruit) // 输出:true(banana、orange长度都大于5)

七、 first(where:) 和 last(where:)

first(where:) 和 last(where:) 方法,它们是集合中用于精准查找第一个 / 最后一个符合条件元素的核心方法,返回值是可选类型(T?)—— 找到则返回对应元素,找不到则返回 nil

first(where:) / last(where:) 就像 “精准查找器”:

  • first(where:)从前往后遍历集合,返回第一个满足条件的元素(可选值)
  • last(where:)从后往前遍历集合,返回最后一个满足条件的元素(可选值)
  • 两者都不会修改原集合,且找到目标元素后会立即停止遍历(性能优于先 filter 再取 first/last
  • 若集合为空或无符合条件的元素,返回 nil
    // 示例1:查找第一个大于3的数字
    let numbers = [1, 2, 3, 4, 5, 4, 3]
    let firstGreaterThan3 = numbers.first { $0 > 3 }
    print(firstGreaterThan3) // 输出:Optional(4)(第一个满足的是索引3的4)

    // 示例2:查找最后一个大于3的数字
    let lastGreaterThan3 = numbers.last { $0 > 3 }
    print(lastGreaterThan3) // 输出:Optional(4)(最后一个满足的是索引5的4)

    // 示例3:查找第一个长度大于5的字符串
    let fruits = ["apple", "banana", "orange", "grape"]
    let firstLongFruit = fruits.first { $0.count > 5 }
    print(firstLongFruit) // 输出:Optional("banana")

    // 示例4:无符合条件元素时返回nil
    let firstGreaterThan10 = numbers.first { $0 > 10 }
    print(firstGreaterThan10) // 输出:nil

八、 firstIndex 和 lastIndex

firstIndex(of:) / firstIndex(where:) 和 lastIndex(of:) / lastIndex(where:) 方法,它们是集合中用于查找元素对应索引的核心方法,返回值为可选类型的 Index(通常是 Int 类型)—— 找到则返回元素的索引,找不到则返回 nil

方法 作用 适用条件
firstIndex(of:) 从前往后找第一个匹配指定元素的索引 元素遵循 Equatable 协议
firstIndex(where:) 从前往后找第一个符合自定义条件的元素的索引 无(更灵活,支持复杂判断)
lastIndex(of:) 从后往前找最后一个匹配指定元素的索引 元素遵循 Equatable 协议
lastIndex(where:) 从后往前找最后一个符合自定义条件的元素的索引
  • 所有方法返回值都是 Index?(数组中等价于 Int?),找不到则返回 nil
  • 找到目标后立即停止遍历,性能高效
  • 原集合不会被修改
 // 基础数组
    let numbers = [1, 2, 3, 2, 5, 2]
    let fruits = ["apple", "banana", "orange", "banana"]

    // 示例1:firstIndex(of:) —— 找第一个2的索引
    if let firstTwoIdx = numbers.firstIndex(of: 2) {
        print("第一个2的索引:\(firstTwoIdx)") // 输出:1
    }

    // 示例2:lastIndex(of:) —— 找最后一个2的索引
    if let lastTwoIdx = numbers.lastIndex(of: 2) {
        print("最后一个2的索引:\(lastTwoIdx)") // 输出:5
    }

    // 示例3:firstIndex(where:) —— 找第一个大于3的数字的索引
    if let firstGreater3Idx = numbers.firstIndex { $0 > 3 } {
        print("第一个大于3的数字索引:\(firstGreater3Idx)") // 输出:4(数字5)
    }

    // 示例4:lastIndex(where:) —— 找最后一个"banana"的索引
    if let lastBananaIdx = fruits.lastIndex { $0 == "banana" } {
        print("最后一个banana的索引:\(lastBananaIdx)") // 输出:3
    }

    // 示例5:无匹配元素时返回nil
    if let noExistIdx = numbers.firstIndex(of: 10) {
        print(noExistIdx)
    } else {
        print("未找到元素10") // 输出:未找到元素10
    }
        

九、prefix( :) 和 dropFirst( :)

prefix(:) 和 dropFirst(:) 方法,它们是集合中用于截取 / 剔除前 N 个元素的核心方法,返回新的集合片段(PrefixSequence/DropFirstSequence,可直接转为数组),原集合保持不变。

方法 核心作用 返回值类型 原集合影响
prefix(_:) 截取集合前 n 个元素(若 n 超过集合长度,返回全部元素) PrefixSequence<T>
dropFirst(_:) 剔除集合前 n 个元素,返回剩余元素(若 n 超过集合长度,返回空集合) DropFirstSequence<T>
  • 补充:还有无参数简化版 prefix()(等价于 prefix(1),取第一个元素)、dropFirst()(等价于 dropFirst(1),剔除第一个元素);
  • 返回的 Sequence 可通过 Array() 转为普通数组,方便后续操作。
// 基础数组
let numbers = [1, 2, 3, 4, 5]
let fruits = ["apple", "banana", "orange", "grape"]

// 示例1:prefix(_:) —— 截取前3个元素
let prefix3Numbers = Array(numbers.prefix(3))
print(prefix3Numbers) // 输出:[1, 2, 3]

// 示例2:prefix(_:) —— n 超过数组长度,返回全部
let prefix10Numbers = Array(numbers.prefix(10))
print(prefix10Numbers) // 输出:[1, 2, 3, 4, 5]

// 示例3:dropFirst(_:) —— 剔除前2个元素
let drop2Numbers = Array(numbers.dropFirst(2))
print(drop2Numbers) // 输出:[3, 4, 5]

// 示例4:dropFirst(_:) —— n 超过数组长度,返回空
let drop10Numbers = Array(numbers.dropFirst(10))
print(drop10Numbers) // 输出:[]

// 示例5:无参数版
let firstFruit = Array(fruits.prefix(1))      // 等价于 prefix(1)
let restFruits = Array(fruits.dropFirst())   // 等价于 dropFirst(1)
print(firstFruit)  // 输出:["apple"]
print(restFruits)  // 输出:["banana", "orange", "grape"]

九、 allSatisfy(_:)

allSatisfy 方法,它是集合中用于判断所有元素是否都满足指定条件的核心方法,返回布尔值(true/false)—— 只有当集合中每一个元素都符合条件时返回 true,只要有一个不符合就返回 false

allSatisfy 就像一个 “全量校验器”:

  • 遍历集合中的每一个元素,依次检查是否符合条件;
  • 只要发现一个元素不符合条件,会立即停止遍历(性能高效),返回 false
  • 只有所有元素都符合条件,才会遍历完成并返回 true
  • 空集合调用 allSatisfy 会直接返回 true(逻辑上 “空集合中所有元素都满足条件”);
  • 原集合不会被修改。
// 基础数组
let numbers = [2, 4, 6, 8]
let mixedNumbers = [2, 4, 7, 8]
let fruits = ["apple", "banana", "orange"]

// 示例1:检查所有数字是否都是偶数
let allEven = numbers.allSatisfy { $0 % 2 == 0 }
print(allEven) // 输出:true

let mixedEven = mixedNumbers.allSatisfy { $0 % 2 == 0 }
print(mixedEven) // 输出:false(7是奇数,遍历到7时立即返回false)

// 示例2:检查所有字符串长度是否大于3
let allLongerThan3 = fruits.allSatisfy { $0.count > 3 }
print(allLongerThan3) // 输出:true(apple=5, banana=6, orange=6)

// 示例3:空集合调用返回true
let emptyArray: [Int] = []
let emptyAllMatch = emptyArray.allSatisfy { $0 > 10 }
print(emptyAllMatch) // 输出:true

十、 lazy:延迟加载

let hugeRange = 1...1000000
let result = hugeRange.lazy
    .filter { $0 % 3 == 0 }
    .map { $0 * 2 }
    .prefix(10)

lazy会延迟计算,直到真正需要结果时才执行操作,避免创建大量中间数组。

iOS开发必备的HTTP网络基础概览

一、从一次HTTP请求说起

以下是一个大体过程,不包含DNS缓存等等细节:

sequenceDiagram
    participant C as 客户端(iOS App)
    participant D as DNS服务器
    participant S as 目标服务器
    participant T as TLS/SSL层
    
    Note over C,S: 1. DNS解析阶段
    C->>D: 查询域名对应IP
    D-->>C: 返回IP地址
    
    Note over C,S: 2. TCP连接建立
    C->>S: SYN (我要连接)
    S-->>C: SYN-ACK (可以连接)
    C->>S: ACK (确认连接)
    
    Note over C,S: 3. TLS握手(HTTPS)
    C->>T: ClientHello
    T-->>C: ServerHello + 证书
    C->>C: 验证证书
    C->>T: 预主密钥(加密)
    T-->>C: 握手完成
    
    Note over C,S: 4. HTTP请求响应
    C->>S: GET /api/data
    S-->>C: 200 OK + 数据
    
    Note over C,S: 5. 连接管理
    alt HTTP/1.1持久连接
        S->>C: 保持连接打开
    else HTTP/2多路复用
        C->>S: 多个请求并行
    end

上图展示了一个完整的HTTPS请求过程。对于iOS开发者,理解每个环节的工作原理至关重要,这有助于优化网络性能、解决连接问题。

二、深入理解网络分层模型

TCP/IP四层模型详解

┌─────────────────────────────────────────┐
│           应用层 (Application)           │
│  HTTP/HTTPS · DNS · WebSocket · FTP     │
├─────────────────────────────────────────┤
│           传输层 (Transport)             │
│       TCP(可靠) · UDP(快速)          │
├─────────────────────────────────────────┤
│           网络层 (Internet)              │
│         IP · ICMP · 路由选择             │
├─────────────────────────────────────────┤
│           链路层 (Link)                  │
│   以太网 · WiFi · 蜂窝网络 · ARP        │
└─────────────────────────────────────────┘

各层在iOS开发中的体现

1. 应用层(iOS开发者最关注)

  • HTTP/HTTPS:URLSession、Alamofire、Moya等框架直接操作
  • DNS:系统自动处理,但可优化
  • WebSocket:实时通信场景
  • 责任:定义数据格式和应用协议

2. 传输层(可靠性保证)

  • TCP:面向连接、可靠传输
    • 三次握手建立连接
    • 丢包重传、顺序保证
    • 流量控制、拥塞控制
    • iOS中:URLSession默认使用TCP
  • UDP:无连接、尽最大努力交付
    • 实时音视频、DNS查询
    • iOS中:NWConnection框架支持

3. 网络层(路由寻址)

  • IP协议:负责主机到主机的通信
  • IPv4 vs IPv6:iOS自动处理兼容性
  • 路由选择:数据包如何到达目标
  • ICMP:ping工具的基础(网络诊断)

4. 链路层(物理连接)

  • 不同网络类型:WiFi、蜂窝网络、有线网络
  • MTU(最大传输单元):影响数据包分片
  • iOS中:通过NWPathMonitor监控网络状态变化

各层常见问题及调试

  • 应用层:HTTP状态码、JSON解析错误
  • 传输层:连接超时、连接重置、端口不可达
  • 网络层:路由不可达、TTL超时
  • 链路层:信号弱、MTU不匹配

iOS调试工具

  • 网络抓包:Charles、Wireshark
  • 命令行:nslookuppingtraceroute
  • Xcode Instruments:Network模板

三、DNS解析深度优化

HTTPDNS基本原理

传统DNS vs HTTPDNS
┌─────────────────┐    ┌─────────────────┐
│   传统DNS流程    │    │   HTTPDNS流程   │
├─────────────────┤    ├─────────────────┤
│ 1. 系统DNS查询   │    │ 1. HTTP API调用  │
│ 2. 递归查询      │    │ 2. 直接返回IP    │
│ 3. 易受劫持      │    │ 3. 防劫持       │
│ 4. 延迟较高      │    │ 4. 低延迟       │
└─────────────────┘    └─────────────────┘

HTTPDNS工作流程

  1. 绕过系统DNS:直接向HTTPDNS服务商(如腾讯云DNSPod、阿里云)发送HTTP/HTTPS请求
  2. 获取最优IP:服务端根据客户端IP返回最近、最优的服务器IP
  3. 本地DNS:建立本地缓存,减少查询频率
  4. 失败降级:HTTPDNS失败时自动降级到系统DNS

iOS实现HTTPDNS的关键步骤

  1. 拦截URL请求,解析出域名
  2. 向HTTPDNS服务查询IP地址
  3. 替换请求的Host头,将域名替换为IP
  4. 添加原始域名到Header(如"Host: www.example.com")
  5. 建立连接时直接使用IP地址

DNS优化综合策略

优化方案 原理 iOS实现要点
本地缓存 减少重复查询 设置合理TTL,监听网络切换清缓存
预解析 提前解析可能用到的域名 在需要前发起异步DNS查询
连接复用 减少DNS查询次数 保持HTTP持久连接
多路复用 并行解析多个域名 异步并发DNS查询
失败重试 提高可靠性 备选DNS服务器,指数退避重试

四、HTTP协议演进详解

HTTP/1.1核心特性

持久连接(Keep-Alive)

graph LR
    A[HTTP/1.0] --> B[每次请求新建连接]
    B --> C[高延迟 高开销]
    D[HTTP/1.1] --> E[连接复用]
    E --> F[降低延迟 减少开销]
    
    G[客户端] -- 请求1 --> H[服务器]
    G -- 请求2 --> H
    G -- 请求3 --> H
    H -- 响应1 --> G
    H -- 响应2 --> G
    H -- 响应3 --> G

关于服务器负载的说明: 持久连接实际上减少了服务器总体负载:

  1. 连接建立成本:TCP三次握手 + TLS握手(HTTPS)消耗大量CPU
  2. 减少并发连接数:每个客户端连接数减少
  3. 内存资源节省:每个连接需要维护状态信息

但需要注意

  • 需要合理设置keep-alive超时时间
  • 监控服务器连接数,避免过多空闲连接占用资源
  • iOS中URLSession默认管理连接池

HTTP/1.1的其他重要特性

  1. 分块传输编码:支持流式传输
  2. 缓存控制:Cache-Control头部
  3. 管道化(理论特性):可并行发送多个请求,但响应必须按序返回,存在队头阻塞问题

HTTP/2革命性改进

graph TD
    subgraph HTTP/1.1
        A1[请求1] --> A2[响应1]
        B1[请求2] --> B2[响应2]
        C1[请求3] --> C2[响应3]
    end
    
    subgraph HTTP/2
        D[二进制分帧层]
        E1[请求1] --> D
        E2[请求2] --> D
        E3[请求3] --> D
        D --> F1[响应1]
        D --> F2[响应2]
        D --> F3[响应3]
    end

HTTP/2核心特性

  1. 二进制分帧

    • 替代HTTP/1.x的文本格式
    • 帧类型:HEADERS、DATA、SETTINGS等
    • 更高效解析,更少错误
  2. 多路复用

    • 单个连接上并行交错多个请求/响应
    • 解决HTTP/1.1队头阻塞问题
    • 请求优先级设置
  3. 头部压缩(HPACK)

    • 静态表(61个常用头部)
    • 动态表(连接期间维护)
    • 哈夫曼编码
  4. 服务器推送

    • 服务器可主动推送资源
    • 客户端可拒绝不需要的推送

iOS适配要点

  • iOS 8+ 自动支持HTTP/2(通过ALPN协商)
  • 无需代码变更,但需确保服务器支持TLS
  • 监控工具可查看是否使用HTTP/2

HTTP/3(基于QUIC)新时代

QUIC协议架构

┌─────────────────┐
│   HTTP/3语义    │
├─────────────────┤
│  QUIC传输协议    │
│  (基于UDP)      │
├─────────────────┤
│   TLS 1.3       │
├─────────────────┤
│  应用层拥塞控制  │
└─────────────────┘

HTTP/3核心改进

  1. 传输层改为UDP:彻底解决TCP队头阻塞
  2. 内置TLS 1.3:0-RTT/1-RTT快速握手
  3. 连接迁移:网络切换时连接不中断
  4. 改进的拥塞控制:更适应现代网络环境

iOS适配

  • iOS 15+ 开始支持
  • URLSession自动协商使用
  • 可通过Network框架检测协议版本

五、HTTPS安全机制深度解析

TLS握手流程详解

sequenceDiagram
    participant C as Client
    participant S as Server
    
    Note over C,S: TLS 1.2 完整握手
    C->>S: ClientHello<br/>支持的版本、密码套件、随机数
    S->>C: ServerHello<br/>选定的版本、密码套件、随机数
    S->>C: Certificate<br/>服务器证书链
    S->>C: ServerHelloDone
    
    C->>C: 验证证书有效性
    C->>S: ClientKeyExchange<br/>预主密钥(用服务器公钥加密)
    C->>S: ChangeCipherSpec<br/>切换加密方式
    C->>S: Finished<br/>加密验证数据
    
    S->>S: 解密预主密钥,生成会话密钥
    S->>C: ChangeCipherSpec
    S->>C: Finished
    
    Note over C,S: TLS 1.3 简化握手
    C->>S: ClientHello<br/>包含密钥共享
    S->>C: ServerHello<br/>证书、Finished
    C->>S: Finished<br/>1-RTT完成

iOS证书验证体系

系统信任链

  1. 根证书库:iOS内置的可信CA根证书
  2. 证书链验证:从服务器证书追溯到可信根证书
  3. 吊销检查:OCSP或CRL检查证书是否被吊销

证书锁定(Pinning)策略

// iOS 安全配置示例
// 1. ATS配置 (Info.plist)
// 2. 证书锁定实现
class CertificatePinner: NSObject, URLSessionDelegate {
    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge,
                    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        
        // 验证服务器证书是否匹配预设公钥
        guard let serverTrust = challenge.protectionSpace.serverTrust else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        
        // 公钥锁定(比证书锁定更灵活)
        let policy = SecPolicyCreateSSL(true, challenge.protectionSpace.host as CFString)
        SecTrustSetPolicies(serverTrust, policy)
        
        // 验证并提取公钥进行比较
        // ... 具体实现代码
    }
}

HTTPS性能优化

  1. 会话恢复

    • Session ID:服务端存储会话信息
    • Session Ticket:客户端存储加密的会话信息
    • 减少完整握手次数
  2. TLS 1.3优势

    • 0-RTT(零往返时间):对重复连接极速握手
    • 1-RTT:首次连接也更快
    • 更安全的密码套件
  3. iOS最佳实践

    • 启用TLS 1.3(iOS 13+ 默认支持)
    • 合理配置ATS策略
    • 监控TLS握手时间指标

六、iOS网络编程综合建议

1. 连接管理策略

  • 连接池管理:每个主机保持2-6个持久连接
  • 超时策略
    • 连接超时:15-30秒
    • 请求超时:根据业务调整
    • 资源超时:大文件下载单独设置
  • 网络切换处理:监听NWPathMonitor,重建连接

2. 协议选择策略

// 协议检测与选择
func checkHTTPVersion() {
    let session = URLSession.shared
    let task = session.dataTask(with: URL(string: "https://api.example.com")!) { data, response, error in
        if let httpResponse = response as? HTTPURLResponse {
            // 查看实际使用的协议
            if #available(iOS 13.0, *) {
                print("使用的协议: \(httpResponse.value(forHTTPHeaderField: "X-Protocol") ?? "未知")")
            }
        }
    }
    task.resume()
}

3. 安全与性能平衡

  • 敏感数据:强制证书锁定 + TLS 1.3
  • 公开内容:标准HTTPS验证即可
  • 性能关键:考虑启用0-RTT,但注意重放攻击风险

4. 监控与调试

  • 关键指标
    • DNS解析时间
    • TCP连接时间
    • TLS握手时间
    • TTFB(首字节时间)
    • 下载速度
  • 网络诊断
    • 实现网络诊断页面
    • 收集不同网络环境下的性能数据
    • 用户反馈问题时的自动诊断报告

总结

iOS网络编程不仅仅是调用API,更是对底层协议的深刻理解和合理应用。从四层模型的分工协作,到DNS解析的优化策略,从HTTP协议的持续演进,到HTTPS安全机制的实现原理,每一个环节都影响着最终的用户体验。

关键认知升级

  1. HTTP/2的多路复用显著提升性能,但需要服务器支持
  2. HTTP/3基于QUIC,解决传输层队头阻塞,是未来方向
  3. HTTPS性能不再是问题,TLS 1.3极大优化了握手延迟
  4. DNS优化常被忽视,但却是首屏加载的关键因素

实践建议

  • 优先使用系统框架(URLSession),充分利用系统优化
  • 渐进增强,支持新协议但不强依赖
  • 全面监控,建立网络性能基线
  • 安全优先,但也要考虑兼容性和维护成本

通过深入理解这些网络基础知识,iOS开发者能够构建更高效、更稳定、更安全的网络层,为用户提供卓越的网络体验。

❌