阅读视图

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

iOS开发:关于URL解析

iOS与Android对URL的解析

首先分析了一下iOS与Android在解析URL上是否有差异。

iOS的解析:

guard let url = URLComponents(string: "http://www.example.com:80/path/to/resource.html?param=value#fragment") else {
    return
}

// 获取协议
let scheme = url.scheme ?? ""
print("url Protocol: " + scheme)

// 获取主机名
let host = url.host ?? ""
print("url Host: " + host)

// 获取端口号
let port = url.port ?? -1
print("url Port: \(port)")

// 获取路径
let path = url.path
print("url Path: " + path)

// 获取查询字符串
let query = url.query ?? ""
print("url Query: " + query)

var dict = [String: String]()
if let queryItems = url.queryItems {
    dict = queryItems.toDict
}
print("url dict: \(dict)")

// 获取片段
let ref = url.fragment ?? ""
print("url Ref: " + ref)

/**
url Protocol: http
url Host: www.example.com
url Port: 80
url Path: /path/to/resource.html
url Query: param=value
url Ref: fragment
*/

Android的解析:

import java.net.URL;
import java.net.MalformedURLException;

public class URLParsingExample {
    public static void main(String[] args) {
        try {
            URL url = new URL("http://www.example.com:80/path/to/resource.html?param=value#fragment");

            // 获取协议
            String protocol = url.getProtocol();
            System.out.println("Protocol: " + protocol);

            // 获取主机名
            String host = url.getHost();
            System.out.println("Host: " + host);

            // 获取端口号
            int port = url.getPort();
            System.out.println("Port: " + port);

            // 获取路径
            String path = url.getPath();
            System.out.println("Path: " + path);

            // 获取查询字符串
            String query = url.getQuery();
            System.out.println("Query: " + query);

            // 获取片段
            String ref = url.getRef();
            System.out.println("Ref: " + ref);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
}

Protocol: http
Host: www.example.com
Port: 80
Path: /path/to/resource.html
Query: param=value
Ref: fragment

小结

其实可以看出,iOS和Android对于URL的解析基本上是一致的。

  • ?用于分割前后,前面可以认为是路径,后面的可以认为是参数

  • param=value&name=season,通过=与&用来分割参数

  • 最后通过#用来分割Fragment

遇到的异常情况

我们在通过name=value这种形式进行传参的时候,不可避免可能会使用到#与?符号,比如下面:

haha://abcd?param=value&id=12345678&jumpType=information&content=我关注的话题#你好啊?

content=我关注的话题#你好啊?其实是{ content: 我关注的话题#你好啊? }这样一个json,但是会被截取成下面这样子:

url Query: param=value&id=12345678&jumpType=information&content=我关注的话题
url dict: ["id": "12345678", "content": "我关注的话题", "jumpType": "information", "param": "value"]
url Ref: 你好啊

content=我关注的话题#你好啊?明明是一个完整的参数,但是因为解析的策略,#与?都被截掉了。

思路

为了保证语义的完整性。

我们可以考虑将name=value这种形式进行传参转换为json:{"id": "12345678", "content": "我关注的话题#你好啊?", "jumpType": "information", "param": "value"},然后再转成jsonString,最后使用base64进行加密获得string,这里使用base64进行加密是为了保证加密后的数据没有可以被解析的符号。

App侧拿到后,先将这个string通过base64解密,获得完整的jsonString,然后就可以正常解析了。

例子

这个例子中有jumpType、id、jsonString三个参数,然后jsonString里面包含了很多个参数

nm://home?jumpType=TopicDetail&id=d308e8ff62dbb382a106def90d946759&jsonString=e1widGFsa2luZ05hbWVcIjpcIuaIkeeahOaPkOi9puaXpeiusFwiLFwiaG90UmF0ZVwiOjEyMzgsXCJ2aWV3Q291bnRcIjo0NzY1MixcInN0YXR1c1wiOjF9

解析jsonString

"{\"talkingName\":\"我的提车日记\",\"hotRate\":1238,\"viewCount\":47652,\"status\":1}"

base64加解密会不会出现#?=这个符号呢?

有关与这个,我去查阅了一下资料

Swift 中,使用 Base64 编码后的字符串不会包含 ?#& 这些符号。Base64 编码的输出只会包含以下字符:

  • 大写字母:A-Z
  • 小写字母:a-z
  • 数字:0-9
  • 加号:+
  • 斜杠:/
  • 等号:=(用于填充)

这些字符是 Base64 编码标准的一部分,确保编码后的字符串可以安全地在文本环境中传输。

但是会不会出现异常情况呢?这个还真不能保证。

总结

URL解析其实不仅涉及到网络请求,有些路由规则与传参也会涉及到URL的拼接与规则,了解URL的细节,以及多端的异同点,可以提高开发效率。

深入解析 Swift 中的并发属性包装器:@Actor、@MainActor 和 @GlobalActor

在 Swift 5.5 中,引入了多个并发相关的属性包装器,其中 @Actor@MainActor@GlobalActor 是非常重要的工具,用于管理并发环境中的数据访问和任务调度。下面我将详细解释这三者的概念和用法。

1. @Actor

@Actor 是 Swift 5.5 引入的一个用于创建并发安全类型的属性包装器。它用于保护对象的状态免受并发访问,确保只有一个线程可以访问数据。这是通过同步访问对象的状态来防止数据竞争和并发冲突。

特点:

  • @Actor 可以标记类、结构体、枚举类型,它自动为所有实例方法提供异步访问(以保证数据的线程安全)。
  • 任何访问 actor 内部数据的操作,都必须通过异步方式进行。
  • actor 会自动处理锁和同步问题,不需要开发者手动管理。

示例:

actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }

    func getValue() -> Int {
        return value
    }
}

func testActor() async {
    let counter = Counter()

    // 在不同的线程执行并发任务
    await counter.increment()
    let currentValue = await counter.getValue()

    print("Current value: \(currentValue)")
}

在这个例子中,Counter 被定义为一个 actor,其状态(value)是并发安全的,任何修改或读取 value 的操作都必须通过 await 来执行。

2. @MainActor

@MainActor 是一个属性包装器,用于确保某个类、结构体、方法或者函数在主线程上执行。它通常用于 UI 更新等必须在主线程上执行的任务,确保这些操作不会因为线程调度问题而出现错误。

特点:

  • @MainActor 可以应用于类、结构体、方法、属性等,确保其相关操作在主线程上执行。
  • 在主线程外的其他线程调用主线程方法时,Swift 会自动将这些方法的执行调度到主线程。

示例:

@MainActor
class ViewController: UIViewController {
    @IBOutlet weak var label: UILabel!

    func updateUI() {
        // 确保 updateUI 总是在主线程中调用
        label.text = "Hello, World!"
    }
}

func testMainActor() async {
    let viewController = ViewController()
    await viewController.updateUI()  // 确保在主线程执行
}

在这个例子中,updateUI 方法被标记为 @MainActor,确保它在主线程上执行,从而安全地更新 UI 元素。

3. @GlobalActor

@GlobalActor 是一个自定义的属性包装器,它允许你定义其他的全局并发环境(例如,特定的并发队列或线程池),而不仅仅是主线程。这个属性包装器用于指定某个特定的 actor,从而可以更灵活地管理并发任务。

特点:

  • @GlobalActor 允许你定义一个全局的、并发安全的执行上下文,类似于主线程,但你可以定义它为其他线程或队列。
  • 你可以将 @GlobalActor 用于任何异步任务,以确保这些任务在指定的 actor 中执行。

示例:

@GlobalActor
struct BackgroundActor {
    static let shared = BackgroundActor()
}

@BackgroundActor
class NetworkManager {
    func fetchData() async {
        // 在指定的 global actor (后台队列) 中执行的异步任务
        print("Fetching data in the background.")
    }
}

