普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月3日iOS

Swift 6.1 新特性

作者 YungFan
2025年4月3日 11:50

Swift 6.1 内置于 Xcode 16.3,这是 Swift 6 之后的首个小版本更新,新特性很少。

尾随逗号

元组、函数的参数、闭包捕获列表以及字符串插值等都可以像数组一样,在最后一个元素的后面添加,,以便轻松地追加、删除、重新排列或者注释最后一个元素。

// 元组
(404, "Not Found",)
// 函数的参数
func sum(num1: Int, num2: Int,) -> Int {
    num1 + num2
}
var vehicle = "Car"
// 闭包捕获列表
let closure = { [vehicle,] in
    print("Vehicle:", vehicle)
}
// 字符串插值
"This is a \(vehicle,)"

混合开发

介绍

  • 增加新的关键字@implementation,配合@objc可以为 Objective-C 导入的.h声明提供实现。
  • 实现方式:在 Swift 中扩展 Objective-C 类,然后通过@objc @implementation实现属性与方法以替换 Objective-C 的@implementation

实现

  1. 新建一个基于 Swift 语言的 iOS 项目。
  2. 创建一个 Objective-C 类,此时会弹出一个提醒对话框(添加这个文件会创建一个 Objective-C 与 Swift 的混合项目,你是否希望 Xcode 自动配置一个桥接头文件来让 2 种语言的类文件相互可见?),点击Create Bridging Header
  3. 项目中多出 3 个文件,分别为创建的 Objective-C 类文件(.h.m)与 Bridging Header 文件,修改 Objective-C 类文件如下。
// .h文件
@interface Person: NSObject

@property(nonatomic, copy) NSString *name; 
-(void)eat;

@end


// .m文件
// @implementation Person
// @end
  1. 在 Bridging Header 文件中通过#import "类名.h"导入所有需要使用的 Objective-C 类的头文件。
  2. 在 Swift 中实现并且调用 Objective-C。
import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // 调用
        let person = Person()
        person.name = "zhangsan"
        person.eat() // zhangsan吃饭了
    }
}

// MARK: - @objc @implementation extension Objective-C类
@objc @implementation extension Person {
    // 实现属性
    var name: String?

    // 实现方法
    func eat() {
        print("\(name ?? "")吃饭了")
    }
}

注意:一旦在 Swift 进行了实现,Objective-C 中的@implementation不需要再实现,否则会报2 duplicate symbols的编译错误。

并发编程

  • actor 允许在属性与函数上使用nonisolated,表示该 API 可以在任何并发上下文中安全调用。Swift 6.1 将nonisolated支持到了类型与扩展,这样其内的所有属性与方法不需要再单独添加nonisolated
// 结构体
nonisolated struct Station {
}

class Train {
}
// 扩展
nonisolated extension Train {
}
  • withTaskGroup()withThrowingTaskGroup()的闭包可以推断出子任务结果类型。
// Swift6.1之前
await withTaskGroup(of: Int.self) { group in
    ...
}
await withThrowingTaskGroup(of: String.self) { group in
    ...
}
// Swift6.1之后
await withTaskGroup { group in
    ...
}
await withThrowingTaskGroup { group in
    ...
}

得物 iOS 启动优化之 Building Closure

作者 得物技术
2025年4月3日 09:50

得物一直重视用户体验,尤其是启动时长这一重要指标。在近期的启动时长跟进中,我们发现了在BuildingClosure 阶段的一个优化方式,成功的帮助我们降低了 1/5 的 BuildingClosure 阶段的启动耗时。Building Closure 并非工程的编译阶段(虽然它有一个building),Building Closure 是应用初次启动时会经历的阶段,因此它会影响应用的启动时长。

单就BuildingClosure阶段而言,我们观察到该阶段其中一个函数从 480ms 暴增到 1200ms 左右(PC 电脑端运行 dyld 调试统计耗时数据),我们通过优化,将耗时从1200ms降低到110ms。即使相比最开始的情况,也相当于从480ms降低到了110ms,由此可见Building Closure 优化是应用进行启动优化必不可少的一个重要手段。因此在这里我们也和各位读者进行分享,期望能够对各自项目有所帮助。

一、神秘的 BuildingClosure

启动优化的技术、实现方案业界有不少的文章可以参考学习,这里不再额外赘述。我们来探索下启动过程中非常神秘的 BuildingClosure。

BuildingClosure 是在 System Interface Initialization 阶段 dyld 生成的,并且我们也无法做任何的干预,另外相关的剖析文章相对较少,所以说 BuildingClosure 较为神秘,也是实至名归。

BuildingClosure 是由 dyld 在应用启动阶段执行的,所以想要了解 BuildingClosure 还是要从 dyld 开始了解。

1.1 dyld && BuildingClosure

Dyld 源码可以在 Apple GitHub 上查阅 github.com/apple-oss-d…

相信大家都应该了解过,BuildingClosure 是在 iOS 13 引入进来的,对应的 dyld 为 dyld3,目的是为了减少启动环节符号查找、Rebase、Bind 的耗时。

核心技术逻辑是将重复的启动工作只做一次,在 App 首次启动、版本更新、手机重启之后的这次启动过程中,将相关信息缓存到 Library/Caches/com.app.dyld/xx.dyld 文件中,App 在下次启动时直接使用缓存好的信息,进而优化二次启动的速度。

在 iOS 15 Dyld4 中更是引入了 SwiftConformance,进一步解决了运行时 Swift 中的类型、协议检查的耗时。

图片

以上优化,我们都无需做任何工作即可享受 dyld 带来的启动速度的优化,可以感受到 Apple 的开发人员也在关心启动速度并为之做了大量的工作。

1.2 BuildingClosure 非常耗时

我们通过 instrument 观测到 BuildingClosure 的耗时占据了启动耗时将近 1/3 的时间。

虽然说,BuildingClosure 只会在首次启动、版本更新、手机重启的第一次启动生成和耗时,但是对用户的体验影响是非常之大的。

图片

1.3 BuildingClosure 文件解析

我们通过对 dyld 的编译和搭建模拟手机环境,成功模拟器了 dyld 加载可执行文件的过程,也就成功解析了 BuildingClosure 文件。BuildingClosure 文件数据格式如下(数据格式、注释仅供参考,并非全部的数据格式):

图片

BuildingClosure 文件内部结构(数据格式、注释仅供参考)

其中占用比较大的部分主要为 Loader-selectorReferencesFixupsSize SwiftTypeConformance  objcSelector objcClass

二、离奇的启动耗时暴增事件

如上,我们已经对 BuildingClosure 有了基本的了解和对 dyld 的执行过程有了一定的了解。但是这份宁静在某一天突然被打破。

2.1 启动耗时暴增 200ms

在我们一个新版本开发过程中,例行对启动耗时进行跟踪测试,但是发现新版本启动耗时暴增 200ms,可以说是灾难级别的事情。

我们开始对最近的出包做了基本的耗时统计,方式为基于 instrument,统计出来启动各个阶段的耗时数据。经过对比,可以明显观测到,200ms 耗时的增加表现在 BuildingClosure 这个环节。

但是 BuildingClosure 耗时的增加既不是阶梯式增加,也不是线性增加,并且只在新版本有增加。在排除相关因素(动态库、工程配置、打包脚本、编译环境)之后,仍然没有定位明确的原因。

在以上定位工作之后,最终确定耗时确实在 dyld 的 BuildingClosure 阶段耗时,并且怀疑可能是某些代码触发了 Dyld 的隐藏彩蛋。所以我们开始了对 BuildingClosure 更深一步的研究。

2.2 BuildingClosure 耗时异常变化定位

通过使用 Instrument 对 System Interface Initialization 阶段进行堆栈分析,最终发现了耗时最高的函数:dyld4::PrebuiltObjC::generateHashTables(dyld4::RuntimeState&)

在对比了新老版本数据,耗时变化差异的函数也是此函数,我们简称为 generateHashTables。这样使得我们更加确定耗时为 dyld 过程中的 BuildingClosure 阶段。

图片

使用 Instrument 分析 BuildingClosure 阶段耗时

三、启动优化新秘境

在发现 BuildingClosure 生成过程中耗时占比非常大,并且有异常时,起初并没有意识到有什么问题,因为这是 dyld 内的代码,并未感觉会有什么问题。但是一切都指向了该函数,于是开始撸起袖子看代码。

