基于S1000D/GJB6600的Android IETM阅读器架构与实现
简介《基于Android系统的IETM阅读器研究与设计》是一篇发表于《计算机测量与控制》2020年第1期的期刊论文PDF面向Android客户端开发人员、装备保障信息化研究者及高校师生聚焦如何将原本依赖Windows平台的交互式电子技术手册迁移到Android系统。文章在分析IETM使用需求的基础上给出阅读器的总体架构与功能模块划分并介绍在Android Studio环境下开发阅读器App的实现方法可作为移动应用开发的参考文献与专业指导。压缩包仅含1个PDF文件体积约2.28MB内容完整清晰适合课题调研、方案论证或论文写作时引用。目前已有110人学习下载可帮助读者快速了解IETM阅读器的设计思路与实现路径为移动端手册类应用的架构设计与开发落地提供参考。1. 为什么要把 IETM 阅读器搬到 Android 平板上很多人第一次接触 IETM交互式电子技术手册是在 PC 端的 Windows 阅读器里界面是传统的三栏布局、树形目录加右侧内容区鼠标点得挺顺手。但真正到了现场——机库、车间、外场保障点——没人愿意抱着一台笔记本翻手册而平板单手就能拿。矛盾就出在这里早期 IETM 阅读器大多基于 Windows 开发直接塞进 Android 平板会出现界面适配差、视频和三维仿真卡顿、触控操作不符合习惯的问题。这时候需要的不是重写数据而是重写阅读器。数据部分依旧遵循 S1000D 或 GJB6600 标准DM、PM、业务规则都是按 Schema 做出来的 XML只有阅读器这一层跟操作系统绑定。本文拆的就是这类 Android IETM 阅读器的总体架构、功能模块划分、Android Studio 工程组织方式以及权限、解析、多媒体、检索这些模块具体怎么落地。2. IETM 数据包结构与 Android 阅读器的分层架构要动手写阅读器先得弄明白它吃进去的是什么。阅读器本身不做数据生产它只是把符合标准的 DDN 包解析出来渲染。搞清楚数据从哪来、怎么装进平板、阅读器内部又拆成几层后面的代码才有落脚点。2.1 DDN 包、DM 与 PM阅读器的输入契约数据制作平台里编辑的内容存储在一个叫 CSDBCommon Source DataBase的库里最终导出的就是 DDNData Dispatch Note包。这个包里装的东西可以粗分成四类DMData Module数据模块、PMPublication Module出版物模块、多媒体资源图片、视频、三维模型以及业务规则文件。DM 是内容的最小单位一段操作步骤、一条警告、一个故障隔离程序都是 DMPM 则是手册的目录骨架它决定了装备介绍手册、操作使用手册、维护保养手册这些分类怎么组织。DM、PM、业务规则按 S1000D 或 GJB6600 的 Schema 做成 XML。这意味着只要你选择的制作平台遵循同一套标准导出的数据在语义上是一致的阅读器不需要为每个厂家定制解析逻辑。真正跟操作系统绑定的是阅读器部分——把 XML 变成用户能看、能点、能操作的界面。这也是为什么同一份 DDN 包能在 Windows 上做一个阅读器在 Android 上做另一个但数据不用改。一个典型的 PM 引用关系在 XML 里长这样!-- PM 文件片段声明手册标题与包含的 DM 引用 -- pm pmTitleXXX 型装备操作使用手册/pmTitle content !-- dmRef 指向具体数据模块identNumber 由数据制作平台生成 -- dmRef dmRefIdent dmCode modelIdentCodeXXX systemDiffCodeA systemCode0102 subSystemCode0 subSubSystemCode0 assyCode00 disassyCode00 disassyCodeVariantA infoCode040 infoCodeVariantA itemLocationCodeA/ language countryIsoCodeCN languageIsoCodezh/ /dmRefIdent /dmRef /content /pm这段 XML 的关键在dmCode那一串属性。它不是随便编的编号而是按标准分段的systemCode表示系统infoCode表示信息类型比如 040 一般是操作类、220 是故障类itemLocationCode表示适用位置。阅读器解析出这串编码之后才能知道这份 DM 属于哪本手册、是什么类型、该用哪种渲染方式。初学者常见的误区是把解析做成读全文文本那样后面按 DM 类型切换展示样式、按密级过滤内容都无从谈起。2.2 阅读器的三层结构UI、业务、数据访问Android 上做阅读器我一般按三层来切。UI 层是 Activity 与 Fragment负责渲染结构树、内容区和菜单栏业务层负责登录鉴权、密级过滤、解析调度、书签与检索数据访问层负责从 DDN 包读 XML、存取本地 SQLite 中的用户和书签记录。为什么要明确分层而不是全塞到一个 Activity 里因为 IETM 阅读器的功能点密度很高登录、手势密码、PM 树、DM 渲染、多媒体、书签、检索、维护提醒、维修信息录入、二维码扫描任何一个模块都可能单独迭代。全部耦合在一起后续加中英文切换或者密级过滤时会非常痛苦。分层之后解析逻辑改了不影响登录渲染样式改了不影响数据访问。提示DDN 包解压后建议放到应用私有目录或 SD 卡固定路径别散落在 assets 里。assets 只读且编译期就固定后期更新手册包要重新打包 APK现场根本没法用。2.3 Android Studio 工程结构与 Gradle 依赖组织环境上官方主推 Android Studio早期用 Eclipse ADT 的方式已经基本淘汰因为 Eclipse 是为 Java 工程设计的对 Android 的资源系统、构建流程支持先天不足。Android Studio 里能直接调用封装好的 API资源目录、依赖管理都顺手很多。开始写之前最新版 SDK 和 JDK 要装好local.properties里的 SDK 路径要指对否则 Gradle 同步会一直报错。建议的模块划分是这样的把解析逻辑单独抽成ietm-parser模块UI 相关的放主app模块公共的工具类放core。app模块的build.gradle里关键依赖大致是这样android { compileSdk 34 defaultConfig { applicationId com.example.ietmreader minSdk 21 targetSdk 34 // 大手册包可能涉及大量资源开启分包避免 65536 方法数限制 multiDexEnabled true } } dependencies { // XML 解析XMLPullParser 是 Android 自带的但如果要处理大文档 // 建议配合流式读取 implementation androidx.recyclerview:recyclerview:1.3.2 implementation androidx.viewpager2:viewpager2:1.1.0 // 本地存储 implementation androidx.room:room-runtime:2.6.1 annotationProcessor androidx.room:room-compiler:2.6.1 // 多媒体 implementation com.google.android.exoplayer:exoplayer:2.19.1 }compileSdk和targetSdk决定你调用 API 的上限和系统行为兼容级别。minSdk 21是个常见起点覆盖多数平板如果现场设备更老得往下调但要注意部分 API 需要做版本判断。multiDexEnabled在大手册项目里几乎是必开的因为多语言资源和高密度图标的 drawable 很容易让方法数超限。ExoPlayer 替代系统 MediaPlayer主要因为它在自定义播放控制和多格式支持上更灵活视频快进、倍速、全屏切换这些需求自己实现更可控。3. 权限安全与手势登录模块的 Android 实现IETM 数据本身带密级所以阅读器的第一道门槛是身份与权限。这一层没做好后面的内容展示、检索、维修记录都可能是越权的。原文把功能分成了权限及安全管理和阅读功能两大块先从安全管理说起因为它决定了用户进来之后能看到什么。3.1 用户角色、密级与内容过滤的判定逻辑权限设计的核心是用户有密级和角色数据模块也有密级用户只能看到等于或低于自身密级、且被允许访问的 PM 和 DM。角色上区分管理员和普通用户管理员才能注册用户、设置密级、删除用户、设置密码复杂度。这个判定不能只在界面层做隐藏处理必须在数据访问层就过滤掉。原因是 IETM 的密级是内容属性如果只在列表里不显示用户通过检索或者书签跳转依然可能摸到高密级内容。常见做法是在解析出 DM 列表之后先用一层过滤器过一遍// 根据用户密级过滤可访问的数据模块 public ListDataModule filterByClearance(ListDataModule all, int userLevel) { ListDataModule visible new ArrayList(); for (DataModule dm : all) { // 1. 密级判定DM 密级必须 用户密级 boolean levelOk dm.getSecurityLevel() userLevel; // 2. 授权判定DM 必须被显式标记为该用户组可访问 boolean allowed dm.getAllowedRoles().contains(currentUser.getRole()); if (levelOk allowed) { visible.add(dm); } } return visible; }userLevel是数值化的密级数值越大级别越高。getSecurityLevel()从 DM 的 XML 元数据里读出getAllowedRoles()同理。两个条件必须同时成立才放行。这么做还有一个好处检索模块直接复用这层过滤后的列表天然不会检索出越权内容。3.2 用户名密码、手势与二维码三种入口的实现差异登录入口原文里给了好几种用户名密码、手势密码、指纹硬件支持时。手势密码的体验最好但要处理好忘记手势密码的路径——一般通过普通密码验证后重置。手势本身可以用 Android 的GestureOverlayView或者自己画九宫格点阵来实现前者是系统控件后者可控性更强。自己画九宫格时密码存储不能存明文路径。常见做法是把选中的点序列做一次哈希再存 SQLite// 手势密码哈希存储避免明文保存触点序列 public String hashGesture(ListInteger points) { StringBuilder sb new StringBuilder(); for (Integer p : points) { sb.append(p).append(,); } try { MessageDigest md MessageDigest.getInstance(SHA-256); // 加盐值 salt 防止彩虹表salt 每个用户独立生成并存储 md.update((sb.toString() userSalt).getBytes(StandardCharsets.UTF_8)); byte[] digest md.digest(); return Base64.encodeToString(digest, Base64.NO_WRAP); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(SHA-256 不可用, e); } }触点序列拼成字符串后加盐再 SHA-256得到的 Base64 存库。验证时用同样流程算出哈希再比对。盐值必须每用户独立不能全局共用一个常量否则两个用户手势相同就会得到相同哈希容易被推断。二维码登录用的是Capture Activity扫描后解析出故障代码再和本地故障库匹配展示对应的故障信息和定位方法——这条路径本质上是把扫码当成故障检索的快捷入口。3.3 密码复杂度策略与安全设置界面密码复杂度一般限制长度和字符类型比如最少 8 位、必须包含大小写字母和数字、可选特殊字符。校验逻辑放在工具类里注册和改密都调用同一份// 密码强度校验长度 字符类型数量 public boolean isPasswordValid(String pwd, int minLen, int requireTypes) { if (pwd null || pwd.length() minLen) return false; int types 0; if (pwd.matches(.*[a-z].*)) types; if (pwd.matches(.*[A-Z].*)) types; if (pwd.matches(.*\\d.*)) types; if (pwd.matches(.*[^a-zA-Z0-9].*)) types; return types requireTypes; }minLen和requireTypes由管理员在安全设置界面配置存在配置表里。改密流程要注意确认旧密码 → 校验新密码复杂度 → 写入新哈希 → 强制退出登录并回到登录页提示重新登录。这三步顺序不能乱否则用户可能改完密码还停在当前会话里安全上不严谨。注意用户表里的密码字段永远只存哈希和盐任何界面、日志、导出功能都不能打印明文或完整哈希。4. DDN 包解析、DM 渲染与多媒体的落地细节阅读器的核心还是把 XML 变成看得懂的东西。这一层做得好不好直接决定用户愿不愿意用它替代纸质手册。4.1 用 XMLPullParser 流式解析 DM 与 PMAndroid 上解析 XMLXmlPullParser是自带且轻量的选择适合流式读取大文档。DOM 方式会把整棵树读进内存一个装备的 DDN 包动辄几十兆DOM 很容易 OOM。用 PullParser 边读边处理内存占用小很多。解析 PM 得到结构树的骨架// 流式解析 PM构建手册目录树 public TreeNode parsePm(InputStream is) throws Exception { XmlPullParser parser Xml.newPullParser(); parser.setInput(is, UTF-8); TreeNode root new TreeNode(root); TreeNode current root; int event parser.getEventType(); while (event ! XmlPullParser.END_DOCUMENT) { String name parser.getName(); if (event XmlPullParser.START_TAG) { if (pmTitle.equals(name)) { // 手册标题作为根节点名称 root.setTitle(parser.nextText()); } else if (dmRef.equals(name)) { // 每个 dmRef 生成一个子节点属性在下一层取 current new TreeNode(dm); root.addChild(current); } else if (dmCode.equals(name) current ! null) { // 取出编码信息供后续定位 DM 文件使用 current.setDmCode( parser.getAttributeValue(null, systemCode), parser.getAttributeValue(null, infoCode)); } } event parser.next(); } return root; }逐标签判断的方式决定了遇到pmTitle就设置标题遇到dmRef就建子节点遇到dmCode就抽编码属性。parser.nextText()会消耗到下一个标签所以调用前要确认当前确实在文本节点上。这套写法对内存友好缺点是需要自己维护状态标签层级一深就容易漏分支建议解析前先对着 Schema 把层级关系画一遍。4.2 按 DM 类型与 GJB6600 字段选择渲染方式DM 类型很多描述类、操作类、程序类、故障类、维修计划类、图解零件类、过程类、接线类。不同类型的展示逻辑差别很大。程序类要支持步进浏览故障类要支持人机问答模式图解零件类要做热点图表和双向链接。内容字段层面段落、标题、列表、表格、链接较常规麻烦的是警告、注意、注这几种。原文要求警告和注意要自动弹醒目对话框确保信息被读到。实现上可以在渲染到这些标签时插入一个特殊标记点击或滚动到该位置时触发弹窗// 渲染警告节点时弹出对话框 private void renderWarning(WarningNode node) { // 用 AlertDialog 高亮展示标题加警告字样 new AlertDialog.Builder(context) .setTitle(警告) .setMessage(node.getText()) .setPositiveButton(已知悉, (d, w) - d.dismiss()) .setCancelable(false) // 必须确认不允许点外部关闭 .show(); }setCancelable(false)是刻意为之警告类信息不允许点一下别处就跳过必须点确认。表格用RecyclerView加GridLayoutManager渲染比TableLayout更灵活尤其是列数不固定的时候。4.3 图片、视频和三维仿真的插件化处理多媒体处理上图片要支持放大、局部放大、旋转、全屏视频要支持播放暂停、快进慢进、倍速、音量、全屏三维仿真要支持缩放旋转、动态展示机构运动和拆装过程。这些需求逐一自研成本很高常见做法是图片用成熟的PhotoView类库处理手势缩放视频用 ExoPlayer三维仿真视资源格式而定如果模型是通用格式可以接第三方渲染库如果自带渲染引擎则通过 WebView 或自定义 View 封装。ExoPlayer 的基本播放控制是这样接的// ExoPlayer 初始化与播放控制 SimpleExoPlayer player new SimpleExoPlayer.Builder(context).build(); player.setMediaItem(MediaItem.fromUri(videoUri)); // 倍速播放现场看拆装演示时很实用 player.setPlaybackSpeed(1.5f); player.prepare(); player.play(); // 监听播放状态用于更新进度条 player.addListener(new Player.Listener() { Override public void onPlaybackStateChanged(int state) { if (state Player.STATE_ENDED) { // 播放结束重置到起点方便重看 player.seekTo(0); } } });setPlaybackSpeed支持 0.5 到 2.0 倍速慢放对理解拆装步骤很有帮助。进度条要自己监听回调刷新Player.STATE_ENDED时重置到 0 秒是常见交互避免用户每次都要手动拖回开头。三维仿真的交互如果交给第三方库重点确认它对触摸旋转、双指缩放的响应是否跟其他模块一致不然用户会觉得各部分手感割裂。4.4 书签、检索与维护提醒的存储设计辅助功能里书签、内容检索、维护任务提醒、访问记录、维修信息录入这几块都涉及本地存储用 SQLite 或 Room 比较合适。书签记录要存 DM 的定位信息比如 dmCode和用户 ID访问记录要记录登录登出时间、浏览内容和顺序维护提醒要存任务初始时间和项目检查表。检索分全文检索和精确检索。全文检索对整本手册做关键字模糊查询走遍历索引的方式比较慢建议在第一次加载 DDN 包时建一个倒排索引表把词和 DM 的对应关系存下来-- 全文检索倒排索引表一个词可能对应多个 DM CREATE TABLE search_index ( id INTEGER PRIMARY KEY AUTOINCREMENT, keyword TEXT NOT NULL, dm_code TEXT NOT NULL, weight INTEGER DEFAULT 1 ); CREATE INDEX idx_keyword ON search_index(keyword);精确检索则按型号、DM 类型、物理结构做条件组合本质是拼 SQL 的WHERE子句。这里要注意无论哪种检索结果集必须先经第 3 章的密级过滤否则等于给越权内容开了后门。维护提醒用AlarmManager或WorkManager定时触发到期弹窗提示点击跳转到对应维护方法。功能模块存储方式关键字段注意事项书签SQLitedm_code、user_id、备注双击定位到 DM检索索引SQLite 倒排表keyword、dm_code、weight加载 DDN 时建索引维护提醒SQLite WorkManager任务时间、检查表 ID到期弹窗并跳转访问记录SQLite登录登出时间、浏览顺序仅管理员可查维修信息SQLite 导出故障、方法、备件、工具导出通用格式数据包5. 混合语言资源、二维码定位与现场调试的几个技巧工程做到后期真正花时间的往往不是核心解析而是这些边角需求。5.1 中英文切换的资源组织与 XML 字段处理军贸装备出口要求阅读器支持中英文切换。如果只是界面文字values-zh和values-en两套strings.xml就够了。但 IETM 的内容本身是多语言的——同一份 DM 可能有中英两版 XML语言标识在language节点里前面 PM 片段中的countryIsoCode和languageIsoCode。所以切换时要做两件事换界面资源再按语言标识重新筛选 DM 内容。常见做法是在解析 PM 树时同时读language节点把中英两份 DM 都纳入索引用户切换语言时只改变过滤条件不重新解压 DDN 包。这样切换几乎是瞬时的不用等解析。// 按当前语言筛选 DM public ListDataModule filterByLanguage(ListDataModule all, String lang) { ListDataModule result new ArrayList(); for (DataModule dm : all) { // languageIsoCode 形如 zh / en if (lang.equalsIgnoreCase(dm.getLanguageCode())) { result.add(dm); } } return result; }这里建议在数据结构设计阶段就给每个 DM 带上语言标签否则后面临时加字段会牵动解析、渲染、检索多个模块。5.2 二维码扫描与故障代码匹配二维码扫描在故障定位场景里很实用设备上贴一个码现场扫一下就跳到对应故障的排查步骤。实现上Capture Activity负责取景和识别拿到字符串后和故障代码库匹配。匹配前最好先做一次格式校验避免扫到无关码就崩溃// 二维码内容匹配故障代码 public FailureInfo matchFailure(String qrContent) { // 约定二维码内容以 FAULT: 开头 if (qrContent null || !qrContent.startsWith(FAULT:)) { return null; // 非故障码忽略 } String code qrContent.substring(FAULT:.length()).trim(); // 从本地故障库查询走主键索引 return failureDao.findByCode(code); }约定前缀是低成本但很有效的做法能过滤掉绝大多数误扫。findByCode走主键查询几乎无延迟返回后直接跳转到故障隔离页面。5.3 大手册包下的内存与启动优化DDN 包大、图片视频多启动时如果一次性全解析白屏时间会很难看。几个实测有效的做法解析放到子线程主线程只更新树节点PM 树先出骨架DM 内容按需加载用户点到哪个节点再解析哪个图片用Glide之类的库做采样和缓存别直接BitmapFactory.decodeFile全尺寸加载。三维资源如果太大考虑首次打开时提示首次加载需解压并显示进度条不要卡在启动页。另外Application里别做重初始化数据库、解析器都延迟到首次使用时再建启动会明显快。提示现场设备内存有限建议在大包加载前后用Runtime.getRuntime().totalMemory()观察堆占用定位是哪一步吃掉了内存比盲猜有效。5.4 真机联调与日志排查平板调试和手机有一点不同屏幕大横竖屏适配要单独验尤其是三栏布局菜单栏、导航树、内容区在横屏下才舒服竖屏要么折叠菜单要么改上下布局。联调时用adb无线调试比插线方便现场设备挪来挪去插着线反而不好操作。关键日志按模块打 tag解析失败、密级过滤命中数、检索耗时这几类信息值得长期保留出问题一眼能看出卡在哪一层。解析异常不要只 catch 了事把出错的 DM 编码和标签路径打出来回头对着 DDN 包里的原始 XML 就能定位是数据问题还是代码问题。本文还有配套的精品资源点击获取