用Delphi打造跨平台日程管理APP:SQLite存储与通知机制实战 简介一份基于 Delphi FMX 框架开发的 Android 日程管理 APP 完整工程围绕 SQLite 数据持久化、自定义 ListView 和手势交互展开适合有 Pascal 基础、希望学习跨平台移动开发的读者。项目以轻量级 SQLite 为存储核心包含建表 SQL 与数据库文件并演示了半透明对话框、系统分享、虚拟键盘辅助组件等移动端常见交互另附自定义 ListView 外观演示包便于直接复用。资源共二十七个文件以 pas 源码、fmx 界面、vlb 布局、sql/db 数据库及 dproj/deployproj 工程配置为主压缩包约四十六 KB结构清晰便于按模块对照学习。已有四百七十四人学习浏览可作为课程设计或企业移动原型参考。借助该工程可重点研究 FMX 中 ListView 的样式定制与滑动/长按手势处理梳理 Delphi 调用 Android 系统分享的 Intent 流程同时理解部署配置与资源文件在真实项目中的用途。 很多老程序员对 Delphi 的感情其实挺复杂的早年靠它吃饭后来又被各种新语言冲击但真到要做一些工具类、内部系统、或者快速上手的跨平台小应用时又总会想起它。这次的项目就是一个典型的例子——用 Delphi 做一个简单的日程管理 APP。不复杂但是五脏俱全从数据存储、界面布局到提醒通知样样都得自己动手。这篇文章就围绕这个项目把我在开发过程中踩过的坑、用到的技巧、以及一些关键代码的思考逻辑完整写一遍给那些还在用 Delphi 或者想重新捡起 Delphi 的朋友一个参考。1. 项目整体设计与思路拆解1.1 为什么用 Delphi 做日程管理 APP先回答一个最直接的问题现在做 APP 有 Flutter、React Native、Kotlin 一堆方案为什么要回头用 Delphi我的理由很简单一是团队里老同事对 Delphi 的熟练度远高于其他语言二是这个项目本身是典型的“内部工具型”应用核心诉求是开发速度快、维护成本低、打包体积小。Delphi 在这类场景下反而很合适尤其是 RAD Studio 的跨平台能力一套代码同时出 Windows 和 Android 版本节省的时间不是一点半点。日程管理这个领域本身也不复杂。无非是记录事件、设置提醒、查看某一天或某个月有哪些安排。业务逻辑清晰数据量不大不需要服务端实时同步甚至可以做成单机应用。用 Delphi 来做刚好避开那些框架本身的复杂度把精力集中在业务实现上。另外还有一个很实际的因素Delphi 对系统底层的掌控能力强。比如在 Windows 端做开机自启、托盘提醒、读取系统日历、和 Outlook 交互等等Delphi 的生态里都有现成的组件。而在 Android 端通过 FireMonkey 框架也能实现本地通知和后台定时任务虽然配置起来比原生麻烦一些但胜在代码复用率高。1.2 功能边界与数据模型设计这个项目的定位是“简单”所以功能边界我一开始就划清楚了不做复杂的东西日程的增删改查支持按日期浏览。设置提醒时间到点触发系统通知。支持农历和节假日的简单标注这个是中国用户比较刚需的功能。数据本地存储不依赖网络。功能定义好之后数据结构其实就很好设计了。日程表的核心字段包括ID、标题、内容、日期、开始时间、结束时间、是否全天事件、提醒提前量、备注。我用了一张数据表来存所有信息ID 用自增整数日期和时间分开存储方便查询。T_Schedule - ID: Integer (主键) - Title: string - Content: string - EventDate: TDate - StartTime: TTime - EndTime: TTime - IsAllDay: Boolean - RemindOffset: Integer // 提前多少分钟提醒-1表示不提醒 - Remark: string这种设计看起来简单但在实际使用中会有一个问题当数据量超过几千条按日期范围查询的性能就会下降。所以我在 EventDate 上加了索引并且在查询时统一用参数化 SQL避免每次都全表扫描。对于这种轻量级应用SQLite 完全够用没必要上 MySQL 或者 PostgreSQL。1.3 技术选型VCL 还是 FireMonkey这是用 Delphi 做跨平台 APP 时绕不开的选择题。VCL 是 Windows 专属的组件丰富第三方控件多开发桌面端体验非常好。FireMonkeyFMX是跨平台框架支持 Windows、macOS、Android、iOS但有些组件行为和原生还是有差距。我这个项目选择的是 FMX因为目标是 Android 端为主的移动应用。但说实话FMX 在 Android 上跑起来性能比不过真正的原生开发界面渲染也偶尔会出现一些奇怪的问题。如果只是做一个给几十个人用的内部工具FMX 完全撑得住如果你想上应用市场对用户体验要求很高那还是要做原生优化或者干脆换个框架。开发时有个细节需要特别注意FMX 在 Android 上的 ListView 和原生 ListView 不一样FMX 的 TListView 是自绘的它的 ItemAppearance 设置为 ListItem 时才能显示标题和副标题。如果你直接拖一个 TListView 进去什么都不设置运行时只会看到一片空白。这个坑新手很容易踩。2. 核心细节解析与实操要点2.1 日期处理判断周六日、节假日与农历日程管理最核心的就是时间计算。用户需要区分工作日和休息日还要在节假日尤其是春节、国庆这种长假前提前安排日程。所以判断周六日只是基础农历节日的支持才是中国用户真正需要的东西。Delphi 没有内置农历转换函数需要自己写算法或者引入三方库。我用了网上的一个农历算法单元原理是维护一个农历数据表然后用天干地支和月日信息来计算农历日期。这里给出一段判断周六日的代码非常简单function IsWeekend(const ADate: TDate): Boolean; var Dow: Integer; begin Dow : DayOfWeek(ADate); Result : (Dow 1) or (Dow 7); // 1Sunday, 7Saturday end;但这里有个坑要注意DayOfWeek 返回的数值在不同区域设置下可能不一样。在 Delphi 中DayOfWeek 返回 1 代表周日7 代表周六。如果项目的区域设置改成从周一开始计算结果就会错乱。稳妥的做法是用 ISO 8601 的 Monday1, Sunday7 标准需要自己封装一层function IsISOWeekend(const ADate: TDate): Boolean; begin Result : (DayOfWeek(ADate) 1) or (DayOfWeek(ADate) 7); end;代码虽然一样但注释里要写明依赖项。农历部分的算法比较大暂不贴全核心思路是“查表法”——用一个长数组保存从 1900 年到 2100 年每一年的农历月大月小、闰月信息然后通过天数累加计算出当前日期对应的农历日期。2.2 数据持久化方案SQLite 还是 TClientDataSet日程数据量虽然不大但我还是推荐用 SQLite而不是把数据存在 JSON 或 XML 文件。原因有两点一是 SQL 查询方便按日期范围、关键字搜索都很方便二是 SQLite 事务性好写入过程中如果程序崩溃数据不容易损坏。Delphi 操作 SQLite 的方式很多可以直接用 FireDAC也可以引入第三方组件如 DISQLite3。FireDAC 是 RAD Studio 自带的数据库访问框架支持 SQLite、MySQL、SQL Server 等语法统一而且支持参数化查询推荐直接使用。以下是用 FireDAC 连接 SQLite 的关键代码FDConnection1.DriverName : SQLite; FDConnection1.Params.Database : schedule.db; FDConnection1.Connected : True; FDQuery1.SQL.Text : SELECT * FROM T_Schedule WHERE EventDate :EventDate ORDER BY StartTime; FDQuery1.ParamByName(EventDate).AsDate : SelectedDate; FDQuery1.Open;值得一提的是 TClientDataSet。热词里有人提到clonecursor函数这个函数的作用是创建当前游标的副本让两个 TDataSet 共享同一份数据。在某些场景下确实有用比如你需要同时以两种不同顺序浏览同一份数据的时候。但在日程管理里我们直接通过 SQL 排序就够了不需要 clone cursor 这种操作知道了但用不上也算是一种知识储备。2.3 提醒机制本地通知与 Timer 轮询这是这个项目的核心亮点。提醒机制在 Android 上实现有两种方式本地通知Notification通过系统的 NotificationManager 发送即使 APP 在后台也能弹出。应用内轮询APP 在前台运行时用 TTimer 定时检查当前时间是否有日程需要提醒。我最终采用了“双轨制”前台用 Timer 轮询后台用系统通知。FireMonkey 在 Android 上没法直接调用 NotificationManager需要借助 Java 桥接JNI或者使用第三方库。我用了Androidapi.JNI.App类来实现本地通知核心代码如下procedure SendNotification(const ATitle, AContent: string); var NotificationManager: JNotificationManager; Builder: JNotificationBuilder; Intent: JIntent; begin NotificationManager : TJNotificationManager.Wrap( TAndroidHelper.Context.getSystemService(TJContext.JavaClass.NOTIFICATION_SERVICE) ); Intent : TJIntent.Create; Intent.setClass(TAndroidHelper.Context, TAndroidHelper.Context.getClass); Builder : TJNotificationBuilder.JavaClass.init( TAndroidHelper.Context, schedule_channel ); Builder.setContentTitle(StringToJString(ATitle)); Builder.setContentText(StringToJString(AContent)); Builder.setSmallIcon(TAndroidHelper.Context.getApplicationInfo.icon); NotificationManager.notify(1, Builder.build); end;这段代码的核心逻辑是获取系统的通知服务然后构建一个通知对象并发送。第二参数是通知 ID如果重复使用同一个 ID会覆盖之前的通知如果不同 ID则会显示多条通知。在日程提醒中我用日程 ID 作为通知 ID这样多条提醒可以并行显示。2.4 界面布局ListView、日期选择器与侧滑菜单日程管理 APP 的界面不需要太花哨核心就是三个页面日程列表、日程详情/编辑、日历视图。我用 FMX 的 TListView 展示日程列表通过设置ItemAppearance : TListViewAppearance.ListItem来显示标题和副标题时间、地点同时在每个 Item 右侧添加一个删除按钮方便快速操作。日期选择器我用的是 FMX 自带的 TCalendarEdit。在桌面上运行没什么问题但在 Android 上点击弹出来的日历选择器样式比较原生有些安卓机型会渲染异常解决方案是换成自定义的弹窗用 TCalendar 组件嵌到 TPopup 里这样能在所有机型上保持一致的体验。侧滑菜单实现起来比较费劲FMX 没有现成的侧滑控件。我当时参考了 Demo 里的一个实现用TLayout包裹左右两个面板然后通过手势事件OnPan来修改Position.X实现侧滑效果。具体代码如下procedure TMainForm.HandlePan(Sender: TObject; const EventInfo: TGestureEventInfo; var Handled: Boolean); begin if EventInfo.GestureId giPan then begin MenuLayout.Position.X : MenuLayout.Position.X EventInfo.Distance; if MenuLayout.Position.X 0 then MenuLayout.Position.X : 0; end; end;思路很简单就是通过手势的 X 轴位移量来平移菜单布局。这里要注意的是手势结束后要判断菜单是应该完全展开还是收起如果手指松开时菜单位置还在某个阈值比如屏幕宽度的 30%之外就用动画平滑滑到目标位置否则回弹。3. 实操过程与核心环节实现3.1 从零开始创建项目环境准备与工程结构在动手写代码之前先把环境准备好。我用的版本是 RAD Studio 11 Alexandria针对 Android 平台开发时需要先安装 Android SDK 和 NDK 并配置好 Java JDK。步骤很简单依次操作下载安装 RAD Studio 11安装时勾选 Android 和 Windows 平台支持。打开 Tools Options Deployment配置 Android SDK 路径。默认情况下 Delphi 会自己下载一部分 SDK 组件。在 Project Manager 中右键 Target Platforms选择 Android 64-bit。创建新项目File New Multi-Device Application。工程结构上我建了一个干净的目录ScheduleApp/ ├── Main.pas ├── ScheduleData.pas ├── Notifications.pas ├── Lunar.pas └── ScheduleApp.dprojMain.pas负责界面和交互ScheduleData.pas负责数据访问和业务逻辑Notifications.pas封装本地通知的接口Lunar.pas是农历算法单元。分这么细的好处是逻辑清晰后续如果要在 Windows 端复用同样的数据逻辑可以直接引用 ScheduleData.pas不用动界面代码。3.2 数据层开发建表、查询与缓存我用 FireDAC 连接 SQLite 时遇到过一个小坑FireDAC 的 SQLite 驱动默认不开启外键支持如果多张表之间有关联关系需要对连接参数做额外设置。这个项目单表结构不存在外键问题所以没有踩雷。建表语句很简单CREATE TABLE IF NOT EXISTS T_Schedule ( ID INTEGER PRIMARY KEY AUTOINCREMENT, Title TEXT NOT NULL, Content TEXT, EventDate DATE NOT NULL, StartTime TIME, EndTime TIME, IsAllDay INTEGER DEFAULT 0, RemindOffset INTEGER DEFAULT -1, Remark TEXT ); CREATE INDEX IF NOT EXISTS idx_eventdate ON T_Schedule(EventDate);在代码里我把对数据的操作封装成了一个TScheduleDataModule类提供以下方法LoadByDate(const ADate: TDate): TDataSet;LoadBetween(const AStartDate, AEndDate: TDate): TDataSet;AddSchedule(ASchedule: TSchedule);UpdateSchedule(ASchedule: TSchedule);DeleteSchedule(const AID: Integer);其中TSchedule是对数据行的对象封装字段和表结构一一对应。这么做的好处是界面层可以直接绑定对象列表不需要和 DataSet 强耦合后面如果要从 SQLite 切换到其他数据库只改数据层就够了。查询时有一个性能优化点如果你在日程列表页要展示整整一个月的日程不建议一次性把整个月的数据全查出来而是分页加载。理想情况是每页只加载 20 条用户滚动到底部时再加载新的 20 条。在 SQLite 里用LIMIT 20 OFFSET 20就可以实现分页。3.3 日程提醒核心逻辑Timer 启动、后台限制与省电策略日程提醒的核心是 Timer 轮询机制。前端界面启动时启动一个全局的 TTimer间隔设置为 30 秒不要低于 10 秒否则 Android 会认为你的应用占用过多资源降低你的进程优先级。每个计时周期做这几件事查当前时间到当前时间 1 小时之间是否有日程开始。如果有并且没有发过提醒则触发SendNotification然后将该日程标记为“已提醒”。已提醒标记存在本地表里用IsNotified字段Integer0/1控制。伪代码如下procedure TScheduleDataModule.CheckReminders; var NowTime: TDateTime; CurrentSchedule: TSchedule; begin NowTime : Now; FDQuery1.SQL.Text : SELECT * FROM T_Schedule WHERE EventDate :Date AND IsNotified 0; FDQuery1.ParamByName(Date).AsDate : DateOf(NowTime); FDQuery1.Open; while not FDQuery1.Eof do begin CurrentSchedule : GetScheduleFromQuery(FDQuery1); if (CurrentSchedule.StartTime NowTime) and (MinutesBetween(CurrentSchedule.StartTime, NowTime) CurrentSchedule.RemindOffset) then begin SendNotification(CurrentSchedule.Title, 日程即将开始); MarkAsNotified(CurrentSchedule.ID); end; FDQuery1.Next; end; end;这里注意查询条件是EventDate :Date而不是EventDate DateOf(Now)。因为某些日程可能是跨天的比如 23:00 开始、次日凌晨 1 点结束。为了保证不漏提醒查询时应该用EventDate BETWEEN :Today AND :Tomorrow这种方式把跨天的情况也算进去。后台提醒是另一个大坑。Android 系统为了省电会杀掉后台进程如果你的 APP 在后台放久了Timer 未必能继续触发。解决方案是使用前台服务Foreground Service但前台服务需要常驻通知栏这需要在启动时创建一条“APP 正在运行”的常驻通知。对内部工具来说这样做好用但如果是要上架应用商店又得考虑用户是否会反感这种常驻通知。3.4 导入导出功能读取 Excel 实现批量日程录入热词里提到delphi 读取 excel这个在日程管理里也有实际应用场景——用户可能会在 Excel 里整理好一堆日程需要批量导入到 APP。Delphi 读 Excel 有几种方式使用 Excel 的 COM 接口仅限 Windows。使用第三方库如 FlexCel、SMExport。使用 TFileStream 直接解析 XLSX 文件XLSX 本质是 ZIP 压缩的 XML 文件。第一种方式最简单但只适用于 Windows 端。考虑到 Android 端也有导入需求我用的是第三种方案用 TZipFile 解压 XLSX然后读取xl/worksheets/sheet1.xml中的内容用 Xml.XMLDoc 解析单元格数据。具体实现有点繁琐但好处是不依赖外部组件跨平台可用。procedure ImportFromExcel(const AFileName: string); var Zip: TZipFile; Stream: TMemoryStream; XMLDoc: IXMLDocument; RootNode, RowNode, CellNode: IXMLNode; I, J: Integer; begin Zip : TZipFile.Create; try Zip.Open(AFileName, TZipMode.zmRead); Zip.Read(xl/worksheets/sheet1.xml, Stream); XMLDoc : LoadXMLDocumentFromStream(Stream); RootNode : XMLDoc.DocumentElement; for I : 0 to RootNode.ChildNodes.Count - 1 do begin RowNode : RootNode.ChildNodes[I]; if RowNode.NodeName row then begin for J : 0 to RowNode.ChildNodes.Count - 1 do begin CellNode : RowNode.ChildNodes[J]; // 解析单元格值跨单元格的 t 属性处理 end; end; end; finally Zip.Free; end; end;这里要注意XLSX 中的日期存储方式有两种一种是数字序列号一种是字符串。解析时需要根据单元格的s属性和t属性来判断。数字序列号需要转换成 Delphi 的 TDate转换公式是TDate(25569) 序列号天数值。25569 是 Delphi 的 TDate 基准日期1899-12-30与 Excel 的基准日期1900-01-01之间的差这个数是从实践中试出来的不同的 Excel 版本可能略有差异所以本地化测试不可少。4. 常见问题与排查技巧实录4.1 中文乱码文件编码、SQLite 连接串与组件显示Delphi 开发的中文乱码问题十个人有九个人会遇到。我这次主要踩了两个坑。第一个坑是源码文件的编码。Delphi 7 和 RAD Studio 高版本对代码文件的默认编码不同。Delphi 7 默认是 ANSI而 RAD Studio 默认是 UTF-8。如果代码中直接写中文并且保存时用了错误的编码编译器会报错或者运行时出现乱码。解决方案很简单在 IDE 中把源码文件另存为 UTF-8 格式。Tools Options IDE Environment Variables 里可以设置默认文本编码。第二个坑是 SQLite 连接字符串的编码设置。FireDAC 的 SQLite 驱动默认使用 UTF-8 编码存储字符串如果你从旧数据库迁移过来里面存的是 GBK 编码就要在连接串中加上CharacterSetutf8并且在查询后手动转换字符串。我的建议是所有新写的代码统一用 UTF-8不要混用编码。组件的显示问题也可能导致乱码。比如 FMX 的 TLabel 在 Android 上显示中文时默认字体可能不支持某些生僻字这个时候你需要给 Label 设置一个中文字体如 Noto Sans CJK SC。这个不难但要提前想到否则界面在特定机型上就会出现方块字。4.2 TClientDataSet 的 CloneCursor 在数据展示中的作用热词里有人在问TClientDataSet的CloneCursor函数是干什么的。这里结合日程管理的实际场景解释一下。CloneCursor的作用是给当前数据集创建一个共享数据的游标两个游标看到的是同一份内存数据但是各自可以有不同的排序、过滤条件、以及当前记录指针。在日程管理里有一个很好的用法你需要在两个界面上展示同一份日程数据一个按日期排序一个按提醒时间排序。如果直接开两个查询会加载两次数据内存浪费且不同步。用CloneCursor就解决这个问题FDQuery1.SQL.Text : SELECT * FROM T_Schedule; FDQuery1.Open; FDQuery2.CloneCursor(FDQuery1, True); FDQuery2.IndexFieldNames : EventDate; FDQuery1.IndexFieldNames : StartTime;这里的第二个参数Reset为 True 会重置克隆游标的当前记录位置。两个游标修改数据会同步非常适合列表和详情页联动。不过有一点需要提醒CloneCursor只能在同一个TDataSet类型之间使用不能把一个FDQuery克隆到另一个TClientDataSet上类型必须匹配。4.3 Timer 不触发进程被系统杀死的排查思路Android 后台的 Timer 不触发是最多人问的问题。我排查的思路是分三步走先确认进程是否活着。在开发者选项里打开“正在运行的服务”看看你的 APP 是否仍在后台运行。如果进程被杀说明需要提升进程优先级。方案是开启前台服务或者让用户手动设置“允许后台运行”和“不受电池优化限制”。如果进程活着但 Timer 不触发检查是否在 Android 6.0 之后的 Doze 模式下被限制。Doze 模式会在设备静止时停止网络访问和延迟任务TTimer 的机制是运行在主线程消息队列里的在 Doze 模式下可能会被延迟数分钟才执行一次。我的解决方案是在 APP 被切换到后台时启动一个TNotificationCenter计划通知服务把未来要提醒的事件一次性注册到系统通知中心。这样即便 APP 进程被杀通知仍然由系统按时发出。Android 上注册计划通知的 API 是AlarmManager但在 FireMonkey 里直接调用比较复杂我用了一个封装好的第三方库Grijjy.Android.AlarmManager它简化了调用步骤核心代码如下AlarmManager.SetAlarm(AlarmTime, Intent, RequestCode);这个方法接收三个参数闹钟触发时间、需要发送的 Intent、用于区分不同闹钟的 RequestCode。我把日程 ID 作为 RequestCode这样在每个闹钟触发时可以通过 RequestCode 找回日程信息。4.4 WebBrowser 控件控制放大缩小这个项目的日程详情页里我引入了 Delphi 自带的TWebBrowser控件来渲染富文本内容——比如用户复制粘贴了一段带格式的网页内容作为日程备注。在 Windows 上这个控件表现良好但在 Android 上缩放行为有差异。默认情况下Android 的 WebView 控件不会主动显示缩放按钮即使用户双指缩放网页内容也不会跟随缩放。解决方案是在页面加载完后执行一段 JavaScript允许网页内容根据自己的视口宽度进行缩放var meta document.createElement(meta); meta.setAttribute(name, viewport); meta.setAttribute(content, widthdevice-width, initial-scale1.0, maximum-scale3.0, user-scalableyes); document.getElementsByTagName(head)[0].appendChild(meta);在 Delphi 中执行这段 JS 的方式是WebBrowser1.EvaluateJavaScript( var meta document.createElement(meta); meta.setAttribute(name, viewport); meta.setAttribute(content, widthdevice-width, initial-scale1.0, maximum-scale3.0, user-scalableyes); document.getElementsByTagName(head)[0].appendChild(meta); );这里有个引号嵌套的问题JS 里的字符串用单引号而 Delphi 的字符串也是单引号所以内部要用双单引号转义否则编译就会报错。另外要注意的是WebBrowse 的在 Windows 上使用的是 IE 内核在 Android 上是 WebView两者对 JavaScript 的支持程度不一样。如果页面的 JS 很复杂在两种平台上可能出现渲染差异需要单独调试。4.5 编译打包DCU 版本不匹配与发布签名Delphi 开发中常见的编译错误是Unit1.pas和编译后的.dcu文件版本不匹配。例如热词里有人提到devexpress delphi 7 dcu许多第三方组件在不同 Delphi 版本下需要重新编译 DCU。我的建议是项目的所有第三方组件源码和 DCU 文件都统一放在同一个Library路径下每次升级 Delphi 版本时都重新编译这些组件不要让 IDE 自动扫描多个版本的 DCU。另外Android 应用发布到真机调试或上架应用商店都需要签名。Debug 模式下 Delphi 会自动生成一个调试证书直接安装到手机没问题。但发布Release模式必须要用正式签名否则即使打包成功安装到其他设备上也会提示“应用未签名”或“安装失败”。签名操作是在 Project Options Provisioning 里配置选择 Release 配置然后点击“Signing”页签选择证书文件。如果没有证书可以用 Delphi 自带的证书创建工具生成一个新证书。发布时还有一个细节要注意Android 的包名Package Name在第一次发布后就不能改了改包名相当于一个全新的应用用户无法通过旧包无缝升级。所以项目创建时就要把包名定义好格式是com.yourcompany.scheduleapp不要用默认的com.embarcadero.ScheduleApp。4.6 真机调试抓包Fiddler 配置与常见问题移动端开发经常需要调试网络请求但日程管理 APP 本身不依赖网络一般不需要抓包。不过如果你在日程管理里集成了同步功能比如将日程同步到云端那抓包工具就很有必要了。用 Fiddler 抓手机 APP 的包常规配置是电脑和手机连同一个 Wi-Fi。Fiddler 开启 HTTPS 解密Tools Options HTTPS Decrypt HTTPS traffic。手机上设置代理指向电脑的 IP 和端口Fiddler 默认 8888。手机浏览器访问http://你的IP:8888安装 Fiddler 的根证书。听起来简单但实际操作中经常会出现“抓不到包”的情况。最常见的原因有三个Fiddler 没有监听正确的端口或者防火墙拦截了 8888 端口。Android 7.0 及以上版本默认不再信任用户安装的 CA 证书应用如果使用了 HTTPS 并且不走系统信任的证书链Fiddler 就解密不了。手机 App 本身使用了证书固定Certificate Pinning明文证书替换之后 App 直接拒绝连接。第二个问题的解决办法是将 Fiddler 的根证书作为系统证书安装需要 root 权限或者修改 App 的网络安全配置允许在 debug 模式下信任用户证书。第三个问题没法在设备端解决只能从代码层面暂时关闭证书固定或者用Frida这类工具做动态注入但这就涉及更深的逆向知识了一般内部工具开发用不上。写在最后的操作心得做这个项目最大的感触是Delphi 它不新但真做小型应用时反而比一些新框架省心。同样是跨平台Flutter 还得装 Dart SDK 配环境Delphi 这边装好 RAD Studio 就能写代码而且控件拖拽绑事件几十行就能把界面搭好速度是真的快。但也要承认不足FMX 的生态和原生开发比还是差了不少。真遇到一些冷门的手机机型界面渲染和系统交互偶尔会有奇怪的兼容问题排查起来比较费时间。我的实用建议是这种小工具类 APP优先保证功能逻辑正确和核心路径的数据一致性界面效果别太花哨能稳定跑起来比一次性把 UI 做漂亮重要得多。所有数据操作统一走数据层界面层只负责展示和交互这样后续出了 bug 定位起来会很轻松。如果你也准备用 Delphi 做类似的内部工具这套思路可以少走不少弯路。本文还有配套的精品资源点击获取