欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_Optiland

在这里插入图片描述

专业光学设计软件长期是 Windows 重型桌面应用的天下:几 GB 的安装包、分钟级的启动、专属的授权体系。我把"搭镜头 → 看光路 → 看像质"这条最核心的工作流完整搬到了 HarmonyOS PC 上,做成一款纯 ArkTS 的原生应用——「境界光学」:约 2000 行 ArkTS、数 MB 包体、权限表为空、秒开秒响应。

这篇文章的重点不在光学算法的教科书式推导,而在两个问题的交汇处:上架前的技术决策如何反哺审核与用户体验(包含把核心引擎从嵌入式运行时路线重写为纯 ArkTS 的完整翻译实录),以及AGC 资料怎么填、.app 包与软件包加密怎么取舍、提审之后市场侧发生了什么。文中代码均取自上架工程的真实源码,截图均来自真机、AGC 控制台与鸿蒙 PC 应用市场实拍。

先看最终成果——在鸿蒙 PC 的应用市场里搜"光学",它已经站在可以点击安装的位置:

鸿蒙 PC 应用市场上的境界光学

图1:上架后的应用市场详情页实拍。副标题"原生光学设计工具,轻量追迹",分类「工具」,年龄分级 3+,支持电脑。截图时正值上架初期:安装 <1万次、评分评论尚在积累——这是每个新应用都走过的起点。

一、作品速览:境界光学是什么

境界光学是一款面向 HarmonyOS PC(2in1)与平板的原生光学设计工作台。它把专业软件的核心工作流收敛成一屏:

  • 镜头数据编辑器:类 CAD 表格,物面 → 折射面 → 光阑 → 像面,逐面编辑曲率半径、厚度、玻璃材料、圆锥系数;单元格点击进入编辑态,失焦或回车提交,模型即时重算;
  • 系统视图 2D:Canvas 实时绘制透镜轴向截面与三视场光束,主光线精确穿过光阑中心,渐晕与全反射处正确截断;
  • 像质分析:点扩散图(RMS 半径)、F/d/C 三色色差叠绘、场曲与畸变曲线,EFL/EPD 实时汇总;
  • 玻璃库:内置 8 种 Schott 常用玻璃,Sellmeier 三项色散公式逐面取折射率;
  • 工程管理:镜头 JSON 持久化、一键加载演示双高斯镜头(f/5、半视场 14°)、重置空白、首启导览。

真机运行实拍——加载演示双高斯后,13 个面的镜头结构与三视场光束一屏呈现,分析区同步完成 254 条光线三色追迹:

境界光学真机运行实拍

图2:鸿蒙 PC 真机运行。左侧镜头数据编辑器 + 系统视图 2D,右侧分析区与信息控制台,右下角状态实时输出每一步操作

支撑这一切的是约 980 行的纯计算引擎(OpticalModel.ets)加约 1000 行的工作台页面(Index.ets),合计约 2000 行 ArkTS,没有任何 .so、没有任何第三方依赖。以核心的向量形式斯涅尔折射为例(含全内反射判定):

// OpticalModel.ets:折射方向 d 在命中点 p 处按 n1 -> n2 折射
// 返回折射后方向;发生全内反射(TIR)时返回 null
private refract(p: Vec3, d: Vec3, nrm: Vec3, n1: number, n2: number): Vec3 | null {
  // 保证法线与光线入射方向相对
  const nd = nrm.dot(d);
  let n = nrm;
  if (nd > 0) {
    n = new Vec3();
    n.x = -nrm.x; n.y = -nrm.y; n.z = -nrm.z;
  }
  const cosI = -(n.dot(d));
  const sinT2 = (n1 / n2) * (n1 / n2) * (1 - cosI * cosI);
  if (sinT2 > 1) {
    return null; // 全内反射
  }
  const cosT = Math.sqrt(1 - sinT2);
  const eta = n1 / n2;
  const out = new Vec3();
  out.x = eta * d.x + (eta * cosI - cosT) * n.x;
  out.y = eta * d.y + (eta * cosI - cosT) * n.y;
  out.z = eta * d.z + (eta * cosI - cosT) * n.z;
  const L = out.len();
  out.x /= L; out.y /= L; out.z /= L;
  return out;
}