从代码中可以看到,此处是为了生成 BuildingClosure 中 objcSelector objcClass objcProtocol 这三个部分的 HashTable(可以参考上面的 【BuildingClosure 文件解析】部分)。

拿起 dyld 开始对耗时异常版本的可执行文件进行调试,通过对该函数和内部实现的代码逻辑阅读,以及增加耗时信息打印。最终确定,耗时的代码在 make_perfect 这个函数中,这个函数是对【输入的字符串列表】生成一个【完美 Hash 表】。

void PrebuiltObjC::generateHashTables(RuntimeState& state)
{
    // Write out the class table
    writeObjCDataStructHashTable(state, PrebuiltObjC::ObjCStructKind::classes, objcImages, classesHashTable, duplicateSharedCacheClassMap, classMap);
    // Write out the protocol table
    writeObjCDataStructHashTable(state, PrebuiltObjC::ObjCStructKind::protocols, objcImages, protocolsHashTable, duplicateSharedCacheClassMap, protocolMap);
    // If we have closure selectors, we need to make a hash table for them.
    if ( !closureSelectorStrings.empty() ) {
        objc::PerfectHash phash;
        objc::PerfectHash::make_perfect(closureSelectorStrings, phash);
        size_t size = ObjCStringTable::size(phash);
        selectorsHashTable.resize(size);
        //printf("Selector table size: %lld\n", size);
        selectorStringTable = (ObjCStringTable*)selectorsHashTable.begin();
        selectorStringTable->write(phash, closureSelectorMap.array());
    }
}

继续深入了解 make_perfect 这个函数的实现。

3.1 Perfect Hash

通过对研读代码逻辑和耗时分析,最终定位到耗时代码部分为PerfectHash.cpp 中 findhash 函数,这个函数也是 完美散列函数 的核心逻辑。

这里涉及到了一个概念PerfectHash,PerfectHash 的核心是完美散列函数,我们看下维基百科的解释:

zh.wikipedia.org/wiki/%E5%AE…

对集合S的完美散列函数是一个将S的每个元素映射到一系列无冲突的整数的哈希函数

简单来讲 完美散列函数 是【对输入的字符串列表】【为每个字符串生成一个唯一整数】。

for (si=1; ; ++si)
    {
        ub4 rslinit;
        /* Try to find distinct (A,B) for all keys */
        *salt = si * 0x9e3779b97f4a7c13LL; /* golden ratio (arbitrary value) */
        initnorm(keys, *alen, blen, smax, *salt);
        rslinit = inittab(tabb, keys, FALSE);
        if (rslinit == 0)
        {
            /* didn't find distinct (a,b) */
            if (++bad_initkey >= RETRY_INITKEY)
            {
                /* Try to put more bits in (A,B) to make distinct (A,B) more likely */
                if (*alen < maxalen)
                {
                    *alen *= 2;
                }
                else if (blen < smax)
                {
                    blen *= 2;
                    tabb.resize(blen);
                    tabq.resize(blen+1);
                }
                bad_initkey0;
                bad_perfect0;
            }
            continue;                             /* two keys have same (a,b) pair */
        }
        /* Given distinct (A,B) for all keys, build a perfect hash */
        if (!perfect(tabb, tabh, tabq, smax, scramble, (ub4)keys.count()))
        {
            if (++bad_perfect >= RETRY_PERFECT)
            {
                if (blen < smax)
                {
                    blen *= 2;
                    tabb.resize(blen);
                    tabq.resize(blen+1);
                    --si;               /* we know this salt got distinct (A,B) */
                }
                else
                {
                    return false;
                }
                bad_perfect0;
            }
            continue;
        }
        break;
    }

此时通过对比新老版本的数据(使用 dyld 分别运行新老版本的可执行文件对比打印的日志),发现:

  • 老版本循环了 31 次成功生成 HashTable

  • 新版本循环了 92 次成功生成 HashTable

至此,我们距离成功已经非常接近了,于是进一步研读 dyld 源码和增加了更多打印信息代码,最终找到了相互冲突的函数字符串名称。

/*
 * put keys in tabb according to key->b_k
 * check if the initial hash might work
 */
static int inittab_ts(dyld3::OverflowSafeArray<bstuff>& tabb, dyld3::OverflowSafeArray<key>& keys, int complete, int si)
// bstuff   *tabb;                     /* output, list of keys with b for (a,b) */
// ub4       blen;                                            /* length of tabb */
// key      *keys;                               /* list of keys already hashed */
// int       complete;        /* TRUE means to complete init despite collisions */
{
  int  nocollision = TRUE;
  ub4 i;
  memset((void *)tabb.begin(), 0, (size_t)(sizeof(bstuff)*tabb.maxCount()));
  /* Two keys with the same (a,b) guarantees a collision */
  for (i0; i < keys.count(); i++) {
    key *mykey = &keys[i];
    key *otherkey;
    for (otherkey=tabb[mykey->b_k].list_b;
     otherkey;
     otherkey=otherkey->nextb_k)
    {
      if (mykey->a_k == otherkey->a_k)
      {
          // 打印冲突的字符串
        std::cout << mykey->name_k << " and " << otherkey->name_k << " has the same ak " << otherkey->a_k << " si is " << si << std::endl;
        nocollision = FALSE;
          /* 屏蔽此处代码,有冲突的情况下,继续执行,便于打印所有的冲突
    if (!complete)
      return FALSE;
           */
      }
    }
    ++tabb[mykey->b_k].listlen_b;
    mykey->nextb_k = tabb[mykey->b_k].list_b;
    tabb[mykey->b_k].list_b = mykey;
  }
  /* no two keys have the same (a,b) pair */
  return nocollision;
}

根据以上信息,我们已经了解到在Building Closure阶段中,可能存在字符串的 Hash 碰撞 引发循环次数大幅增加,进而引发了启动耗时暴增。

在经过 dyld 调试的耗时数据、构建出包后验证的数据验证后,通过避免 Hash 碰撞,我们完成了启动时长的优化。

3.2 向前一步

其实从打印的冲突函数名称来看,历史代码中已经存在了 Hash 碰撞 的现象。

猜想,如果我们解决了所有的字符串的 Hash 碰撞,岂不是不仅可以修复启动耗时异常上升的问题,还可以进一步降低启动耗时,提高启动速度?

于是我们对每个有碰撞的函数名称进行修改,经过出包验证,结果与我们猜测的一致,启动耗时有明显的下降。

图片

数据为 PC 电脑端运行 dyld 生成 BuildingClosure 的耗时数据,非手机端数据

四、总结

我们探索了 BuildingClosure 的生成过程,发现在Building Closure阶段中,可能存在字符串的 Hash 碰撞 引发循环次数大幅增加,进而引发了启动耗时暴增,进而导致启动耗时的大幅增加。

我们也发现,Building Closure Hash碰撞相关的启动耗时,其实与项目配置、编译环境、打包脚本等均无任何关系,就只是存在了字符串的Hash 碰撞 ,才引发循环次数大幅增加,进而导致启动时长增加。

往期回顾

1.分布式数据一致性场景与方案处理分析|得物技术

2.从对话到自主行动:AI应用如何从 Chat 进化为 Agent?开源项目源码深度揭秘|得物技术

3.得物技术部算法项目管理实践分享

4.商家域稳定性建设之原理探索|得物技术

5.得物 Android Crash 治理实践

文 / 道隐

关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

昨天 — 2025年4月2日iOS

WASM I/O 2025 | MoonBit获Kotlin核心开发,Golem Cloud CEO高度评价

作者 MoonBit
2025年4月2日 18:48

图片

图片

2025 年 3 月 27 日至 28 日,为期 2 日的 WASM I/O 2025 大会在巴塞罗那圆满落幕。期间,众多 WASM 社区领袖和技术专家,共同带来了 30 余场精彩的演讲,共同探讨云原生和开源领域的前沿洞察、核心技术与最佳实践。作为一门专为 WebAssembly 优化的新兴编程语言,MoonBit 作为首个中国开发者平台受邀在本次大会上进行技术分享。

主题演讲

在本次活动中,MoonBit 负责人张宏波带来了《MoonBit & WebAssembly》主题演讲。作为一门专为 WebAssembly(Wasm)设计的编程语言,MoonBit 凭借其极致的性能优化、创新的语法设计及开发者友好的工具链,展示了在浏览器与服务器端的全栈潜力。以下是本次演讲的核心技术回顾。图片

