资讯动态

OpenHarmony上基于Flutter的文本去重工具实战:从HashSet到Isolate优化

发布时间:2026/9/11 7:33:36 来源:尧图企业网站定制
1. 需求分析文本去重这个工具到底在解决什么问题1.1 你会在什么时候需要“去除重复行”先别急着上车写代码聊聊我为什么折腾这个小工具。干开发这几年“去除重复行”这个需求几乎每个方向都遇到过。日志分析时老日志和新日志合并里头全是同一个报错刷了几万行运营给的一批用户名单Excel 里粘贴来粘贴去重复项一大把甚至我自己写博客时整理关键词列表复制进来的词条也经常撞车。处理这些场景以前我的第一反应是直接扔给在线工具但数据量一大网页端就卡死而且隐私也是一个考量。后来我干脆自己动手基于 Flutter 在 OpenHarmony 设备上做了一款离线可用的文本去重工具专门解决“去除重复行”这个高频刚需。这个工具的核心能力简单直接把一段文本粘贴进来或者直接读取本地文件点击去重重复的行就被清理干净保留下唯一内容。别小看这一步它解决的是数据处理环节里最磨人的准备工作。适合谁来看这篇文章如果你正在做 Flutter 跨平台开发想了解 OpenHarmony 上的适配流程或者你手上正好有文本清洗需求想用最短时间实现一个能用的桌面或移动小工具再或者你纯粹想看看 Dart 处理字符串、文件、性能优化有哪些门道——这篇实战记录都能给你一些参考。1.2 为什么选 Flutter 来做这个工具在做这个工具之前我纠结过一阵子直接用 OpenHarmony 原生开发一个 ArkTS 应用不行吗当然可以但有两个问题让我打消了这个念头。第一我的目标设备不止一台 OpenHarmony 手机我还有 Windows 笔记本、安卓平板甚至想放到纯血的 Linux 环境里跑。原生方案等于每个平台都写一遍维护成本翻倍。第二Flutter 的渲染引擎在文本类工具上有着天然优势列表滚动、富文本输入、UI 一致性都是它的舒适区。一套 Dart 代码写完编译到 OpenHarmony、Android、Windows、Web 都能跑这对于工具型应用来说太划算了。我当时最担心的是 Flutter 在 OpenHarmony 上的适配成熟度。实际调研后发现OpenHarmony 官方和社区已经提供了专门的 Flutter 适配分支基础组件、文件读写、系统能力封装都做得很完善。这套方案其实比很多人想象中要稳我后面会把搭建环境的具体步骤和版本选择原原本本写出来帮你省掉那些查资料的弯路。2. 环境准备在 OpenHarmony 上搭建 Flutter 开发环境2.1 Flutter SDK 安装与 OpenHarmony 适配分支选择在 OpenHarmony 设备上运行 Flutter 应用和你平时用稳定版 Flutter SDK 开发 Android 是两回事。OpenHarmony 使用的是 Flutter 官方仓库的 OpenHarmony 适配分支目前社区维护较活跃的是 OpenHarmony 组织下的 flutter_flutter 仓库。这里第一个坑就是别直接下载 flutter.dev 的稳定版 SDK那个版本不包含 OpenHarmony 平台支持。我当时安装配置的流程大概是这样的从 OpenHarmony 的 Gitee 镜像仓库克隆适配版 Flutter SDK注意切换到你项目需要的稳定分支例如 OpenHarmony-3.2 或 OpenHarmony-4.0 对应版本。将 SDK 的 bin 目录加入环境变量同时在终端里执行flutter doctor它会自动检测 Dart SDK、OpenHarmony SDK 等依赖项。安装 DevEco Studio用来创建 OpenHarmony 工程和配置签名。这个 IDE 本身自带 SDK Manager可以下载对应版本的 OpenHarmony SDK以及完成后续真机调试所需要的自动签名配置。整个过程和配置安卓环境有些类似但有一个容易忽略的点OpenHarmony 平台的 ArkTS 工程目录是你 Flutter 项目的“宿主外壳”Flutter 代码编译后会以 so 库和资源包的形式嵌入到 ArkTS 应用里。这也就意味着签名、权限声明、应用图标这些原生层的东西仍然要遵循 OpenHarmony 应用开发的规范不能只盯着 Dart 代码看。2.2 创建工程并确认 OpenHarmony 目录结构环境准备好后执行flutter create text_dedup新建项目。创建完成后你会看到一个ohos目录这就是 OpenHarmony 宿主的工程目录。初次见这个目录结构的人容易懵它和 Android 的android目录、iOS 的ios目录是同一个层级的角色专门负责生成 OpenHarmony 安装包。关于 flutter 安装与配置这块我再分享一个我的习惯项目根目录的pubspec.yaml里我通常会提前加上用得到的依赖比如file_picker选文件、path_provider拿缓存目录、permission_handler处理存储权限。OpenHarmony 对这些插件的支持程度不一我用的这几个在对应版本的适配上都比较顺利。如果某些插件在 OpenHarmony 上跑不起来另一个思路是自己写 MethodChannel 调用 ArkTS 原生能力这个后面遇到具体问题再展开讲。环境搞定了工程建起来了下面进入到最核心的部分去重逻辑本身怎么设计、怎么写才能既保证正确性又能在百万行文本面前不卡顿。3. 核心算法用 Dart 实现高可靠的去重逻辑3.1 去重逻辑的三种常见方案写文本去重工具算法上其实没太多花活核心就一件事判断两个“行”是否相同然后把重复的剔除。但实现方式不同性能和可靠性差距非常大。我整理了一下主流方案有三种。第一种是朴素的两重循环。拿到所有行之后遍历每一行再和之前已经保留的行逐一比较。这个方法思路最简单但时间复杂度是 O(n²)。100 行文本没问题1 万行就开始发飘100 万行基本就别想立刻出结果了直接卡到让你怀疑人生。虽然逻辑简单在练手项目里可以用但坚决不能放进生产环境。第二种是哈希集合HashSet去重。遍历每一行时判断当前行是否已经在集合中存在如果不存在就加入集合并保留该行如果存在直接跳过。平均时间复杂度是 O(n)内存占用也只是存储唯一行的量级。这是绝大多数文本去重工具的最终选择也是我这个工具采用的方案。第三种是排序后去重。把所有行排序然后顺序扫描相邻相同的就是重复项。这种方法的时间复杂度是 O(n log n)主要受排序算法限制。它有什么好处内存使用可以做到非常低适合处理超大规模文件甚至可以在磁盘上做外部排序。但缺点是排序过程会打乱原始行的顺序如果你希望保留第一次出现时的位置信息排序方案就多一步“记录首次下标再恢复顺序”的操作麻烦不少。我把三种方案的特点总结在表格里方便你对比选择方案时间复杂度内存占用保留原始顺序适用场景两重循环O(n²)极低是百行级小文本、教学演示HashSetO(n)中等是普通文本、日志、代码片段排序去重O(n log n)极低需额外处理超大文件、内存受限环境大多数场景下HashSet 是综合最优解。它既能保证线性的处理速度又天然保留“首次出现”的顺序代码实现还非常简洁。3.2 Dart 实现保留首次出现的顺序去重明确了方案代码写起来就很顺了。我封装了一个核心函数输入是原始文本字符串输出是去重后的文本。String removeDuplicateLines(String input) { if (input.isEmpty) return input; final seen String{}; final result StringBuffer(); // 统一按行拆分这里用 \n 处理Windows 的 \r\n 会由后续逻辑处理掉 final lines input.split(\n); for (final rawLine in lines) { // 去掉行尾的 \r兼容 Windows 换行 final line rawLine.endsWith(\r) ? rawLine.substring(0, rawLine.length - 1) : rawLine; // 去重判断 if (seen.add(line)) { result.writeln(line); } } return result.toString(); }这段代码看起来简单里面其实有门道。split(\n)是最常见的按行拆分方式但如果你直接处理 Windows 下编辑的文本每行末尾会带一个\r导致a和a\r被当成两行去重失效。所以在拆完行之后我对每行做了一个\r的剥离处理。这是一个特别容易踩的细节坑后面我会单独再讲。另一个细节是StringBuffer的使用。在 Dart 里拼接大量字符串时如果直接写result line每拼一次就会生成一个新的字符串对象内存反复分配性能极差。StringBuffer内部是可变缓冲区性能好得多特别是在处理大文本时这个差距会被放大到肉眼可见的程度。3.3 扩展统计重复次数与自定义去重规则基础去重做完实际使用中很快会遇到新需求不仅要删重还想知道哪些行重复次数最高或者两个文件合并去重还需要标记哪一行来自哪个文件。这个需求一出来单纯用Set就不够用了得换成Map。MapString, int countLineOccurrences(String input) { final counter String, int{}; final lines input.split(\n); for (var rawLine in lines) { final line rawLine.endsWith(\r) ? rawLine.substring(0, rawLine.length - 1) : rawLine; counter.update(line, (value) value 1, ifAbsent: () 1); } return counter; }拿到这个统计 Map 之后你可以按需输出只显示出现次数超过 1 的重复行或者按出现次数排序展示“重复之王”甚至可以做一个“只保留重复项”的逆向功能用来定位数据中真正需要关注的问题。这个扩展非常实用我这里强烈建议你在做工具时顺手加上后面的扩展空间完全不一样。再往深走去重规则本身也有讲究。比如是否忽略首尾空白、是否区分大小写、空行要不要保留这些在不同场景下的预期都不太一样。我的实现里默认是“去除每行首尾空白后再比较”但输出时仍保留原始内容。这样既保证了“看起来一样”的行能正确去重又不会破坏原有数据的完整性。如果你需要严格按字符比较可以把规则做进设置项里让用户自由切换。4. 界面与实践做一个能直接用的去重小工具4.1 UI 设计输入区、按钮、输出区算法搞定了下一步是把它包装成一个普通人能直接用的界面。这个工具的界面架构不复杂三个核心区域文本输入区、执行按钮、结果输出区。我用一个简单的 Column 布局把三块内容纵向排列外面包一层滚动保证在手机小屏上也能顺畅操作。import package:flutter/material.dart; class DedupPage extends StatefulWidget { override _DedupPageState createState() _DedupPageState(); } class _DedupPageState extends StateDedupPage { final TextEditingController _inputController TextEditingController(); final TextEditingController _outputController TextEditingController(); void _onDedup() { final input _inputController.text; final output removeDuplicateLines(input); _outputController.text output; } override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(文本去重工具)), body: Column( children: [ Expanded( child: TextField( controller: _inputController, maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: InputDecoration( hintText: 粘贴需要去重的文本..., border: OutlineInputBorder(), ), ), ), Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton.icon( onPressed: _onDedup, icon: Icon(Icons.cleaning_services), label: Text(一键去重), ), ), Expanded( child: TextField( controller: _outputController, maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, readOnly: true, decoration: InputDecoration( hintText: 去重结果..., border: OutlineInputBorder(), ), ), ), ], ), ); } }这里有几个经验点想分享。第一输入框的expands: true配合maxLines: null可以让 TextField 自动扩展到填满剩余空间不需要手动算高度。第二输出框设置readOnly: true防止用户误编辑结果同时我加了一个“复制结果”的 AppBar action一键复制去重后的内容这个交互大大提升了实际使用体验你别嫌功能小。4.2 从文件读取与结果导出如果你处理的文本量很大光靠复制粘贴效率还是低。所以我加入了文件读取和导出功能。在 OpenHarmony 上我用了file_picker插件来唤起文件选择器用path_provider获取应用缓存目录处理临时文件。文件读取的代码思路如下FutureString pickAndReadFile() async { final result await FilePicker.platform.pickFiles(); if (result null || result.files.single.path null) { return ; } final file File(result.files.single.path!); return await file.readAsString(); }这个流程在 Android 上非常流畅但到了 OpenHarmony 上有一个要注意的地方FilePicker在某些版本的适配里可能无法直接返回可读取的文件路径而是返回一个临时拷贝文件的路径。遇到这种情况不要慌先检查返回的路径是否存在如果文件读取失败可以考虑改用系统分享接口或者手动在应用内做文件浏览器。我实际调试时就是在路径上踩了坑后来发现拿到的 path 需要拼上应用的缓存目录前缀才能读折腾了好一阵子才定位到原因。导出功能更简单直接把去重结果写到一个新文件里然后通过系统分享或者保存到公共目录。由于 OpenHarmony 对公共目录的写入权限管理越来越严格我采用了“写入应用私有目录 分享出去”的组合方案既稳定又安全不会一上来就卡在权限申请上。4.3 内存优化处理大文件时的隔离与分块如果你只想处理几百 KB 的文本前面写的代码完全够用。但人总是贪心的搞定小文件之后就想着拿它去跑几十万行的日志这时候问题就来了。当你在主线程里用split(\n)把 50 万行文本拆成数组再逐个写入StringBufferUI 线程会被完全阻塞页面直接白屏几秒钟体验极差。解决办法是引入 Dart 的Isolate。Isolate 是 Dart 里的独立执行线程和主线程互不共享内存通过消息传递通信。把耗时的去重计算放进 Isolate主线程负责 UI 展示和交互处理完再通过SendPort把结果传回来。在 Flutter 里最方便的使用方式就是compute函数FutureString dedupInBackground(String input) { return compute(removeDuplicateLines, input); }compute会自动开一个 Isolate执行完之后把结果返回。这样即使处理 100 万行文本界面也只是显示一个 Loading 状态等着后台算完不会卡死。再深入一层如果文本量大到一次性读入内存都吃力比如超过 200MB 的日志文件就需要分块处理了。按照固定行数分块每读一个块就去重一次并保留一个全局的Set然后持续往输出文件里写。这样内存占用就被控制在一个固定上界不会随着文件增大而无限膨胀。这个方案我实现过一版在内存有限的设备上跑起来的稳定性明显更好也是 flutter 内存优化里非常典型的做法。4.4 与本地存储结合去重记录留存工具用顺手之后我又加了一个功能把每次去重的结果自动追加到本地数据库方便后续追溯。这里用的是sqflite插件和 OpenHarmony 的适配还算顺利。表结构很简单就三个字段id、内容、时间戳。每次去重结束后我把结果列表和原始行数、去重后行数一并记录下来。过了一阵子回头看这个“顺手”加的功能反而成了高频使用点——我可以随时统计过去一周处理了多少数据、哪些重复类型出现最多。如果你也有类似的诉求我建议从第一天就把存储设计进去不要等到工具成型了再补后期改动成本会高出不少。本地存储同步加上后端同步又是一个大工程但作为单机工具先落库已经能解决绝大多数问题。5. 性能实测与真机调试中的问题排查5.1 不同量级文本的性能表现算法方案和 Isolate 都上了之后我在 OpenHarmony 真机上做了几轮性能测试。为了让你心里有个底我贴一组我在设备上实测的大致数据。测试环境是一台搭载 OpenHarmony 的中端开发板文本内容为随机生成的模拟日志包含部分重复行。数据量行均长度去重耗时备注1,000 行80 字符约 20ms几乎无感知100,000 行80 字符约 300ms主线程会轻微卡顿1,000,000 行80 字符约 2.1s必须使用 Isolate从数据里能清楚看到HashSet 方案在百万行规模下依然能控制在秒级这个表现对于文本去重工具来说已经非常够用。值得注意的是去重耗时和“唯一行数量”强相关因为Set的add操作在冲突较少时接近 O(1)但如果你的数据恰好都是超长字符串且重复率极低内存分配的开销会明显上升。这种极端场景下内存优化要从字符串本身入手比如先计算每行的哈希值用 64 位整数作为Set的元素把内存占用降到一个新量级。5.2 我在真机上踩过的几个坑这个项目虽然不大但在 OpenHarmony 真机调试时前前后后踩了不少坑。我把印象最深的那几个整理成一张问题排查表你如果遇到类似现象可以直接对着查。问题现象原因分析解决方案FilePicker 返回路径找不到文件插件在 OpenHarmony 上返回的是临时目录相对路径不是完整绝对路径在 flutter 层拼接应用缓存目录前缀或改用系统分享接口传递文件中文文件名读取乱码文件编码不是 UTF-8可能是 GBK 或 ANSI 编码先用字节流读取再尝试用不同编码解码如果需要引入gbk编码包文本粘贴 10 万行后输入卡顿TextField 内部对超大文本的刷新机制开销很大限制输入框的长度改为文件导入为主粘贴为辅去重后行数不正确总是偏多文本包含\r\n换行split(\n)后每行末尾残留\r统一在 split 后去除行尾\r或者直接用正则按\r?\n分割UI 白屏 3 秒后才有结果去重逻辑直接跑在主线程阻塞了 UI改成compute或Isolate后台执行其中最后一个坑是很多 Flutter 新手最容易忽略的。Dart 是单线程模型虽然异步编程能处理 IO 等待但 CPU 密集型任务如果放在主 isolate照样会卡死 UI。你写了个removeDuplicateLines函数觉得它跑得快但 100 万行数据进去就不是那么回事了。所以我在项目第一天就强制自己把计算和 UI 分离这个习惯在后续做其他工具时也帮我省了不少事。5.3 去重规则的边界情况处理文本处理领域一个看似简单的问题边界情况却能整得人头大。去重逻辑看着简单实际使用中总会冒出一些“看起来一样程序觉得不一样”或者反过来“看起来不一样程序觉得一样”的情况。第一个边界是首尾空白。用户在复制内容时很容易在行首或者行尾不经意带上空格肉眼根本看不出来但程序会认为这是两个不同的行。我的处理方式是去重判断前对每一行做trim()用清理后的字符串做Set的 key但输出时仍然输出原始行。这样用户看到的结果是干净的信息也没有丢失。第二个边界是空行。文本中间的大段空行要不要保留不同场景预期不同。我的工具默认保留空行而且只保留一个空行连续多个空行会被压缩成一个。如果你做的是代码清单整理可能连空行都想去掉这就可以做成开关选项。第三个边界是大小写。如果你处理的是英文关键词列表Apple和apple大概率应该算重复但对于代码文件来说大小写敏感是必须严格保证的。我在工具设置里加了一个“忽略大小写”的开关默认关闭用户自行决定。这个看似不起眼的设置反而是实际使用中被问得最多的功能点之一。6. 踩坑之后的版本迭代与后续扩展方向工具跑通之后我又花了一个晚上的时间优化了几个体验上的小细节。一个是在去重完成后弹出 SnackBar显示原始行数和去重后的行数以及删除了多少重复项让用户对处理结果一目了然。另一个是增加了一个“统计重复次数”的入口可以弹窗展示 Top 10 高频重复行这个功能在处理日志分析时格外好用。再往后的方向我目前计划是把这个工具模块化封装成独立的 Flutter 包供其他项目复用同时考虑接入本地数据库保存历史处理记录甚至做一个局域网内的多端同步功能。但从实际需求出发去掉重复行这个动作本身并不复杂更重要的是它背后代表的一整套“数据清洗”思路——先清洗再分析最后可视化。这样的工具链建设才是把一个小工具做成长久生产工具的路径。根据我个人经验做这类工具型的应用最忌讳一上来就堆砌复杂框架。先拿一个最直接的需求用最朴素的方式跑通再根据真实使用反馈逐步迭代效率和稳定性都会高很多。希望这篇文章能帮你省下那些我自己踩过的坑直接上手就把这个工具做出来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价