折射公式本身只有十几行,但它被求交、光阑对准、色散、采样等环节复用了同一条代码路径——这正是"把引擎做小做纯"的关键。引擎的其余几块积木(玻璃色散、曲面求交、主光线光阑对准、近轴光路、光瞳采样)在第二节的上架决策叙事里逐段展开。

二、上架前最重要的决策:从嵌入式路线重写成纯 ArkTS

境界光学并不是一开始就长这样。它有一个同源的早期探索版本,走的是常见的"引擎迁移"路线:

  • 交叉编译的 CPython 3.12 静态库整链链接进 liboptiland_native.so(约 22 MB);
  • 首次启动把几十 MB 的 Python 运行时压缩包解压到应用沙箱;
  • 每次分析通过 NAPI 拉起子进程执行 PyRun_SimpleString,回捕 stdout 拿结果。

功能是通的,但放到上架的视角下,这条路有三个硬伤:

维度嵌入式运行时版纯 ArkTS 版(上架版)
首次启动解压运行时,约 1 分钟秒开
每次编辑响应进程级调用 5–10 s< 1 ms,感知为即时
包体数百 MB(运行时 + 库)数 MB
二进制构成22 MB .so + 运行时文件无任何 .so
权限至少文件/进程相关空
审核风险面子进程、动态解压、大包体几乎为零

第一行是用户体验问题,第二行直接决定审核员和普通用户的第一印象——一个点一下"刷新光路"要等 5 秒的应用,很难在几分钟的审核窗口里证明自己"功能正常"。第三行则牵扯到上架环节一个容易被忽略的细节:软件包加密(后文第四节展开)。

于是下决心重构:把光学引擎从 Python 语义逐段翻译成 ArkTS。下面按模块给出翻译实录——这既是技术记录,也是"为什么纯 ArkTS 路线对上架更友好"的论据。

翻译实录①:玻璃库与色散——一张表解决折射率

上游 Python 库的玻璃模型很完整,但对"顺序追迹 + 三色分析"这条工作流,一张 Sellmeier 系数表就够了。每块玻璃携带三项 Sellmeier 系数;没有完整系数的玻璃,由目录值 n_d / 阿贝数 V 经 Cauchy 拟合导出——d 光处精确、F/C 色散与阿贝数定义一致:

// Sellmeier: n^2 - 1 = B1*l^2/(l^2-C1) + B2*l^2/(l^2-C2) + B3*l^2/(l^2-C3)
// l 单位为微米;Cauchy 拟合:n(l) = cauchyA + cauchyB / l^2
export class Glass {
  name: string = '';
  b1: number = 0; b2: number = 0; b3: number = 0;
  c1: number = 0; c2: number = 0; c3: number = 0;
  cauchyA: number = 0;
  cauchyB: number = 0;

  indexOf(l: number): number {
    if (this.cauchyB !== 0) {
      return this.cauchyA + this.cauchyB / (l * l);
    }
    const l2 = l * l;
    const n2 = 1 +
      this.b1 * l2 / (l2 - this.c1) +
      this.b2 * l2 / (l2 - this.c2) +
      this.b3 * l2 / (l2 - this.c3);
    return Math.sqrt(n2);
  }
}

// 由目录 n_d 与阿贝数 V 构造玻璃:d 光精确,F-C 色散用 Cauchy 项匹配
export function glassFromNdV(name: string, nd: number, abbeV: number): Glass {
  const g = new Glass();
  g.name = name;
  const lF = 0.4861327;            // F 线 486.13 nm
  const lC = 0.6562725;            // C 线 656.27 nm
  const lD2 = 0.5875618 * 0.5875618;
  const dispF = 1 / (lF * lF) - 1 / (lC * lC);
  g.cauchyB = (nd - 1) / (abbeV * dispF);
  g.cauchyA = nd - g.cauchyB / lD2;
  return g;
}

GLASS_TABLE 里 N-BK7、N-SF11、N-LAK21、N-SK16、N-SF57 五种用完整 Sellmeier 系数,N-SSK2、N-SK2、F5 三种用 glassFromNdV 导出——内置玻璃库共 8 种 + 空气,UI 材料列的下拉框直接消费 glassNames()。整个"色散系统"不到 100 行 ArkTS。