张宏波开宗明义地指出,现有语言在 Wasm 生态中存在三大痛点:二进制体积臃肿、编译速度缓慢以及运行时性能不足。MoonBit从语言设计之初便围绕Wasm优化,实现了极简二进制体积、闪电编译速度、高性能运行时的突破。

图片

MoonBit在设计时非常重视开发者体验,在工具链设计上体现了 “开发者优先” 的理念。其云 IDE 无需本地安装,集成并行类型检查、实时错误提示及基于 ChatGPT 的代码补全功能。调试支持亦是一大亮点,开发者可直接通过 Chrome DevTools 对 Wasm 模块进行断点调试和性能分析,与传统 JavaScript 工作流无缝衔接。MoonBit 与 WasmGC 的深度整合,使其在浏览器端表现尤为亮眼。通过零成本 JS 互操作Unicode 安全算法,在浏览器端实现无缝集成与性能突破。图片

针对服务器端开发,MoonBit 借助 Wasm 组件模型(Component Model)实现了跨语言协作的模块化架构。通过标准化接口定义语言(WIT),开发者可生成跨语言绑定。在编写 HTTP 服务器的实践中,MoonBit 生成的二进制大小仅为 27KB(Rust 为 100KB,Python 为 17MB)。

在未来,MoonBit 将深度整合组件模型,将 wasm-tools 直接嵌入工具链,支持 WASI 异步流(Phase 5提案)等前沿特性。

图片

MoonBit 的潜力不仅限于 Wasm,其支持多后端编译的特性为开发者提供了灵活的选择:编译为 C 代码时可运行于嵌入式设备(MCU),而实验性 JavaScript 后端则支持 Elm 架构,用于构建全栈应用。

对于一门语言来说,生态系统和开发者体验是其获得广大社区认可的必备条件。在 2025 年,MoonBit 将达到 Beta 版本,并进一步完善语言工具链与生态,随着 Wasm 在边缘计算、微服务等领域的深化,MoonBit 或将成为开发者拥抱下一代 Web 标准的关键选择。

社区反馈

在本次演讲后,Kotlin/Wasm 作者 Zalim 在社交媒体上表示:"MoonBit 在 WebAssembly 平台实现了精彩的成果",对 MoonBit 在 WASM 方向的技术成果给予高度认可。

图片

ZivergeTech & Golem Cloud 公司 CEO John A De Goes 表示:“与张宏波在 WASM I/O 见面后,我对 MoonBit 未来更加充满期待!MoonBit 融合了 Rust 语言的精华特性,还增加了垃圾回收(GC)机制,并优先关注工具链、性能和稳定性。欢迎大家加入我,一起参加在 LambdaConf 举办的 GolemCloud 黑客松活动,届时我会使用MoonBit!” 

图片

MoonBit 在本次 WASM I/O 上的精彩亮相吸引了海外社区诸多关注,我们期待未来 MoonBit 在全球开发者社区的无限潜力!

远离 dismiss,拥抱状态驱动

作者 Fatbobman
2025年4月2日 22:12

在 SwiftUI 开发中,环境值 dismiss 因其灵活、自适应的特性备受开发者青睐。它能够根据当前视图的上下文智能执行关闭操作:在模态视图中关闭窗口、在导航堆栈中弹出视图,甚至在多列导航容器中自动关闭边栏。正是这种看似“万能”的便捷性,让许多开发者将它作为首选工具。然而,便捷的背后往往隐藏着风险。频繁使用 dismiss 可能在应用程序中埋下隐患,引发测试难题乃至难以追踪的稳定性问题。本文将分析我们为何应谨慎对待 dismiss,并介绍更加健壮可靠的状态管理方案。通过重新审视视图呈现与消失的逻辑,我们能够打造出更稳定、易维护且可预测的 SwiftUI 应用。

昨天以前iOS

通过 llms.txt 引导 AI 高效使用网站内容

2025年4月1日 19:00
作为示例,本站也开始提供 llms.txt 和 llms-full.txt 的支持,可以参看下面的链接获取相关文件。 llms.txt llms-full.txt 什么是 llms.txt 大型语言模型(LLMs)是截止至训练日期时的人类知识的总集。而如果想要精确地解决更加实时的问题(比如在进行代码生成、研究辅助等任务中),我们可以通过搜索最新知识,依赖网络信息,来极大提升模型的准确性。然而,标准的 HTML 内容通常包含导航元素、JavaScript、CSS 和其他对于 LLMs 而言非必要的信息。这些冗余信息会在对话中占据 LLMs 有限的上下文窗口,也会干扰和降低处理效率。此外,LLMs 直接抓取和解析完整的 HTML 页面效率也很低下。为了应对这些挑战,llms.txt 应运而生,它是一个正在讨论的标准(参见 llms-txt.org),旨在为 LLMs 提供一个简洁、专业的网站内容概述,以单一且易于访问的 Markdown 文件格式呈现。llms.txt 就像一个网站的“指南”,引导 AI 系统找到站点上的关键信息,并以易于阅读和分析的结构化格式...

iOS启动优化 - 1分钟让你的启动时间降低 1s

作者 yidahis
2025年4月1日 16:01

先回答标题

原理是通过减少启动时加载的动态库数量达到目的。那一分钟就可以?是的,因为 CocoaPods 默认是会加载所有的依赖库,可以在一分钟内通过删除 Other Linker Flags 中的链接标记,取消启动时自动加载的动态库。那 300ms 是不是夸张了?没有一点点夸张,不信大家往下看。(文章结尾有惊喜!)

准备工作

如何测量启动时间?

咱们首选还是 instruments,它的 App Launch 工具 可将启动过程拆解为关键阶段(如 dyld 加载、Runtime 初始化、首帧渲染等),并量化各阶段的耗时。

image.png

如图,这里精确的量化了本次程序运行总的时长、加载可执行文件时长(包含主程序、动态连接器、动态库、各种初始化等)、加载三个动态库的运行时长。这里重点关注加载的动态库,它们的特点是描述以 Map Image + <动态库路径>

如何知道加载了哪些动态库?

如上图,其中以 Map Image 开头的描述表明这就是一个动态库。dlopen() 函数调用后也会生成一个同样的任务。 除了 instruments 还有其他手段。这里就给出一个笔者常用的一个工具。 新建一个类 LazyLoader,将以下代码拷贝到全局调用区。

// 打印动态库名称(兼容 M1/Mac 和 iOS)
void filterThirdPartyLibs(void) {
    uint32_t count = _dyld_image_count();
    for (uint32_t i = 0; i < count; i++) {
        const char* path = _dyld_get_image_name(i);
        if (!strstr(path, "/usr/lib/") &&
            !strstr(path, "/System/Library/")) {
            NSString* pathStr = [NSString stringWithCString:path encoding:NSUTF8StringEncoding];
            NSLog(@"Third-party: %@\n", pathStr.lastPathComponent);
        }
    }
}

// 在程序启动时自动调用
__attribute__((constructor)) static void runtime_init(void) {
    filterThirdPartyLibs();
}

见证奇迹的时刻即将来到,开始发车了。

首先通过 pod 新建一个私有 LoadAFDemo

pod lib create LoadAFDemo

接下来在 podfile 中添加以下依赖,并执行pod install

pod 'AFNetworking'

然后将工具 LazyLoader 拖入工程中并进行桥接。现在开始运行项目,可以得到如下结果:

image.png 可以看到 AFNetworking 在其中,这里过滤了系统的库,其他两个是主工程相关,因为开发环境被拆分为两个。 这个时候可以看到主工程的 Other Linker Flags 很简单,只有 AFNetworking

image.png

接下来打开App Launch 工具查看总的时长 242.06ms, 这个数字很重要,它代表了 pre-main 阶段的启动耗时。

image.png 如图,动态库的加载描述都是以 Map image 开头,其中第一个是笔者添加的依赖库 AFNetworking ,下面三个都是系统库的。

  1. 在 podfile 中添加一些依赖库。
