WWW.YOUINFO.SITE
标签聚合 窄路

/tag/窄路

v2ex · 2026-06-05 11:48:00+08:00 · tech

那是一个周三凌晨两点。 我盯着 Android Studio 的 logcat, 第八次发现: 时间到了, 我写的那个"锁屏页"压根没起来。 我家娃的手机静静地躺在桌上, 他常刷的那个短视频还在继续播。 我打开侧锁屏, 确认我的代码确实运行了 —— Activity 启动指令发出去了, 系统回了一个我没见过的 warning, 然后什么都没发生。 那一瞬间我有一个挺荒谬的念头: 我做这个 App 一年, 是不是从一开始就走在一条已经被堵死的路上 ? 一、我以为这是一个 Android 入门题 我做儿童 App, 核心功能就一句话: 时间到了, 不让孩子切回去继续刷。 我当时的设计极其朴素 —— 起一个 LockActivity , 铺满屏幕, 写一个 PIN 输入框。家长输 PIN, 孩子继续玩; 不输 PIN, 就一直挡着。 按 Android 的标准做法, 时间到了就调用 startActivity() 把这个 Lock 页拉起来。 如果担心系统不给拉起, 那就再加一个 fullScreenIntent 通知 —— 全屏意图, 专门给闹钟、来电、紧急提醒这类场景用, 系统会强制把你的 Activity 拉到最前面。 我写了, 跑了, 在我自己的 Pixel 上一次过。 那天晚上我特别开心, 跟我老婆说: "核心功能搞定了。" 她问: "就这么简单?" 我说: "就这么简单。Android 给了现成的 API 。" 那是我整个项目里, **最后一次说"就这么简单"**。 二、Android 12 之后, 这条路被一刀切了 第二天我换了一台手机测, Android 12 的。 LockActivity 没起来。 我以为是我代码写错了。 调了一整天, 找不到原因。所有的日志都告诉我"启动成功", 但屏幕上什么都没有。 第三天我才在 Google 的官方文档里翻到一篇说明 —— Android 12 之后, 引入了一个叫 BAL 的东西, 全称 Background Activity Launch 限制。 翻译成人话就是: 从后台拉起一个 Activity 这件事, 被默认禁止了 。 只剩极少数情况能起来: 用户刚跟你的 App 交互完不到几秒、有可见的 Window 、是系统级权限。我的"时间监控服务"在后台跑, 时间到了想拉起一个 Activity —— 全部不符合。 那 fullScreenIntent 呢? 我加了啊。 我又翻了一晚上文档。结论是: 从 Android 10 开始, 系统会 悄悄把全屏意图降级成一条普通通知 , 除非用户在设置里手动给你这个 App 开权限。 这两条加在一起, 等于 Android 12 之后, 我设计的整条阻断路径 —— 从代码逻辑上是对的, 从用户感受上完全不存在 。 三、为什么 Google 要这么做? 我得承认它是对的 我有一个晚上在床上想这件事, 越想越烦躁。 我做的是儿童保护, 又不是恶意软件, 凭什么把我也一刀切? 但我后来去看了那个 BAL 引入的初衷 —— 当时市面上有大量的 App 在后台偷偷拉起广告 Activity 。你把手机放桌上, 它突然弹一个全屏视频广告。还有更坏的, 模拟登录页、模拟支付页, 专门做钓鱼。 Google 是被这些东西逼到必须出手的。 我能理解。 但理解归理解 —— 理解不能让我家娃的手机在时间到了之后真的停下来 。 这是做底层产品的一个尴尬: 你的产品哲学跟系统的产品哲学冲突的时候, 输的永远是你。 Google 不会因为我做的是"温柔的儿童 App"就给我开后门。在系统眼里, 我跟那些钓鱼 App 是同一种东西 —— 想在用户没主动操作的情况下, 强行占据屏幕 。 那段时间我反复问自己一个问题: 我做的这件事, 从系统的角度看, 到底是不是合理的 ? 我得承认, 从系统的角度, 它不合理。 但从一个父亲的角度, 它必须合理。 这两个"合理"之间的那条窄缝, 就是我接下来要找的路。 四、能走的路只剩三条, 前两条都不能选 我列出了所有理论上能阻断 App 切换的方案: **第一条, Accessibility (无障碍服务)**。这是 Android 给残障辅助用的, 能读到全局的应用切换事件, 理论上能"看到孩子切走"然后阻断。 但 Google Play 这两年专门盯这条 —— 任何不是真为残障人士设计的 App, 用了无障碍权限都会被下架。我看过太多儿童管控类、防沉迷类的 App, 因为这个被一夜清掉。 这条路死了, 是产品死, 不是技术死 。 第二条, 双进程互锁 。我开两个 Service, 一个挂了另一个把它拉起来。 这个技术上能做, 但它是 Google Play 政策里明确标红的"abusive behavior"。短期能活, 等于在一颗定时炸弹上盖房子。 死了, 是商业死 。 第三条, SYSTEM_ALERT_WINDOW 。也就是悬浮窗权限 —— 你常见的微信视频通话小窗、滴滴司机端的接单浮窗, 用的就是这个。 它的本质是: 让我的 App 可以在 别的 App 上面 画一层东西。 我不需要"启动一个 Activity", 我只需要"在你正在用的那个 App 上面, 盖一块布"。 这块布我自己控制 —— 大小、内容、能不能点穿。 这条路活着。但它有它自己的代价。 五、悬浮窗这条窄路, 本身也有三个大坑 我以为找到了 SYSTEM_ALERT_WINDOW 就解放了。 不是。 第一个坑: 权限要用户手动去设置里开 。装完 App 不会主动给你, 要跳到一个深埋在系统设置里的页面, 让用户找到你的 App 名字、打开开关。我做了三版引导, 从最初的 5 步跳转到现在的 1 步, 光是这个引导我返工了大概 20 次。 第二个坑: 国产 ROM 各家有各家的掐法 。小米要单独的"后台弹出界面"权限。华为有"应用启动管理"。OPPO 把它叫别的名字。同一个 API, 八个厂商八种行为。我专门为这个写了一个能力探测模块, 运行时去试到底能不能画 —— 而不是相信权限申请的返回值。 第三个坑也是最难的: 悬浮窗本身是个"高敏感"权限 。系统认为开了这个的 App 都有"行为不端"的潜力, 所以会在各种地方默默降级你 —— 锁屏后不让画、全屏视频时不让画、某些应用类型上不让画。 每一个降级路径我都得有一条兜底。 最后这个东西被我写成了一个 5 状态 9 事件的状态机, 专门管"什么时候该画、画什么样的、有没有画上、没画上怎么办"。它叫 OverlayStateMachine 。 它的核心逻辑就一句话: 画上了就是赢, 没画上就要立刻知道并补救 。 六、BLOCKING 和 HINT, 是温柔工具的两副脸 这个状态机往外有两种"画法"。 HINT —— 软提醒。顶部一条 80dp 高的横幅, 不拦截点击, 孩子可以继续操作底下的 App 。用在六阶段提醒的前三档: 60%、75%、90% 的时候各冒一次, 告诉孩子"快到时间了"。 BLOCKING —— 硬阻断。全屏覆盖, 拦截所有点击和系统按键, 中间放 PIN 输入框。只在 100% 之后用。 为什么要分两种? 因为如果只有"硬阻断", 那这个 App 就退化成了我最开头骂的那种"机器代替家长吼"的产品 —— 没有过渡, 直接黑屏。 而 HINT 这一层, 是给孩子"自己结束"留的台阶。 我做这两种模式的时候, 反复跟我自己吵过一个问题: HINT 那一档要不要拦截点击? 技术上做拦截更省事, 反正是悬浮窗, 全拦了完事。但拦了之后, 孩子滑手指会感到"卡了一下", 那个体感非常不好 —— 像是被人冷不防戳了一下。 最后我决定, HINT 这一层 坚决不拦点击 。它就是一条横幅, 画在最上面, 孩子手指划过去, 底下的 App 该怎么用怎么用。 它的存在感是视觉的, 不是触感的 。 让孩子"看见一个提醒"和"感到被打扰"是两件不一样的事, 做家长的应该懂这个区别。 七、那天晚上, Overlay 第一次起来的时候 我把整套机制接通的那天晚上, 又是一个周三。 我把手机递给我家娃, 让他打开他平时刷的那个 App 。我把今天的剩余时间设到了最短 —— 5 分钟。 过了一会儿, HINT 横幅冒出来 —— 顶部一条小小的"快到时间啦", 停留几秒, 自动消失。他眼睛瞄了一下, 继续看。 又过了一会儿, 第二条 HINT 。这次他嘟囔了一句"快结束了"。 5 分钟到的那一刻, BLOCKING 弹出来了。底下的 App 还在播, 但屏幕中间多了一张卡片 —— 上面画着一只小乌龟, 旁边一行字告诉他该休息一会儿了, 卡片下方是 PIN 输入框。 他没有大哭。 他只是抬头看了我一眼, 说: "爸爸, 时间到了。" 我那一刻心里特别复杂。 因为这张挡住他的卡片, 背后是我跟 Android 系统较劲那段时间写的全部代码 —— BAL 限制、fullScreenIntent 降级、八个厂商的兼容差异、一个状态机、五种状态、九种事件、还有大概 40 多种从"画上了"到"没画上"再到"用户自己关了"的转换路径。 而这一切, **最后呈现给我家娃的样子, 就是一只小乌龟和一句"该休息一会儿啦"**。 他根本看不到背后的代码。他只看到一件事: 屏幕在告诉他时间到了, 但 没有人在吼他 。 八、最后 这条路技术上不优雅 —— 用一个本来给"小窗视频"做的 API 去做"全屏阻断", 怎么看都歪。 但它有一件事, 是我最初想的那条"标准路"做不到的 —— 那张卡片没有"关掉"任何东西 。底下那个 App 还活着, 孩子刚看到哪一帧, 还停在哪一帧。家长输 PIN, 卡片撤掉, 他还能继续。 强行关闭做不到这一点。强行关闭一关就是真关 —— 孩子刚刚攒到的进度、看到一半的剧情, 全没了。 很久以后我才想明白一件事: 我一开始就不应该想着"关掉"它。我应该想着"挡住"它 。 关和挡, 差一个字。但前者留给孩子的是"我的东西被人拿走了"; 后者留给孩子的是"我的东西在那, 只是现在不让动"。 第二种, 孩子才学得会接受。 如果你也在做这种被系统一刀刀砍权限砍到墙角的事 —— 我想说的是, 那条系统留给你的窄路, 可能比你以为的, 更接近你本来想做的那件事。 只是它要求你, 把"关掉"这两个字从你脑子里彻底删掉。 那两个字, 是父母最容易做出的动作, 也是孩子最难承受的动作。