翻译实录②:光线-曲面求交——解析解打底,牛顿法收尾

面形支持球面、平面与二次圆锥面(Conic 常数 K)。求交分两层:先按球面做解析求交(球心在 (0, 0, zc + R),与 sag 公式的符号约定对齐),若面带圆锥系数,再沿光线方向做牛顿迭代精修:

// 球面解析求交:解 |p - center| = |R|,取最小的非负传播参数 t
const c = 1 / R;
const cz = zc + R;                       // 球心轴向位置
const ox = o.x, oy = o.y, oz = o.z - cz;
const A = dir.dot(dir);
const B = 2 * (ox * dir.x + oy * dir.y + oz * dir.z);
const C = ox * ox + oy * oy + oz * oz - R * R;
const disc = B * B - 4 * A * C;
if (disc < 0) { return null; }           // 光线未打到面
const sq = Math.sqrt(disc);
let t1 = (-B - sq) / (2 * A);
let t2 = (-B + sq) / (2 * A);
// 容差处理从面顶点出发的光线(t == 0 的情况)
const EPS_T = -1e-6;
let t = -1;
if (t1 > EPS_T && (t2 <= EPS_T || t1 <= t2)) {
  t = t1;
} else if (t2 > EPS_T) {
  t = t2;
} else {
  return null;
}

圆锥面修正的残差函数是 F(p) = z − zc − sag(ρ),其中 sag(ρ) = c·ρ² / (1 + √(1 − (1+K)·c²·ρ²)),牛顿步沿光线方向推进(最多 8 次,步长分母做 1e-14 下溢保护)。平面(radius = inf 且 conic = 0)退化为线性求交。渐晕判定也在这里统一收口:命中点超出半口径(semiDiameter)即判渐晕,光线终止——引擎与 2D 绘制两端共用同一判定,保证图和分析结果一致。

翻译实录③:主光线光阑对准——简易演示最容易省略的一步

对离轴视场,如果直接让主光线从入瞳中心出发,它到达光阑的位置几乎必然偏离光阑中心,场曲、畸变的计算基准就全歪了。真实光学软件的解法是"主光线光阑对准":反解一个入瞳高度 y,让主光线恰好穿过光阑中心。这是残差函数与割线迭代:

// 反解入瞳 y 偏移,使该视场的主光线穿过光阑中心
// g(py):从 (0, py) 出发追迹到光阑面,返回命中点与光阑中心偏差
const tanT = Math.tan(th);
const g = (py: number): number => {
  const d = new Vec3();
  d.y = Math.sin(th); d.z = Math.cos(th);
  const o = new Vec3();
  o.y = py; o.z = 0;
  let nCur = 1;
  for (let i = 1; i <= idxStop; i++) {
    const s = this.surfaces[i];
    const p = this.intersectSurface(o, d, s, zs[i]);
    if (p === null) { return py + tanT * zs[i]; }  // 直线外推兜底
    if (i === idxStop) { return p.y; }             // 抵达光阑面:返回偏差
    const nNext = s.isAir() ? 1 : this.refrIndex(s, this.primaryLambda);
    const nrm = this.surfaceNormal(p, s, zs[i]);
    const d2 = this.refract(p, d, nrm, nCur, nNext);
    if (d2 === null) { return p.y; }               // TIR 兜底
    d.x = d2.x; d.y = d2.y; d.z = d2.z;
    nCur = nNext;
    o.x = p.x; o.y = p.y; o.z = p.z;
  }
  return 0;
};
// 割线迭代:初值按无折射直线近似,10 步内收敛到 1e-10
let a = -tanT * zs[idxStop];
let b = a + 0.5;
let fa = g(a);
let fb = g(b);
for (let k = 0; k < 10 && Math.abs(fb) > 1e-10; k++) {
  if (Math.abs(fb - fa) < 1e-14) { break; }
  const c = b - fb * (b - a) / (fb - fa);
  a = b; fa = fb;
  b = c; fb = g(c);
}
return b;