pod 'SDWebImage'
pod 'Realm'
pod 'RxTheme','4.1.1'
pod 'RxGesture','3.0.2'
pod 'NSObject+Rx','5.1.0'
pod 'Moya','~> 14.0.0'
pod 'Alamofire', '~> 5.2.1'
pod 'SwiftyJSON','5.0.0'
pod 'R.swift','5.2.2'
pod 'CryptoSwift','1.4.2'
pod 'SnapKit','5.6.0'
pod 'FlexLayout','1.3.20'
pod 'PinLayout','1.9.2'
pod 'SwifterSwift/Foundation','5.2.0'
pod 'SwifterSwift/UIKit','5.2.0'
pod 'WMPageController','2.5.2'
pod 'CYLTabBarController'
pod 'KMNavigationBarTransition'
pod 'Firebase/AnalyticsWithoutAdIdSupport','9.6.0'
pod 'Firebase/Crashlytics','9.6.0'
pod 'FirebaseCoreDiagnostics','9.6.0'
pod 'FirebaseInstallations','9.6.0'
pod 'FirebasePerformance','9.6.0'
pod 'AppsFlyerFramework','6.5.2'
pod 'MoEngage-iOS-SDK','7.1.4'
pod 'MoEngageInApp', '2.1.2'
pod 'MORichNotification','5.2.0'
pod 'ZLPhotoBrowser', '4.2.5'
pod 'FCUUID','1.3.1'

此时运行pod install再观察,发现刚才添加的依赖库都被添加进来。

image.png

接下来运行项目,查看控制台输出:

image.png

总计44个三方库加载了,这里只截取部分。

接下来再次打开App Launch 工具查看总的时长为 1.47s, 比原来多出了近 1.2s。是不是很惊讶,这里的三方库加载耗时基本在 1ms 左右,共 44 个那也最多 100ms 才对。这里先卖个关子,请看接下来的表演。 image.png

接下来打开主工程,删除 Other Linker Flags 中的所有内容,再次运行项目。

image.png 发现三方库一个也没有加载。再次打开App Launch 工具查看总的时长为 197.07ms

image.png 看到这里的你,是不是惊讶,从 1.47s197.07ms,启动效能完全是飞升了。那这个做法可以直接运用到项目中去吗?接下来通过一些问题来解开谜题。

一些疑点

  1. 清空 Other Linker Flags 中的依赖后,代码能运行吗?
    答:能运行,不过相关的三方库会在 pre-main 阶段加载。请看下图, image.png 上图中首先 import AFNetworking 库,然后使用 AFHTTPSessionManager 获取其实例,且 callAF 方法没有任何地方调用,只是定义了。运行项目可以看到,控制台中输出了 AFNetworking,没有编译和运行报错。(如果只是 import 是不会触发链接操作的)。这里暂且把这种行为定义为 Real Import

  2. 那为什么会这样呢?
    其实答案很明显,获取 AFHTTPSessionManager 实例是编译期就确定的行为。而编译没有报错,那英爱是系统在 pre-main 阶段通过 LC_LOAD_DYLIB 指令加载 AFNetworking

  3. 在私有podspec中添加了 AFNetworking依赖, 还需要清除私有podspec中的 Other Linker Flags 吗?
    答:在私有库没有 Real Import 的前提下,不需要清除 Other Linker Flags 也可以实现依赖的三方库不被加载。就是说,私有podspec断开了和主工程链接关系,那么这个 AFNetworking 也就没有断开了和主工程的联系,所以即使 Other Linker Flags 添加了链接标记,也只是 AFNetworking 和当前私有库的。

  4. AFNetworking 换成 Alamofire 也是一样的吗?
    答:是的

  5. 没有加载AFNetworking的情况下,通过反射(NSClassFromString(@"AFHTTPSessionManager"))可以拿到实例吗? 答:当然不可以,需要时先动态加载 AFNetworking

  6. 说了这么多那 dyld 的加载动态库的原则是什么
    答:这个问题主要与 Mach-O 的文件结构有关。可以使用 otool -l <动态库路径> 查看文件中所以依赖的动态库。主要有两个命令LC_LOAD_DYLIBLC_LOAD_WEAK_DYLIB, 其实上面的问题中提到的Real Import实际上是执行 LC_LOAD_DYLIB 命令将动态库加载到强依赖库列表中,也就对应在Other Linker Flags中标记为-framework的动态库。LC_LOAD_WEAK_DYLIB命令将动态库加载到弱依赖库列表中,对应于Other Linker Flags中的-weak_framework标记的动态库。而无论是强依赖还是弱依赖,正常情况情况下都会加载。

  7. 你搞这么麻烦,直接用系统支持的 -weak_framework标记不就行了? 答:这个笔者经过实测,在目前的系统版本(iOS18)是不行的。经过查阅官方文档发现最后更新日期是2013年,应该是很久没有更新了。而且现在大部分使用的是dyld3

惊喜

为了更加直观的观察依赖关系,笔者专门开发了一个脚本工具。优点是:非常直观和方便。下面以咸鱼 iOS 为例。传送门

xianyu-outputs.html.png

都2025年,你竟然还敢买iOS的源码?

作者 iOS阿玮
2025年4月1日 10:22

App Store审核问题或3.2f咨询,请公众号留言。不免费,不闲聊。iOS研究院

序言

买源码这件事情在互联网公司初创是最常见的行为,除了节约了开发成本,更为重要的是节约了开发的时间

对于想做某方面业务,但是完全不懂技术的初创者来说,是再好不过的选择了。

尤其是在2015~2020年,口罩前期不少外包公司也是雨后春笋般。那个时候没有这么多较为知名的头部产品,也是人人都是产品经理的时代

做微商的想做自己的商城App,做化妆品、做珠宝、做鞋子的,等等...

转折点

在步入2022年之后,苹果先后提高了开发者的门槛。首先就是大陆区账号注册的难度提升,增加了人脸拍摄。提高了代码查重的算法。之前矩阵的打法也就是马甲包,不断沦为苹果重点照顾的重灾区

衍生的条款

在算法一步步提升的背景下,Guideline 4.3(a) - Design - Spam中拓展了3点内容:

  • We noticed your app still shares a similar binary, metadata, and/or concept as apps previously submitted by a terminated Apple Developer Program account.

译:我们注意到你的应用程序仍然与之前被终止的苹果开发者计划帐户提交的应用程序共享类似的二进制文件、元数据和/或概念。

  • Submitting an app with the same source code or assets as other apps already submitted to the App Store

译:提交与其他已提交到app Store的应用程序具有相同源代码或资产的应用程序

  • Purchasing an app template with problematic code from a third party

译:从第三方购买有问题代码的应用模板

加强版

最近有很多同行在咨询3.2f,其中最值得关注的是

Evidence of Dishonest or Fraudulent Activity


Your account is associated with terminated developer accounts and/or accounts 
engaged in fraudulent activities. These associations may include, but are not limited to, shared account information, 
submissions with similar or identical App Store metadata, app binaries with s
hared code, resources, or assets, or the transfer of apps engaged in fraudulent activities between accounts.

包括但不限于共享账户信息、具有相似或相同App Store元数据的提交、具有共享代码、资源或资产的应用程序二进制文件

也就是说在今年之前,仅仅局限于上架被拒4.3a。苹果在最近开始了新一轮的二进制比对,在未进行迭代,上新的账户中,触发了代码关联性的查重。

所以都2025年了,你还敢买iOS的源码?我建议是算了吧,你把握不住的~

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

Flutter 性能优化:实战指南

2025年4月1日 09:46

主要的优化方面,如首屏、列表、渲染、内存、数据加载、工具等。

一、首屏渲染优化

1. 骨架屏与占位符

  • 问题:首屏加载时白屏或长时间等待数据导致用户体验差。

  • 解决方案

    • 骨架屏:使用 SkeletonLoader 等组件模拟页面结构,替代空白屏4。
    • 占位符:数据未加载时返回轻量组件如 SizedBox.shrink(),降低首帧渲染时间(如减少 200ms)。
  • 代码示例

    Widget build(BuildContext context) {
      if (_isLoading) return const SizedBox.shrink(); // 轻量占位
      // 真实内容
    }
    

2. 资源预加载

  • 问题:首屏图片加载延迟。

  • 解决方案:在页面初始化前预加载关键资源(如轮播图、Banner)。

  • 代码示例

    Future<void> _precacheImages() async {
      await Future.wait([
        precacheImage(AssetImage('assets/banner.png'), context),
      ]);
    }
    

3. 数据预取与FFI优化

  • 问题:Flutter 通过 Channel 请求数据时线程切换和编解码耗时。

  • 解决方案

    • Native侧发起请求:在页面路由阶段由 Native 提前请求数据,避免 Flutter 线程切换。
    • FFI(Foreign Function Interface) :通过 FFI 直接读取 Native 缓存数据,减少序列化开销。
  • 效果:详情页启动时间优化 100ms 以上9。


二、长列表与滚动性能优化

