14.总结
14.1 核心要点速览
反射里本系列会用到的几块
- 后面存取数据类字段时,核心是拿 Type 做成员遍历;Assembly 用来从程序集取类型;需要「按类型名 new 出来」时用 Activator.CreateInstance。
- 口诀可以记成:1 个 T,两个 A(Type、Assembly、Activator)。
| API | 作用 | 典型用法 | 注意 |
|---|---|---|---|
Type.IsAssignableFrom(Type) |
判断「调用方类型」能否接收「参数类型」的实例 | typeof(Father).IsAssignableFrom(typeof(Son)) 为真时,可用 Activator.CreateInstance(sonType) as Father |
方向别反:是 fatherType.IsAssignableFrom(sonType) |
Type.GetGenericArguments() |
取出封闭泛型类型的实参类型数组 | List<int> 取 [0] 为 int;Dictionary<K,V> 依次为键、值类型 |
读 List / Dictionary 元素时用来传给下层 LoadValue |
Type fatherType = typeof(Father);
Type sonType = typeof(Son);
if (fatherType.IsAssignableFrom(sonType))
{
Father f = Activator.CreateInstance(sonType) as Father;
}
需求与单例入口
- 多个数据类各自写 Save / Load 会重复;抽一层 PlayerPrefsDataMgr,外部只传 数据对象 + 业务 key,内部反射字段并走 PlayerPrefs。
- 饿汉单例:私有静态实例、私有构造、
Instance对外。最终版在SaveData末尾会 **PlayerPrefs.Save()**,避免只写在内存里不落地。
Key 拼接规则
- 约定:**
keyName_数据类型名_字段类型名_字段名**,例如Player1_PlayerInfo_Int32_age。 - 字段值用
FieldInfo.GetValue(data)取出后交给SaveValue;读的时候用 同一规则 拼loadKeyName,否则对不上盘里的数据。
| 片段 | 含义 |
|---|---|
keyName |
调用方控制的存档槽,例如不同账号、不同档 |
dataType.Name |
外层对象类型,避免不同类里同名字段撞 key |
FieldType.Name |
字段声明类型,区分重载场景下的类型信息 |
info.Name |
字段名 |
SaveValue:基元与 bool
- PlayerPrefs 原生只管 int / float / string;bool 用自订规则:
true → SetInt(1),false → SetInt(0),读的时候GetInt == 1还原。 string存value.ToString(),读GetString。
SaveValue:List
- 用
typeof(IList).IsAssignableFrom(fieldType)判断「像 List 的东西」(接口判断,比写死List<>更宽)。 - 先 **
SetInt(keyName, list.Count)**,再用keyName + index递归SaveValue每个元素;元素已是泛型实参类型(如int),若是自定义类会继续走下面的递归分支。
SaveValue:Dictionary
- 用 **
typeof(IDictionary).IsAssignableFrom(fieldType)**,转成IDictionary遍历 Keys。 - 同样先 **
SetInt(keyName, dic.Count)**;每个下标i上SaveValue(key, keyName + "_key_" + i)与 **SaveValue(dic[key], keyName + "_value_" + i)**,键值都走同一套SaveValue,支持嵌套自定义类型。
SaveValue:自定义类型
- 以上类型都不命中时,当作 嵌套对象:**
SaveData(value, keyName)**,沿用同一套「遍历字段 + 拼 key」逻辑,key 会在前缀上越叠越长,但能保证路径唯一。
LoadData 与 LoadValue:和存储对称
- **
object data = Activator.CreateInstance(type)**,再type.GetFields()遍历,info.SetValue(data, LoadValue(info.FieldType, loadKeyName))。 - 类型须有无参构造,否则
Activator.CreateInstance直接异常。 - 基元与 bool 分支与
SaveValue对称;bool 按GetInt与 0/1 约定还原。
LoadValue:List
GetInt(keyName)得到 Count;**Activator.CreateInstance(fieldType) as IList** 得到具体List<T>实例。- 循环
i:list.Add(LoadValue(fieldType.GetGenericArguments()[0], keyName + i)),与存储时 **keyName+0、keyName+1**… 一一对应。
LoadValue:Dictionary
GetInt(keyName)得元素个数;Activator.CreateInstance(fieldType) as IDictionary;**GetGenericArguments()** 得到[0]键、[1]值类型。- 循环
i:dic.Add(LoadValue(kv[0], keyName + "_key_" + i), LoadValue(kv[1], keyName + "_value_" + i))。
LoadValue:自定义类型
- **
return LoadData(fieldType, keyName)**,与存储侧SaveData(value, keyName)成对,形成递归读回。
加密与预期
- 思路上按三层理解:找不到(藏位置)、看不懂(混淆/编码)、解不出(强算法 + 密钥管理)。课文用 int 存盘前 +10、读取时 -10 演示「动过手脚」,不是真安全。
- 注意:单机本地加密主要是 提高普通玩家改档成本;规则与源码一旦暴露,这种手段很容易失效;对一般玩家仍有一定门槛。
实现顺序建议
先跑通 key 规则 + 基元存取 → 再 List / Dictionary → 再 自定义类型递归 → 需要时加 简单偏移「加密」 与 Save() → 最后 导出 unitypackage。
14.3 面试题精选
基础题
1. PlayerPrefs 原生支持哪些类型?bool 通常怎么存?
题目
Unity PlayerPrefs 直接提供了哪些 Set/Get?课文里的 bool 是怎么落到 PlayerPrefs 上的?
深入解析
- API 层面就是 int、float、string 三组。
- bool 没有专用 API,课文用 int 0/1 约定:
SetInt(key, b ? 1 : 0),读时GetInt(...) == 1。 - 若默认值与「未写入」混淆,要想好 缺 key 时 的默认是 0 还是你想要的业务默认。
答题示例
PlayerPrefs 原生只直接支持 int、float、string。
bool 一般用 int 代存,例如 true 写 1、false 写 0,读取时按 int 再还原成布尔。
参考文章
- 4.反射存储常用成员数据
- 8.反射读取常用成员数据
2. 课文里的 PlayerPrefs key 是怎么拼出来的?为什么要带上类型名和字段名?
题目
说明 SaveData 里为每个字段生成唯一 key 的规则,以及各段分别解决什么问题。
深入解析
- 规则:**
keyName_数据类型名_字段类型名_字段名**。 - keyName:调用方区分不同存档对象或槽位。
- 数据类型名:不同类里可能有同名字段,防止跨类冲突。
- 字段类型名 + 字段名:同一类里区分不同字段;类型名参与也有助于人工排查存档内容。
答题示例
规则是 keyName、外层类名、字段类型名、字段名四段拼接。
这样同一业务 key 下,不同类、不同字段、不同类型都不会轻易撞 PlayerPrefs 的 key。
参考文章
- 4.反射存储常用成员数据
3. 为什么用 typeof(IList).IsAssignableFrom(fieldType) 判断 List,而不是和 typeof(List<>) 直接比?
题目
存储分支里为何用 IList 做 IsAssignableFrom 判断?
深入解析
List<int>、List<string>等是 不同封闭泛型类型,不好用一个typeof写死。- IList 是列表能力的公共基接口,
List<T>实现它;用 IsAssignableFrom 判断「当前运行时类型是否可当作 IList 用」,就能统一处理各类List<>。 - 副作用:其它实现 IList 的类型 也会进这条分支,工程上要清楚自己是否只有
List<>。
答题示例
List 的具体类型随泛型参数变,不能简单等于某一个 typeof。
IList 是列表的统一接口,IsAssignableFrom 用来判断「是不是列表语义」,再转 IList 做 Count 和遍历。
参考文章
- 1.必备知识点-反射知识小补充
- 5.反射存储List成员数据
进阶题
1. 存 List 时为什么先 SetInt(keyName, Count),再按索引拼子 key?
题目
说明长度与元素 key 的对应关系,以及读取时如何还原。
深入解析
- PlayerPrefs 没有「数组」类型,只能 扁平成多个键值对。
- Count 告诉读取端要
Add几次;子 keykeyName+0…keyName+(n-1)与写入次序一致。 - 若只存元素不存长度,读侧不知道循环上界;长度存在 同一前缀 key 下,与课文实现一致。
答题示例
先存 Count,再按 0、1、2… 把每个元素递归存进不同 key。
读的时候先读出 Count,再循环用同样的索引规则去读子 key,用泛型实参类型递归 LoadValue。
参考文章
- 5.反射存储List成员数据
- 9.反射读取List成员数据
2. Dictionary 存成 _key_i 和 _value_i 两套 key,读取时如何保证还原成同一个字典条目?
题目
说明存储与读取时索引 i 的作用,以及键值类型从哪里来。
深入解析
- 每个序位
i上,键走..._key_i,值走..._value_i,成对出现。 - 读取时用
fieldType.GetGenericArguments()得到键、值两个 Type,分别递归LoadValue,再dic.Add。 - 遍历 Keys 的顺序 在写入与读回循环中保持一致,才能保证第
i对确实对应原字典的同一项;若业务强依赖「字典迭代顺序」,要意识到这与真实Dictionary语义未必一致,课文侧重 可逆存档 而非排序语义。
答题示例
每个下标 i 上分别存一对 key、value 的子键,读时用同样的 i 成对读出来 Add 进字典。
键值类型从 Dictionary 的泛型实参数组里取两个 Type,交给 LoadValue 递归。
参考文章
- 6.反射存储Dictionary成员数据
- 10.反射读取Dictionary成员数据
3. LoadData 里为什么要求类型有无参构造?LoadValue 末尾对自定义类型为什么直接 LoadData(fieldType, keyName)?
题目
结合 Activator.CreateInstance 与递归读写说明设计原因。
深入解析
- 外层
LoadData:必须先有一个空对象,再往字段里SetValue,所以依赖 公共无参构造(课文未展示 BindingFlags,示例均为可访问字段 + 无参构造)。 - 内层 遇到自定义类型字段:不再拆成基元,而是 再跑一遍完整 LoadData 流程,与
SaveValue里SaveData(value, keyName)对称,形成树状结构的深度优先存取。
答题示例
Activator 要先 new 出实例再填字段,没有无参构造就 new 不出来。
自定义类型字段和根对象一样要遍历字段,所以直接递归 LoadData,和存储时递归 SaveData 成对。
参考文章
- 3.PlayerPrefs数据管理类创建
- 7.反射存储自定义类成员数据
- 11.反射读取自定义类成员数据
深度题
1. 若发布新版本时改了数据类的类名或字段名,旧 PlayerPrefs 存档可能发生什么?
题目
从 key 拼接规则分析兼容性与迁移思路。
深入解析
- key 含 **
type.Name与info.Name**,改名会导致 整段 key 空间漂移,旧数据读不回来或读到默认值。 - 字段增删:新字段缺 key 时用 Get 默认值;删字段则遗留垃圾 key,一般可接受。
- 工程上常见做法:版本号 key、迁移脚本、或换用 稳定逻辑名 而非易变的类型名(课文为教学选直观规则,上线常要加固)。
答题示例
类名、字段名进了 key,改名等于换了一套键,旧存档会对不上。
线上要加存档版本、迁移或稳定的 key 策略,不能指望改类型名还自动兼容。
参考文章
- 4.反射存储常用成员数据
- 8.反射读取常用成员数据
2. 课文中 int 加 10 再写入、读出减 10,这类「加密」边界在哪里?
题目
说明能防什么、防不住什么,并结合「注意」配图中的观点表述。
深入解析
- 只是 固定偏移,等价于弱混淆,Registry/plist 里仍能看到 有规律 的整数。
- 防不住:知道规则的人、会搜内存的人、拿到源码或反编译的人。
- 配图观点:单机加密是抬门槛;源码与规则泄露则意义大减;对普通玩家仍往往够用。
答题示例
这是演示用的简单变换,不是密码学意义上的加密,懂规则或能看到源码就能还原。
实际项目里更多是防随手改注册表,真要安全要密钥管理与更强算法,并接受客户端终极不可靠。
参考文章
- 12.加密思路
3. 把 PlayerPrefsDataMgr 打成 unitypackage 给别的工程用时,除了拷贝脚本还要留意什么?
题目
从依赖、命名冲突、存档路径角度简述。
深入解析
- 依赖:
System.Reflection、集合类型、UnityEngine均在默认 Player 设置下通常无额外 asmdef 问题;若目标工程拆过程序集,要检查 引用。 - 类名冲突:若对方已有同名
PlayerPrefsDataMgr,需改名或放命名空间。 - 存档:PlayerPrefs 按 公司名+产品名 分桶(Unity Player Settings),不同项目 key 前缀相同也可能 逻辑撞车;导入后仍应用 唯一 keyName 前缀习惯。
- 加密:硬编码偏移如 +10 与业务强耦合,复用时别忘读写两侧一致。
答题示例
unitypackage 主要带走脚本资源,但要确认目标工程程序集引用和类名不冲突。
PlayerPrefs 实际存储还跟 Player Settings 有关,多项目复用时要统一 key 约定,加密规则也要成套搬过去。
参考文章
- 12.加密思路
- 13.生成资源包
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com