/2,085 字/阅读约 8 分钟

安卓 WebView 薄壳踩坑记:模拟器出网、渲染闪退与消失的图标

把一个博客装进 14 KB 的安卓壳只花了大半天,但让它在一台模拟器上真正跑起来,花掉的时间比写壳还多。这篇不谈「怎么造壳」,只记路上踩的五个坑:模拟器出不了网、渲染模式静默回退、宿主内存紧张把模拟器拖垮、装完桌面没有图标、以及一个被样式优先级吃掉的按钮。它们的共同点是几乎都不报错。

眠月眠月
joker1point
本文要点

5 个坑 · 0 条报错日志 · 3 个来自模拟器 · 最深的那个只改了 1 行

  • 坑一 · 出不了网:模拟器被宿主机的代理环境拦死,平台的启动参数救不了 WebView,得设系统级的全局代理。
  • 坑二、三 · 模拟器消失:渲染模式静默回退,叠加宿主机内存紧张,会让模拟器无声消失或界面卡死。
  • 坑四 · 没有图标:LAUNCHER 常量写错一个单词,应用能装能跑,桌面却永远没有入口。
  • 坑五 · 按钮看不见:流水线的自动格式化与 CSS 优先级各吃掉一次,构建全绿而人眼无感。

上一篇讲的是怎么把博客装进一个 14 KB 的安卓壳。这一篇只记路上踩的坑。

调试中最难受的不是「报错」,而是「不报错」——构建全绿、安装成功、进程还在,可功能就是没到用户手里。下面这五个坑,几乎都长这样。

坑一:模拟器的网络被宿主机的代理「拦死」

现象很直接:模拟器里连 ping 一个公网地址都百分之百丢包,DNS 解析报未知主机,应用里的 WebView 直接抛 ERR_NAME_NOT_RESOLVED。表面看像模拟器「没网」。

根因在我自己的机器上:我平时用一类代理软件上网,它只监听本机的回环地址。而模拟器是通过 NAT 出网的,它的流量压根不经过宿主机的回环——于是模拟器自以为在联网,实际上所有请求都被丢掉了。

解法不是去改模拟器启动参数(我试过,那个 -http-proxy 选项对 WebView 用的浏览器内核不生效),而是直接在安卓系统里设一个全局代理,把它指向宿主机的回环:

bash
adb shell settings put global http_proxy 宿主机别名:代理端口

这个「宿主机别名」是安卓模拟器内网里一个保留地址——从模拟器看过去,它就代表那台跑着代理软件的物理机。设完之后,博客首页在 WebView 里完整渲染了。

模拟器出网路径对比:默认 NAT 直连被宿主代理环境拦住,改用指向宿主机回环别名的全局代理后成功出网
图 1 · 模拟器出网的两条路径:默认 NAT 直连失效,全局代理指向宿主回环别名才通

坑二、三:模拟器会「凭空消失」,系统界面会卡死

调试过程中模拟器还崩过好几次,每次都是进程无声无息地没了,日志里找不到一句报错。查下来是两件事叠加:

一是渲染模式选错了。我一开始指定了 angle_indirect,但这个版本的模拟器已经不认它,于是静默回退到「用宿主显卡直通」——在宿主机内存本就紧张的情况下,这条路最容易把图形进程拖垮。换成纯软件渲染 swiftshader_indirect 之后稳定多了。

二是内存。我一开始用的是带应用商店的重型系统镜像,加上模拟器本身要吃两三个 GB,宿主机一紧张,系统的界面进程就报「无响应」。对策很土但有效:把模拟器的内存和 CPU 核数调小,遇到弹窗先点「等待」而不是「关闭」——点关闭反而会把界面进程杀掉,整台模拟器跟着一起走。

模拟器调试三个现象的根因与对策对照:渲染模式静默回退、启动参数对 WebView 不生效、内存紧张导致界面无响应
图 2 · 三个把调试卡住的现象,各自的根因与对策

这三个坑的共同点值得单拎出来说:它们全都不报错。 模拟器消失时没有堆栈,渲染回退时没有警告,界面卡死时应用日志干干净净。如果只盯着「应用日志」,你会把「环境的问题」误判成「代码的问题」。真正的线索在别处——模拟器自己的启动输出、宿主机的可用内存、以及那个「无响应」弹窗。

真机上不会有这一组坑。这是我为「用模拟器代替真机」付的学费。

坑四:装完之后,桌面上找不到图标

第一次装完,我发现了两个「图标找不到」的问题,一个是我自己犯蠢,一个是真 bug。

第一个是概念错位。 从浏览器点「下载」,文件只是进到了「下载」目录,那叫下载,不叫安装。用户需要在文件管理器里再点一次、确认「允许未知来源」,它才会真正装到桌面。这是安卓的安全设计,不是应用的问题——但它足以让第一次接触的人一脸懵。

第二个是真 bug。 我满怀期待地装完、打开,一切正常,可桌面上死活找不到它的图标,只能从别处把应用「启动」出来。查下去发现根因藏在清单文件的一行常量里:

xml
<!-- 写错的版本:把 category 误写成了 action -->
<category android:name="android.intent.action.LAUNCHER" />

我把 category 的名字误写成了 android.intent.action.LAUNCHER——听起来差不多,但系统只认 android.intent.category.LAUNCHER。这一个单词的差别,让包管理器从未注册过任何桌面入口:应用能装、能跑、能从命令启动,就是不在桌面上留痕。

改掉这一行、把版本推到 1.0.1,图标立刻正常。

这类 bug 为什么难被发现

它不报错、不崩溃,构建全绿、安装成功、启动正常——所有「机器能验证的环节」都通过了。唯独用户第一眼要看的那个图标,是缺的。「程序跑起来了」和「功能对用户可见」,从来不是一回事。

坑五:看得见的按钮,被看不见的优先级吃掉

上线之后,关于页那个「下载 APK」按钮又返工了两次:先是按钮的样式在发布流水线里被排版工具重排、流水线直接判红;改完之后,按钮文字又因为 CSS 选择器的优先级被一条更「具体」的规则盖成了「紫底紫字」——点得到,但看不见。

两个问题都是「构建通过、页面 200、但人眼看不清」。它们不来自代码逻辑,而来自两套规则的碰撞:一层是流水线的自动格式化,一层是样式表的优先级。

修完之后

把上面这些坑一个个填上,四种状态终于在真实运行里全部成立——下面是同一台模拟器上的实拍:

模拟器实拍三联图:左侧为博客首页完整渲染,中间为壳内打开的文章页,右侧为断网兜底提示页
图 3 · 同一台模拟器上的三种状态:首页渲染、内链文章、断网兜底(均为真实运行截图)

返回键也验证通过:在文章页按一次返回,回到首页;在首页再按一次,才退出到桌面。

踩坑留下的几句话

验收阶梯:构建通过、安装成功、接口正常逐层递进,但机器能验证的每一层都可能是假绿,终点是用户真正看到的那一版
图 4 · 验收阶梯:机器能验证的每一层,都不等于「用户看到的那一版」

交付物的每一层都要按它真实的形态验收。 构建通过、安装成功、进程存活,全都不能替代「用户在桌面上看得到图标」「按钮的文字他看得清」。机器能验证的每一层,都可能是「假绿」。

当所有报错都消失时,先怀疑环境。 模拟器那三个坑一个堆栈都没有,逼着我把目光从应用日志移向宿主机与运行时本身。

回过头看,这些坑没有一个来自「技术难」,全部来自「我以为它是对的」。把壳写薄不难,难的是一次次回到用户那一侧,去确认它真的对了。