1. 懒加载与按需构建

  • 问题:一次性渲染大量列表项导致卡顿。

  • 解决方案

    • ListView.builder/GridView.builder:仅构建可见项,避免内存溢出。
    • 分帧渲染(分帧加载) :将复杂子组件分帧渲染,避免单帧耗时过长。
  • 代码示例

    ListView.builder(
      itemCount: 1000,
      itemBuilder: (context, index) => ListItem(data[index]),
    )
    

2. 复用与缓存

  • 问题:频繁切换 Tab 导致重复加载数据。

  • 解决方案

    • 状态缓存:使用 PageStorageKey 或 AutomaticKeepAliveClientMixin 缓存页面状态。
    • 数据分级管理:通过 RxMap 维护不同分类的数据状态,减少重复请求。

三、UI渲染优化

1. 减少Widget重建

  • 问题:频繁调用 setState 导致不必要的 Widget 重建。

  • 解决方案

    • const Widget:使用 const 构造函数创建静态组件,避免重复构建。
    • Provider 选择性刷新:通过 Selector 或 Consumer 局部刷新,而非全量更新。
  • 代码示例

    const MyWidget(); // 编译时确定,避免重建
    

2. 动画优化

  • 问题:复杂动画导致帧率下降。

  • 解决方案

    • AnimatedBuilder:分离动画逻辑与静态组件,仅重绘动画部分。
    • 复用Child:将不变部分作为 child 参数传入,避免重复构建。
  • 代码示例

    AnimatedBuilder(
      animation: _controller,
      child: const StaticWidget(),
      builder: (context, child) => Transform.rotate(
        angle: _controller.value,
        child: child,
      ),
    )
    

四、内存与GPU优化

1. 内存泄漏检测

  • 问题:未释放资源导致内存持续增长。

  • 解决方案

    • 及时销毁监听:在 dispose 方法中取消订阅和释放资源。
    • 工具检测:使用 DevTools 的 Memory 视图分析内存泄漏。

2. GPU渲染优化

  • 问题:多图层叠加(如半透明蒙层)导致 GPU 负载高。

  • 解决方案

    • 避免 saveLayer:使用 ClipRRect 替代复杂蒙层,减少离屏渲染。
    • 缓存静态图像:通过 checkerboardRasterCacheImages 检测未缓存的图像。

五、工具与工程化实践

1. 性能分析工具

  • DevTools:通过 Timeline 视图分析帧耗时,定位构建、布局、绘制阶段的瓶颈。
  • Profile模式:打包 Profile 版本,获取接近真实环境的性能数据。

2. 模块级混合开发

  • 问题:全 Flutter 页面启动性能差(如动态库加载耗时)。

  • 解决方案

    • Native与Flutter混合:核心模块用 Native 实现,非核心模块保留 Flutter,降低启动时间。
    • FlutterBoost 扩展:支持模块级混合容器,解决生命周期和布局问题。

六、其他高级优化

1. Isolate 并行计算

  • 问题:Dart 单线程阻塞 UI。

  • 解决方案:将耗时计算(如 JSON 解析)放入 Isolate 执行。

  • 代码示例

    final result = await compute(heavyTask, data);
    

2. 资源压缩与预置

  • 问题:资源加载慢(如图片、字体)。

  • 解决方案

    • 图片压缩:使用 flutter_image_compress 优化图片体积。
    • 本地预置:关键资源(如 JSON 模板)预置到本地,减少网络依赖。

总结

  • 核心原则:减少 Widget 重建、按需加载、复用资源、分离耗时操作。

  • 实战案例

    • 携程酒店通过分帧渲染优化;
    • 淘宝特价版使用 FFI 和 Native 数据预取提升首屏速度;
    • 长列表懒加载和状态缓存避免卡顿。

iOS性能优化:OC和Swift实战指南

2025年4月1日 09:29

常见的iOS性能优化点,比如内存管理、UI优化、多线程处理、网络请求优化、启动优化、I/O操作、图像处理、算法优化、工具使用等。

一、内存优化

1. 循环引用处理

  • 原理:对象之间的强引用导致无法释放,内存泄漏。

  • Objective-C

    • 使用 __weak 或 __unsafe_unretained(需谨慎)打破循环:

      __weak typeof(self) weakSelf = self;
      self.block = ^{
          // 弱引用避免循环
          [weakSelf doSomething];
      };
      
    • 对 NSTimer 使用 NSProxy 或 weak 委托模式。

  • Swift

    • 使用 [weak self] 或 [unowned self] 捕获列表:

      self.block = { [weak self] in
          guard let self = self else { return }
          self.doSomething()
      }
      
    • unowned 适用于生命周期相同或更短的场景(如父子关系)。


2. 自动释放池(Autorelease Pool)

  • 原理:批量创建临时对象时,及时释放内存。

  • Objective-C

    @autoreleasepool {
        for (int i = 0; i < 100000; i++) {
            NSString *temp = [NSString stringWithFormat:@"%d", i];
            // 临时对象会被及时释放
        }
    }
    
  • Swift

    autoreleasepool {
        for i in 0..<100000 {
            let temp = "(i)"
            // 临时对象在块结束时释放
        }
    }
    
  • 适用场景:大量临时对象生成(如解析 JSON 数组、图像处理)。


二、UI 性能优化

1. 避免离屏渲染(Offscreen Rendering)

  • 原理:离屏渲染(如圆角、阴影)触发 GPU 额外绘制,导致卡顿。

  • 优化方法

    • 预渲染圆角

      • Objective-C

        // 使用 Core Graphics 提前绘制圆角
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0);
        [[UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:10] addClip];
        [image drawInRect:view.bounds];
        UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        view.layer.contents = (id)roundedImage.CGImage;
        
      • Swift

        let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
        let roundedImage = renderer.image { context in
            UIBezierPath(roundedRect: view.bounds, cornerRadius: 10).addClip()
            image.draw(in: view.bounds)
        }
        view.layer.contents = roundedImage.cgImage
        
    • 避免 shouldRasterize:除非复用图层,否则会触发离屏渲染。


2. Cell 复用与轻量化

  • 原理:避免频繁创建/销毁 Cell,减少 CPU 和内存压力。

  • Objective-C

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        // 复用 Cell
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CellID"];
        if (!cell) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CellID"];
        }
        // 轻量配置(避免复杂计算)
        cell.textLabel.text = [self.dataArray[indexPath.row] title];
        return cell;
    }
    
  • Swift

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CellID", for: indexPath)
        cell.textLabel?.text = dataArray[indexPath.row].title
        return cell
    }
    
  • 优化点

    • 避免在 cellForRow 中执行耗时操作(如网络请求)。
    • 使用 prepareForReuse 清理旧数据。

三、多线程优化

1. 主线程任务最小化

  • 原理:主线程阻塞导致 UI 卡顿(16ms 内未完成一帧绘制)。

  • Objective-C

    // 将耗时操作放到后台线程
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData *data = [NSData dataWithContentsOfURL:url];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = [UIImage imageWithData:data];
        });
    });
    
  • Swift

    DispatchQueue.global(qos: .userInitiated).async {
        let data = try? Data(contentsOf: url)
        DispatchQueue.main.async {
            self.imageView.image = UIImage(data: data!)
        }
    }
    

2. 线程安全与锁优化

  • Objective-C

    • @synchronized 实现简单但性能较低:

      objc

      @synchronized(self) {
          // 临界区
      }
      
    • 高性能场景使用 os_unfair_lock(替代已废弃的 OSSpinLock):

      #include <os/lock.h>
      os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
      os_unfair_lock_lock(&lock);
      // 临界区
      os_unfair_lock_unlock(&lock);
      
  • Swift

    • 使用 NSLock 或 DispatchQueue 屏障:

      let queue = DispatchQueue(label: "com.example.threadSafe", attributes: .concurrent)
      queue.async(flags: .barrier) {
          // 写操作(独占访问)
      }
      

四、网络优化

1. 请求合并与缓存

  • Objective-C

    // 使用 NSURLSession 的缓存策略
    NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10];
    
  • Swift

    let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10)
    
  • 优化点

    • 减少重复请求(如短时间内多次刷新)。
    • 使用 HTTP/2 多路复用降低连接开销。

五、启动时间优化

1. 冷启动阶段优化

  • 原理:减少 main() 函数之前的加载时间(T1)和首帧渲染时间(T2)。

  • Objective-C

    • 减少动态库数量,合并 Category。
    • 避免在 +load 方法中执行代码。
  • Swift

    • 使用 @UIApplicationMain 减少启动代码。

    • 延迟非必要初始化:

      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          // 延迟初始化第三方 SDK
      }
      

