<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Android on Trevor's Blog</title><link>http://hitrevor.com/tags/android/</link><description>Recent content in Android on Trevor's Blog</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Tue, 03 Nov 2026 09:30:00 +0800</lastBuildDate><atom:link href="http://hitrevor.com/tags/android/index.xml" rel="self" type="application/rss+xml"/><item><title>Android 16 删了你的竖屏锁，我在模拟器上验了三遍</title><link>http://hitrevor.com/posts/a16-rotate/</link><pubDate>Tue, 03 Nov 2026 09:30:00 +0800</pubDate><guid>http://hitrevor.com/posts/a16-rotate/</guid><description>同一份代码编出 targetSdk 35 和 36 两个包，把屏幕做成平板再命令它旋转。35 被锁死还有黑边，36 直接横过来；换回手机尺寸，限制又回来了。</description><content:encoded><![CDATA[<p>这两天中文圈在传「谷歌下场施压，逼开发者适配折叠屏」。</p>
<p>我去翻了 <a href="https://developer.android.com/about/versions/16/behavior-changes-16">Android 16 的行为变更文档</a>，官方用的词不是「鼓励」，是 ignore。忽略你的竖屏锁，忽略你的不可调整大小，忽略你的宽高比上限。</p>
<p>它不是劝你，是直接把这个开关从系统层面拿掉了。</p>
<p>我做了一组对照实测：同一份代码，编译出 targetSdk 35 和 36 两个包，装进同一个模拟器，把屏幕做成平板大小，然后命令它旋转。</p>
<h2 id="官方原话是这样写的">官方原话是这样写的</h2>
<blockquote>
<p>For apps targeting Android 16 (API level 36), orientation, resizability, and aspect ratio restrictions no longer apply on displays with smallest width &gt;= 600dp.</p>
</blockquote>
<p>条件是两条：目标版本 36，且屏幕最小宽度到 600dp。满足就生效，应用铺满整个窗口，连黑边都不给。</p>
<p>被忽略的东西列得很具体。清单属性三个：<code>screenOrientation</code>、<code>resizeableActivity</code>、<code>minAspectRatio</code>、<code>maxAspectRatio</code>。运行时方法两个：<code>setRequestedOrientation()</code> 和 <code>getRequestedOrientation()</code>。</p>
<p>竖屏、横屏、反向竖屏、传感器竖屏、用户竖屏，这一串取值全部失效。</p>
<p><img alt="Android 16 到底忽略了什么，例外在哪，逃生门能开到什么时候" loading="lazy" src="/images/a16-rotate/fig1.png"></p>
<h2 id="官方为什么敢直接拿掉">官方为什么敢直接拿掉</h2>
<p>Android Developers 文档写道，现在的应用要跑在手机、平板、折叠屏、桌面模式、车机、电视上，还要跑在分屏和桌面窗口这类多窗口形态里。文档的原话是，锁方向、锁大小这类做法在今天「过于限制性」。</p>
<p>说白了，一个竖屏锁在手机上保护了你的排版，在平板和桌面窗口上就变成了让用户没法用。</p>
<p>官方也点了名，说要改成「应用预期自己适配各种方向、尺寸和宽高比」。这话听着软，落到系统行为上是硬的。</p>
<h2 id="我的实测怎么搭的">我的实测怎么搭的</h2>
<p>一个 Activity，纯 Java，不引 Compose，界面上只打印四行字：当前 orientation、screenWidthDp、screenHeightDp、smallestScreenWidthDp。</p>
<p>清单里故意把限制全上满：<code>android:screenOrientation=&quot;portrait&quot;</code>、<code>android:resizeableActivity=&quot;false&quot;</code>、<code>android:maxAspectRatio=&quot;1.5&quot;</code>。</p>
<p>同一份源码编两遍，只改 targetSdk，两个包各 2.5 MB。设备是 API 36 的模拟器。</p>
<p>把屏幕做成平板：<code>wm size 1200x1920</code>、<code>wm density 240</code>，算下来应用区 1280×800dp，最小宽度 800dp，刚好过线。然后用 <code>settings put system user_rotation 1</code> 命令它横过来。</p>
<h2 id="结果一targetsdk-35-被锁死还挨了一句提醒">结果一：targetSdk 35 被锁死，还挨了一句提醒</h2>
<p>35 那个包老老实实保持竖屏，内容还是 800×1200，被塞进一块 1280×800 的横屏窗口里，左右两条大黑边。</p>
<p>这是老行为，叫兼容模式。系统还在右下角弹了一句：Tap to restart this app for a better view。</p>
<p>翻译过来是「点一下重开，看得清楚点」。系统的态度已经写在脸上了。</p>
<h2 id="结果二targetsdk-36-直接违背我的清单">结果二：targetSdk 36 直接违背我的清单</h2>
<p>换成 36 那个包，同样一条旋转命令，它横过来了。</p>
<p>界面上自己报：orientation 等于 LANDSCAPE，screenWidthDp 1280，screenHeightDp 800，smallestScreenWidthDp 800。铺满，没有黑边。</p>
<p>清单里那三行限制，一行都没起作用。</p>
<p><img alt="同一份代码，两个目标版本，同一块屏幕" loading="lazy" src="/images/a16-rotate/fig2.png"></p>
<h2 id="结果三换回手机尺寸限制又回来了">结果三：换回手机尺寸，限制又回来了</h2>
<p>我又把屏幕改回手机（1080×2400，420dpi，最小宽度 411dp），再让 36 那个包旋转。</p>
<p>它没动，还是 PORTRAIT。</p>
<p>所以这条规则不是「新版就一定不管」，是「最小宽度过 600dp 才不管」。411dp 的手机上，你的竖屏锁照样有效。</p>
<p>这一点很关键，因为很多转载把它写成了「Android 16 取消了方向限制」，那是错的。</p>
<p><img alt="三组对照：阈值就是 600dp" loading="lazy" src="/images/a16-rotate/fig3.png"></p>
<h2 id="例外还有一道临时逃生门">例外，还有一道临时逃生门</h2>
<p>据 Android 16 行为变更页原文，例外有三个：游戏（按 <code>android:appCategory</code> 判断），用户在系统设置的宽高比里主动选了应用默认行为，以及屏幕小于 600dp。</p>
<p>还有一个 opt-out，写在清单里：</p>
<pre tabindex="0"><code>&lt;property android:name=&#34;android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY&#34;
          android:value=&#34;true&#34; /&gt;
</code></pre><p>可以写在 activity 上，也可以写在 application 上。写了就退回兼容模式。</p>
<p>但它有保质期。同一份文档提到，这条 opt-out 在未来的 Android 版本里、以 API 37 为目标时不再生效。也就是说，这是给你一年时间搬家，不是给你一个永久开关。</p>
<p>想在 36 上提前看效果，不用改 targetSdk，打开兼容性框架里的 <code>UNIVERSAL_RESIZABLE_BY_DEFAULT</code> 标志就行。我这次是直接编了两个包，更干净。</p>
<h2 id="这个改动会撞出什么">这个改动会撞出什么</h2>
<p>官方文档在 Common breaking changes 一节列了典型症状，我觉得写得比任何二手稿都准：拉伸变形的布局、跑到屏幕外的动画和组件。</p>
<p>这两个现象背后是同一件事：你的布局假设了某个固定的长宽比。写死高度的 RecyclerView、用屏幕宽算出来的 margin、只给了一种尺寸的图片背景，锁着竖屏时全都看不出来，一横过来就露。</p>
<p>还有一类更隐蔽的：以前锁竖屏，Activity 根本不会因为旋转重建，<code>onSaveInstanceState</code> 写得糙也没人发现。现在会重建了，没存好的状态就是用户手里的数据丢了。</p>
<h2 id="那到底该怎么改">那到底该怎么改</h2>
<p>官方的答案只有一个：按窗口尺寸做布局，不要按设备类型做。</p>
<p>窗口尺寸类现在分了五档，看宽度：小于 600 是 compact，600 到 840 是 medium，840 到 1200 是 expanded，1200 到 1600 是 large，1600 以上是 extra large。</p>
<p>高度单独再分三档：480 和 900 两个断点。</p>
<p>断点表在官方的 <a href="https://developer.android.com/develop/ui/compose/layouts/adaptive/window-size-classes">Use window size classes</a> 那一页。</p>
<p>那页还给了一个很好用的参照。compact 宽度覆盖 99.96% 的手机竖屏，expanded 覆盖 97.22% 的平板横屏。</p>
<p>文档里有一句我建议你抄下来：窗口尺寸类不由设备屏幕大小决定，它不是用来写 isTablet 逻辑的。同一个应用分屏之后窗口只有一半宽，它就该走 medium 那一套。</p>
<p><img alt="五档宽度、三档高度，按窗口算不按设备算" loading="lazy" src="/images/a16-rotate/fig4.png"></p>
<p>还有一件事容易被忽略：允许旋转意味着 Activity 重建次数变多。以前你锁着竖屏，旋转根本不发生，<code>onSaveInstanceState</code> 写得糙也没人发现。现在会发生了，状态没存好就是丢数据。</p>
<h2 id="我这组实测的局限得说清楚">我这组实测的局限，得说清楚</h2>
<p>第一，我用的是模拟器改屏幕尺寸，不是真的折叠屏。折叠的悬停态、桌面窗口模式、分屏里的 resize，这些我都拿不到真机行为，只能按文档描述写。</p>
<p>第二，我只测了一个 Activity、一个空的界面，验证的是系统层面的限制有没有生效，不是测某个真实 App 的适配质量。</p>
<p>第三，Android Studio 的折叠屏 AVD 我这次没建，因为要下载新的系统镜像，时间上不划算。你要更真的数据，官方有专门的折叠屏模拟器配置，装一个就能拿到悬停态。</p>
<p>能把话说清楚的部分我说死，拿不到证据的部分我标出来，这是我写所有实测稿的规矩。</p>
<h2 id="我的判断">我的判断</h2>
<p>这次改动的分量，比「适配折叠屏」这个说法重得多。</p>
<p>过去十年，竖屏锁是一种说白了的设计妥协：手机屏幕窄，横过来排版就崩，索性锁死。现在系统把它拿掉了，等于把「你的布局能不能自适应」这件事变成了硬指标。</p>
<p>对做产品的人是好事，偷懒的空间少了。对写代码的人是麻烦，<code>onConfigurationChanged</code> 和状态保存这两件事得认真写一遍。</p>
<p>你手上那个 App，现在横过来会不会崩？不用等 37，开个模拟器把屏幕调成 1200×1920、density 调成 240，五分钟就知道了。</p>
]]></content:encoded></item><item><title>Google 把每个 App 变成了 MCP server</title><link>http://hitrevor.com/posts/appfunctions/</link><pubDate>Mon, 02 Nov 2026 10:50:00 +0800</pubDate><guid>http://hitrevor.com/posts/appfunctions/</guid><description>Android 16 上每个 App 都能像 MCP server 一样对外声明函数。我在模拟器上隔着界面发出一条消息，全程没碰屏幕；也翻出了那道 internal|privileged 的权限门。</description><content:encoded><![CDATA[<p>MCP 这一年解决了服务端的事：agent 怎么发现工具、怎么调、怎么拿回结构化结果。手机上一直是空白。你的 App 里有一堆能力，搜索、发消息、加日程，agent 一个都碰不到，只能靠截图猜坐标，点一下看一眼。</p>
<p>今年二月，Google 在 Android 官方博客提了一个东西叫 AppFunctions。上个月二十一号，它的 SDK 发了 1.0.0-beta01。我把它在模拟器上跑通了，包括让「agent」隔着我 App 的界面，直接发出去一条消息。</p>
<h2 id="官方的说法很直白">官方的说法很直白</h2>
<p>那篇博客是 Android 开发者产品副总裁 Matthew McCullough 写的，原文用了这么一句：AppFunctions 让 App 像后端通过 MCP cloud server 声明能力那样，把能力声明出来，只不过执行发生在设备本地。</p>
<p>同一段里还提了 WebMCP，说这俩是一路：一个在浏览器，一个在系统。</p>
<p>所以定位很清楚，它不是又一个自定义 intent 协议，是有意照着 MCP 的形状做的。10 月那次 beta 更新里有个细节能佐证：他们把元数据字段从 <code>description</code> 改名成了 <code>instructions</code>，理由写的是「对齐 MCP 命名惯例」。</p>
<h2 id="一条-appfunction-要走四步">一条 AppFunction 要走四步</h2>
<p>第一步，在你的 App 里写一个服务类，继承 <code>AppFunctionService</code>，方法上打 <code>@AppFunctionDeclaration</code>，KDoc 会被当成给模型的说明。第二步，构建时注解处理器生成一个 XML，把参数、类型、枚举约束、返回结构写进去。</p>
<p>第三步，这个 XML 打进 APK 的 assets 目录，Android 系统读它，把函数注册进一张全局表。</p>
<p>第四步，有权限的调用方拿到 <code>AppFunctionManager</code>，先查这个函数开没开，再调。</p>
<p>我特意去 APK 里翻了一遍，这个 XML 确实在。样例工程的 <code>assets/chat_app_function_service.xml</code> 二十二 KB，六个函数，参数描述全是从 KDoc 抽的。</p>
<p><img alt="一条 AppFunction 从声明到执行要走四步" loading="lazy" src="/images/appfunctions/fig1.png"></p>
<h2 id="本机实测从编译到真的发出一条消息">本机实测：从编译到真的发出一条消息</h2>
<p>样本是官方仓库 <code>android/appfunctions</code> 里的 ChatApp，Compose 加 Hilt，编译出十四点八 MB 的 debug 包。装进模拟器，一打开就注册了六个函数：搜索联系人、发消息、拨号、搜消息、查发送详情、换壁纸。</p>
<p>设备那边列出来是这样，同一台模拟器上还有系统自己的人：</p>
<ul>
<li><code>com.example.chatapp</code> 六个，全是这个聊天 App 的业务</li>
<li><code>com.android.settings</code> 六个，全叫 <code>getXxxDeviceState</code>，读电池、存储、通知这些状态</li>
<li><code>com.google.android.permissioncontroller</code> 一个，读权限状态</li>
<li><code>com.google.android.apps.wellbeing</code> 一个</li>
</ul>
<p>系统 App 自己在用这套东西，这点比文档更有说服力。</p>
<h2 id="调用一次看它返回什么">调用一次，看它返回什么</h2>
<p>我用 ADB 直接调，绕开界面，模拟一个 agent 的行为。先按函数说明里写的「必须先搜联系人拿到 endpointValue」，搜一个人：</p>
<pre tabindex="0"><code>androidAppfunctionsReturnValue:
  contactDisplayName: Alice Smith
  endpointValue: 1
  contactType: INDIVIDUAL
  endpointDisplayName: alice@example.com