v2ex · 2026-06-05 11:33:21+08:00 · tech

那是一个周三凌晨两点。 我盯着 Android Studio 的 logcat, 第八次发现: 时间到了, 我写的那个"锁屏页"压根没起来。 我家娃的手机静静地躺在桌上, 他常刷的那个短视频还在继续播。 我打开侧锁屏, 确认我的代码确实运行了 —— Activity 启动指令发出去了, 系统回了一个我没见过的 warning, 然后什么都没发生。 那一瞬间我有一个挺荒谬的念头: 我做这个 App 一年, 是不是从一开始就走在一条已经被堵死的路上 ? 一、我以为这是一个 Android 入门题 我做儿童 App, 核心功能就一句话: 时间到了, 不让孩子切回去继续刷。 我当时的设计极其朴素 —— 起一个 LockActivity , 铺满屏幕, 写一个 PIN 输入框。家长输 PIN, 孩子继续玩; 不输 PIN, 就一直挡着。 按 Android 的标准做法, 时间到了就调用 startActivity() 把这个 Lock 页拉起来。 如果担心系统不给拉起, 那就再加一个 fullScreenIntent 通知 —— 全屏意图, 专门给闹钟、来电、紧急提醒这类场景用, 系统会强制把你的 Activity 拉到最前面。 我写了, 跑了, 在我自己的 Pixel 上一次过。 那天晚上我特别开心, 跟我老婆说: "核心功能搞定了。" 她问: "就这么简单?" 我说: "就这么简单。Android 给了现成的 API 。" 那是我整个项目里, **最后一次说"就这么简单"**。 二、Android 12 之后, 这条路被一刀切了 第二天我换了一台手机测, Android 12 的。 LockActivity 没起来。 我以为是我代码写错了。 调了一整天, 找不到原因。所有的日志都告诉我"启动成功", 但屏幕上什么都没有。 第三天我才在 Google 的官方文档里翻到一篇说明 —— Android 12 之后, 引入了一个叫 BAL 的东西, 全称 Background Activity Launch 限制。 翻译成人话就是: 从后台拉起一个 Activity 这件事, 被默认禁止了 。 只剩极少数情况能起来: 用户刚跟你的 App 交互完不到几秒、有可见的 Window 、是系统级权限。我的"时间监控服务"在后台跑, 时间到了想拉起一个 Activity —— 全部不符合。 那 fullScreenIntent 呢? 我加了啊。 我又翻了一晚上文档。结论是: 从 Android 10 开始, 系统会 悄悄把全屏意图降级成一条普通通知 , 除非用户在设置里手动给你这个 App 开权限。 这两条加在一起, 等于 Android 12 之后, 我设计的整条阻断路径 —— 从代码逻辑上是对的, 从用户感受上完全不存在 。 三、为什么 Google 要这么做? 我得承认它是对的 我有一个晚上在床上想这件事, 越想越烦躁。 我做的是儿童保护, 又不是恶意软件, 凭什么把我也一刀切? 但我后来去看了那个 BAL 引入的初衷 —— 当时市面上有大量的 App 在后台偷偷拉起广告 Activity 。你把手机放桌上, 它突然弹一个全屏视频广告。还有更坏的, 模拟登录页、模拟支付页, 专门做钓鱼。 Google 是被这些东西逼到必须出手的。 我能理解。 但理解归理解 —— 理解不能让我家娃的手机在时间到了之后真的停下来 。 这是做底层产品的一个尴尬: 你的产品哲学跟系统的产品哲学冲突的时候, 输的永远是你。 Google 不会因为我做的是"温柔的儿童 App"就给我开后门。在系统眼里, 我跟那些钓鱼 App 是同一种东西 —— 想在用户没主动操作的情况下, 强行占据屏幕 。 那段时间我反复问自己一个问题: 我做的这件事, 从系统的角度看, 到底是不是合理的 ? 我得承认, 从系统的角度, 它不合理。 但从一个父亲的角度, 它必须合理。 这两个"合理"之间的那条窄缝, 就是我接下来要找的路。 四、能走的路只剩三条, 前两条都不能选 我列出了所有理论上能阻断 App 切换的方案: **第一条, Accessibility (无障碍服务)**。这是 Android 给残障辅助用的, 能读到全局的应用切换事件, 理论上能"看到孩子切走"然后阻断。 但 Google Play 这两年专门盯这条 —— 任何不是真为残障人士设计的 App, 用了无障碍权限都会被下架。我看过太多儿童管控类、防沉迷类的 App, 因为这个被一夜清掉。 这条路死了, 是产品死, 不是技术死 。 第二条, 双进程互锁 。我开两个 Service, 一个挂了另一个把它拉起来。 这个技术上能做, 但它是 Google Play 政策里明确标红的"abusive behavior"。短期能活, 等于在一颗定时炸弹上盖房子。 死了, 是商业死 。 第三条, SYSTEM_ALERT_WINDOW 。也就是悬浮窗权限 —— 你常见的微信视频通话小窗、滴滴司机端的接单浮窗, 用的就是这个。 它的本质是: 让我的 App 可以在 别的 App 上面 画一层东西。 我不需要"启动一个 Activity", 我只需要"在你正在用的那个 App 上面, 盖一块布"。 这块布我自己控制 —— 大小、内容、能不能点穿。 这条路活着。但它有它自己的代价。 五、悬浮窗这条窄路, 本身也有三个大坑 我以为找到了 SYSTEM_ALERT_WINDOW 就解放了。 不是。 第一个坑: 权限要用户手动去设置里开 。装完 App 不会主动给你, 要跳到一个深埋在系统设置里的页面, 让用户找到你的 App 名字、打开开关。我做了三版引导, 从最初的 5 步跳转到现在的 1 步, 光是这个引导我返工了大概 20 次。 第二个坑: 国产 ROM 各家有各家的掐法 。小米要单独的"后台弹出界面"权限。华为有"应用启动管理"。OPPO 把它叫别的名字。同一个 API, 八个厂商八种行为。我专门为这个写了一个能力探测模块, 运行时去试到底能不能画 —— 而不是相信权限申请的返回值。 第三个坑也是最难的: 悬浮窗本身是个"高敏感"权限 。系统认为开了这个的 App 都有"行为不端"的潜力, 所以会在各种地方默默降级你 —— 锁屏后不让画、全屏视频时不让画、某些应用类型上不让画。 每一个降级路径我都得有一条兜底。 最后这个东西被我写成了一个 5 状态 9 事件的状态机, 专门管"什么时候该画、画什么样的、有没有画上、没画上怎么办"。它叫 OverlayStateMachine 。 它的核心逻辑就一句话: 画上了就是赢, 没画上就要立刻知道并补救 。 六、BLOCKING 和 HINT, 是温柔工具的两副脸 这个状态机往外有两种"画法"。 HINT —— 软提醒。顶部一条 80dp 高的横幅, 不拦截点击, 孩子可以继续操作底下的 App 。用在六阶段提醒的前三档: 60%、75%、90% 的时候各冒一次, 告诉孩子"快到时间了"。 BLOCKING —— 硬阻断。全屏覆盖, 拦截所有点击和系统按键, 中间放 PIN 输入框。只在 100% 之后用。 为什么要分两种? 因为如果只有"硬阻断", 那这个 App 就退化成了我最开头骂的那种"机器代替家长吼"的产品 —— 没有过渡, 直接黑屏。 而 HINT 这一层, 是给孩子"自己结束"留的台阶。 我做这两种模式的时候, 反复跟我自己吵过一个问题: HINT 那一档要不要拦截点击? 技术上做拦截更省事, 反正是悬浮窗, 全拦了完事。但拦了之后, 孩子滑手指会感到"卡了一下", 那个体感非常不好 —— 像是被人冷不防戳了一下。 最后我决定, HINT 这一层 坚决不拦点击 。它就是一条横幅, 画在最上面, 孩子手指划过去, 底下的 App 该怎么用怎么用。 它的存在感是视觉的, 不是触感的 。 让孩子"看见一个提醒"和"感到被打扰"是两件不一样的事, 做家长的应该懂这个区别。 七、那天晚上, Overlay 第一次起来的时候 我把整套机制接通的那天晚上, 又是一个周三。 我把手机递给我家娃, 让他打开他平时刷的那个 App 。我把今天的剩余时间设到了最短 —— 5 分钟。 过了一会儿, HINT 横幅冒出来 —— 顶部一条小小的"快到时间啦", 停留几秒, 自动消失。他眼睛瞄了一下, 继续看。 又过了一会儿, 第二条 HINT 。这次他嘟囔了一句"快结束了"。 5 分钟到的那一刻, BLOCKING 弹出来了。底下的 App 还在播, 但屏幕中间多了一张卡片 —— 上面画着一只小乌龟, 旁边一行字告诉他该休息一会儿了, 卡片下方是 PIN 输入框。 他没有大哭。 他只是抬头看了我一眼, 说: "爸爸, 时间到了。" 我那一刻心里特别复杂。 因为这张挡住他的卡片, 背后是我跟 Android 系统较劲那段时间写的全部代码 —— BAL 限制、fullScreenIntent 降级、八个厂商的兼容差异、一个状态机、五种状态、九种事件、还有大概 40 多种从"画上了"到"没画上"再到"用户自己关了"的转换路径。 而这一切, **最后呈现给我家娃的样子, 就是一只小乌龟和一句"该休息一会儿啦"**。 他根本看不到背后的代码。他只看到一件事: 屏幕在告诉他时间到了, 但 没有人在吼他 。 八、最后 这条路技术上不优雅 —— 用一个本来给"小窗视频"做的 API 去做"全屏阻断", 怎么看都歪。 但它有一件事, 是我最初想的那条"标准路"做不到的 —— 那张卡片没有"关掉"任何东西 。底下那个 App 还活着, 孩子刚看到哪一帧, 还停在哪一帧。家长输 PIN, 卡片撤掉, 他还能继续。 强行关闭做不到这一点。强行关闭一关就是真关 —— 孩子刚刚攒到的进度、看到一半的剧情, 全没了。 很久以后我才想明白一件事: 我一开始就不应该想着"关掉"它。我应该想着"挡住"它 。 关和挡, 差一个字。但前者留给孩子的是"我的东西被人拿走了"; 后者留给孩子的是"我的东西在那, 只是现在不让动"。 第二种, 孩子才学得会接受。 如果你也在做这种被系统一刀刀砍权限砍到墙角的事 —— 我想说的是, 那条系统留给你的窄路, 可能比你以为的, 更接近你本来想做的那件事。 只是它要求你, 把"关掉"这两个字从你脑子里彻底删掉。 那两个字, 是父母最容易做出的动作, 也是孩子最难承受的动作。