六、I/O 优化

1. 文件读写异步化

  • Objective-C

    dispatch_io_t channel = dispatch_io_create_with_path(DISPATCH_IO_STREAM, [path UTF8String], O_RDONLY, 0, queue, ^(int error) {});
    dispatch_io_read(channel, 0, SIZE_MAX, queue, ^(bool done, dispatch_data_t data, int error) {});
    
  • Swift

    let queue = DispatchQueue.global(qos: .background)
    DispatchIO.read(fromFileDescriptor: fd, queue: queue) { data, _ in
        // 处理数据
    }
    

七、图像处理优化

1. 异步解码与降采样

  • Objective-C

    // 使用 ImageIO 进行降采样
    CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
    CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)@{(id)kCGImageSourceThumbnailMaxPixelSize: @(300)});
    UIImage *image = [UIImage imageWithCGImage:imageRef];
    
  • Swift

    let source = CGImageSourceCreateWithURL(url as CFURL, nil)!
    let options: [CFString: Any] = [kCGImageSourceThumbnailMaxPixelSize: 300]
    let imageRef = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)!
    let image = UIImage(cgImage: imageRef)
    

八、算法与数据结构

1. 高效遍历与查询

  • Objective-C

    • 使用 NSDictionary 替代 NSArray 快速查找(O(1) vs O(n))。
  • Swift

    • 使用 lazy 延迟计算集合:

      let filteredData = data.lazy.filter { $0.isValid }.map { $0.value }
      
    • 使用 ContiguousArray 提升性能(连续内存布局)。


九、工具使用

1. Instruments 分析

  • Time Profiler:定位 CPU 热点函数。
  • Allocations:分析内存分配类型和泄漏。
  • Core Animation:检测离屏渲染和帧率。

十、Swift 特有优化

1. 减少动态派发

  • 使用 final 和 private 修饰类或方法:

    final class NetworkManager {
        private func fetchData() { ... }
    }
    

2. 值类型优先

  • 使用结构体(struct)替代类(class)减少引用计数开销:

    struct Point {
        var x: Double
        var y: Double
    }
    

总结

  • Objective-C 优化重点:手动内存管理、@autoreleasepool、避免 performSelector 潜在泄漏。
  • Swift 优化重点:值类型、协议扩展、DispatchQueue 和现代语法(如 Result 类型)。
  • 通用原则:减少主线程阻塞、复用资源、异步化耗时操作、利用工具分析瓶颈。

为什么 Swift 的反射像个“玩具”?聊聊 Mirror 背后的设计哲学

作者 JQShan
2025年3月31日 21:03

引子:当程序员想“窥探”一个对象

如果你用过 Java 或 C#,大概对「反射」这个词不陌生——它能让你在运行时像变魔术一样动态调用方法、修改属性,甚至篡改私有字段。但在 Swift 中,如果你想“窥探”一个对象的内部,苹果只给了你一个看似简陋的工具:Mirror。它不能修改属性,无法获取方法,连私有变量都藏着掖着。这不禁让人想问:反射这么重要的能力,苹果为何只做了个“玩具”出来?

今天,我们就来聊聊 Mirror 的设计逻辑,以及它背后隐藏的 Swift 语言哲学。

一、Mirror 的“克制”:安全与性能的双重防线

想象一下,你正在造一辆车。Java 的反射像是给你一套万能扳手,能拆发动机、改刹车片,甚至把车门卸下来。而 Swift 的 Mirror 更像是一把车钥匙——它能让你打开车门,看看内饰,但别想乱改零件。这种“克制”背后,是苹果对 Swift 的两大核心坚持:

  1. 安全第一 Swift 从诞生起就带着「杜绝未定义行为」的执念。反射能绕过编译检查,就像给代码开了个后门。比如,Java 中你可以用反射强行修改 final 字段,但这可能导致不可预知的崩溃。而 Mirror 的只读设计,本质上是在说: “你可以看看,但别乱动。”
  2. 速度不能妥协 Swift 被用于 iOS 系统内核、高性能游戏引擎等场景。反射的动态类型检查(as?Any)会带来运行时开销,而 Mirror 的轻量化设计,让它在需要时足够快,甚至能被编译器优化掉部分成本。

二、为什么不需要“万能扳手”?Swift 的编译时魔法

苹果似乎对动态反射兴趣缺缺,但其实他们找到了一种更“Swift 风格”的解决方案:把问题消灭在编译时。举个例子:

  • 场景:JSON 解析 在 Java 中,你可能用反射遍历字段,匹配 JSON 的 key。而在 Swift 中,Codable 协议通过编译器自动生成代码,直接映射属性——无需运行时反射,且类型安全零开销。
// 编译器自动生成编解码逻辑!
struct User: Codable {
    var name: String
    var age: Int
}
  • 场景:依赖注入 Java 的 Spring 框架依赖反射创建对象,而 Swift 可以通过泛型 + 协议,在编译时完成类型绑定:
// 编译时就知道 Container 里存了什么类型
container.register(UserService()) 
let service: UserService = container.resolve()

苹果的逻辑很明确:能通过类型系统解决的问题,绝不留到运行时。

三、Mirror 的生存空间:优雅的妥协

当然,总有些场景需要运行时信息。比如调试工具、动态生成日志,或是教学demo中展示对象结构。这时 Mirror 就派上用场了:

// 打印对象的所有属性
func debugPrint(_ value: Any) {
    let mirror = Mirror(reflecting: value)
    for child in mirror.children {
        print("(child.label ?? "?"): (child.value)")
    }
}

但你会发现,Mirror 的设计处处透着“小心翼翼”:

  • 不支持方法反射(避免动态派发)
  • 不暴露内存布局(防止不安全访问)
  • 对枚举和结构体的支持有限(鼓励模式匹配)

它更像是一个“安全气囊”,只在必要时弹出,而非让开发者随时飙车。

四、从 Mirror 看 Swift 的“价值观”

  1. 开发者不是敌人 Java 的反射默认允许访问私有字段,而 Swift 的 Mirror 对私有属性的可见性取决于模块边界——它假设开发者是理性的,但依然用访问控制保护代码的封装性。
  2. 工具链即力量 Swift 更倾向于通过编译器(如自动生成 Codable 代码)、Xcode 工具链(如 LLDB 调试器)来辅助开发,而非依赖运行时动态能力。
  3. 生态的统一性 在 SwiftUI 中,属性包装器(@State)、函数式编程等特性,让开发者无需反射也能实现动态 UI 和数据绑定。反射不再是必需品,而是备胎。

五、如果你真的需要“万能扳手”……

虽然苹果不鼓励,但总有极客想突破限制。比如:

  • 用 @dynamic 修饰符兼容 Objective-C 的运行时
  • 通过指针黑魔法直接操作内存布局(危险!但刺激)
  • 第三方库如 Runtime 提供元编程能力

但当你走这条路时,苹果的设计师可能会在背后叹气: “何必呢?明明有更安全的方式啊。”

结语:Mirror 是一面镜子,照出 Swift 的灵魂

Mirror 的简陋,恰恰反映了 Swift 的野心——它不想成为另一个“什么都能做,但处处是坑”的动态语言,而是试图用类型安全、编译时优化和清晰的API,重新定义现代编程的边界。就像 Swift 之父 Chris Lattner 所说: “我们希望开发者写出明显正确的代码,而非依赖运行时的小聪明。”

所以,下次当你嫌弃 Mirror 功能弱时,不妨换个角度想:或许不是苹果吝啬,而是他们相信,最好的魔法,应该发生在编译时 ✨。

MCP 崛起与苹果的 AI 框架设想 - 肘子的 Swift 周报 #77

作者 Fatbobman
2025年3月31日 22:00

在最近一段时间,在社交网络上,越来越多的 Model Context Protocol(MCP)使用者展示了各种丰富多彩的应用场景,从操控 Blender 创建精美场景,到利用最新的 GPT-4o 图片构建完整的漫画故事。MCP 巧妙地打开了以文本为主要互动手段的大模型,与现实世界之间的大门。

Swift运行时以及与OC混编

作者 jz_study
2025年3月30日 20:44

Swift运行时

  • 在swift中如果我们要定义一个表示错误的类型非常简单,只要遵循Error协议就可以了,我们通常用枚举或结构体来表示错误类型,枚举可能用的多些,因为它能更直观的表达当前错误类型的每种错误细节。

