资讯动态

HarmonyOS实战:文件大小获取与文件类型识别的完整方案

发布时间:2026/10/19 10:00:31 来源:尧图企业网站定制
做文件管理类功能的时候“获取文件大小和文件类型”几乎是你绕不过去的第一步。列表页要显示文件大小、文件管理器要区分文件夹和文件、下载模块要校验文件完整性、相册应用要根据类型渲染不同的缩略图……几乎所有和文件打交道的地方都会先问一句这文件多大是什么类型这篇是HarmonyOS 6.0实战系列的第16篇我梳理了在实现文件信息采集时的完整思路从stat接口的选用到大小格式化、类型识别的三种方案再到一个可以直接抄作业的工具类Demo。不管你是刚开始接触HarmonyOS应用开发还是已经在做文件相关模块这篇都会给你一些值得参考的细节。1. 为什么“获取文件大小和文件类型”是绕不开的一步1.1 这个功能到底解决什么问题很多初学者会觉得获取文件大小不就是file.size一下吗文件类型不就是后缀名吗真做起来会发现没那么简单。文件大小涉及字节数、格式化显示、大文件溢出处理文件类型至少有三层理解维度文件系统层面的“是不是普通文件/目录”、业务层面的“这是什么扩展名”、内容层面的“这个文件真实格式是什么”。不同场景需要不同层面去回答。举个最常见的场景你做的是一个文件管理器列表页需要展示每个文件的大小和类型图标。用户看到的是一张1.5 MB的PNG图片而底层给你的可能是某个文件的字节数。这中间隔着一个“把字节数转成可读文本”的步骤还隔着一个“把PNG映射成图片模板”的步骤。两个步骤处理不好列表页就会显示怪异的“1572864 B”或者把临时文件也当成正式类型展示。还有一个容易被忽略的点HarmonyOS的沙箱机制决定了你不能拿一个file://开头的URI直接调用文件API。必须先获取到应用沙箱内的实际路径或者通过文件描述符fd操作。这个坑我在第6章会详细展开。1.2 HarmonyOS 6.0文件能力的整体印象HarmonyOS 6.0把文件操作能力集中在kit.CoreFileKit里面核心模块是fileIo也就是传统意义上ohos.file.fs的升级版。官方推荐从kit.CoreFileKit统一导入代码看起来会更整洁后续版本兼容性也更好。这个模块提供了同步和异步两套API。同步API用起来像日常写脚本一样直接适合逻辑简单、文件量少的场景异步API适合在UI线程做大文件操作时使用避免卡顿。两个版本的能力完全一致不存在“异步功能更强”的说法选哪种只取决于你的调用上下文。fileIo的核心思想是按照文件描述符来管理资源。你open一个文件拿到fd之后所有读写、查询都围绕这个fd展开用完必须close。如果你只是想获取大小和类型甚至不需要open直接stat就够了。stat顾名思义就是查看文件状态它返回的Stat对象里包含的信息相当丰富。2. 核心API从stat开始理解文件状态2.1 stat和statSync怎么选fs.stat(path)返回一个Promisefs.statSync(path)直接返回Stat对象。二选一的核心标准只有一个你当前是否在UI主线程。在HarmonyOS应用里主线程承担着UI绘制和交互响应。如果在一个按钮点击回调里用statSync去获取一个网络磁盘上数十GB的文件状态轻则卡顿掉帧重则触发ANR。这时候应该用stat搭配await或者通过异步任务调度。但如果是在TaskPool这类后台线程里处理statSync是更好的选择。同步API写起来逻辑顺畅省去then/catch嵌套可读性好而且后台线程本来就不怕阻塞。我个人在项目中还总结了一条隐性规律凡是需要遍历大量文件的场景比如扫描整个文件夹无脑用同步API放到子线程里凡是单个文件、用户主动触发的场景用异步API更安全。道理很简单遍历场景的数据量不可控用同步API挂到后台线程里反而比无数个Promise回调更可控、更省资源。2.2 Stat对象里藏着哪些有用信息Stat对象提供的信息很像个文件身份证字段大致分五类第一类是设备相关的dev和ino。dev表示文件所在设备的编号ino是文件系统中的索引节点号。这两个字段对普通业务没什么用但如果你要做文件的唯一性校验、去重它们比路径可靠得多。路径可以变但同一个文件的dev和ino在生命周期内是稳定的。第二类是链接信息包括nlink硬链接数量、uid、gid。HarmonyOS沙箱内一般用不上但是做系统级文件管理或者调试权限问题时可以辅助判断。第三类是时间戳atime最后访问时间、mtime内容修改时间、ctime状态变更时间。排序文件列表时用mtime就足够了。要注意ctime不是创建时间别再当成birth time用。第四类是尺寸信息size是逻辑大小单位是字节。后面还有个blocks和blksize分别表示实际占用的扇区块数和块大小。这里有个隐藏知识点size是文件内容的逻辑长度blocks * blksize才是它在磁盘上实际占用的物理空间。稀疏文件、小块文件会出现两者不一致的情况。第五类是类型判断方法。isFile()判断普通文件isDirectory()判断目录还有isSymbolicLink()、isBlockDevice()、isCharacterDevice()、isFIFO()、isSocket()。除了前两个后面几个在应用开发中几乎用不到但你要知道它们存在因为沙箱内的守护进程、日志文件偶尔也会展现出特殊文件属性。2.3 使用fs.open的另一种思路有些同学看到stat需要传路径就以为只能操作文件路径。其实fs模块还支持直接传入文件描述符。也就是说你先open拿到fd再用fstat(fd)来获取状态。这在处理已经打开的文件时效率更高省去了一次路径解析。不过对于“获取大小和类型”这个简单需求直接stat就够了。我之所以提open路线是因为在实际项目中有些文件来自跨进程的fd传递这种情况下你根本拿不到路径只能通过fd去查询。记住fs.fstatSync(fd)这个接口它专治这种场景。3. 实操获取文件大小的完整代码与细节3.1 基础尺寸读取获取文件大小最稳定的方式就是通过Stat对象的size属性。看代码import { fileIo as fs } from kit.CoreFileKit; import { BusinessError } from kit.BasicServicesKit; function getFileSizeByPath(path: string): number { try { const stat fs.statSync(path); return stat.size; } catch (err) { const e err as BusinessError; console.error(获取文件状态失败, code${e.code}, message${e.message}); return -1; } }这段代码有几个值得注意的点。statSync在目标文件不存在、路径非法、没有权限时都会抛异常。你不能想着用一个try/catch包住就完事而是要针对不同的错误码给出不同的兜底策略。比如文件不存在时返回-1由上层决定是否提示“文件已被移动”权限不足时返回-2由上层决定是否引导用户授权。如果你在异步场景写法稍有不同async function getFileSizeAsync(path: string): Promisenumber { try { const stat await fs.stat(path); return stat.size; } catch (err) { const e err as BusinessError; console.error(异步获取文件状态失败, code${e.code}, message${e.message}); return -1; } }两种方式返回的size都是纯数字的字节数。别觉得这一步简单就跳过真正决定体验的是后面怎么把这个数字展示给用户。3.2 文件大小格式化拿到字节数之后第一件事是格式化。文件系统里的单位是1024进制1 KB等于1024 B1 MB等于1024 KB1 GB等于1024 MB。这和存储厂商习惯用的1000进制不一样所以你会看到一块“标称512 GB”的硬盘在系统里显示可用空间只有476 GB左右这是正常的不是系统吃了你空间。格式化的核心逻辑是找到合适的单位并且控制小数位。我一般这样写function formatFileSize(bytes: number): string { if (bytes 0) { return 未知; } if (bytes 1024) { return ${bytes} B; } const units: string[] [KB, MB, GB, TB]; let value bytes / 1024; let unitIndex 0; while (value 1024 unitIndex units.length - 1) { value / 1024; unitIndex; } return ${value.toFixed(1)} ${units[unitIndex]}; }这里我有意让小于1024字节的文件直接显示整数不做小数。原因是1 KB以下的文件显示“0.5 KB”不仅啰嗦而且在列表页还要占用更宽的列宽得不偿失。大于等于1024字节的文件统一保留一位小数。比如2048字节会被格式化为“2.0 KB”而不是“2 KB”。理由也很实在——很多用户并不会把“2 KB”和“2.0 KB”当成一回事一位小数能有效减少歧义和信息损耗。你如果做的是英文环境应用可以再存一个Intl.NumberFormat的格式化逻辑把小数点和千分位按照地区习惯处理。3.3 大文件与超限场景size的返回值在HarmonyOS的API定义里是number类型对于绝大多数文件来说够用。但是要注意一点JavaScript的number超过Number.MAX_SAFE_INTEGER约9007199254740991之后整数精度就会丢失。普通文件不可能这么大但如果你去获取某些块设备文件的逻辑大小或者做的是网络文件系统挂载这个边界值得留意。实际开发中更常见的大文件问题是格式化。一个几GB的视频文件如果还用value.toFixed(1)去处理不会有问题但我见过有人用Math.round(value * 10) / 10去代替toFixed结果在特定场景下因为浮点精度出现“3.6000000000000005 GB”这种怪值。一律用toFixed或Intl.NumberFormat别自己手动做浮点四舍五入。还有一个小技巧如果你要展示的信息里同时包含“文件大小”和“磁盘占用空间”建议用两个不同的字段区分。因为稀疏文件、压缩文件的存在文件逻辑大小和实际占用空间可能差距很大。普通用户看的是逻辑大小技术运维看的是占用空间。一个工具类里最好同时暴露getSize()和getOccupiedSize()两个方法后者用blocks * blksize计算。4. 实操文件类型判断的三种思路4.1 方案一基于stat模式位判断这算是最正统的“文件系统层面”的判断思路。Stat对象里除了size还有mode属性。这个mode沿用类POSIX的定义是一组用八进制表示的位标志里面既包含权限位也包含文件类型位。你可以直接写function judgeFileKind(path: string): string { const stat fs.statSync(path); if (stat.isFile()) { return regular_file; } if (stat.isDirectory()) { return directory; } if (stat.isSymbolicLink()) { return symbolic_link; } return other; }Stat提供的isFile()这些方法在内部已经帮你做了位掩码运算。但如果你想深入了解原理或者做一些底层工具手工判断也完全可以const S_IFMT 0o170000; // 0xF000 const S_IFREG 0o100000; // 0x8000 const S_IFDIR 0o040000; // 0x4000 function judgeByMode(mode: number): string { const fileType mode S_IFMT; if (fileType S_IFREG) { return regular_file; } if (fileType S_IFDIR) { return directory; } return other; }方案一的边界非常明确它只能回答“文件系统认为这是什么”回答不了“这个文件业务上是什么”。一个文件即使改名为note.txt底层mode依然是普通文件。所以方案一适合用来做目录遍历、文件筛选这些底层逻辑不适合直接告诉用户“这是文本文件”。4.2 方案二扩展名映射MIME类型方案二才是业务侧最常见的做法。通过文件名后缀去映射MIME类型比如.png映射为image/png.mp4映射为video/mp4。MIME类型可以驱动图标选择、预览组件调用、分享面板过滤等一整套业务逻辑。在HarmonyOS里获取扩展名可以用fs模块提供的能力也可以自己解析路径字符串。官方fs模块其实提供了extname能力但我个人更推荐用一个通用工具方法去处理因为文件路径里可能带查询参数或特殊字符自己做解析更可控function getExtensionFromPath(path: string): string { const fileName path.substring(path.lastIndexOf(/) 1); const dotIndex fileName.lastIndexOf(.); if (dotIndex 0) { return ; } return fileName.substring(dotIndex 1).toLowerCase(); }拿到扩展名后映射表就是一张纯静态配置我通常把日常需要覆盖的几十种类型汇总成一张表const MIME_MAP: Recordstring, string { jpg: image/jpeg, jpeg: image/jpeg, png: image/png, gif: image/gif, webp: image/webp, bmp: image/bmp, svg: image/svgxml, mp3: audio/mpeg, wav: audio/wav, flac: audio/flac, aac: audio/aac, ogg: audio/ogg, mp4: video/mp4, mkv: video/x-matroska, avi: video/x-msvideo, mov: video/quicktime, webm: video/webm, m3u8: application/vnd.apple.mpegurl, pdf: application/pdf, txt: text/plain, html: text/html, htm: text/html, css: text/css, js: application/javascript, json: application/json, xml: application/xml, zip: application/zip, rar: application/vnd.rar, 7z: application/x-7z-compressed, doc: application/msword, docx: application/vnd.openxmlformats-officedocument.wordprocessingml.document, xls: application/vnd.ms-excel, xlsx: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, ppt: application/vnd.ms-powerpoint, pptx: application/vnd.openxmlformats-officedocument.presentationml.presentation }; function getMimeByExtension(extension: string): string { return MIME_MAP[extension] || application/octet-stream; }映射不到的类型我统一给application/octet-stream也就是“未知二进制流”。这个默认值非常关键它意味着“我不知道具体类型但不会误导系统”。如果你给它错误地映射成text/plain某些组件真的会尝试用文本方式打开二进制文件造成乱码甚至崩溃。方案二的优点是快、稳、可预测缺点是扩展名可以被随便改。一个本质上PNG的图片改成.zip之后方案二就会错误地把它识别为压缩包。很多病毒木马也喜欢利用扩展名伪装。所以如果你做的是安全扫描、文件校验类应用就需要方案三兜底。4.3 方案三读取魔数识别真实类型魔数Magic Number是文件格式设计者在文件头部固定的几个字节。比如JPEG文件开头是FF D8 FFPNG文件开头是89 50 4E 47PDF文件开头是25 50 44 46即%PDF。读取文件的前几个字节和已知魔数做比对就能绕过扩展名判断出真实文件类型。示例代码如下function sniffRealType(path: string): string { const file fs.openSync(path, fs.OpenMode.READ_ONLY); try { const buffer new ArrayBuffer(16); const readLen fs.readSync(file.fd, buffer); if (readLen 0) { return ; } const bytes new Uint8Array(buffer); if (readLen 3 bytes[0] 0xFF bytes[1] 0xD8 bytes[2] 0xFF) { return image/jpeg; } if (readLen 4 bytes[0] 0x89 bytes[1] 0x50 bytes[2] 0x4E bytes[3] 0x47) { return image/png; } if (readLen 3 bytes[0] 0x47 bytes[1] 0x49 bytes[2] 0x46) { return image/gif; } if (readLen 4 bytes[0] 0x25 bytes[1] 0x50 bytes[2] 0x44 bytes[3] 0x46) { return application/pdf; } if (readLen 2 bytes[0] 0x50 bytes[1] 0x4B) { return application/zip; } if (readLen 4 bytes[0] 0x52 bytes[1] 0x49 bytes[2] 0x46 bytes[3] 0x46) { return audio/wav; // RIFF } return ; } finally { fs.closeSync(file); } }这段代码有几个细节。finally里关闭文件是必须的。魔数识别阶段因为提前return可能会跳过正常的关闭流程有了finally就不会泄漏文件描述符。HarmonyOS对文件描述符数量是有上限的泄漏多了后续open会报“Too many open files”。读取的长度我给了16字节实际主流格式的魔数最多也就12字节左右16足够用了。你可以根据自己的业务场景扩展更多魔数比如MP3的ID3标签、FLV的FLV开头、ZIP的50 4B等。魔数识别也不是万能的。有些文件格式没有魔数比如纯文本。还有些格式之间存在魔数重叠比如ZIP和基于ZIP的DOCX/XLSX/PPTX开头都是50 4B光看前4字节无法区分具体是ZIP还是Office文档。真要区分还得继续解析文件内部的结构。所以方案三通常作为方案二的补充而不是替代。4.4 方案选型建议这三个方案不是互斥的工程上会组合使用。我个人在项目里沉淀出的判断模型是function getFileType(path: string): string { const kind judgeFileKind(path); if (kind ! regular_file) { return kind; } const extMime getMimeByExtension(getExtensionFromPath(path)); const snifMime sniffRealType(path); if (extMime application/octet-stream snifMime) { return snifMime; } if (extMime ! application/octet-stream snifMime) { // 扩展名和魔数一致说明文件类型可信 if (extMime snifMime) { return extMime; } // 不一致时通常信任魔数但保留扩展名信息 return snifMime; } return extMime; }简单说目录优先走模式位判断扩展名优先给出业务类型魔数用来纠偏防止“表面类型”和“真实类型”不一致。这个模型的准确率在绝大多数正规应用场景下都足够高而且性能开销只在文件打开的那一瞬间可以忽略不计。5. 完整案例做一个文件信息采集工具5.1 场景定义与界面结构前面聊了这么多还是落到一个能直接用的场景里。假设我要做一个文件管理器里的“文件详情页”需要在进入页面时展示三样东西文件大小、文件类型、文件类型图标。界面上至少需要三个文本区域分别显示名称、大小、类型描述。我在这里不写完整UI布局代码重点放在数据准备层因为无论你用List、Column还是Grid去展示数据层的逻辑都是一样的。数据层我封装成一个工具类取名FileInfoUtil包含四个对外能力获取字节大小、获取格式化大小、获取类型描述、获取展示用MIME类型。这样做的好处是上层业务无论来自文件管理器、下载管理还是聊天附件都可以直接复用不会出现“一个页面一套判断逻辑”的乱象。5.2 核心模块实现完整工具类如下我加了详细注释import { fileIo as fs } from kit.CoreFileKit; import { BusinessError } from kit.BasicServicesKit; const S_IFMT 0o170000; const S_IFREG 0o100000; const S_IFDIR 0o040000; const MIME_MAP: Recordstring, string { jpg: image/jpeg, jpeg: image/jpeg, png: image/png, gif: image/gif, webp: image/webp, bmp: image/bmp, svg: image/svgxml, mp3: audio/mpeg, wav: audio/wav, flac: audio/flac, aac: audio/aac, ogg: audio/ogg, mp4: video/mp4, mkv: video/x-matroska, avi: video/x-msvideo, mov: video/quicktime, webm: video/webm, m3u8: application/vnd.apple.mpegurl, pdf: application/pdf, txt: text/plain, html: text/html, htm: text/html, css: text/css, js: application/javascript, json: application/json, xml: application/xml, zip: application/zip, rar: application/vnd.rar, 7z: application/x-7z-compressed, doc: application/msword, docx: application/vnd.openxmlformats-officedocument.wordprocessingml.document, xls: application/vnd.ms-excel, xlsx: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, ppt: application/vnd.ms-powerpoint, pptx: application/vnd.openxmlformats-officedocument.presentationml.presentation }; export class FileInfoUtil { static getSize(path: string): number { try { const stat fs.statSync(path); return stat.size; } catch (err) { const e err as BusinessError; console.error(getSize失败: ${e.code}, ${e.message}); return -1; } } static getFormatSize(path: string): string { const size this.getSize(path); if (size 0) { return 未知; } return this.formatSize(size); } static formatSize(bytes: number): string { if (bytes 0) { return 未知; } if (bytes 1024) { return ${bytes} B; } const units: string[] [KB, MB, GB, TB]; let value bytes / 1024; let unitIndex 0; while (value 1024 unitIndex units.length - 1) { value / 1024; unitIndex; } return ${value.toFixed(1)} ${units[unitIndex]}; } static getKind(path: string): string { try { const stat fs.statSync(path); if ((stat.mode S_IFMT) S_IFDIR) { return directory; } else if ((stat.mode S_IFMT) S_IFREG) { return file; } else { return other; } } catch (err) { const e err as BusinessError; console.error(getKind失败: ${e.code}, ${e.message}); return unknown; } } static getExtension(path: string): string { const fileName path.substring(path.lastIndexOf(/) 1); const dotIndex fileName.lastIndexOf(.); if (dotIndex 0) { return ; } return fileName.substring(dotIndex 1).toLowerCase(); } static getMimeByExt(path: string): string { const ext this.getExtension(path); return MIME_MAP[ext] || application/octet-stream; } static sniffByMagic(path: string): string { const file fs.openSync(path, fs.OpenMode.READ_ONLY); try { const buffer new ArrayBuffer(16); const readLen fs.readSync(file.fd, buffer); if (readLen 0) { return ; } const bytes new Uint8Array(buffer); if (readLen 3 bytes[0] 0xFF bytes[1] 0xD8 bytes[2] 0xFF) { return image/jpeg; } if (readLen 4 bytes[0] 0x89 bytes[1] 0x50 bytes[2] 0x4E bytes[3] 0x47) { return image/png; } if (readLen 3 bytes[0] 0x47 bytes[1] 0x49 bytes[2] 0x46) { return image/gif; } if (readLen 4 bytes[0] 0x25 bytes[1] 0x50 bytes[2] 0x44 bytes[3] 0x46) { return application/pdf; } if (readLen 2 bytes[0] 0x50 bytes[1] 0x4B) { return application/zip; } if (readLen 4 bytes[0] 0x52 bytes[1] 0x49 bytes[2] 0x46 bytes[3] 0x46) { return audio/wav; } return ; } finally { fs.closeSync(file); } } static getSmartType(path: string): string { const kind this.getKind(path); if (kind directory || kind other || kind unknown) { return kind; } const extType this.getMimeByExt(path); const magicType this.sniffByMagic(path); if (magicType ! ) { return magicType; } return extType; } }这个类里的getSmartType就是给UI用的最终类型入口。它在常规情况下优先信任魔数因为魔数反映的是真实格式魔数识别失败时回退到扩展名映射。如果文件根本不是普通文件直接返回目录或其他类型避免把目录也拿去读魔数。我特意保留了getKind里的mode位运算版本而不是直接isFile()是想让你看看底层的判断逻辑是怎么写的。两者最终效果一样位运算版本可以帮你锻炼阅读理解真到了需要和原生代码比对校验的场景也有用。5.3 整体工作流程用户进入详情页时页面代码只需要调用两行const sizeText FileInfoUtil.getFormatSize(path); const typeText FileInfoUtil.getSmartType(path);这两行可以在aboutToAppear()生命周期里执行也可以放进异步任务里。如果文件很大建议把getSmartType里的sniffByMagic放到子线程去跑因为读取文件头部毕竟有一次IO操作。而getKind和getMimeByExt是纯计算主线程跑也无妨。有一个工程细节要说getSmartType读了一次文件头如果后续业务还要读取文件内容比如生成缩略图尽量复用已经打开的文件fd避免重复open。我这里为了保持工具类的方法独立性用了各自的open。在实际性能敏感的场景可以考虑增加一个“打开一次、执行多个查询”的重载方法。6. 常见问题与排查记录6.1 权限拒绝HarmonyOS的沙箱机制非常严格。应用访问自己的filesDir、cacheDir下文件不需要任何权限但访问公共媒体库、其他应用的文件必须申请对应权限并且用户授权。最常见的报错是error: 13900001, Operation not permitted或者类似信息。排查思路是先确认路径是否在应用沙箱内。context.filesDir、context.cacheDir、context.databaseDir这些拿到的路径天然在沙箱内没有权限问题。用fileUri.getUriFromPath()把绝对路径转成URI时也要注意这个URI是否属于当前应用。如果确实要操作公共媒体文件先看是否有对应权限。读取媒体文件时需要ohos.permission.READ_IMAGEVIDEO、ohos.permission.READ_AUDIO、ohos.permission.READ_DOCUMENT等权限。就算申请了权限用户依然可以在系统设置里随时关闭所以代码里必须能处理catch到的权限错误弹窗引导用户去设置页补开。我自己的习惯是在工具类入口做一个“路径可达性检查”function checkPathAccess(path: string): boolean { try { const stat fs.statSync(path); return stat.isFile() || stat.isDirectory(); } catch (err) { return false; } }这个函数只回答一个问题现在能不能访问这个路径。如果不能就别往下走复杂的类型识别了。6.2 路径不存在或非法statSync抛error: 13900002, No such file or directory基本可以断定路径写错了。这里面最容易出问题的是路径拼接。HarmonyOS沙箱的filesDir末尾不带斜杠很多同学习惯拼成${filesDir}/filename.txt这没问题。但如果你的路径来自Uri转换可能带着file://前缀直接拿去拼接就会变成无效路径。我在项目中故意维护了一个normalizePath函数专门剥掉file://前缀、规范化../和./片段。虽然绝大多数时候用不到但每次接入新数据源之前调用一次能省掉一排查问题。6.3 拿到的size和系统显示不一样你获取的大小跟系统文件管理器显示的一样但和网速计算器、存储分析软件显示的不一样。这背后是1024进制和1000进制的差异。前面说过文件系统的1 KB等于1024 B但存储行业习惯用1000 B。所以同样的一个文件系统文件管理器显示“1.0 MB”某些硬盘厂商工具显示“1.05 MB”谁都没错只是计算进制不同。如果你的应用要面向国内用户建议明确跟随文件系统的1024进制这更符合系统文件管理器的呈现习惯。还有一种情况是“逻辑大小”和“占用空间”的差异。一个内容只有10 KB的文件如果文件系统块大小是4 KB它可能占用12 KB甚至16 KB的磁盘空间。用户看到的“大小”是逻辑大小磁盘清理工具显示的“占用”是物理大小。如果产品经理要求展示占用空间记得用blocks * blksize。6.4 扩展名判断类型不准扩展名判断不准通常有三种原因。第一种扩展名大小写问题。.PNG和.png应该是同一个类型。所以我在getExtension里统一toLowerCase()了。这个细节看起来微不足道但确实有人因此写出一堆冗余判断分支。第二种复合扩展名问题。比如archive.tar.gz简单取最后一个点之后的内容会得到gz但用户期望的是tar.gz。解决方案是维护一个复合扩展名表比如tar.gz、log.txt这类优先匹配长后缀。我在上面代码里没写这个逻辑实战中如果你是做压缩包管理工具必须补上。第三种无扩展名文件。很多Linux风格文件、临时文件根本没有扩展名。此时getMimeByExt会回退到application/octet-stream这是合理的。如果你想做得更好可以在无扩展名时用魔数判断如果魔数也识别不了那就当成二进制流处理别强行猜。我还遇到过一种特殊情况从图库选出来的图片系统给的是content://media/external/images/media/xxx这种URI直接对它做getExtension会得到乱码或者空。这种URI必须先解析成真实路径或者用媒体库的displayName字段拿文件名。HarmonyOS里可以通过fileUri.getPathFromUri()进行转换但不同来源的URI兼容性还需要你多测几种设备。6.5 文件描述符泄漏用open打开文件后不关闭短时间内可能发现不了问题但操作多了就会出现Too many open files。这类问题的隐蔽性在于普通文件操作偶尔一两个fd丢失不会触发系统限制只有长时间运行、频繁操作才会炸锅。我排查过某公司的日志组件就是在一个循环里不断open却不close运行一周后应用开始频繁crash。规避思路有三条。第一能用stat解决的事就别open。第二必须open时把close放到finally。第三建立“谁打开谁关闭”的代码审查习惯不要把fd传出工具类再期待调用方去关闭。6.6 异步回调里更新UI导致崩溃很多同学拿到stat结果后直接在回调里更新Text组件代码看着没问题但偶尔会报view management is not available之类的错误。原因是异步回调的执行线程不一定在主线程。HarmonyOS的UI操作必须在主线程执行如果你在TaskPool或其它子线程拿到的结果直接刷新UI就会撞上线程限制。正确做法是把计算和IO放到子线程把结果通过Emitter或mainThread调度回主线程再更新UI。或者更简单的方式用async/await搭配主线程调用点确保await之后的代码回到主线程执行。这个细节曾让项目里的几个新同学卡了两天在这里特别提醒一下。7. 踩坑总结与后续扩展我在实际项目里再做文件信息相关功能时已经形成了一套固定动作先判断路径类型再判断文件类型再展示格式化大小最后才谈后续业务。顺序不能反。如果你先展示大小再去判断类型遇到目录时size字段虽然存在但代表目录项本身大小展示出来会让用户疑惑“这个目录怎么只有4 KB”。这套工具类后续还可以扩展的地方很多。比如给FileInfoUtil增加缓存避免同一路径被反复stat增加大文件的异步识别策略再或者把魔数表抽成配置文件交给运营人员动态更新。这些都属于锦上添花核心的点还是在于理解stat返回的字段含义以及三种类型判断各自的能力边界。我个人最想强调的是文件大小和文件类型看似是“一个小功能”但它处在所有文件业务的最底层。底层一旦有偏差上层列表、预览、分享、搜索全都会跟着错。把这几行逻辑吃透、写稳比后面堆多少个页面都管用。最后一个小技巧送给你调试文件API时多打印stat对象的完整信息你会对HarmonyOS文件系统有更直观的理解。不少诡异的bug其实都是对某个字段理解不到位导致的。

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

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

免费获取报价 →
↑