与 AI 共舞:我的 Claude Code 一月谈
转眼间,我使用 Claude Code 已经整整一个月了。这段时间里,它迅速成为了开发者们的新宠,关于 Claude Code 的讨论充斥着我的社交媒体时间线。恰好有网友在 Discord 上让我聊聊 Claude Code,借此机会,我想梳理一下这段时间的使用感受,以及过去两年中 AI 为我的开发工作带来的便利与思考。
转眼间,我使用 Claude Code 已经整整一个月了。这段时间里,它迅速成为了开发者们的新宠,关于 Claude Code 的讨论充斥着我的社交媒体时间线。恰好有网友在 Discord 上让我聊聊 Claude Code,借此机会,我想梳理一下这段时间的使用感受,以及过去两年中 AI 为我的开发工作带来的便利与思考。
这里每天分享一个 iOS 的新知识,快来关注我吧
在应用开发的过程中,总会遇到一些突发情况,比如应用版本在上线后出现重大 bug 或崩溃,严重影响了大部分用户的使用体验。这种情况下,及时响应和修复问题至关重要,不仅可以减少用户的不良体验,还能防止应用评分的下降。
然而,即便你已经找到了问题的根源,并准备好了解决方案,想要快速发布更新版本,也并非完全由你决定。
因为首先苹果是不允许热更新的,每个版本发布都必须经过苹果的审核,其次审核是否通过完全取决于苹果,因此加快审核流程就显得尤为重要。
但很多 iOS 开发者可能不知道,其实可以通过“加急审核”来告知 App Review 团队有紧急更新需要审核,从而加快审核速度。
本文将详细介绍如何通过 Apple 开发者门户请求加急审核。
要请求加急审核,你需要前往 developer.apple.com 并登录到你的开发者账号。登录的账号必须具备管理需要请求加急审核的应用的权限。
2. 从列表中选择“应用审核”类别。
需要在表单中提供的信息包括:
你希望向应用审核团队提出的请求类型:在我们这种情况下,选择“请求加急审核”选项。
请求加急审核的人员姓名。
请求加急审核的人员邮箱。
拥有该应用的组织名称。
应用名称。
需要加急审核的版本平台。
完成以上步骤后,点击最后的“Send”按钮,将请求提交给应用审核团队。
正常情况下,几个小时后会收到苹果审核的结果。
虽然加急审核是开发者工具箱中的一个强大工具,是在紧急情况下将应用快速交到用户手中的最佳方式,但它应仅在特殊情况下使用。正如 Apple 在审核表单中提到的:
如果你面临紧急情况,比如修复关键 bug 或发布应用以配合某个事件,可以通过填写此表单请求加急审核。
同时需要注意,太多次的加急审核请求可能会导致 Apple 对你的反感,然后可能会导致对你之后的加急请求不予理会,因此要谨慎使用。
这里每天分享一个 iOS 的新知识,快来关注我吧
本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!
@concurrent
是 Swift 6.2 引入的新特性,用于明确标记需要卸载到全局执行器(后台线程)的函数。它与 nonisolated(nonsending)
共同构成 Swift 并发模型的改进,旨在解决以下问题:
nonisolated(nonsending)
(统一行为)// 始终在调用者的执行器上运行
nonisolated(nonsending) func decode<T: Decodable>(_ data: Data) async throws -> T
版本 | 行为差异 |
---|---|
Swift 6.1 | 异步函数 → 全局执行器 |
Swift 6.1 | 同步函数 → 调用者执行器 |
Swift 6.2 | 统一在调用者执行器运行 |
@concurrent
(显式卸载)// 明确卸载到全局执行器
@concurrent func decode<T: Decodable>(_ data: Data) async throws -> T
特性 | 说明 |
---|---|
自动标记 nonisolated
|
无需额外声明 |
创建新隔离域 | 要求状态实现 Sendable
|
使用限制 | 不能与显式隔离声明(如 @MainActor )共存 |
@concurrent
class Networking {
// 主线程安全的网络请求
func loadData(from url: URL) async throws -> Data { ... }
// 耗时解码 → 适合 @concurrent
@concurrent func decode<T: Decodable>(_ data: Data) async throws -> T {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}
func getFeed() async throws -> Feed {
let data = try await loadData(from: Feed.endpoint)
// 避免阻塞调用者线程
let feed: Feed = try await decode(data)
return feed
}
}
Sendable
约束特性 |
nonisolated (旧) |
nonisolated(nonsending) |
@concurrent |
---|---|---|---|
执行位置 | 异步→全局/同步→调用者 | 始终在调用者执行器 | 全局执行器 |
隔离域 | 可能创建新隔离域 | 不创建新隔离域 | 创建新隔离域 |
状态要求 | 潜在需要 Sendable
|
无特殊要求 | 必须 Sendable
|
使用场景 | 兼容旧版 | 默认推荐 | 显式并发需求 |
代码可读性 | 意图模糊 | 行为明确 | 意图明确 |
最近又开始学习Swift了,前段时间在AI的帮助下做了一个可以和大模型聊天的软件,当时VAD的功能很头痛,搜了下有一个付费的Cobra VAD,另外就只有靠音频能量判断了,这种方式不准。
最近做的东西又有VAD需求了,研究了很久后可以在Swift里跑Silero VAD了,直接把代码丢出来。
由于我不知道如何把ONNX模型转成Core ML的,官方ONNX Runtime只有Pods的包,我用的是另一个Swift Packags版本的ONNX Runtime,用Pods的包要把import OnnxRuntimeBindings
换一下。
//
// SileroVAD.swift
// Real-time Captions
//
// Created by yu on 2025/6/30.
//
import AVFoundation
import Foundation
import OnnxRuntimeBindings
/// 说话起止事件回调
protocol SileroVADDelegate: AnyObject {
/// 检测到"开始说话"
/// - Parameter probability: 触发时那一帧的 VAD 概率
func vadDidStartSpeech(probability: Float)
/// 检测到"结束说话"
/// - Parameter probability: 触发时那一帧的 VAD 概率
func vadDidEndSpeech(probability: Float)
}
final class SileroVAD {
// MARK: - 可调参数
public struct Config {
/// 进入说话的高阈值
public var threshold: Float = 0.5
/// 退出说话的低阈值(自动与 threshold 保持 0.15 差值)
public var negThreshold: Float { max(threshold - 0.15, 0.01) }
/// 连续多长时间高于 threshold 才算"开始说话"(秒)
public var startSecs: Float = 0.20
/// 连续多长时间低于 negThreshold 才算"结束说话"(秒)
public var stopSecs: Float = 0.80
/// 采样率,仅支持 8 kHz / 16 kHz
public var sampleRate: Int = 16000
public init() {}
}
// MARK: - 内部状态
private enum VADState {
case silence // 静音状态
case speechCandidate // 可能开始说话
case speech // 正在说话
case silenceCandidate // 可能结束说话
}
private enum VADError: Error {
case modelLoadFailed(String)
case invalidAudioFormat(String)
case inferenceError(String)
case tensorCreationFailed(String)
}
// MARK: - 核心属性
private let session: ORTSession
private var state: ORTValue
private let config: Config
public weak var delegate: SileroVADDelegate?
// 状态机相关
private var vadState: VADState = .silence
private var speechFrameCount = 0
private var silenceFrameCount = 0
private var lastProbability: Float = 0.0
// 阈值(基于配置计算的帧数)
private let speechFrameThreshold: Int
private let silenceFrameThreshold: Int
// 音频缓冲
private var sampleBuffer: [Float] = []
private let bufferSize = 512
// MARK: - 公有方法
public init(config: Config = Config(), delegate: SileroVADDelegate? = nil) {
self.config = config
self.delegate = delegate
// 计算帧数阈值(基于配置动态计算窗口时长)
let windowDurationSecs = Float(bufferSize) / Float(config.sampleRate)
speechFrameThreshold = Int(config.startSecs / windowDurationSecs)
silenceFrameThreshold = Int(config.stopSecs / windowDurationSecs)
guard let modelPath = Bundle.main.path(forResource: "silero_vad", ofType: "onnx") else {
fatalError("SileroVAD: Model file not found in bundle")
}
do {
let env = try ORTEnv(loggingLevel: .warning)
let sessionOptions = try ORTSessionOptions()
// 性能优化配置
try sessionOptions.setGraphOptimizationLevel(.all)
try sessionOptions.setIntraOpNumThreads(Int32(ProcessInfo.processInfo.processorCount))
// 尝试启用Core ML硬件加速
do {
let coreMLOptions = ORTCoreMLExecutionProviderOptions()
try sessionOptions.appendCoreMLExecutionProvider(with: coreMLOptions)
print("SileroVAD: Using Core ML Execution Provider (Neural Engine/NPU)")
} catch {
print("SileroVAD: Using optimized CPU execution with \(ProcessInfo.processInfo.processorCount) cores")
}
session = try ORTSession(env: env, modelPath: modelPath, sessionOptions: sessionOptions)
} catch {
fatalError("SileroVAD: Failed to create ONNX session: \(error)")
}
// 初始化RNN状态 (shape: 2, 1, 128)
let stateData = Array(repeating: Float(0.0), count: 2 * 1 * 128)
do {
state = try ORTValue(tensorData: NSMutableData(data: Data(bytes: stateData, count: stateData.count * 4)),
elementType: .float,
shape: [2, 1, 128])
} catch {
fatalError("SileroVAD: Failed to create initial state tensor: \(error)")
}
}
/// 输入音频样本,自动处理状态检测
public func feed(_ samples: [Float]) {
sampleBuffer.append(contentsOf: samples)
// 当有足够样本时自动检测
while sampleBuffer.count >= bufferSize {
if let probability = performDetection() {
updateVADState(probability: probability)
}
}
}
/// 重置内部状态机 & RNN 隐状态
public func reset() {
// 重置状态机
vadState = .silence
speechFrameCount = 0
silenceFrameCount = 0
lastProbability = 0.0
// 清空缓冲区
sampleBuffer.removeAll()
// 重置RNN状态
let stateData = Array(repeating: Float(0.0), count: 2 * 1 * 128)
do {
state = try ORTValue(tensorData: NSMutableData(data: Data(bytes: stateData, count: stateData.count * 4)),
elementType: .float,
shape: [2, 1, 128])
} catch {
print("SileroVAD: Failed to reset state tensor: \(error)")
}
}
// MARK: - 私有方法
private func performDetection() -> Float? {
guard sampleBuffer.count >= bufferSize else {
return nil
}
// 取出一个窗口的样本
let vadInput = Array(sampleBuffer.prefix(bufferSize))
sampleBuffer.removeFirst(bufferSize)
do {
let probability = try runInference(audioData: vadInput)
lastProbability = probability
return probability
} catch {
print("SileroVAD: Detection error: \(error)")
return nil
}
}
private func runInference(audioData: [Float]) throws -> Float {
guard audioData.count == 512 else {
throw VADError.invalidAudioFormat("Audio data must be exactly 512 samples")
}
// 创建输入张量
let inputTensor = try ORTValue(
tensorData: NSMutableData(data: Data(bytes: audioData, count: audioData.count * 4)),
elementType: .float,
shape: [1, 512]
)
// 创建采样率张量
var srData = Int64(config.sampleRate)
let srTensor = try ORTValue(
tensorData: NSMutableData(data: Data(bytes: &srData, count: 8)),
elementType: .int64,
shape: [1]
)
// 准备输入
let inputs: [String: ORTValue] = [
"input": inputTensor,
"state": state,
"sr": srTensor,
]
// 执行推理
let allOutputNames = try session.outputNames()
let outputs = try session.run(withInputs: inputs, outputNames: Set(allOutputNames), runOptions: nil)
// 提取结果
guard let outputTensor = outputs["output"] else {
throw VADError.inferenceError("Missing 'output' tensor")
}
guard let newStateTensor = outputs["stateN"] else {
throw VADError.inferenceError("Missing 'stateN' tensor")
}
// 更新状态
state = newStateTensor
// 提取概率值
let tensorData = try outputTensor.tensorData() as Data
let probability = tensorData.withUnsafeBytes { bytes in
bytes.load(as: Float.self)
}
return probability
}
private func updateVADState(probability: Float) {
let isHighProbability = probability >= config.threshold
let isLowProbability = probability <= config.negThreshold
switch vadState {
case .silence:
if isHighProbability {
vadState = .speechCandidate
speechFrameCount = 1
silenceFrameCount = 0
}
case .speechCandidate:
if isHighProbability {
speechFrameCount += 1
if speechFrameCount >= speechFrameThreshold {
vadState = .speech
delegate?.vadDidStartSpeech(probability: probability)
}
} else {
vadState = .silence
speechFrameCount = 0
}
case .speech:
if isLowProbability {
vadState = .silenceCandidate
silenceFrameCount = 1
speechFrameCount = 0
} else if isHighProbability {
// 继续说话,重置静音计数
silenceFrameCount = 0
}
case .silenceCandidate:
if isLowProbability {
silenceFrameCount += 1
if silenceFrameCount >= silenceFrameThreshold {
vadState = .silence
delegate?.vadDidEndSpeech(probability: probability)
}
} else if isHighProbability {
vadState = .speech
silenceFrameCount = 0
}
}
}
}
要下载模型silero_vad.onnx
丢进项目。
当然这个代码也是Claude帮我写的。
在 weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。
继 2025 年 2 月 Swift 社区论坛发布关于启动 Android Community Workgroup 的消息数月后,Swift.org 于上周正式宣布成立官方 Android 工作组。这标志着由官方主导的 Swift 安卓平台支持正式启动,未来 Swift 开发者有望获得更完善的安卓适配工具链与开发体验。
不过,在欣喜之余,我们也应正视一个现实:对于绝大多数 Swift 开发者来说,长期以来的开发工作深度依赖苹果生态,日常所用 API 多与系统框架强耦合。尽管 Swift 社区和苹果已着手推进 Foundation 的纯 Swift 化改造,并陆续提供更多跨平台基础库,但这距离满足实际跨平台开发的需求仍有相当差距。
不久前,Swift Package Index 在原有对苹果平台和 Linux 的兼容性标识基础上,新增了对 Android 与 Wasm 平台的支持,侧面反映出社区对多平台适配的重视。我也借此机会让自己的两个库完成了对 Linux 的兼容。不过在适配过程中也深刻体会到,目前还缺乏一个便捷、统一的跨平台开发环境。虽然这两个库的适配较为简单,仅通过 GitHub Actions 就完成了编译测试和修复,但若将来需要支持更多平台,社区能否构建一个便利、安全的适配机制将变得至关重要。
近年来,Swift 在多平台战略上的推进明显提速,但若想真正成为跨平台开发者的主流选择,仅靠官方与苹果的努力还远远不够。我们每一位 Swift 开发者的参与同样不可或缺。Swift 越强大,Swift 开发者越受益。Swift 的多平台生态,需要我们共同建设!
NotificationCenter 作为 iOS 开发中的经典组件,为开发者提供了灵活的广播——订阅机制。然而,随着 Swift 并发模型的不断演进,传统基于字符串标识和 userInfo
字典的通知方式暴露出了诸多问题。为了彻底解决这些痛点,Swift 6.2 在 Foundation 中引入了全新的并发安全通知协议:NotificationCenter.MainActorMessage
和 NotificationCenter.AsyncMessage
。它们充分利用 Swift 的类型系统和并发隔离特性,让消息的发布与订阅在编译期就能得到验证,从根本上杜绝了“线程冲突”和“数据类型错误”等常见问题。
在 Xcode 26 中,苹果正式推出了备受期待的 AI 编码助手 —— Coding Intelligence。相较于市面上已有的 AI 编程工具,苹果在系统提示词(system prompt)的设计上是否有自己的哲学?Peter Friese 借助 Proxyman 对其进行了深入逆向分析。通过这些解析出的提示词内容,我们不仅可以了解 Coding Intelligence 的工作机制,也能窥见苹果对现代开发实践的倾向性,比如:强烈推荐使用 Swift Concurrency(async/await、actor)而非 Combine,测试建议使用 Swift Testing 框架与宏。这些设计细节,是苹果开发范式的重要指标。
在构建 SwiftUI 设计系统 API 时,如何优雅地处理 语义颜色(Semantic Colors) 始终是一个令人头疼的问题。Magnus Jensen 在本文中系统梳理了常见方案的优缺点,并提出了一种基于宏(macro)的解决路径,力求实现 可读性强、类型安全、上下文感知 的色彩系统。如果你正打算为自己的 SwiftUI 项目设计一套结构清晰、可维护的风格体系,这篇文章值得一读。
随着项目复杂度的提升,开发者终将面对内存相关的问题:内存泄漏、系统警告,甚至因资源占用过高被系统强制终止。在这种情况下,如何诊断问题、控制内存占用,是对开发者经验与体系理解的深度考验。Anton Gubarenko 在两篇文章(内存优化篇)中,系统梳理了 iOS 应用内存使用的评估方式、诊断工具以及优化手段,构建出一套完整、实用的内存管理知识体系。
从 Swift 最近的几个版本更新和 Xcode 26 的表现可以看出,Swift 团队正有意识地优化并发编程的开发体验。通过启用新的默认行为,开发者无需在一开始就理解所有细节,便能写出更安全的并发代码。@concurrent
的引入,正是这一策略下的产物之一。在 Donny Wals 的这篇文章中,他详细介绍了 @concurrent
的背景与用途。简单来说,@concurrent
是 Swift 6.2 引入的显式并发标记,主要用于在启用 NonIsolatedNonSendingByDefault
特性时,明确指定函数运行在全局执行器上,从而在需要时将工作负载转移到后台线程,避免阻塞调用者所在的 actor(如主线程)。
或许有人会质疑 Swift 是否又在“用新关键字补旧洞”,但从语言设计趋势来看,随着并发模型逐步完善,许多旧关键字的使用将逐渐被默认机制吸收、简化甚至隐藏。
Swift 与 Java 的互操作并非新鲜事物,但过往的解决方案往往过程复杂且容易出错。Swift 6.2 引入的 swift-java
包具有划时代意义——这是首次提供官方支持、与工具链深度集成、开发体验接近一等公民的互操作方案,标志着 Swift 和 Java 之间真正意义上的“无缝互通”正式到来。Artur Gruchała 通过一个完整的示例项目,详细演示了如何从 Swift 端调用 Java 方法、构建双语言协作的 CLI 应用,并深入分析了实际开发中容易踩坑的关键细节——特别是 classpath 配置等看似简单却至关重要的环节。
Swift 6 引入了更严格的并发规则与更加结构化的编程范式。在迁移过程中,理解隔离域、Sendable 类型、默认行为,以及 @concurrent
的使用变得尤为重要。Audrey Tam 通过一个完整的 SwiftUI 示例项目(附项目源码),系统演示了从 Swift 5 迁移至 Swift 6.2 的全过程,涵盖 Xcode 设置、并发语义调整与数据隔离等核心环节,是一篇很具实用价值的迁移教程。
如何在 async/await 中实现类似 Combine 的 throttle 操作?如何持续追踪 @Observable
属性的变化?如何构建支持多消费者的异步流?Lucas van Dongen 在这个开源项目中给出了系统性的实践示例。他汇集了 Swift 6.2 并发模型下的多种模式,演示了如何在实际项目中逐步替代 Combine,迁移到更现代、类型安全的并发范式。
WWDC 25 中 Liquid Glass 的登场令人惊艳,但要同时支持两种视觉风格,对开发资源是一大考验。这也让很多开发者开始思考是否应放弃对旧系统的支持。David Smith 建议从两个角度判断:现有用户影响与新用户流失。以他的 Widgetsmith 应用为例,当前仍有约 9% 的新增用户来自旧系统,一旦抬高最低支持版本将直接失去这部分潜在用户。他认为,只有当旧系统用户占比降至个位数时,再做版本升级才更合理——简化技术负担,不应以牺牲业务增长为代价。
AdventureX 25 将于 2025 年 7 月 23 日至 27 日在杭州市湖畔创研中心与未来科技城学术交流中心举行。本指南包含活动行程介绍、参与方式、群聊福利、出行与住宿建议及注意事项等内容。不论你是来逛展、互动,还是寻找志同道合的伙伴,这份指南都将帮助你轻松规划行程~
如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。
在 weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。
书接上文,之前已经实现一个铺满整个窗口的红色填充,这趟来实现光线、相机及背景。
其实这个光追的思维模式很简单,就是从相机处开始发射一束射线,射线撞到哪些“物体”,就计算跟该“物体”相交的颜色。如图所示,从相机处发射射线,以左上角开始逐像素扫一遍,计算对应像素的颜色
我们再看看 viewport 的坐标,假设一个窗口大小是 (没错,PSP 的分辨率😁)的宽高,那么 的区间就是 , 的区间就是
现在我们要来处理一个标准化的像素坐标,处理像素在屏幕中的 2D 位置
struct Vertex {
float4 position [[position]];
};
fragment float4 fragmentFn(Vertex in [[stage_in]]) {
auto uv = in.position.xy / float2(float(480 - 1), float(272 - 1));
// ...
}
上面这一步的作用是把像素级的屏幕坐标转成区间 的归一化坐标。
假设现在有一个物体,它的坐标是 ,通过上面的计算式子可以得出
,说明它在屏幕的中间
接着我们假定相机的位置是原点 ,相机距离 viewport 。我们计算出宽高的比例再套进这个计算 (2 * uv - float2(1))
,等于讲把 映射成 的范围,其实就是
原始 uv | 变换后 |
---|---|
(0, 0) | (-1, -1) 左下角 |
(1, 0) | (1, -1) 右下角 |
(0.5, 0.5) | (0, 0) 居中 |
(1, 1) | (1, 1) 右上角 |
再把 (2 * uv - float2(1))
跟 float2(aspect_ratio, -1)
相乘等于讲横向乘以 aspect_ratio
用来做等比例变换
至于纵向乘以 -1
,那是因为在 Metal 中, 轴是向下为正,乘一下 -1
就可以把 轴翻转变成向上为正,接下来计算方向就简单多了,因为 轴面向相机,其实就是相机距离取反,上面假定相机距离为 1
,所以取反再跟 放一块就是方向,同时我们又假定相机的位置是原点 ,那么求光线就很容易了
struct Ray {
float3 origin;
float3 direction;
};
fragment float4 fragmentFn(Vertex in [[stage_in]]) {
// ...
const auto focus_distance = 1.0;
// ...
const auto direction = float3(uv, -focus_distance);
Ray ray = { origin, direction };
}
现在既然有了光线,再就是要计算一下光线的颜色,因为目前场景中没有物体,所以就默认计算背景色,我们先把光线从 映射回 ,然后再线性插值计算渐变天空颜色,所以先要让光线经过归一化操作到
// [-1, 1]
normalize(ray.direction)
然后再给该向量加
// [-1, 1] + 1 = [0, 2]
normalize(ray.direction) + 1
然后把 乘以 就转成 了,之后再代入线性插值公式计算结果,具体渐变色值可以根据自己的需求调整,我这里直接使用 Ray Tracing in One Weekend 的色值 float3(0.5, 0.7, 1)
float3 sky_color(Ray ray) {
const auto a = 0.5 * (normalize(ray.direction).y + 1);
return (1 - a) * float3(1) + a * float3(0.5, 0.7, 1);
}
最后总结一下代码
struct Ray {
float3 origin;
float3 direction;
};
float3 sky_color(Ray ray) {
const auto a = 0.5 * (normalize(ray.direction).y + 1);
return (1 - a) * float3(1) + a * float3(0.5, 0.7, 1);
}
fragment float4 fragmentFn(Vertex in [[stage_in]]) {
const auto origin = float3(0);
const auto focus_distance = 1.0;
const auto aspect_ratio = 480 / 272;
auto uv = in.position.xy / float2(float(480 - 1), float(272 - 1));
uv = (2 * uv - float2(1)) * float2(aspect_ratio, -1);
const auto direction = float3(uv, -focus_distance);
Ray ray = { origin, direction };
return float4(sky_color(ray), 1);
}
笔记主要记录Swift和OC底层原理差异的地方,OC的底层原理之前的笔记有详细记录。
课程是逻辑教育的,视频基本只看了总结部分,然后结合网上已有笔记进行的重点梳理。
HeapObject
,有两个属性:一个是Metadata
,一个是Refcount
,默认占用16
字节大小,就是对象中没有任何东西也是16
字节。8
字节。refCounted
引用计数大小,也就是多了8字节。getClassObject
函数是根据kind
获取object
类型kind
(理解为isa指针)是Class
类型,则将当前的Metadata
强转成ClassMetadata
,而ClassMetadata
是TargetClassMetadata
根据类型的别名,其中TargetClassMetadata
结构:
TargetClassMetadata
继承自TargetAnyClassMetadata
OC
中的objc_class
的结构一样,有isa
,有父类,有cacheData
,Data
类似于objc_class
中的bits
metadata
的kind
为Class
时,有如下的继承关系:
swift
的类内存结构可以理解为:struct Metadata {
void *kind; // 类型标识(如类、结构体、枚举)
void *superClass; // 父类的 Metadata 指针
void *cacheData; // 方法缓存(类似 OC 的 cache_t)
void *data; // 指向额外数据的指针
// 实例布局信息
uint32_t flags; // 类型标志位
uint32_t instanceAddressOffset; // 实例变量的起始偏移量
uint32_t instanceSize; // 实例对象占用的内存大小
uint16_t instanceAlignMask; // 实例的对齐掩码
uint16_t reserved; // 保留字段
// 类布局信息
uint32_t classSize; // 类对象占用的内存大小
uint32_t classAddressOffset; // 类变量的起始偏移量
void *description; // 类型描述信息
// 类对象与元类对象的关键区别
uint32_t flags; // 包含类型标志位(如是否为元类)
void *vtable; // 类对象的 vtable 指向实例方法表
void *classVtable; // 元类对象的 vtable 指向类方法表
// 方法签名表(所有方法),存放在类对象中
MethodDescriptor* methodDescriptors;
uint32_t methodCount;
}
// 例子:
class MyClass {
func instanceMethod() {} // 实例方法
static func classMethod() {} // 类方法
}
// 实例方法调用(通过类对象的 vtable)
let obj = MyClass() obj.instanceMethod() // 类对象 → Metadata → vtable → 方法实现
// 类方法调用(通过元类对象的 classVtable)
MyClass.classMethod() // 类对象 → 元类对象 → Metadata → classVtable → 方法实现
类的存储属性
、继承的存储属性
、继承的计算属性
中init
中改变属性值不会触发
属性观察,子类调用父类的init
会触发
属性观察willSet
方法,先子类后父类didSet
方法,先父类后子类访问后
内存中才有值,延迟属性对内存有影响,不能保证线程安全
类型属性:类型属性必须有初始值,内存只分配一次,通过swift_once
函数创建,类似dispatch_once
,是线程安全的。
class XXX {
static let share: XXX = XXX()
private init(){}
}
struct WSPerson {
var age: Int = 18
}
struct WSTeacher {
var age: Int
}
栈区
。深拷贝
,并且有写时复制
的机制。self
类型为let
,即不可以被修改。函数修改属性
, 需要在函数前添加mutating
关键字,本质是给函数的默认参数self
添加了inout
关键字,将self
从let
常量改成了var
变量。mutating
方法修改结构体属性时,采用的是 "in-place" 的方式,也就是直接在当前实例的内存空间里修改属性值,并没有重新创建一个新的实例来替换原来的实例。这一特性和赋值操作有着本质的区别。值类型对象的函数的调用方式是静态调用
,即直接地址调用
,调用函数指针,这个函数指针在编译、链接完成后就已经确定了
,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用
这个符号哪里来的?
是从Mach-O
文件中的符号表Symbol Tables
,但是符号表中并不存储字符串
,字符串存储在String Table
(字符串表,存放了所有的变量名和函数名,以字符串形式存储),然后根据符号表中的偏移值到字符串中查找对应的字符,然后进行命名重整:工程名+类名+函数名
Objective-C
里,方法重载是不被支持的,不过Swift
却支持,这主要是由它们不同的函数签名机制和语言设计理念造成的。只依据方法名
,和参数类型没有关系。 比如下面这两个方法,在OC看来是一样的,所以无法共存:- (void)doSomethingWithInt:(int)value;
- (void)doSomethingWithInt:(NSString *)value;
由方法名和参数类型
共同组成的。 下面这样的重载在Swift中是被允许的:func doSomething(value: Int)
func doSomething(value: String)
2. 消息传递机制
[obj doSomethingWithInt:1]
这样的调用,在运行时会被解析为SEL @selector(doSomethingWithInt:)
,要是有多个同名方法,就会引发冲突。doSomethingWithInt:
和doSomethingWithString:
。总结来说,Swift支持方法重载是其类型系统和编译时检查机制的自然结果,而OC不支持则是受限于其动态特性和历史设计。
final
、非static
、非@objc
修饰的)会被存放在一个名为 vtable 的表中。内存布局示例:
[实例对象内存]
├ isa 指针 ───→ [类对象]
├ Metadata 指针 ───→ [Metadata]
│ └ vtable 指针 ───→ [vtable 内存区域]
│ ├ 0: init()
│ ├ 1: method1()
│ └ 2: method2()
└ 其他类数据...
isa
指针找到类对象。Metadata
指针。vtable
指针。vtable
中的索引,调用对应的函数实现。可重写的方法
,而类的所有方法(包括不可重写的)仍通过元数据(Metadata)管理。static
/class
方法)的实现信息。Witness Table
管理,与类对象 / 元类对象的 Metadata
是分离的。struct
是值类型
,它的函数调度是直接调用
,即静态调度
Mutating
修饰class
是引用类型
,它的函数调度是通过vtable函数
,即动态调度
extension
中的函数是直接调用
,即静态调度
final
修饰的函数是直接调用
,即静态调度
@objc
修饰的函数是methodList函数表调度
,如果方法需要在OC
中使用,则类需要继承NSObject
dynamic
修饰的函数调度方式是methodList函数表调度
,它是动态可以修改的,可以进行method-swizzling
@objc+dynami
修饰的函数是通过objc_msgSend
来调用的参数想要被更改
,则需要在参数的类型前面增加inout
关键字,调用时需要传入参数的地址
Metadata
,一个Refcount
。后者记录引用计数。Refcount
最终可以获得64位整型数组bits
,其结构:
// 简化的 Refcount 结构(实际实现可能更复杂)
struct Refcount {
// 64 位中的高 32 位:强引用计数
uint32_t strongRefCount: 32;
// 64 位中的低 32 位:
uint32_t hasWeakRefs: 1; // 是否有弱引用
uint32_t hasUnownedRefs: 1; // 是否有 unowned 引用
uint32_t isDeiniting: 1; // 是否正在析构
uint32_t sideTableMask: 1; // 是否使用 Side Table
uint32_t weakRefCount: 28; // 弱引用计数
};
sideTableMask
标志切换到全局 Side Table 存储。Swift
在创建实例对象时的默认引用计数是1
,而OC
在alloc
创建对象时是没有引用计数的。refCounts.formWeakReference
,即去操作sideTable表,添加对象的弱引用关系,这里和OC处理是一致的。dynamic
(因为swift是静态语言),方法和属性不加任何修饰符的情况下,已经不具备runtime特性,此时的方法调度,依旧是函数表调度即V_Table调度。@objc
标识的情况下,可以通过runtime API获取到,但是在OC中是无法进行调度的,原因是因为swift.h文件中没有swift类的声明。NSObject
类来说,如果想要动态的获取当前属性+方法,必须在其声明前添加@objc
关键字,如果想要使用方法交换,还必须在属性+方法前添加dynamic关键字,否则当前属性+方法只是暴露给OC使用,而不具备任何动态特性。function
类型、optional
类型instance
、类的类型、仅类遵守的协议,可以看作是Any的子类AnyObject.Type
metadata.T.self
的类型是T.Type
这里每天分享一个 iOS 的新知识,快来关注我吧
在 iOS 生态系统中,Apple App Site Association(AASA)文件扮演着至关重要的角色。它通过在你的 iOS 应用和网络域之间建立安全且经过验证的链接,实现了诸如通用链接(Universal Links)、共享网络凭证、Handoff 和 App Clips 等功能。
不知道你有没有注意过,当你在手机浏览器上访问知乎、小红书或 YouTube 时,有些链接会让你继续留在浏览器中,而另一些则会直接跳转到对应的应用中?这背后的驱动力正是 AASA 文件。
Apple App Site Association 文件是一个配置文件,它定义了 URL 的处理方式,并指定它们是打开在浏览器中还是直接链接到应用程序内的内容。
以下是一个基本示例:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "ABCDE12345.com.example.app",
"paths": [
"/",
"/about-us/",
"/products/*",
"/services/detail/?id=*",
"/news/article/*",
"/promo/*",
"NOT /private/",
"NOT /settings/*",
"*.html"
]
}
]
},
"webcredentials": {
"apps": ["ABCDE12345.com.example.app"]
},
"appclips": {
"apps": ["ABCDE12345.com.example.app"]
}
}
访问下边这些路径的请求都会将用户路由到应用程序中:
"/",
"/about-us/",
"/products/*",
"/services/detail/?id=*",
"/news/article/*",
...
而对于那些以 NOT 前缀的路径,系统将不会重新路由用户,允许体验继续在浏览器中进行:
"NOT /private/",
"NOT /settings/*",
...
当你准备好部署 AASA 文件时,需要将其托管在网站的根目录或 .well-known
目录下:
/apple-app-site-association
/.well-known/apple-app-site-association
注意:AASA 文件不应有文件扩展名。为了让内容分发网络(CDN)成功缓存此文件,它必须托管在可供所有 IP 地址和范围访问的域上,通过 HTTPS 提供文件,不重定向,并且不被访问策略阻止。
你还应该确保服务器以 Content-Type: application/json 的形式提供文件,并且文件大小不超过 128KB。
当你首次托管 AASA 文件时,苹果的 CDN 将在 24 小时内获取它,这意味着它们将在文件发布的第一天内从你的服务器请求并缓存一份副本。
根据苹果文档的说法,这个 24 小时的窗口仅与首次将文件放入苹果的 CDN 有关。随后的更新会在不同的时间间隔发生。
其实我们可以通过下边的链接看下其他公司的 AASA 文件是如何写的:
随着应用程序的迭代,你将需要修订 AASA 文件,以便为新的用户流程整合通用链接(Universal Link)。
AASA 文件中的错误很常见,因此验证此文件的行为和语法成为流程中不可或缺的一部分。一旦被苹果的 CDN 确认,此文件中的任何错误都可能影响所有用户的通用链接行为——无论是新用户还是老用户。
幸运的是,在更改到达苹果之前,有几种方法可以验证 AASA 文件的行为。
关联域支持一种备用模式,允许开发者通过绕过 CDN 直接从服务器获取文件,以验证修改后的 AASA 文件的行为。
要在 Xcode 项目中激活此模式:
前往 Signing & Capabilities -> Associated Domains
在域条目的末尾添加 ?mode=developer
(例如:applinks:yourdomain.com?mode=developer
)
现在,当你构建并运行应用程序时,它将直接从你的服务器检索更新的 AASA 文件。
在开发期间只能使用备用模式,你必须在将应用程序提交到 App Store 之前从关联域中删除后面这段字符串。
你可以使用以下工具来验证 AASA 文件的语法和配置:
这是一个免费的工具,用于验证和测试 Apple App Site Association(AASA)文件。确保你的通用链接配置正确,通过简单的链接创建、实时测试和团队协作功能简化 AASA 文件的故障排除。
你可以通过访问以下网址检查苹果的 CDN 是否已获取文件的最新版本:
https://app-site-association.cdn-apple.com/a/v1/{YOUR_DOMAIN_HERE}
如果 CDN 确实具有文件的最新版本,那么任何新应用安装也将获得此最新版本。但是对于老用户,他们的设备每周只会检查一次更新的副本。
重新安装应用程序将从 CDN 获取最新版本。
实际上,AASA 文件的任何更改的推出周期是 CDN 刷新其缓存所需的时间与现有用户每周检查的时间相结合。
为了更好地理解缓存版本更新前剩余的时间,我们可以查看来自 CDN 的响应头中的 Cache-Control 字段。
Cache-Control 头由网络服务器使用,以决定浏览器和中间缓存(如 CDN)应该如何以及在多长时间内缓存文件的提供版本。
在 Facebook 和 Yelp 上,我们可以通过以下链接查看:
https://app-site-association.cdn-apple.com/a/v1/facebook.com
https://app-site-association.cdn-apple.com/a/v1/yelp.com
然后我们可以通过从 max-age 中减去 age 来确定缓存何时更新:
max-age:6 小时,缓存刷新时间为 17,760 秒 / 约 5 小时。
max-age:1 小时,缓存刷新时间为 3,112 秒 / 约 50 分钟。
在实际情况中,最短的 max-age 是 3,600 秒,即 1 小时,最长的是 21,600 秒,即 6 小时。
值得注意的是,苹果的 CDN 会覆盖原始网站指定的 Cache-Control 设置。例如,直接从 Yelp 访问 AASA 文件的 max-age 为 1200,但从苹果的 CDN 检索时,其 max-age 为 3600。
通过更长时间的缓存文件,苹果能够最大限度地减少对原始服务器的请求频率,从而减少网络和原始服务器的流量和负载。
AASA 文件是 iOS 生态系统中一个非常重要的组件,它通过在 iOS 应用和网络域之间建立安全且经过验证的链接,实现了诸如通用链接(Universal Links)、共享网络凭证、Handoff 和 App Clips 等功能。
通过了解 AASA 文件的创建、部署、测试与验证,开发者可以更好地优化其应用的链接配置,提升用户体验,并确保应用的稳定性和可靠性。
[1]
Universal Link & Apple App Site Association Testing Tool: getuniversal.link/?ref=digita…
这里每天分享一个 iOS 的新知识,快来关注我吧
本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!
继 2025 年 2 月 Swift 社区论坛发布关于启动 Android Community Workgroup 的消息数月后,Swift.org 于上周正式宣布成立官方 Android 工作组。这标志着由官方主导的 Swift 安卓平台支持正式启动,未来 Swift 开发者有望获得更完善的安卓适配工具链与开发体验。
Swift.org 在 2025年6-26日宣布成立 Android Workgroup,目标是:将Android确立为Swift的官方支持平台并持续维护
Establish and maintain Android as an officially supported platform for Swift.
Google的Kotlin Multiplatform 正快速发展,Flutter也逐渐成熟,成为跨端主流开发方案,官方希望能够提升 Swift 的竞争力。
下面将主要讲解:
“官方支持”绝非简单的“Swift代码能在Android编译”就算完成,包括:
所有Swift的Pull Request将自动运行Android目标测试,防止平台兼容性退化
Foundation和Concurrency等核心库将针对Android的文件系统、线程模型差异进行专门优化
设计Swift与Java/Kotlin之间的双向调用机制,打破语言壁垒
Swift对Android的支持并非从零开始构建,Swift编译器从诞生之初就基于LLVM架构,而Android NDK从R13版本开始就完全转向基于LLVM的Clang编译器。
这种同源架构使得Swift编译器能够被“重定向”,为Android支持的CPU架构生成原生机器码。在具体实现上:
虽然完整支持还在路上,但开发者已经可以尝试Swift开发Android应用。以下是一个简单的互操作示例:
// Swift
@_cdecl("sayHello")
public func sayHello() -> UnsafePointer<CChar> {
return strdup("Hello from Swift 🐦")
}
// kotlin
class HelloBridge {
companion object {
init { System.loadLibrary("hello") }
}
external fun sayHello(): String
}
通过JNI将Swift函数挂载到Kotlin,编译的.so文件打包进APK即可运行。
社区已有Tokamak UI框架等尝试支持Android,预示着更完整的解决方案即将到来。
对比主流的KMP、Flutter、RN方案
这次不仅是Swift语言的一次跨界尝试,更是移动开发的一次生态震荡:苹果主导语言首次系统性拥抱 Google 生态,同时也是苹果对于跨端研发模式与 Kotlin / KMP 生态 的全新博弈。
前沿技术周刊 是一份专注于技术生态的周刊,每周更新。本周刊深入挖掘高质量技术内容,为开发者提供持续的知识更新与技术洞察。
码圈新闻
新技术介绍
大厂在做什么
深度技术
博客推荐
零一开源 是我自己做的一个文章和开源项目的分享站,有写博客或开源项目的也欢迎来提供投递。
每周会搜集、整理当前的新技术、新文章,欢迎大家订阅
c
// Block 基础结构体
struct Block_layout {
void *isa; // 指向类信息:_NSConcreteGlobalBlock/_NSConcreteStackBlock/_NSConcreteMallocBlock
int flags; // 状态标志位
int reserved; // 保留字段
void (*invoke)(void *, ...); // 函数指针
struct Block_descriptor *descriptor; // 描述信息
// 捕获的变量跟随在此后
};
// Block 描述符
struct Block_descriptor {
unsigned long reserved;
unsigned long size; // Block 总大小
void (*copy)(void *dst, const void *src); // 拷贝辅助函数
void (*dispose)(const void *); // 析构辅助函数
};
// __block 变量包装结构
struct __Block_byref {
void *isa;
struct __Block_byref *forwarding; // 指向真实地址
int flags;
int size;
// 原始变量存储在此
};
objectivec
// 原始代码
void example() {
__block int counter = 0;
void (^block)(void) = ^{ counter++; };
}
// 编译器转换后的伪代码
struct __block_impl_counter {
void *isa = &_NSConcreteStackBlock;
int flags = 0;
int reserved;
void (*invoke)(struct __block_impl *);
struct __Block_descriptor *descriptor;
struct __Block_byref_counter *counter_ref; // 捕获的__block变量
};
struct __Block_byref_counter {
void *isa;
__Block_byref_counter *forwarding;
int flags;
int size;
int counter; // 原始变量
};
static void __example_block_invoke(struct __block_impl *__cself) {
struct __Block_byref_counter *counter_ref = __cself->counter_ref;
counter_ref->forwarding->counter++; // 通过forwarding指针修改
}
static void __example_block_copy(struct __block_impl *dst, struct __block_impl *src) {
_Block_object_assign(&dst->counter_ref, src->counter_ref, BLOCK_FIELD_IS_BYREF);
}
static void __example_block_dispose(struct __block_impl *src) {
_Block_object_dispose(src->counter_ref, BLOCK_FIELD_IS_BYREF);
}
static struct __Block_descriptor {
unsigned long reserved;
unsigned long size;
void (*copy)(struct __block_impl *, struct __block_impl *);
void (*dispose)(struct __block_impl *);
} __block_descriptor = { 0, sizeof(struct __block_impl), __example_block_copy, __example_block_dispose };
void example() {
// 创建__block变量
struct __Block_byref_counter counter_ref = {
NULL,
&counter_ref, // forwarding指向自己
0,
sizeof(struct __Block_byref_counter),
0 // counter初始值
};
// 创建Block
struct __block_impl block_impl = {
&_NSConcreteStackBlock,
BLOCK_HAS_COPY_DISPOSE,
0,
__example_block_invoke,
&__block_descriptor
};
block_impl.counter_ref = &counter_ref;
// 当Block被复制到堆时
struct __block_impl *heap_block = malloc(sizeof(struct __block_impl));
memcpy(heap_block, &block_impl, sizeof(struct __block_impl));
__example_block_copy(heap_block, &block_impl); // 处理捕获变量
// 堆上Block的isa指针更新
heap_block->isa = &_NSConcreteMallocBlock;
// __block变量复制到堆
struct __Block_byref_counter *heap_counter = malloc(sizeof(struct __Block_byref_counter));
memcpy(heap_counter, &counter_ref, sizeof(struct __Block_byref_counter));
heap_counter->forwarding = heap_counter; // 指向堆上副本
counter_ref.forwarding = heap_counter; // 栈变量指向堆副本
// 调用Block
heap_block->invoke(heap_block);
}
变量捕获:
__block
变量:包装为__Block_byref
结构体内存管理:
objectivec
// 栈Block -> 堆Block转换
void (^heapBlock)(void) = [^{} copy];
[heapBlock release];
循环引用解决方案:
objectivec
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf doSomething];
};
c
// Swift闭包在SIL(Swift中间语言)中的表示
struct Closure {
void *metadata; // 类型元数据
struct HeapObject *object; // 引用计数头部
void (*function)(void *context); // 函数指针
struct Context *context; // 捕获上下文
};
// 捕获上下文结构
struct Context {
void *metadata;
struct HeapObject *object;
// 捕获的变量按顺序排列
// 对于值类型:直接存储值
// 对于引用类型:存储指针
};
swift
// 原始Swift代码
func createClosure() -> () -> Void {
var counter = 0
return { counter += 1 }
}
// 编译器生成的伪代码
struct Context {
// 引用计数头
struct HeapObject {
void *metadata;
atomic_int refCount;
} header;
// 捕获的变量
struct Box {
int value; // counter的存储
} *counterBox;
};
// 闭包函数实现
void closureImp(struct Context *context) {
context->counterBox->value += 1; // 修改捕获变量
}
// 创建闭包
Closure createClosure() {
// 在堆上分配上下文
struct Context *context = swift_allocObject(sizeof(struct Context));
// 在堆上分配Box存储counter
context->counterBox = swift_allocObject(sizeof(struct Box));
context->counterBox->value = 0;
// 创建闭包实例
return (Closure){
.metadata = ClosureTypeMetadata,
.object = &context->header,
.function = closureImp,
.context = context
};
}
// 调用闭包
let closure = createClosure()
closure.function(closure.context) // counter = 1
变量捕获:
swift
var a = 10
let closure = {
a += 1 // a被捕获到Box中
print(a)
}
捕获列表:
swift
[unowned self, b = a + 1] in // 显式控制捕获
内存管理:
循环引用解决方案:
swift
class MyClass {
var closure: (() -> Void)?
func setup() {
closure = { [weak self] in
self?.doSomething()
}
}
}
特性 | Objective-C Block | Swift Closure |
---|---|---|
内存分配 | 手动管理(栈/堆转换需copy ) |
自动管理(ARC) |
值类型捕获 | 默认值捕获,__block 修饰可修改 |
直接可修改(自动Boxing) |
引用类型 | 强引用捕获 | 强引用捕获 |
生命周期控制 |
__weak /__strong
|
[weak] /[unowned] /捕获列表
|
逃逸行为 | 无显式标记 |
@escaping 显式标记 |
优化 | 全局Block不捕获变量 | 非逃逸闭包可栈分配 |
调用开销 | 通过函数指针调用 | 通过上下文+函数指针调用 |
Objective-C:
objectivec
// 优先使用栈Block
void (^stackBlock)(void) = ^{ /* ... */ };
// 需要长期持有时才复制到堆
void (^heapBlock)(void) = [stackBlock copy];
Swift:
swift
// 非逃逸闭包优化(编译器自动识别)
func optimize(@noescape _ closure: () -> Void) {
closure()
}
// 避免不必要的捕获
let safeClosure = { [weak self] in
guard let self = self else { return }
self.process()
}
Swift 自动闭包:
swift
// @autoclosure 实现
func assert(_ condition: @autoclosure () -> Bool) {
if !condition() { /* 处理 */ }
}
// 编译器转换:
func assert(_ condition: () -> Bool) {
if !condition() { /* ... */ }
}
assert({ 2 > 1 }()) // 自动包装
Objective-C Block 与 C 交互:
objectivec
// 作为C函数参数
void acceptBlock(void (^block)(void));
// 桥接调用
void (^ocBlock)(void) = ^{ NSLog(@"Called"); };
acceptBlock((__bridge void (^)(void))ocBlock);
通过深入分析底层实现,可以更好地理解闭包的内存行为、性能特征和最佳实践,避免常见陷阱如循环引用和性能损耗。
Flutter作为跨平台移动应用开发框架,一直广受欢迎,在开发过程中,确保应用的性能是至关重要的。以下是一些优化Flutter应用性能的工具和方法
使用`const`关键字创建不会改变的Widget,这样可以避免不必要的重建。
使用`const`构造函数创建常量Widget。
flutter 中 widget 都是 inmutable 的,使用const
构造函数后,Flutter可以在编译期创建Widget实例,而不是在每次重建时都创建新实例。由于相同的const
对象在内存中只存在一份,这减少了内存消耗和对象创建的开销。当父Widget重建时,Flutter会复用这些const
Widget而不需要重新创建,从而显著提升渲染性能。
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8.0),
child: Text('Hello World'),
);
}
// 优化后
Widget build(BuildContext context) {
return const Container(
padding: EdgeInsets.all(8.0),
child: Text('Hello World'),
);
}
Flutter的重建机制会从setState()被调用的StatefulWidget开始,重建整个子树。通过将状态隔离在更小的组件中,可以显著减少重建范围。这样当状态变化时,只有包含该状态的小组件会重建,而不是整个页面,从而降低CPU使用率并提高渲染效率。
class MyPage extends StatefulWidget { // 优化前 - 整个页面重建
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
bool isLoading = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('标题永远不变'),
if (isLoading) CircularProgressIndicator() else DataList(),
],
);
}
}
// 优化后 - 只重建变化部分
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('标题永远不变'),
LoadingStateWidget(),
],
);
}
}
// 示例代码
class LoadingStateWidget extends StatefulWidget {
Widget build(BuildContext context) {
return isLoading ? CircularProgressIndicator() : DataList();
}
}
当Widget重绘时,Flutter默认会重新绘制该Widget及其所有子Widget。RepaintBoundary会创建一个新的图层,将其子Widget的重绘行为隔离。这意味着当子Widget需要重绘时,Flutter无需重绘边界外的内容。防止不必要的GPU渲染工作,提高复杂界面的性能。可以开启 Inspector 查看重绘
class MyComplexUI extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// 频繁更新的区域
RepaintBoundary(
child: AnimatedProgressIndicator(),
),
ComplexStaticContent(), // 静态内容
],
);
}
}
Column
会一次性构建所有子Widget,无论它们是否在视口内可见。这会导致大量内存使用和初始渲染延迟。相比之下,ListView.builder
实现了"视口渲染"技术,只构建和渲染当前可见的项目,其他项会在滚动进入视口时才被构建。这大大减少了内存占用和初始渲染时间,对于长列表尤其重要。
// 优化前 - 使用Column一次性构建所有项
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: List.generate(1000, (index) =>
ListTile(title: Text('Item $index'))
),
),
);
}
// 优化后 - 使用ListView.builder按需构建
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) => ListTile(
title: Text('Item $index'),
),
);
}
当列表涉及到 增删重排以及列表动效的情况下,需要使用 key,避免重建以后丢失状态,只更新/创建/销毁发生变动的那一项,极大减少无谓的 build、layout、paint 和 State 重建,提升渲染效率。
ListView(
children: items.map((item) => ListTile(
key: ValueKey(item.id),
title: Text(item.title),
)).toList(),
)
itemExtent
时,ListView 会对子项逐一进行布局测量(layout),每次滚动或构建都需遍历和计算高度,开销大。itemExtent
后,ListView 可以直接通过数学公式(比如滚动偏移/高度)精确定位和渲染可见Item,避免了对子项的反复测量。ListView.builder(
itemCount: 1000,
itemExtent: 60.0, // 每项高度固定为 60
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
)
网络图片加载是耗时操作,如果图片不缓存,同一图片会被重复下载多次,导致网络资源浪费和UI闪烁。使用CachedNetworkImage
加载图片,实现了多级缓存机制:内存和持久缓存,可以有效降低内存的占用
import 'package:cached_network_image/cached_network_image.dart';
CachedNetworkImage(
imageUrl: 'https://example.com/image.jpg',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
)
加载远超实际显示尺寸的大图是常见的性能问题。例如,下载一张5MB的4K图片,却只在200x200像素的区域显示,会导致更多的带宽消耗、更大的内存占用,并且图片的解码和缩放会消耗性能,可以使用 Inspector 检测哪些图片过大
// 优化前 - 加载原始大小图片
Image.network('https://example.com/large_image.jpg')
// 优化后 - 指定适当尺寸
Image.network(
'https://example.com/large_image.jpg?w=300&h=200', // 服务端支持动态调整图片尺寸
width: 300,
height: 200,
fit: BoxFit.cover,
)
在Flutter中,build
方法可能会非常频繁地调用,每次UI状态变化都可能触发重建。如果在build方法中执行复杂计算,会导致: UI卡顿、电池耗电增加、设备发热
int _calculate(int n) {
// 检查缓存
if (_cache.containsKey(n)) {
return _cache[n]!;
}
、、、、计算逻辑
}
Widget build(BuildContext context) {
final result = _calculate(input);
return Text('Result: $result');
}
虽然 flutter 有多个线程,但是Dart是单线程执行,dart 一次只能执行一个任务,任务按照顺序一个接一个的执行,具体查看 详解 Flutter engine多线程、Dart isolate和异步普罗哈基米
当执行耗时操作时:UI线程被阻塞 动画会卡顿,帧率下降 可能触发ANR(Android)或页面frezen(iOS)
通过使用compute
函数或Isolate
,可以将计算密集型任务移至后台线程执行,保持UI线程流畅响应。Flutter的compute
函数封装了Isolate创建和通信的复杂性,适合大多数场景。
Image.asset
或Image.network
加载GIF图片时,如果该GIF在界面上多处被使用,或在滚动列表中多次出现,每次都会重新解码一次。这会导致CPU资源浪费、卡顿、甚至内存暴涨。ClipRRect
、ClipOval
等圆角裁剪组件,或Container
的decoration: BoxDecoration(borderRadius: ...)
,在Flutter内部会创建新的图层并触发离屏渲染(offscreen rendering)。MaterialApp(
showPerformanceOverlay: true,
checkerboardOffscreenLayers: true,
)
Opacity
widget 会导致其子widget单独绘制到一个新的图层,再整体设置透明度。频繁使用会带来离屏渲染,尤其在动画、列表中影响大。Container(color: Colors.black.withOpacity(0.2))
)。FadeTransition
(配合AnimationController
),避免整个子树离屏渲染。Flutter的Widget嵌套层级过深会导致:
Container
替代嵌套的Padding
+DecoratedBox
+Align
)。动画每一帧都会触发 build。如果子树内容不变却每帧都重建,浪费性能。把不变的内容放到 child 参数,build 只处理“变”的部分,大大减少无意义的构建。
AnimatedBuilder
AnimatedWidget
FadeTransition
、ScaleTransition
、RotationTransition
等
以上组件适用 child 参数AnimatedBuilder(
animation: controller,
child: const Text('静态内容'), // 只 build 一次
builder: (context, child) {
return Transform.rotate(
angle: controller.value * 2 * pi,
child: child, // 每帧只变 transform
);
},
)
Transform
、Opacity
这类属性动画,底层是 GPU 合成变换,不涉及重新布局和绘制,性能极高。相比之下,若动画导致 Widget 结构/布局频繁变动,每帧都要 layout 和 paint,性能很低。
// 推荐
FadeTransition(
opacity: animation,
child: Image.asset('xxx.png'),
)
// 不推荐(每帧都重建图片)
AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Opacity(
opacity: controller.value,
child: Image.asset('xxx.png'),
);
},
)
内存泄漏是Flutter应用中常见的性能问题,特别是在长时间运行的应用中。主要原因包括:
应用体积直接影响用户下载意愿和存储空间占用。Flutter应用体积较大的主要原因包括:
通过以下策略可以减小应用体积:
--split-per-abi
生成特定架构的APK,避免包含所有架构的原生库性能调优的第一步是准确测量和定位问题。Flutter提供了强大的性能分析工具:
通过 flutter run --profile 开启 profile 模式,使用性能视图(Performance view) 通过火焰图查看哪些方法耗时,修改对应的方法提高性能
Flutter 性能优化的本质是:减少不必要的重建和重绘、减少内存/CPU/GPU压力、让静态内容尽量复用,动态内容最小化刷新,按需加载和渲染,让 UI 始终流畅响应用户。
何曾几时,小伙伴们在 Xcode 的 CoreData 模型编辑器里可以肆无忌惮的浏览数据库表结构的拓扑图,造福了我们这些秃头码农们,可惜这一功能现在已不复存在!
那么,还有没有什么替代方案呢?本文由此应运而生了。
在本篇博文中,您将学到如下内容:
众所周知,Core Data 模型编辑器在 Xcode 早期版本中确实提供了可视化的关系拓扑图(如实体间的关联关系视图)显示功能,但在 Xcode 14 及之后的版本中,这一功能已被取消。
以下是当前可用的替代方案和注意事项:
.xcdatamodeld
)中仍包含 elements
部分的 XML 数据,但这些信息已不再用于显示实体间的布局关系了。.momd
或 .xcdatamodeld
文件,生成实体关系图。NSManagedObject
子类代码,可以间接查看实体间的关联关系。例如,若实体 Book
与 Author
存在一对多关系,生成的代码中会包含 @NSManaged
修饰的关联属性。.xcdatamodeld
)本质是 XML 格式。开发者可以直接查看其内容,解析实体间的关联关系(通过 <relationship>
标签)。<entity name="Book" representedClassName="Book">
<relationship name="author" destinationEntity="Author" inverseName="books"/>
</entity>
如果依赖可视化拓扑图进行开发,推荐以下步骤:
NSManagedObject
子类和模型文件 XML 内容,手动验证关系逻辑。若需进一步调试数据库内容,可参考如何通过 SQLite 工具查看 Core Data 存储文件。
感谢观赏,再会啦!8-)
你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。
@JonyFang: btrace 是字节开源的一款高性能 Android/iOS 端性能追踪(Tracing)工具,基于 Perfetto 进行数据展示。它能够详细记录方法的调用过程,精准分析耗时,并归因性能瓶颈,兼具高采样精度和低性能损耗。与 Apple 的 Time Profiler 等传统工具相比,btrace 更加灵活、可自定义,并支持系统方法追踪、有丰富的数据归因和可视化能力,能帮助开发者深入理解和优化 App 性能。
btrace 3.0 相比 2.0 的优化(iOS 视角):
采集方案升级。
3.0 由单一编译期插桩,升级为“同步抓栈 + 异步抓栈”的混合采样方案。同步抓栈通过 hook 高频系统方法和关键节点,实时采集 Trace 数据;异步抓栈则通过独立采样线程定时回溯线程调用栈,保证采集的时间连续性。相比 2.0,3.0 大幅降低了接入和维护成本,采集更全面、对系统方法也支持更好。
数据存储与压缩优化。
3.0 针对 Trace 数据量大、存储压力大等问题,设计了高效的调用栈去重与压缩结构。通过空间相似性(调用栈公共前缀合并)、时间相似性(连续相同栈合并)等手段,进一步减少内存和磁盘占用,提升了大体量数据下的可用性。
多线程与性能再提升。
3.0 优化了多线程数据写入的并发安全性与性能,采用 CAS 等无锁 / 低锁技术,兼顾高性能与数据一致性,在复杂多线程场景下依然保持低开销。
死锁规避与线程采样精细化。
异步采样时规避了 Time Profiler 可能导致的死锁风险,通过黑名单和信号安全 API 控制,提升了工具的稳定性。同时,仅采集活跃线程,有效降低了对 App 性能的影响。
丰富的性能归因与可视化。
除了基本的方法调用追踪,3.0 进一步支持 CPU 时间、对象分配、缺页 / 上下文切换、线程阻塞等多维度的耗时归因,配合 Perfetto 可视化,帮助开发者一站式定位性能瓶颈。
易用性和生态提升。
3.0 极大简化了接入流程,无需业务侧代码大改,无侵入式支持线上场景,支持性能自动诊断和多端(Android/iOS/ 鸿蒙 /Web)扩展,生态愈发完善。
整体来看,btrace 3.0 对 iOS 开发者而言,是一款集高性能、易用性、灵活性于一体的专业 Trace 工具。相比 2.0,3.0 大幅优化了采集方式、性能、安全性和数据分析能力,适合需要深入性能调优、线上问题定位和日常性能治理使用,推荐纳入工程实践!
self.
@AidenRao:这篇文章探讨了在 Swift 开发中避免不必要的 self.
前缀使用,利用编译器检查减少循环引用风险。它基于 Swift 5.3(SE-0269)和 5.8(SE-0365)的演进,在闭包中省略 self.
能让编译器强制捕获语义(如使用 [weak self]
),从而暴露潜在内存泄漏问题。
@阿权:文章分享了作者关于 Apple Feedback 的心得体会与收益:
开发者可以积极参与反馈提交,尤其在 WWDC 和测试版周期中,通过结构化报告和社区分享推动平台改进。反馈不仅是对 Apple 的贡献,更是优化自身开发流程的重要手段。
@zhangferry:Xcode 26 提供了 Coding Intelligence 功能,并且支持自定义模型。但当前自定义模型支持的 URL 格式 是 ChatGPT 风格的,非这类格式例如 Gemini 还需要依赖 Proxyman 这类网络代理工具做一层转换。(感觉算是 Bug,希望后续能修复)
文中以 Xcode 中使用 Gemini 为例,抓取和分析了 AI 相关的代码解释、文档生成、代码生成这几个功能所涉及的 Prompt,每一个功能都对应一组封装好的 Prompt,可以了解到 Apple 是如何使用 PE 的:
@Damien:由于 iOS 26 beta1 禁止了 Debug 时 mprotect 的 RX 权限,导致 Flutte 在 iOS 26 真机上 Debug 运行时出现了问题。为了解决这一问题,Flutter 团队采用了一种临时方案,即创建了 NOTIFY_DEBUGGER_ABOUT_RX_PAGES 函数。当 Flutter 应用需要执行新代码时,该函数会暂停应用并通知调试器,调试器随后利用其特权,通过 debugserver 修改内存权限,实现“双地址映射”,其中一个地址用于写入代码,另一个地址用于执行代码。这一方案虽然解决了当前的运行问题,但存在一定的延迟和较高的环境要求,未来仍需开发高性能的 Debug 解释器来提供更完善的解决方案。
@Barney:EFQRCode
是一个轻量级纯 Swift
二维码库,支持生成带水印 / 图标的风格化二维码和图片识别功能。基于 CoreGraphics
、CoreImage
和 ImageIO
,全平台支持 iOS/macOS/watchOS/tvOS/visionOS
。最新 7.0.0 版本重构了 API
,引入 EFQRCodeGenerator
和 EFQRCodeRecognizer
类,支持链式配置,改进 Objective-C
兼容性。可通过 CocoaPods
、Carthage
或 SPM
集成。
@Kyle-Ye: Point-Free 团队在 WWDC 2025 期间免费放送了一期重磅视频,深入对比 SwiftData 与他们自家 SQL Query Builder(Structured Queries)在实际开发中的表现。视频以还原 Apple Reminders 复杂查询为例,展示了两种方案在代码简洁性、可组合性和类型安全等方面的差异。
Structured Queries 方案只需 23 行代码即可线性表达复杂查询逻辑,支持类型安全、可读性强;而 SwiftData 不仅写法更繁琐(32 行),还存在布尔和枚举类型无法直接排序 / 筛选、可选字段排序不灵活等问题,甚至有些写法在运行时会直接崩溃。
如果你关心 Swift 持久化方案、数据层架构,或在 SwiftData 和 SQL 之间犹豫,强烈建议观看本期视频。
重新开始更新「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)
引入Masonry编译报错如下:
File not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a
Linker command failed with exit code 1 (use -v to see invocation)
这是非常常见的问题,自我第一次使用Masonry库就有这个错误。但是觉得很奇怪,Masonry作为一个广泛使用的成熟库,为什么引入还会报错,这也太low了吧?
今天建立新项目,引入Masonry库,又报错,接二连三的遇到该问题让我开始正视了这个问题。
方案1 临时方案:
xcworkspace工程中选择Pods工程,Targets下选择Masonry库,将minimum deployments最新部署版本升级至 >= 9.0即可。
方案2 推荐方案:
方案1 缺点:直接修改Pods工程中的配置并不是最佳方案,因为当你下次执行 pod update
或 pod install
时,这些更改可能会被覆盖。如果你确实需要对某- 使用 post_install
hook:可以在你的 Podfile
中添加脚本,在安装或更新 Pods 后自动修改某些 Targets 的设置。些 Pod 进行自定义配置,推荐的做法是:
修改Podfile文件,对Masonry库的最低部署版本进行设置。
post_install do |installer|
installer.pods_project.targets.each do |target|
if target.name == 'Masonry'
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end
end
File not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a
这个错误的核心含义是:
找不到 ARC(Automatic Reference Counting)支持库
libarclite_iphonesimulator.a
而通过将 Masonry Pod 的 Deployment Target 从 iOS 8.0
改为 iOS 11.0
后,问题解决了。
libarclite_iphonesimulator.a
找不到?libarclite_iphonesimulator.a
的支持,所以如果你仍然试图链接它,就会报错。Masonry 本身是 Objective-C 编写的库,并且它的 .podspec
文件里可能指定了较低的 deployment target(如 iOS 8.0
)。这会导致 CocoaPods 在生成 Pod Target 时:
-fobjc-arc
标志;libarclite_iphonesimulator.a
来兼容非 ARC 环境;这样会:
libarclite_iphonesimulator.a
;libarclite
是 Apple 为了向后兼容,在早期 iOS 版本上支持 ARC 的“过渡性”库。post_install
统一管理 Pod 的构建设置;既然有错误,那为什么Masonry库不把最低版本8.0升级呢? Masonry 库维持较低的 iOS 部署目标(如 iOS 8.0)主要是为了最大化兼容性,使得尽可能多的项目能够使用该库,包括那些需要支持旧版 iOS 系统的应用。然而,这种做法有时会导致与最新版本的 Xcode 或其他工具链不完全兼容的问题,就像我遇到的情况一样。
在移动应用开发中,跨平台技术始终是开发者追求的圣杯。借助ArkUI-X框架,我们仅用一套ArkTS代码即可实现应用在HarmonyOS和iOS双端的原生级运行。本文以连连看游戏为例,深度解析跨平台开发的核心优势。
图:ArkUI-X跨平台运行原理示意图
ArkUI-X通过以下设计实现"一次开发,双端部署":
// 跨平台UI组件示例 - 在双端自动适配原生控件
Grid() {
ForEach(this.gridData, (row: Cell[], i: number) => {
ForEach(row, (cell: Cell, j: number) => {
GridItem() {
this.cellView(cell, i, j) // 自动转为iOS UICollectionViewCell或HarmonyOS GridItem
}
})
})
}
# 安装DevEco Studio 5.0.4后只需:
npm install -g @arkui-x/cli
arkui-x init LinkGame
步骤 | macOS操作 | 效果 |
---|---|---|
连接设备 | 同时接入华为/iPhone | 设备列表自动识别 |
编译运行 | 点击"双端运行"按钮 | 源码同步编译到双设备 |
实时热重载 | 修改ArkTS代码后保存 | 双端界面同时刷新 |
指标 | HarmonyOS (Nova12 Ultra) | iOS (iPhone13Pro) |
---|---|---|
帧率(FPS) | 59.8 | 60.1 |
内存占用(MB) | 86.3 | 91.7 |
启动时间(ms) | 423 | 487 |
@ObservedV2
class Cell {
@Trace value: number = 0 // 数据变更自动触发双端UI更新
}
// 棋盘数据变更后,iOS/HarmonyOS同时重绘网格
removeIcons(): void {
const newGrid = [...this.gridData] // 使用响应式更新
newGrid[r1][c1].value = 0
this.gridData = newGrid // 触发双端UI同步
}
// BFS核心算法在双端完全一致
private bfsCheck(): boolean {
const queue: QueueItem[] = [] // 使用标准TypeScript语法
while (queue.length > 0) {
// 路径计算逻辑无需平台适配
if (current.row === r2 && current.col === c2) {
return current.turns <= 2 // 直接返回计算结果
}
}
}
// 使用逻辑像素确保双端显示一致
GridItem()
.width(`${600/this.COLS}lpx`) // lpx自动适配屏幕密度
.height(`${600/this.COLS}lpx`)
// 图标组件根据平台自动选择渲染引擎
@Builder
cellView() {
Text(`${value.value}`)
// 在HarmonyOS使用ArkUI渲染,在iOS转为UILabel
}
图:在华为Nova 12 Ultra(上)和iPhone13Pro(下)同步运行效果
ArkUI-X通过三大核心能力重新定义跨平台开发:
✅ 真原生性能 - 告别WebView和JS桥接的性能损耗
✅ 开发范式统一 - ArkTS语法屏蔽平台差异
✅ 生态无缝集成 - 直接调用HarmonyOS/iOS原生API
"当我在DevEco Studio按下运行键,看着游戏同时在鸿蒙和iOS设备上启动的瞬间,真正感受到了跨平台开发的未来已来。"
通过本实践可见,ArkUI-X在保持原生性能的前提下,真正实现了"一次编码,双端原生运行"的开发范式升级,为全场景应用开发开辟了新路径。
本案例基于鸿蒙(HarmonyOS)开发的聚合热搜热榜应用,通过调用韩小韩博客提供的热搜热榜聚合API,展示了多平台榜单数据并支持网页详情查看。项目采用ArkUI框架开发,现通过ArkUI-X实现iOS平台的无缝迁移。
HotListApp
├── entry/src/main/ets
│ ├── pages
│ │ ├── Index.ets # 主界面
│ │ └── MyWeb.ets # 网页视图
│ └── model # 数据模型
└── ohosTest # 测试模块
// 通用网络请求模块
async function commonRequest(url: string): Promise<any> {
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
return await response.json();
} catch (error) {
console.error('Network Error:', error);
return null;
}
}
ios/App/Info.plist
中添加网络权限:<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Tabs({ barPosition: BarPosition.Start })
.barAdaptive(true) // 启用自适应布局
.platformStyle({ // 平台差异化样式
ios: {
itemSpacing: 8,
selectedColor: '#007AFF'
},
default: {
itemSpacing: 12,
selectedColor: '#FF0000'
}
})
Web({
src: this.mobil_url,
controller: this.controller
})
.platformComponent({ // 平台原生组件映射
ios: (props) => new WKWebView(props)
})
@ObservedV2
class ResponseData {
@Trace success: boolean = true;
@Trace data: Array<ItemData> = [];
// 通用反序列化方法
static fromJSON(json: any): ResponseData {
const instance = new ResponseData();
instance.success = json.success;
instance.data = json.data.map(ItemData.fromJSON);
return instance;
}
}
npm install -g @arkui-x/cli
arkui-x init
# 生成iOS工程
arkui-x build ios
# 运行调试
arkui-x run ios
console.info()
输出跨平台日志现象:iOS平台无法获取数据
解决:
现象:iOS平台显示错位
方案:
Column()
.width('100%')
.platformAdaptive({ // 平台自适应布局
ios: { padding: 8 },
default: { padding: 12 }
})
处理策略:
// 统一数据格式处理
processData(data: any): ResponseData {
if (data?.hotList) { // 处理不同平台的返回格式
return this.transformLegacyFormat(data.hotList);
}
return ResponseData.fromJSON(data);
}
性能优化:
const cachedData = localStorage.getItem('hotData');
if (cachedData) {
this.myResponseData = ResponseData.fromJSON(JSON.parse(cachedData));
}
体验增强:
多平台扩展:
通过本项目的实践,我们验证了ArkUI-X在跨平台开发中的强大能力。开发者可以复用超过80%的HarmonyOS代码快速实现iOS应用开发,显著降低多平台维护成本。项目已开源至Gitee仓库,欢迎开发者共同参与完善。
未来展望:
通过持续优化,我们将进一步证明"一次开发,多端部署"理念的可行性,为移动应用开发提供新的范式参考。