# 〖共创稿事节〗HarmonyOS 7 新特性实战47把装机门临时提到 26让 linter 把迁移债一次吐干净我原本以为这个工程的 API 26 迁移已经做完了。targetSdkVersion写着26.0.0需要按新版分支处理的地方都用deviceInfo.sdkApiVersion判断过了构建一路全绿。直到有一天把compatibleSdkVersion也临时改到 26重新编一遍——31 条警告冒出来其中 5 条是我完全没意识到的。问题出在一个容易忽略的地方linter 按哪个版本检查取决于compatibleSdkVersion而它同时是装机门。官方文档应用版本兼容性配置核对日期 2026-10-29一、装机门有两副面孔compatibleSdkVersion在构建配置里只是一个字符串但它同时决定两件毫不相干的事它决定什么表现谁在关心这台设备能不能装低于该版本的设备直接装不上用户、应用市场linter 按哪个口径检查检查规则集、弃用 API 清单都跟着走编译器第二件事是我之前没意识到的。compatibleSdkVersion是6.0.0(20)linter 就老老实实按 API 20 的规则集检查——那些API 26 起建议改用 XX的提示一条都不会出现因为按 20 的口径它们本来就不成立。这不是 bug是设计。你告诉编译器我要兼容到 20它就不该拿 26 的规矩挑你的刺。但对迁移来说这是个陷阱targetSdkVersion提到 26 只说明我愿意按 26 的行为跑不代表代码里没有 26 才废弃的用法。想看到这些债得让 linter 站到 26 的角度去看。二、先踩一个格式坑改之前我随手写了个26.0两段。构建直接拒绝 hvigor ERROR: 00306042 Specification Limit Violation Error Message: In HarmonyOS configuration: - For API versions 10 to 25, the value of compatibleSdkVersion/targetSdkVersion must be a string (for example, 5.0.0(12)). - For API version 26 and later, the value of compatibleSdkVersion/targetSdkVersion must be a string (for example, 26.0.0).这条报错把规则说全了而且是两种互斥的写法API 区间合法写法例子10 – 25三段 括号里的数字5.0.0(12)26 及以上纯三段不带括号26.0.0跨过 26 这道坎写法就得换。这一点靠猜是猜不出来的26.0、26.0.0(26)都会被拒。改成26.0.0后格式校验通过linter 这才开始按 26 的口径检查。三、31 条警告只有 5 条是债拿到手的 31 条按文件分布如下出警告的模块条数警告正文音频路由监控21The system capacity of this api audio is not supported on all devices异兽超分查看器54×px2vp has been deprecated 1×getMediaContent has been deprecated空间音频封装31×module for libshanhaiaudio.so is not verified 2×Function may throw exceptions讲解服务1The system capacity of this api media is not supported on all devices沉浸式外框1This API is unavailable to 2in1合计COMPILE RESULT:FAIL {ERROR:1 WARN:31}。31 条看着吓人但按要不要动代码分是两类完全不同的问题5 条deprecated是真债。SDK 26 里有明确的替代 API现在是警告将来可能是错误。必须改。其余 26 条不是你写错了。其中 21 条 audio 1 条 media 是设备容量提示——这类能力在部分设备上不存在1 条 2in1 不可用1 条 napi 模块未验证SDK 后续版本才启用校验2 条Function may throw exceptions是提醒补异常处理。它们要的答案是能力探测 降级——代码里那唯一一处getDevicesSync本来就包在 try/catch 里取不到就返回上一次快照。把这 26 条当债去修只会写出毫无意义的空判断或多余的 try/catch。这一点值得单独说同一份警告列表里性质不同的东西混在一起先分类再动手比逐条处理快得多。四、替代项不要猜去读 d.ts5 条deprecated集中在同一个文件。我没有查文档直接翻了 SDK 的.d.ts——被弃用的方法上面都有useinstead字段写着该换成什么。这一步值得强调编译器的建议和它想让你用的替代品是同一份数据查文档拿到的可能是过时描述读 d.ts 拿到的是编译器真正据以判定的那条。px2vp那 4 处在一段避让区计算里四行连着出现private loadSafeInsets(): void { try { const hostWindow: window.Window this.getUIContext().getHostContext() as window.Window; const types: window.AvoidAreaType[] [ window.AvoidAreaType.TYPE_SYSTEM, window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR ]; let top: number 0; let bottom: number 0; let left: number 0; let right: number 0; for (const type of types) { const area: window.AvoidArea hostWindow.getWindowAvoidArea(type); // 改前是全局 px2vp(...)SDK 26 linter 标 deprecated top Math.max(top, this.getUIContext().px2vp(area.topRect.height)); bottom Math.max(bottom, this.getUIContext().px2vp(area.bottomRect.height)); left Math.max(left, this.getUIContext().px2vp(area.leftRect.width)); right Math.max(right, this.getUIContext().px2vp(area.rightRect.width)); hilog.info(SR_LOG_DOMAIN, ShanhaiSR, avoid type%{public}d t%{public}d b%{public}d l%{public}d r%{public}d, type, area.topRect.height, area.bottomRect.height, area.leftRect.width, area.rightRect.width); } this.safeTop top; this.safeBottom bottom; this.safeLeft left; this.safeRight right; } catch (error) { const failure error as BusinessError; hilog.warn(SR_LOG_DOMAIN, ShanhaiSR, avoid area unavailable code%{public}d, failure.code); } }px2vp这类全局函数的问题在于它拿不到UIContext——它只能按某种进程级的默认密度换算。在多窗口、多实例场景下同一个数值在不同窗口里应该得到不同的 vp 结果而全局函数给不出这个区分。改成UIContext的实例方法后密度来源就明确了是这个界面实例所在的窗口在换算。getMediaContent那处在解码原图的方法里改完连带把整条解码链看得更清楚// 解码期就降到低清长边 SR_INPUT_LONG_SIDE按原始长宽比缩放不做拉伸变形。 private async loadLowRes(): Promiseimage.PixelMap | undefined { try { const context this.getUIContext().getHostContext(); if (context undefined) { this.note 无法获取应用上下文请稍后再试。; this.phase ShanhaiSuperResolutionPhase.FAILED; return undefined; } // 改前await context.resourceManager.getMediaContent(...) // 改后同步版SDK 26 linter 建议的替代 const raw context.resourceManager.getMediaContentSync(shanhaiBeastImage(this.beastId)); const source image.createImageSource(raw.buffer as ArrayBuffer); const info await source.getImageInfo(); this.srcW info.size.width; this.srcH info.size.height; // 拿到原图宽高就定屏幕方向不等 4 倍重建跑完 —— 否则用户会先看一小条图 this.syncOrientation(info.size.width, info.size.height); const longSide Math.max(info.size.width, info.size.height); const ratio SR_INPUT_LONG_SIDE / longSide; const options: image.DecodingOptions { desiredSize: { width: Math.max(1, Math.round(info.size.width * ratio)), height: Math.max(1, Math.round(info.size.height * ratio)) } }; const pixelMap await source.createPixelMap(options); await source.release(); this.lowRes pixelMap; this.pane SR_PANE_LOW; return pixelMap; } catch (error) { const failure error as BusinessError; console.error([shanhai-sr] decode failed code failure.code message failure.message); this.note 这张插画暂时没能打开请稍后再试。; this.phase ShanhaiSuperResolutionPhase.FAILED; return undefined; } }异步改同步一开始我有点犹豫——不会卡住 UI 线程吗实际这处读的是一张压缩过的图片资源几十 KB 的量级同步读完的开销比走一次 Promise 往返还小而且紧接着的createPixelMap和 4 倍重建本身就是异步的这一处省掉的往返不会改变整体的响应形态。SDK 把它标成deprecated since 20并推同步版本身就是基于这个判断。改的过程中还有个容易看错的地方紧跟着的syncOrientation(...)里的sync和这次改动没有关系它指的是「让屏幕方向和图片长宽比同步」不是「同步调用」。同一个词在同一段代码里出现两种含义我在改到这里时才意识到名字会误导人。改完再编一次BUILD SUCCESSFULArkTS:WARN计数 0deprecated计数 0。五、改完必须还原而且得证明还原了这一步是最容易忘的。compatibleSdkVersion是装机门提到 26 之后所有低于 26 的设备都装不上更隐蔽的是代码里那些deviceInfo.sdkApiVersion 26的判断会全部变成死代码——分支还在但永远走不到。还原之后我做了逐字节比对和改之前的备份diff完全一致。这不是形式主义做实验改的文件必须能证明它回到了原样否则临时改一下会变成一次没人发现的静默变更。有个细节帮了大忙这个文件在本机一直在 git 的skip-worktree里git ls-files -v前缀是S因为它要写本机签名路径不能提交。这意味着改动不会污染仓库git status也看不见它——好处是不用担心误提交代价是必须自己负责确认还原git 不会提醒你。六、换一条渠道再查一遍linter 报完 31 条我手上的信息是规则认识的弃用项都清了。但我不知道规则不认识的东西还有没有。于是换了另一条路用对话式 Agent 读一遍代码。用的是 DevEco Code 的 plan 模式——只读不改文件。选 plan 而不是默认模式有个现实原因当时另一个 agent 正在同一个仓库里写代码不能有两个写者。问的问题就是把这次迁移说清楚compatibleSdkVersion从6.0.0(20)提到26.0.0主模块的 ArkTS 源码里有哪些 deprecated API 要迁移给文件 行号 替代写法。它的回答分两半第一半让我愣了一下它说px2vp和getMediaContent不用改。理由是这两处已经是this.getUIContext().px2vp(...)和getMediaContentSync(...)。它说的没错——因为它看到的是修完之后的代码。它跑的时候第四节那 5 处已经改完了。这是纯读码分析的一个固有局限它不知道历史只能看到当前快照。第二半是 linter 一条都没报的两处ohos.deviceInfo旧导入路径。出现位置行原写法改为首页入口3import deviceInfo from ohos.deviceInfoimport { deviceInfo } from kit.BasicServicesKit异兽超分查看器6同上同上该文件第 3 行已从kit.BasicServicesKit导BusinessError合并为一行这个改动看着简单实际要处理一个细节超分查看器那个文件的顶部本来就有一行从kit.BasicServicesKit导入BusinessError新加的deviceInfo必须并进同一行不能另起一行重复导入同一个模块——重复导入在 ArkTS 里是编译错误不是警告。改完的导入区是这样import { image } from kit.ImageKit; import { imageSuperResolution, visionBase } from kit.CoreVisionKit; // 原先这里是 import { BusinessError } from kit.BasicServicesKit; // 外加一行 import deviceInfo from ohos.deviceInfo; 合并成一行 import { BusinessError, deviceInfo } from kit.BasicServicesKit; import { hilog } from kit.PerformanceAnalysisKit; import { window } from kit.ArkUI; // 下面还有一行导入工程自己的异兽插画资源表与本次迁移无关略调用处一行都不用动——deviceInfo.sdkApiVersion这种写法两种导入方式都支持改的只是deviceInfo这个标识符从哪儿来。这也是这类迁移不难却容易漏的原因它在调用处没有任何痕迹只有导入区那一行能看出来。它还给了个我没注意到的旁证同一个工程里另外两个文件早就是kit.BasicServicesKit的新写法只有这两处漏了。这个旁证的价值在于——如果全仓库都是旧写法那是时代问题只有两处漏了那是漏改后者才值得改。这个发现比多改两行更有价值的地方在于它说明了同一份代码里两种写法并存。新的kit.*聚合导入是后来的推荐写法旧的ohos.*分散导入并没有被标 deprecated所以 linter 完全不管。规则管不到这种风格漂移但读整个仓库能看出来。顺带一提kit.*这种聚合式导入是 API 12 之后主推的形态它把同一套能力收进一个 Kit 命名空间代价是从旧文档抄来的导入语句不会报错、只是不会更新。两条渠道的对照检查项linter编译期事实对话式 Agent读码分析px2vp×4✅ 5 条 deprecated❌ 已修完看不到ohos.deviceInfo×2❌ 一条不报✅ 抓到并给了同仓库旁证设备容量audio / media✅ 26 条未提及2in1 不可用接口✅ 1 条未提及两边都只看到一半而且是不重叠的那一半。linter 靠规则命中报得准、不漏它认识的项但只覆盖有规则的Agent 读码靠语义理解能发现不一致这种规则表达不出来的问题但看不见改动历史也拿不到编译器级别的设备能力清单。七、改完的验证导入改完全量搜一遍ohos.前缀只剩一条注释里提到ohos.measure实际导入路径清零。构建BUILD SUCCESSFUL in 21 s 822 ms0 ERROR门禁项目里 50 个verify-*.mjs全部通过模拟器phone 实例API 26安装成功 → 启动成功 → 日志 446 行中 0 条jscrash/UnhandledForegroundLifecycle EntryAbility与UIContent OnNewWant都在位进程稳定 3 分钟未重启模拟器画面停在了锁屏没取到应用界面截图。查了日志是模拟器实例的UpdateWindowVisible: 0不是应用没起来——ForegroundLifecycle和OnNewWant都在进程存活能证明的是装得上、起得来、不崩。界面截图这一环我没有硬凑。八、这套做法没覆盖的部分只查了主模块。主模块之外的代码没进这次扫描范围如果工程里还有别的模块迁移债统计不完整。os.*前缀的新 API 一个都没用。API 26 引入的新能力这次没碰这篇只处理旧用法要不要换不涉及新能力要不要用。对话式 Agent 的结论没有全部核实。它说px2vp已迁移是对的当时确实改完了但我没法假设它的每一条判断都准确——这次只采纳了它给的ohos.deviceInfo那两处并且逐条核对过行号和写法。AI 给的清单要当线索用不能当结论用。模拟器版本 vs 真机。模拟器是 phone 实例 API 26真机上低于 26 的会不会走到降级分支这件事靠模拟器验证不了。九、收个尾compatibleSdkVersion写多少决定了 linter 用哪一套规矩审你。装机门和检查口径是同一个字段的两副面孔——想看清 26 的债就得让检查站到 26 的位置上看完再把它放回去并且能证明放回去了。下一篇把这条线索往回接上架包怎么做到可追溯——签名、APP 构建、预检和版本回滚。