</code></pre><p>拿到 <code>1</code> 之后，第二步发消息，参数就是这个 id 加正文。返回一句 <code>Message sent to: Alice Smith.</code>。然后我打开 App 看了一眼，那条「这条是 agent 通过 AppFunction 发出来的」就躺在 Alice 的会话里。</p>
<p>全程没有碰过一次界面，没有截屏，没有点坐标。</p>
<p><img alt="隔着界面发一条消息：ADB 两步调用，右边是 App 里那一秒" loading="lazy" src="/images/appfunctions/fig3.png"></p>
<h2 id="最该记住的一条谁能调">最该记住的一条：谁能调</h2>
<p>这是我最想让你注意的地方。</p>
<p>要让别的应用发现并执行你的 AppFunction，调用方必须持有 <code>EXECUTE_APP_FUNCTIONS</code>。我在模拟器上把这个权限的定义翻出来了：</p>
<blockquote>
<p>Permission [android.permission.EXECUTE_APP_FUNCTIONS]
sourcePackage=android
prot=internal|privileged</p>
</blockquote>
<p>internal 加 privileged，意思是普通 App 拿不到，只有系统签名或者特权白名单里的应用才有。现阶段就是 Gemini 这类系统助手。</p>
<p>所以外面那两种声音都能对上：说「安卓 App 终于能跟 AI agent 对话了」没错，说「这不等于开放 agent 访问」也没错。函数是公开声明的，门是锁着的。</p>
<p><img alt="模拟器上的函数注册表，和那道 internal|privileged 的权限门" loading="lazy" src="/images/appfunctions/fig2.png"></p>
<h2 id="它已经在往政策里走">它已经在往政策里走</h2>
<p>搜索结果的另一条线索是 AEP，Google Play 的 Apps Experience Program。</p>
<p>它在里面有一条明确的准则叫 Android MCP，要求参与的应用必须用 AppFunctions 实现至少一个相关场景，通过必测项。适用范围写得很具体：消息、笔记、日历、任务、健康、购物。执行时间是 2027 年 3 月 1 日，或者自助注册开放后三个月，取较晚的那个。</p>
<p>也就是说，今天它是实验性 API，明年它是 Play 的一条合规项。这个节奏值得提前看一眼。</p>
<p><img alt="从博客到 Play 合规项的八个月，以及今天能验和不能验的边界" loading="lazy" src="/images/appfunctions/fig4.png"></p>
<h2 id="还有一条零代码的路线得放在一起看">还有一条零代码的路线，得放在一起看</h2>
<p>同一篇博客里，Google 还放了另一条路：UI 自动化。</p>
<p>意思是平台自己去操作你的界面，开发者的工作量是零。用户在 Galaxy S26 和部分 Pixel 10 上长按电源键，就能把一段多步骤任务交给 Gemini，首发只开美国、韩国的外卖、生鲜、网约车几个品类。</p>
<p>用户能看进度，能随时接管，涉及付款这类敏感动作前会被提醒。</p>
<p>这两条路摆在一起看，取舍就很清楚了：AppFunctions 给你控制权，你自己决定暴露什么、怎么描述、失败怎么回；UI 自动化给你覆盖面，但你不参与定义，agent 只能像人一样点你的界面。</p>
<p>官方说 Android 17 会扩大这两块。我的判断是，认真做产品的会把前者当主场，后者当兜底。</p>
<h2 id="还没到的地方说清楚">还没到的地方，说清楚</h2>
<p>SDK 是 beta，不是正式版。官方文档标着 experimental preview，接口还会变，十月那次就动了不少。Gemini 侧的集成现在是私有预览，只对受信任的测试方开放。能真正用起来的公开案例是三星相册，在 Galaxy S26 上，很快扩到 One UI 8.5。</p>
<p>还有一条我得如实说：我试着用 ADB 把其中一个函数关掉，验证「用户可以禁用」这个说法，命令跑了三十秒超时，函数状态没变。这条我没验证成功，不应该写进结论。</p>
<h2 id="我的判断">我的判断</h2>
<p>这次和以往那些「AI 集成 API」不一样的地方，在于形态。</p>
<p>过去是各家自己定一套 intent，agent 要逐个适配。这次是照 MCP 的模子刻：自描述、可发现、有枚举约束、有必填项、有错误码。对写 App 的人来说，真正的变化不在今年，在于明年那条政策生效时，你的 App 有没有把自己的核心能力说清楚。</p>
<p>一个想清楚了的函数声明，比你写十页帮助文档更能让 agent 用对。</p>
<p>你手上那个 App，如果只允许暴露三个函数给系统助手，你会挑哪三个？</p>
<p>评论区说说，我挑几个有意思的，下篇拆开写。</p>
]]></content:encoded></item><item><title>给 AI 装上 DevTools：它能看见每一帧</title><link>http://hitrevor.com/posts/agent-ddp/</link><pubDate>Sun, 01 Nov 2026 11:13:00 +0800</pubDate><guid>http://hitrevor.com/posts/agent-ddp/</guid><description>同一块屏幕，截图 77 KB 让模型去猜，ddp tree 只用 4,184 字节就把结构说清楚。我在模拟器上给它做了对照实测，也撞上了它现在还做不到的地方。</description><content:encoded><![CDATA[<p>AI 写前端早就不是猜的。Chrome DevTools Protocol 摆在那儿，agent 能读 DOM、能点按钮、能量颜色，改完自己回头验收。</p>
<p>写原生 App 就不是这回事。iOS 和 Android 没有等价的协议，agent 拿到手的只有一张截图。</p>
<p>截图能看，但看不出「这个按钮的边框从 5.25 变到 15.75 用了 1.8 秒」，也看不出「这行字被一个半透明层盖住了」。它只能猜，猜完就改，改完再截一张。</p>
<p>十月二十三号，有个叫 agent-ddp 的项目开了源，想补的就是这块。</p>
<h2 id="它把界面变成几百行文本">它把界面变成几百行文本</h2>
<p>装完之后，agent 问一句屏幕上有啥，拿到的是这个：</p>
<pre tabindex="0"><code>@e1 animatedButton &#34;Selected&#34; [383, 158, 315, 137]
  z=1 layer fill #0000FFFF corner 31.5
  z=2 bitmap [467, 195, 147, 63]
  z=3 layer border 15.75 #FFA500FF corner 31.5