func testGlobalActor() async {
    let networkManager = NetworkManager()
    await networkManager.fetchData()  // 会在 BackgroundActor 的调度环境中执行
}

在这个例子中,BackgroundActor 被标记为 @GlobalActor,并将 NetworkManager 中的 fetchData 方法标记为使用该全局 actor。这意味着 fetchData 方法将在 BackgroundActor 相关联的调度环境中执行。

总结

这三个属性包装器各自有不同的应用场景:

  • @Actor:用于创建并发安全的数据类型(actor)。它会自动管理数据访问的同步问题。
  • @MainActor:用于确保某个任务在主线程上执行,通常用于 UI 更新等必须在主线程执行的操作。
  • @GlobalActor:允许开发者自定义并发执行的上下文,适用于需要将任务调度到特定线程或队列的场景。

SwiftUI 表达式

www.youtube.com/shorts/EDd6…

"^[\(count)Person](inflect: true)"

代码

struct ContentView: View {
    
    @State private var count = 1
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
            
            Spacer()
            
            Button("Add") { count += 1}
            Button("Remove") { count -= 1}
            Text("^[\(count)People](inflect: true)").padding()
            Text("^[\(count)Dog](inflect: true)").padding()
            Text("^[\(count)Hobby](inflect: true)").padding()
        }
        .padding()
    }
} 
"^[\(count)People](inflect: true)"

或者

"^[\(count)Person](inflect: true)"

输出

0People  1Person 2People...

Q: ^[\(count)People](inflect: true) 这是个表达式吗?

Deepseek: 是的,^[\(count)People](inflect: true) 是一个表达式,通常出现在某些模板引擎或文本处理系统中(比如 Sugarcube、Twine 或其他类似工具)。它的作用可能是:

  1. 动态显示数量+名词

    • \(count) 是一个变量插值,会被替换为 count 变量的值(比如 5)。
    • People 是默认文本,但会根据 count 的值自动调整单复数形式(由 inflect: true 控制)。
  2. 功能解析

    • ^[...] 可能是特殊语法,表示动态内容或宏。
    • inflect: true 启用语法变形(如英语中 1 person vs 5 people)。
  3. 示例结果

    • count = 1 → 输出 1 Person
    • count = 3 → 输出 3 People

具体行为取决于你使用的工具文档,但整体是一个动态文本生成表达式

但是有个问题,一直减会出现负数

Q: 这个限制一下 不能出现负数

Deepseek:

使用 max 确保 count ≥ 0

在 Text 显示时强制 count 最小为 0(但实际变量值仍为负数):

Text("^[\(max(count, 0)) People](inflect: true)")  // 显示时限制最小值

询问更多表达式

在 SwiftUI 中,除了 ^[\(count) People](inflect: true) 这种基础的本地化复数/语法变形表达式外,Apple 还提供了更多强大的本地化字符串插值格式化表达式。以下是几种进阶用法:


1. 自动复数变形(Inflection)

适用于不同数量的名词变形(英语等语言):

Text("^[\(count) Person](inflect: true)")  
// 自动输出: "1 Person" / "2 People" / "0 People"

2. 单位格式化(Measurement)

自动处理单位转换本地化格式(如温度、距离):

let speed = Measurement(value: 100, unit: UnitSpeed.kilometersPerHour)
Text(speed, format: .measurement(width: .abbreviated)) 
// 输出: "62 mph"(根据系统语言/地区自动转换)

3. 数字/日期格式化

数字格式化(货币、百分比等):

Text(1234.56, format: .currency(code: "USD")) // "$1,234.56"
Text(0.75, format: .percent))                 // "75%"

日期格式化

Text(Date.now, format: .dateTime.year().month().day())) // "Jul 28, 2023"

4. 条件化字符串(Conditional)

根据条件显示不同文本:

Text("You have \(count) \(count == 1 ? "item" : "items")")
// 或使用 Swift 5 的字符串插值:
Text("You have \(count) \("item", count: count)")

5. 高级本地化(LocalizedStringKey)

直接使用本地化键值:

Text("welcome_message") // 自动匹配 Localizable.strings 中的键

带参数的本地化:

Text("cart_items_count \(count)") 
// Localizable.strings 中定义:
// "cart_items_count %lld" = "You have %lld items in cart";

6. 动态样式(Markdown/Inline Styling)

支持 Markdown 和动态样式:

Text("**Hello** *World*! [Learn More](https://apple.com)") 
// 粗体、斜体、可点击链接

7. 安全处理负数(你的需求)

如果坚持用 inflect 但要避免负数:

Text("^[\(max(count, 0)) Person](inflect: true)") // 强制最小为 0

总结

场景 表达式示例
复数变形 ^[\(count) Book](inflect: true)
单位格式化 Text(speed, format: .measurement(width: .narrow))
数字格式化 Text(9.99, format: .currency(code: "EUR"))
条件文本 Text("\(count) file\(count == 1 ? "" : "s")")
本地化键 Text("greeting")
Markdown Text("Press **Next** to continue")

根据需求选择最适合的表达式,多数情况下 inflect: true + 数值限制 已足够处理你的案例。

2. 单位格式化(Measurement) 🔍 swiftui measurement

Working with Units and Measurements in SwiftUI - Cocoacasts

Measurement | Apple Developer Documentation

Units and Measurement | Apple Developer Documentation

Swift Measurement API

视频上有Automatic Grammar Agreement 遂🔍

InflectionRule | Apple Developer Documentation

How to use Automatic Grammar Agreement in Swift

Automatic grammar agreement - Forums > SwiftUI

这个里面有code,视频链接就是最上面的链接

突然发现0个人竟然是0People

Q: 0个人是 0 person 还是 0 people

在英语中,数字 "0" 后面的可数名词通常用复数形式,即使数量为零。这是因为复数形式更符合语法习惯,表示“零个”的概念。

永远用复数:0 + 可数名词复数(0 apples, 0 people)

在英语中,"0" 后面的名词通常使用复数形式,因此正确的表达是:

0 people

原因:

  1. 语法习惯:在英语中,数字 "0" 之后的计数名词通常用复数形式(即使数量为零)。类似的例子包括:

    • 0 apples(不是 0 apple)
    • 0 cars(不是 0 car)
  2. 逻辑意义:虽然 "0" 表示没有,但复数形式 "people" 更符合英语中数量描述的习惯,强调“零个个体”而非单数概念。

例外情况:

  • 如果 "person" 作为不可数名词(泛指“人”这一概念),可能会用单数,但这种情况极少见且不自然。例如:
    • "There is 0 person in the room."(听起来不自然)
    • 更自然的表达"There are 0 people in the room."

结论:

正确用法0 people(绝大多数情况下)
不自然:0 person

希望这个解释对你有帮助! 😊

在英语中,数字 "0" 后面的可数名词通常用复数形式,即使数量为零。这是因为复数形式更符合语法习惯,表示“零个”的概念。

更多例子:

正确(复数) 错误(单数)
0 books ❌ 0 book
0 dogs ❌ 0 dog
0 computers ❌ 0 computer
0 students ❌ 0 student
0 chairs ❌ 0 chair
0 phones ❌ 0 phone
0 trees ❌ 0 tree
0 cups ❌ 0 cup
0 ideas ❌ 0 idea
0 problems ❌ 0 problem

为什么用复数?

  1. 语法规则:英语中,任何非 "1" 的数量(包括 0),后面的可数名词都用复数形式。

    • 1 apple(单数)
    • 0 apples / 2 apples / 100 apples(复数)
  2. 逻辑一致性:即使数量为零,仍然表示“多个事物不存在”,因此用复数更自然。

例外情况(极少见)

  • 如果名词本身是不可数名词(如 "water"、"information"),则保持不变:
    • 0 water(不是 "0 waters")
    • 0 money(不是 "0 monies",除非指不同种类的货币)

