Windows应用的数据存储位置怎么选 ── SQLite / JSON / 注册表 / Access 判断表

· · SQLite, Windows, .NET, C#, 数据存储, 注册表, Access, 设计, 判断表, 技术咨询

「配置用INI文件就行了吗」「历史数据越来越多,想改用Access保存」「注册表和配置文件该怎么分工」。开发Windows业务应用时,数据存储位置的选择是必经之路。然而这个选择往往在项目初期随意确定之后就不再回顾,数年后才以「JSON文件膨胀到几十MB,启动变慢」「共享文件夹里的Access文件每周坏一次」「直接写入Program Files目录,导致在Windows 11环境下无法运行」等形式暴露出问题。

本文将针对业务用Windows应用的本地数据存储,把「放在哪里」(文件夹的选择)与「用什么保存」(格式・引擎的选择)分开整理。以本博客多次采用的判断表形式,汇总SQLite / JSON / 注册表 / Access各自的优势与陷阱。

1. 先说结论

  • 存储位置的选择是「放在哪里」与「用什么保存」这两个独立的判断。前者选错会导致权限、多用户相关的事故,后者选错则会导致损坏、性能、维护方面的事故。
  • 存放位置的基本原则是:用户个人的配置与数据放在 %LOCALAPPDATA%Environment.SpecialFolder.LocalApplicationData),全用户共享的数据放在 %PROGRAMDATA%,而绝不写入与exe相同的文件夹(Program Files目录下)1
  • 格式的首选很简单,只有两种:结构化的小型配置用JSON文件,持续增加的业务数据・历史记录・需要检索的数据用SQLite。这两者就能覆盖业务应用本地存储的大部分需求。2
  • 注册表是「存放小型标志或与Windows联动信息的地方」,并不是应用的数据存储区。如果不了解32bit/64bit的注册表重定向机制(Wow6432Node)就直接使用,会踩到「明明写入了却读不到值」的坑。3
  • 在新开发项目中选择Access(.accdb)作为数据存储区的理由几乎已经不存在。即使是为了与现有资产联动而使用,也会伴随「ACE提供程序的位数必须一致」这项部署上的限制。4
  • 不论选用哪种格式,机密信息(密码、API密钥)都必须单独处理:不要以纯文本方式放进JSON或注册表,而要用DPAPI保护。详情请参阅另一篇文章「Windows应用的机密信息存储 - 用DPAPI避免纯文本配置」。

2. 把要保存的数据分成4种类型

在决定用什么保存之前,先对打算保存的数据性质做分类。业务应用的本地数据,大致可分为以下4类。

分类 示例 特征
配置 连接目标、界面布局、上次打开的文件夹 小。启动时全部读取。有时用户想直接编辑
业务数据・历史记录 测量结果、处理历史记录、主数据的本地副本 持续增加。需要检索・汇总。损坏时业务影响很大
缓存 缩略图、已下载的资源 消失也能重新生成。需要容量管理
机密信息 保存的密码、Token 少量。不可以纯文本形式存放

每种分类适合的存放位置与格式各不相同,这正是本文的核心思路。如果你的应用是「配置和历史记录全部塞进同一个XML」,重新做这项分类就是改进的第一步。

3. 放在哪里 ── 文件夹选择的基本原则

如果使用.NET,就以 Environment.GetFolderPath 能取得的位置为基准。5

位置 获取方式 用途
%LOCALAPPDATA%\公司名\应用名 SpecialFolder.LocalApplicationData 用户个人数据的默认位置。优先考虑这里
%APPDATA%\公司名\应用名(Roaming) SpecialFolder.ApplicationData 仅限在漫游用户配置文件环境中,希望随用户移动的配置
%PROGRAMDATA%\公司名\应用名 SpecialFolder.CommonApplicationData 全用户共享的数据。需要设计ACL
文档文件夹下 SpecialFolder.MyDocuments 仅限用户会当作自己的文件处理的产出物(如导出的报表)

写成代码就是这么简单,但如果连「公司名\应用名」这层目录结构,以及首次运行时创建文件夹,都包进公共处理里,就能防止存储位置随意增加的情况。

public static class AppPaths
{
    public static string DataDir { get; } = CreateDir(
        Environment.SpecialFolder.LocalApplicationData);