@e2 flashBox &#34;Flash&#34; [460, 358, 160, 115]
</code></pre><p>每个元素都有引用号 <code>@e1</code>，下一条命令可以直接用。填充色、边框、圆角、坐标、层级，全是数字。</p>
<p>我量了一下：这个界面 25 个元素、59 个绘制节点，<code>ddp tree</code> 输出 4,184 字节。同一块屏幕截图存下来是 77,397 字节。</p>
<p>差 18.5 倍，而且差的不是大小，是确定性。截图要模型自己看，文本它直接读。</p>
<p><img alt="同一块屏幕：77 KB 的截图和 4,184 字节的文本" loading="lazy" src="/images/agent-ddp/fig1.png"></p>
<h2 id="本机跑起来从装到出结果">本机跑起来：从装到出结果</h2>
<p>我用的是 Mac 上现成的模拟器，Medium Phone，API 36.1。</p>
<p><code>npm i -g agent-ddp</code> 装 CLI，一个 Rust 编译的原生二进制，没有运行时依赖。样例工程用 Gradle 编出 10 MB 的 debug APK，装进模拟器，打开。</p>
<p>第一步跑 <code>ddp doctor</code>，它把设备、应用、库、端点全查一遍：</p>
<blockquote>
<p>ok   agent-ddp library in the build
ok   control endpoint answers
ok   25 elements on screen
ok   hook compose.zSortedChildren (internal): answered 4608 times</p>
</blockquote>
<p>二十来行全绿。这一步很关键，因为它不只查「连没连上」，还列出每个隐藏 hook 被调了多少次。后面会说为什么。</p>
<p>接着我只敲了一行 <code>ddp check all</code>，2.7 秒，扫了 116 帧，报出九个问题，退出码 1。</p>
<h2 id="它报的九个问题长这样">它报的九个问题长这样</h2>
<ul>
<li>两处文字被盖住：<code>boxCovered</code> 的字在它的框里一个像素都找不到</li>
<li>一处圆角越界：<code>hatchCard</code> 的灰线画到了圆角外面</li>
<li>一处动画不停：<code>clockBar</code> 最后 500 毫秒还在变，29 次</li>
<li>两处无障碍没标签、两处点击区只有 37×47，低于 48×48</li>
<li>一处对比度 1.5:1，低于 4.5:1</li>
</ul>
<p>每一条都带证据坐标，比如那句「它的字在 [42, 1283, 258, 85] 里一个像素都不出现，那个位置屏幕是 #9E9E9EFF」。</p>
<p>我核对了截图，那块地方确实是灰色方块压在文字上。</p>
<p><img alt="一条 check all 报出的九条问题，退出码 1" loading="lazy" src="/images/agent-ddp/fig2.png"></p>
<h2 id="我最喜欢的是它拒绝我的那一下">我最喜欢的是它拒绝我的那一下</h2>
<p>界面上那个被盖住的按钮，我试着让它点：</p>
<blockquote>
<p>ddp: boxCovered is covered by pointerInput: no point of it takes a touch
next: ddp check tappable, or ddp tap boxCovered &ndash;force to touch it anyway</p>
</blockquote>
<p>它不点，并且告诉你为什么，还给出两个下一步。</p>
<p>凡是做过 UI 自动化的人都知道这有多反常。传统框架碰到这种情况会硬点下去，然后你去猜为什么没反应。</p>
<h2 id="它给自己定的三条硬规矩">它给自己定的三条硬规矩</h2>
<p>翻文档时我发现，这个项目把三句话写成了不可违反的规矩，排在所有其他规则前面。</p>
<p>第一条，零误报。一条报警只有在确定有问题时才报，拿不准就什么都不说。理由写在文档里：一次误报会教 agent 养成随手加 <code>--force</code> 的习惯，那比漏报贵得多。</p>
<p>第二条，平台引擎是唯一事实源。什么吃到点击、画了什么、谁盖住谁，这些答案必须来自 UIKit、Compose、View 分发这些平台自己的代码。它不许自己重写一套规则去模拟平台的判断。</p>
<p>第三条，每个 hook 都必须计量。凡是用了私有符号或者框架内部成员的地方，都要登记，并且每次调用都计数。hook 失效时，结论变成「未知」，不许变成「有问题」。</p>
<p><code>ddp doctor</code> 里那行 <code>answered 4608 times</code> 就是这条规矩的产物。系统升级后某个内部结构变了，计数会直接掉下去，你立刻知道有个检查悄悄失明了。</p>
<p><img alt="写在所有规则前面的三条硬规矩" loading="lazy" src="/images/agent-ddp/fig4.png"></p>
<h2 id="它写了-cdp-方案当天就自己否了">它写了 CDP 方案，当天就自己否了</h2>
<p>这个项目有份决策记录特别值得看。</p>
<p>最早的设计是让 Debug 包里的库直接说 Chrome DevTools Protocol，这样 Chrome 和现成的 CDP 客户端都能连上，agent 也不用学第二套协议。</p>
<p>同一天，它自己把这份方案否了。</p>
<p>理由很实在：为了模仿浏览器，它得在原生元素树上面假装出一个 <code>#document &gt; html &gt; body</code>，得支持 CSS 选择器，得给偏好设置起名 localStorage，还得为 Chrome 要的字段填假值。</p>
<p>每一样都是翻译层，学会它、维护它的人是你。更糟的是，这些翻译会把 App 真实的结构藏起来。</p>
<p>换掉之后，协议名改成 <code>screen.elements</code>、<code>input.touch</code>、<code>prefs.write</code> 这种原生叫法。删掉的浏览器层大约六百行。</p>
<h2 id="装进去的东西很轻">装进去的东西很轻</h2>
<p>iOS 那份是 Swift，四千三百多行；Android 那份 Kotlin，四千一百多行；CLI 是 Rust，九千七百多行。</p>
<p>Debug 包里的库自己就是服务器，跑在一个本地 WebSocket 上。Android 用的是只有 shell 用户和 root 能连的抽象 socket，iOS 只监听 127.0.0.1，还挡掉非 localhost 的 Host 头。</p>
<p>没有守护进程。每条 <code>ddp</code> 命令都是一个短进程，录下来就是一串 jsonl 文件。同一个文件，可以反复问不同的问题。</p>
<p>Release 包里这些代码一行都没有，Android 是 <code>debugImplementation</code>，iOS 整个包在 <code>#if DEBUG</code> 里面。</p>
<p><img alt="Debug 库、Rust CLI、agent 三段，中间只有一个本地 WebSocket" loading="lazy" src="/images/agent-ddp/fig3.png"></p>
<h2 id="说点不好听的">说点不好听的</h2>
<p>它 0 star。十月二十三号建仓，现在共 114 次提交、五个版本，五个开着的 issue 全是依赖升级机器人提的。</p>
<p>iOS 这半边只支持模拟器，真机还没做。看不见的东西也列了一长串：SwiftUI Canvas 里的图文、系统弹窗、部分覆盖的文字、Compose 里某些画进图层的缓存绘制。</p>
<p>还有一类我特意试了：一个按钮只在手指抬起时才响应触摸，这种它判断不了，只会说触摸能到，然后 <code>tap</code> 之后告诉你「什么都没变」。</p>
<p>这不是缺陷，是它诚实。宁可说不知道，也不猜一个答案给你。</p>
<h2 id="我的判断">我的判断</h2>
<p>这波工具真正的分水岭不是能读多少属性，是敢不敢闭嘴。</p>
<p>一个什么都敢报的检查器，agent 学会的是绕过它。一个只在该报的时候报的检查器，agent 才敢把它当验收条件写进流程。</p>
<p>九个真问题、2.7 秒、一千零九十一字节的输出，一个退出码。这套东西如果稳，agent 做 UI 就从「写代码」变成「写完自己验」。</p>
<p>你现在让 agent 改 UI，是发截图给它看，还是让它自己摸？</p>
]]></content:encoded></item><item><title>交完 25 美元才知道，Google Play 给自己装应用不要钱</title><link>http://hitrevor.com/posts/gplay-personal/</link><pubDate>Mon, 26 Oct 2026 09:31:22 +0800</pubDate><guid>http://hitrevor.com/posts/gplay-personal/</guid><description>10 月谷歌给 Google Play 开了个新入口：对着 AI 说句话生成的应用，不用开发者账号、不用 25 美元、不过审核，扫码就装。代价是只有你自己能装。</description><content:encoded><![CDATA[<p>十月我为了一个开发者账号忙了小半个月。25 美元报名费，身份验证要政府签发的证件，付款资料的地址必须和证件上一字不差，新注册的个人号还得先找 12 个测试人员连续挂满 14 天，中途账号还被封过一次，重新走了一遍流程。这三件事我都写过。</p>
<p>就在我跑这些流程的时候，10 月 13 日，谷歌在 Play 的产品博客上发了一篇更新，标题是 New on Google Play: More ways to create, discover, and play。</p>
<p>里面有这么一句。</p>
<blockquote>
<p>Using Google AI Studio, you can vibe code a custom Android app. Since many of these apps start out as personal projects, through Google Play you can now launch these personal apps on your own device. This means your creations stay private, secure, and separate from the public store catalog. You only need your standard Google account to do so — no developer account required.</p>
</blockquote>
<p>用大白话说，对着 AI 说几句话生成一个安卓应用，点一下发布，扫个二维码，应用就装到你手机上了。不用开发者账号，不用 25 美元，不用审核。</p>
<p>同一个 Google Play，两套门槛。</p>
<p><img alt="开发者账号和个人库，同一个 Google Play 上的两套门槛" loading="lazy" src="/images/gplay-personal/gplay-two-doors.png"></p>
<h2 id="这条路具体怎么走">这条路具体怎么走</h2>
<p>Play Console 的帮助中心新开了一页，标题叫 Personal library apps in Google Play，把步骤写得很细。</p>
<p>在电脑浏览器打开 Google AI Studio，用你的个人 Google 账号登录。点 New app，选 Build an Android app，输入你想做什么。生成预览之后，右上角点 Publish。第一次用要连一下 Google 账号，然后点 Create app。</p>
<p>几分钟后，屏幕上出现一个二维码。</p>
<p>手机用同一个 Google 账号登录过 Play 商店，扫这个码，装。</p>
<p>不想扫码也行，路径是 Play 商店应用里点个人资料，管理应用和设备，找到 Your personal library，里面就是你自己做的应用，点安装。更新也一样，回 AI Studio 改 prompt，点 Update app，手机上自动更新。</p>
<p>整个过程我最想确认的是那句 no developer account required。官方帮助页写得更死。</p>
<blockquote>
<p>You only need a Google Account to get started.</p>
</blockquote>
<p>一句话，一个账号，一个二维码。</p>
<p><img alt="从一句 prompt 到装进手机，官方帮助页写的五步" loading="lazy" src="/images/gplay-personal/gplay-flow.png"></p>
<h2 id="有开发者账号的人反而用不了">有开发者账号的人反而用不了</h2>
<p>这是我读到最意外的一条，官方帮助页单列了一个问答。</p>
<blockquote>
<p>Owners of Google Play Console developer accounts will need to use a different Google Account to use this feature.</p>
</blockquote>
<p>意思是，如果你已经有 Play Console 开发者账号，想用这个功能，得换一个 Google 账号。</p>
<p>谷歌没解释原因。我的理解是账户体系要隔离，个人库里的东西不能和你在 Play Console 里以开发者身份提交的应用混在一起，否则你用一个号既能提交公开应用又能绕过审核装私人应用，边界就糊了。这是我的推断，官方没写。</p>
<p>但对开发者来说这个体验挺怪的。你为了正经发布交过 25 美元，现在想顺手给自己做个小工具，反而要再注册一个账号。</p>
<h2 id="私人这两个字要打个引号">「私人」这两个字要打个引号</h2>
<p>官方反复强调私人这两个字。不进公开目录，不出现在搜索结果、榜单和推荐里，只能你自己装。</p>
<p>但它不等于谷歌看不见。</p>
<p>帮助页末尾那段写得很清楚，当你在 AI Studio 里创建、修改或发布应用时，工具会把这些东西传给 Google Play：应用的包、创建过程的元数据（包括 prompt）、AI 生成标记、软件依赖和 SDK、用户可见的字符串，还有源代码。</p>
<p>拿去干什么？官方列了四项：安全扫描、滥用防护、政策合规，以及训练和改进自动化的应用安全分类器。</p>
<p>所以你写的每一句 prompt，AI 生成的每一行 Kotlin，都在谷歌的扫描管道里过了一遍。免的是人工审核和上架流程，不是数据采集。</p>
<p>这条我建议大家读原文，因为「私人」在中文语境里容易被读成「没人管」。</p>
<h2 id="审核是免了但不是没规矩">审核是免了，但不是没规矩</h2>
<p>同一页列了明确的内容禁区，个人应用一样不能碰。</p>
<p>不能做银行、贷款、加密货币、真钱赌博、政府服务这几类高风险业务。不能申请无障碍服务、短信和通话记录、绑定通知、请求安装应用这类敏感权限。不能侵犯第三方知识产权，不能冒充已有品牌、政府机构或个人。不能含恶意软件、间谍软件、钓鱼、未授权的数据采集，也不能有仇恨言论、色情内容和暴力。</p>
<p>违规的后果是应用从 AI Studio 里被移除，屡次违规会终止你对 AI Studio 的访问权限。</p>
<p>免审只是省掉了排队和人工核对，该守的线一条没少。区别在于以前是谷歌在上架前拦，现在是你自己撞上去才处理。</p>
<h2 id="它生成的是个什么工程">它生成的是个什么工程</h2>
<p>这部分我对写代码的人有用。</p>
<p>AI Studio 生成的是一套标准 Gradle 项目，构建脚本用 build.gradle.kts 的 Kotlin DSL，界面层是 Jetpack Compose 配 Material 3，架构是单 Activity 加 ViewModel 和数据类，还带 AndroidManifest、drawable、strings 这些资源文件。</p>
<p>开了 Firebase 的话会多出 google-services.json、firebase_applet_config.xml 和 firestore.rules。依赖由 agent 自己从 Maven 和谷歌仓库里加。</p>
<p>你可以下载成 ZIP，导进 Android Studio 接着改。</p>
<p>限制也写得很直白，四条。</p>
<p>只有客户端，没有服务端组件，涉及密钥管理、多人协作、Google Workspace 接口的功能都做不了。单 Activity 单 module，结构超过这个规模就不行。只认 Kotlin 和 Jetpack Compose，Java 和 XML 布局不支持。没有 NDK，C 和 C++ 进不来。可穿戴和 Android TV 也不支持，只有手机和平板。</p>
<p>还有个细节挺有意思，浏览器里的模拟器是跑在云端的，你不需要装 Android SDK、Android Studio 或者本地模拟器。但它不支持相机、NFC、蓝牙，GPS 是模拟的，而且没有 Google Play services，谷歌登录和地图在模拟器里用不了，得装到真机才正常。</p>
<p>这套工程的上限很清楚，适合做个人工具、小应用、原型，不适合往上叠大工程。</p>
<p><img alt="AI Studio 生成的安卓工程结构，以及官方列出的四条硬限制" loading="lazy" src="/images/gplay-personal/gplay-limits.png"></p>
<h2 id="同一个按钮两条路">同一个按钮，两条路</h2>
<p>写到这里我发现最值得讲的是 AI Studio 里那个 Publish 按钮的解释。</p>
<p>它有两条出口。一条通向个人库，零门槛，只有你能装。另一条通向 Play Console 的内测轨道，需要 25 美元的开发者账号和完整开发者资料，最多能给 100 个测试人员装。</p>
<p>同一个界面，同一次生成，两套规则差别这么大。</p>
<p>谷歌在博客里把后续路径也铺好了。未来几个月，你可以把个人应用分享给一小圈朋友和家人，那时候应用要先过一遍 Trust &amp; Safety 审核。如果你想让全世界都能用，可以升级成完整开发者账号，个人库里的所有应用会迁移到 Play Console，变成内测轨道上的草稿应用。</p>
<p>我读这段的感觉是，谷歌在把「做个小工具自己用」和「做一个产品给人用」拆成了两个完全不同的入口，中间留了一条升级通道。</p>
<p>以前只有后者，你想给自己手机装个 AI 刚生成的东西，得先变成开发者。现在前者单独开了门，代价是你得先承认自己不是开发者。</p>
<p><img alt="谷歌铺好的三级台阶：个人库、分享给朋友家人、升级开发者账号" loading="lazy" src="/images/gplay-personal/gplay-stairs.png"></p>
<h2 id="这功能现在中国用不了">这功能现在中国用不了</h2>
<p>这个功能现在是 beta，只对美国地区的 18 岁以上用户开放，未来几周内逐步铺开。Play 商店应用要 53.5 以上版本。其他国家「near future」，官方没给时间表。</p>
<p>我这边不在支持的地区，功能没摸到。上面这些流程、限制、原文，全部来自三份官方文档：10 月 13 日的 Play 产品博客、Play Console 帮助页的 Personal library apps in Google Play、以及 AI Studio 的安卓构建文档。哪句话是官方原文，哪句是我的推测，正文里都标了。</p>
<p>那问题来了。</p>
<p>如果以后你在美国的朋友对着 AI 说几句话就把应用装上了，而你还在为 12 个测试人员连挂 14 天发愁，你会去注册一个普通 Google 账号绕过去吗？还是说，这 25 美元和那五道关，本来买的就是「能给别人装」这个资格？</p>
<p>再想一层。谷歌这次拆的其实只是「给自己用」这半堵墙，另一半一动不动。等到哪天 vibe coding 出来的人都想分享给朋友了，这道 Trust &amp; Safety 审核会不会又变成新的五道关？</p>
<p>你手上有没有那种「就我自己用，但一直懒得为它开账号」的小工具？如果现在只要一句 prompt 就能装上手机，你会先做一个什么？</p>
]]></content:encoded></item><item><title>Google Play 账号被封，我重开了一遍</title><link>http://hitrevor.com/posts/gplay-reopen/</link><pubDate>Tue, 20 Oct 2026 10:49:18 +0800</pubDate><guid>http://hitrevor.com/posts/gplay-reopen/</guid><description>账号被封那天，我的第一反应和你一样：再注册一个。</description><content:encoded><![CDATA[<h2 id="先别急着注册先看你那个号是怎么没的">先别急着注册，先看你那个号是怎么没的</h2>
<p>账号被封那天，我的第一反应和你一样：再注册一个。</p>
<p>但翻完官方文档我才发现，重开这件事根本不是一个流程问题。它分两种情况，结果差得很远。</p>
<p>第一种是休眠。账号长期没有发布更新、没有查看统计、没有管理商品详情，Google 会把它停用。帮助中心的原话是：休眠账号被停用后，您日后仍可创建新账号并在 Google Play 上发布内容。这种号，重开就是正常操作。</p>
<p>第二种是违规终止。同一篇文章里写着：如果您屡次或严重违反这些政策或开发者分发协议，您的开发者账号或相关的 Google Play 开发者账号可能会被终止。</p>
<p>请留意「相关的」这三个字。另一处措辞更直白：多次收到警示可能会导致个人以及相关的 Google Play 开发者账号被终止。</p>
<p>所以真正的问题不是「怎么重开」，是「重开之后能不能活」。</p>
<p><img alt="先判断你的号属于哪种情况" loading="lazy" src="/images/gplay-reopen/fig1.png"></p>
<h2 id="我为什么还是选了个人号">我为什么还是选了个人号</h2>
<p>重开的时候，账号类型这一步比第一次注册更要紧。</p>
<p>组织号要交邓白氏编码、组织注册文件、授权代表身份证，三份材料还得互相对得上。D-U-N-S 申请最多要等 30 天。号已经没了，再等一个月，太慢。</p>
<p>个人号只要一份政府签发的带照片证件，材料少，出错的点也少。</p>
<p>但不能为了省事硬选。官方有一份清单：金融产品与服务、健康类应用、使用 VpnService 的应用、政府应用，这四类必须用组织号。你的应用沾上其中任何一条，个人号阶段看不出来，审核到后面一样会被打回来。</p>
<h2 id="重开的六个动作">重开的六个动作</h2>
<p>顺序有讲究，尤其第二步。第一步是把 Google 账号准备好，用干净的号，别拿注册过别的开发者账号的，也别买号。</p>
<p>第二步，先去 Google Payments 建付款资料。国家/地区选中国，法定姓名和地址按身份证原文填。这一步不能省，也不能放到后面补。</p>
<p>第三步，用这个账号在 Play 管理中心注册开发者账号，交 25 美元。卡种限 MasterCard、Visa、American Express、Discover（仅限美国）和 Visa Electron（非美国），不收预付卡。</p>
<p>第四步，选个人号。</p>
<p>第五步，身份验证，交政府签发的带照片身份证件。</p>
<p>第六步，设备验证。找一台 Android 10 以上、没 root 的真手机，装 Play 管理中心手机应用扫二维码，官方说这一分钟就能完成。</p>
<p><img alt="重开的六步，第二步是地基" loading="lazy" src="/images/gplay-reopen/fig2.png"></p>
<p>顺序为什么不能跳？电话号码验证排在身份验证和设备验证之后。前面没做完，电话验证的入口压根不给你开。</p>
<h2 id="通过之后首页长这样">通过之后，首页长这样</h2>
<p>验证过了那天进 Console，第一眼容易失望：正式版 0 个应用、政策任务 0 个应用、发布提交内容 0 个应用，三张卡片全是零。</p>
<p>这是对的。账号通过只代表你现在能创建应用，不代表应用能上架。真正的起点是右上角那个「创建应用」。</p>
<p>这一页有两个地方值得记一下。顶上那排按钮里有个「政策状态」，账号信誉出问题会先出现在那里。下面列表里那个带「草稿」标签的，是建好还没提交审核的应用。中间那条绿色横幅只是 Play 的改版通知，跟你的账号状态没有任何关系。</p>
<p><img alt="审核通过后的 Play Console 中文首页" loading="lazy" src="/images/gplay-reopen/fig4-console.jpg"></p>
<h2 id="地址这一段我花的时间最多">地址这一段，我花的时间最多</h2>
<p>第一次注册时随手填的地址，这次不敢了。</p>
<p>官方在「按国家/地区划分的必要文件」那页写了一句话，我给抄下来：</p>
<blockquote>
<p>提交的所有文件都必须与您的支付资料中的信息完全一致，包括：个人身份详细信息、组织名称（针对组织账号）、地址信息。如果您发现自己的证件和支付资料之间存在任何不一致，可在执行验证任务期间更新支付资料信息。</p>
</blockquote>
<p>这句话解释了大部分人被驳回的原因。验证不是核对材料齐不齐，是核对材料和你自己填的资料对不对得上。</p>
<p>有三条线要逐字对。</p>
<p>姓名这条，证件上是中文就写中文，别一处写「张伟」一处写「Zhang Wei」。</p>
<p>地址这条，付款资料里的地址才是法定地址，Play 对外展示的国家和地区也按它算。证件写「XX 区 XX 路 128 号 3 栋 502」，你只写到「XX 路」，就算不一致。省市区街道门牌，能写到哪层写哪层。</p>
<p>证件这条，官方给了四条硬要求：有效未过期、彩色不能黑白、清晰可辨且明暗适中、不能是复印件。</p>
<p><img alt="三条线要逐字对上" loading="lazy" src="/images/gplay-reopen/fig3.png"></p>
<p>已经被驳回了也不用重新注册。同一句话的后半段写着，验证任务期间可以更新支付资料，把地址改成和证件一致，再交一次。</p>
<h2 id="新号怎么不被连坐">新号怎么不被连坐</h2>
<p>这一段是我这次最想写清楚的部分，因为它没有技巧，只有事实。</p>
<p>官方说「相关的开发者账号可能会被终止」，意思是 Google 会看几个东西是不是同一套：同一个人、同一张付款卡、同一台设备、同一批包名、同一个收款账户。</p>
<p>重开一个号，这些痕迹如果原封不动搬过去，等于自己举手说「还是我」。</p>
<p>真正能减少风险的，是原号被封的原因本身不严重，或者已经申诉和解。如果是恶意软件、欺诈这类严重违规，换十个号也一样。这种情况下重开，25 美元不退，号还会再被封一次。</p>
<p>说难听点，官方那句「相关的开发者账号」就是专门堵这条路的。</p>
<h2 id="交材料之前先在本地看两眼">交材料之前，先在本地看两眼</h2>
<p>官方那四条照片要求，有两条在 Mac 上就能验：是不是彩色、分辨率够不够。系统自带 sips，不用装东西。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sips -g pixelWidth -g pixelHeight -g space 你的证件.jpg
</span></span></code></pre></div><p>看 space 那一行。写 RGB 是彩色，写 Gray 就是黑白，黑白图一定被退。短边低于 1000 像素也建议重拍，微信转存过的图经常只剩几百像素。</p>
<p>我写了个小脚本把这两条串起来，跑出来是这样：</p>
<pre tabindex="0"><code>id-color.png        1600×1000  space=RGB   ✓ 通过
id-gray-small.png   640×400    space=Gray  ✗ 短边 400px  ✗ 灰度
</code></pre><h2 id="最后一句实话">最后一句实话</h2>
<p>重开这件事，材料和流程都是次要的，第一位的永远是你上一个号为什么没了。</p>
<p>休眠停用的，按上面六步走一遍就行。严重违规被终止的，别指望换号能翻篇，先把问题解决，再谈注册。</p>
<p>你重开过吗，卡在哪一步？欢迎评论区聊聊。</p>
]]></content:encoded></item><item><title>Google Play 注册：地址要和身份证一致</title><link>http://hitrevor.com/posts/gplay-register/</link><pubDate>Tue, 20 Oct 2026 10:00:45 +0800</pubDate><guid>http://hitrevor.com/posts/gplay-register/</guid><description>上周又有人问我 Play 开发者账号被拒了怎么办。翻他的材料，身份证拍了、信用卡也绑了、25 美元扣款成功，问题出在地址，他那张卡的开户地址是公司，身份证上的地址是老家。</description><content:encoded><![CDATA[<h2 id="驳回最多的原因不是材料不齐">驳回最多的原因，不是材料不齐</h2>
<p>上周又有人问我 Play 开发者账号被拒了怎么办。翻他的材料，身份证拍了、信用卡也绑了、25 美元扣款成功，问题出在地址，他那张卡的开户地址是公司，身份证上的地址是老家。</p>
<p>中文教程讲注册流程的很多，讲地址对齐的几乎没有。而 Google 自己把这件事写在了帮助中心最显眼的位置。</p>
<h2 id="官方原话">官方原话</h2>
<blockquote>
<p>提交的所有文件都必须与您的支付资料中的信息完全一致，包括：个人身份详细信息、组织名称（针对组织账号）、地址信息。如果您发现自己的证件和支付资料之间存在任何不一致，可在执行验证任务期间更新支付资料信息。</p>
</blockquote>
<p>紧接着还有一句，说的是失败原因：</p>
<blockquote>
<p>提交不受支持的文件是开发者验证失败的主要原因，而提供经过修改或伪造的文件则可能会导致您的账号和应用被立即移除。</p>
</blockquote>
<p>这两句话把规则说透了：验证不是看你有没有材料，是看你交的材料能不能和你自己填的资料对上。对不上，材料再齐全也是错的。</p>
<p><img alt="三条线必须逐字对上" loading="lazy" src="/images/gplay-register/fig1.png"></p>
<h2 id="地址不是你填在注册页的那个">地址不是你填在注册页的那个</h2>
<p>很多人以为地址是注册表单里随手填的一栏。不是。</p>
<p>Play 的法定全名和法定地址，是从你账号关联的 Google 付款资料里取的。也就是说，你在 Google Payments 里存的那份地址，才是最终被拿去和身份证比对的东西。Play 页面上对外展示的国家和地区，也是按这份法定地址来算的。</p>
<p>顺序就清楚了：先把付款资料写对，再开开发者账号。反过来做，等于给自己的审核埋雷。</p>
<h2 id="三条线必须对得上">三条线必须对得上</h2>
<p>第一条是姓名。证件上的名字是中文，付款资料里也写中文。如果系统只吃拉丁字符，就用一套统一的转写规则，别一处写「张伟」一处写「Zhang Wei」，更别一处写全拼一处写缩写。</p>
<p>第二条是地址。省、市、区、街道、门牌号，能写到哪一层就写到哪一层，和证件上的写法逐字对。证件写「XX 区 XX 路 128 号 3 栋 502」，你付款资料只写「XX 路」，这就算不一致。</p>
<p>第三条是证件照片。官方给的四条要求很具体：证件有效未过期、彩色而非黑白、清晰可辨且明暗适中、不能是复印件。四项里任何一项不过，都算不受支持的文件。</p>
<p>有一条官方原文里没有、但实操中反复踩到的：绑定的银行卡账单地址最好也落在同一套地址上。这条没有官方依据，是经验判断，仅供你参考。</p>
<h2 id="全中文注册的六步顺序">全中文注册的六步顺序</h2>
<p>第一步，先做一个干净的 Google 账号。别用之前注册过别的开发者账号的号，也别用买来的号，后面身份验证会认人。</p>
<p>第二步，进 Google Payments 建付款资料，把国家/地区选中国，法定姓名和地址按身份证原文填。这一步的地址，后面所有验证都以它为准。</p>
<p>第三步，在 Play 管理中心用这个账号注册开发者账号，交 25 美元。卡种限 MasterCard、Visa、American Express、Discover（仅限美国）和 Visa Electron（非美国），不收预付卡。</p>
<p>第四步，选账号类型。个人号选个人，别为了「看起来正式」硬选组织，组织号要多交 D-U-N-S 编码和公司证件，材料差一样就卡住。</p>
<p>第五步，做身份验证，交政府签发的带照片身份证件。个人号只要这一份。</p>
<p>第六步，设备验证。拿一台 Android 10 以上、没 root 的真手机，装 Play 管理中心手机应用扫二维码，一分钟。</p>
<p><img alt="六步顺序，第二步是地基" loading="lazy" src="/images/gplay-register/fig2.png"></p>
<p>顺序不能跳。电话号码验证被排在这几项之后，身份验证没做完，电话验证的入口根本不给你开。</p>
<h2 id="组织号要多两份地址同样要对齐">组织号要多两份，地址同样要对齐</h2>
<p>组织号除了代表人的身份证，还要组织注册文件（营业执照这类）和邓白氏编码。三份材料里的组织名称和地址，必须和付款资料里写的逐字一致。</p>
<p>D-U-N-S 在邓白氏官网免费申请，官方特意提醒过有第三方收高价代办费，别上当。申请最多要等 30 天，所以准备做组织号的，这件事要提前一个月动手。</p>
<h2 id="交上去之前自己先过一遍">交上去之前，自己先过一遍</h2>
<p>官方那四条照片要求，在本机就能验两条：是不是彩色、分辨率够不够。macOS 自带 sips，不用装东西。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sips -g pixelWidth -g pixelHeight -g space 你的证件.jpg
</span></span></code></pre></div><p>看 <code>space</code> 那一行。写着 <code>RGB</code> 是彩色，写着 <code>Gray</code> 就是黑白，黑白图一定会被退回。短边低于 1000 像素也建议重拍，压缩过的微信图片经常只有几百像素。</p>
<p>我把这几条写成了一个小脚本，跑两个样图给你看效果：</p>
<pre tabindex="0"><code>id-color.png        1600×1000  space=RGB   ✓ 通过
id-gray-small.png   640×400    space=Gray  ✗ 短边只有 400px  ✗ 色彩空间是 Gray
</code></pre><p><img alt="本机自检的两条" loading="lazy" src="/images/gplay-register/fig3.png"></p>
<h2 id="已经被驳回了怎么办">已经被驳回了怎么办</h2>
<p>不用重新注册。官方说的是，在执行验证任务期间可以更新支付资料信息，把地址改成和证件一致，再重新提交一次。</p>
<p>真正没救的是伪造。用 PS 改过地址的证件、借来的身份证、买来的营业执照，被查出来是账号和应用直接移除，25 美元也不退。</p>
<p>地址这件事说小很小，一行字。说大也很大，它决定了你后面所有验证能不能过。你注册时在地址上踩过坑吗？欢迎评论区聊聊。</p>
]]></content:encoded></item><item><title>Google Play 开发者账号：25 美元之后的五道关</title><link>http://hitrevor.com/posts/gplay-dev-account/</link><pubDate>Mon, 19 Oct 2026 15:38:12 +0800</pubDate><guid>http://hitrevor.com/posts/gplay-dev-account/</guid><description>中文教程里关于 Play 开发者账号的说法，大多停在两年前：25 美元、一张信用卡、填个地址、等一天。这套现在不够用了。</description><content:encoded><![CDATA[<h2 id="二十五美元只是买张门票">二十五美元只是买张门票</h2>
<p>中文教程里关于 Play 开发者账号的说法，大多停在两年前：25 美元、一张信用卡、填个地址、等一天。这套现在不够用了。</p>
<p>官方帮助中心现在写的是：年满 18 周岁，接受开发者分发协议，用信用卡或借记卡交 25 美元，卡种限 MasterCard、Visa、American Express、Discover（仅限美国）和 Visa Electron（非美国），明确不收预付卡。</p>
<p>交完钱才刚进门。后面还有身份验证、设备验证、十二个人十四天的封闭测试，以及 9 月 30 日生效的那套新规则。</p>
<p><img alt="开一个号要走的六步" loading="lazy" src="/images/gplay-dev-account/fig2.png"></p>
<h2 id="个人号和机构号先选对">个人号和机构号，先选对</h2>
<p>Play 的开发者账号只有两种：个人、组织。功能一样，都能创收，也都能加协作者。</p>
<p>差别在材料。组织号必须提供邓白氏编码，也就是 D-U-N-S，那是邓白氏发的九位企业标识，能免费申请，但要等。官方另给了一份清单：金融服务、健康类应用、使用 VpnService 的应用、政府应用，这四类必须走组织号。</p>
<p>个人号注册快，代价是后面几条测试要求专门冲着个人号来。</p>
<h2 id="身份验证证件和卡得是同一个人">身份验证：证件和卡得是同一个人</h2>
<p>官方原话是，为处理注册申请，可能要求提供由政府签发的有效身份证件和信用卡信息，持有人必须都是你本人。核实后发现信息无效，注册费不退。</p>
<p>这条在国内常被忽略。拿别人的卡付款、或者用亲戚的证件补材料，都算信息不一致。25 美元不算多，但退不回来。</p>
<h2 id="设备验证一分钟但要一台真手机">设备验证：一分钟，但要一台真手机</h2>
<p>2024 年初起，新个人号在上架前必须先证明自己有一台真实的 Android 设备。做法是用 Play 管理中心的手机应用扫二维码，登录同一个 Google 账号，选中开发者账号，点验证。</p>
<p>门槛是 Android 10 或更高、没有 root 的实体设备。模拟器不算，root 过的机器也不算。官方说这一步不该超过一分钟，同一台设备可以验证多个账号，验证过程不会采集设备手机号。</p>
<h2 id="十二个人十四天">十二个人，十四天</h2>
<p>这条最容易被过期教程坑。很多文章还写着找二十个测试者连续测十四天，官方页面现在写的是：至少 12 名测试人员，在过去至少 14 天内选择连续参与封闭测试。</p>
<p>这个数字变过，中文教程大面积没跟上。按官方当前口径就是 12。</p>
<p>规则只对 2023 年 11 月 13 日之后创建的个人号生效。满足之后才能在信息中心申请正式版发布权限，申请时要回答应用设计、测试流程、上线准备这几类问题。</p>
<h2 id="9-月-30-日起验证的范围超出了-play">9 月 30 日起，验证的范围超出了 Play</h2>
<p>这段是这次真正的新东西。Android 开发者验证在 2026 年 9 月 30 日进入执行阶段，首批是巴西、印度尼西亚、新加坡、泰国四个国家，覆盖七家商店的分发：Google Play、荣耀应用市场、OPPO 软件商店、三星 Galaxy Store、传音 Palm Store、vivo 应用商店、小米 GetApps。</p>
<p>换句话说，认证设备上装应用从此要问一句，这个包是谁签的。官方给的时间线是 2027 年全球铺开。</p>
<p><img alt="规则是一层一层加上去的" loading="lazy" src="/images/gplay-dev-account/fig1.png"></p>
<p>它把开发者分成三条路，身份验证、费用、能分发到哪，各不相同。</p>
<p><img alt="三条分发路径的取舍" loading="lazy" src="/images/gplay-dev-account/fig3.png"></p>
<p>三个关键细节。</p>
<p>完整分发要在 Android Developer Console 单独开号，费用同样是 25 美元，和 Play 那笔是两回事。有限分发免费、不需要身份证件，但只能覆盖 20 台设备，用户还得接受邀请才能安装。未验证侧载那条路，用户可以自己走高级流程放行，官方把它描述成给用户争取时间和空间的一道缓冲。</p>
<p>还有一个细节对写代码的人最重要。官方 FAQ 写得很直白：用 ADB 安装应用不需要注册，也不需要验证。开发、调试、自己刷机测试，这条通道没关。企业托管设备上的应用同样不用走验证，IT 管理员已经审过了。</p>
<h2 id="注册包名要交的东西本机能先备好">注册包名要交的东西，本机能先备好</h2>
<p>新验证体系的第三步是注册包名，官方要求提供用你的私钥签名的 APK，以此证明这个包归你。这一步不用等账号批下来，本机就能预演。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">keytool -genkeypair -v -keystore demo.jks -storetype PKCS12 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -alias upload -keyalg RSA -keysize <span class="m">4096</span> -validity <span class="m">10000</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -dname <span class="s2">&#34;CN=Demo Dev, O=Demo, C=CN&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">keytool -list -v -keystore demo.jks <span class="p">|</span> grep SHA256
</span></span></code></pre></div><p>跑出来是一串 SHA-256，我这边生成的是 52:05:B5:73:…，后面还有三十个字节。这串指纹就是以后核对包名归属的东西，私钥一丢，它就再也对不上。所以从第一天起，密钥文件和它的备份要当成同一件东西管。</p>
<h2 id="跨平台开发者要算的账">跨平台开发者要算的账</h2>
<p>如果你用 Kotlin Multiplatform 或者 Flutter 一套代码打安卓，这里的账要分开算。Play 上架 25 美元，Play 之外分发再走 Android Developer Console，又 25 美元，两个号都要身份验证。</p>
<p>能省的地方在别处：设备验证、十二人十四天的测试，只跟个人号的正式版发布权限绑。组织号不背这一条。</p>
<p>真正省事的做法是上架照旧走 Play，同时在 Android Developer Console 把包名和签名密钥登记好。这样以后无论用户从哪个商店下、还是从你官网直装，安装时都不会被拦下来。</p>
<p>25 美元买的是一张门票。门票后面这几道关，才是今年真正花时间的地方。你开号时卡在哪一步？欢迎评论区聊聊。</p>
]]></content:encoded></item><item><title>Agent 的官方命令行：Android CLI 1.0</title><link>http://hitrevor.com/posts/android-cli/</link><pubDate>Wed, 14 Oct 2026 14:36:53 +0800</pubDate><guid>http://hitrevor.com/posts/android-cli/</guid><description>你让 agent 写了一段 Kotlin，它说改好了。然后呢？</description><content:encoded><![CDATA[<h2 id="先说一个断掉的地方">先说一个断掉的地方</h2>
<p>你让 agent 写了一段 Kotlin，它说改好了。然后呢？</p>
<p>你得自己开 Android Studio，等 Gradle 转圈，等模拟器起来，再用鼠标戳一下界面，看那句文案是不是真的变了。命令行里 agent 跑得飞快，到了看屏幕这一步就断了，剩下的活儿归你的眼睛。</p>
<p>Google 这次补的就是这一段。Android CLI 进了 1.0 稳定版，官方博客的原话是这样：</p>
<blockquote>
<p>无论您是在 Android Studio、Antigravity 2.0、Antigravity CLI 中使用 Gemini，还是使用 Anthropic 的 Claude Code 或 OpenAI 的 Codex 等第三方代理，我们的使命始终如一：确保随时随地都能进行高质量的 Android 开发。</p>
</blockquote>
<p>意思很直白：不管你用哪个 agent，官方都想让你别离开命令行。我在本机跑了一遍，从建工程一直跑到看屏幕。</p>
<h2 id="它交付了什么">它交付了什么</h2>
<p>下面这份清单，我按「它能替你的 agent 少做什么」来说。</p>
<h3 id="一个-android-命令">一个 android 命令</h3>
<p>装上之后你得到 <code>android</code> 这一个命令，19 个顶层子命令，建工程、起模拟器、装应用、看屏幕都在里面：</p>
<pre tabindex="0"><code>auth completion create describe device docs emulator info init
install layout run screen sdk skills studio update help
</code></pre><p>以前 agent 要记 adb、emulator、gradle 三套参数，现在一个入口对着一个 SDK。它只往用户目录装一个 3.6 MB 的单文件程序，装完 <code>android info</code> 就能报出 SDK 路径和版本。</p>
<h3 id="官方技能库">官方技能库</h3>
<p><code>android init</code> 这步有点意思。它没有任何参数，跑完却把一份 24,922 字节的 <code>android-cli</code> 技能写进了你机器上每一个 agent 的技能目录。我这边命中了 10 个：</p>
<pre tabindex="0"><code>~/.claude/skills          ~/.gemini/skills           ~/.agents/skills
~/.commandcode/skills     ~/.hermes/skills           ~/.junie/skills
~/.openclaw/skills        ~/.config/opencode/skills  ~/.pi/agent/skills
~/.gemini/antigravity/skills
</code></pre><p>Hermes 在里面，Claude Code、Codex、Gemini、OpenClaw、OpenCode 这些也都在。跑完建议看一眼它都往哪些目录写了东西，毕竟是往你的 home 里落文件。</p>
<p>技能库里现在有 24 个技能，<code>android skills list</code> 能列全。界面类的有 adaptive（自适应界面）、camerax（相机迁移）、edge-to-edge、wear-compose-m3（Wear OS）。</p>
<p>工程治理类的也有几个：play-policy-insights（政策解读）、restore-credentials、testing-setup、r8-analyzer。装上就一句 <code>android skills add &lt;技能名&gt;</code>，还能指定装给哪个 agent。</p>
<h3 id="离线的官方文档库">离线的官方文档库</h3>
<p><code>android docs search &quot;edge to edge&quot;</code> 第一次跑会下载索引，实测 5,319 条。返回的不是网址，是 <code>kb://android/...</code> 这种协议地址。</p>
<p>连技能里的示例代码都在里面，比如 <code>kb://android/agents/skills/wear/wear-compose-m3/references/material3/EdgeButtonSample.kt</code>。agent 不用再去搜博客，官方文档本地可查。</p>
<h3 id="看屏幕这组命令">看屏幕这组命令</h3>
<p>这组是整篇里最实用的。<code>screen capture --annotate</code> 会把界面上的元素标成 <code>#1</code>、<code>#2</code>，<code>screen resolve</code> 再把标号换成坐标：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">android --no-metrics screen capture --annotate --output shots/12-annotated.png
</span></span><span class="line"><span class="cl">android --no-metrics screen resolve --screenshot shots/12-annotated.png --string <span class="s2">&#34;tap #1 and #2&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 输出：tap 72 31 and 835 30</span>
</span></span></code></pre></div><p>agent 不必再算像素，指着 <code>#3</code> 说点这个就行。同一组的 <code>layout</code> 直接返回界面 JSON 树，每个控件的 <code>bounds</code> 和中心点都在。</p>
<h3 id="借用-android-studio-的语义分析">借用 Android Studio 的语义分析</h3>
<p>新增的 <code>android studio</code> 子命令有这些：<code>find-declaration</code> 找定义、<code>find-usages</code> 找引用、<code>analyze-file</code> 跑检查、<code>render-compose-preview</code> 渲染 Compose 预览、<code>version-lookup</code> 查依赖版本、<code>open-file</code> 在 IDE 里开文件。</p>
<p>等于 agent 能借到 IDE 攒了十几年的语义分析，不用靠正则猜。这条有门槛，要求 Android Studio Quail 1 或更高。</p>
<h3 id="预约一台真机">预约一台真机</h3>
<p><code>android device remote</code> 可以预约远程真机来跑，官方博客贴的例子是连着一台 Pixel 10 Pro。这条路要先 <code>android auth</code> 登录 Google 账号。</p>
<h2 id="我在-macos-上从零跑了一遍">我在 macOS 上从零跑了一遍</h2>
<p>能不能跑通才算数。下面这套是我本机实跑的命令，一条没省：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 装：先把脚本下下来读一遍，别直接 pipe 进 shell</span>
</span></span><span class="line"><span class="cl">curl -fsSL -o /tmp/android-cli-install.sh <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://dl.google.com/android/cli/latest/darwin_arm64/install.sh
</span></span><span class="line"><span class="cl">bash /tmp/android-cli-install.sh            <span class="c1"># → ~/.local/bin/android，3,650,880 字节</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">android --no-metrics -V                     <span class="c1"># 1.0.16500706，首次要过一次 Android SDK 条款</span>
</span></span><span class="line"><span class="cl">android init                                <span class="c1"># 把技能写进各 agent 的技能目录</span>
</span></span><span class="line"><span class="cl">android --no-metrics skills list            <span class="c1"># 官方技能目录，实测 24 个</span>
</span></span><span class="line"><span class="cl">android --no-metrics docs search <span class="s2">&#34;edge to edge&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 端到端：建工程 → 起模拟器 → 出包 → 装上去 → 看屏幕</span>
</span></span><span class="line"><span class="cl">android --no-metrics create --name CliDemo <span class="se">\
</span></span></span><span class="line"><span class="cl">  --application-id com.example.clidemo --min-sdk <span class="m">36</span> -o ~/android-cli-repro/CliDemo
</span></span><span class="line"><span class="cl">android --no-metrics emulator list          <span class="c1"># 我这儿已有的 AVD：Medium_Phone_API_36.1</span>
</span></span><span class="line"><span class="cl">android --no-metrics emulator start Medium_Phone_API_36.1   <span class="c1"># 等到就绪才返回，约 17 秒</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/android-cli-repro/CliDemo
</span></span><span class="line"><span class="cl">./gradlew :app:assembleDebug                <span class="c1"># 首次：BUILD SUCCESSFUL in 1m 24s</span>
</span></span><span class="line"><span class="cl">android --no-metrics run --apks app/build/outputs/apk/debug/app-debug.apk
</span></span><span class="line"><span class="cl">android --no-metrics screen capture --output shots/10-before.png
</span></span><span class="line"><span class="cl">android --no-metrics layout
</span></span></code></pre></div><p>先说建工程。<code>create</code> 出来的工程是 Compose + AGP 9 + Kotlin 2.3.20，我都没去 SDK Manager 里点，它自己发现缺 <code>platforms/android-36</code> 就装上了。</p>
<p><code>emulator start</code> 这个设计挺舒服：一直等到设备完全启动才返回，中间只打印剩余秒数，不用自己轮询 adb。我这边 17 秒就绪。</p>
<p>真正让我意外的是第二次。我改了一行文案，把数据源里那句 <code>listOf(&quot;Android&quot;)</code> 改成 <code>listOf(&quot;Android CLI&quot;)</code>，重跑一轮：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">./gradlew :app:assembleDebug                <span class="c1"># 增量：BUILD SUCCESSFUL in 8s</span>
</span></span><span class="line"><span class="cl">android --no-metrics run --apks app/build/outputs/apk/debug/app-debug.apk
</span></span><span class="line"><span class="cl"><span class="c1"># Delta Install: Sent 167.94 KB patch instead of 28.67 MB (1 APK, 99.43% savings)</span>
</span></span></code></pre></div><p>第一次装传了 28.64 MB，第二次只传了 167.94 KB 的补丁，省了 99.43%。增量安装默认就是开的（<code>--use-delta-install</code> 默认 true），改文案、看效果这种来回折腾的活儿，省下的全是等待时间。</p>
<p>改完再用 <code>layout</code> 读界面树，文案已经换新的了：</p>
<pre tabindex="0"><code>应用窗口: CliDemo  焦点=True  边界=[0,0][1080,2400]
  - android.widget.TextView | text=&#39;Hello Android CLI!&#39; | center=[220,136]