注意 g(py) 内部走的求交/折射代码与普通光线完全相同——对准迭代不引入任何平行实现,这是保证"对准后的主光线"与"画出来的主光线"一致的结构性约束。大视场强渐晕下迭代可能不收敛,此时该视场光线被安全截断(点列图上表现为该视场点数减少),而不是抛错。

翻译实录④:近轴光路与像面定位——y-nu 追迹十几行搞定

像面放在哪?答案不是"最后一个面之后随便放",而是吸附到近轴后焦点。用经典 y-nu 追迹(n'u' = n·u − y·φ,φ = (n₂−n₁)/R)从入瞳边缘出发,追一条轴上边缘光线,出射后与光轴的交点即焦点:

// 近轴后焦距:追迹轴上边缘光线 (y=h, u=0),焦点在最后一次折射后光线与光轴交点
paraxialFocusZ(): number {
  const zs = this.surfaceZs();
  let y = this.entryBeamRadius;
  let u = 0;
  let nCur = 1;
  let zRef = 0;                    // 最后一个折射面的轴向位置
  for (let i = 1; i < this.surfaces.length; i++) {
    const s = this.surfaces[i];
    if (s.isImage) { break; }
    const gap = this.surfaces[i - 1].thickness;
    if (Number.isFinite(gap)) {
      y = y + u * gap;             // 传输:y' = y + u·gap
    }
    const nNext = s.isAir() ? 1 : this.refrIndex(s, this.primaryLambda);
    const R = s.radius;
    const phi = (Number.isFinite(R) && R !== 0) ? (nNext - nCur) / R : 0;
    u = (nCur * u - y * phi) / nNext;   // 折射:n'u' = n·u − y·φ
    nCur = nNext;
    zRef = zs[i];
  }
  if (Math.abs(u) < 1e-12) { return zRef; }   // 无偏折兜底
  return zRef + y / (-u);                     // 与光轴交点
}

有效焦距 EFL 由边缘光线出射角反解,入瞳直径 EPD 取入瞳面光束直径(2 × entryBeamRadius)——分析区顶部的三个汇总数全部来自这条十几行的追迹,而不是独立再写一套"近轴子系统"。

翻译实录⑤:光瞳采样与点扩散图——hexapolar 网格 + 主光线基准

点扩散图的光瞳采样用六角极坐标网格(hexapolar):中心 1 点 + 逐环 6·ring 点。每个视场先经主光线对准拿到基准入瞳高度,再逐采样点追迹、收集像面坐标:

// 六角光瞳采样:中心 + numRings 环,每环 6·ring 个点
hexapolar(numRings: number): number[][] {
  const pts: number[][] = [];
  pts.push([0, 0]);
  const r = this.entryBeamRadius;
  for (let ring = 1; ring <= numRings; ring++) {
    const n = ring * 6;
    for (let k = 0; k < n; k++) {
      const a = 2 * Math.PI * k / n;
      pts.push([r * ring / numRings * Math.cos(a),
                r * ring / numRings * Math.sin(a)]);
    }
  }
  return pts;
}

// 点扩散图:逐视场追迹全部光瞳采样,返回像面坐标
spotDiagram(numRings: number, imageZ: number): SpotFieldData[] {
  const out: SpotFieldData[] = [];
  const pupil = this.hexapolar(numRings);
  for (const f of this.fieldAngles) {
    const baseY = this.aimChiefY(f);          // 主光线对准基准
    const pts: number[][] = [];
    for (const smp of pupil) {
      const hits = this.traceRay(f, smp[0], smp[1] + baseY,
                                 this.primaryLambda, imageZ);
      if (hits !== null && hits.length > 0) {
        pts.push([hits[hits.length - 1].x, hits[hits.length - 1].y]);
      }
    }
    // ...逐视场收集,RMS 由质心距离统计得出
  }
  return out;
}

色差分析复用同一条追迹链路:把 primaryLambda 换成 F/d/C 三个波长,逐波长重取折射率(玻璃库的 indexOf 在此兑现),叠色绘制并计算 F-C 垂轴色差。一套追迹引擎支撑三种分析模式,这就是"翻译成纯 ArkTS"后结构带来的复利。

工程结构自证:没有 cpp 目录的 ArkTS 工程

翻译完成后,工程结构本身就是"纯 ArkTS"的结构性证据:

