news 2026/9/11 7:33:27

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenHarmony上基于Flutter的文本去重工具实战:从HashSet到Isolate优化

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 平台支持。

我当时安装配置的流程大概是这样的:

  1. 从 OpenHarmony 的 Gitee 镜像仓库克隆适配版 Flutter SDK,注意切换到你项目需要的稳定分支,例如 OpenHarmony-3.2 或 OpenHarmony-4.0 对应版本。
  2. 将 SDK 的 bin 目录加入环境变量,同时在终端里执行flutter doctor,它会自动检测 Dart SDK、OpenHarmony SDK 等依赖项。
  3. 安装 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,导致aa\r被当成两行,去重失效。所以在拆完行之后,我对每行做了一个\r的剥离处理。这是一个特别容易踩的细节坑,后面我会单独再讲。

另一个细节是StringBuffer的使用。在 Dart 里拼接大量字符串时,如果直接写result += line,每拼一次就会生成一个新的字符串对象,内存反复分配,性能极差。StringBuffer内部是可变缓冲区,性能好得多,特别是在处理大文本时,这个差距会被放大到肉眼可见的程度。

3.3 扩展:统计重复次数与自定义去重规则

基础去重做完,实际使用中很快会遇到新需求:不仅要删重,还想知道哪些行重复次数最高;或者两个文件合并去重,还需要标记哪一行来自哪个文件。这个需求一出来,单纯用Set就不够用了,得换成Map

Map<String, 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 State<DedupPage> { 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获取应用缓存目录,处理临时文件。

文件读取的代码思路如下:

Future<String> 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 万行文本拆成数组,再逐个写入StringBuffer,UI 线程会被完全阻塞,页面直接白屏几秒钟,体验极差。

解决办法是引入 Dart 的Isolate。Isolate 是 Dart 里的独立执行线程,和主线程互不共享内存,通过消息传递通信。把耗时的去重计算放进 Isolate,主线程负责 UI 展示和交互,处理完再通过SendPort把结果传回来。在 Flutter 里最方便的使用方式就是compute函数:

Future<String> 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 方案在百万行规模下依然能控制在秒级,这个表现对于文本去重工具来说已经非常够用。值得注意的是,去重耗时和“唯一行数量”强相关,因为Setadd操作在冲突较少时接近 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改成computeIsolate后台执行

其中最后一个坑,是很多 Flutter 新手最容易忽略的。Dart 是单线程模型,虽然异步编程能处理 IO 等待,但 CPU 密集型任务如果放在主 isolate,照样会卡死 UI。你写了个removeDuplicateLines函数,觉得它跑得快,但 100 万行数据进去就不是那么回事了。所以我在项目第一天就强制自己把计算和 UI 分离,这个习惯在后续做其他工具时也帮我省了不少事。

5.3 去重规则的边界情况处理

文本处理领域,一个看似简单的问题,边界情况却能整得人头大。去重逻辑看着简单,实际使用中总会冒出一些“看起来一样,程序觉得不一样”或者反过来“看起来不一样,程序觉得一样”的情况。

第一个边界是首尾空白。用户在复制内容时,很容易在行首或者行尾不经意带上空格,肉眼根本看不出来,但程序会认为这是两个不同的行。我的处理方式是:去重判断前,对每一行做trim(),用清理后的字符串做Set的 key,但输出时仍然输出原始行。这样用户看到的结果是干净的,信息也没有丢失。

第二个边界是空行。文本中间的大段空行要不要保留?不同场景预期不同。我的工具默认保留空行,而且只保留一个空行,连续多个空行会被压缩成一个。如果你做的是代码清单整理,可能连空行都想去掉,这就可以做成开关选项。

第三个边界是大小写。如果你处理的是英文关键词列表,Appleapple大概率应该算重复,但对于代码文件来说,大小写敏感是必须严格保证的。我在工具设置里加了一个“忽略大小写”的开关,默认关闭,用户自行决定。这个看似不起眼的设置,反而是实际使用中被问得最多的功能点之一。

6. 踩坑之后的版本迭代与后续扩展方向

工具跑通之后,我又花了一个晚上的时间优化了几个体验上的小细节。一个是在去重完成后弹出 SnackBar,显示原始行数和去重后的行数以及删除了多少重复项,让用户对处理结果一目了然。另一个是增加了一个“统计重复次数”的入口,可以弹窗展示 Top 10 高频重复行,这个功能在处理日志分析时格外好用。

再往后的方向,我目前计划是把这个工具模块化,封装成独立的 Flutter 包,供其他项目复用;同时考虑接入本地数据库保存历史处理记录,甚至做一个局域网内的多端同步功能。但从实际需求出发,去掉重复行这个动作本身并不复杂,更重要的是它背后代表的一整套“数据清洗”思路——先清洗,再分析,最后可视化。这样的工具链建设,才是把一个小工具做成长久生产工具的路径。

根据我个人经验,做这类工具型的应用,最忌讳一上来就堆砌复杂框架。先拿一个最直接的需求,用最朴素的方式跑通,再根据真实使用反馈逐步迭代,效率和稳定性都会高很多。希望这篇文章能帮你省下那些我自己踩过的坑,直接上手就把这个工具做出来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 7:32:37

C++代码复杂度控制与优化实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 7:31:00

Java性能优化核心技能与实战经验分享

1. 互联网大厂Java程序员的真实能力画像 在技术社区里&#xff0c;我们经常能看到关于"大厂程序员是否都精通性能优化"的讨论。作为在多家头部互联网企业担任过技术面试官的从业者&#xff0c;我想通过实际案例和数据来还原这个问题的真相。 首先要明确的是&#xf…

作者头像 李华
网站建设 2026/9/11 7:26:39

GHelper:5分钟上手的华硕笔记本轻量控制工具

GHelper&#xff1a;5分钟上手的华硕笔记本轻量控制工具 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivobook, Zenbook, Expertbook,…

作者头像 李华
网站建设 2026/9/11 7:25:30

Linux内核架构解析与核心子系统详解

1. Linux内核全景图&#xff1a;从宏观视角看内核架构作为一名在Linux系统开发领域摸爬滚打十年的老手&#xff0c;我见过太多初学者面对内核源码时那种茫然无措的表情。让我们先抛开复杂的代码细节&#xff0c;站在上帝视角审视Linux内核的整体架构。内核本质上是一个用C语言编…

作者头像 李华
网站建设 2026/9/11 7:22:22

SystemInformer 历史版本获取与版本回退:3条路径装对旧版本

SystemInformer 历史版本获取与版本回退&#xff1a;3条路径装对旧版本 【免费下载链接】systeminformer A free, powerful, multi-purpose tool that helps you monitor system resources, debug software and detect malware. Brought to you by Winsider Seminars & Sol…

作者头像 李华