</code></pre><p><img loading="lazy" src="/images/android-cli/fig-loop.png"></p>
<p><img loading="lazy" src="/images/android-cli/fig-agent.png"></p>
<h2 id="上手门槛和几个坑">上手门槛和几个坑</h2>
<p>最容易绊人的是 <code>run</code>。它的说明写「构建、部署、启动」，我这里不带 <code>--apks</code> 直接失败：</p>
<pre tabindex="0"><code>Error: No apks specified. Use --apks &lt;path1,path2,...&gt;
Component check failed
</code></pre><p>APK 已经构建好、<code>describe</code> 也刚定位到它，<code>run</code> 还是不肯自己动手。实际分工是出包归工程自己的 <code>./gradlew</code>，<code>run</code> 负责装和起。</p>
<p>还有一个更隐蔽的。包部署上去了，界面却没变，旧进程还活着，<code>run</code> 不会替你强杀。补一条 <code>adb shell am force-stop com.example.clidemo</code> 再跑就对了。</p>
<p>版本门槛也在。<code>android studio</code> 那组命令要 Android Studio Quail 1 或更高，版本不够报的是 <code>No running Studio instances found</code>，还附一句让你去官网下新版。</p>
<p>另外两点：<code>android init</code> 是往你的 home 目录里写文件，不是只读操作，机器上 agent 多的话建议先看清名单；子命令是单数 <code>device</code>，写成 <code>android device list</code> 会报 <code>'list' is not a valid command</code>。</p>
<p>还有 <code>android completion</code> 会往 shell 配置里写自动补全，不想动配置就别跑它。</p>
<p>遥测也得提一句。这个 CLI 默认会收数据，官方文档写了收什么：命中的子命令、用到的参数名，以及一部分固定取值，比如模拟器模板名和 agent 名（<code>GEMINI</code>、<code>CLAUDE</code>、<code>CODEX</code>）。不想要的加 <code>--no-metrics</code>，我上面所有命令都带着。</p>
<h2 id="最后一句">最后一句</h2>
<p>这版没有另搞一套给 agent 用的流程，改的是原本那套东西：文档、技能、模拟器、真机、IDE 的语义分析，都换成了机器能读的接口。</p>
<p>细节上也照顾到了，像素坐标换成 <code>#3</code> 这种标号，UI 树直接给 JSON。</p>
<p>你手上那套工具链，敢不敢把「改代码到看屏幕」这一段整个交给 agent？评论区聊聊。</p>
]]></content:encoded></item><item><title>安卓侧载收紧，跨平台开发者的分发账要重算了</title><link>http://hitrevor.com/posts/android-distro-cost/</link><pubDate>Fri, 09 Oct 2026 07:39:00 +0800</pubDate><guid>http://hitrevor.com/posts/android-distro-cost/</guid><description>一个跨平台项目到了发版的时候，分发路径通常不止一条。Google Play 一条，官网直发一条，有时候还要给测试同学一个内测包。安卓这套开发者验证，把每条路的准入成本重新标了一次价，而这三笔账的算法完全不一样。</description><content:encoded><![CDATA[<p>一个跨平台项目到了发版的时候，分发路径通常不止一条。Google Play 一条，官网直发一条，有时候还要给测试同学一个内测包。安卓这套开发者验证，把每条路的准入成本重新标了一次价，而这三笔账的算法完全不一样。</p>
<h2 id="三条路三笔账">三条路，三笔账</h2>
<p>先说走 Google Play 这条。对已经有 Play 开发者账号的团队，这笔账几乎是零。应用已经在验证流程里，用了 Play App Signing 的包名还会被自动认领，你要做的只是去开发者验证页确认状态。新增的成本是时间，审核周期照旧。</p>
<p>再说侧载这条。用户在浏览器里下你的 APK 直接装，2026 年 9 月 30 日之后在四个国家要过身份校验：包名必须注册过，而且这个包得由你的私钥签名。</p>
<p>官方留了出口，Android Debug Bridge 的开发安装不受影响，advanced flow 让用户可以一次性放行，免费的有限分发账号能覆盖 20 台设备。代价在于，advanced flow 一旦关掉，未注册应用的更新就会失败。留给测试同学的那些包，得算进这 20 台里。</p>
<p>然后是走企业内部分发这条。托管设备从组织商店安装应用，官方给了豁免，前提是设备真的被管着。如果同一个应用还会流到员工自己没托管的手机上，建议照样注册。这条对跨平台团队的意义在于，内部测试包和演示包也算包名和身份，不能靠「内部用」蒙过去。</p>
<h2 id="跨平台团队的账为什么更复杂">跨平台团队的账为什么更复杂</h2>
<p>第一是包名数量。KMP 和 Flutter 项目一个仓库常常出好几个产物，主包、各种 flavor、内测包，每个都有自己的包名。验证是按包名和签名密钥走的，所以清单要覆盖所有产物和所有密钥，而不是只覆盖上架那一个。</p>
<p>第二是密钥来源。同一个团队里可能同时存在 CI 机器上生成的 keystore、某位同学本地留着的 keystore、以及 Play App Signing 托管的那份。名字一多，就容易出现「发出去的包不是这把密钥签的」。</p>
<p>第三是提交物。这套验证最终要你提交的只有两样：包名，和用你私钥签名的 APK。前者要查重，后者要能自证。</p>
<p>顺便说清楚边界，这套验证只管 Android 认证设备上的包名。如果同一个项目还发鸿蒙或者 iOS，那两边的签名材料是另一套体系，不在这套规则里。但「归档密钥加能自查」这个动作，两边都该做。</p>
<h2 id="第一件事密钥归档并且能自证">第一件事：密钥归档，并且能自证</h2>
<p>注册时要证明包名归你，官方给的凭据是「用你的私钥签名的 APK」。也就是说签名密钥从构建缓存里的东西，升格成了身份证。官方 FAQ 专门写了一条，密钥丢了就没法注册包名，建议用密钥管理方案。</p>
<p>光归档还不够，你得能随时自证。我写了一个 30 行的小脚本，跑一次就能看出一个 APK 的包名和证书指纹，还能跟归档的指纹清单对比。同一份代码、同一个包名，换一把密钥签，输出的结果就分叉了：</p>
<p><img loading="lazy" src="/images/android-distro-cost/terminal.png"></p>
<p>包名一模一样，指纹完全不同，这就是「用错了密钥」的样子。这种事在团队里发生的概率比想象中高，尤其在换了 CI 机器或者有人重新生成了 keystore 之后。</p>
<p>归档建议就三条。keystore 文件、别名、指纹清单放一起，进密码管理器或者团队保险库；指纹清单用纯文本，一行一个 SHA-256，方便机器比对；这一步挂进发布流程当门禁，比对不过就不许发。官方文档里还提到，一个包名可以登记多把签名密钥，团队做密钥轮换时用得上。</p>
<h2 id="第二件事上架前先查包名有没有被占">第二件事：上架前先查包名有没有被占</h2>
<p>包名是先到先得认领的，撞名这件事要在起名的时候查，不是在上架那天查。</p>
<p>Google 开了 Android Developer ID Status API 专门查这个，返回三种状态：已注册、未注册、以及已注册但对应的是另一把证书指纹。最后一种最麻烦，说明包名被别人占了。</p>
<p>撞名之后官方给的路是申请注册，但要额外审核，而且文档里明说了，那个包名可能同时被其他开发者使用。两个应用共用一个包名意味着什么，做过 Android 的都清楚。</p>
<p>这个接口有配额，每个项目每天 1000 次，要挂在 Google Cloud 项目上：先建项目、开通对应的 API、配好鉴权。我本机没有这个条件，所以这条只按文档写，我没有实际发起过请求。日常看包名状态，Play Console 和 Android Developer Console 的包名列表里直接就有，显示为已注册、未注册或者草稿。</p>
<h2 id="第三件事账号和材料能办的先办">第三件事：账号和材料，能办的先办</h2>
<p>个人开发者就是身份验证这一步。组织要多两样：D-U-N-S 编号，以及一个用 Search Console 验证过的官网。</p>
<p>D-U-N-S 免费，去邓白氏官网就能申请，但流程最长 28 天。这是整份清单里唯一一个等待时间压不掉的环节，官方帮助文档还特意提醒，当心第三方服务拿这件事收高额手续费。</p>
<p>费用方面，完整分发账号 25 美元一次性，和 Play 的注册费一个价位；有限分发免费。钱和填表都不是大问题，真正的时间成本在前面那 28 天。</p>
<h2 id="三个会让你卡住的坑">三个会让你卡住的坑</h2>
<p>第一个是密钥丢失。这不是「麻烦一下」，是没法注册包名，等于这个包名你再也认不回来。</p>
<p>第二个是包名被别人先注册。申请之后可能和对方共用，等于埋了一颗不知道什么时候炸的雷。</p>
<p>第三个藏在有限分发这条路上。advanced flow 关掉之后，未注册应用的更新就会失败。官方 FAQ 写得很明确，未注册的应用只能靠 advanced flow 或者 Android Debug Bridge 安装与更新。那 20 台设备的场景，记得别把开关关掉。</p>
<h2 id="哪些现在做哪些可以等">哪些现在做，哪些可以等</h2>
<p>已经生效的是四个国家、七家参与商店（Google Play、三星 Galaxy Store、小米 GetApps 这些），覆盖 Android 7 以上的认证设备，第一阶段只管手机和平板两种形态。</p>
<p>现在就能做的三件事：归档密钥并接上自查、查包名有没有被占、把 D-U-N-S 和账号材料办起来。</p>
<p>可以等的部分：不在名单里的商店，以及用户直接侧载，在这一轮铺开前不受管。官方给的时间表是 2027 年及以后全球铺开，也建议开发者在铺开前完成验证；媒体口径里的节奏是先到美英。上了 Play 的应用要对所有形态注册，只是执行层面先落手机和平板。</p>
<p>有个措辞值得记住，官方 FAQ 写的是「暂时不适用」，不是「永远不会」。这两句话之间的差距，就是 2027 那一格。</p>
<h2 id="最后">最后</h2>
<p>这三笔账算下来，最贵的不是那 25 美元，是身份材料的准备时间，以及签名密钥的不可替代性。填表十分钟，D-U-N-S 要 28 天，密钥丢了就没有第二把。所以对一个跨平台团队来说，它不该是上架前才想起来的事，而该是发布流程里的一步。</p>
<p>你手上的项目走的是哪条路？那把签名密钥现在找得到吗，评论区聊聊。</p>
<p>参考：</p>
<ul>
<li>Android developer verification 官方指南 <a href="https://developer.android.com/developer-verification/guides">https://developer.android.com/developer-verification/guides</a></li>
<li>官方 FAQ <a href="https://developer.android.com/developer-verification/guides/faq">https://developer.android.com/developer-verification/guides/faq</a></li>
<li>查包名注册状态（Developer ID Status API）https://developer.android.com/developer-verification/guides/check-registration-status</li>
<li>用 Console API 注册包名 <a href="https://developer.android.com/developer-verification/guides/developer-console-api">https://developer.android.com/developer-verification/guides/developer-console-api</a></li>
</ul>
]]></content:encoded></item><item><title>Android 侧载的苹果化只走了一半：要身份，不审内容</title><link>http://hitrevor.com/posts/android-verify/</link><pubDate>Thu, 08 Oct 2026 08:14:49 +0800</pubDate><guid>http://hitrevor.com/posts/android-verify/</guid><description>如果你有朋友在巴西、印尼、新加坡或者泰国，9 月 30 日之后他可能会遇到一种新情况：从某个应用商店点安装，先弹一句开发者未验证，然后装不上。同一个时间点，你手上这台国行安卓机，装什么都不会有变化。</description><content:encoded><![CDATA[<p>如果你有朋友在巴西、印尼、新加坡或者泰国，9 月 30 日之后他可能会遇到一种新情况：从某个应用商店点安装，先弹一句开发者未验证，然后装不上。同一个时间点，你手上这台国行安卓机，装什么都不会有变化。</p>
<p>中文技术圈对这件事聊得不多，英文社区已经吵了一些日子。Hacker News 上 10 月 1 日那条帖子拿到 338 分、141 条评论，自由软件阵营的 F-Droid 联合电子前哨基金会（Electronic Frontier Foundation）发了公开信，标题就一句，反对 Android developer verification。</p>
<p>所以这条值得拆开看。它改了什么，谁真的会疼。</p>
<h2 id="官方原话应用要由已验证的开发者注册">官方原话：应用要由已验证的开发者注册</h2>
<p>Google 的说法很直接，<a href="https://developer.android.com/developer-verification/guides">官方指南</a>第一句就是，Android 要求所有应用都由已验证的开发者注册，用户才能把它装进认证 Android 设备。</p>
<p>几个边界先划清楚：</p>
<ul>
<li>生效时间 9 月 30 日，先落巴西、印尼、新加坡、泰国四个国家</li>
<li>覆盖七家商店，Google Play、HONOR 应用市场、OPPO 应用市场、三星 Galaxy Store、传音 Palm Store、vivo 应用商店、小米 GetApps</li>
<li>管的是 Android 7 以上的认证设备，检查通过 Google Play services 下发</li>
<li>这一轮只覆盖手机和平板两种形态</li>
<li>2027 年全球铺开，并把验证能力扩展到所有第三方 Android 商店</li>
</ul>
<p>有个细节比表面重要：这轮只管上面这七家商店。官方 FAQ 写得很清楚，如果你从其他渠道分发，或者用户直接侧载，这些新要求暂时不管你。</p>
<p>也就是说，侧载的路没被堵，是入口处多了个证件查验。</p>
<p><img loading="lazy" src="/images/android-verify/timeline.png"></p>
<p>Google 自己给的比喻也挺坦白，他们说这像机场查证件，和安检是两件事，确认的是开发者是谁，不审应用的内容，也不审它从哪儿来。</p>
<h2 id="三条路你得挑一条">三条路，你得挑一条</h2>
<p>官方给了三条路径，差别在身份验证这一栏。</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>完整分发</th>
					<th>有限分发</th>
					<th>侧载未注册应用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>适合谁</td>
					<td>组织与职业开发者</td>
					<td>学生、爱好者、个人</td>
					<td>不愿验证的开发者</td>
			</tr>
			<tr>
					<td>要不要身份验证</td>
					<td>要</td>
					<td>不要</td>
					<td>不要</td>
			</tr>
			<tr>
					<td>分发范围</td>
					<td>任何商店或自有网站</td>
					<td>最多 20 台设备</td>
					<td>任何渠道，但用户要开 advanced flow</td>
			</tr>
			<tr>
					<td>用户体验</td>
					<td>和今天一样</td>
					<td>接受邀请后安装</td>
					<td>一次设置，之后能装</td>
			</tr>
	</tbody>
