<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Android on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/android/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Thu, 10 Aug 2023 09:10:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/android/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>Android 开发从入门到精通：环境搭建、Kotlin、Compose、架构、系统能力与发布全指南</title>
      <link>https://ftmao.com/blog/posts/android-beginner-to-mastery/</link>
      <pubDate>Thu, 10 Aug 2023 09:10:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/android-beginner-to-mastery/</guid>
      <description>&lt;h1 id=&#34;android-开发从入门到精通环境搭建kotlincompose架构系统能力与发布全指南&#34;&gt;Android 开发从入门到精通：环境搭建、Kotlin、Compose、架构、系统能力与发布全指南&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：想进入 Android 开发的新人，或者写过一阵 Java/旧版 XML 布局、想切换到现代技术栈（Kotlin + Compose）的同学。&lt;br&gt;
知识骨架参考《Android 编程权威指南》（Big Nerd Ranch Guide），结合 2026 年的技术现状做了取舍，按「由浅入深」重新编排。&lt;br&gt;
姊妹篇：《&lt;a href=&#34;https://ftmao.com/blog/blog/posts/frontend-ui-vue-mastery/&#34;&gt;前端 UI 开发从入门到精通（Vue 技术栈）&lt;/a&gt;》——写 UI 的心法，两端是相通的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;全文共八篇，顺序就是学习的顺序——每一篇都依赖前一篇打下的基础：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;一 环境搭建&amp;lt;br/&amp;gt;能跑起 Hello World&amp;#34;] --&amp;gt; B[&amp;#34;二 Kotlin 基础&amp;lt;br/&amp;gt;语法 + 协程&amp;#34;]
    B --&amp;gt; C[&amp;#34;三 Compose 界面&amp;lt;br/&amp;gt;写出第一个页面&amp;#34;]
    C --&amp;gt; D[&amp;#34;四 架构与生命周期&amp;lt;br/&amp;gt;从写页面到写应用&amp;#34;]
    D --&amp;gt; E[&amp;#34;五 网络与存储&amp;lt;br/&amp;gt;数据从哪来 存到哪去&amp;#34;]
    E --&amp;gt; F[&amp;#34;六 系统能力&amp;lt;br/&amp;gt;定位 通知 唤醒 适配&amp;#34;]
    F --&amp;gt; G[&amp;#34;七 质量与发布&amp;lt;br/&amp;gt;测试 调试 签名上线&amp;#34;]
    G --&amp;gt; H[&amp;#34;八 AI 时代&amp;lt;br/&amp;gt;让 AI 写 80% 的代码&amp;#34;]
    style H fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;为什么是这个顺序？前三篇解决「&lt;strong&gt;能不能写出来&lt;/strong&gt;」；第四篇解决「&lt;strong&gt;代码多了以后会不会散架&lt;/strong&gt;」；第五、六篇解决「&lt;strong&gt;能不能和服务器、和操作系统打交道&lt;/strong&gt;」；第七篇解决「&lt;strong&gt;敢不敢发给真实用户&lt;/strong&gt;」；第八篇解决「&lt;strong&gt;如何持续快下去&lt;/strong&gt;」。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第一篇-环境搭建90-的新人倒在这一关&#34;&gt;第一篇 环境搭建：90% 的新人倒在这一关&lt;/h2&gt;
&lt;p&gt;先泼一盆冷水再给你信心：&lt;strong&gt;Android 开发的门槛不在写代码，而在环境&lt;/strong&gt;。新人放弃 Android 的第一大原因不是学不会语法，而是装了三天 Android Studio 跑不起来一个 Hello World。所以环境放在最前面，并且写得足够细。&lt;/p&gt;
&lt;h3 id=&#34;11-你需要安装的三样东西&#34;&gt;1.1 你需要安装的三样东西&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;JDK 17+&amp;lt;br/&amp;gt;编译 Kotlin/Java 的底座&amp;#34;] --&amp;gt; D[&amp;#34;Android Studio&amp;#34;]
    B[&amp;#34;Android SDK&amp;lt;br/&amp;gt;系统 API + 平台工具 adb&amp;#34;] --&amp;gt; D
    C[&amp;#34;Android Emulator&amp;lt;br/&amp;gt;或一台真机&amp;#34;] --&amp;gt; D
    D --&amp;gt; E[&amp;#34;能跑起 Hello World&amp;lt;br/&amp;gt;= 入门完成一半&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;好消息是 2026 年的 Android Studio 已经是「全家桶」安装：下载安装包一路 Next，JDK（内嵌 JBR）、SDK、模拟器全都有了。真正需要你动手的只有三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;下载地址认准官方&lt;/strong&gt;：&lt;a href=&#34;https://developer.android.google.cn/studio&#34;&gt;developer.android.google.cn&lt;/a&gt;（国内可直连）。别去下载站，装到捆绑了全家桶的旧版本是新人常见悲剧。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第一次启动时让它下载 SDK&lt;/strong&gt;，默认勾选 Android SDK Platform、SDK Build-Tools、Android Emulator 即可。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配环境变量&lt;/strong&gt;（命令行构建和 AI 工具都会用到）：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Windows: 系统环境变量里添加&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ANDROID_HOME&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;C:&lt;span style=&#34;color:#ae81ff&#34;&gt;\U&lt;/span&gt;sers&lt;span style=&#34;color:#ae81ff&#34;&gt;\你&lt;/span&gt;的用户名&lt;span style=&#34;color:#ae81ff&#34;&gt;\A&lt;/span&gt;ppData&lt;span style=&#34;color:#ae81ff&#34;&gt;\L&lt;/span&gt;ocal&lt;span style=&#34;color:#ae81ff&#34;&gt;\A&lt;/span&gt;ndroid&lt;span style=&#34;color:#ae81ff&#34;&gt;\S&lt;/span&gt;dk
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Path 里追加&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;%ANDROID_HOME%&lt;span style=&#34;color:#ae81ff&#34;&gt;\p&lt;/span&gt;latform-tools   &lt;span style=&#34;color:#75715e&#34;&gt;# adb 在这里&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;%ANDROID_HOME%&lt;span style=&#34;color:#ae81ff&#34;&gt;\c&lt;/span&gt;mdline-tools&lt;span style=&#34;color:#ae81ff&#34;&gt;\l&lt;/span&gt;atest&lt;span style=&#34;color:#ae81ff&#34;&gt;\b&lt;/span&gt;in&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;验证：新开一个终端跑 &lt;code&gt;adb version&lt;/code&gt;，有输出即成功。&lt;/p&gt;
&lt;h3 id=&#34;12-第一个项目别折腾用模板&#34;&gt;1.2 第一个项目：别折腾，用模板&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;New Project → Empty Activity&lt;/code&gt;（注意选 &lt;strong&gt;Empty Activity&lt;/strong&gt;，这是 Compose 模板；不要选带 &amp;ldquo;Views&amp;rdquo; 后缀的，那是旧 XML 布局）。点 Run，等模拟器起来，看到 &amp;ldquo;Hello Android&amp;rdquo; 就算通关。&lt;/p&gt;
&lt;h3 id=&#34;13-踩坑故事一gradle-同步转了一小时的圈&#34;&gt;1.3 踩坑故事一：Gradle 同步转了一小时的圈&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：新建项目后底部状态栏 &amp;ldquo;Gradle Sync&amp;rdquo; 转圈一小时不动，或者报 &lt;code&gt;Could not resolve com.android.tools.build:gradle:x.x.x&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：Gradle 要从国外 Maven 仓库下载几百 MB 的构建工具和依赖，国内直连极慢。这是国内 Android 新人的「成人礼」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：给项目根目录的 &lt;code&gt;settings.gradle.kts&lt;/code&gt; 换上国内镜像（阿里源）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// settings.gradle.kts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pluginManagement {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    repositories {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        maven(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://maven.aliyun.com/repository/gradle-plugin&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        maven(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://maven.aliyun.com/repository/google&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        google()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        mavenCentral()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;dependencyResolutionManagement {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    repositories {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        maven(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://maven.aliyun.com/repository/google&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        maven(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://maven.aliyun.com/repository/central&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        google()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        mavenCentral()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：镜像要放在 &lt;code&gt;google()&lt;/code&gt; &lt;strong&gt;前面&lt;/strong&gt;，Gradle 按顺序找，先命中镜像就不用走官方源了。同样的思路适用于一切包管理器（npm、pip、Maven）——「换源」是国内开发者的通用生存技能。&lt;/p&gt;
&lt;h3 id=&#34;14-踩坑故事二agpgradlejdk-的三角恋情&#34;&gt;1.4 踩坑故事二：AGP、Gradle、JDK 的三角恋情&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：拉下同事的项目，构建报错 &lt;code&gt;Unsupported class file major version 65&lt;/code&gt; 或 &lt;code&gt;Minimum supported Gradle version is x.x&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：Android 开发有三个版本必须互相匹配，这是新人最懵的地方：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;Android Gradle Plugin (AGP)&amp;lt;br/&amp;gt;项目级 build.gradle 里声明&amp;#34;] --&amp;gt;|&amp;#34;要求&amp;#34;| B[&amp;#34;Gradle 版本&amp;lt;br/&amp;gt;gradle-wrapper.properties&amp;#34;]
    A --&amp;gt;|&amp;#34;要求&amp;#34;| C[&amp;#34;JDK 版本&amp;lt;br/&amp;gt;Android Studio 内嵌或系统&amp;#34;]
    B -.-&amp;gt;|&amp;#34;版本对照表&amp;lt;br/&amp;gt;查官方文档&amp;#34;| C
    style A fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;AGP 8.x 要求 Gradle 8.x + JDK 17。&lt;/li&gt;
&lt;li&gt;报错时&lt;strong&gt;不要瞎改&lt;/strong&gt;，打开 Google 官方的「Android Gradle 插件版本说明」对照表，三者对齐。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：项目里 &lt;code&gt;gradle-wrapper.properties&lt;/code&gt; 锁定了 Gradle 版本是有深意的——它保证任何同事、任何 CI 机器拉下代码构建结果一致。新人最容易犯的错就是全局升级 Gradle 然后所有老项目全炸。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第二篇-kotlin-基础写给会别的语言的人&#34;&gt;第二篇 Kotlin 基础：写给会别的语言的人&lt;/h2&gt;
&lt;p&gt;环境通了，接下来是语言。如果你会 Java 或 JavaScript，这一篇很快；如果是零基础，建议搭配《Kotlin 核心编程》或官方教程把例子都敲一遍。&lt;/p&gt;
&lt;h3 id=&#34;21-十分钟速览和-javajs-的核心差异&#34;&gt;2.1 十分钟速览：和 Java/JS 的核心差异&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 1. val / var：不可变优先
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; name = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;方糖猫&amp;#34;&lt;/span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 不可变，类似 const
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; age = &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;               &lt;span style=&#34;color:#75715e&#34;&gt;// 可变
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// age = &amp;#34;三&amp;#34;             // ❌ 类型推导为 Int，改 String 编译报错
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 2. 空安全：类型系统层面消灭 NPE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; nickname: String? = &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// nickname.length        // ❌ 编译不过，必须处理 null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; len = nickname&lt;span style=&#34;color:#f92672&#34;&gt;?.&lt;/span&gt;length &lt;span style=&#34;color:#f92672&#34;&gt;?:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;// ?. 安全调用 + ?: 默认值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 3. when 表达式：加强版 switch
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; desc = &lt;span style=&#34;color:#66d9ef&#34;&gt;when&lt;/span&gt; (age) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;..&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;幼猫&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;壮年&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;老猫&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 4. data class：一行顶 Java 五十行
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;User&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; id: Long, &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; name: String, &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; email: String = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 自动生成 equals/hashCode/toString/copy
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 5. 函数式：lambda 和集合操作
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; names = users.filter { &lt;span style=&#34;color:#66d9ef&#34;&gt;it&lt;/span&gt;.name.isNotBlank() }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 .map { &lt;span style=&#34;color:#66d9ef&#34;&gt;it&lt;/span&gt;.name }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 6. 扩展函数：给别人的类加方法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;String&lt;/span&gt;.isEmail(): Boolean = &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.contains(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ftmao@example.com&amp;#34;&lt;/span&gt;.isEmail()   &lt;span style=&#34;color:#75715e&#34;&gt;// true&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;空安全是 Kotlin 最值得吹的设计。Java 程序员的一生之敌 &lt;code&gt;NullPointerException&lt;/code&gt;，在 Kotlin 里被类型系统提前拦下——&lt;code&gt;String?&lt;/code&gt; 和 &lt;code&gt;String&lt;/code&gt; 是两个不同的类型，编译器强迫你处理 null。&lt;/p&gt;
&lt;h3 id=&#34;22-协程异步代码同步写&#34;&gt;2.2 协程：异步代码同步写&lt;/h3&gt;
&lt;p&gt;App 开发的核心动作是「发起网络请求 → 更新界面」，而且&lt;strong&gt;网络请求绝不能跑在主线程&lt;/strong&gt;（否则界面卡死，系统直接弹 ANR 杀掉你的 App）。协程让这件事写得像同步代码：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// viewModelScope 是 ViewModel 自带的协程作用域，页面销毁自动取消
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;loadUsers&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    viewModelScope.launch {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _uiState.&lt;span style=&#34;color:#66d9ef&#34;&gt;value&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;UiState&lt;/span&gt;.Loading
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;// withContext(Dispatchers.IO) 切到 IO 线程执行耗时操作
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; users = withContext(&lt;span style=&#34;color:#a6e22e&#34;&gt;Dispatchers&lt;/span&gt;.IO) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                api.getUsers()        &lt;span style=&#34;color:#75715e&#34;&gt;// 挂起函数：等结果，但不阻塞线程
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _uiState.&lt;span style=&#34;color:#66d9ef&#34;&gt;value&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;UiState&lt;/span&gt;.Success(users)   &lt;span style=&#34;color:#75715e&#34;&gt;// 回到主线程更新 UI
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; (e: Exception) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _uiState.&lt;span style=&#34;color:#66d9ef&#34;&gt;value&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;UiState&lt;/span&gt;.Error(e.message &lt;span style=&#34;color:#f92672&#34;&gt;?:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;未知错误&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant UI as 主线程(UI)
    participant CO as 协程
    participant IO as IO 线程池
    UI-&amp;gt;&amp;gt;CO: viewModelScope.launch
    CO-&amp;gt;&amp;gt;IO: withContext(IO) 发起网络请求
    Note over UI: 主线程不阻塞&amp;lt;br/&amp;gt;界面照常响应触摸
    IO--&amp;gt;&amp;gt;CO: 请求返回
    CO--&amp;gt;&amp;gt;UI: 切回主线程更新状态
    UI-&amp;gt;&amp;gt;UI: Compose 自动重组界面&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;思考&lt;/strong&gt;：协程最难的不是语法，是「作用域」的概念——协程挂在谁的 scope 上，决定了它什么时候被取消。用 &lt;code&gt;viewModelScope&lt;/code&gt; 而不是裸 &lt;code&gt;GlobalScope.launch&lt;/code&gt;，你的协程就会随页面销毁自动取消，不会内存泄漏。这是面试高频题，更是线上事故高频源。现在只需要记住结论，第四篇讲完生命周期后你会真正理解为什么。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第三篇-用-compose-写出第一个界面&#34;&gt;第三篇 用 Compose 写出第一个界面&lt;/h2&gt;
&lt;p&gt;语言有了，马上写界面——学习正反馈来得越早越好。&lt;/p&gt;
&lt;h3 id=&#34;31-从-xml-到-compose一次范式革命&#34;&gt;3.1 从 XML 到 Compose：一次范式革命&lt;/h3&gt;
&lt;p&gt;旧时代的 Android UI 是「XML 写布局 + Java/Kotlin 找控件 + 手动 setText」，命令式的，状态和视图靠人肉同步。Compose 是声明式的：&lt;strong&gt;UI 是状态的函数&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph old[&amp;#34;旧时代：命令式&amp;#34;]
        A1[&amp;#34;XML 布局&amp;#34;] --&amp;gt; B1[&amp;#34;findViewById&amp;#34;] --&amp;gt; C1[&amp;#34;手动 setText/setVisibility&amp;#34;]
        C1 --&amp;gt; D1[&amp;#34;状态变了？&amp;lt;br/&amp;gt;记得手动再 set 一遍&amp;lt;br/&amp;gt;(漏了就出 bug)&amp;#34;]
    end
    subgraph new[&amp;#34;Compose：声明式&amp;#34;]
        A2[&amp;#34;状态 State&amp;#34;] --&amp;gt;|&amp;#34;状态变化自动触发重组&amp;#34;| B2[&amp;#34;@Composable 函数&amp;lt;br/&amp;gt;UI = f(state)&amp;#34;]
    end
    style new fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;写过 Vue/React 的人会秒懂——这和前端框架的响应式是同一种思想，只是语法换成了 Kotlin。&lt;/p&gt;
&lt;h3 id=&#34;32-主流-ui-组件库答案比前端简单&#34;&gt;3.2 主流 UI 组件「库」：答案比前端简单&lt;/h3&gt;
&lt;p&gt;前端有 Element Plus / Ant Design / Naive UI 群雄割据，Android 的答案简单得多：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Q{&amp;#34;做什么样的 App？&amp;#34;}
    Q --&amp;gt;|&amp;#34;绝大多数情况&amp;#34;| A[&amp;#34;Material 3 (material3)&amp;lt;br/&amp;gt;官方出品 随 SDK 维护&amp;#34;]
    Q --&amp;gt;|&amp;#34;要高度自定义设计&amp;#34;| B[&amp;#34;Compose Foundation 无样式组件&amp;lt;br/&amp;gt;+ 自建 Design System&amp;#34;]
    Q --&amp;gt;|&amp;#34;跨端 iOS + Android&amp;#34;| C[&amp;#34;Compose Multiplatform&amp;lt;br/&amp;gt;或 Flutter(另一条技术栈)&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Material 3（&lt;code&gt;androidx.compose.material3&lt;/code&gt;）&lt;/strong&gt;：官方组件库，Button、Card、TextField、Dialog、Scaffold、NavigationBar 一应俱全，且深度适配系统特性（深色主题、动态取色）。&lt;strong&gt;先用它，用到不够用再谈别的&lt;/strong&gt;。这和前端「别和组件库对着干」是同一个道理——对抗官方设计体系的自定义，维护成本极高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;accompanist 已成历史&lt;/strong&gt;：早年 Compose 功能不全，靠 accompanist 库补（下拉刷新、权限等），2026 年这些能力已全部并入官方库，新项目不要再引入。&lt;/li&gt;
&lt;li&gt;第三方生态里值得一提的：&lt;a href=&#34;https://coil-kt.github.io/coil/&#34;&gt;Coil&lt;/a&gt;（图片加载，事实标准）、&lt;a href=&#34;https://composedestinations.rafaelcosta.xyz/&#34;&gt;Compose Destinations&lt;/a&gt;（类型安全导航）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;33-demo一个五脏俱全的登录页&#34;&gt;3.3 Demo：一个五脏俱全的登录页&lt;/h3&gt;
&lt;p&gt;把 Kotlin、协程、Compose、Material 3 串起来——这是每个 App 都有的页面：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Composable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;LoginScreen&lt;/span&gt;(viewModel: LoginViewModel = viewModel()) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; state &lt;span style=&#34;color:#66d9ef&#34;&gt;by&lt;/span&gt; viewModel.uiState.collectAsState()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Scaffold { padding &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Column(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            modifier = Modifier
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .padding(padding)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .fillMaxSize()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .padding(&lt;span style=&#34;color:#ae81ff&#34;&gt;24&lt;/span&gt;.dp),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            verticalArrangement = &lt;span style=&#34;color:#a6e22e&#34;&gt;Arrangement&lt;/span&gt;.Center,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            OutlinedTextField(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;value&lt;/span&gt; = state.email,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                onValueChange = viewModel&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;onEmailChange,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                label = { Text(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;邮箱&amp;#34;&lt;/span&gt;) },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                isError = state.emailError &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                supportingText = { state.emailError&lt;span style=&#34;color:#f92672&#34;&gt;?.&lt;/span&gt;let { Text(&lt;span style=&#34;color:#66d9ef&#34;&gt;it&lt;/span&gt;) } },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                modifier = &lt;span style=&#34;color:#a6e22e&#34;&gt;Modifier&lt;/span&gt;.fillMaxWidth(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            Spacer(&lt;span style=&#34;color:#a6e22e&#34;&gt;Modifier&lt;/span&gt;.height(&lt;span style=&#34;color:#ae81ff&#34;&gt;16&lt;/span&gt;.dp))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            OutlinedTextField(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;value&lt;/span&gt; = state.password,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                onValueChange = viewModel&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;onPasswordChange,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                label = { Text(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;密码&amp;#34;&lt;/span&gt;) },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                visualTransformation = PasswordVisualTransformation(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                modifier = &lt;span style=&#34;color:#a6e22e&#34;&gt;Modifier&lt;/span&gt;.fillMaxWidth(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            Spacer(&lt;span style=&#34;color:#a6e22e&#34;&gt;Modifier&lt;/span&gt;.height(&lt;span style=&#34;color:#ae81ff&#34;&gt;24&lt;/span&gt;.dp))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            Button(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                onClick = viewModel&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;login,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                enabled = !state.loading,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                modifier = &lt;span style=&#34;color:#a6e22e&#34;&gt;Modifier&lt;/span&gt;.fillMaxWidth(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (state.loading) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    CircularProgressIndicator(&lt;span style=&#34;color:#a6e22e&#34;&gt;Modifier&lt;/span&gt;.size(&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;.dp))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                } &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    Text(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;登录&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;对应的 ViewModel，注意状态管理的写法：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;LoginUiState&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; email: String = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; password: String = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; emailError: String? = &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; loading: Boolean = &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;LoginViewModel&lt;/span&gt; : ViewModel() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; _uiState = MutableStateFlow(LoginUiState())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; uiState: StateFlow&amp;lt;LoginUiState&amp;gt; = _uiState.asStateFlow()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onEmailChange&lt;/span&gt;(v: String) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _uiState.update { &lt;span style=&#34;color:#66d9ef&#34;&gt;it&lt;/span&gt;.copy(email = v, emailError = &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;login&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; s = _uiState.&lt;span style=&#34;color:#66d9ef&#34;&gt;value&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (!s.email.contains(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@&amp;#34;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _uiState.update { &lt;span style=&#34;color:#66d9ef&#34;&gt;it&lt;/span&gt;.copy(emailError = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;邮箱格式不对&amp;#34;&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        viewModelScope.launch {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _uiState.update { &lt;span style=&#34;color:#66d9ef&#34;&gt;it&lt;/span&gt;.copy(loading = &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                delay(&lt;span style=&#34;color:#ae81ff&#34;&gt;1500&lt;/span&gt;)  &lt;span style=&#34;color:#75715e&#34;&gt;// 假装发请求
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#75715e&#34;&gt;// api.login(s.email, s.password)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                _uiState.update { &lt;span style=&#34;color:#66d9ef&#34;&gt;it&lt;/span&gt;.copy(loading = &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; (e: Exception) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                _uiState.update { &lt;span style=&#34;color:#66d9ef&#34;&gt;it&lt;/span&gt;.copy(loading = &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;设计要点&lt;/strong&gt;（这些比语法值钱）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单一数据源&lt;/strong&gt;：整个页面的所有状态装进一个 &lt;code&gt;UiState&lt;/code&gt; data class，UI 只读它。改状态用 &lt;code&gt;update { it.copy(...) }&lt;/code&gt; 不可变更新，和前端 Redux/Pinia 一个思想。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI 状态的状态机&lt;/strong&gt;：登录按钮有「正常 / 加载中 / 禁用」形态，错误有「无错 / 有错」形态——「UI 本质是状态机」，在任何平台都成立。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;34-踩坑故事三列表滑动卡成-ppt&#34;&gt;3.4 踩坑故事三：列表滑动卡成 PPT&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;LazyColumn&lt;/code&gt; 展示 500 条消息，滑动掉帧明显。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;排查与解法&lt;/strong&gt;，按出现频率排序：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;LazyColumn 卡顿&amp;#34;] --&amp;gt; B{&amp;#34;Composable 里&amp;lt;br/&amp;gt;有耗时计算吗？&amp;#34;}
    B --&amp;gt;|&amp;#34;有，最常见&amp;#34;| C[&amp;#34;用 remember 缓存计算结果&amp;lt;br/&amp;gt;remember(list) { expensive(list) }&amp;#34;]
    B --&amp;gt;|&amp;#34;没有&amp;#34;| D{&amp;#34;item 有稳定的 key 吗？&amp;#34;}
    D --&amp;gt;|&amp;#34;没写 key&amp;#34;| E[&amp;#34;items(list, key = { it.id })&amp;lt;br/&amp;gt;没 key 时位置变动全量重组&amp;#34;]
    D --&amp;gt;|&amp;#34;写了&amp;#34;| F{&amp;#34;在 Debug 包测的？&amp;#34;}
    F --&amp;gt;|&amp;#34;是，冤枉了&amp;#34;| G[&amp;#34;换 Release 包测！&amp;lt;br/&amp;gt;Debug 包 Compose 性能差数倍&amp;#34;]
    style G fill:#ffe4e1&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：第三条最坑——&lt;strong&gt;Compose 的 Debug 包性能不代表线上&lt;/strong&gt;。无数新人在 Debug 包测出「卡顿」，花两天优化，打完 Release 包发现根本不卡。性能问题永远以 Release 包为准。&lt;/p&gt;
&lt;h3 id=&#34;35-踩坑故事四旋转屏幕数据全没了&#34;&gt;3.5 踩坑故事四：旋转屏幕，数据全没了&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：页面填了一半表单，用户一转屏幕，输入全没了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：旋转屏幕默认会&lt;strong&gt;销毁并重建 Activity&lt;/strong&gt;。存在 Composable 局部变量里的状态随重建灰飞烟灭。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法分三层&lt;/strong&gt;，按持久程度选：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 1. remember：只管重组不丢，不管重建 —— 最弱
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; text &lt;span style=&#34;color:#66d9ef&#34;&gt;by&lt;/span&gt; remember { mutableStateOf(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 2. rememberSaveable：重建也能恢复（进 Bundle，有大小限制）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; text &lt;span style=&#34;color:#66d9ef&#34;&gt;by&lt;/span&gt; rememberSaveable { mutableStateOf(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 3. ViewModel + StateFlow：配置变更（旋转、深浅色切换）都不丢 —— 正道
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;//    状态放 ViewModel 里，Activity 重建 ViewModel 不重建&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;感悟&lt;/strong&gt;：Android 的「生命周期地狱」本质上是系统在告诉你：&lt;strong&gt;UI 是廉价的，随时会被销毁；状态是珍贵的，要放在比 UI 活得久的地方&lt;/strong&gt;。这个坑也是下一篇的入口——旋转只是「销毁」的一种，更狠的还在后面。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第四篇-架构与生命周期从写页面到写应用&#34;&gt;第四篇 架构与生命周期：从「写页面」到「写应用」&lt;/h2&gt;
&lt;p&gt;前三篇你能写出一个页面了。但真实 App 是几十个页面、在几亿台不可控的设备上被随意切换、随时被杀。这一篇建立组织代码的地基——《Android 编程权威指南》全书真正想教的就是这一篇的东西。&lt;/p&gt;
&lt;h3 id=&#34;41-activity-生命周期不是八股是保命符&#34;&gt;4.1 Activity 生命周期：不是八股，是保命符&lt;/h3&gt;
&lt;p&gt;生命周期回调几乎每个教程都有，但多数人只背了图没理解「什么时候会走哪条边」：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;stateDiagram-v2
    [*] --&amp;gt; onCreate: 启动
    onCreate --&amp;gt; onStart
    onStart --&amp;gt; onResume: 获得焦点，可交互
    onResume --&amp;gt; onPause: 被半透明遮挡(弹窗)
    onPause --&amp;gt; onResume: 回到前台
    onPause --&amp;gt; onStop: 完全不可见(跳别的页面/按Home)
    onStop --&amp;gt; onRestart: 从后台回来
    onRestart --&amp;gt; onStart
    onStop --&amp;gt; onDestroy: 用户退出或系统回收
    onDestroy --&amp;gt; [*]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;真正值钱的是这两条规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可见性配对&lt;/strong&gt;：&lt;code&gt;onStart/onStop&lt;/code&gt; 管「看不看得见」（注册/注销广播监听放这里），&lt;code&gt;onResume/onPause&lt;/code&gt; 管「能不能交互」（相机、传感器这类独占资源放这里）。放错层级是资源泄漏的常见来源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;onDestroy&lt;/code&gt; 不保证被调用&lt;/strong&gt;：进程被系统直接杀死时，什么回调都不会来。重要数据不能指望「退出时再保存」。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事五：切后台半小时，回来从头再来&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：用户在表单页填到一半，切出去回了个微信，半小时后切回来，App 重新从启动页加载，填的内容全没了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：这不是 3.5 节旋转屏幕那种&lt;strong&gt;配置变更&lt;/strong&gt;，而是&lt;strong&gt;进程死亡（Process Death）&lt;/strong&gt;——App 在后台久了，系统内存不足时直接杀掉整个进程。ViewModel 也跟着进程一起死了，3.5 节的三层方案全部失效。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：ViewModel 里的关键状态用 &lt;strong&gt;SavedStateHandle&lt;/strong&gt;，它会把数据写入进程死亡也能存活的 Bundle：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;FormViewModel&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; state: SavedStateHandle) : ViewModel() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 进程被杀再恢复，draft 还在
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; draft: String
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;get&lt;/span&gt;() = state[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;draft&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;?:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;set&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;value&lt;/span&gt;) { state[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;draft&amp;#34;&lt;/span&gt;] = &lt;span style=&#34;color:#66d9ef&#34;&gt;value&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;怎么测试&lt;/strong&gt;：开发者选项里打开「不保留活动（Don&amp;rsquo;t keep activities）」，或者在 App 切后台后执行 &lt;code&gt;adb shell am kill &amp;lt;包名&amp;gt;&lt;/code&gt; 模拟进程死亡。不测这一关，你永远不知道自己的状态恢复写对了没有——这是权威指南反复强调而多数教程略过的点。&lt;/p&gt;
&lt;h3 id=&#34;42-四大组件与-intentandroid-的宪法&#34;&gt;4.2 四大组件与 Intent：Android 的「宪法」&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;Activity&amp;lt;br/&amp;gt;一页界面&amp;#34;] --- E[&amp;#34;Intent 把它们串起来&amp;#34;]
    B[&amp;#34;Service&amp;lt;br/&amp;gt;无界面的后台工作&amp;lt;br/&amp;gt;(今天多由 WorkManager/前台服务承担)&amp;#34;] --- E
    C[&amp;#34;BroadcastReceiver&amp;lt;br/&amp;gt;监听系统/应用事件&amp;lt;br/&amp;gt;开机完成 网络变化&amp;#34;] --- E
    D[&amp;#34;ContentProvider&amp;lt;br/&amp;gt;跨进程共享结构化数据&amp;lt;br/&amp;gt;联系人 媒体库&amp;#34;] --- E&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;应知应会的点不在概念，而在分寸：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Intent 分显式/隐式&lt;/strong&gt;：显式指定类名跳自己 App 的页面；隐式声明「我想看一张图片 / 打开一个网页」，系统找能干的 App 来接。隐式 Intent 配合 intent-filter，就是 Android 应用间协作的全部基础——第六篇的 App 唤醒全靠它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task 与返回栈&lt;/strong&gt;：启动的 Activity 层层叠在一个任务栈里，返回键出栈。第六篇的 &lt;code&gt;launchMode&lt;/code&gt; 就是在控制这个栈的行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;今天的比重&lt;/strong&gt;：新项目里 Service 大多被 WorkManager 和协程替代，BroadcastReceiver 的系统广播被限制得越来越严，ContentProvider 多数时候你只是「使用者」（读联系人、读相册）。&lt;strong&gt;概念要懂，但别再像十年前的教程那样上来先写三个 Service&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;43-fragmentcompose-时代怎么看它&#34;&gt;4.3 Fragment：Compose 时代怎么看它&lt;/h3&gt;
&lt;p&gt;权威指南的老版本用了大量篇幅讲 Fragment——它是 View 时代组织页面的核心。2026 年的答案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;新项目&lt;/strong&gt;：单 Activity + Compose + Navigation 库，页面就是 Composable，&lt;strong&gt;不需要 Fragment&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绕不开的场景&lt;/strong&gt;：维护老项目、集成只有 View 版本的第三方 SDK（某些地图、支付 SDK），以及系统级 UI（如 &lt;code&gt;PreferenceFragmentCompat&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以对待 Fragment 的正确姿势是：&lt;strong&gt;看懂生命周期比 Activity 多一层（view 的生命周期和 Fragment 本身分离），够用即可，不必深入&lt;/strong&gt;。把省下的时间投给 Compose。&lt;/p&gt;
&lt;h3 id=&#34;44-架构mvvm--repository--单向数据流&#34;&gt;4.4 架构：MVVM + Repository + 单向数据流&lt;/h3&gt;
&lt;p&gt;权威指南从 MVC 讲到 MVVM 的演进，核心思想收敛成今天 Google 官方推荐的这张图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph UI层
        A[&amp;#34;Composable&amp;lt;br/&amp;gt;只负责渲染和上报事件&amp;#34;]
    end
    subgraph 状态层
        B[&amp;#34;ViewModel&amp;lt;br/&amp;gt;持有 UiState(StateFlow)&amp;lt;br/&amp;gt;暴露事件处理函数&amp;#34;]
    end
    subgraph 数据层
        C[&amp;#34;Repository&amp;lt;br/&amp;gt;数据的唯一入口&amp;lt;br/&amp;gt;决定走网络还是缓存&amp;#34;]
        D[&amp;#34;Retrofit&amp;lt;br/&amp;gt;远程数据&amp;#34;]
        E[&amp;#34;Room / DataStore&amp;lt;br/&amp;gt;本地数据&amp;#34;]
    end
    A --&amp;gt;|&amp;#34;用户事件&amp;#34;| B
    B --&amp;gt;|&amp;#34;StateFlow 数据流&amp;#34;| A
    B --&amp;gt; C
    C --&amp;gt; D
    C --&amp;gt; E&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三条铁律，比记住「MVVM」三个字母重要得多：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;UI 不直接碰网络和数据库&lt;/strong&gt;——Composable 里出现 Retrofit 调用，就是架构腐烂的开始。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据向下流，事件向上流（UDF）&lt;/strong&gt;：UI 只读状态、只发事件，ViewModel 改状态。3.3 节的登录 Demo 就是这个模式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repository 是数据的唯一入口&lt;/strong&gt;：ViewModel 不关心数据来自网络还是 Room 缓存，这让「离线优先」的 App 成为可能——先展示 Room 里的旧数据，网络回来再刷新。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意这张图里数据层的两个角色——Retrofit 和 Room。它们就是下一篇的主角。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第五篇-网络与存储数据从哪来存到哪去&#34;&gt;第五篇 网络与存储：数据从哪来，存到哪去&lt;/h2&gt;
&lt;p&gt;架构定了，现在填数据层的肉。这一篇学完，你就能写出「联网 CRUD + 离线缓存」的完整 App 了。&lt;/p&gt;
&lt;h3 id=&#34;51-联网的第一步权限与明文拦截&#34;&gt;5.1 联网的第一步：权限与明文拦截&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事六：请求发出去了，App 里却报 CLEARTEXT 错误&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：Retrofit 请求发出去了，模拟器浏览器也能打开接口地址，App 里却报 &lt;code&gt;CLEARTEXT communication not permitted&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;，两个新手必踩的组合拳：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;AndroidManifest.xml&lt;/code&gt; 忘了声明网络权限（Android 不像前端，联网要申请）：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;manifest&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;...&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;android.permission.INTERNET&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/manifest&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ol start=&#34;2&#34;&gt;
&lt;li&gt;Android 9+ 默认禁止 HTTP 明文请求，测试环境是 &lt;code&gt;http://&lt;/code&gt; 就会撞上。开发期放行：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;application&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;android:usesCleartextTraffic=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;   &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;&amp;lt;!--&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;仅限&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;debug&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;包这么干&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;--&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：上线前把明文流量关掉，让后端上 HTTPS。安全这类事情，「先放行以后再收」基本等于永远不收。&lt;/p&gt;
&lt;h3 id=&#34;52-retrofit网络层的事实标准&#34;&gt;5.2 Retrofit：网络层的事实标准&lt;/h3&gt;
&lt;p&gt;移动端网络层的标准组合：&lt;strong&gt;Retrofit（定义接口）+ OkHttp（发请求）+ 协程（写异步）&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 1. 接口定义：挂起函数 + 注解，Kotlin 协程原生支持
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ApiService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@GET&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;users&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;suspend&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;getUsers&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;@Query&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;page&amp;#34;&lt;/span&gt;) page: Int): ApiResponse&amp;lt;List&amp;lt;User&amp;gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@POST&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;login&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;suspend&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;login&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;@Body&lt;/span&gt; body: LoginRequest): ApiResponse&amp;lt;LoginData&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 2. OkHttp：拦截器统一带 token、打日志、配超时
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; okHttp = &lt;span style=&#34;color:#a6e22e&#34;&gt;OkHttpClient&lt;/span&gt;.Builder()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .connectTimeout(&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;TimeUnit&lt;/span&gt;.SECONDS)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .readTimeout(&lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;TimeUnit&lt;/span&gt;.SECONDS)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .addInterceptor { chain &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; req = chain.request().newBuilder()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .addHeader(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Authorization&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Bearer &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${TokenHolder.token}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .build()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        chain.proceed(req)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .addInterceptor(HttpLoggingInterceptor().setLevel(&lt;span style=&#34;color:#a6e22e&#34;&gt;HttpLoggingInterceptor&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Level&lt;/span&gt;.BODY))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .build()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 3. Retrofit 组装
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; api = &lt;span style=&#34;color:#a6e22e&#34;&gt;Retrofit&lt;/span&gt;.Builder()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .baseUrl(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://api.example.com/&amp;#34;&lt;/span&gt;)   &lt;span style=&#34;color:#75715e&#34;&gt;// ⚠️ 必须以 / 结尾，见踩坑故事七
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .client(okHttp)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .addConverterFactory(&lt;span style=&#34;color:#a6e22e&#34;&gt;GsonConverterFactory&lt;/span&gt;.create())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .build()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .create(ApiService&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt;.java)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事七：baseUrl 结尾少了一个斜杠&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：Retrofit 初始化直接 crash：&lt;code&gt;IllegalArgumentException: baseUrl must end in /&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：几乎每个 Android 开发者都撞过一次。顺手记住另一个相关坑：&lt;code&gt;@GET&lt;/code&gt; 里的相对路径如果以 &lt;code&gt;/&lt;/code&gt; 开头，会丢弃 baseUrl 的路径段——&lt;code&gt;baseUrl(&amp;quot;https://x.com/api/&amp;quot;)&lt;/code&gt; 配 &lt;code&gt;@GET(&amp;quot;/users&amp;quot;)&lt;/code&gt; 实际请求的是 &lt;code&gt;https://x.com/users&lt;/code&gt;，&lt;code&gt;/api&lt;/code&gt; 没了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事八：拦截器里打印了 body，业务层就拿不到数据了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：在自定义拦截器里写了 &lt;code&gt;response.body()!!.string()&lt;/code&gt; 打日志，结果业务层解析永远失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：OkHttp 的响应体是&lt;strong&gt;流，只能消费一次&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：打日志用官方 &lt;code&gt;HttpLoggingInterceptor&lt;/code&gt;（上面 Demo 里已带）；自定义拦截器里要读内容，用 &lt;code&gt;peekBody&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; bodyText = response.peekBody(&lt;span style=&#34;color:#ae81ff&#34;&gt;1024&lt;/span&gt; * &lt;span style=&#34;color:#ae81ff&#34;&gt;1024&lt;/span&gt;).string()  &lt;span style=&#34;color:#75715e&#34;&gt;// ✅ 复制一份读，不影响原流&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;移动端特有的提醒&lt;/strong&gt;：手机用户会进电梯、进地铁、切 Wi-Fi 和流量，弱网和断线是&lt;strong&gt;常态而不是异常&lt;/strong&gt;。超时必须配、失败要给重试入口、一次抖动失败别急着弹「网络异常」的全屏错误——静默重试一次，体验天差地别。&lt;/p&gt;
&lt;h3 id=&#34;53-本地存储四个层级各管各的&#34;&gt;5.3 本地存储：四个层级，各管各的&lt;/h3&gt;
&lt;p&gt;「存东西」在 Android 里不是一件事，先想清楚存什么：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Q{&amp;#34;要存什么？&amp;#34;}
    Q --&amp;gt;|&amp;#34;键值对&amp;lt;br/&amp;gt;设置项 / token&amp;#34;| A[&amp;#34;DataStore&amp;lt;br/&amp;gt;(SharedPreferences 的接班人)&amp;#34;]
    Q --&amp;gt;|&amp;#34;结构化数据&amp;lt;br/&amp;gt;离线缓存 / 消息列表&amp;#34;| B[&amp;#34;Room 数据库&amp;lt;br/&amp;gt;(下一节详解)&amp;#34;]
    Q --&amp;gt;|&amp;#34;图片 / 日志 / 大文件&amp;#34;| C[&amp;#34;App 私有目录&amp;lt;br/&amp;gt;context.filesDir&amp;#34;]
    Q --&amp;gt;|&amp;#34;要存进相册 / 共享给用户&amp;#34;| D[&amp;#34;MediaStore API&amp;lt;br/&amp;gt;(不要直接写 /sdcard)&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;键值存储的 Demo，协程化的 DataStore 用法：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; Context.dataStore &lt;span style=&#34;color:#66d9ef&#34;&gt;by&lt;/span&gt; preferencesDataStore(name = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;settings&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; KEY_TOKEN = stringPreferencesKey(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;token&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;suspend&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;saveToken&lt;/span&gt;(context: Context, token: String) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    context.dataStore.edit { &lt;span style=&#34;color:#66d9ef&#34;&gt;it&lt;/span&gt;[KEY_TOKEN] = token }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 读取是 Flow，可以在 ViewModel 里直接 collect
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; tokenFlow: Flow&amp;lt;String?&amp;gt; = context.dataStore.&lt;span style=&#34;color:#66d9ef&#34;&gt;data&lt;/span&gt;.map { &lt;span style=&#34;color:#66d9ef&#34;&gt;it&lt;/span&gt;[KEY_TOKEN] }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事九：SharedPreferences 引发的 ANR&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：老项目用户反馈「切换页面偶尔卡死」，ANR 日志里堆栈停在 &lt;code&gt;QueuedWork.waitToFinish()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;code&gt;SharedPreferences.apply()&lt;/code&gt; 号称异步，但系统会在 Activity 停顿时把磁盘写入队列 flush 完才放行——数据多了，主线程就在这等着。另外首次 &lt;code&gt;getSharedPreferences()&lt;/code&gt; 的读盘也发生在主线程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：新项目一律 DataStore（协程挂起，天然不阻塞主线程）；老项目遇到这类 ANR，迁移 DataStore 是治本方案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事十：targetSdk 升到 30，读不到文件了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：原来「把文件存到 &lt;code&gt;/sdcard/Download&lt;/code&gt;」的代码，升级 targetSdk 后全部失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：Android 10 引入&lt;strong&gt;分区存储（Scoped Storage）&lt;/strong&gt;，App 再也不能随意读写公共目录了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法分级&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;App 自己的文件 → 用 &lt;code&gt;context.filesDir&lt;/code&gt; / &lt;code&gt;cacheDir&lt;/code&gt;，不需要任何权限，卸载自动清理。&lt;/li&gt;
&lt;li&gt;图片视频要进相册 → 走 &lt;code&gt;MediaStore&lt;/code&gt; API。&lt;/li&gt;
&lt;li&gt;让用户选文档 → 走 SAF（系统文件选择器）。&lt;/li&gt;
&lt;li&gt;想申请 &lt;code&gt;MANAGE_EXTERNAL_STORAGE&lt;/code&gt; 一把梭？Google Play 会拒审，除非你是文件管理器类 App。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;感悟&lt;/strong&gt;：分区存储当年被骂得很惨，但回头看是对的——它终结了「每个 App 都在 SD 卡根目录拉一坨文件夹」的蛮荒时代。适配它的正确姿势不是对抗（到处找绕过方案），而是&lt;strong&gt;按数据的归属选 API&lt;/strong&gt;：属于自己的放私有目录，属于用户的走 MediaStore。&lt;/p&gt;
&lt;h3 id=&#34;54-room编译期检查-sql-的数据库&#34;&gt;5.4 Room：编译期检查 SQL 的数据库&lt;/h3&gt;
&lt;p&gt;结构化数据（消息列表、账单、离线缓存）的答案。完整三件套 Demo：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 1. Entity：一张表
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Entity&lt;/span&gt;(tableName = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;expenses&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Expense&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@PrimaryKey&lt;/span&gt;(autoGenerate = &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; id: Long = &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; title: String,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; amount: Double,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; createdAt: Long,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 2. DAO：操作表的方法，SQL 在编译期校验
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Dao&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ExpenseDao&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Query&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;SELECT * FROM expenses ORDER BY createdAt DESC&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;observeAll&lt;/span&gt;(): Flow&amp;lt;List&amp;lt;Expense&amp;gt;&amp;gt;   &lt;span style=&#34;color:#75715e&#34;&gt;// 返回 Flow：数据变了 UI 自动更新
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Insert&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;suspend&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;insert&lt;/span&gt;(expense: Expense)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Delete&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;suspend&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;delete&lt;/span&gt;(expense: Expense)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 3. Database：单例持有
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Database&lt;/span&gt;(entities = [Expense&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt;], version = &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;abstract&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AppDatabase&lt;/span&gt; : RoomDatabase() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;abstract&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;expenseDao&lt;/span&gt;(): ExpenseDao
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; db = &lt;span style=&#34;color:#a6e22e&#34;&gt;Room&lt;/span&gt;.databaseBuilder(context, AppDatabase&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt;.java, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;app.db&amp;#34;&lt;/span&gt;).build()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;@Query&lt;/code&gt; 返回 &lt;code&gt;Flow&lt;/code&gt; 是 Room 最优雅的用法——数据库一变，Flow 发射新值，Compose 自动重组，「增删改 → 界面刷新」这条链路不用写一行通知代码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事十一：加了个字段，老用户升级就闪退&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：新版给 &lt;code&gt;Expense&lt;/code&gt; 加了一个 &lt;code&gt;category&lt;/code&gt; 字段，自己测没问题，上线后老用户打开就崩：&lt;code&gt;IllegalStateException: A migration from 1 to 2 was required&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：Room 用 &lt;code&gt;version&lt;/code&gt; 管理表结构。老用户手机里的库还是 version 1 的结构，新代码按 version 2 读，Room 要求你提供 Migration（怎么从 1 迁移到 2），你没有提供。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; MIGRATION_1_2 = &lt;span style=&#34;color:#66d9ef&#34;&gt;object&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Migration&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;override&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;migrate&lt;/span&gt;(db: SupportSQLiteDatabase) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        db.execSQL(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ALTER TABLE expenses ADD COLUMN category TEXT NOT NULL DEFAULT &amp;#39;&amp;#39;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Room&lt;/span&gt;.databaseBuilder(&lt;span style=&#34;color:#f92672&#34;&gt;..&lt;/span&gt;.)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .addMigrations(MIGRATION_1_2)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 开发期偷懒可以用 .fallbackToDestructiveMigration()（清库重建）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 上线前必须删掉 —— 那是拿用户数据开玩笑
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .build()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：数据库字段变更和接口变更一样，是&lt;strong&gt;对存量用户的承诺&lt;/strong&gt;。每次改 Entity，先想「老用户的库里有什么」。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第六篇-系统能力定位通知唤醒与平台适配&#34;&gt;第六篇 系统能力：定位、通知、唤醒与平台适配&lt;/h2&gt;
&lt;p&gt;到这里，你的 App 已经「自成一体」了。这一篇让它和操作系统、和其他 App、和真实世界的几亿台设备打交道。系统能力的坑有一个共同特征：&lt;strong&gt;Demo 里都好好的，一到用户手机上就出事&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;61-定位别再用-locationmanager-了&#34;&gt;6.1 定位：别再用 LocationManager 了&lt;/h3&gt;
&lt;p&gt;官方推荐的是 Google Play 服务里的 &lt;code&gt;FusedLocationProviderClient&lt;/code&gt;——它自动在 GPS、基站、Wi-Fi 之间选最优来源，比你手写 &lt;code&gt;LocationManager&lt;/code&gt; 监听省心得多。&lt;/p&gt;
&lt;p&gt;权限申请的关键流程（Android 12 之后有变化）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;请求定位权限&amp;lt;br/&amp;gt;ACCESS_FINE + ACCESS_COARSE&amp;#34;] --&amp;gt; B{&amp;#34;用户怎么选的？&amp;#34;}
    B --&amp;gt;|&amp;#34;精确授权&amp;#34;| C[&amp;#34;高精度定位&amp;lt;br/&amp;gt;米级&amp;#34;]
    B --&amp;gt;|&amp;#34;Android 12+ 只给『大致位置』&amp;#34;| D[&amp;#34;只有约 3km 精度&amp;lt;br/&amp;gt;⚠️ 代码必须兼容这种情况&amp;#34;]
    B --&amp;gt;|&amp;#34;拒绝&amp;#34;| E[&amp;#34;优雅降级&amp;lt;br/&amp;gt;引导手动选择城市&amp;#34;]
    B --&amp;gt;|&amp;#34;需要后台定位&amp;#34;| F[&amp;#34;必须再单独申请&amp;lt;br/&amp;gt;ACCESS_BACKGROUND_LOCATION&amp;lt;br/&amp;gt;且只能跳设置页让用户手动开&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;获取定位的 Demo（配合 &lt;code&gt;kotlinx-coroutines-play-services&lt;/code&gt; 的 &lt;code&gt;await()&lt;/code&gt;）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; fused = &lt;span style=&#34;color:#a6e22e&#34;&gt;LocationServices&lt;/span&gt;.getFusedLocationProviderClient(context)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;suspend&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;getCurrentLocation&lt;/span&gt;(): Location? {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        fused.getCurrentLocation(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;Priority&lt;/span&gt;.PRIORITY_HIGH_ACCURACY,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            CancellationTokenSource().token,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ).await()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; (e: SecurityException) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;// 权限被收回
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事十二：定位永远是 null，或者位置是三天前的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;getLastLocation()&lt;/code&gt; 一会儿返回 null，一会儿返回几个小时前的旧坐标。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：这个方法只是&lt;strong&gt;读系统缓存&lt;/strong&gt;——最近有别的 App 定位过才有值，它自己不会发起定位。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：像上面 Demo 那样用 &lt;code&gt;getCurrentLocation()&lt;/code&gt; 主动请求一次新定位。需要持续追踪（跑步轨迹这类）再用 &lt;code&gt;requestLocationUpdates()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事十三：坐标偏了五百米，人都站到河里了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：GPS 拿到的经纬度画到高德地图上，位置偏移了几百米，用户「站在」马路对面的河里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：国内坐标系的经典天坑——世界上同时存在三种坐标：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;GPS 芯片原始坐标&amp;lt;br/&amp;gt;WGS-84&amp;#34;] --&amp;gt;|&amp;#34;国测局加密算法&amp;#34;| B[&amp;#34;GCJ-02 火星坐标&amp;lt;br/&amp;gt;高德/腾讯/Google 国内用&amp;#34;]
    B --&amp;gt;|&amp;#34;百度再加密一次&amp;#34;| C[&amp;#34;BD-09&amp;lt;br/&amp;gt;百度地图专用&amp;#34;]
    A -.-&amp;gt;|&amp;#34;直接画到高德地图&amp;lt;br/&amp;gt;❌ 偏移 300-600 米&amp;#34;| D[&amp;#34;线上事故&amp;#34;]
    style D fill:#ffe4e1&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：用什么地图 SDK 就转成什么坐标。&lt;code&gt;FusedLocationProviderClient&lt;/code&gt; 返回的是 WGS-84，上高德地图前先用高德 SDK 的 &lt;code&gt;CoordinateConverter&lt;/code&gt; 转 GCJ-02；接百度地图就转 BD-09。&lt;strong&gt;永远不要自己写转换算法&lt;/strong&gt;，加密参数不公开，网上的「破解版公式」精度堪忧。&lt;/p&gt;
&lt;h3 id=&#34;62-消息通知两段历史决定了今天的写法&#34;&gt;6.2 消息通知：两段历史决定了今天的写法&lt;/h3&gt;
&lt;p&gt;通知 API 看着简单，但它是 Android 版本行为变更的重灾区。先记住这两个分水岭：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;Android 8 (API 26)&amp;lt;br/&amp;gt;通知必须归属&amp;lt;br/&amp;gt;NotificationChannel&amp;#34;] --&amp;gt; B[&amp;#34;Android 13 (API 33)&amp;lt;br/&amp;gt;通知变成运行时权限&amp;lt;br/&amp;gt;POST_NOTIFICATIONS&amp;#34;]
    B --&amp;gt; C[&amp;#34;今天的完整姿势&amp;lt;br/&amp;gt;建渠道 + 申请权限&amp;lt;br/&amp;gt;缺一不可&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;完整 Demo：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 第 1 步：建渠道（App 启动时建，重复调用无副作用）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; channel = NotificationChannel(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order&amp;#34;&lt;/span&gt;,                          &lt;span style=&#34;color:#75715e&#34;&gt;// 渠道 id
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;订单消息&amp;#34;&lt;/span&gt;,                        &lt;span style=&#34;color:#75715e&#34;&gt;// 用户在系统设置里看到的名字
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;NotificationManager&lt;/span&gt;.IMPORTANCE_HIGH,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;context.getSystemService(NotificationManager&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt;.java)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .createNotificationChannel(channel)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 第 2 步：Android 13+ 运行时申请权限
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; launcher = rememberLauncherForActivityResult(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;ActivityResultContracts&lt;/span&gt;.RequestPermission()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) { granted &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;/* 记录用户选择，别循环弹 */&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;launcher.launch(&lt;span style=&#34;color:#a6e22e&#34;&gt;Manifest&lt;/span&gt;.permission.POST_NOTIFICATIONS)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 第 3 步：发通知
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; notification = &lt;span style=&#34;color:#a6e22e&#34;&gt;NotificationCompat&lt;/span&gt;.Builder(context, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .setSmallIcon(&lt;span style=&#34;color:#a6e22e&#34;&gt;R&lt;/span&gt;.drawable.ic_notification)   &lt;span style=&#34;color:#75715e&#34;&gt;// 必须，否则直接抛异常
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .setContentTitle(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;订单已发货&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .setContentText(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;您的包裹正在派送&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .setAutoCancel(&lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;)                        &lt;span style=&#34;color:#75715e&#34;&gt;// 点击后自动消失
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .build()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;NotificationManagerCompat&lt;/span&gt;.from(context).notify(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, notification)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事十四：通知死活不显示&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;按出现概率排查：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;通知不显示&amp;#34;] --&amp;gt; B{&amp;#34;建 Channel 了吗？&amp;#34;}
    B --&amp;gt;|&amp;#34;没建（Android 8+ 直接静默丢弃）&amp;#34;| C[&amp;#34;补上 createNotificationChannel&amp;#34;]
    B --&amp;gt;|&amp;#34;建了&amp;#34;| D{&amp;#34;Channel 的 importance&amp;lt;br/&amp;gt;是不是 LOW/MIN？&amp;#34;}
    D --&amp;gt;|&amp;#34;是&amp;#34;| E[&amp;#34;⚠️ 渠道等级一旦创建&amp;lt;br/&amp;gt;代码里改不了！&amp;lt;br/&amp;gt;只能换渠道 id 或引导用户去设置&amp;#34;]
    D --&amp;gt;|&amp;#34;否&amp;#34;| F{&amp;#34;Android 13+ 申请&amp;lt;br/&amp;gt;POST_NOTIFICATIONS 了吗？&amp;#34;}
    F --&amp;gt;|&amp;#34;没申请&amp;#34;| G[&amp;#34;补运行时权限申请&amp;#34;]
    style E fill:#ffe4e1&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;第二个分支最阴：渠道等级创建后&lt;strong&gt;被系统记住&lt;/strong&gt;，你在代码里把 &lt;code&gt;IMPORTANCE_LOW&lt;/code&gt; 改成 &lt;code&gt;IMPORTANCE_HIGH&lt;/code&gt; 毫无作用，要么换一个新的渠道 id，要么引导用户去系统设置里改。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关于推送（Push）多说两句&lt;/strong&gt;：本地通知只是通知的一半，另一半是服务器推消息下来。这里国内生态是割裂的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FCM（Google 推送）在国内不可用。&lt;/li&gt;
&lt;li&gt;国内正道是&lt;strong&gt;厂商通道&lt;/strong&gt;：华为、小米、OPPO、vivo、荣耀各自的推送 SDK。它们是系统级通道，&lt;strong&gt;App 被杀也能收到&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;第三方聚合（个推、极光）可以一套代码接多家厂商通道，小团队首选。&lt;/li&gt;
&lt;li&gt;只靠自己的长连接保活推消息？国产 ROM 杀后台从不手软，重要消息（订单、支付）务必走厂商通道。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;63-app-唤醒deep-link拉起与被拉起&#34;&gt;6.3 App 唤醒：Deep Link、拉起与被拉起&lt;/h3&gt;
&lt;p&gt;4.2 节讲了隐式 Intent 和 intent-filter 的原理，这一节把它用起来。「唤醒」分两个方向：别人拉起你，你拉起别人。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Q{&amp;#34;从哪拉起你的 App？&amp;#34;}
    Q --&amp;gt;|&amp;#34;网页/短信里的 https 链接&amp;#34;| A[&amp;#34;App Links&amp;lt;br/&amp;gt;需域名资产验证 体验最好&amp;#34;]
    Q --&amp;gt;|&amp;#34;其他 App / 二维码&amp;#34;| B[&amp;#34;自定义 Scheme&amp;lt;br/&amp;gt;myapp://detail/123&amp;lt;br/&amp;gt;简单但可被抢注&amp;#34;]
    Q --&amp;gt;|&amp;#34;通知点击&amp;#34;| C[&amp;#34;PendingIntent&amp;lt;br/&amp;gt;配 FLAG_IMMUTABLE&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;Manifest 里声明可唤起入口（Android 12+ 必须显式 &lt;code&gt;android:exported&lt;/code&gt;）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;activity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;.DetailActivity&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;android:exported=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;android:launchMode=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;singleTask&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- ⚠️ 见踩坑故事十五 --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;intent-filter&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;android:autoVerify=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;action&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;android.intent.action.VIEW&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;category&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;android.intent.category.DEFAULT&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;category&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;android.intent.category.BROWSABLE&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;android:scheme=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;android:host=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ftmao.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;android:pathPrefix=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/app&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/intent-filter&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/activity&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事十五：从链接唤起后，页面参数是旧的，或者叠了三四层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：App 已在后台时点击链接，打开的还是上次的旧内容；多点几次链接，返回键要按四下才退出去。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：默认 &lt;code&gt;launchMode=&amp;quot;standard&amp;quot;&lt;/code&gt; 每次唤醒都新建一个 Activity 实例，叠在任务栈顶（4.2 节的返回栈）；而且热启动时新参数走 &lt;code&gt;onNewIntent()&lt;/code&gt; 回调，&lt;strong&gt;不走 &lt;code&gt;onCreate()&lt;/code&gt;&lt;/strong&gt;——只在 &lt;code&gt;onCreate&lt;/code&gt; 里读 intent 的代码自然漏掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：&lt;code&gt;singleTask&lt;/code&gt; + 两个入口都处理：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;override&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onCreate&lt;/span&gt;(savedInstanceState: Bundle?) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;super&lt;/span&gt;.onCreate(savedInstanceState)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    handleDeepLink(intent)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;override&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onNewIntent&lt;/span&gt;(intent: Intent) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;super&lt;/span&gt;.onNewIntent(intent)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    handleDeepLink(intent)   &lt;span style=&#34;color:#75715e&#34;&gt;// 热启动的参数在这里
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;handleDeepLink&lt;/span&gt;(intent: Intent?) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; id = intent&lt;span style=&#34;color:#f92672&#34;&gt;?.&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;data&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;?.&lt;/span&gt;lastPathSegment &lt;span style=&#34;color:#f92672&#34;&gt;?:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 跳详情页
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;反方向（你拉起别的 App）的版本坑&lt;/strong&gt;：Android 11 引入&lt;strong&gt;包可见性&lt;/strong&gt;限制，&lt;code&gt;packageManager.getPackageInfo(&amp;quot;com.tencent.mm&amp;quot;)&lt;/code&gt; 会抛 &lt;code&gt;NameNotFoundException&lt;/code&gt;——不是你代码错了，是系统假装微信不存在。需要在 Manifest 里声明 &lt;code&gt;&amp;lt;queries&amp;gt;&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;queries&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;package&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;com.tencent.mm&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/queries&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;64-平台适配-tips碎片化才是-android-的本体&#34;&gt;6.4 平台适配 tips：碎片化才是 Android 的本体&lt;/h3&gt;
&lt;p&gt;前端适配的是几个浏览器，Android 适配的是「几十种 ROM × 十几个活跃系统版本 × 无数种屏幕」。以下是我项目里反复用到的适配清单：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;屏幕与手势&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尺寸单位永远 &lt;code&gt;dp&lt;/code&gt;/&lt;code&gt;sp&lt;/code&gt;，别碰 &lt;code&gt;px&lt;/code&gt;；字号用 &lt;code&gt;sp&lt;/code&gt; 才能跟随用户的系统字体设置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Android 15 起强制 edge-to-edge&lt;/strong&gt;：内容会画到状态栏和手势导航条下面。用 Compose 的 &lt;code&gt;Scaffold&lt;/code&gt; + &lt;code&gt;Modifier.padding(paddingValues)&lt;/code&gt; 或 &lt;code&gt;WindowInsets&lt;/code&gt; 处理，别自己猜状态栏高度。&lt;/li&gt;
&lt;li&gt;折叠屏和平板：用 &lt;code&gt;WindowSizeClass&lt;/code&gt; 判断屏幕档位切换单栏/双栏布局，别写死「宽度大于 600dp 就是平板」。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;深色模式&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;颜色全部走 &lt;code&gt;MaterialTheme.colorScheme&lt;/code&gt;，&lt;strong&gt;一个 &lt;code&gt;Color.White&lt;/code&gt; 都不要写死&lt;/strong&gt;。写死颜色的页面在深色模式下就是一块刺眼白斑，而且这种 bug 只在用户半夜打开 App 时才暴露。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;版本策略&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;minSdk 26&amp;lt;br/&amp;gt;覆盖 95%+ 活跃设备&amp;#34;] --&amp;gt; B[&amp;#34;targetSdk 跟着&amp;lt;br/&amp;gt;Google Play 年度要求走&amp;#34;]
    B --&amp;gt; C[&amp;#34;行为变更按版本处理&amp;lt;br/&amp;gt;Build.VERSION.SDK_INT 分支&amp;#34;]
    C --&amp;gt; D[&amp;#34;能力检测优于版本判断&amp;lt;br/&amp;gt;能查权限就查权限&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;踩坑故事十六：小米上好好的，华为上定时任务就是不执行&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：提醒功能在小米、Pixel 上正常，部分华为、OPPO 用户反馈「到点不提醒」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：国产 ROM 的后台管控比原生 Android 激进得多——自启动权限、电池优化白名单、后台弹出界面权限，&lt;strong&gt;每家厂商的名称、入口、默认策略都不一样&lt;/strong&gt;。原生 API 层的 AlarmManager、前台服务，到了这些 ROM 上随时可能被「省电策略」干掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;周期性、可延迟的任务用 &lt;strong&gt;WorkManager&lt;/strong&gt;——它是 Google 专门为解决各厂商后台差异做的兼容层，会根据系统能力自动选底层实现。&lt;/li&gt;
&lt;li&gt;必须精确到点的（闹钟类），检测并引导用户跳转对应设置页（&lt;code&gt;Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS&lt;/code&gt; 等），文案讲清楚「不开会怎样」。&lt;/li&gt;
&lt;li&gt;重要消息走 6.2 节说的厂商推送通道，别指望自己保活。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;感悟&lt;/strong&gt;：适配的终极心法只有一句——&lt;strong&gt;不要假设环境，永远做能力检测&lt;/strong&gt;。&lt;code&gt;if (Build.VERSION.SDK_INT &amp;gt;= 33)&lt;/code&gt;、&lt;code&gt;if (checkSelfPermission(...) == GRANTED)&lt;/code&gt;、&lt;code&gt;if (packageManager.resolveActivity(intent) != null)&lt;/code&gt; 这些防御性代码写多了，你就从「在我手机上能跑」毕业到「在所有人手机上能跑」。这也是 Android 开发和前端开发气质上最大的不同：前端的环境是相对可控的浏览器，Android 的环境是几亿台你从没见过的手机。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第七篇-质量与发布从能跑到敢上线&#34;&gt;第七篇 质量与发布：从「能跑」到「敢上线」&lt;/h2&gt;
&lt;p&gt;功能做完只是上半场。这一篇是把 App 交给真实用户之前的最后一道关。&lt;/p&gt;
&lt;h3 id=&#34;71-测试少但要有&#34;&gt;7.1 测试：少但要有&lt;/h3&gt;
&lt;p&gt;测试金字塔在移动端同样成立，按性价比排：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;单元测试 多写&amp;lt;br/&amp;gt;ViewModel / 工具函数&amp;lt;br/&amp;gt;毫秒级 不依赖设备&amp;#34;] --&amp;gt; B[&amp;#34;Compose UI 测试 精选&amp;lt;br/&amp;gt;核心页面渲染与交互&amp;#34;]
    B --&amp;gt; C[&amp;#34;端到端 极少&amp;lt;br/&amp;gt;发版前跑主流程&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;单元测试 Demo（ViewModel + 协程，&lt;code&gt;kotlinx-coroutines-test&lt;/code&gt;）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;LoginViewModelTest&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;`邮箱格式错误时不发起登录`&lt;/span&gt;() = runTest {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; vm = LoginViewModel()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        vm.onEmailChange(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;not-an-email&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        vm.login()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        assertEquals(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;邮箱格式不对&amp;#34;&lt;/span&gt;, vm.uiState.&lt;span style=&#34;color:#66d9ef&#34;&gt;value&lt;/span&gt;.emailError)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        assertFalse(vm.uiState.&lt;span style=&#34;color:#66d9ef&#34;&gt;value&lt;/span&gt;.loading)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Compose UI 测试 Demo：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;LoginScreenTest&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@get&lt;/span&gt;:Rule
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;val&lt;/span&gt; rule = createComposeRule()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;点击登录按钮触发校验提示&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        rule.setContent { LoginScreen() }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        rule.onNodeWithText(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;登录&amp;#34;&lt;/span&gt;).performClick()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        rule.onNodeWithText(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;邮箱格式不对&amp;#34;&lt;/span&gt;).assertIsDisplayed()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;务实建议&lt;/strong&gt;：别追求覆盖率数字。给「钱相关的计算、状态机的转移、数据解析」这三类写单元测试，给「登录、主列表、下单」这三条主链路写 UI 测试，投入产出比最高。&lt;/p&gt;
&lt;h3 id=&#34;72-调试三板斧logcatadb-与-layout-inspector&#34;&gt;7.2 调试三板斧：Logcat、adb 与 Layout Inspector&lt;/h3&gt;
&lt;p&gt;遇到问题时的三板斧，按使用频率排：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Logcat&lt;/strong&gt;：分级日志 &lt;code&gt;Log.d/e/w&lt;/code&gt;，配上明确的 TAG。经验：TAG 用类名，日志里带上关键 id（订单号、用户 id），半年后排查线上问题你会感谢自己。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;adb 命令行&lt;/strong&gt;，高频就这几条：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;adb devices                          &lt;span style=&#34;color:#75715e&#34;&gt;# 设备列表&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;adb install app-debug.apk            &lt;span style=&#34;color:#75715e&#34;&gt;# 安装&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;adb shell am start -n 包名/.MainActivity   &lt;span style=&#34;color:#75715e&#34;&gt;# 拉起页面&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;adb shell am kill 包名                &lt;span style=&#34;color:#75715e&#34;&gt;# 模拟进程死亡（4.1 节测试用）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;adb logcat -s 你的TAG                 &lt;span style=&#34;color:#75715e&#34;&gt;# 只看自己的日志&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ol start=&#34;3&#34;&gt;
&lt;li&gt;&lt;strong&gt;Layout Inspector&lt;/strong&gt;：Compose 时代的「布局透视镜」，实时看每个 Composable 的重组次数——重组次数异常飙升，就是 3.4 节性能问题的第一现场。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;73-国际化与无障碍专业完成度的分水岭&#34;&gt;7.3 国际化与无障碍：专业完成度的分水岭&lt;/h3&gt;
&lt;p&gt;这两件事区分了「能用的 App」和「专业的 App」：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;国际化（i18n）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有文案进 &lt;code&gt;res/values/strings.xml&lt;/code&gt;，代码里用 &lt;code&gt;stringResource(R.string.xxx)&lt;/code&gt;，&lt;strong&gt;一个中文字符串都不要硬编码&lt;/strong&gt;——今天不国际化，明天产品说「做个英文版」你就要全文搜索替换。&lt;/li&gt;
&lt;li&gt;复数用 &lt;code&gt;&amp;lt;plurals&amp;gt;&lt;/code&gt;（英文的 1 item / 2 items 规则不同），别用字符串拼接数字。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;无障碍（a11y）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图片和图标按钮加 &lt;code&gt;contentDescription&lt;/code&gt;（视障用户的屏幕阅读器 TalkBack 靠它朗读）：&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;IconButton(onClick = onBack) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Icon(&lt;span style=&#34;color:#a6e22e&#34;&gt;Icons&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;AutoMirrored&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Filled&lt;/span&gt;.ArrowBack, contentDescription = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;返回&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;可点击区域不小于 48dp——Material 组件默认满足，自定义 Canvas 绘制按钮时最容易违规。&lt;/li&gt;
&lt;li&gt;最后打开 TalkBack 把自己的 App 主流程走一遍，十分钟，胜过读十篇规范。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;74-发布签名混淆与-aab&#34;&gt;7.4 发布：签名、混淆与 AAB&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 签名（Keystore）&lt;/strong&gt;：Android 用签名识别「同一开发者」。生成 release keystore 后&lt;strong&gt;立刻备份到至少两个安全的地方&lt;/strong&gt;——丢了它，你的 App 永远无法更新，只能换包名重新上架，用户和评分清零。这不是理论风险，是每个公司都流传过的真实事故。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 混淆（R8）&lt;/strong&gt;：Release 包默认开启，压缩、优化、混淆代码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑故事十七：Debug 包好好的，Release 包登录一直失败&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：Debug 一切正常，打完 Release 包，接口返回的 JSON 解析出来所有字段都是 null。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：R8 把 data class 的字段名混淆成了 &lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;c&lt;/code&gt;，而 Gson 默认靠&lt;strong&gt;字段名反射&lt;/strong&gt;匹配 JSON 的 key——名字没了，匹配全落空。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;，二选一：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 方案 A：给序列化模型加 keep 规则（proguard-rules.pro）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;-keep &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;com&lt;/span&gt;.example.app.&lt;span style=&#34;color:#66d9ef&#34;&gt;data&lt;/span&gt;.model.** { *; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 方案 B：换 Moshi/kotlinx-serialization，编译期生成适配器，不靠反射&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：&lt;strong&gt;所有验证都必须用 Release 包做最后一遍&lt;/strong&gt;。Debug 和 Release 的差异（混淆、压缩、日志）大到足以让「我测过了」变成一句空话——3.4 节的性能测量如此，7.4 节的功能验证同样如此。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 打包 AAB&lt;/strong&gt;：Google Play 要求上传 Android App Bundle（.aab）而非 APK，由 Play 按用户设备分发精简后的安装包。&lt;code&gt;versionCode&lt;/code&gt; 单调递增（系统靠它判断更新），&lt;code&gt;versionName&lt;/code&gt; 给人看。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第八篇-ai-时代怎么少写一半代码把-app-做出来&#34;&gt;第八篇 AI 时代：怎么少写一半代码把 App 做出来&lt;/h2&gt;
&lt;p&gt;前七篇是「内功」，这一篇是「兵器」。我的结论先放在这：&lt;strong&gt;AI 没有让 Android 开发变简单，它让「懂行的人」快了 3-5 倍，让「不懂行的人」更快地写出能跑但烂尾的代码&lt;/strong&gt;。区别在于你怎么用它。&lt;/p&gt;
&lt;h3 id=&#34;81-ai-在-android-开发各环节的胜任度&#34;&gt;8.1 AI 在 Android 开发各环节的胜任度&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph good[&amp;#34;AI 做得极好 放心交出去&amp;#34;]
        A1[&amp;#34;样板代码&amp;lt;br/&amp;gt;ViewModel / data class / Repository&amp;#34;]
        A2[&amp;#34;单页面 UI&amp;lt;br/&amp;gt;给我画一个设置页&amp;#34;]
        A3[&amp;#34;单元测试&amp;#34;]
        A4[&amp;#34;解释报错信息&amp;#34;]
    end
    subgraph mid[&amp;#34;AI 能做 但你要盯着&amp;#34;]
        B1[&amp;#34;跨页面架构改造&amp;#34;]
        B2[&amp;#34;性能优化&amp;#34;]
        B3[&amp;#34;依赖升级适配&amp;#34;]
    end
    subgraph bad[&amp;#34;AI 做不好 还得靠人&amp;#34;]
        C1[&amp;#34;环境/构建问题&amp;lt;br/&amp;gt;(它看不到你的机器)&amp;#34;]
        C2[&amp;#34;真机/特定 ROM 才出现的 bug&amp;#34;]
        C3[&amp;#34;产品取舍&amp;lt;br/&amp;gt;这个交互要不要做&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;82-实战工作流需求--app-的-ai-协作五步法&#34;&gt;8.2 实战工作流：需求 → App 的 AI 协作五步法&lt;/h3&gt;
&lt;p&gt;这是我自己做一个新 App 时的真实流程：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：先写「项目宪法」，再让 AI 写代码。&lt;/strong&gt; 在项目里放一个 &lt;code&gt;AGENTS.md&lt;/code&gt; / &lt;code&gt;.cursorrules&lt;/code&gt;，写明技术栈和约定：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# 项目约定
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Kotlin + Jetpack Compose + Material 3，不用 XML 布局
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 架构：MVVM + Repository，单向数据流，状态全部走 ViewModel + StateFlow + UiState
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 网络：Retrofit + OkHttp + 协程，挂起函数返回 ApiResponse&amp;lt;T&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 本地：Room 存结构化数据，DataStore 存键值，定时/后台任务用 WorkManager
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 图片加载用 Coil，导航用 Navigation Compose
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; 禁止 GlobalScope；禁止硬编码颜色和文案，一律走 MaterialTheme 和 strings.xml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- Room 改 Entity 必须同步提供 Migration 并升 version&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;没有这份文件，AI 每次生成代码的风格都随机漂移；有了它，AI 的输出稳定得像团队里最守规矩的那个工程师。注意这份清单里的每一条，都对应着前面某一篇的某个坑——&lt;strong&gt;项目宪法本质上是踩坑笔记的浓缩&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：页面级生成，而不是 App 级生成。&lt;/strong&gt; 不要对 AI 说「给我写一个记账 App」，要说「按项目约定写一个记账首页：顶部本月支出汇总卡片，下面是 LazyColumn 账单列表，每条显示分类图标、备注、金额」。一次一个页面、一个模块，AI 的输出质量和你的可控性都最高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步：让它先写数据层和状态，再写 UI。&lt;/strong&gt; 先生成 Entity/DAO/Repository + &lt;code&gt;UiState&lt;/code&gt; + ViewModel，review 通过后再让它写 Composable——这正是第四、五篇的架构顺序。状态设计错了，UI 写得再漂亮也是白写。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四步：编译错误直接喂回去。&lt;/strong&gt; Android Studio 现在集成了 Gemini，编译报错选中 → 问 AI → 应用修复，循环两三次大部分构建问题都能解决。但 1.3、1.4 那种&lt;strong&gt;环境级问题 AI 帮不了你&lt;/strong&gt;——它看不到你本地的 SDK 装在哪个盘；6.4 那种「只在华为手机上复现」的问题它更帮不了——它没有真机。这就是为什么 AI 篇必须排在最后：&lt;strong&gt;它是放大器，放大的是你前七篇的水平&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第五步：人工 review 的四个必查点。&lt;/strong&gt; AI 生成的 Android 代码，我每次都只细查四处：① 协程作用域是否用了 &lt;code&gt;viewModelScope&lt;/code&gt;（内存泄漏重灾区）；② 状态是否放进了 ViewModel 而不是 &lt;code&gt;remember&lt;/code&gt; 在深层 Composable 里（旋转和进程死亡丢数据）；③ 网络/IO 是否切了 &lt;code&gt;Dispatchers.IO&lt;/code&gt;（ANR 来源）；④ 权限和版本分支有没有做能力检测（AI 爱写「假设权限已授予」的代码）。其他代码粗看即可——&lt;strong&gt;review 的精力要花在 AI 的已知短板上&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;83-一个真实对比demo-app-的工时变化&#34;&gt;8.3 一个真实对比：Demo App 的工时变化&lt;/h3&gt;
&lt;p&gt;3.3 节那个登录页 + 一个用户列表页的小 Demo，三年前我要写一整天（查 API、调布局、处理状态）。2026 年用 AI 的实际耗时：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;pie title 一个双页面 Demo 的 2 小时去哪了
    &amp;#34;写 AGENTS.md 和提示词&amp;#34; : 20
    &amp;#34;AI 生成 + 我 review 修改&amp;#34; : 60
    &amp;#34;环境构建问题排查(AI帮不上)&amp;#34; : 25
    &amp;#34;真机测试与细节打磨&amp;#34; : 15&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;注意那张饼图里最大的启示：&lt;strong&gt;省下来的时间没有消失，它转移到了「描述需求」和「审查代码」上&lt;/strong&gt;。AI 时代的核心能力从「打字速度」变成了两样：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;把模糊需求拆成精确技术任务的能力&lt;/strong&gt;——你对第四篇架构的理解越深，给 AI 的指令就越准。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快速判断代码对错的能力&lt;/strong&gt;——这恰恰要求你懂前七篇讲的所有基本功。看不懂协程作用域的人，review 不了 AI 写的 Android 代码。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;84-思考ai-是放大器不是替代品&#34;&gt;8.4 思考：AI 是放大器，不是替代品&lt;/h3&gt;
&lt;p&gt;每次技术圈出现「AI 能不能取代程序员」的争论，我都想起一个类比：计算器没有取代会计，它淘汰了「只会打算盘的会计」，成就了「用计算器做财务分析的会计」。AI 对 Android 开发者的影响一样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它消灭了&lt;strong&gt;记忆型知识&lt;/strong&gt;的价值（某个 API 的参数顺序，查得再快也不如 AI 直接生成）。&lt;/li&gt;
&lt;li&gt;它放大了&lt;strong&gt;判断型知识&lt;/strong&gt;的价值（这个状态该放 ViewModel 还是 Composable？这个版本冲突该升 AGP 还是降 Gradle？这个定时任务该用 WorkManager 还是 AlarmManager？这次 Room 改表要不要写 Migration？）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以这篇文章前七篇的内容，在 AI 时代不但没有过时，反而更重要了——它们是你在 AI 写完代码后，能说「对」或者「不对」的全部依据。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;结语学习路线收个尾&#34;&gt;结语：学习路线收个尾&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;第 1 周&amp;lt;br/&amp;gt;环境搭建 + 跑通模板&amp;lt;br/&amp;gt;+ Kotlin 语法&amp;#34;] --&amp;gt; B[&amp;#34;第 2-4 周&amp;lt;br/&amp;gt;Compose 基础&amp;lt;br/&amp;gt;写 5 个静态页面&amp;#34;]
    B --&amp;gt; C[&amp;#34;第 2-3 个月&amp;lt;br/&amp;gt;架构 + 网络 + Room&amp;lt;br/&amp;gt;写一个完整的&amp;lt;br/&amp;gt;联网 CRUD App&amp;#34;]
    C --&amp;gt; D[&amp;#34;第 3-6 个月&amp;lt;br/&amp;gt;定位/通知/唤醒/适配&amp;lt;br/&amp;gt;测试 签名 发布上架&amp;#34;]
    D --&amp;gt; E[&amp;#34;持续&amp;lt;br/&amp;gt;把 AI 变成&amp;lt;br/&amp;gt;你的初级工程师&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三条反直觉的建议作为结尾：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一周死磕环境不丢人&lt;/strong&gt;。每个 Android 老兵都经历过 Gradle 转圈的下午，这不是你笨，是这个生态的真实成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用真机开发，而且要用不止一台&lt;/strong&gt;。模拟器掩盖了低端机的性能问题，单品牌真机掩盖了 ROM 碎片化问题——你的用户分布在十几种厂商的手机上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;让 AI 写代码，让自己写笔记&lt;/strong&gt;。每踩一个坑记下「现象、根因、下次怎么一眼认出」，这些笔记是 AI 时代你不可被替代的资产——因为 AI 知道所有答案，但它不知道&lt;strong&gt;你的项目昨天为什么炸&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;祝你的 Gradle 永远秒同步，keystore 永远有备份，App 在华为和小米上都一样坚挺。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>基于 UDP 通信的洋葱网络设计：服务器、Android 客户端与 P2P 控制总结</title>
      <link>https://ftmao.com/blog/posts/udp-onion-network-and-p2p-design/</link>
      <pubDate>Tue, 19 Dec 2017 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/udp-onion-network-and-p2p-design/</guid>
      <description>&lt;h1 id=&#34;基于-udp-通信的洋葱网络设计服务器android-客户端与-p2p-控制总结&#34;&gt;基于 UDP 通信的洋葱网络设计：服务器、Android 客户端与 P2P 控制总结&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;最近要实现端到端的加密通信，现在把设计方案记录下来。目标读者：刚接触洋葱网络、P2P 或 UDP 信令设计，想了解「怎么用 UDP 搭一个匿名转发网络，再基于它做 P2P」的同学。&lt;br&gt;
阅读目标：建立整体架构图、理解关键模块职责、记住常见踩坑点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;0-写在前面为什么用-udp-做洋葱网络--p2p-信令&#34;&gt;0. 写在前面：为什么用 UDP 做洋葱网络 + P2P 信令？&lt;/h2&gt;
&lt;p&gt;洋葱网络（Onion Network）的核心思想是：&lt;strong&gt;数据像剥洋葱一样，一层一层加密转发&lt;/strong&gt;。每一跳节点只知道上一跳和下一跳，不知道完整路径，从而保护通信双方的隐私。&lt;/p&gt;
&lt;p&gt;传统洋葱路由（如 Tor）多跑在 TCP 上，因为 TCP 可靠、有序。但在移动场景或 P2P 场景里，UDP 有几个独特优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;开销小&lt;/strong&gt;：UDP 头只有 8 字节，没有连接建立/拆除开销，适合移动弱网。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NAT 友好&lt;/strong&gt;：UDP 是 NAT 打洞（Hole Punching）最常用的传输层协议，STUN/TURN/ICE 都优先用 UDP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适合信令&lt;/strong&gt;：P2P 建立连接前的「打招呼」消息很短，UDP 足够用；真正的大数据再切到 P2P 直连或中继。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抗审查更灵活&lt;/strong&gt;：无连接特性让流量更像普通 DNS/QUIC/游戏报文，便于混淆。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但 UDP 也带来问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不保证可靠、不保证顺序，需要自己在应用层做重传、去重、拥塞控制。&lt;/li&gt;
&lt;li&gt;报文可能被分片、被防火墙限制、被运营商 QoS。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这篇文章会带你从「一个 UDP 报文怎么进洋葱网络」开始，逐步拆解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;洋葱网络的层次结构与加密过程。&lt;/li&gt;
&lt;li&gt;服务器（目录服务 + 中继 + 信令服务）应该怎么设计。&lt;/li&gt;
&lt;li&gt;Android 客户端的结构与关键代码思路。&lt;/li&gt;
&lt;li&gt;怎么用这套 UDP 信令网络做 P2P 控制（NAT 穿透、打洞、直连、中继回退）。&lt;/li&gt;
&lt;li&gt;新人最容易踩的坑与规避方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-洋葱网络核心概念先建立知识地图&#34;&gt;1. 洋葱网络核心概念：先建立「知识地图」&lt;/h2&gt;
&lt;h3 id=&#34;11-洋葱路由-vs-普通代理的区别&#34;&gt;1.1 洋葱路由 vs 普通代理的区别&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;对比项&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;普通代理&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;洋葱网络&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;路径&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;单跳&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;多跳（通常 3 跳）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;加密&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;端到端一次加密&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;每跳一层加密&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;节点知情范围&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;代理知道你是谁、你访问哪&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;每跳只知道上一跳和下一跳&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;代表&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;HTTP/SOCKS 代理&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Tor、I2P、自定义洋葱网络&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;12-一次洋葱转发的过程&#34;&gt;1.2 一次洋葱转发的过程&lt;/h3&gt;
&lt;p&gt;假设 Alice 要给 Bob 发消息，路径为：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[Alice] --&amp;gt; E[&amp;#34;入口节点 (Entry)&amp;#34;] --&amp;gt; M[&amp;#34;中间节点 (Middle)&amp;#34;] --&amp;gt; X[&amp;#34;出口节点 (Exit)&amp;#34;] --&amp;gt; B[Bob]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;Alice 会先把报文&lt;strong&gt;从后往前&lt;/strong&gt;逐层加密：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 Exit 的公钥加密「Exit → Bob 的真实数据」，得到 &lt;code&gt;C3&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用 Middle 的公钥加密「下一站是 Exit + C3」，得到 &lt;code&gt;C2&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用 Entry 的公钥加密「下一站是 Middle + C2」，得到 &lt;code&gt;C1&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后 Alice 把 &lt;code&gt;C1&lt;/code&gt; 发给 Entry。Entry 用自己的私钥解密后，看到「下一站是 Middle」，把 &lt;code&gt;C2&lt;/code&gt; 转发给 Middle。Middle 解密后转发 &lt;code&gt;C3&lt;/code&gt; 给 Exit。Exit 解密后拿到真实数据，发给 Bob。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[Alice] --&amp;gt;|C1| E[Entry&amp;lt;br/&amp;gt;解密: 下一站 Middle] --&amp;gt;|C2| M[Middle&amp;lt;br/&amp;gt;解密: 下一站 Exit] --&amp;gt;|C3| X[Exit&amp;lt;br/&amp;gt;解密: 真实数据] --&amp;gt;|明文| B[Bob]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;新人记忆点&lt;/strong&gt;：洋葱网络的数据是「加密好的俄罗斯套娃」，每到一个节点就剥掉一层外衣。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;13-udp-洋葱报文的基本结构&#34;&gt;1.3 UDP 洋葱报文的基本结构&lt;/h3&gt;
&lt;p&gt;一个 UDP 洋葱报文可以简单设计为：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+----------------+----------------+----------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|  版本 (1B)     |  命令 (1B)     |    跳数 (1B)   |   预留 (1B)    |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+----------------+----------------+----------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|                        会话 ID / 电路 ID (4B)                       |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+----------------+----------------+----------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|                         下一跳洋葱层数据 ...                       |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+------------------------------------------------------------------+&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版本&lt;/strong&gt;：协议版本，方便后续升级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;命令&lt;/strong&gt;：路由、数据、建立、销毁、心跳等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳数 / TTL&lt;/strong&gt;：防止报文无限循环，每经过一跳减 1。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会话 ID / 电路 ID&lt;/strong&gt;：标识一条逻辑链路，让节点知道该用哪把密钥解密。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下一跳洋葱层数据&lt;/strong&gt;：被当前节点私钥解密后，才能得到真正的下一跳地址和载荷。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;新人容易混淆的点&lt;/strong&gt;：洋葱网络里的「加密」不是端到端 TLS，而是&lt;strong&gt;逐跳加密&lt;/strong&gt;。每一跳都能解密自己最外层，但看不到内层。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-系统整体架构&#34;&gt;2. 系统整体架构&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph 公网服务层
        DS[Directory Server&amp;lt;br/&amp;gt;目录服务：发布可信节点]
        SS[Signaling Server&amp;lt;br/&amp;gt;信令服务：NAT 信息交换]
        RS[Relay Server&amp;lt;br/&amp;gt;中继服务：UDP 洋葱转发]
    end

    subgraph Android 客户端
        AC[App 业务层]
        CM[Connection Manager&amp;lt;br/&amp;gt;管理 Circuits / Peers]
        OC[Onion Core&amp;lt;br/&amp;gt;分层加密 / 解密]
        NC[Network Core&amp;lt;br/&amp;gt;UDP Socket / NAT / ICE]
    end

    subgraph 对端
        P2P[Peer B&amp;lt;br/&amp;gt;Android / Server / IoT]
    end

    AC --&amp;gt; CM
    CM --&amp;gt; OC
    CM --&amp;gt; NC
    OC --&amp;gt; NC
    NC --&amp;gt;|UDP| RS
    NC --&amp;gt;|UDP| SS
    NC -.-&amp;gt;|查询节点列表| DS
    NC -.-&amp;gt;|P2P 直连或中继| P2P&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;21-三类服务器职责&#34;&gt;2.1 三类服务器职责&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;服务&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;职责&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;协议&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Directory Server（目录服务）&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;维护可信中继节点列表、公钥、负载、版本&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;HTTPS / UDP 查询&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Signaling Server（信令服务）&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;帮助两个 Peer 交换公网 IP:Port、NAT 类型、候选地址&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;UDP + 可靠信令协议&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Relay Server（中继服务）&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;当 P2P 打洞失败时，转发 UDP 洋葱流量&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;UDP 洋葱路由&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;实际部署中，目录服务可以用 HTTPS 更稳；信令和中继为了速度和 NAT 友好，优先用 UDP。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;3-服务器端设计&#34;&gt;3. 服务器端设计&lt;/h2&gt;
&lt;h3 id=&#34;31-目录服务directory-server&#34;&gt;3.1 目录服务（Directory Server）&lt;/h3&gt;
&lt;p&gt;目录服务就像「黄页」，让客户端知道哪些中继节点在线、可信、负载低。&lt;/p&gt;
&lt;h4 id=&#34;核心数据模型&#34;&gt;核心数据模型&lt;/h4&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Node {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    node_id:        节点唯一 ID
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    public_key:     Ed25519/X25519 公钥
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    endpoint:       公网 IP:UDP 端口
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    bandwidth:      声明带宽（Mbps）
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    load:           当前负载（连接数 / 带宽占用）
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    version:        协议版本
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    flags:          入口 / 中间 / 出口 / 中继 标志位
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    last_seen:      最后一次心跳时间
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    signature:      节点用私钥对以上字段签名
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h4 id=&#34;客户端获取节点列表的流程&#34;&gt;客户端获取节点列表的流程&lt;/h4&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant C as Android Client
    participant D as Directory Server
    participant N1 as Node Entry
    participant N2 as Node Middle
    participant N3 as Node Exit

    C-&amp;gt;&amp;gt;D: HTTPS GET /nodes?count=3&amp;amp;flags=entry|middle|exit
    D--&amp;gt;&amp;gt;C: 返回节点列表 + 签名
    C-&amp;gt;&amp;gt;C: 验证签名、选择路径
    C-&amp;gt;&amp;gt;N1: UDP 建立 Circuit 请求
    N1--&amp;gt;&amp;gt;C: 返回 Circuit ID
    C-&amp;gt;&amp;gt;N1: 经过 N1 转发到 N2 建立
    N2--&amp;gt;&amp;gt;C: 返回确认
    C-&amp;gt;&amp;gt;N2: 经过 N1→N2 转发到 N3 建立
    N3--&amp;gt;&amp;gt;C: 返回确认&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;新人避坑&lt;/strong&gt;：目录服务返回的节点列表必须带签名，客户端要校验，否则会被恶意节点钓鱼。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;32-中继服务relay-server&#34;&gt;3.2 中继服务（Relay Server）&lt;/h3&gt;
&lt;p&gt;中继服务是洋葱网络里的「快递员」：收到一个 UDP 报文，解密最外层，转发给下一跳。&lt;/p&gt;
&lt;h4 id=&#34;核心状态&#34;&gt;核心状态&lt;/h4&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Circuit {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    circuit_id:     本节点分配的 32 位 ID
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    prev_node:      上一跳地址（IP:Port）
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    next_node:      下一跳地址（IP:Port）
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    shared_key:     与创建者协商的共享密钥
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    created_at:     创建时间
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    last_active:    最后活跃时间
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    bytes_forward:  转发字节数
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h4 id=&#34;转发流程&#34;&gt;转发流程&lt;/h4&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[收到 UDP 报文] --&amp;gt; B{报文格式合法?}
    B --&amp;gt;|否| C[直接丢弃]
    B --&amp;gt;|是| D[查找 circuit_id]
    D --&amp;gt; E{存在?}
    E --&amp;gt;|否| C
    E --&amp;gt;|是| F[用 shared_key 解密洋葱层]
    F --&amp;gt; G{解密成功?}
    G --&amp;gt;|否| C
    G --&amp;gt;|是| H[读取下一跳地址 + 内层数据]
    H --&amp;gt; I[TTL - 1]
    I --&amp;gt; J{TTL &amp;gt; 0?}
    J --&amp;gt;|否| K[发送销毁通知]
    J --&amp;gt;|是| L[转发给下一跳]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h4 id=&#34;关键设计点&#34;&gt;关键设计点&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;限速与防滥用&lt;/strong&gt;：单 Circuit 限制带宽，防止被当免费代理滥用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL 与环检测&lt;/strong&gt;：每跳 TTL 减 1，到 0 丢弃，防止恶意构造环路。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;密钥派生&lt;/strong&gt;：每个 Circuit 用 X25519 ECDH 协商出共享密钥，再用 HKDF 派出发送/接收两个方向的对称密钥。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无日志策略&lt;/strong&gt;：中继节点不记录明文内容、不记录完整路径（实际上也看不到）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;33-信令服务signaling-server&#34;&gt;3.3 信令服务（Signaling Server）&lt;/h3&gt;
&lt;p&gt;信令服务是 P2P 的「媒人」：两个 Peer 无法直接找到对方公网地址时，通过信令服务交换信息。&lt;/p&gt;
&lt;h4 id=&#34;数据模型&#34;&gt;数据模型&lt;/h4&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PeerInfo {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    peer_id:        用户/设备唯一 ID
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    public_key:     用于加密信令的公钥
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    nat_type:       全锥型 / 限制型 / 端口限制型 / 对称型
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    local_candidates: [局域网 IP:Port, ...]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    srflx_candidates: [STUN 映射后的公网 IP:Port, ...]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    relay_candidates: [TURN 中继地址, ...]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    timestamp:      更新时间
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    signature:      防止伪造
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h4 id=&#34;典型信令流程&#34;&gt;典型信令流程&lt;/h4&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant A as Peer A
    participant S as Signaling Server
    participant B as Peer B

    A-&amp;gt;&amp;gt;S: 注册/上报 PeerInfo（UDP 心跳包）
    B-&amp;gt;&amp;gt;S: 注册/上报 PeerInfo

    A-&amp;gt;&amp;gt;S: 请求连接 peer_B
    S-&amp;gt;&amp;gt;S: 检查 A、B 是否在线
    S--&amp;gt;&amp;gt;A: 返回 B 的候选地址列表
    S--&amp;gt;&amp;gt;B: 转发 A 的连接请求 + A 的候选地址

    A-&amp;gt;&amp;gt;A: 按优先级尝试直连 / 打洞
    B-&amp;gt;&amp;gt;B: 同时尝试直连 / 打洞
    A--&amp;gt;&amp;gt;B: UDP 打洞成功，建立 P2P
    B--&amp;gt;&amp;gt;A: 业务数据互通

    alt 打洞失败
        A-&amp;gt;&amp;gt;S: 请求中继
        S--&amp;gt;&amp;gt;A: 分配 Relay 地址
        S--&amp;gt;&amp;gt;B: 通知 B 通过 Relay 通信
        A-&amp;gt;&amp;gt;R: 经中继与 B 通信
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;新人避坑&lt;/strong&gt;：信令服务本身不传输业务数据，只传「怎么找到对方」。不要把它当成聊天服务器用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;4-android-客户端设计&#34;&gt;4. Android 客户端设计&lt;/h2&gt;
&lt;p&gt;Android 客户端需要同时处理：业务逻辑、洋葱路由、UDP 网络、NAT 穿透、后台保活。建议按层拆分。&lt;/p&gt;
&lt;h3 id=&#34;41-客户端分层架构&#34;&gt;4.1 客户端分层架构&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph 应用层
        UI[UI / ViewModel]
        API[业务 API]
    end

    subgraph 网络中间层
        CM[ConnectionManager&amp;lt;br/&amp;gt;管理多 Circuit / 多 Peer]
        P2P[P2PController&amp;lt;br/&amp;gt;打洞 / 直连 / 中继决策]
        SM[SignalingClient&amp;lt;br/&amp;gt;与信令服务通信]
    end

    subgraph 核心引擎层
        OC[OnionCore&amp;lt;br/&amp;gt;加密 / 解密 / 报文封装]
        NC[NetworkCore&amp;lt;br/&amp;gt;UDP Socket / ICE / STUN]
        CR[CircuitStore&amp;lt;br/&amp;gt;持久化 Circuit 状态]
    end

    UI --&amp;gt; API
    API --&amp;gt; CM
    CM --&amp;gt; P2P
    CM --&amp;gt; SM
    CM --&amp;gt; OC
    P2P --&amp;gt; OC
    P2P --&amp;gt; NC
    SM --&amp;gt; NC
    OC --&amp;gt; NC
    CM --&amp;gt; CR&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-关键模块说明&#34;&gt;4.2 关键模块说明&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;模块&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;职责&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;技术要点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;ConnectionManager&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;管理所有活跃 Circuit 和 Peer 连接&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;连接池、复用、超时清理&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;P2PController&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;决定是否直连、打洞、还是走中继&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;ICE 候选排序、NAT 类型判断&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;SignalingClient&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;与信令服务器通信&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;UDP 心跳、请求/响应匹配、重传&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;OnionCore&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;分层加密/解密、报文打包&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;X25519、ChaCha20-Poly1305、HKDF&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;NetworkCore&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;UDP Socket、STUN/ICE、NAT 类型探测&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;DatagramChannel、线程模型、电池优化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;CircuitStore&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;本地缓存 Circuit 信息&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;SQLite / MMKV、加密存储&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;43-networkcore-线程模型&#34;&gt;4.3 NetworkCore 线程模型&lt;/h3&gt;
&lt;p&gt;UDP 网络层要避免在主线程做 IO，也避免一个线程处理所有事导致卡顿。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 网络线程
        RT[ReceiveThread&amp;lt;br/&amp;gt;阻塞 recvFrom]
        RT --&amp;gt;|Raw Packet| DP[DecoderPipeline&amp;lt;br/&amp;gt;STUN / 洋葱 / 业务]
        DP --&amp;gt;|业务消息| MQ[MessageQueue]
        DP --&amp;gt;|信令响应| SR[SignalingResponseMap]
    end

    subgraph 发送线程
        SQ[SendQueue&amp;lt;br/&amp;gt;优先级队列]
        ST[SendThread&amp;lt;br/&amp;gt;定时 flush]
        SQ --&amp;gt; ST --&amp;gt;|sendTo| Socket
    end

    subgraph 业务线程
        BH[BusinessHandler]
        MQ --&amp;gt; BH
    end

    Socket --&amp;gt; RT&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;44-android-端-nat-类型探测&#34;&gt;4.4 Android 端 NAT 类型探测&lt;/h3&gt;
&lt;p&gt;客户端启动时，先用 STUN 服务器探测自己的 NAT 类型：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;1. 向 STUN Server A 发 Binding Request，得到映射地址 M1。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2. 向 STUN Server B 发 Binding Request，得到映射地址 M2。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;3. 比较 M1 和 M2：
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   - 相同 → 可能全锥型 / 限制型
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   - 不同 → 对称型 NAT（打洞困难）
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;4. 再测试其他 IP/Port 是否能把包送进本机，判断是限制型还是端口限制型。&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[开始探测] --&amp;gt; B[STUN A 请求]
    B --&amp;gt; C[得到 M1]
    C --&amp;gt; D[STUN B 请求]
    D --&amp;gt; E[得到 M2]
    E --&amp;gt; F{M1 == M2?}
    F --&amp;gt;|是| G[全锥 / 限制 / 端口限制]
    F --&amp;gt;|否| H[对称型 NAT]
    G --&amp;gt; I[进一步测试过滤规则]
    H --&amp;gt; J[P2P 大概率失败&amp;lt;br/&amp;gt;优先准备 Relay]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;45-android-后台保活与省电&#34;&gt;4.5 Android 后台保活与省电&lt;/h3&gt;
&lt;p&gt;P2P 需要客户端能被找到，但 Android 对后台限制越来越严。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;方案&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;适用场景&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;注意事项&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Foreground Service&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;需要长期在线&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;必须显示通知，用户可能反感&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;WorkManager 周期性心跳&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;非实时场景&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;最小间隔 15 分钟，无法保证即时&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;FCM / 厂商推送&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;唤醒 App 建立连接&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;国内厂商通道需要适配&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;保活 socket + 心跳&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;与信令服务维持 NAT 映射&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;心跳间隔建议 15-30s， balancing 电量&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;新人避坑&lt;/strong&gt;：不要试图用「双进程守护」「1 像素 Activity」等旧保活手段，Android 高版本会被杀，且应用商店可能下架。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;5-通过-udp-信令网络实现-p2p-控制&#34;&gt;5. 通过 UDP 信令网络实现 P2P 控制&lt;/h2&gt;
&lt;h3 id=&#34;51-p2p-连接的三种结果&#34;&gt;5.1 P2P 连接的三种结果&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[Peer A 想连 Peer B] --&amp;gt; B[通过信令服务获取 B 的候选地址]
    B --&amp;gt; C{尝试直连}
    C --&amp;gt;|成功| D[UDP P2P 直连]
    C --&amp;gt;|失败| E{尝试 NAT 打洞}
    E --&amp;gt;|成功| D
    E --&amp;gt;|失败| F[通过 Relay 中继]
    D --&amp;gt; G[业务通信]
    F --&amp;gt; G&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-ice-like-候选地址优先级&#34;&gt;5.2 ICE-like 候选地址优先级&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[直连候选&amp;lt;br/&amp;gt;局域网 IP] --&amp;gt;|优先级最高| B[srflx 候选&amp;lt;br/&amp;gt;STUN 映射公网地址]
    B --&amp;gt;|次高| C[prflx 候选&amp;lt;br/&amp;gt;对端看到的地址]
    C --&amp;gt;|再次| D[relay 候选&amp;lt;br/&amp;gt;TURN/中继服务器]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;优先级规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;host 候选&lt;/strong&gt;：同一局域网直接连，延迟最低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;server reflexive（srflx）&lt;/strong&gt;：STUN 映射后的公网地址，适合普通 NAT。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;peer reflexive（prflx）&lt;/strong&gt;：对端实际看到的地址，可能在打洞过程中发现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;relay 候选&lt;/strong&gt;：最后兜底，走服务器转发。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;53-nat-打洞详细流程&#34;&gt;5.3 NAT 打洞详细流程&lt;/h3&gt;
&lt;p&gt;假设 A 和 B 都在普通家用路由器后，NAT 类型为「端口限制锥型」。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant A as Peer A
    participant SA as STUN/Signaling Server
    participant B as Peer B

    A-&amp;gt;&amp;gt;SA: 注册：我的 srflx 是 1.1.1.1:10000
    B-&amp;gt;&amp;gt;SA: 注册：我的 srflx 是 2.2.2.2:20000

    A-&amp;gt;&amp;gt;SA: 请求连接 B
    SA--&amp;gt;&amp;gt;A: 返回 B 的地址 2.2.2.2:20000
    SA--&amp;gt;&amp;gt;B: 转发 A 的地址 1.1.1.1:10000

    A-&amp;gt;&amp;gt;B: 发送 UDP 打洞包（可能到达或被过滤）
    B-&amp;gt;&amp;gt;A: 发送 UDP 打洞包
    Note over A,B: 双方都先发包，让各自 NAT 建立映射

    A-&amp;gt;&amp;gt;B: 收到 B 的包，回确认
    B-&amp;gt;&amp;gt;A: 收到 A 的包，回确认
    Note over A,B: 打洞成功，后续直接通信&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;新人记忆点&lt;/strong&gt;：NAT 打洞的本质是&lt;strong&gt;让双方路由器先记录一条「允许对方包进来」的映射&lt;/strong&gt;。所以必须双方同时主动发一个包到对方公网地址。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;54-基于洋葱网络的-p2p-控制通道&#34;&gt;5.4 基于洋葱网络的 P2P 控制通道&lt;/h3&gt;
&lt;p&gt;除了直接 P2P，还可以把「P2P 控制信令」本身也套在洋葱网络里，增强隐私：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[Alice] --&amp;gt;|洋葱隧道| X[出口节点] --&amp;gt; S[信令服务器]
    B[Bob] --&amp;gt;|洋葱隧道| X&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;控制消息包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求连接、交换候选地址。&lt;/li&gt;
&lt;li&gt;协商加密密钥（用于 P2P 直连后的端到端加密）。&lt;/li&gt;
&lt;li&gt;心跳、断开通知。&lt;/li&gt;
&lt;li&gt;中继切换通知。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样即使信令服务器被监控，攻击者也只能看到「某个洋葱出口在发信令」，不知道 Alice 和 Bob 的真实身份。&lt;/p&gt;
&lt;h3 id=&#34;55-p2p-直连后的加密&#34;&gt;5.5 P2P 直连后的加密&lt;/h3&gt;
&lt;p&gt;P2P 直连本身不保证安全，还需要端到端加密：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant A as Peer A
    participant S as Signaling Server（洋葱保护下）
    participant B as Peer B

    A-&amp;gt;&amp;gt;S: 发送 X25519 公钥 PA
    S-&amp;gt;&amp;gt;B: 转发 PA
    B-&amp;gt;&amp;gt;S: 发送 X25519 公钥 PB
    S-&amp;gt;&amp;gt;A: 转发 PB

    A-&amp;gt;&amp;gt;A: ECDH 计算共享密钥 K
    B-&amp;gt;&amp;gt;B: ECDH 计算共享密钥 K

    A-&amp;gt;&amp;gt;B: 用 K 加密业务数据，UDP P2P 直连发送
    B-&amp;gt;&amp;gt;A: 用 K 加密业务数据，UDP P2P 直连发送&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;新人避坑&lt;/strong&gt;：洋葱网络的逐跳加密 ≠ P2P 端到端加密。如果数据离开出口节点后是明文，出口节点仍然能看到内容。真正保密需要在 P2P 直连后再做一次端到端加密。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;6-协议报文设计示例&#34;&gt;6. 协议报文设计示例&lt;/h2&gt;
&lt;h3 id=&#34;61-洋葱路由报文&#34;&gt;6.1 洋葱路由报文&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; 0                   1                   2                   3
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| Version       | Command       | TTL           | Reserved        |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|                        Circuit ID                             |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|                                                               |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+              Encrypted Onion Payload (variable)               +
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|                                                               |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Command 定义示例：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;值&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;命令&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0x01&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;CREATE&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;建立 Circuit&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0x02&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;CREATED&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;建立成功响应&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0x03&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;RELAY&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;中继数据&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0x04&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;DESTROY&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;销毁 Circuit&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0x05&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;HEARTBEAT&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;保活心跳&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;62-信令报文&#34;&gt;6.2 信令报文&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; 0                   1                   2                   3
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| Magic (4B)    | Version (1B)  | Command (1B)  | Flags (2B)    |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|                         Sequence Number                       |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|                         Timestamp                             |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|                                                               |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+                    Encrypted Payload                          +
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|                                                               |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;信令 Command 示例：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;值&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;命令&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0x10&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;REGISTER&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;客户端注册&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0x11&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;OFFER&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;发起连接请求&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0x12&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;ANSWER&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;响应连接请求&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0x13&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;CANDIDATE&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;交换候选地址&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0x14&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;HEARTBEAT&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;保活&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;0x15&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;ERROR&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;错误通知&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;新人避坑&lt;/strong&gt;：UDP 报文要加 Magic 数和版本号，防止收到错乱的广播/扫描包时解析出错。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;7-关键算法与数据结构&#34;&gt;7. 关键算法与数据结构&lt;/h2&gt;
&lt;h3 id=&#34;71-circuit-选择算法&#34;&gt;7.1 Circuit 选择算法&lt;/h3&gt;
&lt;p&gt;客户端从目录服务拿到节点列表后，选择一条路径：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;choose_path&lt;/span&gt;(nodes, desired_hops&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 1. 过滤在线、版本兼容、flags 符合要求的节点&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    valid &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [n &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; n &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; nodes &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; n&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;is_online &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; n&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;version &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; PROTO_VERSION]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 2. 按负载和带宽评分，负载越低、带宽越高分越高&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scored &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [(n, score(n)) &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; n &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; valid]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scored&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sort(key&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;lambda&lt;/span&gt; x: x[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;], reverse&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 3. 随机选择前 K 个中的节点，避免总是选同一批&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    candidates &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; scored[:max(len(scored)&lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;, desired_hops &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 4. 确保 Entry、Middle、Exit 来自不同运营商 / 网段，增加路径多样性&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    path &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; role &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;entry&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;middle&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;exit&amp;#39;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        chosen &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pick_diverse(path, candidates, role)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(chosen)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; path&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;72-消息可靠性保证&#34;&gt;7.2 消息可靠性保证&lt;/h3&gt;
&lt;p&gt;UDP 不保证可靠，应用层需要自己实现：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[发送消息] --&amp;gt; B[分配 Seq]
    B --&amp;gt; C[加入重传队列]
    C --&amp;gt; D[UDP 发送]
    D --&amp;gt; E{收到 ACK?}
    E --&amp;gt;|是| F[从队列移除]
    E --&amp;gt;|否| G{超时?}
    G --&amp;gt;|否| E
    G --&amp;gt;|是| H[重传 N 次]
    H --&amp;gt; I{达到最大重试?}
    I --&amp;gt;|否| C
    I --&amp;gt;|是| J[通知上层失败]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;重传策略建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始超时：200ms（RTT 估计）。&lt;/li&gt;
&lt;li&gt;退避：指数退避，最多 5 次。&lt;/li&gt;
&lt;li&gt;ACK 去重：用滑动窗口或位图记录已确认 Seq。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;73-连接状态机&#34;&gt;7.3 连接状态机&lt;/h3&gt;
&lt;p&gt;P2P 连接可以建模为状态机：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;stateDiagram-v2
    [*] --&amp;gt; Idle: 用户发起连接
    Idle --&amp;gt; Gathering: 收集 ICE 候选
    Gathering --&amp;gt; Connecting: 开始打洞/直连
    Connecting --&amp;gt; Connected: 收到对端包
    Connecting --&amp;gt; Relaying: 打洞失败，切中继
    Relaying --&amp;gt; Connected: 中继链路建立
    Connected --&amp;gt; Disconnected: 超时未收到心跳
    Disconnected --&amp;gt; [*]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;8-新人避坑技巧大全&#34;&gt;8. 新人避坑技巧大全&lt;/h2&gt;
&lt;h3 id=&#34;81-udp-层面的坑&#34;&gt;8.1 UDP 层面的坑&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;坑&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;现象&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;规避方法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;UDP 报文丢失&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;部分消息收不到&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;应用层 ACK + 重传；关键信令走可靠通道&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;UDP 乱序&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;消息顺序错乱&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;按 Seq 排序或丢弃过期包；业务层幂等&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;UDP 分片&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;大报文丢失率陡增&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;控制报文大小 &amp;lt; 1200 字节；必要时分片重组&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;运营商 QoS&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;高峰丢包严重&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;使用常见端口（如 443/8080）或混淆成 QUIC/DNS&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;防火墙拦截&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;某些网络 UDP 不通&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;准备 TCP 回退或 Relay 兜底&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;82-洋葱网络层面的坑&#34;&gt;8.2 洋葱网络层面的坑&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;坑&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;现象&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;规避方法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Circuit 被重用&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;旧连接和新连接混用&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;每个 Circuit 带唯一 ID，过期销毁&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;时序攻击&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;入口和出口流量时间关联&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;引入填充（padding）和混合延迟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;恶意出口节点&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;出口监听或篡改明文&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;P2P 必须端到端加密，不依赖出口&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;节点列表被污染&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;连上恶意节点&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;目录服务签名 + 节点身份固定 + 信誉系统&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;密钥管理混乱&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;加密失败或性能差&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;一个 Circuit 一对密钥，用 HKDF 派生&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;83-android-层面的坑&#34;&gt;8.3 Android 层面的坑&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;坑&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;现象&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;规避方法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;主线程网络&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;ANR / 卡顿&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;所有 UDP IO 放子线程或用协程&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Doze 模式心跳中断&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;后台连接断开&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;使用 JobScheduler / WorkManager 补心跳&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;IPv4/IPv6 双栈&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;一端 v4 一端 v6 连不上&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;同时收集 v4/v6 候选地址&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;端口复用失败&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;BindException&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Socket 设置 &lt;code&gt;SO_REUSEADDR&lt;/code&gt; / &lt;code&gt;SO_REUSEPORT&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;电量消耗高&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;用户卸载&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;动态心跳间隔，网络空闲时延长&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;84-p2p--nat-层面的坑&#34;&gt;8.4 P2P / NAT 层面的坑&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;坑&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;现象&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;规避方法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;对称型 NAT 打洞失败&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;两边都收不到包&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;提前探测 NAT 类型，准备 Relay&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;** hairpin NAT 不支持**&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;同一局域网却走公网&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;优先尝试 host 候选&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;NAT 映射超时&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;长时间没通信后连不上&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;定期发送心跳保活&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;双方不同时打洞&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;一方先发可能被防火墙拦&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;设计让 A、B 同时主动发包&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;信令与数据通道混淆&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;安全边界不清&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;信令用短期密钥，数据用长期会话密钥&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;85-安全与隐私层面的坑&#34;&gt;8.5 安全与隐私层面的坑&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;坑&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;现象&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;规避方法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;只做了洋葱加密，没做端到端加密&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;出口节点可读明文&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;P2P 直连后再做一次 ECDH&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;时间戳不校验&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;重放攻击&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;每个报文带时间戳 + 滑动窗口&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;随机数可预测&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Circuit ID 被猜中&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;用 SecureRandom / crypto.random&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;元数据泄露&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;包大小暴露内容类型&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;固定包长或随机填充&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;证书/公钥不校验&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;中间人攻击&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;目录服务签名 + 证书锁定&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;9-一个最小可运行流程串起来&#34;&gt;9. 一个最小可运行流程串起来&lt;/h2&gt;
&lt;p&gt;为了让新人有整体感，下面把一次「Alice 通过 UDP 洋葱网络控制 P2P 连接 Bob」的完整流程串起来。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant A as Android Alice
    participant D as Directory Server
    participant S as Signaling Server
    participant R as Relay Server
    participant B as Android Bob

    %% 阶段 1：准备节点
    A-&amp;gt;&amp;gt;D: HTTPS 获取节点列表
    D--&amp;gt;&amp;gt;A: 返回 Entry/Middle/Exit + 签名
    A-&amp;gt;&amp;gt;A: 校验签名，选择路径
    A-&amp;gt;&amp;gt;Entry: UDP CREATE Circuit
    Entry--&amp;gt;&amp;gt;A: CREATED
    A-&amp;gt;&amp;gt;Middle: 经 Entry 转发 CREATE
    Middle--&amp;gt;&amp;gt;A: CREATED
    A-&amp;gt;&amp;gt;Exit: 经 Entry→Middle 转发 CREATE
    Exit--&amp;gt;&amp;gt;A: CREATED

    %% 阶段 2：注册信令
    A-&amp;gt;&amp;gt;S: 经洋葱隧道 REGISTER Alice
    B-&amp;gt;&amp;gt;S: 经洋葱隧道 REGISTER Bob

    %% 阶段 3：发起 P2P
    A-&amp;gt;&amp;gt;S: OFFER（请求连接 Bob）
    S-&amp;gt;&amp;gt;B: 转发 OFFER + Alice 候选地址
    B-&amp;gt;&amp;gt;S: ANSWER + Bob 候选地址
    S-&amp;gt;&amp;gt;A: 转发 ANSWER

    %% 阶段 4：打洞
    A-&amp;gt;&amp;gt;B: UDP 打洞包
    B-&amp;gt;&amp;gt;A: UDP 打洞包
    A--&amp;gt;&amp;gt;B: 直连成功

    %% 阶段 5：业务通信
    A-&amp;gt;&amp;gt;B: 端到端加密业务数据
    B-&amp;gt;&amp;gt;A: 端到端加密业务数据

    %% 阶段 6：失败兜底
    alt 打洞失败
        A-&amp;gt;&amp;gt;S: 请求 Relay
        S--&amp;gt;&amp;gt;A: 分配 Relay 地址
        S--&amp;gt;&amp;gt;B: 通知走 Relay
        A-&amp;gt;&amp;gt;R: 经洋葱→Relay→Bob
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;10-总结一张图记住全文&#34;&gt;10. 总结：一张图记住全文&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    A[UDP 洋葱网络 + P2P] --&amp;gt; B[目录服务&amp;lt;br/&amp;gt;给节点]
    A --&amp;gt; C[信令服务&amp;lt;br/&amp;gt;撮合 Peer]
    A --&amp;gt; D[中继服务&amp;lt;br/&amp;gt;兜底转发]
    A --&amp;gt; E[Android 客户端&amp;lt;br/&amp;gt;洋葱 + NAT + P2P]
    E --&amp;gt; F[OnionCore&amp;lt;br/&amp;gt;逐跳加密]
    E --&amp;gt; G[NetworkCore&amp;lt;br/&amp;gt;UDP / STUN / ICE]
    E --&amp;gt; H[P2PController&amp;lt;br/&amp;gt;直连 / 打洞 / 中继]
    F --&amp;gt; I[隐私保护]
    G --&amp;gt; J[NAT 穿透]
    H --&amp;gt; K[去中心化通信]
    A --&amp;gt; L[避坑：可靠传输 / 端到端加密 / 后台保活]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;核心要点回顾：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;洋葱网络用 UDP 是为了轻量、NAT 友好、适合信令&lt;/strong&gt;；但可靠性和顺序要自己补。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务器分三类&lt;/strong&gt;：目录服务（给节点）、信令服务（撮合 P2P）、中继服务（兜底转发）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Android 客户端分层&lt;/strong&gt;：业务层、连接管理层、核心引擎层、网络层，不要混在一处。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P2P 控制靠 UDP 信令交换地址&lt;/strong&gt;，再用 ICE 候选优先级尝试直连、打洞、中继。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐私不等于安全&lt;/strong&gt;：洋葱网络保护路径隐私，P2P 业务数据还要端到端加密。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新人最容易踩的坑&lt;/strong&gt;：UDP 当 TCP 用、忽略 NAT 类型、不做端到端加密、Android 主线程 IO。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考与延伸阅读&#34;&gt;参考与延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Tor Project: &lt;a href=&#34;https://www.torproject.org/&#34;&gt;https://www.torproject.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ICE / STUN / TURN RFCs: RFC 5245, RFC 5389, RFC 5766&lt;/li&gt;
&lt;li&gt;WebRTC NAT Traversal 实践&lt;/li&gt;
&lt;li&gt;Android &lt;code&gt;DatagramSocket&lt;/code&gt;、&lt;code&gt;WorkManager&lt;/code&gt;、&lt;code&gt;Foreground Service&lt;/code&gt; 官方文档&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你正在动手实现，建议先做一个「最小闭环」：一个目录服务 + 一个中继节点 + 两个 Android 客户端，先跑通「Alice 经洋葱网络发一条消息给 Bob」，再逐步加入 P2P 直连和 NAT 打洞。先跑通，再优化。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>