ohos_Optiland_ArkTS/
├── AppScope/app.json5                  # bundleName: org.optiland.arkts
├── entry/src/main/
│   ├── ets/
│   │   ├── entryability/EntryAbility.ets
│   │   ├── model/OpticalModel.ets      # 光学引擎(纯计算,约 980 行)
│   │   └── pages/
│   │       ├── Index.ets               # 主工作台(约 1000 行)
│   │       └── About.ets               # 介绍页
│   ├── resources/                      # 图标 / 颜色 / 字符串
│   └── module.json5                    # requestPermissions: []
├── build-profile.json5
├── LICENSE                             # MIT
└── README.md

没有 cpp 目录、没有 CMakeLists、没有 NAPI 桥、没有运行时压缩包——对比早期版本"交叉编译工具链 + 桥接层 + 运行时打包"三件套,工程复杂度直接归零。这也为第三节的"权限表为空"和第四节的"软件包加密全覆盖"铺平了道路。

三、上架前的技术准备:五件事

上架不是"打包上传",下面这些决定要在写代码阶段就做对:

1. 包名要想好,上架后改不了。 境界光学的包名是 org.optiland.arkts(AppScope/app.json5):

{
  "app": {
    "bundleName": "org.optiland.arkts",
    "versionCode": 1010000,
    "versionName": "1.1.0",
    "icon": "$media:app_icon",
    "label": "$string:app_name"
  }
}

包名在 AGC 创建应用时绑定,一旦上架就锁死——签名证书、市场身份全跟着它走。早期探索版用的是另一个包名,借着重构的窗口期一起换掉了。重构期是改包名的最后机会,过了这村就没这店。

2. versionCode 定一套编码规则。 我用"主版本占高位、次版本次之"的十进制编码:1.0.0 → 1000000,1.1.0 → 1010000,1.2.0 → 1020000。每次提审 AGC 都要求 versionCode 严格递增,规则定早了,后面发版不用现想。

3. deviceTypes 别漏了电脑。 鸿蒙 PC 走的是 2in1 设备类型,module.json5 里必须声明:

// module.json5(节选)
"deviceTypes": [ "2in1", "tablet" ],
"requestPermissions": []

漏了它,PC 端应用市场直接搜不到你的应用,其他一切白做。

4. 权限表做到极致——直接为空,文件持久化交给系统 picker。 大多数应用至少留一条 INTERNET,境界光学连这条都没有:镜头文件的保存与加载全部通过系统 DocumentViewPicker 弹出原生对话框,由用户在对话框里授权位置,应用既不申请存储权限,也不感知任何全局文件系统:

// Index.ets:镜头保存——picker 授权 + fd 写入 + fsync 落盘
private async saveLens(): Promise<void> {
  let file: fs.File | null = null;
  try {
    const json = this.model.toJSON();
    const options = new picker.DocumentSaveOptions();
    options.newFileNames = ['lens.json'];
    options.fileSuffixChoices = ['JSON 文件|.json'];
    const pickerView = new picker.DocumentViewPicker();
    const result = await pickerView.save(options);   // 系统另存为对话框
    if (!result || result.length === 0) {
      this.statusText = '已取消保存';
      return;
    }
    file = fs.openSync(result[0],
      fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);
    fs.writeSync(file.fd, json);
    fs.fsyncSync(file.fd);                           // 强制落盘
    fs.closeSync(file);
    file = null;
    this.statusText = '已保存';
  } catch (e) {
    if (file !== null) { try { fs.closeSync(file); } catch (_) {} }
    this.statusText = '保存失败';
  }
}

requestPermissions: [] 意味着隐私声明可以写成一句"本应用不收集任何个人信息",隐私政策、个人信息处理规则这些材料统统化繁为简。零权限不只是过审技巧,本来就是本地工具类应用该有的样子。

5. 签名必须用发布材料。 平时 hdc 直装的自动签名是 debug Profile,而 AGC 的提审通道要求发布证书 + 发布 Profile 签出的包。材料在 AGC 上按包名申请,在 DevEco Studio 里配置进 signingConfigs 后执行 Build > Build App(s)/Hap(s) > Build App(s),产出 ohos_Optiland_ArkTS-default-signed.app。用错材料最典型的症状就是 hdc 安装报 9568322(not trusted app source)——调试包和上架包是两个世界的东西。

