14.PlayerPrefs实践项目总结

  1. 14.总结
    1. 14.1 核心要点速览
      1. 反射里本系列会用到的几块
      2. 需求与单例入口
      3. Key 拼接规则
      4. SaveValue:基元与 bool
      5. SaveValue:List
      6. SaveValue:Dictionary
      7. SaveValue:自定义类型
      8. LoadData 与 LoadValue:和存储对称
      9. LoadValue:List
      10. LoadValue:Dictionary
      11. LoadValue:自定义类型
      12. 加密与预期
      13. 实现顺序建议
    2. 14.3 面试题精选
      1. 基础题
        1. 1. PlayerPrefs 原生支持哪些类型?bool 通常怎么存?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 课文里的 PlayerPrefs key 是怎么拼出来的?为什么要带上类型名和字段名?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 为什么用 typeof(IList).IsAssignableFrom(fieldType) 判断 List,而不是和 typeof(List<>) 直接比?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      2. 进阶题
        1. 1. 存 List 时为什么先 SetInt(keyName, Count),再按索引拼子 key?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. Dictionary 存成 _key_i 和 _value_i 两套 key,读取时如何保证还原成同一个字典条目?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. LoadData 里为什么要求类型有无参构造?LoadValue 末尾对自定义类型为什么直接 LoadData(fieldType, keyName)?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      3. 深度题
        1. 1. 若发布新版本时改了数据类的类名或字段名,旧 PlayerPrefs 存档可能发生什么?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 课文中 int 加 10 再写入、读出减 10,这类「加密」边界在哪里?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 把 PlayerPrefsDataMgr 打成 unitypackage 给别的工程用时,除了拷贝脚本还要留意什么?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章

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]intDictionary<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 / stringbool 用自订规则:true → SetInt(1)false → SetInt(0),读的时候 GetInt == 1 还原。
  • stringvalue.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)**;每个下标 iSaveValue(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> 实例。
  • 循环 ilist.Add(LoadValue(fieldType.GetGenericArguments()[0], keyName + i)),与存储时 **keyName+0keyName+1**… 一一对应。

LoadValue:Dictionary

  • GetInt(keyName) 得元素个数;Activator.CreateInstance(fieldType) as IDictionary;**GetGenericArguments()** 得到 [0] 键、[1] 值类型。
  • 循环 idic.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 几次;子 key keyName+0keyName+(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 流程,与 SaveValueSaveData(value, keyName) 对称,形成树状结构的深度优先存取。
答题示例

Activator 要先 new 出实例再填字段,没有无参构造就 new 不出来。

自定义类型字段和根对象一样要遍历字段,所以直接递归 LoadData,和存储时递归 SaveData 成对。

参考文章
  • 3.PlayerPrefs数据管理类创建
  • 7.反射存储自定义类成员数据
  • 11.反射读取自定义类成员数据

深度题

1. 若发布新版本时改了数据类的类名或字段名,旧 PlayerPrefs 存档可能发生什么?

题目

从 key 拼接规则分析兼容性与迁移思路。

深入解析
  • key 含 **type.Nameinfo.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

×

喜欢就点赞,疼爱就打赏