总结

永远用复数:0 + 可数名词复数(0 apples, 0 people)
避免单数:0 apple, 0 person(不自然)

希望这些例子能帮你掌握这个规则! 😊

person(dictionary.cambridge)

腾讯云 人脸核身 Flutter 插件功能开发(一 IOS 端实现)

前言

项目中需要使用到 腾讯云的人脸核身功能,但是官方没有提供对应的flutter plugin ,所以需要我们自己构建一个flutter plugin ,用于实现IOS 端人脸核身功能,plugin 插件地址github.com/qiangzengli…

  1. 进入官方网站,下载 SDK 工具包

  2. 创建 tencent_cloud_face_verify 插件

  3. 在Plugin 的 ios 目录中 创建Framework 文件夹,将第一步中SDK 中的 TencentCloudHuiyanSDKFace_framework 放置到 Framework 文件夹中

  4. Plugin 中 tencent_cloud_face_verify.podspec文件引入Framework tencent_cloud_face_verify.podspec

#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint tencent_cloud_face_verify.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
  s.name             = 'tencent_cloud_face_verify'
  s.version          = '0.0.1'
  s.summary          = 'A new Flutter plugin project.'
  s.description      = <<-DESC
A new Flutter plugin project.
                       DESC
  s.homepage         = 'http://example.com'
  s.license          = { :file => '../LICENSE' }
  s.author           = { 'Your Company' => 'email@example.com' }
  s.source           = { :path => '.' }
  s.source_files = 'Classes/**/*'
  #.framework文件路径
  s.vendored_frameworks = 'Framework/TencentCloudHuiyanSDKFace_framework/Libs/*framework'
  #.bundle资源文件路径
  # 推荐方式 - 创建独立的资源bundle
#    s.resource_bundles = {
#      'TencentCloudFaceVerifyPluginResources' => ['Framework/TencentCloudHuiyanSDKFace_framework/Resources/*bundle']
    }
  s.ios.resource = 'Framework/TencentCloudHuiyanSDKFace_framework/Resources/*bundle'
#   s.resource = ['Framework/TencentCloudHuiyanSDKFace_framework/Resources/*.bundle']
#   s.resource_bundles ='Framework/TencentCloudHuiyanSDKFace_framework/Resources/*.bundle'
# s.resource_bundles = {
#   'TencentCloudHuiyanSDKFace' => ['Framework/TencentCloudHuiyanSDKFace_framework/Resources/*bundle']
# }
  #swift _oc Header 桥接文件
  s.public_header_files = 'Classes/**/*.h'
  s.dependency 'Flutter'
  s.platform = :ios, '12.0'

  # 1. 添加所有腾讯云认证需要的系统 Framework
  s.frameworks = [
    'UIKit',
    'AVFoundation',
    'CoreVideo',
    'Security',
    'SystemConfiguration',
    'CoreMedia',
    'CoreTelephony',
    'ImageIO',
    'VideoToolbox',
    'Accelerate',
    'WebKit'  # 注意:WebKit.framework(不是 webkit.framework)
  ]
  # 2. 添加腾讯云认证需要的系统库(.tbd)
  s.libraries = 'c++', 'z'  # libc++.tbd 和 libz.tbd



  # Flutter.framework does not contain a i386 slice.
  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
  s.swift_version = '5.0'

  # If your plugin requires a privacy manifest, for example if it uses any
  # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
  # plugin's privacy impact, and then uncomment this line. For more information,
  # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
#  s.resource_bundles = {'tencent_cloud_face_verify_privacy' => ['Framework/Resources/PrivacyInfo.xcprivacy']}
end
  1. Classes目录中创建OC和Swift 的桥接文件(因为人脸核身库使用OC 开发的,Swift 调用需要桥接文件指定.h头文件才能在Swift中正常调用OC 函数) TencentCloudFaceVerifyPlugin-Bridging-Header.h
#import <TencentCloudHuiyanSDKFace/WBFaceVerifyCustomerService.h>
  1. TencentCloudFaceVerifyPlugin.swift
import Flutter
import UIKit

public class TencentCloudFaceVerifyPlugin: NSObject, FlutterPlugin, WBFaceVerifyCustomerServiceDelegate {
  private var flutterResult: FlutterResult?
  
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "tencent_cloud_face_verify", binaryMessenger: registrar.messenger())
    let instance = TencentCloudFaceVerifyPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
    case "getPlatformVersion":
      result("iOS " + UIDevice.current.systemVersion)
    case "launchVerify":
      self.flutterResult = result
      if let dict = call.arguments as? Dictionary<String, String?> {
        let userId:String = dict["userId"] as? String ?? ""
        let sign:String = dict["sign"] as? String ?? ""
        let appId:String = dict["appId"] as? String ?? ""
        let orderNo:String = dict["orderNo"] as? String ?? ""
        let apiVersion:String = dict["apiVersion"] as? String ?? ""
        let licence:String = dict["licence"] as? String ?? ""
        let faceId:String = dict["faceId"] as? String ?? ""
        let nonce:String = dict["nonce"] as? String ?? ""
        let config = WBFaceVerifySDKConfig()
        config.theme = WBFaceVerifyTheme.lightness
        config.language = WBFaceVerifyLanguage.ZH_CN
       
        // 设置 bundlePath
        if let bundlePath = Bundle(for: TencentCloudFaceVerifyPlugin.self).path(forResource: "TencentCloudHuiyanSDKFace", ofType: "bundle") {
            config.bundlePath = (bundlePath as NSString).deletingLastPathComponent
            print("Successfully set bundlePath: (config.bundlePath)")
        }

        // 设置代理
        WBFaceVerifyCustomerService.sharedInstance().delegate = self
    
        WBFaceVerifyCustomerService.sharedInstance().initSDK(withUserId: userId, nonce: nonce, sign: sign, appid: appId, orderNo: orderNo, apiVersion: apiVersion, licence: licence, faceId: faceId,sdkConfig: config) {
            DispatchQueue.main.async {
                WBFaceVerifyCustomerService.sharedInstance().startWbFaceVeirifySdk()
            }
        } failure: { error in
//            print("SDK initialization failed: (error.domain), code: (error.code), description: (error.localizedDescription)")
//            self.flutterResult?(FlutterError(code: String(error.code),
//                                           message: error.localizedDescription,
//                                           details: error.domain))
        }
      }
    default:
      result(FlutterMethodNotImplemented)
    }
  }

  // MARK: - WBFaceVerifyCustomerServiceDelegate Methods
  public func wbfaceVerifyCustomerServiceDidFinished(with result: WBFaceVerifyResult) {
    // 人脸验证完成的回调
    if result.isSuccess {
        // 验证成功
        let resultDict: [String: Any] = [
            "success": true,
            "orderNo": result.orderNo ?? "",
            "sign": result.sign ?? "",
            "isSuccess": result.isSuccess
        ]
        self.flutterResult?(resultDict)
    } else {
        // 验证失败
        let resultDict: [String: Any] = [
            "success": false,
            "orderNo": result.orderNo ?? "",
            "sign": result.sign ?? "",
            "isSuccess": result.isSuccess
        ]
        self.flutterResult?(resultDict)
    }
  }

//  // 可选:添加即将上传最佳照片的回调
//  public func wbfaceVerifyCustomerServiceWillUploadBestImage(_ bestImage: UIImage) {
//    print("Will upload best image")
//  }
}
  1. tencent_cloud_face_verify_platform_interface.dart
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'tencent_cloud_face_verify_method_channel.dart';

abstract class TencentCloudFaceVerifyPlatform extends PlatformInterface {
  /// Constructs a TencentCloudFaceVerifyPlatform.
  TencentCloudFaceVerifyPlatform() : super(token: _token);

  static final Object _token = Object();