    private static string CreateDir(Environment.SpecialFolder root)
    {
        var dir = Path.Combine(
            Environment.GetFolderPath(root), "KomuraSoft", "MyApp");
        Directory.CreateDirectory(dir);  // 若已存在则不做任何事
        return dir;
    }
}

之所以不用字符串拼接自己组出 %LOCALAPPDATA%,而是使用 Environment.GetFolderPath,是因为即使在服务账户、其他用户身份运行,或配置了文件夹重定向的环境下,它都能返回正确的位置。用任务计划程序以不同账户运行的瞬间路径就变了的事故(这是任务计划程序文章第5章提到的「手动运行才能用」问题的一种),也能借此避免。

下面列举3个陷阱。

  • 不要写入exe所在的文件夹。 标准用户无法写入Program Files目录下。旧版32bit应用可能因为UAC兼容性功能而被悄悄重定向到VirtualStore,这会造成「以管理员身份运行和以标准用户身份运行时,配置文件内容却不一样」这种令人费解的问题。
  • ProgramData是「能写,但不安全」。 在默认ACL下,可能形成某用户创建的文件,其他用户无法修改的状态。若要供全用户共享读写,应在安装程序中创建文件夹并明确设置ACL。
  • 不要把Roaming当作默认选项。 在域的漫游用户配置文件环境中,Roaming目录下的内容会在登录・注销时同步。若把体积大的数据或机器特有的数据(缓存、硬件配置)放在Roaming,会造成同步延迟,或「污染」到其他机器。犹豫不决时就选Local。

4. 用什么保存 ── 4种选项的真面目

4.1 JSON文件 ── 配置的首选

可以用System.Text.Json直接读写、人类可读、便于用Git管理与比对差异——用于配置用途的优点一应俱全。需要注意的地方有两点。

要做好防止损坏的对策。 若在写入过程中断电,残留下不完整的文件,下次启动时就会读不到。惯用做法是「先写入临时文件再替换」,.NET的话File.Replace就提供了带备份的替换功能。

var json = JsonSerializer.Serialize(settings, options);
var tmp = path + ".tmp";
File.WriteAllText(tmp, json);
if (File.Exists(path))
    File.Replace(tmp, path, path + ".bak");
else
    File.Move(tmp, path);

读取端也一开始就内置「若损坏就尝试.bak,如果也不行就以默认值启动并发出警告」这种降级处理,配置文件损坏就不会变成支持工单。

不要把它当作数据存储区使用。 JSON的适用范围,是「启动时全部读取、结束时全部写入」这个前提能成立的大小(大约数百KB以内)。如果开始把会持续追加的历史记录,或需要检索的数据放进JSON,那就是该换成SQLite的信号。

4.2 SQLite ── 持续增加的数据・需要检索的数据的首选

SQLite是无需服务器、单一文件、属于公共领域的嵌入式数据库,在.NET中可以通过Microsoft维护的ADO.NET提供程序Microsoft.Data.Sqlite,或EF Core的SQLite提供程序来操作。2 Microsoft本身也将其推荐为Windows应用本地数据存储的手段6,对于「在本地持续增加的结构化数据」,目前的情况是可以优先考虑SQLite。

首先用代码展示它有多方便。只要通过NuGet添加Microsoft.Data.Sqlite,不需要服务器搭建,也不需要连接字符串的管理界面,只要指定文件路径就能开始使用。

using Microsoft.Data.Sqlite;

var dbPath = Path.Combine(AppPaths.DataDir, "app.db");
using var conn = new SqliteConnection($"Data Source={dbPath}");
conn.Open();

// 仅首次运行:启用WAL模式并创建表
using (var cmd = conn.CreateCommand())
{
    cmd.CommandText = """
        PRAGMA journal_mode=WAL;
        CREATE TABLE IF NOT EXISTS measurement (
            id         INTEGER PRIMARY KEY AUTOINCREMENT,
            device_id  TEXT    NOT NULL,
            value      REAL    NOT NULL,
            created_at TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS ix_measurement_device
            ON measurement(device_id, created_at);
        """;
    cmd.ExecuteNonQuery();
}

