638 处旧名称残留——Swift/ObjC 混编项目安全重命名完整指南

老项目的旧名称在代码里出现了 638 次,旧资源前缀出现了 366 次。

你要把这些全部换成新的------不是一次全局查找替换就能搞定的事。因为旧名字不只是出现在代码里。它们还藏在桥接头里、藏在 Storyboard 的 XML 里、藏在图片资源的 JSON 描述文件里、藏在持久化的数据库字段名里、藏在外部平台的配置 plist 里。

改掉 95% 不难。剩下 5% 才是最危险的------它们不会在编译时报错,甚至不会在运行时报错,只会在某个特定条件下静默失败。比如"某个老用户升级后本地缓存加载不出来了"、"某个语言的翻译突然不显示了"。

这篇是做完这个项目之后,整理出来的完整操作指南。前半部分讲怎么分阶段推进,后半部分是一份逐项 checklist。

typescript 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   旧名称不是只出现在代码里。它散在六条链路上:                      │
│                                                                 │
│   ┌──────────────┐                                              │
│   │ 1. 代码       │ 类名、方法名、属性名、变量名、注释              │
│   └──────┬───────┘                                              │
│          ▼                                                      │
│   ┌──────────────┐                                              │
│   │ 2. 桥接头     │ Objective-C #import 和 @objc selector         │
│   └──────┬───────┘                                              │
│          ▼                                                      │
│   ┌──────────────┐                                              │
│   │ 3. 界面文件   │ Storyboard/XIB 的 custom class、reuse ID      │
│   └──────┬───────┘                                              │
│          ▼                                                      │
│   ┌──────────────┐                                              │
│   │ 4. 资源       │ 图片名、Lottie JSON、字体、视频、多语言 key     │
│   └──────┬───────┘                                              │
│          ▼                                                      │
│   ┌──────────────┐                                              │
│   │ 5. 持久化     │ UserDefaults key、Realm 类名、JSON 文件名       │
│   └──────┬───────┘                                              │
│          ▼                                                      │
│   ┌──────────────┐                                              │
│   │ 6. 外部配置   │ Info.plist、第三方 SDK 配置、URL Scheme        │
│   └──────────────┘                                              │
│                                                                 │
│   漏了任何一条链路,旧名称就会残留下来。                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

策略篇:分四阶段推进

阶段一:先列清单,不动代码

在改任何一行代码之前,先把所有旧名称出现的位置登记清楚。这一步的关键是不要跳过任何一类符号------包括你以为"肯定不用改"的。

清单里至少写清楚:文件名、所在路径、旧名称、新名称、符号类型(类名 / 属性 / 方法 / 资源 / key / selector / Storyboard class)、风险等级、是否已经处理。

为什么先列清单?因为你会在登记过程中发现三种"没想到":

  • 有些旧名字出现在第三方 SDK 的配置里------比如一个 plist 里的某个字段值带着老品牌前缀。改它会破坏 SDK 初始化,不能改。
  • 有些旧名字跟系统 API 重名------比如一个叫 Configuration 的类。它看起来像旧项目专属的,但自动扫描脚本容易把它当成通用词放过。
  • 有些旧名字出现在你已经忘记的角落------比如一个 Intent Extension 的 intentdefinition 文件、一个 Pod 的 build script 注释里。

清单列完之后,你就能回答"全项目到底有多少处要改"这个问题。在我这个项目里,答案是 638 + 366。有了总数,你才能算进度,才能知道"还差多少"。

阶段二:从低风险开始改

第一次动手改名字,先从改错之后影响最小的符号开始。

推荐顺序:

复制代码
Model 类名 → 方法名 → 私有属性名 → 文件级常量名

这些符号的特点是:编译不过就等于改错了。 编译器会帮你检查每个引用是否一致。改完一个 Model 的类名,编译器把所有用到它的地方标红------你跟着红点逐个改就行。这条链路基本不会漏。

但有一条底线:改类名和属性名的时候,顺手检查一下它们有没有被 @objc 暴露给 Objective-C。如果有,这个符号就不能再按"低风险"处理------它的风险等级立刻升到高,因为它涉及跨语言的字符串匹配。

阶段三:高风险符号逐条核对后再改

以下符号类型属于高风险,每一条都要在清单里单独标记,改完了人工核对,不能只靠编译器:

符号类型 为什么高风险
@objc selector 字符串 编译不检查字符串内容,写错了不会报错,运行到那一行才崩溃
Storyboard / XIB 的 custom class 不编译 XML,界面文件里的类名写错了,打开页面时才闪退
reuse identifier 字符串 跟 Storyboard 里的注册名不对应,cell 出不来
通知名(Notification.Name 发送方和接收方的字符串不匹配,消息静默丢失
UserDefaults / 持久化 key 改名后老数据读不到,用户看起来像是"数据丢了"
Realm 模型类名 / Codable CodingKeys 改名后数据库迁移失败或 JSON 解析失败
多语言 key 改名后某个语言 fallback 到英文或直接显示 key 原文
Intent Extension 的 intentdefinition 改名后快捷指令找不到对应 Intent,充电自动化失效

改这类符号之前,先确认三件事:它的旧名字在哪些文件里被引用、改掉之后有没有对应的运行时验证方式、如果出错了回退方案是什么。

阶段四:受保护的不改

有些东西永远不改。不是"暂时不改",是"不能改"。

外部平台下发的配置文件------比如某个 GoogleService-Info.plist、某个第三方 SDK 的初始化参数里的 Bundle ID 前缀------这些值跟服务端的白名单绑定。改了之后服务端不认,功能直接不可用。

这类符号在清单里标记为 protected,后续任何自动扫描脚本跑出来的相关告警全部自动忽略。

vbnet 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   四层风险 + 对应处理方式                                         │
│                                                                 │
│   🟢 低风险:编译器能检查                                          │
│      类名、方法名、私有属性、文件级常量                              │
│      → 直接改,跟编译错误走                                        │
│                                                                 │
│   🟡 中风险:编译不检查,运行才出错                                 │
│      Storyboard custom class、reuse identifier、@objc selector    │
│      → 逐条登记 → 改完人工核对 → 真机过一遍主流程                    │
│                                                                 │
│   🔴 高风险:静默失败,不容易发现                                   │
│      通知名、持久化 key、Realm 类名、Codable CodingKeys、多语言 key  │
│      → 逐条登记 → 改完逐条验证运行时行为 → 至少覆盖一次冷启动          │
│                                                                 │
│   ⚫ 不可改:受保护的外部配置                                       │
│      第三方 SDK 配置、Info.plist 的 URL Scheme、服务端绑定的字段     │
│      → 清单标记 protected → 扫描自动忽略 → 永不修改                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

操作篇:逐项 checklist

1. 桥接头

swift 复制代码
// Bridging-Header.h
#import "OldProjectName-Swift.h"  // ← 检查所有 #import
#import "OldClass.h"              // ← 旧类名是否还在

改法:把桥接头里引用的每一个 Objective-C 类的文件名和 #import 路径逐个检查。改完之后,所有依赖桥接头的 Swift 代码必须能编译通过。

2. @objc selector

swift 复制代码
// 改前
@objc func oldActionName() { ... }
button.addTarget(self, action: #selector(oldActionName), for: .touchUpInside)

// 改后
@objc func newActionName() { ... }
button.addTarget(self, action: #selector(newActionName), for: .touchUpInside)

编译器对 #selector() 有语法检查,改成不存在的方法名会报错。但如果你的 selector 是以字符串形式出现的------比如 Selector("oldActionName")------编译器不检查,风险立刻升高,必须人工核对。

3. Storyboard / XIB 的 custom class

Storyboard 是 XML 文件。里面有一行类似这样的内容:

xml 复制代码
<viewController customClass="OldHomeViewController"
                storyboardIdentifier="OldHomeVC">

把它改成新的类名。但不要手动编辑 XML------在 Xcode 的 Identity Inspector 里改。手动编辑容易破坏 XML 结构。改完之后,在 Xcode 里打开这个 Storyboard,确认每个页面的 custom class 旁边没有黄色警告。

4. 资源:五层验证

这是最容易出现"改到 95% 以为做完了"的地方。因为资源的变更不是改一个地方就结束的,它是一串动作:

第一层:文件存在。 OldImage.imageset 文件夹重命名为 NewImage.imageset。这一步只是改了磁盘上的文件夹名字。

第二层:Contents.json。 打开 NewImage.imageset/Contents.json,确认里面的 filename 字段引用的是新图片文件名。

第三层:代码引用。 全局搜索 "OldImage",把所有 UIImage(named: "OldImage") 改成 UIImage(named: "NewImage")。Storyboard 里通过 Asset Catalog 选的图片,要在 Xcode 的属性面板里重新选一次。

第四层:Target membership。 在 Xcode 里选中新图片,打开 File Inspector,确认 Target Membership 里勾选的是正确的 Target。如果 App 和 Extension 共享资源,两边都要确认。

第五层:真机显示。 前四层全都对了,图片还是可能出不来------比如 Assets.xcassets 的缓存没刷新、编译时图片没打进包。最终验证必须是:装到真机上,打开用到这张图片的页面,看到它正常显示。

每一层的检查方式不一样。前四层是静态的------看文件、看代码、看 Xcode 面板。第五层是运行的------必须跑起来。

5. UserDefaults key

swift 复制代码
// 改前
UserDefaults.standard.set(value, forKey: "old_key_prefix_value")

// 改后
UserDefaults.standard.set(value, forKey: "new_key_prefix_value")

如果旧项目和新 App 使用不同的 Bundle ID,它们天然有独立的 UserDefaults 存储空间,旧的 key 不会影响新 App。但如果你在 App Group 共享存储里用了旧 key,其他 Extension 或同开发者的 App 可能还在读旧名字------这种情况不能改 key,或者需要同时更新所有读取方。

6. 通知名

swift 复制代码
// 改前
Notification.Name("old_notification_name")

// 改后
Notification.Name("new_notification_name")

改完之后,同一个通知的发送方和接收方都用了新名字吗?跨模块的通知------比如一个 Extension 发送、主 App 接收------两边都改到了吗?搜索旧通知名字符串,确认结果为零。

7. 持久化格式

Realm 模型类改名了,但旧的 Realm 文件里还是旧类名------新 App 读旧数据会报"数据结构不匹配"的错误。如果你的策略是新 Bundle ID 新沙盒、不继承旧数据,就不需要做数据迁移,只需要确认新数据用新类名写入即可。

Codable 的 CodingKeys 如果手写了字符串映射,记得把这些字符串里的旧名字也改掉:

swift 复制代码
// 改前
enum CodingKeys: String, CodingKey {
    case oldPrefixId = "old_prefix_id"
}

// 改后
enum CodingKeys: String, CodingKey {
    case newPrefixId = "new_prefix_id"
}

8. 外部 SDK 配置

Firebase 的 GoogleService-Info.plist 里有 BUNDLE_ID 字段。Google Sign-In 的 URL Scheme 里有 com.googleusercontent.apps.XXXX。第三方分析 SDK 的初始化参数里可能有老 App 的名字。

这些值跟服务端配置绑定,不能随意改。在清单里标记为 protected,扫描脚本产出告警时自动忽略。

9. Intent Extension

如果你的 App 有 Siri Intent 或快捷指令自动化(比如充电自动触发某个功能),它的 intentdefinition 文件里可能有旧名字。Extension 自己的 Info.plist 里有 NSExtensionPrincipalClass,它的值是一个类名字符串------这个字符串不会被编译器检查,写错了 Extension 启动不了。

arduino 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   验证的五个层次------缺一层都不算做完                                  │
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │ 1. 文件存在                                               │   │
│   │    文件夹名 / 文件名改了吗?                                │   │
│   ├─────────────────────────────────────────────────────────┤   │
│   │ 2. 名称正确                                               │   │
│   │    Contents.json / 代码引用 / Storyboard 引用改了吗?       │   │
│   ├─────────────────────────────────────────────────────────┤   │
│   │ 3. Target 正确                                            │   │
│   │    Xcode 的 Target Membership 勾选正确吗?                 │   │
│   ├─────────────────────────────────────────────────────────┤   │
│   │ 4. 扫描结果为零                                            │   │
│   │    全局搜索旧名称,没有残留。                               │   │
│   ├─────────────────────────────────────────────────────────┤   │
│   │ 5. 真机显示正常                                            │   │
│   │    装到手机上,打开页面,看到它。                            │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│   扫描结果为零 ≠ 做完了。前四层只证明"没忘记改"。                     │
│   第五层才证明"改了之后能正常工作"。                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

扫描脚本的常见误报

自动扫描脚本能帮你找到大部分旧名称残留,但它也会产出大量误报。

最典型的误报:脚本不认识"这个英文词跟旧前缀恰好重名"。比如旧前缀是 CAI,脚本会把 UIScrollViewDelegate 里完全无关的 CAI 片段标出来。如果你的旧品牌是一个普通英文单词的不同写法------比如 Clean------脚本会把所有包含 clean 的正常代码当成残留。

还有一类:脚本把系统类型当成旧名称。SceneDelegateConfigurationAppDelegate------这些是 Apple 的类名,不是旧项目专属的。但如果你没有在规则里把系统类型加入白名单,脚本会把这些也标成"旧名称残留"。

怎么处理误报?建一个"明确保留名单"。扫描结果出来后,先把 protected(外部配置)、system(系统类型)、common-word(普通英文词)三类标记过滤掉,剩下才是需要人工判断的残留项。


重命名的结束信号不是"扫描结果为零"。是"扫描结果里只剩白名单上的已知项,其余全部处理完毕,并且真机主流程全部走通。"

扫描结果为零只代表你没有忘了改某一处,不代表改对了。

相关推荐
2501_915909064 小时前
全面解析iOS应用上架到App Store的完整流程与关键注意事项
android·macos·ios·小程序·uni-app·cocoa·iphone
00后程序员张5 小时前
抓包鹰 电脑本机网络连接表怎么看?哪些程序在联网 进程归属与可疑 IP 追溯
网络协议·计算机网络·网络安全·ios·adb·https·udp
2501_915918417 小时前
免 Xcode 开发 iOS,工具链替代方案与编译、签名、安装
ide·vscode·macos·ios·个人开发·xcode·敏捷流程
初级代码游戏8 小时前
iOS开发 Swift 速记5:高级运算符
ios·移动开发·swift
初级代码游戏9 小时前
iOS开发 Swift 速记4:函数与闭包
开发语言·ios·swift
2501_9151063210 小时前
iOS 证书类型及作用说明 账号证书与签名配置的完整解读
android·ios·小程序·https·uni-app·iphone·webview
凡泰AI12 小时前
如何通过小程序多端框架,让一个小程序同时运行在iOS、安卓、鸿蒙以及PC客户端,实现一次开发多端运行的效果
android·ios·小程序
ZacJi1 天前
当苹果将系统 API 划归 @MainActor:一次跨 SDK 版本的 Swift 并发踩坑与破局实战
ios·swift
mardelan1 天前
2026年iOS短视频总结工具测评强识别提效率 整理清晰更省心
ios·音视频