四、AGC 资料填写实战

在 AGC 控制台(HarmonyOS 开发者中心)创建应用后,上架资料分两块:应用信息和版本信息。

应用信息:让审核员三秒看懂你是谁

应用信息各字段的填法与理由:

字段填法理由
应用名称境界光学四字中文名,好搜索、好记忆
副标题原生光学设计工具,轻量追迹技术卖点直接进副标题
分类工具与功能严格一致,错类是打回项
年龄分级3+无社交、无支付、无用户生成内容,分级自然低
应用介绍三句话讲清"是什么、为谁、为什么快",再列功能亮点首屏决定转化,市场页"更多"之前只显示开头
隐私声明不收集任何个人信息零权限、无网络、无账号体系的直接推论
应用介绍图真机界面实拍加工,3 张对用户是转化率,对审核是功能演示

三张介绍图的叙事线是"从零搭建 → 引擎特性 → 一屏掌控":

应用介绍图一:从零搭建你的第一支镜头

图3:介绍图一——新手视角:输入曲率与厚度、下拉选玻璃,光路与像质即时刷新

应用介绍图二:纯原生引擎,秒开秒响应

图4:介绍图二——技术特性:"ArkTS 本地追迹计算,无外部运行时依赖"直接写在图上

应用介绍图三:从面数据到光路,一屏掌控

图5:介绍图三——专业视角:13 面镜头结构、三视场光束、RMS 与色差同屏

这三张图同时承担两个职能:对用户是转化率,对审核员是"功能演示"——每张图都是应用真实界面的实拍加工,审核时点开应用能一一对应。

版本信息:上传 .app 包与加密取舍

版本信息页是上架的核心操作台:上传签名好的软件包,AGC 自动解析出版本号:

AGC 版本信息页实拍

图6:AGC 版本信息页。包名 org.optiland.arkts,软件包 ohos_Optiland_ArkTS-default-signed.app,版本 1.1.0 (1010000),下方依次是软件包加密与发布国家或地区

这一页有三个值得展开的点:

① 上架推荐用 .app 包,不只是 .hap。 日常开发接触最多的是 .hap(模块包),而 .app 是应用级的发布包(App Package),由 DevEco 的 Build App(s) 产出。单 HAP 应用两种格式都能提审,但 .app 是应用市场分发的标准形态,配合签名与加密体系走完整链路。

② 软件包加密:选"加密(推荐)",但要看懂保护边界。 AGC 的加密选项旁边有一行容易被忽略的提示,大意是:包内 appx 格式的文件支持加密,.so 格式文件不支持加密、会明文打进应用包,若涉敏感逻辑还需额外加固。这正是第二节重构的红利兑现时刻——境界光学整个包里没有任何 .so,全部业务逻辑都是编译后的 ArkTS 字节码,加密保护范围天然覆盖了全部核心代码。而如果走嵌入式运行时路线,22 MB 的 liboptiland_native.so 连同整套光学算法恰恰落在保护范围之外。同一个功能需求,技术路线的不同选择在上架环节给出了不同的安全水位。

③ 发布国家或地区:按需勾选 AppGallery 覆盖范围。工具类本地应用没有地域合规负担,选全量即可。

五、提审之后:状态流转与市场侧实拍

提交后状态流转很清晰:准备提交 → 审核中 → 已上架。图6 截下的正是"准备提交"状态的版本信息页;过审后侧栏会亮起"已上架",控制台可下载审核报告。

市场侧随之生效:搜索"光学"能找到(图1),详情页展示安装量、年龄分级、分类、预览图与完整介绍。刚上架时的数据不必遮掩——安装 <1万次、暂无评分评论,这是所有新应用的真实起点;对工具类应用,接下来拼的是搜索词命中、介绍图转化与真实口碑积累。

审核视角:你的应用会在哪几秒被"审判"