// 插入时必须使用参数(不要用字符串拼接组出SQL)
using (var cmd = conn.CreateCommand())
{
    cmd.CommandText =
        "INSERT INTO measurement (device_id, value) VALUES ($device, $value)";
    cmd.Parameters.AddWithValue("$device", "CAM-01");
    cmd.Parameters.AddWithValue("$value", 23.5);
    cmd.ExecuteNonQuery();
}

可以看到,只需花费和「往JSON文件里追加内容」差不多的力气,就能获得带索引的检索・汇总,以及数量不受限的历史记录。如果想套用ORM,EF Core的SQLite提供程序就是构建在这个库之上的。

在此基础上,列举几个实务要点。

  • 启用WAL模式。 就是上面代码中的PRAGMA journal_mode=WAL;。这能提升读取与写入的并发性,即使是UI线程与后台处理同时操作同一个数据库的架构,也比较不容易卡住。WAL的设置会持久保存在数据库文件本身,所以不需要每次连接都重新发出。
  • 把写入集中到单一进程的单一路径。 SQLite的写入是以数据库为单位进行互斥的。若要从多个线程写入,通过队列把写入角色统一为一个,这样的设计比较安全。此外,大量执行细碎的INSERT时,用明确的事务整批处理,速度会比逐条commit快上好几个量级。
  • 不要放在网络共享文件夹。 通过SMB的文件锁定有很多与环境相关的问题,SQLite官方也把网络文件系统上的共享列为损坏原因的首位。7 一旦需要多台机器、多用户同时使用,那就已经是客户端・服务器型数据库(如SQL Server Express)的范畴了。
  • 要知道类型实际上只有4种。 SQLite的实际类型是INTEGER / REAL / TEXT / BLOB,日期和GUID都是以TEXT形式保存。事先确认一次Microsoft.Data.Sqlite的类型映射规则,日后在日期比较与排序上就不会犯难。8 上面示例中把created_at设为datetime('now')(UTC),也是因为混入本地时间会在排序与夏令时切换时产生问题。显示时再转换成本地时间的做法比较安全。
  • 备份不要用「复制文件」,而要用VACUUM INTO或Backup API。 单纯复制运行中的数据库文件,有时会拿到WAL与主体不一致的内容(详见第6章)。

4.3 注册表 ── 仅限小型标志与Windows联动信息

注册表适合存放的是「是否已安装」「启动项注册」「文件关联」等与Windows自身联动的信息,以及极小型的用户配置。原则是应用自己使用的配置放在HKCU下,机器通用的信息由安装程序写入HKLM(如果设计成在运行期间写入HKLM,会需要管理员权限,应避免)。

最大的陷阱在于位数。在64bit Windows中,从32bit进程看到的HKLM\Software会被重定向到HKLM\Software\Wow6432Node3 「用注册表编辑器看明明有值,应用却读不到」「32bit应用写入的值,64bit维护工具却看不到」,这些问题几乎都是这个原因。这类问题常在迁移到AnyCPU或转为64bit时暴露出来,请与COM、ActiveX的32bit/64bit问题(「COM/OCX/ActiveX的陷阱 ── Visual Studio的位数与管理员权限」)放在同样的语境下理解。

如果从.NET非得读取另一种位数视图不可(例如仍维持32bit的应用,需要读取注册在64bit侧的值),可以用RegistryView明确指定要用哪种视图。

using Microsoft.Win32;

// 从32bit进程读取64bit视图的HKLM
using var hklm64 = RegistryKey.OpenBaseKey(
    RegistryHive.LocalMachine, RegistryView.Registry64);
using var key = hklm64.OpenSubKey(@"SOFTWARE\KomuraSoft\MyApp");
var installDir = key?.GetValue("InstallDir") as string;

反过来说,一旦需要这样指定,也代表「究竟该写在32bit还是64bit才对」这个设计判断被拖延了。让写入端与读取端的位数保持一致,才是正道。

把超过数KB的数据或数组型数据放进注册表,无论在备份、迁移还是诊断方面都很不利。这类用途应该让位给文件(JSON / SQLite)。

4.4 Access (.accdb) ── 新项目几乎不再采用,现有系统对接则要有所取舍

过去业务应用的本地数据库首选是Access(JET/ACE),但现在几乎没有理由在新开发项目中选它。主要原因在于部署。要以代码访问.accdb,需要ACE(Access Database Engine)提供程序,而且应用的位数必须与ACE的位数一致才能连接。4 还存在与Office位数共存的问题,「在开发机上能跑,但客户端出现Microsoft.ACE.OLEDB.12.0 提供程序未在本地计算机上注册」是经典的支持工单。此外还需要安装可再发行组件包(Access Database Engine 2016 Redistributable),这也增加了部署物。9