  static TencentCloudFaceVerifyPlatform _instance = MethodChannelTencentCloudFaceVerify();

  /// The default instance of [TencentCloudFaceVerifyPlatform] to use.
  ///
  /// Defaults to [MethodChannelTencentCloudFaceVerify].
  static TencentCloudFaceVerifyPlatform get instance => _instance;

  /// Platform-specific implementations should set this with their own
  /// platform-specific class that extends [TencentCloudFaceVerifyPlatform] when
  /// they register themselves.
  static set instance(TencentCloudFaceVerifyPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<String?> getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }

  Future<Map<Object?, Object?>> launchVerify({
    required String userId,
    required String sign,
    required String appId,
    required String orderNo,
    required String apiVersion,
    required String licence,
    required String faceId,
    required String nonce,
  }){
    throw UnimplementedError('launchVerify() has not been implemented.');

  }
}
  1. tencent_cloud_face_verify_method_channel.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

import 'tencent_cloud_face_verify_platform_interface.dart';

/// An implementation of [TencentCloudFaceVerifyPlatform] that uses method channels.
class MethodChannelTencentCloudFaceVerify
    extends TencentCloudFaceVerifyPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('tencent_cloud_face_verify');

  @override
  Future<String?> getPlatformVersion() async {
    final version =
        await methodChannel.invokeMethod<String>('getPlatformVersion');
    return version;
  }

  @override
  Future<Map<Object?, Object?>> launchVerify({
    required String userId,
    required String sign,
    required String appId,
    required String orderNo,
    required String apiVersion,
    required String licence,
    required String faceId,
    required String nonce,
  }) async {
    return await methodChannel.invokeMethod('launchVerify', {
      "userId": userId,
      "sign": sign,
      "appId": appId,
      "orderNo": orderNo,
      "apiVersion": apiVersion,
      "licence": licence,
      "faceId": faceId,
      "nonce": nonce,
    });
  }
}
  1. tencent_cloud_face_verify.dart
import 'tencent_cloud_face_verify_platform_interface.dart';

class TencentCloudFaceVerify {

  Future<Map<Object?, Object?>> launchVerify({
    required String userId,
    required String sign,
    required String appId,
    required String orderNo,
    required String apiVersion,
    required String licence,
    required String faceId,
    required String nonce,
  }) {
    return TencentCloudFaceVerifyPlatform.instance.launchVerify(
      userId: userId,
      sign: sign,
      appId: appId,
      orderNo: orderNo,
      apiVersion: apiVersion,
      licence: licence,
      faceId: faceId,
      nonce: nonce,
    );
  }
}
  1. 使用方式
import 'package:flutter/material.dart';

import 'package:tencent_cloud_face_verify/tencent_cloud_face_verify.dart';

class VerifyPage extends StatefulWidget {
  const VerifyPage({super.key});

  @override
  State<VerifyPage> createState() => _VerifyPageState();
}

class _VerifyPageState extends State<VerifyPage> {
  final TencentCloudFaceVerify tencentCloudFaceVerify =
      TencentCloudFaceVerify();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("实名认证"),
      ),
      body: Column(
        children: [
          TextButton(
              onPressed: () async {
                final Map<String, String> a = {"orderNo":"orderNum0000000001","appId":"TIDAuFAT","sign":"39299F4E10C61481A30CE4B7E257BE1467DA72C7","faceId":"tx011818046f90367f8413489ebfe270","version":"1.0.0","nonce":"pok0utbgOyTY5k71bI1DUk1DnHuSGOvD","userId":"userId000000000001"};
                final Map<Object?, Object?> result =
                    await tencentCloudFaceVerify.launchVerify(
                  userId: a["userId"] ?? "",
                  sign: a["sign"] ?? "",
                  appId: a["appId"] ?? "",
                  orderNo: a["orderNo"] ?? "",
                  apiVersion: a["version"] ?? "",
                  licence:
                      "Q5sBOcmVygfQJjdMcItosSpbz9ivBiY4RiMPpoogd9xpOjUQk9uqkxE/w9h8yTPR8pklsNZjOXT3ikedc9UEDNHKmP2qFVc8oFQotqOu5fCTC3iFTodmJ5TQ6Mh6hjqgD+rIXBQcztoQb8EpEQc0msh9oCpFVsMp1EcU+j4OcJwR0yFkqvU+aLnXQ7mhhWX2779bTejo59+V2SeM6xrE27gdrC7J4BCgu21vCIsiX/4VmMSJp0QkQzPDHK6EeMaLsZ2Hjn5u6r05YoTR/ROh8ie5E0B0vv3Qk9Sc0J8vgnkzdEW+HUtfR/1+twbHUcwWtoPZ+cM/h5ht3xC8bD4d5Q==",
                  faceId: a["faceId"] ?? "",
                  nonce: a["nonce"] ?? "",
                );
                print(result.toString());
              },
              child: Text("拉起实名认证")),
        ],
      ),
    );
  }
}

在H5页面的SSR中,客户端需要做哪些工作?

1、前言

作为前端开发,对于H5页面的SSR,我们一般只关心webview启动之后的工作,如数据请求、水合、渲染等。实际上,H5页面的SSR,需要和客户端高度配合,才能实现所需效果。比如在笔者的上一篇文章【 手把手带你实现 Vite+React 的简易 SSR 改造【含部分原理讲解】】中,简要提到了流式SSR+FCC优化这一工作便主要是借助客户端的缓存实现相应优化:
20250413105055.png

笔者除了前端开发外,对客户端开发也有一定了解(主要是iOS开发、flutter开发等),因此本文从iOS客户端的角度讲解所需要关注的细节,让读者对整个SSR流程更加了解。

2、客户端核心逻辑梳理

20250413115312.png

2-1 入口启动注册

在App启动时进行SSR模块的初始化(主要是注册自定义的 URL 协议类 SSRURLProtocol):

[RequestSSRHandler setup];

2-2 URL拦截和处理

  • SSRURLProtocol
    • 使用 NSURLProtocol 来拦截特定的网络请求。这些拦截基于请求 URL 的某些标志位或头字段。
    • canInitWithRequest: 方法用于判断是否对请求进行拦截,避免重复拦截,并进行必要的日志记录和异常处理。

2-3 请求发送与响应处理

  • RequestSSRHandler:
    • setup 方法设置与应用相关的请求参数,如 appkeyappverutdid 等,并进行统一管理。
    • sendRequest: 方法将请求发送给后端,可选择缓存命中的内容直接响应,优化响应时间。
    • ssrRequest:didReceiveResponse: 处理响应,检查缓存数据是否可用并适用的判断逻辑。
    • 对于特定错误进行降级处理(如网络错误),通过 requestOnline:receiver: 方法发起普通网络请求。

2-4 缓存管理

  • 缓存数据 ( SSRCacheData ) :

    • 负责序列化缓存的 HTML 数据,并保存相关的版本、过期时间等,用于快速响应请求。
    • 提供 getCacheHtmlsetCacheHtml 方法来管理缓存数据的存取,加速处理过程。
  • 缓存存储 ( FCCStorage ) :

    • 管理缓存的持久化。
    • 提供方法来保存、获取和移除缓存数据,以确保缓存的有效利用。
    • 使用特定查询参数和用户标识决定缓存的唯一键值,使缓存管理更具灵活性。

2-5 响应版本和开关校验

  • 在处理请求和响应时,对版本和开关的启用状态进行检查,确保缓存的正确性和适用性。
  • 确保在版本不匹配或开关关闭的情况下替代渲染方法,以保障应用的稳定性。

2-6 请求上下文管理

  • SSRRequestContext:
    • 管理请求的状态和数据,包括是否启用首 chunk 缓存、是否命中缓存、是否复用完缓存等。
    • produceResponse:produceData: 负责处理接收到的响应和数据,根据缓存状态进行处理,包括使用缓存、替代渲染、保存缓存等。
    • 提供 matchFirstChunkCache, saveFirstChunkCache, 和 matchFCC:cacheData:response: 等方法来管理首 chunk 缓存数据的匹配和存取。