image.png

如何抛出错误

  • 函数、方法和初始化器都可以抛出错误。需要在参数列表后面,返回值前面加throws关键字

image.png

image.png

image.png

使用Do-Catch做错误处理

  • 在Swift中我们使用do-catch块对错误进行捕获,当我们调用一个throws声明的函数或方法时,我们必须把调用语句放在do语句块中,同时do语句块后面紧接着使用catch语句块。

image.png

try?

  • try?会将错误转换为可选值,当调用try?+函数或方法语句时候,如果函数或方法抛出错误,程序不会发生崩溃,而返回一个nil,如果没有抛出错误则返回可选值

image.png

try!

  • 如果你确信一个函数或者方法不会抛出错误,可以使用try!来中断错误的传播。但是如果错误真的发生了,你会得到一个运行时错误。
let photo = try! loadImage(atPath:"./Resources/John Appleseed.jpg")

指定退出的清理动作

  • defer关键字:defer block里的代码会在函数return之前执行,无论函数是从哪个分支return的,还是有throw,还是自动而然走到最后一行(类似于ts的finally)

image.png

权限控制(模块和源文件)

  • 模块指的是独立的代码分发单元,框架或应用程序会作为一个独立的模块来构建和发布。在Swift中,一个模块可以使用import关键字导入另外一个模块
  • 源文件就是Swift中的源代码文件,它通常属于一个模块,即一个应用程序或者框架。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型、函数之类的定义。

访问级别

image.png

潜规则1
  • 如果一个类的访问级别是fileprivate或private那么该类的所有成员都是fileprivate或private(此时成员无法修改访问级别),如果一个类的访问级别是open、internal或者public那么它的所有成员都是internal,类成员的访问级别不能高于类的访问级别(注意:嵌套类型的访问级别也符合此条规则)
潜规则2
  • 常量、变量、属性、下标脚本访问级别低于其所声明的类型级别,并且如果不是默认访问级别(internal)要明确声明访问级别(例如一个常量是一个private类型的类类型,那么此常量必须声明为private或fileprivate)
潜规则3
  • 在不违反1、2两条潜规则的情况下,setter的访问级别可以低于getter的访问级别(例如一个属性访问级别是internal),那么可以添加private(set)修饰符将setter权限设置为private,在当前模块中只有此源文件可以访问,对外部是只读的)
潜规则4
  • 必要构造方法(required修改)的访问级别必须和类访问级别相同,结构体的默认逐一构造函数的访问级别不高于其成员的访问级别(例如一个成员时private那么这个构造函数就是private,但是可以通过自定义来声明一个public的构造函数),其他方法(包括其他构造方法和普通方法)的访问级别遵循潜规则1
不透明类型(why)

image.png

  • 代码是可以编译通过的,但是makeTrapezoid的返回类型有凑有偿,被暴露了出去

image.png

  • 不能将其Container用作函数的返回类型,因为该协议具有关联类型。也不能将它用作返回类型的泛型约束,因为函数体外没有足够的信息来推断泛型类型需要什么

image.png

解决问题

image.png

返回不透明类型vs返回协议类型
  • 返回opaque类型看起来非常类似于使用协议类型作为函数的返回类型,但这两种返回类型的不同之处在于它们是否保留了类型标识。opaque类型是指一种特定类型,尽管函数的调用者不能看到是哪种类型;协议类型可以指代符合协议的任何类型。一般来说,协议类型为存储值的基础类型提供了更大的灵活性,而不透明类型可以对这些基础类型做出更强有力的保证。

image.png

ARC

  • Swift使用自动引用计数(ARC)来跟踪并管理应用使用的内存。大部分情况下,这意味着在Swift语言中,内存管理"仍然工作",不需要自己去考虑内存管理的事情。当实例不再被使用时,ARC会自动释放这些类的实例所占用的内存。
  • 引用计数只应用在类的实例。结构体(Structure)和枚举类型是值类型,并非引用类型,不是以引用的方式来存储和传递的

ARC如何工作

image.png

循环引用

  • 在两个类实例彼此保持对方的强引用,使得每个实例都使对方保持有效时会发生这种情况。我们称之为强引用环。
  • 通过用弱引用或者无主引用来取代强引用,我们可以解决强引用环问题

image.png

image.png

解决循环引用
  • 弱引用和无主引用允许引用环中的一个实例引用另外一个实例,但不是强引用。因此实例可以互相引用但是不会产生强引用环。
  • 对于生命周期中引用会变为nil的实例,使用弱引用;对于初始化时赋值之后引用再也不会赋值为nil的实例,使用无主引用

弱引用

  • 弱引用不会增加实例的引用计数,因此不会阻止ARC销毁引用的实例。这种特性使得引用不会变成强引用环。声明属性或者变量的时候,关键字weak表明引用弱引用。
  • 弱引用只能声明为变量类型,因为运行时它的值可能改变。弱引用绝对不能声明为常量。
  • 因为弱引用可以没有值,所以声明弱引用的时候必须是可选类型的。在Swift语言中,推荐用可选类型来作为可能没有值的引用的类型。

image.png

image.png

无主引用

  • 和弱引用相似,无主引用也不强持有实例。但是和弱引用不同的是,无主引用默认始终有值。因此,无主引用定义为非可选类型(non-optional-type).在属性、变量前添加unowned关键字,可以声明一个无主引用。
  • 因为是非可选类型,因此当使用无助引用的时候,不需要展开,可以直接访问。不过非可选类型变量不能赋值为nil,因此当实例被销毁的时候,ARC无法将引用赋值为nil。
  • 当实例被销毁后,试图访问该实例的无主引用会触发运行时错误。使用无主引用时请确保引用始终指向一个未销毁的实例。

image.png

闭包引用循环

  • 将一个闭包赋值给类实例的某个属性,并且这个闭包使用了实例,这样也会产生强引用环。这个闭包可能访问了实例的某个属性,例如self.someProperty,或者调用了实例的某个方法,例如self.someMethod。这两种情况导致了闭包使用self,从而产生了循环引用。
闭包引用循环解决
  • 定义占有列表-占有列表中的每个元素都是由weak或者unowned关键字和实例的引用(如self或someInstance)组成。每一对都在中括号中,通过逗号分开。
  • 当闭包和占有的实例总是互相引用时并且总是同时销毁时,将闭包内的占有定义为无主引用。
  • 相反的,当占有引用有时可能会是nil时,将闭包内的占有定义为弱引用。

image.png

内存安全

  • 默认情况下,Swift会克服代码层面上的一些不安全的行为,如:确保一个变量被初始化完后才能被访问、确保变量在销毁后不会被访问等等安全操作。
  • Swift也会确保在多路访问内存中同一区域时不会冲突(独占访问该区域)。通常情况下,我们完全无需考虑内存访问冲突的问题,因为Swift是自动管理内存的。然而,在码代码的时候,了解那些地方可能发生内存访问冲突是非常重要的。通常情况喜爱,如果你的代码有内存访问冲突,那么Xcode会提示编译错误或者运行时错误。
  • 内存访问分为两种
  1. 即时访问:即在访问开始至结束前都不可能有其他代码来访问同一区域
  2. 长期访问:即在访问开始至结束前可能有其他代码来访问同一区域。长期访问可能和其他即时访问或者长期访问重叠
inout参数访问冲突

image.png

image.png

image.png

解决访问冲突问题:

image.png

self访问冲突

image.png

image.png

有问题

image.png

三方库

简介

使用cocoapods

image.png

  • 网络请求:Alamofire
  • JSON解析:SwiftJSON
  • 资源管理:R.swift
  • 社交分享:MonkeyKing
  • 图片缓存与加载:Kingfisher
  • 自动布局:SnapKit
  • 标准款扩展:Dollar
网络请求:Alamofire
  • Alamofire是在平果URL Loading System基础上封装的网络库,简单易用并且可扩展
  • Alamofire
基本用法
  • AF命名空间,链式调用
AF.request("https://time.geekbang.org").response { response in
 debugPrint(response)
}
request方法详解

image.png

SwiftJSON

image.png

Dollar(是对标准库的扩展,类似于js的loadash)

image.png

image.png

image.png

Snapkit

image.png

  • make初始化约束还调用
  • update更新约束的使用
  • remark有冲突约束的时候使用
图片加载和缓存-Kingfisher(类似SDWebImage)

image.png

R.Swift

image.png

image.png

image.png

R.Swift之前

image.png

R.Swift的使用