尽管如此,实务中仍有需要用到Access的场合,比如与现有的Access业务系统做数据对接、读取用Access制作的主数据等。此时建议做出以下取舍:

  • 固定读写进程的位数(实务中多半固定为x86较为现实),并在安装程序中确认对应的ACE是否存在
  • 设计上要避免多人同时写入放在共享文件夹的.accdb(一旦损坏,恢复成本并不划算)
  • 长期而言,要保留迁移到SQLite或服务器型数据库的路径

建议做上述取舍。包含Excel/VBA资产在内的现有资产处理方式,也整理在「VBA是什么 - 局限性、未来性、该替换的场合」一文中。

5. 判断表

视角 JSON文件 SQLite 注册表 Access (.accdb)
擅长的数据 小型配置 持续增加的结构化数据、检索・汇总 小型标志、Windows联动 与现有Access资产对接
数据量参考值 ~数百KB ~数十GB ~数KB ~2GB(规格上限)
检索・汇总 ✕(前提是全部读入) ◎(SQL) ○(SQL)
人类可直接阅读 △(需工具) △(需Access)
抗损坏能力 △(需自行处理) ○(事务)
多进程同时访问 ○(同一台机器内)
多机共享 ✕(事实上)
额外的部署物 无(NuGet随附) 必须有ACE提供程序

如最后一行所示,任何一种本地存储技术都不适合「多机共享」。放进共享文件夹看起来像是能共享,但JSON没有互斥机制,SQLite在SMB上的锁定不可靠,Access则会伴随损坏风险而遭遇瓶颈。一旦出现多站点、多用户需要操作同一份数据的需求,请把它当作该搭建SQL Server Express之类的服务器型数据库,或搭建Web API的判断分界线。

6. 不损坏・可迁移・可恢复 ── 不分格式的通用设计

不论选择哪种格式,只要运行个几年,就一定会需要以下3项设计。是否在首次发布时就纳入,会大幅影响之后的维护成本。

6.1 为架构・格式添加版本号

应用一旦更新,保存数据的形式也会随之改变。「新版本读取旧版本写入的数据」的瞬间必定会到来,所以要让数据本身带有格式版本信息。

如果是SQLite,PRAGMA user_version正是为这个用途准备的。

int GetVersion(SqliteConnection conn)
{
    using var cmd = conn.CreateCommand();
    cmd.CommandText = "PRAGMA user_version";
    return Convert.ToInt32(cmd.ExecuteScalar());
}

void Migrate(SqliteConnection conn)
{
    void Exec(string sql)
    {
        using var cmd = conn.CreateCommand();
        cmd.CommandText = sql;
        cmd.ExecuteNonQuery();
    }

    var v = GetVersion(conn);
    if (v > 2)
        // 用旧版应用打开了新版应用创建的数据库的情况。
        // 不去碰触不了解的架构,在此中止才安全
        throw new InvalidOperationException(
            $"这个数据库(版本 {v})是由更新版本的应用创建的。");

    using var tx = conn.BeginTransaction();
    if (v < 1) Exec("ALTER TABLE measurement ADD COLUMN unit TEXT");
    if (v < 2) Exec("CREATE TABLE operator (id INTEGER PRIMARY KEY, name TEXT)");
    Exec("PRAGMA user_version = 2");
    tx.Commit();
}

这是启动时检查版本,只应用差异部分,也就是所谓迁移(migration)的最简形式。开头之所以拒绝「比自己更新的版本」,是为了防止把应用回退到旧版本(rollback)时,旧代码写入自己不了解的架构而造成损坏的事故。JSON的思路也一样,在根层放入"version": 2,读取时插入从旧格式转换的步骤,并拒绝读取过新的格式。「不要让没有版本号的数据格式问世」,只要守住这一点,未来的自己就能得救。

6.2 事先确定损坏时的降级行为