2-7 错误处理与降级策略

  • 在请求失败的情况下,如果满足特定条件,会自动降级请求为普通在线请求,以保证系统的稳定性和用户体验。

3、附上WebViewController应有的一些逻辑

  1. WebView初始化与配置

    • 使用WKWebView,进行URL拦截。
    • 支持下拉刷新、自定义导航栏、状态栏样式等。
  2. 请求处理与拦截

    • 登录拦截。
    • URL安全校验,防止恶意链接。
    • 路由拦截,处理本地协议跳转。
  3. 性能监控与埋点

    • 页面加载时间统计。
    • 错误监控与上报。
    • 应用启动阶段H5页面加载的性能追踪。
  4. UI交互

    • 显示加载状态(自定义Loading动画)。
    • 处理横竖屏切换。
    • 导航栏返回按钮和关闭按钮的逻辑。
  5. 其他功能

    • Cookie同步、第三方验证、字体注入等。

了解客户端处理的这些逻辑之后,可以考虑h5页面首屏进一步的性能优化:webview预热和文档预请求,具体的实现逻辑需要笔者和读者一起去学习探索:
7CE0DEC0-5FBB-41A3-AC06-487735635733.png

Trae + SwiftUI 1 小时实现一个单词本 Mac App

前言

在 AI 发展越来越好的现在,它的应用已经不仅仅限制于帮我们生成问题的答案,还可以直接通过对自然语言的理解帮助我们直接生成对象的代码。对于某些简单的场景,如模版代码实现、结构简单的 UI 绘制等,它现在已经做得很好,这对于程序员的生产力提升还是非常有帮助的。

接下来,我通过一个简单的单词本应用,来给大家展示一下 Trae 的真实体验。

应用功能

首先,我们需要将应用的功能通过自然语言去描述出来,比如这个单词本 App,主要包含三个功能,单词本、错词和已掌握三个模块,每个模块都是以列表的形式进行展示。单词本中的单词如果不熟悉可以添加到错词中,如果很熟悉就添加到已掌握中,且支持 SwiftData 。

下面是我梳理的需求描述:

  • 新建一个 Swift 文件,文件名为 Word,并在里面实现一个 Word 类,包含 title 字符串类型、isError 布尔类型、isMaster 布尔类型,需要支持 SwiftData
  • 生成一个长度为 50 的数组 words,元素为 Word 类型,title 为随机的英文单词,10 个元素 isErrortrue,5 个元素 isMastertrue,其余的 isErrorisMasterfalse
  • 在侧边栏实现三个按钮,标题分别为单词本,错题,已掌握,点击按钮切换右侧视图。
  • 单词本、错词、已掌握三个 detail 都为列表形式。
  • 单词本列表内容为 words 中的所有元素,表格样式包含一个文本展示单词,两个按钮,一个按钮是添加到错词,若该模型的 isErrortrue 隐藏该按钮,若为 false 才显示。点击该按钮,将该条数据模型的 isError 赋值为 true。一个按钮是已掌握,若该模型的 isMastertrue 隐藏该按钮,若为 false 才显示。点击该按钮,将该条数据模型的 isMaster 赋值为 true
  • 错词列表内容为 words 中 isErrortrue 的所有元素,表格样式包含一个文本展示单词,一个按钮已掌握,点击该按钮,将该条数据模型的 isMaster 赋值为 trueisError 赋值为 false
  • 已掌握列表内容为 wordsisMastertrue 的所有元素,表格样式包含一个文本展示单词,一个按钮移除,点击该按钮,将该条数据模型的 isMaster 赋值为 false

梳理完,我们就可以通过 Trae 进行代码创建了。

Trae

首先,我们创建一个 SwiftUI 的 macOS app,然后通过 Trae 打开该项目。接着在 AI 对话流中,通过 #Folder 来选定当前文件夹,将第一条需求复制进去点击回车即可生成。

截屏2025-04-13 10.15.14.png

对话流中会生成代码的详细解释,右侧是代码实现,头部有拒绝和接受的选项,点击接受,代码就会自动写入项目中。

其余的需求描述我们需要 #File 选定相应的文件进行需求转代码实现。这里就不一一举例赘述了。

下面让我们来看下 Trae 实现的效果:

录屏2025-04-13 10.18.01.gif

小瑕疵

在代码实现过程中,虽然大部分代码都是正确可编译通过的,但还是碰到了下面的两个小问题:

  • if words.isEmpty { generateWords() } 直接写在了 View 中,代码视图如下:
var body: some View {
NavigationSplitView {
    VStack {}
} detail: {
    if words.isEmpty { generateWords() } // 这里会编译报错
    List(words, id: \.self) { word in
    ...
    }
}

正确的代码:

var body: some View {
NavigationSplitView {
    VStack {}
} detail: {
    if words.isEmpty { generateWords() } // 这里会编译报错
    List(words, id: \.self) { word in
    ...
    }.onAppear {
        if words.isEmpty {
            generateWords()
        }
    }
}
  • if !word.isMaster 写成了 if!word.isMaster,这个错误感觉有点不应该...

总结

从这个小例子的使用感受上来说,对开发者的帮助肯定是正大于负的,比我想象中的要聪明很多。希望大家能够拥抱变化,早早的享受到 AI 的红利。

音视频学习笔记十六——图像处理之OpenCV基础一

题记:前文介绍GPUImage滤镜链的原理,但实际上要写出效果,还需要理解其中图片处理的过程,所以本章开始会介绍一些OpenCV基础相关。图像处理需要用到很多专业的算法,本人业余学习略知皮毛,只是庶竭驽钝叙其所得,在音视频学习Demo有一些的示例。文章或代码若有错误,也希望大佬不吝赐教。

opencv绘图.jpg

一、OpenCV简介

OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉和机器学习库,广泛应用于人脸识别与生物识别、自动驾驶、工业检测等,核心功能包括:

  • 图像处理:滤波、边缘检测、几何变换、颜色空间转换、直方图均衡化等。
  • 视频分析:运动检测、光流检测等。
  • 特征提取与匹配:SIFT、SURF、ORB、角点检测等。
  • 目标检测与识别:Haar级联分类器(人脸检测)、HOG+SVM(行人检测)、深度学习模型(YOLO、SSD)。
  • 机器学习:支持向量机(SVM)、神经网络等算法。

二、基础操作

2.1. 输入/输出

// 读取图像
cv::Mat img = cv::imread("xxx/xxx.jpg", cv::IMREAD_COLOR);
// 保存图像
cv::imwrite("xxx/xxx.jpg", img);

cv::imshow(winname, img)创建窗口显示,移动端没有实现,iOS端转换为UIImage:

- (UIImage *)matToUIImage:(const cv::Mat&)mat {
    NSData *data = [NSData dataWithBytes:mat.data length:mat.elemSize() * mat.total()];
    CGColorSpaceRef colorSpace = mat.channels() == 1 ? CGColorSpaceCreateDeviceGray() : CGColorSpaceCreateDeviceRGB();
    
    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
    CGImageRef imageRef = CGImageCreate(mat.cols, mat.rows, 8, 8 * mat.channels(), mat.step[0], colorSpace, kCGImageAlphaNone|kCGBitmapByteOrderDefault, provider, NULL, false, kCGRenderingIntentDefault);
    UIImage *image = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);
    CGDataProviderRelease(provider);
    CGColorSpaceRelease(colorSpace);
    
    return image;
}

2.2. Mat对象

Mat基本是OpenCV中基本操作单元,可以从图片中读取(channels为BGR注意与移动端常用的RGB区别),也可以创建空矩阵。

// 空矩阵
cv::Mat emptyMat;

// 指定尺寸和类型(行,列,数据类型)
cv::Mat mat(480, 640, CV_8UC3);          // 3通道 8位无符号(BGR图像)
cv::Mat floatMat(100, 100, CV_32FC1);    // 单通道浮点矩阵

// 初始化值
cv::Mat redMat(100, 100, CV_8UC3, cv::Scalar(0, 0, 255)); // 全红色图像
cv::Mat ones = cv::Mat::ones(3, 3, CV_32F); // 全1矩阵

Mat的数据实际存储在u(UMatData)中,而data的内存管理,使用引用计数,可以使用mat.u->refcount查看引用的计数。

  • 浅拷贝:默认赋值或传参,共享数据内存

    cv::Mat shallow = mat;
    
  • 深拷贝:独立内存

    cv::Mat deep = mat.clone();
    // 或
    mat.copyTo(deep);
    

2.2.1. 访问和修改像素

  • 单通道(灰度):
uchar pixel = mat.at<uchar>(y, x); // 读取 (y,x) 处的值(注意行列顺序!)
mat.at<uchar>(y, x) = 255;         // 修改
  • 多通道(如 BGR 图像):
cv::Vec3b& pixel = mat.at<cv::Vec3b>(y, x); 
pixel[0] = 255; // 蓝色通道
pixel[1] = 0;   // 绿色通道
pixel[2] = 0;   // 红色通道
  • 使用指针高效遍历:
for (int i = 0; i < mat.rows; i++) {
    uchar* row = mat.ptr<uchar>(i);
    for (int j = 0; j < mat.cols; j++) {
        row[j] = ...; // 修改像素
    }
}

2.2.2. 图像处理操作

  • 调整大小:
cv::Mat resized;
cv::resize(inputMat, resized, cv::Size(newWidth, newHeight));
  • 颜色空间转换:
cv::Mat gray;
cv::cvtColor(colorMat, gray, cv::COLOR_BGR2GRAY);
  • 旋转:
cv::Mat rotated;
cv::rotate(inputMat, rotated, cv::ROTATE_90_CLOCKWISE);
旋转.jpg
  • 裁剪 ROI(Region of Interest):
int x = (cols - 200) / 2;
int y = (rows - 200) / 2;
cv::Rect roi_rect(x, y, 200, 200);
roi显示.jpg

矩阵运算

cv::Mat A = ... , B = ... , C;
cv::add(A, B, C);           // 矩阵加法
cv::multiply(A, B, C);      // 逐元素乘法
C = A * B;                  // 矩阵乘法(非逐元素)
cv::transpose(A, C);        // 转置

数据类型转换

cv::Mat floatMat;
mat.convertTo(floatMat, CV_32F, 1.0/255.0); // 转为浮点并归一化
  • 单通道显示

cv::split分离通道操作,注意是按照BGR的顺序,所以R通道为channels[2]

std::vector<cv::Mat> channels;
cv::split(mat, channels);

// 创建零矩阵并合并三通道
cv::Mat zeroMat = cv::Mat::zeros(mat.size(), CV_8UC1);
std::vector<cv::Mat> mergedChannels{zeroMat, zeroMat, channels[2]};
cv::Mat des;
cv::merge(mergedChannels, des);
红色通道.jpg

Blend效果

// 调整img2尺寸与输入图像匹配
cv::resize(img2, img2, mat.size(), 0, 0, cv::INTER_LINEAR);
// 使用addWeighted进行混合
cv::addWeighted(mat, 0.6, img2, 0.4, 0.0, blended);
融合.jpg

二值操作

cv::Mat gray, dst;
// 转换为灰度图像
cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);

// 应用Otsu二值化
cv::threshold(gray, dst, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
return dst;
二值效果.jpg

三、形态学处理

形态学是一类基于图像形状的图像处理技术,以下图为例,看一下形态学的变化。形态学是对实际上会对各个通道进行独立操作,默认是对单通道图像(如灰度图或二值图)操作,所以一般使用二值图看效果。

burr.jpg

3.1. 腐蚀

腐蚀操作原理是取邻域最小值,如下图,处理像素点1时,检查周围像素点(MORPH_RECT),取色值最小的点(右下),所以当前的点变成黑色。换个角度,黑色像素点会把周围点都变成黑色,像黑色来腐蚀了白色。

腐蚀原理.jpg

代码如下:

cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3));
cv::morphologyEx(mat, mat, cv::MORPH_ERODE, kernel);

效果如图,整体变小了,毛刺少了很多。

腐蚀.jpg

3.2. 膨胀

膨胀操作原理是取邻域最大值,就是和腐蚀相反的操作。如下图,处理像素点1时,检查周围像素点(MORPH_RECT),取色值最大的点(右下),所以当前的点变成白色。换个角度,白色像素点会把周围点都染白,像白色像素进行了膨胀。

膨胀原理.jpg

代码如下:

cv::Mat dilated;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3));
cv::dilate(mat, dilated, kernel);

效果如图,整体变大了,毛刺变得更粗壮了。

膨胀.jpg

3.3. 开运算

上述两种运算都会原来的形状(变大或缩小),而先腐蚀后膨胀就是开运算。开运算一般用于去噪,如下图,先腐蚀会让黑色区域变大,从而中间的白色噪点消失,再膨胀白色区域恢复(原来的噪点消失)。

开运算.jpg

代码如下:

cv::Mat opened;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5,5));
cv::morphologyEx(mat, opened, cv::MORPH_OPEN, kernel);

效果如图,可以通过改变kernel大小调整效果(5x5效果):

开运算效果.jpg

3.4. 闭运算

闭运算是开运算相反的操作先膨胀再腐蚀,运用孔洞填充,如下图字母T,由于打印或拍摄问题,有些像素点缺失。先膨胀就可以把区域连通,再腐蚀恢复成原来大小。

闭运算.jpg

代码如下:

cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5,5));
cv::morphologyEx(mat, mat, cv::MORPH_CLOSE, kernel);

效果如图,可以通过改变kernel大小调整效果(5x5效果),连通了毛刺中间的区域:

闭操作效果.jpg

3.5. 礼帽

礼帽操作是用原图-开运算,开运算作用是去毛刺,那么礼帽的作用就是获取图片中的毛刺,提取亮细节。

代码如下:

cv::Mat result;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3));
cv::morphologyEx(mat, result, cv::MORPH_BLACKHAT, kernel);

效果如图,获取到毛刺:

礼帽.jpg

3.6. 黑帽

黑帽操作是用闭运算-原图,闭运算作用是连通,那么黑帽的作用就是提取暗细节。

黑帽原理.jpg

代码如下:

cv::Mat result;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3));
cv::morphologyEx(mat, result, cv::MORPH_BLACKHAT, kernel);

Socket 与 WebSocket 的区别是啥?

一、基础概念

1. Socket

  • 定义
    Socket 是操作系统提供的 网络通信接口,是应用程序与网络协议(如 TCP/UDP)之间的桥梁。它通过底层协议直接与网络交互,提供 全双工通信能力
    • 协议类型
      • TCP Socket:基于 TCP 协议,保证数据可靠传输(有序、无丢失)。
      • UDP Socket:基于 UDP 协议,提供低延迟传输(但不保证可靠性)。
    • 特点
      • 灵活性:可完全控制数据格式、连接状态和传输逻辑。
      • 低层控制:适合需要精细控制网络行为的场景(如游戏、物联网设备)。

2. WebSocket

  • 定义
    WebSocket 是一种 应用层协议,基于 TCP,通过 HTTP 协议升级为持久化双向连接。它通过一次握手后,建立长期稳定的通信通道。
    • 特点
      • 全双工通信:客户端和服务器可随时主动发送数据。
      • 轻量高效:数据帧头部仅 2-10 字节,传输效率高。
      • 兼容性:原生支持浏览器和移动设备,适合实时 Web 应用。