v2ex · 2026-06-05 11:06:51+08:00 · tech

那是一个周三凌晨两点。 我盯着 Android Studio 的 logcat, 第八次发现: 时间到了, 我写的那个"锁屏页"压根没起来。 我家娃的手机静静地躺在桌上, 他常刷的那个短视频还在继续播。 我打开侧锁屏, 确认我的代码确实运行了 —— Activity 启动指令发出去了, 系统回了一个我没见过的 warning, 然后什么都没发生。 那一瞬间我有一个挺荒谬的念头: 我做这个 App 一年, 是不是从一开始就走在一条已经被堵死的路上 ? 一、我以为这是一个 Android 入门题 我做儿童 App, 核心功能就一句话: 时间到了, 不让孩子切回去继续刷。 我当时的设计极其朴素 —— 起一个 LockActivity , 铺满屏幕, 写一个 PIN 输入框。家长输 PIN, 孩子继续玩; 不输 PIN, 就一直挡着。 按 Android 的标准做法, 时间到了就调用 startActivity() 把这个 Lock 页拉起来。 如果担心系统不给拉起, 那就再加一个 fullScreenIntent 通知 —— 全屏意图, 专门给闹钟、来电、紧急提醒这类场景用, 系统会强制把你的 Activity 拉到最前面。 我写了, 跑了, 在我自己的 Pixel 上一次过。 那天晚上我特别开心, 跟我老婆说: "核心功能搞定了。" 她问: "就这么简单?" 我说: "就这么简单。Android 给了现成的 API 。" 那是我整个项目里, **最后一次说"就这么简单"**。 二、Android 12 之后, 这条路被一刀切了 第二天我换了一台手机测, Android 12 的。 LockActivity 没起来。 我以为是我代码写错了。 调了一整天, 找不到原因。所有的日志都告诉我"启动成功", 但屏幕上什么都没有。 第三天我才在 Google 的官方文档里翻到一篇说明 —— Android 12 之后, 引入了一个叫 BAL 的东西, 全称 Background Activity Launch 限制。 翻译成人话就是: 从后台拉起一个 Activity 这件事, 被默认禁止了 。 只剩极少数情况能起来: 用户刚跟你的 App 交互完不到几秒、有可见的 Window 、是系统级权限。我的"时间监控服务"在后台跑, 时间到了想拉起一个 Activity —— 全部不符合。 那 fullScreenIntent 呢? 我加了啊。 我又翻了一晚上文档。结论是: 从 Android 10 开始, 系统会 悄悄把全屏意图降级成一条普通通知 , 除非用户在设置里手动给你这个 App 开权限。 这两条加在一起, 等于 Android 12 之后, 我设计的整条阻断路径 —— 从代码逻辑上是对的, 从用户感受上完全不存在 。 三、为什么 Google 要这么做? 我得承认它是对的 我有一个晚上在床上想这件事, 越想越烦躁。 我做的是儿童保护, 又不是恶意软件, 凭什么把我也一刀切? 但我后来去看了那个 BAL 引入的初衷 —— 当时市面上有大量的 App 在后台偷偷拉起广告 Activity 。你把手机放桌上, 它突然弹一个全屏视频广告。还有更坏的, 模拟登录页、模拟支付页, 专门做钓鱼。 Google 是被这些东西逼到必须出手的。 我能理解。 但理解归理解 —— 理解不能让我家娃的手机在时间到了之后真的停下来 。 这是做底层产品的一个尴尬: 你的产品哲学跟系统的产品哲学冲突的时候, 输的永远是你。 Google 不会因为我做的是"温柔的儿童 App"就给我开后门。在系统眼里, 我跟那些钓鱼 App 是同一种东西 —— 想在用户没主动操作的情况下, 强行占据屏幕 。 那段时间我反复问自己一个问题: 我做的这件事, 从系统的角度看, 到底是不是合理的 ? 我得承认, 从系统的角度, 它不合理。 但从一个父亲的角度, 它必须合理。 这两个"合理"之间的那条窄缝, 就是我接下来要找的路。 四、能走的路只剩三条, 前两条都不能选 我列出了所有理论上能阻断 App 切换的方案: **第一条, Accessibility (无障碍服务)**。这是 Android 给残障辅助用的, 能读到全局的应用切换事件, 理论上能"看到孩子切走"然后阻断。 但 Google Play 这两年专门盯这条 —— 任何不是真为残障人士设计的 App, 用了无障碍权限都会被下架。我看过太多儿童管控类、防沉迷类的 App, 因为这个被一夜清掉。 这条路死了, 是产品死, 不是技术死 。 第二条, 双进程互锁 。我开两个 Service, 一个挂了另一个把它拉起来。 这个技术上能做, 但它是 Google Play 政策里明确标红的"abusive behavior"。短期能活, 等于在一颗定时炸弹上盖房子。 死了, 是商业死 。 第三条, SYSTEM_ALERT_WINDOW 。也就是悬浮窗权限 —— 你常见的微信视频通话小窗、滴滴司机端的接单浮窗, 用的就是这个。 它的本质是: 让我的 App 可以在 别的 App 上面 画一层东西。 我不需要"启动一个 Activity", 我只需要"在你正在用的那个 App 上面, 盖一块布"。 这块布我自己控制 —— 大小、内容、能不能点穿。 这条路活着。但它有它自己的代价。 五、悬浮窗这条窄路, 本身也有三个大坑 我以为找到了 SYSTEM_ALERT_WINDOW 就解放了。 不是。 第一个坑: 权限要用户手动去设置里开 。装完 App 不会主动给你, 要跳到一个深埋在系统设置里的页面, 让用户找到你的 App 名字、打开开关。我做了三版引导, 从最初的 5 步跳转到现在的 1 步, 光是这个引导我返工了大概 20 次。 第二个坑: 国产 ROM 各家有各家的掐法 。小米要单独的"后台弹出界面"权限。华为有"应用启动管理"。OPPO 把它叫别的名字。同一个 API, 八个厂商八种行为。我专门为这个写了一个能力探测模块, 运行时去试到底能不能画 —— 而不是相信权限申请的返回值。 第三个坑也是最难的: 悬浮窗本身是个"高敏感"权限 。系统认为开了这个的 App 都有"行为不端"的潜力, 所以会在各种地方默默降级你 —— 锁屏后不让画、全屏视频时不让画、某些应用类型上不让画。 每一个降级路径我都得有一条兜底。 最后这个东西被我写成了一个 5 状态 9 事件的状态机, 专门管"什么时候该画、画什么样的、有没有画上、没画上怎么办"。它叫 OverlayStateMachine 。 它的核心逻辑就一句话: 画上了就是赢, 没画上就要立刻知道并补救 。 六、BLOCKING 和 HINT, 是温柔工具的两副脸 这个状态机往外有两种"画法"。 HINT —— 软提醒。顶部一条 80dp 高的横幅, 不拦截点击, 孩子可以继续操作底下的 App 。用在六阶段提醒的前三档: 60%、75%、90% 的时候各冒一次, 告诉孩子"快到时间了"。 BLOCKING —— 硬阻断。全屏覆盖, 拦截所有点击和系统按键, 中间放 PIN 输入框。只在 100% 之后用。 为什么要分两种? 因为如果只有"硬阻断", 那这个 App 就退化成了我最开头骂的那种"机器代替家长吼"的产品 —— 没有过渡, 直接黑屏。 而 HINT 这一层, 是给孩子"自己结束"留的台阶。 我做这两种模式的时候, 反复跟我自己吵过一个问题: HINT 那一档要不要拦截点击? 技术上做拦截更省事, 反正是悬浮窗, 全拦了完事。但拦了之后, 孩子滑手指会感到"卡了一下", 那个体感非常不好 —— 像是被人冷不防戳了一下。 最后我决定, HINT 这一层 坚决不拦点击 。它就是一条横幅, 画在最上面, 孩子手指划过去, 底下的 App 该怎么用怎么用。 它的存在感是视觉的, 不是触感的 。 让孩子"看见一个提醒"和"感到被打扰"是两件不一样的事, 做家长的应该懂这个区别。 七、那天晚上, Overlay 第一次起来的时候 我把整套机制接通的那天晚上, 又是一个周三。 我把手机递给我家娃, 让他打开他平时刷的那个 App 。我把今天的剩余时间设到了最短 —— 5 分钟。 过了一会儿, HINT 横幅冒出来 —— 顶部一条小小的"快到时间啦", 停留几秒, 自动消失。他眼睛瞄了一下, 继续看。 又过了一会儿, 第二条 HINT 。这次他嘟囔了一句"快结束了"。 5 分钟到的那一刻, BLOCKING 弹出来了。底下的 App 还在播, 但屏幕中间多了一张卡片 —— 上面画着一只小乌龟, 旁边一行字告诉他该休息一会儿了, 卡片下方是 PIN 输入框。 他没有大哭。 他只是抬头看了我一眼, 说: "爸爸, 时间到了。" 我那一刻心里特别复杂。 因为这张挡住他的卡片, 背后是我跟 Android 系统较劲那段时间写的全部代码 —— BAL 限制、fullScreenIntent 降级、八个厂商的兼容差异、一个状态机、五种状态、九种事件、还有大概 40 多种从"画上了"到"没画上"再到"用户自己关了"的转换路径。 而这一切, **最后呈现给我家娃的样子, 就是一只小乌龟和一句"该休息一会儿啦"**。 他根本看不到背后的代码。他只看到一件事: 屏幕在告诉他时间到了, 但 没有人在吼他 。 八、最后 这条路技术上不优雅 —— 用一个本来给"小窗视频"做的 API 去做"全屏阻断", 怎么看都歪。 但它有一件事, 是我最初想的那条"标准路"做不到的 —— 那张卡片没有"关掉"任何东西 。底下那个 App 还活着, 孩子刚看到哪一帧, 还停在哪一帧。家长输 PIN, 卡片撤掉, 他还能继续。 强行关闭做不到这一点。强行关闭一关就是真关 —— 孩子刚刚攒到的进度、看到一半的剧情, 全没了。 很久以后我才想明白一件事: 我一开始就不应该想着"关掉"它。我应该想着"挡住"它 。 关和挡, 差一个字。但前者留给孩子的是"我的东西被人拿走了"; 后者留给孩子的是"我的东西在那, 只是现在不让动"。 第二种, 孩子才学得会接受。 如果你也在做这种被系统一刀刀砍权限砍到墙角的事 —— 我想说的是, 那条系统留给你的窄路, 可能比你以为的, 更接近你本来想做的那件事。 只是它要求你, 把"关掉"这两个字从你脑子里彻底删掉。 那两个字, 是父母最容易做出的动作, 也是孩子最难承受的动作。