第4章提到了各格式的损坏对策(JSON的原子写入、SQLite的事务),但即便如此,仍会遇到「读不到的数据」——磁盘故障、防病毒软件误报而隔离、用户手动编辑。如果没有事先确定应用此时该如何应对,就会变成连启动都做不到的应用。

  • 配置读不到 → 以默认值启动,并通知用户此事(若默默套用默认值,会变成「我的配置丢了」的投诉)
  • 业务数据读不到 → 以只读模式或错误界面呈现「哪个文件损坏了」。不要自动覆盖修复(会让证据消失)
  • 有备份 → 提议恢复。不过自动恢复伴随着「误判为损坏而回退到旧数据」的风险,原则上要经过用户操作确认

6.3 备份的关键不在于「有没有备」,而在于「能不能恢复」

本地数据和服务器上的数据库不同,没有人会帮你备份。若要由应用端负责处理,就要确定以下3点。

  • 备份什么:业务数据要备,缓存不用备,机密信息则要考虑到DPAPI的特性——只能在同一用户、同一台机器上解密(迁移到其他机器需要另外的流程)
  • 何时・备到哪里:可在启动时或每日,以带世代编号的方式备份到%LOCALAPPDATA%内的backup文件夹。是否再放到共享文件夹或现有的PC备份对象文件夹,则要与运维方协商
  • 怎么备份:SQLite禁止在运行中直接单纯复制文件。用VACUUM INTO 'backup.db'可以一句话获得具有一致性的快照
// VACUUM INTO 不会自动创建父文件夹,且若输出目标已存在会报错。
// 事先完成文件夹创建与不重复文件名的确定
var backupDir = Path.Combine(AppPaths.DataDir, "backup");
Directory.CreateDirectory(backupDir);
var backupPath = Path.Combine(backupDir, $"app-{DateTime.Now:yyyyMMdd-HHmmss}.db");

using var cmd = conn.CreateCommand();
cmd.CommandText = "VACUUM INTO $path";
cmd.Parameters.AddWithValue("$path", backupPath);
cmd.ExecuteNonQuery();

因为世代会持续累积,备份后最好也一并加入「只保留最近N个世代、删除较旧者」的处理。

而且请务必实际演练一次恢复。有备份文件却没人知道怎么还原、也没试过,这是业务系统的「常见状况」。试着写一份在更换PC时把数据迁移到新设备的操作手册,通常就能发现备份设计的漏洞(比如用DPAPI保护的凭据无法迁移、路径包含用户名导致换用户就出问题等)。关于PC报废时如何清除数据,也请参阅「报废Windows PC前该做的事」。

7. 容易犯难的场景指南

  • 「虽然是配置,但将来似乎会增加」 ── 如果预计「启动时全部读取」的用法迟早撑不住,一开始就直接用SQLite。在SQLite里建一个「settings表」并没有什么不妥。
  • 「从INI/XML迁移」 ── 如果只是单纯换格式,改用JSON即可;如果这时混杂着历史记录类数据,就分离出来改用SQLite。在读取端保留1~2个版本的旧格式兜底方案,迁移会更安全。
  • 「被要求要能用Excel看」 ── 不要把数据存储区直接改成Excel/Access,而是保存在SQLite后再加上导出到CSV/Excel的功能,这样才能同时满足数据可靠性与需求。报表输出的做法请参阅「如何制作Excel报表输出」。
  • 「想用多个进程读写同一份文件」 ── 如果在同一台机器内,SQLite(WAL)相当够用,但需要设计写入冲突的处理。如果要以文件为基础进行对接,请使用「文件对接与锁定的最佳实践」中的互斥模式。
  • 「想在多台机器间共享」 ── 这就代表要从本地存储毕业了。首选是在相当于文件服务器的机器上搭建SQL Server Express(免费,数据库上限10GB)的客户端・服务器架构。另外,SQL Server的「LocalDB」尽管名称如此,实际上是供开发用途的单用户环境,请不要为了共享目的而选它。如果需要跨站点、也要从公司外部使用,就到了该考虑加入Web API架构的阶段。

8. 总结

存储位置的选择,只要拆成「放在哪里」(LocalAppData/ProgramData,且不写入Program Files)与「用什么保存」(配置用JSON、持续增加的数据用SQLite、注册表尽量少用、Access仅限现有系统对接)来思考,绝大多数情况都能毫不犹豫地决定。

