老项目的旧名称在代码里出现了 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 的正常代码当成残留。
还有一类:脚本把系统类型当成旧名称。SceneDelegate、Configuration、AppDelegate------这些是 Apple 的类名,不是旧项目专属的。但如果你没有在规则里把系统类型加入白名单,脚本会把这些也标成"旧名称残留"。
怎么处理误报?建一个"明确保留名单"。扫描结果出来后,先把 protected(外部配置)、system(系统类型)、common-word(普通英文词)三类标记过滤掉,剩下才是需要人工判断的残留项。
重命名的结束信号不是"扫描结果为零"。是"扫描结果里只剩白名单上的已知项,其余全部处理完毕,并且真机主流程全部走通。"
扫描结果为零只代表你没有忘了改某一处,不代表改对了。