v2ex · 2026-06-05 10:35:18+08:00 · tech

那是一个周三凌晨两点。 我盯着 Android Studio 的 logcat, 第八次发现: 时间到了, 我写的那个"锁屏页"压根没起来。 我家娃的手机静静地躺在桌上, 他常刷的那个短视频还在继续播。 我打开侧锁屏, 确认我的代码确实运行了 —— Activity 启动指令发出去了, 系统回了一个我没见过的 warning, 然后什么都没发生。 那一瞬间我有一个挺荒谬的念头: 我做这个 App 一年, 是不是从一开始就走在一条已经被堵死的路上 ? 一、我以为这是一个 Android 入门题 我做儿童 App, 核心功能就一句话: 时间到了, 不让孩子切回去继续刷。 我当时的设计极其朴素 —— 起一个 LockActivity , 铺满屏幕, 写一个 PIN 输入框。家长输 PIN, 孩子继续玩; 不输 PIN, 就一直挡着。 按 Android 的标准做法, 时间到了就调用 startActivity() 把这个 Lock 页拉起来。 如果担心系统不给拉起, 那就再加一个 fullScreenIntent 通知 —— 全屏意图, 专门给闹钟、来电、紧急提醒这类场景用, 系统会强制把你的 Activity 拉到最前面。 我写了, 跑了, 在我自己的 Pixel 上一次过。 那天晚上我特别开心, 跟我老婆说: "核心功能搞定了。" 她问: "就这么简单?" 我说: "就这么简单。Android 给了现成的 API 。" 那是我整个项目里, **最后一次说"就这么简单"**。 二、Android 12 之后, 这条路被一刀切了 第二天我换了一台手机测, Android 12 的。 LockActivity 没起来。 我以为是我代码写错了。 调了一整天, 找不到原因。所有的日志都告诉我"启动成功", 但屏幕上什么都没有。 第三天我才在 Google 的官方文档里翻到一篇说明 —— Android 12 之后, 引入了一个叫 BAL 的东西, 全称 Background Activity Launch 限制。 翻译成人话就是: 从后台拉起一个 Activity 这件事, 被默认禁止了 。 只剩极少数情况能起来: 用户刚跟你的 App 交互完不到几秒、有可见的 Window 、是系统级权限。我的"时间监控服务"在后台跑, 时间到了想拉起一个 Activity —— 全部不符合。 那 fullScreenIntent 呢? 我加了啊。 我又翻了一晚上文档。结论是: 从 Android 10 开始, 系统会 悄悄把全屏意图降级成一条普通通知 , 除非用户在设置里手动给你这个 App 开权限。 这两条加在一起, 等于 Android 12 之后, 我设计的整条阻断路径 —— 从代码逻辑上是对的, 从用户感受上完全不存在 。 三、为什么 Google 要这么做? 我得承认它是对的 我有一个晚上在床上想这件事, 越想越烦躁。 我做的是儿童保护, 又不是恶意软件, 凭什么把我也一刀切? 但我后来去看了那个 BAL 引入的初衷 —— 当时市面上有大量的 App 在后台偷偷拉起广告 Activity 。你把手机放桌上, 它突然弹一个全屏视频广告。还有更坏的, 模拟登录页、模拟支付页, 专门做钓鱼。 Google 是被这些东西逼到必须出手的。 我能理解。 但理解归理解 —— 理解不能让我家娃的手机在时间到了之后真的停下来 。 这是做底层产品的一个尴尬: 你的产品哲学跟系统的产品哲学冲突的时候, 输的永远是你。 Google 不会因为我做的是"温柔的儿童 App"就给我开后门。在系统眼里, 我跟那些钓鱼 App 是同一种东西 —— 想在用户没主动操作的情况下, 强行占据屏幕 。 那段时间我反复问自己一个问题: 我做的这件事, 从系统的角度看, 到底是不是合理的 ? 我得承认, 从系统的角度, 它不合理。 但从一个父亲的角度, 它必须合理。 这两个"合理"之间的那条窄缝, 就是我接下来要找的路。 四、能走的路只剩三条, 前两条都不能选 我列出了所有理论上能阻断 App 切换的方案: **第一条, Accessibility (无障碍服务)**。这是 Android 给残障辅助用的, 能读到全局的应用切换事件, 理论上能"看到孩子切走"然后阻断。 但 Google Play 这两年专门盯这条 —— 任何不是真为残障人士设计的 App, 用了无障碍权限都会被下架。我看过太多儿童管控类、防沉迷类的 App, 因为这个被一夜清掉。 这条路死了, 是产品死, 不是技术死 。 第二条, 双进程互锁 。我开两个 Service, 一个挂了另一个把它拉起来。 这个技术上能做, 但它是 Google Play 政策里明确标红的"abusive behavior"。短期能活, 等于在一颗定时炸弹上盖房子。 死了, 是商业死 。 第三条, SYSTEM_ALERT_WINDOW 。也就是悬浮窗权限 —— 你常见的微信视频通话小窗、滴滴司机端的接单浮窗, 用的就是这个。 它的本质是: 让我的 App 可以在 别的 App 上面 画一层东西。 我不需要"启动一个 Activity", 我只需要"在你正在用的那个 App 上面, 盖一块布"。 这块布我自己控制 —— 大小、内容、能不能点穿。 这条路活着。但它有它自己的代价。 五、悬浮窗这条窄路, 本身也有三个大坑 我以为找到了 SYSTEM_ALERT_WINDOW 就解放了。 不是。 第一个坑: 权限要用户手动去设置里开 。装完 App 不会主动给你, 要跳到一个深埋在系统设置里的页面, 让用户找到你的 App 名字、打开开关。我做了三版引导, 从最初的 5 步跳转到现在的 1 步, 光是这个引导我返工了大概 20 次。 第二个坑: 国产 ROM 各家有各家的掐法 。小米要单独的"后台弹出界面"权限。华为有"应用启动管理"。OPPO 把它叫别的名字。同一个 API, 八个厂商八种行为。我专门为这个写了一个能力探测模块, 运行时去试到底能不能画 —— 而不是相信权限申请的返回值。 第三个坑也是最难的: 悬浮窗本身是个"高敏感"权限 。系统认为开了这个的 App 都有"行为不端"的潜力, 所以会在各种地方默默降级你 —— 锁屏后不让画、全屏视频时不让画、某些应用类型上不让画。 每一个降级路径我都得有一条兜底。 最后这个东西被我写成了一个 5 状态 9 事件的状态机, 专门管"什么时候该画、画什么样的、有没有画上、没画上怎么办"。它叫 OverlayStateMachine 。 它的核心逻辑就一句话: 画上了就是赢, 没画上就要立刻知道并补救 。 六、BLOCKING 和 HINT, 是温柔工具的两副脸 这个状态机往外有两种"画法"。 HINT —— 软提醒。顶部一条 80dp 高的横幅, 不拦截点击, 孩子可以继续操作底下的 App 。用在六阶段提醒的前三档: 60%、75%、90% 的时候各冒一次, 告诉孩子"快到时间了"。 BLOCKING —— 硬阻断。全屏覆盖, 拦截所有点击和系统按键, 中间放 PIN 输入框。只在 100% 之后用。 为什么要分两种? 因为如果只有"硬阻断", 那这个 App 就退化成了我最开头骂的那种"机器代替家长吼"的产品 —— 没有过渡, 直接黑屏。 而 HINT 这一层, 是给孩子"自己结束"留的台阶。 我做这两种模式的时候, 反复跟我自己吵过一个问题: HINT 那一档要不要拦截点击? 技术上做拦截更省事, 反正是悬浮窗, 全拦了完事。但拦了之后, 孩子滑手指会感到"卡了一下", 那个体感非常不好 —— 像是被人冷不防戳了一下。 最后我决定, HINT 这一层 坚决不拦点击 。它就是一条横幅, 画在最上面, 孩子手指划过去, 底下的 App 该怎么用怎么用。 它的存在感是视觉的, 不是触感的 。 让孩子"看见一个提醒"和"感到被打扰"是两件不一样的事, 做家长的应该懂这个区别。 七、那天晚上, Overlay 第一次起来的时候 我把整套机制接通的那天晚上, 又是一个周三。 我把手机递给我家娃, 让他打开他平时刷的那个 App 。我把今天的剩余时间设到了最短 —— 5 分钟。 过了一会儿, HINT 横幅冒出来 —— 顶部一条小小的"快到时间啦", 停留几秒, 自动消失。他眼睛瞄了一下, 继续看。 又过了一会儿, 第二条 HINT 。这次他嘟囔了一句"快结束了"。 5 分钟到的那一刻, BLOCKING 弹出来了。底下的 App 还在播, 但屏幕中间多了一张卡片 —— 上面画着一只小乌龟, 旁边一行字告诉他该休息一会儿了, 卡片下方是 PIN 输入框。 他没有大哭。 他只是抬头看了我一眼, 说: "爸爸, 时间到了。" 我那一刻心里特别复杂。 因为这张挡住他的卡片, 背后是我跟 Android 系统较劲那段时间写的全部代码 —— BAL 限制、fullScreenIntent 降级、八个厂商的兼容差异、一个状态机、五种状态、九种事件、还有大概 40 多种从"画上了"到"没画上"再到"用户自己关了"的转换路径。 而这一切, **最后呈现给我家娃的样子, 就是一只小乌龟和一句"该休息一会儿啦"**。 他根本看不到背后的代码。他只看到一件事: 屏幕在告诉他时间到了, 但 没有人在吼他 。 八、最后 这条路技术上不优雅 —— 用一个本来给"小窗视频"做的 API 去做"全屏阻断", 怎么看都歪。 但它有一件事, 是我最初想的那条"标准路"做不到的 —— 那张卡片没有"关掉"任何东西 。底下那个 App 还活着, 孩子刚看到哪一帧, 还停在哪一帧。家长输 PIN, 卡片撤掉, 他还能继续。 强行关闭做不到这一点。强行关闭一关就是真关 —— 孩子刚刚攒到的进度、看到一半的剧情, 全没了。 很久以后我才想明白一件事: 我一开始就不应该想着"关掉"它。我应该想着"挡住"它 。 关和挡, 差一个字。但前者留给孩子的是"我的东西被人拿走了"; 后者留给孩子的是"我的东西在那, 只是现在不让动"。 第二种, 孩子才学得会接受。 如果你也在做这种被系统一刀刀砍权限砍到墙角的事 —— 我想说的是, 那条系统留给你的窄路, 可能比你以为的, 更接近你本来想做的那件事。 只是它要求你, 把"关掉"这两个字从你脑子里彻底删掉。 那两个字, 是父母最容易做出的动作, 也是孩子最难承受的动作。