</table>
<p>完整分发要交身份信息，开发者控制台账号 25 美元一次，注册成组织还要 D-U-N-S 编号，申请最长要等 28 天，另外得有一个用 Google Search Console 验证过的官网。</p>
<p>有限分发是给不想交政府证件的人留的口子，免费，不限应用数量，但只能装到 20 台设备上，用户得接受你的邀请。学生、爱好者、自己写给自己用的人走这条最省事。</p>
<p>第三条路是给用户的：未注册开发者的应用仍然能装，但用户要先做一次 advanced flow 设置，官方说这些保障是给用户留出中断胁迫的时间。</p>
<p>有一处官方说法要留意，advanced flow 一旦关掉，未注册应用的更新就会失败。这条路只适合当终点，不适合当日常。</p>
<h2 id="我拆了一个-apk验证的对象其实就两样东西">我拆了一个 APK，验证的对象其实就两样东西</h2>
<p>讲概念不如自己跑一遍。我本机的 Android SDK 里有 aapt2、d8、zipalign、apksigner 和 keytool，够手搓一个 APK 了，没借助 Android Studio。</p>
<p>流程是：写一个 AndroidManifest.xml，里面只有一行包名 com.example.handmade，javac 编一个类，d8 转成 classes.dex，aapt2 link 生成 APK，最后用 keytool 现造一对 RSA 密钥，apksigner 签名。</p>
<p>签完把身份拆出来看：</p>
<pre tabindex="0"><code>Signer #1 certificate DN: CN=Handmade Identity Demo, O=none, C=CN
Signer #1 certificate SHA-256 digest:
0a220aaa9b719f34f97e88cb46a95de020fba812fee540251cc2cae7a188a269
Verified using v2 scheme (APK Signature Scheme v2): true

