下载 App
打包是另一门手艺:Electron 与 aapt2 的四次翻车
2026-10-17(补记于 10-20)
代码能跑和能上架是两件事。这一章记四次翻车。
一、APK 全设备秒退
症状:TapPlay 上传后所有设备打开即闪退,日志里一行都不给。
原因查了很久:d8 只喂了 MainActivity.class,没喂匿名内部类 MainActivity$1~3。dex 缺类,真机 onCreate 直接崩。
诊断过程里的坑比 bug 本身更值得记:判断 dex 里有没有某个类,不能用 strings 搜类名。那只是符号表引用,类没打进去也能搜到。必须用 dexdump 数 Class descriptor 的数量。
现在 build-apk.py 里内置了自检:dex 类数必须等于 javac 产物数,不等就打包失败,不让问题流到真机上。
二、ESC 在包里失灵
症状:浏览器里 ESC 正常,Electron 打包后毫无反应。
原因在 Electron 文档里写着:before-input-event 里调用 preventDefault() 会连页面自己的 keydown / keyup 一起吃掉。想让 ESC 交给游戏处理,正确做法是什么都不做,直接放行;只有真正的全屏态才拦。
写反之后,浏览器正常、包里失灵,极容易被误判成引擎问题。
更深的教训是:Node 测试覆盖不到宿主容器这一层。input-check.js 的 ESC 断言是直接派发 DOM 事件,绕过了主进程。涉及容器的改动只能靠静态断言 + 用户实测。所以 build.py 里加了一条 check_main_js() 守着这个反向逻辑。
三、打包脚本把产出全删了
有一版 clean() 里写了 shutil.rmtree(DIST)。问题是 dist/ 既是 PC 包的出口,也是 APK 的出口,还放着手写的上传说明。一把清空,全没了。
规矩改成两条:
- 清理范围只能是本脚本自己的产出物,绝不能是整个 dist/
- 全程不删除,只覆盖
第二条还有环境原因:沙箱把 rmtree 和 os.remove 都劫持到回收站,回收站不可用时直接 fail-closed 抛错,整个打包流程被打断。所以 copytree(..., dirs_exist_ok=True) 覆盖,改 exe 名用 os.replace()。
四、第二次打包就 PermissionError
这条是补记的,今天又踩了一次。
用户给的音乐和视频文件是只读的(444)。shutil.copy2 会把只读属性一起带到目标 —— 第一次打包成功,第二次往同一个目标写就直接 PermissionError。
修法是所有拷贝走 force_copy():先 chmod(dst, 0o666) 再拷,拷完再置 666;copytree 要传 copy_function=force_copy。APK 往 D:/toolchain/apkbuild/ember/ 同步那一处同样要传。
四次翻车的共同点
打包脚本的失败往往发生在第二次运行。
第一次是全新环境,什么都不缺;第二次才碰到上次遗留的只读文件、残留目录、占着端口的旧进程。所以打包逻辑必须假设「目标已存在」,而不是假设「这是一张白纸」。
另外,两个平台的知识几乎没有交集:Electron 那边是事件拦截的生命周期,Android 那边是 dex 打包和资源路径。唯一共通的是 —— 都只能靠真机和用户实测,模拟环境测不出来。