在此基础上,不论选用哪种格式,都要在首次发布时就纳入第6章的3项套餐──格式版本号、损坏时的降级行为、可恢复的备份。唯独机密信息,永远要单独用DPAPI处理。只要掌握本文的判断表与通用设计,就能大致避开「膨胀到几十MB的JSON」「每周坏一次的共享Access」这类日后代价高昂的架构。如果对现有应用的存储方式感到不安,建议先从「盘点什么数据存在哪里」开始入手。

相关文章

相关咨询领域

合同会社小村软件承接业务应用数据存储方式的重新评估(包括从INI/XML/Access迁移的设计),以及数据损坏、性能劣化的原因排查。

参考链接

  1. Microsoft Learn,KNOWNFOLDERID。关于LocalAppData、RoamingAppData、ProgramData等Windows已知文件夹的定义。 

  2. Microsoft Learn,Microsoft.Data.Sqlite overview。关于Microsoft维护的SQLite用ADO.NET提供程序概述,以及它是EF Core SQLite提供程序的基础。  2

  3. Microsoft Learn,Registry Redirector。关于在64bit Windows中,32bit进程的注册表访问被重定向到Wow6432Node的机制。  2

  4. Microsoft Learn,Can’t establish a connection to Access Database Engine OLE DB。关于ACE OLE DB提供程序必须与访问的进程位数一致。  2

  5. Microsoft Learn,Environment.SpecialFolder Enum。关于用于从.NET获取已知文件夹的枚举类型。 

  6. Microsoft Learn,Use a SQLite database in a Windows app。官方教程,推荐在Windows应用的本地数据存储中使用SQLite与Microsoft.Data.Sqlite / EF Core。 

  7. SQLite,How To Corrupt An SQLite Database File。关于网络文件系统上的锁定失效是数据库损坏的主要原因。 

  8. Microsoft Learn,Data types (Microsoft.Data.Sqlite)。关于SQLite的4种基本类型,以及DateTime、Guid映射到TEXT的规则。 

  9. Microsoft,Microsoft Access Database Engine 2016 Redistributable。关于用于访问.accdb / .mdb的ACE可再发行组件包(32bit/64bit)。 

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

Windows应用的配置文件该保存在哪里?
如果是用户个人的配置・数据,基本原则是放在 %LOCALAPPDATA%(Environment.SpecialFolder.LocalApplicationData)下,以「公司名\应用名」的层级结构存放。若是全用户共享,则放在 %PROGRAMDATA%,但由于默认ACL可能形成其他用户无法修改的状态,需要在安装程序中明确设置ACL。绝对不可以写入与exe相同的文件夹(Program Files下)。标准用户无法写入该处,旧版32bit应用还会因为被悄悄重定向到VirtualStore而产生难以理解的问题。
配置该用JSON还是SQLite保存?
结构化的小型配置用JSON文件,会持续增加的业务数据・历史记录・需要检索的数据则以SQLite为首选,这两者就能覆盖业务应用本地存储的大部分需求。JSON的适用范围大约是「启动时全部读取、结束时全部写入」这个前提能成立的数百KB以内。如果开始把持续追加的历史记录或需要检索的数据放进JSON,那就是该迁移到SQLite的信号。即使是配置,只要预期未来会增加,一开始就在SQLite里建一个settings表也没有问题。
可以把SQLite数据库放在网络共享文件夹里吗?
应该避免。通过SMB的文件锁定有很多与环境相关的问题,SQLite官方也把网络文件系统上的共享列为损坏原因的首位。JSON没有互斥机制,Access也会伴随损坏风险而遭遇瓶颈,因此任何一种本地存储技术都不适合多机共享。一旦出现多站点、多用户需要操作同一份数据的需求,就是该考虑搭建SQL Server Express(免费,数据库上限10GB)之类的服务器型数据库,或搭建Web API的判断分界线。
可以把应用的数据保存进注册表吗?
注册表适合存放的仅限于启动项注册、文件关联等与Windows自身联动的信息,以及极小型的用户配置。超过数KB的数据或数组型数据,无论在备份、迁移还是诊断方面都不利,应该让位给JSON或SQLite。此外,在64bit Windows中,从32bit进程看到的 HKLM\Software 会被重定向到 Wow6432Node,这会造成「用注册表编辑器看明明有值,应用却读不到」的问题。让写入端与读取端的位数保持一致,才是正道。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表