package: name=&#39;com.example.handmade&#39; versionCode=&#39;1&#39; versionName=&#39;1.0&#39;
</code></pre><p>一个 8619 字节的 APK，身份就是这两行，包名 com.example.handmade，加证书指纹 0a22 开头那串 SHA-256。Google 要你登记的就是这一对东西，官方说法是提供用你私钥签名的 APK，把这个 APK 和你的开发者账号连起来。</p>
<p><img loading="lazy" src="/images/android-verify/terminal.png"></p>
<p>再做一个对照就更有意思了。</p>
<p>我拿没签名的那个 APK 去验，直接报 DOES NOT VERIFY，缺 META-INF/MANIFEST.MF。然后我用第二把密钥签同一份 APK，代码一个字没改，包名也没变，证书指纹从 0a22 开头换成了 ee56 开头。</p>
<p>所以验证验的是人，不是代码。这也解释了官方 FAQ 为什么专门写一条，签名密钥丢了就没法注册包名。那把 keystore 文件不是构建缓存，是身份证，得跟隐私数据一样归档。</p>
<h2 id="这不是-app-store-化是-macos-公证化">这不是 App Store 化，是 macOS 公证化</h2>
<p>很多人第一反应是 Google 苹果化了。这个判断只说对了一半。</p>
<p>像的地方确实是「分发要挂账号」。苹果那边每个安装路径都挂在 Apple Developer Program 上，99 美元一年，组织同样要 D-U-N-S。macOS 上更直接，Gatekeeper 首次运行联网查一次公证票据，苹果在欧盟放开的第三方市场，也要求在安装时确认来源是授权渠道。</p>
<p>不像的地方才是关键，而且有两处。</p>
<p>第一，Google 不审内容。官方帮助文档里明确写了，不收集应用内容和功能信息。苹果那套是审查加抽成，内容规则一大堆，欧盟最新的条款里，走应用内购买抽 26%，走替代支付抽 20%，跳链接出去抽 15%，走第三方市场或网页分发再收 5%。</p>
<p>第二，Google 留了免身份的出口，20 台设备的有限分发、advanced flow、还有完全不受影响的 adb 开发安装，苹果那边没有对应物。</p>
<p>方向上还有个对照挺有意思。苹果在欧盟是被数字市场法按着往开走，Google 这边是主动往收。这不是谁学谁，是两条方向相反的曲线在中间碰头。</p>
<p>真正值得盯的是那句话，验证能力会扩展到所有第三方 Android 商店。到时候 Google 不只是自己院子的门卫，还是别人家商店的身份签发机关。</p>
<h2 id="谁会疼">谁会疼</h2>
<p>普通用户不会。体验真的没变，这也是反对者最难讲通的地方。</p>
<p>会疼的是另外三拨人。F-Droid 这样的非营利仓库首当其冲，他们的公开信里写得很直白：</p>
<blockquote>
<p>在认证设备上安装或启动任何应用，都会回连 Google，验证开发者和应用是否已被批准。</p>
</blockquote>
<p>他们托管几千个应用，验证按包名绑定，意味着每个志愿者都要把身份交给 Google。他们的结论是建议开发者不要注册。</p>
<p>第二拨是没法完成验证的人。官方 FAQ 自己承认，制裁地区的开发者无法完成验证。离线环境、拿不出证件、或者所在地区根本走不通流程的人，等于被排除在分发之外。</p>
<p>第三拨是未来任何想跟 Play 竞争的分发方，他们连上线都要先过 Google 的注册流程。</p>
<h2 id="对国内开发者三条实际动作">对国内开发者，三条实际动作</h2>
<p>第一，如果你的应用只上 Google Play，而且用了 Play App Signing，你已经在这套流程里了，官方会自动认领，不需要额外操作。想确认状态，去 Play Console 的开发者验证页看一眼就行。</p>
<p>第二，如果你有海外用户、又走非 Play 渠道，现在就该做的事是把签名密钥归档管理起来，把 D-U-N-S 提前申请了，把 2027 当真正的截止日期，别盯着今年九月。</p>
<p>第三，只写给自己或小圈子用的应用，adb 这条官方保留，advanced flow 也能用，但记住更新未注册应用要靠它，别关掉。</p>
<p>还有个查询接口值得知道，Google 开了 Android Developer ID Status API，可以查某个包名有没有被注册，每天 1000 次。它要挂在 Google Cloud 项目上才能调，我本机没这个条件，所以只当线索记着。对做分发的人来说，它意味着装机前可以先问一句这个包名归谁。</p>
<h2 id="最后">最后</h2>
<p>这不是一次安全检查，是分发的守门权从操作系统挪进了 Google 的账号体系。对普通用户几乎无感，对非主流分发生态是结构性打击。</p>
<p>2027 年全球铺开才是分水岭，现在这四国只是一个成本很低、随时可调的试点。</p>
<p>你手上要是有个只在国内分发的应用，会去注册吗，还是等 2027 再说？评论区聊聊。</p>
<p>参考：</p>
<ul>
<li>Android developer verification 官方指南 <a href="https://developer.android.com/developer-verification/guides">https://developer.android.com/developer-verification/guides</a></li>
<li>官方 FAQ <a href="https://developer.android.com/developer-verification/guides/faq">https://developer.android.com/developer-verification/guides/faq</a></li>
<li>F-Droid 公开信 <a href="https://f-droid.org/en/2026/02/24/open-letter-opposing-developer-verification.html">https://f-droid.org/en/2026/02/24/open-letter-opposing-developer-verification.html</a></li>
<li>Hacker News 讨论 <a href="https://news.ycombinator.com/item?id=49917761">https://news.ycombinator.com/item?id=49917761</a></li>
</ul>
]]></content:encoded></item><item><title>Flutter 1.7.8 稳定版本发布</title><link>http://hitrevor.com/posts/flutter-1-7-8/</link><pubDate>Tue, 09 Jul 2019 09:00:00 +0800</pubDate><guid>http://hitrevor.com/posts/flutter-1-7-8/</guid><description>Flutter 1.7.8 稳定版本发布，修复了各种 bug</description><content:encoded><![CDATA[<p>Flutter 1.7.8 最新稳定版发布了, 最主要修复了在<a href="https://github.com/flutter/flutter/issues/18494">同一 APK 同时支持 32 位和 64 位的bug</a>， 这样就不用 使用这种黑科技来打包支持国内市场了，真香！！</p>
<p>修复的其他一些 bug 列表（部分）</p>
<h3 id="v176">v1.7.6</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/34684">#34684</a> The return type of the almost never used <code>ParentDataWidget.debugDescribeInvalidAncestorChain</code> method is now <code>Iterable&lt;DiagnosticsNode&gt;</code> instead of <code>String</code>. Existing uses can call <code>.join('\n')</code> on the return value if they need to continue to use a <code>String</code> instead of a <code>DiagnosticsNode</code>.</li>
</ul>
<h3 id="v174">v1.7.4</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/30069">#30069</a> Text inline widgets allows embedding arbitrary widgets inline with text. <code>TextSpan</code>has also been reworked to inherit from <code>InlineSpan</code>, in order to support <code>WidgetSpan</code>, which is used to define inline widgets. Existing uses of <code>TextSpan</code> should not assume the object is of type <code>TextSpan</code> anymore.</li>
</ul>
<h3 id="v171">v1.7.1</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/issues/33480">#33480</a> The <code>FadeInImage</code> widget no longer supports the <code>placeholderSemanticLabel</code>property.</li>
</ul>
<h3 id="v167">v1.6.7</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/issues/32374">#32374</a> The <code>Image</code> widget now supports two new properties: <code>frameBuilder</code>, and <code>loadingBuilder</code>, which enable callers to easily add a placeholder image which the image ios loading, add effects to the image once it loads (such as fading it in), and monitor the loading progress of the image (e.g. to show the user a loading indicator).</li>
</ul>
<h3 id="v163">v1.6.3</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/33148">#33148</a> ExpandIcon now uses Colors.white60 instead of Colors.white54 when dark theme is used (ThemeData.brightness is set to Brightness.dark).</li>
</ul>
<h3 id="v162">v1.6.2</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/32936">#32936</a> cleaned up the <code>ImageStream</code> listener API. <code>addListener()</code> and <code>removeListener()</code>now take an instance of <code>ImageStreamListener</code>, which contains references to the individual callbacks. See <a href="https://groups.google.com/forum/#!topic/flutter-announce/NWTszrEq9U0">the announcement</a> for more information.</li>
</ul>
<h3 id="v161">v1.6.1</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/32528">#32528</a> Tapping a modal bottom sheet no longer dismisses it by default.</li>
</ul>
<h3 id="v160">v1.6.0</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/30983">#30983</a> To support structured error messages <a href="https://github.com/flutter/flutter/issues/27327">#27327</a> the signature of InformationCollector and the context parameter of the FlutterErrorDetails constructor were changed. This should not impact you unless your package is attempting behave like part of the Flutter framework. See <a href="https://github.com/flutter/flutter/issues/31962#issuecomment-488882515">this comment</a> which discusses how to write code that is forwards and backwards compatible with this change.</li>
<li><a href="https://github.com/flutter/flutter/pull/31569">framework #31569</a>/<a href="https://github.com/flutter/engine/pull/8695">engine #8695</a> Rect and RRect are now const constructable and are backed by 64 bit doubles rather than 32 bit floats internally.</li>
<li><a href="https://github.com/flutter/flutter/pull/31917">framework #31917</a> &lt;- <a href="https://github.com/flutter/engine/pull/8796">engine #8796</a> &lt;- <a href="https://github.com/dart-lang/sdk/commit/1dd0f88c84">dart 1dd0f88c84</a> Fixes bug in StreamIterator which allowed constructor argument to be null. Also allowed await for on a null stream. This is now a runtime error.</li>
<li><a href="https://github.com/flutter/flutter/pull/30884">#30884</a> <code>TabController.animationController</code> changed to be <code>unbounded</code> to allow <code>DefaultTabController</code> to handle changes to number of tabs (after initial build).</li>
</ul>
<h3></h3>
]]></content:encoded></item></channel></rss>