iOS 摸鱼周报 #100 | 再摸最后一次鱼
本期概要
本期话题:关于摸鱼周报停刊的一些感受 本周学习:Xcode 中实现更好的异常提示 内容推荐:String Catalogs、自定义字体加载、HTTP 类型、单向数据流、构建类 Facetime 应用等内容 摸一下鱼:不换账号切换 App Store 商店内容;吐司 Tusi.Art;Swift AST 可视化展示网站;开源的 AI 角色模拟框架 RealChar;一份小册:「精益副业:程序员如何优雅地做副业」
本期话题
再摸最后一次鱼
不知不觉摸鱼周报已经做到第 100 期了。从公众号后台看第 99 期到第 1 期的变化,找到第二期周报下一位读者的留言:
当时很受鼓舞,但也没想过要做多久,100 期是一个很玄幻的目标。但从那之后命运的齿轮开始转动,两年半之后,竟然真的做到了 100 期。通过后台数据能大致分析出周报的一些变化,前中期阅读量和互动量都还不错,到了近期反而有些下滑。这也正好反应了我做周报的状态,早期的时间投入和积极性都是比较理想的,到了近期周报对我来说反而有点像是一种负担了。想着怎么找到更好的内容,如何进一步突破,但基本都没有达到预期,确实有过迷茫,摸鱼周报此时就是到了那个难以突破的瓶颈期。
当然这只是一方面,另一方面是时间分配问题。工作上有很多事情需要更全面的规划,有很多东西想要深入了解,都还没有抽出时间;对 AI、前后端、绘画都比较有兴趣,已经列了好几个支线任务,都还是待完成状态;家庭方面,随着孩子的成长,也需要给她更多陪伴,尽量早点下班,每周至少逛两次公园。需要做的事情很多,但时间却是有限的,一个地方的投入变多就意味着另一个地方就要减少,现在想想,最近两个月玩原神的时间好像都不超过2 小时。
摸鱼周报要不要就此休刊,就变成了一个需要衡量 ROI 的事情了。早期的收获更多是定期输出的习惯培养、公众号运营、认识更多的朋友等等。现在这些也都没变,但带给我的激励要小很多了。我还是会有分享的诉求,但就投入度来说,摸鱼周报相对来说显得会比较重,好几次都搞到很晚。有想过改成两周一期、减少内容、利用 AI 生成内容,但这些都只是在强行续命,内容瓶颈没有解决,而且不满意的内容因为更新提醒占用读者时间,还不如不发。100 是一个很完美的数字,就让摸鱼周报停留在这一期吧。
感谢一直以来提供帮助的摸鱼周报编辑们,特别是肘子,不是他的帮助可能 100 期都做不到;也感谢各位陪伴的读者,被人关注,被人期待的感觉很美好,这些都是你们给的。后面也还是会继续输出一些内容,只不过密度会小一些,不再是周报形式。
本周学习
整理编辑:zhangferry
给代码异常提供更丝滑的提醒方式
在平常开发过程中,对于一些非预期的行为,我们通常使用 assert
或者 fatalError
附加一些信息,并中断程序来提醒开发者注意这类异常。它最大的问题在于触发此类语句,程序会立即崩溃,除了异常语句提供的信息和堆栈,什么都获取不到了,想跟进一些执行的上下文时也变得不可能。这里介绍两种相对不那么生硬,还能起到比较直接提醒的形式。方案实现逻辑参考自 Unobtrusive runtime warnings for libraries[1]。
断点提醒
就是程序运行到此处时,不再崩溃,取而代之是一个自动的断点,你可以选择调试上下文,也可以选择继续执行。主要逻辑是向运行的进程发送 SIGTRAP 信号:
raise(SIGTRAP)
它生效的前提是当前程序已经被 LLDB attach 了,SIGTRAP 全名是 "Trace/breakpoint trap",它是 POSIX 标准定义下的信号之一。这个信号的默认操作是让进程停止执行,并进入断点停止状态。raise(SIGTRAP)
就是向当前进程发送 SIGTRAP 信号,使其进入断点停止状态。如果要跳过断点,输入 "c",或点击跳过断言按钮都可以。
完整代码如下:
@inline(__always) func breakpoint(_ message: @autoclosure () -> String = "") {
#if DEBUG
var name = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
var info = kinfo_proc()
var info_size = MemoryLayout<kinfo_proc>.size
let isDebuggerAttached = sysctl(&name, 4, &info, &info_size, nil, 0) != -1
&& info.kp_proc.p_flag & P_TRACED != 0
if isDebuggerAttached {
fputs(
"""
\(message())
Caught debug breakpoint. Type "continue" ("c") to resume execution.
""",
stderr
)
raise(SIGTRAP)
}
#endif
}
这个方案的问题在于断点的位置无法控制,可能会让人疑惑。
issue 提醒
Xcode 会默认开启主线程检查,就是非主线程操作 UI 会被识别到并以一种紫色的提醒展示出来,它不会打断程序执行,仅仅用于展示。
这种能力其实也是可以自定义的,有一种方案是通过函数地址去调用 __main_thread_checker_on_report
,但因为它展示的分类还是 Main Thread Cheker,效果不太好。还有一种方案是通过 os_log,这种形式相对 trick,记得把它限定在 DEBUG 环境下。
// os_log 定义
@available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *)
public func os_log(_ type: OSLogType, dso: UnsafeRawPointer = #dsohandle, log: OSLog = .default, _ message: StaticString, _ args: CVarArg...)
// os_log 使用
os_log(
.fault, // 日志类型,有:default, info, debug, error, fault
dso: <UnsafeRawPointer>,
log: OSLog(
subsystem: "com.apple.runtime-issues", // 可固定
category: "CustomXcodeIssue" // 日志类别
),
"We encountered a runtime warning" // 需要自定义展示的日志信息
)
这里还有一个参数没有填,就是这个 dso
,它对应的是一个系统动态库的基址,而且它还不是随便一个动态库都行,哪谁可以呢,文章中用的是 SwiftUI,但非 SwiftUI 项目回来加载这个库,测试发现 Foundation 也是可以。获取 Foundation 的基地址有两种方式:
-
通过
nm
获取一个 Foundation 里的符号,然后通过dladdr
获取库的基值:var info = Dl_info()
dladdr(
dlsym(
dlopen(nil, RTLD_LAZY), ".." // Foundation 内部符号
),
&info
)
// info.dli_fbase 就是这个基址 -
通过检测所有需要加载的动态库,根据加载地址判断出 Foundation:
let count = _dyld_image_count()
for i in 0..<count {
guard let name = _dyld_get_image_name(i),
String(cString: name).hasSuffix("/Foundation"),
let header = _dyld_get_image_header(i) else {
continue
}
// header对应的就是基地址
}
为了让提示能够停留在我们的调用的地方,还需要给日志函数加上内联,最终效果如下:
完整代码:
@_transparent
@inline(__always)
func runtimeWarning(
_ message: @autoclosure () -> StaticString,
_ args: @autoclosure () -> [CVarArg] = []
) {
#if DEBUG
let message = message()
let log = OSLog(subsystem: "com.apple.runtime-issues", category: "CustomXcodeIssue")
struct DSOFinder {
static let dso: UnsafeMutableRawPointer = {
let count = _dyld_image_count()
for i in 0..<count {
guard let name = _dyld_get_image_name(i),
String(cString: name).hasSuffix("/Foundation"),
let header = _dyld_get_image_header(i) else {
continue
}
return UnsafeMutableRawPointer(mutating: UnsafeRawPointer(header))
}
return UnsafeMutableRawPointer(mutating: #dsohandle)
}()
}
os_log(.fault, dso: DSOFinder.dso, log: log, message, args())
#endif
}
内容推荐
推荐近期的一些优秀博文,内容涵盖 String Catalogs、自定义字体加载、HTTP 类型、单向数据流、构建类 Facetime 应用等方面。
整理编辑:东坡肘子
1、与 String Catalogs 有关的常见问题解答[2] -- 作者:Cihat Gündüz
@东坡肘子: 在 WWDC23 上,苹果为 Xcode 推出了一个新功能:String Catalogs。该功能取代了传统的本地化文件,简化了本地化流程。本文作者同时为 RemafoX( 一个本地化工具)的开发者,他在 Slack activity 上与苹果的工程师进行了深入探讨。作者将通过问题解答的形式对 String Catalogs 进行说明,以帮助开发者了解为什么应该对 Xcode 15 中的这个强大工具感到兴奋。
2、使用 Swift Package 插件将自定义字体加载到应用程序中[3] -- 作者:Pol Piella Abadia
@东坡肘子: 如果你发现自己一遍又一遍地使用相同的字体,那么就要考虑是否需要创建一个 Swift Package 来包含共享的字体文件和字体加载代码。这样可以更快地创建新的应用程序,通过一个单一的地方来更新所有应用程序的字体文件,并减少代码重复。本文作者将向你展示如何使用 SwiftGen 来实现这一点,让你的应用程序更加高效和可维护。
3、介绍 Swift HTTP 类型[4] -- 作者:Guoye Zhang、Eric Kinnear、Cory Benfield
@东坡肘子: Swift 社区刚刚发布了一个名为 Swift HTTP Types 的开源软件包,通过 HTTPRequest 和 HTTPResponse 提供了 HTTP 消息的核心构建块的通用表示。在项目中采用这些类型,可以在客户端和服务器之间共享更多的代码,从而减少在类型之间进行转换的成本。Swift 社区的最终目标是使用 Swift HTTP Types 替换 SwiftNIO 的 HTTPRequestHead 和 HTTPResponseHead,以及 Foundation 的 URLRequest 和 URLResponse 中的 HTTP 消息信息。
4、单向数据流[5] -- 作者:Majid
@东坡肘子: Majid 写过很多关于 SwiftUI 数据流的文章,分享了他在该领域的灵感和想法。这些想法经过多年应用程序构建的实践,最终产生了一个名为 swift-unidirectional-flow 的 Swift 软件包。该软件包实现了 Majid 所有的想法,并被用于他的项目中,支持并发以及构建实际应用所需的其他功能(如可预测、可预览、可调试、模块化)。但是,Majid 并不建议开发者直接使用该软件包。他认为,开发者不应该导入任何第三方库或框架来构建应用程序的核心功能。你可以使用它作为灵感,在应用程序中根据你的需求构建状态管理系统。
5、流式视频通话:如何使用 SwiftUI 构建类似 FaceTime 的应用[6] -- 作者:Amos G
@东坡肘子: 无论通话参与者身在何处,使用苹果设备,都可以通过 FaceTime 创建一对一或群组的音频/视频通话。本文将演示如何使用 SwiftUI 和 iOS Video SDK from Stream 来构建一个类似 FaceTime 的应用,与朋友和家人进行面对面的聊天。构建的 iOS 语音和视频通话应用程序可以支持多种用例,例如 1-1 通话,群组会议,远程医疗,约会和会议等。
摸一下鱼
整理编辑:zhangferry
1、切换 App Store[7]:要登录不同国家的 App Store 通常都需要有各个国家的 AppleID 才行,这个网站则可以做到随意切换 App Store 商城。用 iPhone 端 Safari 打开网站地址:https://as.dogged.cn,点击对应的国家会跳转至 App Store,并刷新 App Store 内容为当前国家的信息。当然你只能浏览商店,无法下载。
2、吐司 Tusi.Art[8]:一个国内的类 civitai 网站,有 AI 模型和 AI 图片的分类展示,我看一些 C 站有名的作者也有在吐司发布模型。该网站还提供了在线生成 AI 图片的功能,每天提供 100 算力,如果都使用文生图 + 高清修复的话,每天可以白嫖 50 张图片。最主要的是比我本地跑的要快。
3、Swift AST Explorer[9]:一个把 Swift 代码的 AST 可视化展示出来的网站。如下图是 Swift 中一个枚举声明 EnumCaseDecl 的结构:
4、RealChar[10]:最近比较火的一个 AI 项目,类似 character.ai,但要更优秀,它可以自定义 AI 角色的行为、背景,甚至声音;可以实现语音对话,且实时性很高,语音也让交流过程显得更加自然;提供多端接入能力,网页、iPhone、终端都可以。
还有提供了一个体验的平台:https://realchar.ai/,可能是声音的加持,跟这里的乔布斯交流,更显自然,很奇妙的感觉。对了,他还能说中文😅
5、精益副业:程序员如何优雅地做副业[11]:副业是一个比较理想的存在,这篇小书提到的几个实践有知识付费、社区付费、独立产品,这些适用性已经没有那么强了,能做出来的概率很低。但考虑副业,把一个目标拆分,尝试做成产品这件事本身还是比较有价值的。作为程序员很多时候我们了解的只是技术细节,而完成的产品设计还有很多。其中举了一个「福利单词」项目的商业模式画布,对于大多数产品都可以通过类似环节进行拆分。
关于我们
iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。
往期推荐
iOS 摸鱼周报 #96 | Vision Pro 打开空间计算的大门
引用资料
Unobtrusive runtime warnings for libraries: https://www.pointfree.co/blog/posts/70-unobtrusive-runtime-warnings-for-libraries
[2]与 String Catalogs 有关的常见问题解答: https://www.fline.dev/the-missing-string-catalogs-faq-for-xcode-15/
[3]使用 Swift Package 插件将自定义字体加载到应用程序中: https://www.polpiella.dev/load-custom-fonts-with-no-code-using-swift-package-plugins/
[4]介绍 Swift HTTP 类型: https://www.swift.org/blog/introducing-swift-http-types/
[5]单向数据流: https://swiftwithmajid.com/2023/07/11/unidirectional-flow-in-swift/
[6]流式视频通话:如何使用 SwiftUI 构建类似 FaceTime 的应用: https://getstream.io/blog/facetime-clone/
[7]切换 App Store: https://as.dogged.cn
[8]吐司 Tusi.Art: https://tusi.art/images/615780714069126582?post_id=616351352009977730&source_id=nz-zoFvmkkS7pfsjbX3x8xMh
[9]Swift AST Explorer: https://swift-ast-explorer.com/
[10]RealChar: https://github.com/Shaunwei/RealChar
[11]精益副业:程序员如何优雅地做副业: https://github.com/easychen/lean-side-bussiness