二、核心区别对比

1. 协议与连接方式

维度 Socket WebSocket
协议层级 传输层(TCP/UDP)或应用层(自定义) 应用层(基于 HTTP/TCP)
连接建立 通过 TCP 三次握手直接建立连接 通过 HTTP 协议升级(如 GET /ws
连接模式 短连接(需手动维护长连接) 长连接(持久化,一次建立长期有效)
数据格式 纯字节流(需自行解析) 帧格式(自动分隔和重组数据)

2. 数据传输与性能

维度 Socket WebSocket
传输效率 低延迟(直接 TCP) 实时性高,但略高于纯 TCP(因握手开销)
头部开销 无额外 HTTP 头 初始握手有 HTTP 头,后续数据头小
可靠性 TCP 保证可靠传输 基于 TCP,但需处理协议层错误

3. 开发复杂度与维护

维度 Socket WebSocket
连接管理 需手动处理心跳、重连、错误恢复 自动管理连接(内置心跳机制)
数据处理 需自行解析分包、处理编码/解码 自动处理数据分帧(如文本/二进制)
安全性 需手动实现加密(如 SSL/TLS) 支持 WSS(WebSocket over TLS)
防火墙穿透 可能被拦截(需开放特定端口) 通过 HTTP/80 或 HTTPS/443 穿透

三、iOS 开发实践

1. Socket 实现

  • 常用框架
    • GCDAsyncSocket(TCP):支持异步操作,适合复杂协议控制。
    • SwiftSocket(TCP/UDP):轻量级,适合简单通信。
  • 开发流程
    1. 建立连接:通过 connect(toHost:onPort:) 建立 TCP 连接。
    2. 数据收发:手动发送字节流(需协议解析)。
    3. 错误处理:监听 didDisconnectWithError 等回调。
  • 示例代码(TCP)
    let socket = GCDAsyncSocket()
    do {
        try socket.connect(toHost: "example.com", onPort: 8080, withTimeout: 10)
        socket.readData(withTimeout: -1, tag: 0)
    } catch {
        print("连接失败:\(error)")
    }
    

2. WebSocket 实现

  • 常用框架
    • Starscream:轻量级,支持文本/二进制消息。
    • Socket.IO:兼容多种传输协议(WebSocket 优先)。
  • 开发流程
    1. 建立连接:通过 connect() 发送 HTTP 升级请求。
    2. 消息监听:通过 onTextonData 等回调接收数据。
    3. 自动重连:支持配置重连策略(如指数退避)。
  • 示例代码(Starscream)
    import Starscream
    
    let socket = WebSocket(url: URL(string: "wss://example.com/ws")!)
    socket.onConnect = { print("连接成功") }
    socket.onText = { text in print("收到文本消息:\(text)") }
    socket.connect()
    

四、场景选择与性能优化

1. 适用场景

场景类型 Socket WebSocket
实时性要求极高 游戏、物联网设备控制(毫秒级响应) 聊天、股票行情(秒级响应)
低延迟传输 传感器数据流、在线游戏同步 实时消息推送、音视频同步
复杂协议控制 自定义二进制协议(如金融交易) 标准化协议(如聊天协议)
浏览器/移动端混合开发 需额外适配(如 WebSockets 桥接) 原生支持,适合跨平台实时通信

2. 性能优化

Socket 优化建议

  • 复用连接:避免频繁建立/关闭连接,复用长连接。
  • 数据压缩:使用二进制协议(如 Protobuf)减少传输体积。
  • 心跳机制:定期发送心跳包检测连接状态(如每 30 秒一次)。

WebSocket 优化建议

  • 消息队列:批量发送消息以减少网络开销。
  • 协议压缩:启用 Permessage-Deflate 压缩(需服务器支持)。
  • 服务器选择:使用高性能 WebSocket 服务器(如 Nginx、Socket.IO 服务端)。

五、总结:如何选择?

  • 选 Socket 的场景

    • 需要 底层协议控制(如自定义二进制协议)。
    • 高实时性要求(如游戏、传感器数据)。
    • 局域网或封闭环境(防火墙限制较少)。
  • 选 WebSocket 的场景

    • 快速开发:标准化协议,开发效率高。
    • 实时 Web 应用:聊天、在线协作、股票推送。
    • 混合开发:需要与浏览器或跨平台应用通信。

六、关键术语表

术语 解释
TCP 可靠传输协议,保证数据顺序和完整性。
UDP 无连接协议,低延迟但不保证可靠性。
HTTP 升级 WebSocket 通过 HTTP 请求升级连接。
Permessage-Deflate WebSocket 的数据压缩扩展协议。

SwiftUI 国际化

已经2025年了,不要再用Strings File这种过时的文件来实现多语。 Xcode15(2023年9月)推出了新的多语格式String Catalog。

iOS开发:关于路由

在iOS开发中引入路由框架一直是一个有争议的话题。

因为即使不使用路由框架,似乎也不会有太大的影响。那么我们先来回顾一下几个典型的跳转场景:

从外部跳转到App

  1. Safari浏览器网页点击事件跳转到App的页面

  2. App在挂起或者杀死状态,收到推送跳转到App的页面

  3. 收到短信,短信里面有短链接跳转到App的页面

  4. 从邮件中的链接跳转到App的页面

  5. 从社交媒体应用(如微信、微博)中的链接跳转到App的页面

  6. 从App的小组件跳转到App的页面

  7. Siri、ShotCut进行跳转

App内部跳转

  1. 单一主工程,无业务模块依赖,页面间进行跳转

  2. 多模块工程,多个模块之间可以随意跳转

需要注意的是,对于多模块工程,模块可能是第三方开发的,并不遵守内部开发标准,这种情况不在考虑范围内。

路由中心

  1. 跳转到微信小程序:其实跳转到其他App的小程序也可以认为是这种业务场景。目前已经无法从微信小程序跳转到App了。点击查看
  2. 跳转到其他App:这种情况跳转API相对固定,传值也有规则,无需路由框架,不在我们的讨论之中。
  3. 跳转到Safari浏览器:这种情况跳转API也相对固定,传值也有规则,无需路由框架,不在我们的讨论之中。

image.png

上图展示了非常复杂的跳转场景。在日常开发过程中,比如推送跳转到不同页面,实际上是通过推送信息中的字符串创建一个枚举映射判断,不同的字段跳转到不同的页面。浏览器和短信消息以及App开屏广告跳转也是使用枚举映射的逻辑。

维护一个枚举映射表可以完成这个功能,但随着业务量的增大,这种方式虽然可行,但不够友好,下面是伪代码示例:


func pushToAppPage(model: LaunchAdModel,
                   tabbarController: UITabBarController,
                   navigationController: UINavigationController) {
        switch model.appPage {
        case "app_home_page":
            break
        case "app_message_center":/// 消息中心
            navigationController.pushViewController(MessageCenterController(), animated: true)
        case "app_message_center_detail":/// 公告详情
            let vc = AppMessageDetailController()
            vc.messageId = model.appItemId
            navigationController.pushViewController(vc, animated: true)
        case "community_topics":// 资讯
            break
        case "activity":
            break
        /// 业务增加会case也逐渐增加,如果入参规则不同,还需要不同的构建器,初始化方法和赋值

        }
}

如果使用路由框架将页面和路由表提前绑定,此时外部跳转进来,只需一行代码即可搞定。可以认为是将集中的枚举映射分散到了路由框架中。

其实我完全可以把上面的伪代码封装成一个路由中心,然后制定一系列入参传参规则来保证一致性,但是我也可以直接使用现有的框架来避免我重复造轮子,我只用了解框架的使用与传参规则就可以。 另外,良好的跳转逻辑,不仅需要移动端制定规则,还需要后端配合,完成数据下发的格式的对应。 可以想象一下再App中增加了一个路由中心,所有的跳转情况逻辑与跳转Action都由路由管理,然后再从路由中心发出去: image.png

TheRouter

我最近研究了一下相关框架,目前觉得TheRouter的功能和业务场景符合要求,因为它同时兼容OC,所以在某些语法上看起来很怪异。

其实所有的路由这种从前端借鉴过来的舶来物,总需要这样个过程:

  • 注册路由

  • 保证注册之后再使用路由

  • 异常路由侧进去了定义好的错误页面

比如Flutter中使用GetX的路由,我们会这样:


abstract class Routes {

  Routes._();

  static const coinRink = '/coinRink';

  static const unknown = "/unknown";

  ///页面合集

  static final routePage = [

    GetPage(
      name: coinRink,
      page: () => const CoinRankPage(),
      binding: CoinRankBinding(),
      middlewares: [LoginMiddleware()],
    ),

   GetPage(
      name: unknown,
      page: () => const UnknownPage(),
    ),
  ];

  static final unknownPage = GetPage(
    name: Routes.unknown,
    page: () => const UnknownPage(),
  );
}

TheRouter对比这种思路,手动注册之外,有一个我觉得很有特色功能就是通过runtime遍历进行路由的自动注册,减少了手动注册的不舒适度。


  let beginRegisterTime = CFAbsoluteTimeGetCurrent()

  var resultXLClass = [AnyClass]()

  let bundles = CFBundleGetAllBundles() as? [CFBundle]

  for bundle in bundles ?? [] {
      let identifier = CFBundleGetIdentifier(bundle);
      if let id = identifier as? String {
          if excludeCocoapods {
              if  id.hasPrefix(kSAppleSuffix) || id.hasPrefix(kSCocoaPodsSuffix) {
                  continue
              }
          } else {
              if  id.hasPrefix(kSAppleSuffix) {
                  continue
              }
          }
      }

      guard let execURL = CFBundleCopyExecutableURL(bundle) as NSURL? else { continue }
      let imageURL = execURL.fileSystemRepresentation
      let classCount = UnsafeMutablePointer<UInt32>.allocate(capacity: MemoryLayout<UInt32>.stride)
      guard let classNames = objc_copyClassNamesForImage(imageURL, classCount) else {
          continue
      }

      for idx in 0..<classCount.pointee {
          let currentClassName = String(cString: classNames[Int(idx)])
          guard let currentClass = NSClassFromString(currentClassName) else {
              continue
          }

          if class_getInstanceMethod(currentClass, NSSelectorFromString("methodSignatureForSelector:")) != nil,
             class_getInstanceMethod(currentClass, NSSelectorFromString("doesNotRecognizeSelector:")) != nil {

              if let cls =  currentClass as? UIViewController.Type {
                  resultXLClass.append(cls)
              }
          }
#if DEBUG
          if let clss = currentClass as? CustomRouterInfo.Type {
              apiArray.append(clss.patternString)
              classMapArray.append(clss.routerClass)
          }
#endif
      }
  }


  for i in 0 ..< resultXLClass.count {
      let currentClass: AnyClass = resultXLClass[i]
      if let cls = currentClass as? TheRouterable.Type {
          let fullName: String = NSStringFromClass(currentClass.self)
          if fullName.contains(kSADelegateClassSensorsSuffix)  {
              break
          }

          for s in 0 ..< cls.patternString.count {

              if fullName.contains(NSKVONotifyingPrefix) {
                  let range = fullName.index(fullName.startIndex, offsetBy: NSKVONotifyingPrefix.count)..<fullName.endIndex
                  let subString = fullName[range]
                  registerRouterList.append([TheRouterPath: cls.patternString[s], TheRouterClassName: "\(subString)", TheRouterPriority: "\(cls.priority)"])
              } else {
                  registerRouterList.append([TheRouterPath: cls.patternString[s], TheRouterClassName: fullName, TheRouterPriority: "\(cls.priority)"])
              }
          }

      } else if currentClass.self.conforms(to: TheRouterableProxy.self) {
          let fullName: String = NSStringFromClass(currentClass.self)
          if fullName.contains(kSADelegateClassSensorsSuffix)  {
              break
          }

          for s in 0 ..< currentClass.patternString().count {
              if fullName.contains(NSKVONotifyingPrefix) {
                  let range = fullName.index(fullName.startIndex, offsetBy: NSKVONotifyingPrefix.count)..<fullName.endIndex
                  let subString = fullName[range]
                  registerRouterList.append([TheRouterPath: currentClass.patternString()[s], TheRouterClassName: "\(subString)", TheRouterPriority: "\(String(describing: currentClass.priority()))"])
              } else {
                  registerRouterList.append([TheRouterPath: currentClass.patternString()[s], TheRouterClassName: fullName, TheRouterPriority: "\(String(describing: currentClass.priority()))"])
              }
          }
      }
  }
  let endRegisterTime = CFAbsoluteTimeGetCurrent()

另外需要注意,在最新的Xcode16下面,Debug模式下面自动注册runtime不起作用,需要修改一下工程配置:

Xcode16 下 Debug 模式 ENABLE_DEBUG_DYLIB 选项默认开启,开启之后 objc_copyClassNamesForImage 主工程 image 调用失败,Debug 模式下会使用 **.debug.dylib文件,所以会有点问题。可以先将ENABLE_DEBUG_DYLIB 关闭

image.png

github.com/HuolalaTech…

同时TheRouter会有一个强制校验过程,也就是必须在工程中手动维护一张路由表,来保证自动注册的路由表和手动注册的路由表一致,这种措施是为了保证在Debug环境下的一致性,当然如果工程不那么复杂,这个功能不用也罢:


/// - Parameters:
///   - excludeCocoapods: 排除一些非业务注册类,这里一般会将 "com.apple", "org.cocoapods" 进行过滤,但是如果组件化形式的,创建的BundleIdentifier也是
///   org.cocoapods,这里需要手动改下,否则组件内的类将不会被获取。
///   - urlPath: 将要打开的路由path
///   - userInfo: 路由传递的参数
///   - forceCheckEnable: 是否支持强制校验,强制校验要求Api声明与对应的类必须实现TheRouterAble协议
///   - forceCheckEnable 强制打开TheRouterApi定义的便捷类与实现TheRouterAble协议类是否相同,打开的话,debug环境会自动检测,避免线上出问题,建议打开
return TheRouterManager.addGloableRouter(true, url, userInfo, forceCheckEnable: false)

TheRouter

如果项目是多模块组成,传统的push与pop可能需要模块对外暴露Controller,以保证可以构建控制器与页面跳转。使用路由框架可以抹掉这些细节与传参、构造器方法,对外暴露跳转路径即可。

同时我也在思考,如果一个Flutter项目也是多模块的情况下,主工程无法知道子模块的暴露的Page,是如何维护路由表的呢?

截止我发文的时候,掘金的货拉拉又发了一篇文章,《iOS货运用户App组件路由器设计与实践》,不过目前被删除了,也不知道是个啥情况,反正就是说他们还有一套与TheRouter的不同的路由框架,嗯,好吧~

结论

  1. 路由框架并不是iOS开发的必备工具。如果外部跳转到App场景少,App内部跳转简单,单一工程,或者多模块但模块间跳转场景少、不复杂,可以不用。

  2. 路由带来方便的同时,可能会导致页面切换转场动画的固定化,因为路由的目的是打开页面,而页面相关的动画等,如果放在路由框架中,显然又不太合适,所以当存在路由框架时,在需要使用转场动画时,可能无法尽善尽美。

  3. 如果外部跳转和App内部跳转复杂,可以考虑使用路由框架,以减轻维护逻辑的编写。同时如果考虑双端一致性,甚至可以一次配置,双端可行。

❌