第 4 篇:通读源码,规划移植路线
本系列第 4 篇。面对 28 个陌生的 Java 文件、近万行代码,一个新手怎么不慌、理出「先翻什么后翻什么」的顺序?这篇是方法论 + 实战规划。
一、面对陌生代码库,先别急着写
新手最容易犯的错:一打开代码就从第一个文件开始逐行翻译。结果翻到一半发现「这个类依赖那个还没翻的类」,卡死。
正确姿势是先俯瞰全局,理清依赖关系,再决定顺序。核心原则只有一句:
被别人依赖的,先翻;依赖别人的,后翻。
这样翻译任何一个文件时,它需要的东西都已经准备好了。
二、dd-plist 的整体架构
通读后,我发现这个库逻辑上分成两大块:
① 数据模型层(NSObject 家族)—— 表示 plist 里的各种数据类型
② 读写层(Parser / Writer) —— 把三种格式的文件 ⇄ 数据模型 互转三种格式(XML、Binary、ASCII)各有一对 Parser + Writer。再加上一个统一入口 PropertyListParser 负责自动识别格式并分发。
三、依赖分层图(这就是翻译顺序)
我把 28 个文件按「谁依赖谁」分成了 5 层,从上往下翻,永远只依赖已经翻好的东西:
第 0 层|基础 / 无依赖(最先翻,最简单)
├─ PropertyListFormatException 自定义异常
├─ Base64 Base64 编解码(★可用仓颉标准库替代,省 2000 行!)
├─ ByteOrderMarkReader BOM 字节序识别
├─ ByteOrderMarkFilterInputStream 输入流 BOM 过滤
└─ LocationInformation(抽象)+3个子类 报错位置信息(可最后做甚至先跳过)
第 1 层|数据模型核心基类
└─ NSObject(抽象基类,722 行) ★所有数据类型的父类
第 2 层|具体数据类型(都继承 NSObject)
├─ NSNumber / NSString / NSData / NSDate / NSNull
├─ NSArray / NSSet / NSDictionary
└─ UID
第 3 层|辅助
└─ ParsedObjectStack 解析时的对象栈(防循环 / 深度限制)
第 4 层|读写器(依赖上面所有数据类型)
├─ ASCII: ASCIIPropertyListParser(1089) + ASCIIPropertyListWriter
├─ Binary: BinaryPropertyListParser(641) + BinaryPropertyListWriter(369)
└─ XML: XMLPropertyListParser(544) + XMLPropertyListWriter + XMLLocationFilter
第 5 层|统一入口(依赖所有读写器)
├─ PropertyListParser 自动识别格式并分发
└─ PropertyListConverter 格式互转四、推荐的翻译顺序 + 难度评估
| 顺序 | 模块 | 行数 | 难度 | 说明 |
|---|---|---|---|---|
| 1 | PropertyListFormatException | 87 | 🟢 极易 | 先建异常体系,练手 |
| 2 | NSObject(骨架) | 722 | 🟡 中 | 先翻抽象方法+简单部分,反射相关最后处理(不能砍,见下) |
| 3 | NSNumber / NSString / NSDate / NSNull | ~1300 | 🟡 中 | 核心数据类型,注意字符串编码 |
| 4 | Base64 | 2124 | 🟢 易 | 不用翻!用仓颉标准库,直接省 2000 行 |
| 5 | NSData | 219 | 🟢 易 | 依赖 Base64 |
| 6 | NSArray / NSSet / NSDictionary | ~1200 | 🟡 中 | 容器类型,对应仓颉 Array/HashMap/HashSet |
| 7 | UID + ParsedObjectStack | ~260 | 🟢 易 | 小工具 |
| 8 | ASCII Parser + Writer | ~1256 | 🟠 较难 | 手写状态机解析器,纯算法好翻 |
| 9 | Binary Parser + Writer | ~1010 | 🟠 较难 | 字节操作多,注意大小端 / 无符号 |
| 10 | XML Parser + Writer | ~630 | 🔴 最难 | ⚠️ 最大难点(见下) |
| 11 | ByteOrderMark 两个类 | ~196 | 🟢 易 | 流处理 |
| 12 | PropertyListParser + Converter | ~620 | 🟡 中 | 最后拼装统一入口 |
| — | LocationInformation 家族 | ~260 | 🟢 易 | 报错行列信息,可最后做/先简化 |
五、三个提前预警的难点
1. XML 解析(最难)🔴
Java 版用了 javax.xml(系统自带的 DOM / SAX 库)来解析 XML。仓颉标准库大概率没有等价的 XML DOM 库。 两个选择:
- 找仓颉生态里现成的 XML 库(如果有);
- 自己写一个小型 XML 解析器——plist 的 XML 结构其实很简单,标签就那么几个(
<dict> <array> <string> <integer>等),手写并不难,而且可控性强。我倾向后者。
2. 反射(NSObject 里的对象 ⇄ plist 互转)🟡
Java 版用反射把「任意 Java 对象」和 plist 互转(fromJavaObject / toJavaObject)。仓颉的反射机制不一样。
⚠️ 重要修正:共建规则要求「与上游对外接口、功能定义全量一致」,所以这块不能直接砍掉。策略是——放到最后处理,能用仓颉原生反射实现就实现;实在实现不了的,推荐用 FFI(调用现成的 C 等实现)兜底,保证功能对齐,等仓颉后续能力完善再无痛替换。
开发节奏上仍然「最后做」,但目标是「做完」,不是「不做」。
3. 无符号数 / 字节序 🟠
Java 没有无符号类型,二进制解析里全是 & 0xFF 这种操作。仓颉有 UInt8 / UInt32 / UInt64,反而更清晰,但翻译时要一一对应准确。
六、我的落地路线
第一版先打通核心闭环,反射和位置信息放到最后补齐(但不省略,见上):
异常 → NSObject 骨架 → NSNumber/NSString/NSData/NSDate/NSNull
→ NSArray/NSSet/NSDictionary → ASCII 读写 → Binary 读写 → XML 读写
→ 统一入口 → 用 test-files/ 里的真实 plist 验证
→ 补齐反射(toJavaObject/fromJavaObject) + 位置信息 → 全量对齐上游一个好消息:原库的 test-files/ 里有几十个真实的 .plist 测试文件(XML / 二进制 / ASCII 都有),src/test/ 里也有完整的 Java 测试用例。我们可以直接拿它们来验证翻译对不对——这是检验正确性最好的标尺。
七、这一步的收获(方法论)
这一篇其实教了一个通用技能:怎么啃一个陌生代码库。
- 先分层,别硬啃:找出「数据模型」和「功能逻辑」,理清依赖方向。
- 从叶子往根翻:先翻没有依赖的基础件,再翻依赖它们的。
- 识别难点,提前决策:哪些能用标准库替代(Base64)、哪些放到最后补齐(反射,用原生或 FFI)、哪些必须硬啃(XML)。
- 抓住现成的测试:原库的测试文件就是你的「标准答案」。
下一篇(待更新):用 cjpm 搭建 plist4cj 仓颉工程,并翻译第一个模块,写出第一段能跑的仓颉代码。