审核员点开应用后的头几十秒,基本决定了结论。零权限纯 ArkTS 应用在这几十秒里的表现是可以提前设计的:

  • 启动即用:无运行时解压、无 .so 加载、无 IPC 握手,主页面直接可交互——首启只多一层可关闭的功能导览;
  • 每个按钮都有真实响应:工具栏六个操作(保存/加载/重置/示例/增加面/删除面)全部接了真实现,没有占位项;
  • 坏输入不崩:表格单元格解析对 inf、空串、非法数字统一防御;JSON 加载在 try/catch 中进行并做字段校验,坏文件给出"文件格式无效"而不是闪退:
// Index.ets:加载镜头——坏文件必须止步于一条状态栏提示
const decoder = util.TextDecoder.create('utf-8');
const json = decoder.decodeToString(new Uint8Array(buffer, 0, bytesRead));
if (this.model.loadFromJSON(json)) {
  this.consoleOut += `[文件] 已从所选文件加载 ${result[0]}\n`;
  this.afterModelEdit();
} else {
  this.consoleOut += '[文件] 加载失败:JSON 格式无效\n';
  this.statusText = '文件格式无效';
}
  • 异常有出口:光瞳采样、割线迭代都有兜底分支,极端输入下宁可"该视场点数减少"也不抛未捕获异常。

这些不是上架前临时补的功课,而是第二节那次重构顺带完成的:单进程、零桥接的结构,天然消灭了"子进程拉不起来""运行时解压失败"这类只能在真机上暴露的审核风险。

两个提审经验:

  1. 过审不等于零关注点。 即便通过,审核报告里也可能附"建议关注"的事项,提审当天就把报告下载下来读完,别等下个版本提审时被翻旧账;
  2. 上架以控制台状态为准。 市场页面随分发节奏刷新,刚过审时偶尔搜不到,等一会儿或用包名验证。

六、上架之后:下一个版本已经在路上

上架不是终点,而是迭代闭环的起点。侧栏里"新版本 - 准备提交"与"已上架"并存的时刻,就是这套闭环运转的样子。发新版本的流程,是把第三节的纪律原样重复一遍:

  1. 改 versionName,versionCode 按编码规则递增;
  2. 发布证书签出新包;
  3. AGC 上传新包、更新"新功能"文案、提审;
  4. 过审后市场自动向已安装用户推送更新。

对已上架用户,升级无感完成——这正是市场渠道相对"发安装包让用户自己装"的核心价值,也是个人开发者能持续触达用户唯一正门。

而代码侧的迭代方向也已经排好:自定义玻璃条目(用户自带 Sellmeier/Cauchy 系数)、偶次非球面高次项、MTF 与包围能量曲线、撤销/重做与多镜头标签页。因为引擎是模型/视图分离的纯计算层,这些能力都能在不引入新权限、新原生依赖的前提下演进——保证下一个版本的提审材料和这一个一样干净。

七、上架踩坑清单(浓缩版)

#坑结论
1debug 签名包拿去提审,安装报 9568322上架必须 AGC 发布证书 + 发布 Profile 签名
2包名上架后想改改不了,重构期是换包名的最后窗口
3deviceTypes 只写 tabletPC 端必须声明 2in1,否则 PC 市场不可见
4versionCode 忘了递增AGC 直接拒收新包,编码规则早定
5.so 以为跟着包一起加密加密只覆盖 appx 文件,.so 明文打包;敏感逻辑上移 ArkTS 或另做加固
6首启解压运行时 + 子进程调用审核窗口内"功能响应慢"是硬伤,高频交互逻辑尽量本地化、进程内化
7存储权限一把梭读写文件优先 DocumentViewPicker 用户授权制,能空着权限表就空着
8每个分析模式一套独立代码多套并行实现迟早算出互相矛盾的结果;一套引擎 + 参数化波长/采样才是正解
9隐私政策链接 404必须常驻可访问;零权限应用材料最简
10只看"已通过"不看审核报告关注点会累积,下一版本前提要处理掉

八、常见问题 QA

Q1:个人开发者上架鸿蒙 PC 应用,需要企业资质吗?

不需要。本文的上架全程使用个人开发者账号完成。工具类应用按 AGC 提示准备材料即可(分类资质、隐私政策页);像境界光学这种 3+ 分级、无社交、无支付、无用户生成内容的本地工具,是个人开发者材料负担最小的品类。