image.png

MonkeyKing

image.png

OC和Swift运行时简介

Objective-C运行时

  • 动态类型(dynamic typing)
  • 动态绑定(dynamic binding)
  • 动态加载(dynamic loading)

image.png

image.png

派发方式

  • 直接派发(Direct Dispatch)
  • 函数表派发(Table Dispatch)
  • 消息机制派发(Message Dispatch)
直接派发
  • 直接派发是最快的,不止是因为需要调用的指令集会更少,并且编译器还能够有很大的优化空间,例如函数内联等,直接派发也有人称为静态调用
  • 然而,对于编程来说直接调用也是最大的局限,而且因为缺乏动态性所以没办法支持继承和多继承
函数表派发

image.png

image.png

  • 查表是一种简单,易实现,而且性能可预知的方式.然而,这种派发方式比起直接派发还是慢一点,从字节码角度来看,多了两次读和一次跳转,由此带来了性能的损耗,另一个慢的原因在于编译器可能会由于函数内执行的任务导致无法优化(如果函数带有副作用的话)
  • 这种基于数组的实现,缺陷在于函数表无法拓展.子类会在虚数函数表的最后插入新的函数,没有位置可以让extesion安全地插入函数
消息机制派发
  • 消息机制是调用函数最动态的方式.也是Cocoa的基石,这样的机制催生了KVO,UIAppearence和CoreData等功能.这种运作方式的关键在于开发者可以在运行时改变函数的行为.不止可以通过swizzling来改变,甚至可以用isa-swizzling修改对象的继承关系,可以在面向对象的基础上实现自定义派发

Swift运行时

  • 纯Swift类的函数调用已经不再是Objective-c的运行时发消息,而是类似C++的vtable,在编译时就确定了调用哪个函数,所以没法通过runtime获取方法、属性。
  • 而Swift为了兼容Objective-C,凡是继承自NSObject的类都会保留其动态性,所以我们能通过runtime拿到他的方法。这里有一点说明:老版本的Swift(如2.2)是编译期隐式的自动帮我加上了@objc,而4.0以后版本的Swift去掉了隐式特性,必须使用显式添加
  • 不管是纯Swift类还是继承自NSObject的类只要在属性和方法前面添加@objc关键字就可以使用runtime

image.png

  • 值类型总是会使用直接派发,简单易懂
  • 而协议和类的extension都会使用直接派发
  • NSObject的extension会使用消息机制进行派发
  • NSObject声明作用域里的函数都会使用函数表进行派发
  • 协议里声明的,并且带有默认实现的函数会使用函数表进行派发

image.png

Swift运行时-final @objc

  • 可以在标记为final的同时,也是用@objc来让函数可以使用消息机制派发。这么做的结果就是,调用函数的时候会使用直接派发,但也会在Objective-C的运行时里注册响应的selector.函数可以响应perfrom(selector:)以及别的Objective-C特性,但在直接调用时又可以有直接派发的性能
Swift运行时

image.png

image.png

image.png

Swift与OC的桥接

Swift调用OC

image.png

OC调用Swift

image.png

image.png

NS_SWIFT_NAME
  • 在Objective-C中,重新命名在swift中的名称
NS_SWIFT_UNAVAILABLE
  • 在Swift中不可见,不能使用
  • OC中可以调用Swift方法,要保证swift的类是继承自NSObject的

Subclass

  • 对于自定义的类而言,Objective-C的类,不能继承自Swift的类,即要混编的OC类不能是Swift类的子类。反过来,需要混编的Swift类可以继承自OC的类。

  • 定义一个常量值,后面可以方便使用;如#define TOOLBAR_HEIGHT 44;
  • 定义一个不变化的常用值,或者一个较长的对象属性;如#define SCREEN_WIDTH ([[UIScreen mainScreen] bounds].size.width)
  • 定义一个会变化的常用变量值,或者一个较长的对象属性;如: #define STATUS_BAR_HEIGHT ([UIApplication sharedApplication].statusBarFrame.size.height)
  • 定义一个带参数的宏,类似于一个函数;如#define RGB_COLOR(r,g,b) [UIColor colorWithRed:r/255.f gree:g/255.f blue:b/255.f alpha:1.0]

Swift独有特性

  • Swift中有许多OC没有的特性,比如,Swift元组、为一等公民的函数、还有特有的枚举类型。所以,要使用的混编文件要注意Swift独有属性问题。

NS_REFINED_FOR_SWIFT

  • Objective-C的API和Swift的风格相差比较大,Swift调用Objective-C的API时可能由于数据类型等不一致导致无法达到预期(比如,Objective-C里的方法采用了C语言风格的多参数类型;或者Objective-C方法返回NSNotFound,在Swift中期望返回nil)。这时候就要NS_REFINED_FOR_SWIFT

image.png

SwiftUI-国际化

作者 YungFan
2025年3月30日 20:13

介绍

  • 如果 App 需要提供给不同国家的用户使用,则需要进行国际化处理。
  • SwiftUI 项目的国际化主要包括:Info.plist 文件国际化、文本国际化等。

配置国际化语言

在进行国际化之前,必须要添加需要的国际化语言,选中国际化的项目 —> PROJECT —> Info —> Localizations,点击+添加需要的国际化语言(默认已经存在英文)。

Info.plist文件国际化

  1. 新建一个Strings File,必须命名为InfoPlist.strings
  2. 选中InfoPlist.strings,在 Xcode 的右侧文件检查器中找到Localization,点击Localize...,然后勾选配置的国际化语言。
  3. InfoPlist.strings左侧多了一个箭头,点击箭头展开后可以看见不同语言的Strings File,里面存放的是形如Key = Value的键值对。
  4. 在不同语言的Strings File中设置需要国际化的内容,如 App 名称等。
// 英文App名
"CFBundleName" = "I18N";
// 中文App名
"CFBundleName" = "国际化";

文本国际化

  1. 新建一个Strings File,必须命名为Localizable.strings
  2. 选中InfoPlist.strings,在 Xcode 的右侧文件检查器中找到Localization,点击Localize...,然后勾选配置的国际化语言。
  3. Localizable.strings左侧多了一个箭头,点击箭头展开后可以看见不同语言的Strings File
  4. 在不同语言的Strings File中设置需要国际化的文本键值对。
// 英文
"title" = "Reminder";
"message" = "Weather Information";
// 插值
"Weather is %@" = "Today is %@";
"Temperature is %lld" = "The temperature is %lld";
// 中文
"title" = "提示";
"message" = "今日天气";
// 插值
"Weather is %@" = "今天 %@";
"Temperature is %lld" = "气温 %lld 度";
  1. SwiftUI 文本国际化非常简单,开箱即用,因为大多数 View 与 Modifier 的构造方法中都将LocalizedStringKey作为参数类型,该参数的值为文本键值对中的键。
import SwiftUI

struct ContentView: View {
    let weather = "Sunny"
    let temperature = 10

    var body: some View {
        VStack {
            // 纯文本,有3种方式
            Text(title)
            
            Text(LocalizedStringKey("title"))

            Text("title", comment: "The title of the dialog.")
            
            // 自定义View
            MessageView("message")
            
            // 插值
            Text("Weather is \(weather)")
            
            Text("Temperature is \(temperature)")   
        }
    }
}

struct MessageView: View {
    var messaege: LocalizedStringKey

    init(_ messaege: LocalizedStringKey) {
        self.messaege = messaege
    }

    var body: some View {
        Text(messaege)
    }
}

注意:插值格式参考 String Format Specifiers

测试

默认情况下,App 的语言随着系统语言的变化而变化。但在开发阶段,如果才能快速测试 App 的国际化效果?主要有以下几种方式。

  1. 运行 App 之后在设备/模拟器通过设置(Settings)—> 通用(General)—> 语言与地区(Languages & Region) 切换系统语言以查看 App 的国际化效果。
  2. 通过 Xcode 菜单 —> Product —> Scheme —> Edit Scheme... —> Run —> Options —> App Language,选择需要测试的国际化语言之后再运行 App。
  3. 通过 Xcode 菜单 —> Product —> Scheme —> Manage Scheme... —> 选择需要复制的 Scheme —> 点击下方的圆形...图标 —> Duplicate —> 重命名 Scheme,然后将复制的 Scheme 按照方式 2 将 App Language 设置为需要测试国际化语言,最后运行时选择对应国际化语言的 Scheme。

效果

  • 英文。

英文.png

  • 中文。

中文.png

❌
❌