Q2:.hap 和 .app 到底上传哪个?

单 HAP 应用两种格式 AGC 都能解析提审,但 .app 是应用级发布包(App Package),是应用市场分发的标准形态,配合签名与加密体系走完整链路。建议直接用 DevEco 的 Build > Build App(s) 产出 .app 提审——本文即按此路径走通。

Q3:软件包加密选"不加密"会不会更省事?

不会,"加密(推荐)"没有额外成本,选它即可。真正要看懂的是保护边界:加密只覆盖包内 appx 格式文件,.so 会明文打进应用包。纯 ArkTS 应用全部业务逻辑都是编译后的 ArkTS 字节码,加密天然覆盖全部核心代码;含 .so 的应用,敏感逻辑要么上移到 ArkTS 层,要么另做加固。

Q4:调试用的 debug 签名包直接传 AGC 行不行?

不行。AGC 提审通道校验发布签名材料,debug 包(自动签名 Profile)拿去分发,安装报 9568322(not trusted app source)。正确路径:在 AGC 按包名申请发布证书 + 发布 Profile,配置进 DevEco 的 signingConfigs 后重新出包。调试包和上架包是两个世界的东西。

Q5:零权限应用怎么保存/打开用户的文件?

用系统 DocumentViewPicker:弹出原生对话框由用户选择位置并授权,应用拿到 URI 后经 fd 读写(写入后 fsyncSync 强制落盘)。全程不申请存储权限、不感知全局文件系统。境界光学的镜头保存/加载就是这个实现,这也是权限表能清成空数组的前提。

Q6:包名中途想换怎么办?

上架前随时可以换(改 app.json5 的 bundleName + 重新签名 + AGC 重建应用),上架后锁死。境界光学早期探索版用的是另一个包名,就是借重构窗口换成了 org.optiland.arkts——要换趁早。

Q7:为什么把光学引擎手工翻译成 ArkTS,而不是继续嵌 Python 运行时?

看本文第二节的对比表:镜头编辑是典型的高频小计算场景,进程内重算 < 1 ms,而嵌入式路线每次分析要走 NAPI 子进程、5–10 s——审核窗口里"点一下等 5 秒"是硬伤。嵌入式路线的价值在"必须复用全量第三方库"的场景(优化器、MTF 等),两条路线互补而不互替;本应用的核心工作流恰好不需要那些全量能力。

Q8:主光线光阑对准是什么?不做会怎样?

离轴视场的主光线如果不做对准,它到达光阑面的位置几乎必然偏离光阑中心,后续场曲、畸变、点列的计算基准全部歪掉。主流简易演示程序常省略这一步,真实光学软件必备。实现上是对"入瞳高度 → 光阑偏差"这个残差函数做割线迭代(10 步内收敛到 1e-10),关键是迭代内部复用与普通光线完全相同的求交/折射代码路径,不引入平行实现。

Q9:提审通过后,市场里多久能搜到?

以 AGC 控制台"已上架"状态为准。市场页面随分发节奏刷新,刚过审时按应用名搜偶尔搜不到,等一会儿,或直接按包名验证应用是否已生效。

Q10:后续版本怎么推给已安装用户?

四步闭环:改 versionName、按编码规则递增 versionCode → 发布证书签出新包 → AGC 上传新包、更新"新功能"文案、提审 → 过审后市场自动向已安装用户推送更新,用户无感升级。这正是走市场渠道相对"发安装包让用户自己装"的核心价值。

九、结语

回头看这次上架,AGC 后台实际花掉的时间不多,真正的功夫都在写代码之前的两个决定上:把核心引擎做成进程内纯 ArkTS(而不是背着一个嵌入式运行时),把权限表清成空数组(而不是留一条"反正要用"的网络权限)。这两个决定让包体从数百 MB 降到数 MB、启动从分钟级降到秒级、加密保护覆盖全部业务代码、隐私材料简化成一句话——审核环节几乎不存在可挑剔的攻击面。

对想在鸿蒙 PC 上做工具类应用的开发者,一句话总结:上架的难度,在你按下"创建工程"的那一刻就大致决定了;AGC 只是替你把这份答卷誊抄一遍。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