<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>协程 - 标签 - ISLAND</title><link>https://youngxhui.top/tags/%E5%8D%8F%E7%A8%8B/</link><description>协程 - 标签 - ISLAND</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><managingEditor>youngxhui@gmail.com (youngxhui)</managingEditor><webMaster>youngxhui@gmail.com (youngxhui)</webMaster><copyright>本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。</copyright><lastBuildDate>Sun, 06 Sep 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://youngxhui.top/tags/%E5%8D%8F%E7%A8%8B/" rel="self" type="application/rss+xml"/><item><title>轻糖的 KMP 实战：一次冷启动 3 秒必现崩溃，与 viewModelScope 异常逃逸的代价</title><link>https://youngxhui.top/2026/09/sugarlite-kmp-exception/</link><pubDate>Sun, 06 Sep 2026 00:00:00 +0800</pubDate><author><name>youngxhui</name></author><guid>https://youngxhui.top/2026/09/sugarlite-kmp-exception/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://island-hexo.oss-cn-beijing.aliyuncs.com/20260519191237432.png" referrerpolicy="no-referrer">
            </div><p>第三篇文章收尾时，我们说迁移完成后的日常是&quot;新功能默认 KMP-first&quot;。这句话没错，但它隐藏了一个前提：<strong>共享层代码在两端的运行环境并不对等</strong>——同样的代码，在 Android 上是一次可捕获的异常，在 iOS 上可能是整个进程的直接死亡。</p>
<p>这篇文章记录一次真实事故：轻糖 iOS 版冷启动 3 秒必现 SIGABRT，堆栈指向一个陌生的符号 <code>propagateExceptionFinalResort</code>。根因不在 iOS 侧的任何一行 Swift 代码，而在 shared 层 ViewModel 里几个看似无害的 Flow 订阅——<strong>它们的异常从未被捕获，在 Kotlin/Native 上直接杀死了进程</strong>。</p>
<h2 id="-事故现场启动-3-秒进程消失" class="headerLink">
    <a href="#-%e4%ba%8b%e6%95%85%e7%8e%b0%e5%9c%ba%e5%90%af%e5%8a%a8-3-%e7%a7%92%e8%bf%9b%e7%a8%8b%e6%b6%88%e5%a4%b1" class="header-mark"></a>💥 事故现场：启动 3 秒，进程消失</h2><p>现象非常规律：iOS 冷启动后，首页数据刚开始加载，大约 3 秒，App 闪退。必现，不分机型，不分系统版本。</p>
<p>崩溃报告里能看到的信息：</p>
<ul>
<li>信号是 <strong>SIGABRT</strong>，不是常见的 SIGSEGV / SIGTRAP；</li>
<li>栈底出现 <code>propagateExceptionFinalResort</code> 这个符号——它不是我们的代码，也不是 Swift 运行时的，而是 <strong>Kotlin/Native 协程运行时的&quot;最终手段&quot;</strong>；</li>
<li>Android 侧同样的版本、同样的数据，完全不崩。</li>
</ul>
<p>&ldquo;Android 不崩 iOS 必崩&quot;这个组合，基本排除了业务逻辑错误（比如空指针、数组越界），指向了平台运行时的行为差异。而 <code>propagateExceptionFinalResort</code> 这个名字已经把事情说得很明白了：<strong>有异常一路传播到了协程的最顶端，没有人接住它，运行时只能把进程杀掉</strong>。</p>
<h2 id="-根因没有-coroutineexceptionhandler-的-viewmodelscope" class="headerLink">
    <a href="#-%e6%a0%b9%e5%9b%a0%e6%b2%a1%e6%9c%89-coroutineexceptionhandler-%e7%9a%84-viewmodelscope" class="header-mark"></a>🔍 根因：没有 CoroutineExceptionHandler 的 viewModelScope</h2><p>回顾第三篇文章里的 KMP ViewModel 范式：</p>
<div class="code-block highlight is-open show-line-numbers  tw:group tw:my-2">
  <div class="
    
    tw:flex 
    tw:flex-row
    tw:flex-1 
    tw:justify-between 
    tw:w-full tw:bg-bgColor-secondary
    ">      
    <button 
      class="
        code-block-button
        tw:mx-2 
        tw:flex
        tw:flex-row
        tw:flex-1"
      aria-hidden="true">
          <span class="tw:group-[.is-open]:rotate-90 tw:transition-[transform] tw:duration-500 tw:ease-in-out tw:print:hidden! tw:w-min tw:h-min tw:my-1 tw:mx-1"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"/></svg></span>
          <span class="tw:select-none tw:my-1! tw:block">kotlin</span>
      </button>

   <div class="tw:flex">
      <button 
        class="
          line-number-button
          tw:mx-2 
          tw:hidden 
          tw:group-[.is-open]:block 
          tw:group-[.show-line-numbers]:text-fgColor-link 
          tw:print:hidden!" 
        title="Toggle line numbers"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22.83a157.41 157.41 0 0 0-11 12.31l-5.61 7c-4 5.07-5.25 10.13-2.8 14.88l1.05 1.93c3 5.76 6.29 7.88 12.25 7.88h4.73c10.33 0 15.94 2.44 15.94 9.09 0 4.72-4.2 8.22-14.36 8.22a41.54 41.54 0 0 1-15.47-3.12c-6.49-3.88-11.74-3.5-15.6 3.12l-5.59 9.31c-3.72 6.13-3.19 11.72 2.63 15.94 7.71 4.69 20.38 9.44 37 9.44 34.16 0 48.5-22.75 48.5-44.12-.03-14.38-9.12-29.76-28.73-34.88zM496 224H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM16 160h64a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H64V40a8 8 0 0 0-8-8H32a8 8 0 0 0-7.14 4.42l-8 16A8 8 0 0 0 24 64h8v64H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8zm-3.91 160H80a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H41.32c3.29-10.29 48.34-18.68 48.34-56.44 0-29.06-25-39.56-44.47-39.56-21.36 0-33.8 10-40.46 18.75-4.37 5.59-3 10.84 2.8 15.37l8.58 6.88c5.61 4.56 11 2.47 16.12-2.44a13.44 13.44 0 0 1 9.46-3.84c3.33 0 9.28 1.56 9.28 8.75C51 248.19 0 257.31 0 304.59v4C0 316 5.08 320 12.09 320z"/></svg></button>

      <button 
        class="
          wrap-code-button
          tw:select-none 
          tw:mx-2 
          tw:hidden 
          tw:group-[.is-open]:block 
          tw:group-[.is-wrap]:text-fgColor-link 
          tw:print:hidden!" 
        title="Toggle code wrap"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"/></svg></button>
      
      <button 
        class="
          copy-code-button
          tw:select-none
          tw:mx-2 
          tw:hidden
          tw:group-[.is-open]:block
          tw:hover:text-fgColor-link 
          tw:print:hidden!"
        title="Copy code">
          <span class="copy-icon tw:block"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM266 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 0 1-6 6zm128-96H182a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h106v88c0 13.255 10.745 24 24 24h88v202a6 6 0 0 1-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 0 1 1.757 4.243V112z"/></svg></span>
          <span class="check-icon tw:hidden"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/></svg></span>
      </button>
        
      <button 
        class="
          tw:select-none 
          tw:mx-2 
          tw:block 
          tw:group-[.is-open]:hidden 
          tw:print:hidden!" 
        disabled
        aria-hidden="true"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M328 256c0 39.8-32.2 72-72 72s-72-32.2-72-72 32.2-72 72-72 72 32.2 72 72zm104-72c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72zm-352 0c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72z"/></svg></button>
    </div>
  </div>
  <pre style="counter-reset: codeblock;" class="tw:block tw:m-0 tw:p-0"><code 
    id="codeblock-id-9" 
    class="
      chroma 
      tw:block! 
      tw:p-0
      tw:m-0
      tw:transition-[max-height] 
      tw:duration-500 
      tw:ease-in-out 
      tw:group-[.is-closed]:max-h-0! 
      tw:group-[.is-wrap]:text-wrap
      tw:overflow-y-hidden
      tw:overflow-x-auto
      tw:scrollbar-thin
      "><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">HomeViewModel</span> <span class="p">:</span> <span class="n">ViewModel</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="k">val</span> <span class="py">_uiState</span> <span class="p">=</span> <span class="n">MutableStateFlow</span><span class="p">(</span><span class="n">viewModelScope</span><span class="p">,</span> <span class="n">HomeUiState</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">init</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">viewModelScope</span><span class="p">.</span><span class="n">launch</span> <span class="p">{</span> <span class="n">observeDailyData</span><span class="p">()</span> <span class="p">}</span>   <span class="c1">// 订阅每日数据
</span></span></span><span class="line"><span class="cl">        <span class="n">viewModelScope</span><span class="p">.</span><span class="n">launch</span> <span class="p">{</span> <span class="n">observeWeekData</span><span class="p">()</span> <span class="p">}</span>    <span class="c1">// 订阅周数据
</span></span></span><span class="line"><span class="cl">        <span class="n">viewModelScope</span><span class="p">.</span><span class="n">launch</span> <span class="p">{</span> <span class="n">observeBloodSugarUnit</span><span class="p">()</span> <span class="p">}</span> <span class="c1">// 订阅血糖单位
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</div>
<p><code>observeDailyData()</code> 内部是一条典型的响应式链路：<code>combine</code> 5 个数据库 Flow，<code>flatMapLatest</code> 切换日期，最后 <code>collect</code> 进 UiState。问题就藏在这条链路里——<strong>Flow 的收集过程中，上游任何一个环节抛异常（数据库查询失败、网络抖动、反序列化出错），异常都会顺着协程向上传播</strong>。</p>
<p>在 Android 上，这样的异常最终到达 <code>Thread.uncaughtExceptionHandler</code>，表现为一次普通的 Java Crash，崩溃 SDK 能完整捕获堆栈，运气好还有兜底逻辑。但这里有两个致命差异：</p>
<ol>
<li><strong>KMP-ObservableViewModel 的 <code>viewModelScope</code> 没有安装 <code>CoroutineExceptionHandler</code></strong>。它就是一个裸的 <code>SupervisorJob + Dispatchers.Main</code>，异常传到根部没有任何处理者。</li>
<li><strong>Kotlin/Native 对&quot;协程根部未处理异常&quot;的默认处置是 <code>propagateExceptionFinalResort</code></strong>——名字直译就是&quot;最终手段传播异常&rdquo;：在主线程上直接 <code>abort()</code>，进程收到 SIGABRT，立即死亡。没有 Java 层的异常栈，没有崩溃 SDK 的从容上报，就是一刀毙命。</li>
</ol>
<p>于是整条事故链条清晰了：</p>
<div class="code-block highlight is-open show-line-numbers  tw:group tw:my-2">
  <div class="
    
    tw:flex 
    tw:flex-row
    tw:flex-1 
    tw:justify-between 
    tw:w-full tw:bg-bgColor-secondary
    ">      
    <button 
      class="
        code-block-button
        tw:mx-2 
        tw:flex
        tw:flex-row
        tw:flex-1"
      aria-hidden="true">
          <span class="tw:group-[.is-open]:rotate-90 tw:transition-[transform] tw:duration-500 tw:ease-in-out tw:print:hidden! tw:w-min tw:h-min tw:my-1 tw:mx-1"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"/></svg></span>
          <span class="tw:select-none tw:my-1! tw:block">text</span>
      </button>

   <div class="tw:flex">
      <button 
        class="
          line-number-button
          tw:mx-2 
          tw:hidden 
          tw:group-[.is-open]:block 
          tw:group-[.show-line-numbers]:text-fgColor-link 
          tw:print:hidden!" 
        title="Toggle line numbers"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22.83a157.41 157.41 0 0 0-11 12.31l-5.61 7c-4 5.07-5.25 10.13-2.8 14.88l1.05 1.93c3 5.76 6.29 7.88 12.25 7.88h4.73c10.33 0 15.94 2.44 15.94 9.09 0 4.72-4.2 8.22-14.36 8.22a41.54 41.54 0 0 1-15.47-3.12c-6.49-3.88-11.74-3.5-15.6 3.12l-5.59 9.31c-3.72 6.13-3.19 11.72 2.63 15.94 7.71 4.69 20.38 9.44 37 9.44 34.16 0 48.5-22.75 48.5-44.12-.03-14.38-9.12-29.76-28.73-34.88zM496 224H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM16 160h64a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H64V40a8 8 0 0 0-8-8H32a8 8 0 0 0-7.14 4.42l-8 16A8 8 0 0 0 24 64h8v64H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8zm-3.91 160H80a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H41.32c3.29-10.29 48.34-18.68 48.34-56.44 0-29.06-25-39.56-44.47-39.56-21.36 0-33.8 10-40.46 18.75-4.37 5.59-3 10.84 2.8 15.37l8.58 6.88c5.61 4.56 11 2.47 16.12-2.44a13.44 13.44 0 0 1 9.46-3.84c3.33 0 9.28 1.56 9.28 8.75C51 248.19 0 257.31 0 304.59v4C0 316 5.08 320 12.09 320z"/></svg></button>

      <button 
        class="
          wrap-code-button
          tw:select-none 
          tw:mx-2 
          tw:hidden 
          tw:group-[.is-open]:block 
          tw:group-[.is-wrap]:text-fgColor-link 
          tw:print:hidden!" 
        title="Toggle code wrap"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"/></svg></button>
      
      <button 
        class="
          copy-code-button
          tw:select-none
          tw:mx-2 
          tw:hidden
          tw:group-[.is-open]:block
          tw:hover:text-fgColor-link 
          tw:print:hidden!"
        title="Copy code">
          <span class="copy-icon tw:block"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM266 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 0 1-6 6zm128-96H182a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h106v88c0 13.255 10.745 24 24 24h88v202a6 6 0 0 1-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 0 1 1.757 4.243V112z"/></svg></span>
          <span class="check-icon tw:hidden"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/></svg></span>
      </button>
        
      <button 
        class="
          tw:select-none 
          tw:mx-2 
          tw:block 
          tw:group-[.is-open]:hidden 
          tw:print:hidden!" 
        disabled
        aria-hidden="true"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M328 256c0 39.8-32.2 72-72 72s-72-32.2-72-72 32.2-72 72-72 72 32.2 72 72zm104-72c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72zm-352 0c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72z"/></svg></button>
    </div>
  </div>
  <pre style="counter-reset: codeblock;" class="tw:block tw:m-0 tw:p-0"><code 
    id="codeblock-id-10" 
    class="
      chroma 
      tw:block! 
      tw:p-0
      tw:m-0
      tw:transition-[max-height] 
      tw:duration-500 
      tw:ease-in-out 
      tw:group-[.is-closed]:max-h-0! 
      tw:group-[.is-wrap]:text-wrap
      tw:overflow-y-hidden
      tw:overflow-x-auto
      tw:scrollbar-thin
      "><span class="line"><span class="cl">Room / 网络层抛出异常
</span></span><span class="line"><span class="cl">  → Flow collect 中无人捕获
</span></span><span class="line"><span class="cl">  → 沿 viewModelScope.launch 的 Job 树上传到根部
</span></span><span class="line"><span class="cl">  → 根部没有 CoroutineExceptionHandler
</span></span><span class="line"><span class="cl">  → Kotlin/Native: propagateExceptionFinalResort
</span></span><span class="line"><span class="cl">  → abort() → SIGABRT → 进程消失</span></span></code></pre>
</div>
<p>为什么恰好是启动 3 秒？因为 <code>HomeViewModel</code> 在 <code>init</code> 里同时启动了三个 Flow 订阅，启动后数据库查询、缓存读取集中发生，异常在数据加载窗口内被引爆——3 秒正好是第一轮 Flow 发射到达的时间。</p>
<p>这也解释了为什么事故在迁移完成后才出现：迁移前这些订阅逻辑写在 iOS 本地的 StateHolder 里，<code>for await</code> 循环外面有 Swift 的 <code>do/catch</code> 兜底；迁移后逻辑下沉到 shared，<strong>兜底没有跟着下沉</strong>。</p>
<h2 id="-修复给每个订阅一个兜底的地" class="headerLink">
    <a href="#-%e4%bf%ae%e5%a4%8d%e7%bb%99%e6%af%8f%e4%b8%aa%e8%ae%a2%e9%98%85%e4%b8%80%e4%b8%aa%e5%85%9c%e5%ba%95%e7%9a%84%e5%9c%b0" class="header-mark"></a>🔧 修复：给每个订阅一个兜底的&quot;地&quot;</h2><p>修复方案很朴素：在每个 <code>viewModelScope.launch</code> 内部，用 <code>try/catch</code> 把 Flow 收集和业务调用整个包起来。以 <code>observeDailyData()</code> 为例：</p>
<div class="code-block highlight is-open show-line-numbers  tw:group tw:my-2">
  <div class="
    
    tw:flex 
    tw:flex-row
    tw:flex-1 
    tw:justify-between 
    tw:w-full tw:bg-bgColor-secondary
    ">      
    <button 
      class="
        code-block-button
        tw:mx-2 
        tw:flex
        tw:flex-row
        tw:flex-1"
      aria-hidden="true">
          <span class="tw:group-[.is-open]:rotate-90 tw:transition-[transform] tw:duration-500 tw:ease-in-out tw:print:hidden! tw:w-min tw:h-min tw:my-1 tw:mx-1"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"/></svg></span>
          <span class="tw:select-none tw:my-1! tw:block">kotlin</span>
      </button>

   <div class="tw:flex">
      <button 
        class="
          line-number-button
          tw:mx-2 
          tw:hidden 
          tw:group-[.is-open]:block 
          tw:group-[.show-line-numbers]:text-fgColor-link 
          tw:print:hidden!" 
        title="Toggle line numbers"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22.83a157.41 157.41 0 0 0-11 12.31l-5.61 7c-4 5.07-5.25 10.13-2.8 14.88l1.05 1.93c3 5.76 6.29 7.88 12.25 7.88h4.73c10.33 0 15.94 2.44 15.94 9.09 0 4.72-4.2 8.22-14.36 8.22a41.54 41.54 0 0 1-15.47-3.12c-6.49-3.88-11.74-3.5-15.6 3.12l-5.59 9.31c-3.72 6.13-3.19 11.72 2.63 15.94 7.71 4.69 20.38 9.44 37 9.44 34.16 0 48.5-22.75 48.5-44.12-.03-14.38-9.12-29.76-28.73-34.88zM496 224H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM16 160h64a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H64V40a8 8 0 0 0-8-8H32a8 8 0 0 0-7.14 4.42l-8 16A8 8 0 0 0 24 64h8v64H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8zm-3.91 160H80a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H41.32c3.29-10.29 48.34-18.68 48.34-56.44 0-29.06-25-39.56-44.47-39.56-21.36 0-33.8 10-40.46 18.75-4.37 5.59-3 10.84 2.8 15.37l8.58 6.88c5.61 4.56 11 2.47 16.12-2.44a13.44 13.44 0 0 1 9.46-3.84c3.33 0 9.28 1.56 9.28 8.75C51 248.19 0 257.31 0 304.59v4C0 316 5.08 320 12.09 320z"/></svg></button>

      <button 
        class="
          wrap-code-button
          tw:select-none 
          tw:mx-2 
          tw:hidden 
          tw:group-[.is-open]:block 
          tw:group-[.is-wrap]:text-fgColor-link 
          tw:print:hidden!" 
        title="Toggle code wrap"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"/></svg></button>
      
      <button 
        class="
          copy-code-button
          tw:select-none
          tw:mx-2 
          tw:hidden
          tw:group-[.is-open]:block
          tw:hover:text-fgColor-link 
          tw:print:hidden!"
        title="Copy code">
          <span class="copy-icon tw:block"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM266 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 0 1-6 6zm128-96H182a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h106v88c0 13.255 10.745 24 24 24h88v202a6 6 0 0 1-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 0 1 1.757 4.243V112z"/></svg></span>
          <span class="check-icon tw:hidden"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/></svg></span>
      </button>
        
      <button 
        class="
          tw:select-none 
          tw:mx-2 
          tw:block 
          tw:group-[.is-open]:hidden 
          tw:print:hidden!" 
        disabled
        aria-hidden="true"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M328 256c0 39.8-32.2 72-72 72s-72-32.2-72-72 32.2-72 72-72 72 32.2 72 72zm104-72c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72zm-352 0c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72z"/></svg></button>
    </div>
  </div>
  <pre style="counter-reset: codeblock;" class="tw:block tw:m-0 tw:p-0"><code 
    id="codeblock-id-11" 
    class="
      chroma 
      tw:block! 
      tw:p-0
      tw:m-0
      tw:transition-[max-height] 
      tw:duration-500 
      tw:ease-in-out 
      tw:group-[.is-closed]:max-h-0! 
      tw:group-[.is-wrap]:text-wrap
      tw:overflow-y-hidden
      tw:overflow-x-auto
      tw:scrollbar-thin
      "><span class="line"><span class="cl"><span class="k">private</span> <span class="k">suspend</span> <span class="k">fun</span> <span class="nf">observeDailyData</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// viewModelScope 无 CoroutineExceptionHandler，Flow 收集中的任何异常
</span></span></span><span class="line"><span class="cl">    <span class="c1">// 都会经 propagateExceptionFinalResort 在 iOS 上直接 abort，必须在此兜底
</span></span></span><span class="line"><span class="cl">    <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">combine</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">_uiState</span><span class="p">.</span><span class="n">map</span> <span class="p">{</span> <span class="k">it</span><span class="p">.</span><span class="n">selectedDate</span> <span class="p">}.</span><span class="n">distinctUntilChanged</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">            <span class="n">currentUserIdFlow</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span> <span class="p">{</span> <span class="n">date</span><span class="p">,</span> <span class="n">uid</span> <span class="o">-&gt;</span> <span class="n">date</span> <span class="n">to</span> <span class="n">uid</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="n">flatMapLatest</span> <span class="p">{</span> <span class="p">(</span><span class="n">date</span><span class="p">,</span> <span class="n">uid</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="n">combine</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="n">getDailyBloodSugarRecordsFlow</span><span class="p">(</span><span class="n">dateTs</span><span class="p">,</span> <span class="n">uid</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                    <span class="n">getDailyExerciseRecordsFlow</span><span class="p">(</span><span class="n">dateTs</span><span class="p">,</span> <span class="n">uid</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                    <span class="n">getDailyFoodRecordsWithItemsFlow</span><span class="p">(</span><span class="n">dateTs</span><span class="p">,</span> <span class="n">uid</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                    <span class="n">getDailyMedicationRecordsFlow</span><span class="p">(</span><span class="n">dateTs</span><span class="p">,</span> <span class="n">uid</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                    <span class="n">getDailyInsulinRecordDtosFlow</span><span class="p">(</span><span class="n">dateTs</span><span class="p">,</span> <span class="n">uid</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="p">)</span> <span class="p">{</span> <span class="n">bsDtos</span><span class="p">,</span> <span class="n">exDtos</span><span class="p">,</span> <span class="n">foodDtos</span><span class="p">,</span> <span class="n">medDtos</span><span class="p">,</span> <span class="n">insulinDtos</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">                    <span class="n">enrichExerciseReferences</span><span class="p">(</span><span class="n">exDtos</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="n">assembleStateChunk</span><span class="p">(</span><span class="n">bsDtos</span><span class="p">,</span> <span class="n">exDtos</span><span class="p">,</span> <span class="n">foodDtos</span><span class="p">,</span> <span class="n">medDtos</span><span class="p">,</span> <span class="n">insulinDtos</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="n">collect</span> <span class="p">{</span> <span class="n">chunk</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="n">_uiState</span><span class="p">.</span><span class="n">update</span> <span class="p">{</span> <span class="n">state</span> <span class="o">-&gt;</span> <span class="n">chunk</span><span class="p">(</span><span class="n">state</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="n">CancellationException</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">throw</span> <span class="n">e</span>  <span class="c1">// 取消语义必须照常传播
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="n">Exception</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nc">AppLogger</span><span class="p">.</span><span class="n">e</span><span class="p">(</span><span class="s2">&#34;HomeViewModel&#34;</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span> <span class="p">{</span> <span class="s2">&#34;订阅每日数据失败&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="n">_uiState</span><span class="p">.</span><span class="n">update</span> <span class="p">{</span> <span class="k">it</span><span class="p">.</span><span class="n">copy</span><span class="p">(</span><span class="n">isLoading</span> <span class="p">=</span> <span class="k">false</span><span class="p">,</span> <span class="n">errorMessage</span> <span class="p">=</span> <span class="s2">&#34;加载失败: </span><span class="si">${e.message}</span><span class="s2">&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</div>
<p>这套写法有三个不可省略的要点：</p>
<ol>
<li><strong><code>CancellationException</code> 必须 rethrow</strong>。协程取消本身就靠抛 <code>CancellationException</code> 实现，如果被 <code>catch (e: Exception)</code> 吞掉，视图销毁后订阅还会继续跑，就是第三篇里我们刚刚消灭掉的泄漏问题。所以它必须单独 catch 并原样抛出，保证取消语义完整。</li>
<li><strong>记日志，写 UiState</strong>。异常不再逃逸，但也不能静默吞掉——<code>AppLogger.e</code> 留排查线索，同时把 <code>isLoading = false</code> 和 <code>errorMessage</code> 写进 UiState，用户看到的是&quot;加载失败&quot;而不是整个 App 消失。<strong>崩溃和错误提示之间，隔着的就是这一个 catch</strong>。</li>
<li><strong>兜底位置在 collect 外侧，不是上游每个 Flow</strong>。<code>combine</code> / <code>flatMapLatest</code> 链中任何一环抛出的异常都会传播到 <code>collect</code> 所在的协程，一个外层 <code>try/catch</code> 就能全部兜住，不需要给每个上游 Flow 单独包。</li>
</ol>
<p>这次修复一共覆盖了 5 个 ViewModel：</p>
<table>
	<thead>
			<tr>
					<th>文件</th>
					<th>兜底位置</th>
					<th>改动量</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>HomeViewModel.kt</code></td>
					<td>每日数据、周数据、血糖单位三个启动期订阅</td>
					<td>+67 -44</td>
			</tr>
			<tr>
					<td><code>ProfileViewModel.kt</code></td>
					<td>用户访问状态订阅、用户资料订阅</td>
					<td>+40 -23</td>
			</tr>
			<tr>
					<td><code>AIFoodScanViewModel.kt</code></td>
					<td>AI 识谱的挂起调用</td>
					<td>+32 -22</td>
			</tr>
			<tr>
					<td><code>AddFoodRecordViewModel.kt</code></td>
					<td>AI 识餐提交</td>
					<td>+28 -19</td>
			</tr>
			<tr>
					<td><code>AddMedicationRecordViewModel.kt</code></td>
					<td>药品名称订阅</td>
					<td>+17 -8</td>
			</tr>
	</tbody>
</table>
<p>模式完全一致：<code>try</code> 包住整个订阅或调用，<code>CancellationException</code> rethrow，其余异常记日志 + 写 <code>errorMessage</code>。没有例外，没有变体。</p>
<h2 id="-为什么不装一个全局-coroutineexceptionhandler" class="headerLink">
    <a href="#-%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b8%8d%e8%a3%85%e4%b8%80%e4%b8%aa%e5%85%a8%e5%b1%80-coroutineexceptionhandler" class="header-mark"></a>🤔 为什么不装一个全局 CoroutineExceptionHandler？</h2><p>第一反应可能是：既然根部缺处理器，那给 <code>viewModelScope</code> 装一个全局 <code>CoroutineExceptionHandler</code> 不就一劳永逸了？</p>
<div class="code-block highlight is-open show-line-numbers  tw:group tw:my-2">
  <div class="
    
    tw:flex 
    tw:flex-row
    tw:flex-1 
    tw:justify-between 
    tw:w-full tw:bg-bgColor-secondary
    ">      
    <button 
      class="
        code-block-button
        tw:mx-2 
        tw:flex
        tw:flex-row
        tw:flex-1"
      aria-hidden="true">
          <span class="tw:group-[.is-open]:rotate-90 tw:transition-[transform] tw:duration-500 tw:ease-in-out tw:print:hidden! tw:w-min tw:h-min tw:my-1 tw:mx-1"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"/></svg></span>
          <span class="tw:select-none tw:my-1! tw:block">kotlin</span>
      </button>

   <div class="tw:flex">
      <button 
        class="
          line-number-button
          tw:mx-2 
          tw:hidden 
          tw:group-[.is-open]:block 
          tw:group-[.show-line-numbers]:text-fgColor-link 
          tw:print:hidden!" 
        title="Toggle line numbers"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22.83a157.41 157.41 0 0 0-11 12.31l-5.61 7c-4 5.07-5.25 10.13-2.8 14.88l1.05 1.93c3 5.76 6.29 7.88 12.25 7.88h4.73c10.33 0 15.94 2.44 15.94 9.09 0 4.72-4.2 8.22-14.36 8.22a41.54 41.54 0 0 1-15.47-3.12c-6.49-3.88-11.74-3.5-15.6 3.12l-5.59 9.31c-3.72 6.13-3.19 11.72 2.63 15.94 7.71 4.69 20.38 9.44 37 9.44 34.16 0 48.5-22.75 48.5-44.12-.03-14.38-9.12-29.76-28.73-34.88zM496 224H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM16 160h64a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H64V40a8 8 0 0 0-8-8H32a8 8 0 0 0-7.14 4.42l-8 16A8 8 0 0 0 24 64h8v64H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8zm-3.91 160H80a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H41.32c3.29-10.29 48.34-18.68 48.34-56.44 0-29.06-25-39.56-44.47-39.56-21.36 0-33.8 10-40.46 18.75-4.37 5.59-3 10.84 2.8 15.37l8.58 6.88c5.61 4.56 11 2.47 16.12-2.44a13.44 13.44 0 0 1 9.46-3.84c3.33 0 9.28 1.56 9.28 8.75C51 248.19 0 257.31 0 304.59v4C0 316 5.08 320 12.09 320z"/></svg></button>

      <button 
        class="
          wrap-code-button
          tw:select-none 
          tw:mx-2 
          tw:hidden 
          tw:group-[.is-open]:block 
          tw:group-[.is-wrap]:text-fgColor-link 
          tw:print:hidden!" 
        title="Toggle code wrap"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"/></svg></button>
      
      <button 
        class="
          copy-code-button
          tw:select-none
          tw:mx-2 
          tw:hidden
          tw:group-[.is-open]:block
          tw:hover:text-fgColor-link 
          tw:print:hidden!"
        title="Copy code">
          <span class="copy-icon tw:block"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM266 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 0 1-6 6zm128-96H182a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h106v88c0 13.255 10.745 24 24 24h88v202a6 6 0 0 1-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 0 1 1.757 4.243V112z"/></svg></span>
          <span class="check-icon tw:hidden"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/></svg></span>
      </button>
        
      <button 
        class="
          tw:select-none 
          tw:mx-2 
          tw:block 
          tw:group-[.is-open]:hidden 
          tw:print:hidden!" 
        disabled
        aria-hidden="true"><svg class="icon"
    xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M328 256c0 39.8-32.2 72-72 72s-72-32.2-72-72 32.2-72 72-72 72 32.2 72 72zm104-72c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72zm-352 0c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72z"/></svg></button>
    </div>
  </div>
  <pre style="counter-reset: codeblock;" class="tw:block tw:m-0 tw:p-0"><code 
    id="codeblock-id-12" 
    class="
      chroma 
      tw:block! 
      tw:p-0
      tw:m-0
      tw:transition-[max-height] 
      tw:duration-500 
      tw:ease-in-out 
      tw:group-[.is-closed]:max-h-0! 
      tw:group-[.is-wrap]:text-wrap
      tw:overflow-y-hidden
      tw:overflow-x-auto
      tw:scrollbar-thin
      "><span class="line"><span class="cl"><span class="c1">// 看起来很美，但我们没有选这条路
</span></span></span><span class="line"><span class="cl"><span class="k">val</span> <span class="py">handler</span> <span class="p">=</span> <span class="n">CoroutineExceptionHandler</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">throwable</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nc">AppLogger</span><span class="p">.</span><span class="n">e</span><span class="p">(</span><span class="s2">&#34;VM&#34;</span><span class="p">,</span> <span class="n">throwable</span><span class="p">)</span> <span class="p">{</span> <span class="s2">&#34;未捕获异常&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</div>
<p>原因是 <code>CoroutineExceptionHandler</code> 解决的是&quot;进程不死&quot;，解决不了&quot;用户看到什么&quot;：</p>
<ul>
<li><strong>它拿不到上下文</strong>。异常到达根部处理器时，只剩下一个 <code>Throwable</code> 和一个 <code>CoroutineContext</code>——不知道是哪条订阅挂了、用户在哪个页面、应该给哪个 UiState 写 <code>errorMessage</code>。进程保住了，但页面永远卡在 loading 转圈。</li>
<li><strong>它对 <code>async</code> 无效</strong>。<code>async</code> 的异常被封装进 <code>Deferred</code>，要等 <code>await()</code> 才抛出，根本不经过根部处理器。</li>
<li><strong>它是最后防线，不是业务逻辑</strong>。把&quot;加载失败请重试&quot;这种本该属于 UiState 的状态交给全局处理器，等于让错误处理脱离了状态管理的单轨——而我们整个架构的根基就是&quot;UiState 是 UI 的唯一事实来源&quot;。</li>
</ul>
<p>局部 <code>try/catch</code> 的价值恰恰在于<strong>它在业务现场</strong>：知道失败的是哪个操作，能把异常翻译成该页面的 <code>errorMessage</code>，能把 <code>isLoading</code> 复位。全局处理器兜的是&quot;万一漏了&quot;的底，局部 catch 管的是&quot;每个操作失败了怎么办&quot;。两者不冲突，但后者是必须做的。</p>
<h2 id="-沉淀共享层异常处理的三条军规" class="headerLink">
    <a href="#-%e6%b2%89%e6%b7%80%e5%85%b1%e4%ba%ab%e5%b1%82%e5%bc%82%e5%b8%b8%e5%a4%84%e7%90%86%e7%9a%84%e4%b8%89%e6%9d%a1%e5%86%9b%e8%a7%84" class="header-mark"></a>📌 沉淀：共享层异常处理的三条军规</h2><p>事故修复后，我们把这次的教训固化成了共享层的编码约定：</p>
<ol>
<li><strong><code>viewModelScope.launch</code> 里 collect Flow，必须有 <code>try/catch</code> 兜底</strong>。这是本次事故的直接根因。Flow 的上游是数据库、网络、平台桥接——都是不可信边界，异常一定会发生，问题只是发生在哪台设备上。</li>
<li><strong><code>catch (e: CancellationException) { throw e }</code> 永远是第一个 catch 分支</strong>。取消语义是协程的命根子，吞掉它等于制造泄漏。这条写在最前面，形成肌肉记忆。</li>
<li><strong>异常只进两个出口：<code>AppLogger</code> 和 <code>UiState.errorMessage</code></strong>。不允许静默吞掉，也不允许逃逸出协程。用户可见的错误走 UiState 单轨，排查用的细节走日志——和整个架构&quot;UiState 是唯一事实来源&quot;的原则保持一致。</li>
</ol>
<p>回头看，这类问题其实是 KMP 共享层的一个系统性陷阱：<strong>Kotlin 代码在 Android 上的容错惯性，不能直接平移到 iOS</strong>。JVM 世界有成熟的崩溃捕获体系托底，很多&quot;没 catch 也没事&quot;的代码就这样活了下来；而 Kotlin/Native 的 <code>propagateExceptionFinalResort</code> 对这种代码零容忍——<strong>未捕获，即死刑</strong>。共享层每下沉一分，这类平台行为差异就要多敬畏一分。</p>
<hr>
<h2 id="-关于轻糖" class="headerLink">
    <a href="#-%e5%85%b3%e4%ba%8e%e8%bd%bb%e7%b3%96" class="header-mark"></a>🍬 关于轻糖</h2><p><strong>轻糖（SugarLite）</strong> 是一款专注于血糖管理的 App，帮助糖尿病及血糖关注人群记录和分析血糖、饮食、运动、用药与胰岛素数据，并提供 AI 识餐、AI 食谱、统计报告等智能功能。iOS 与 Android 双端基于 Kotlin Multiplatform 构建，业务逻辑与状态管理全量共享。</p>
<ul>
<li>官网：<a href="https://sugarlite.top" target="_blank" rel="noopener noreferrer">https://sugarlite.top</a></li>
<li>App Store：<a href="https://apps.apple.com/app/id6753901096" target="_blank" rel="noopener noreferrer">下载轻糖</a></li>
</ul>
<div class="appstore-card" id="appstore-531624" data-url="https://apps.apple.com/app/id6753901096" data-country="cn">
  <div class="appstore-loading">
    <div class="appstore-skeleton-icon"></div>
    <div class="appstore-skeleton-info">
      <div class="appstore-skeleton-line short"></div>
      <div class="appstore-skeleton-line medium"></div>
      <div class="appstore-skeleton-line long"></div>
    </div>
  </div>
</div>

<script data-cfasync="false">
(function() {
  var el = document.getElementById('appstore-531624');
  if (!el) return;
  var url = el.getAttribute('data-url');
  var country = el.getAttribute('data-country') || 'cn';

  
  var match = url.match(/\/id(\d+)/i);
  if (!match) {
    el.innerHTML = '<p style="color:red;">无法从链接中提取 App ID，请检查链接格式，例如：https://apps.apple.com/cn/app/.../id123456789</p>';
    return;
  }
  var appId = match[1];

  var apiUrl = 'https://itunes.apple.com/lookup?id=' + appId + '&country=' + country;

  fetch(apiUrl)
    .then(function(res) { return res.json(); })
    .then(function(data) {
      if (data.resultCount === 0 || !data.results || !data.results[0]) {
        el.innerHTML = '<p style="color:red;">未找到该应用信息，请检查 App ID 是否正确</p>';
        return;
      }
      var app = data.results[0];
      var name = app.trackName || '未知应用';
      var icon = app.artworkUrl512 || app.artworkUrl100 || '';
      var rating = app.averageUserRating;
      var ratingCount = app.userRatingCount;
      var desc = app.description || '';
      
      var shortDesc = desc.length > 180 ? desc.substring(0, 180) + '…' : desc;
      var viewUrl = app.trackViewUrl || url;
      var genre = app.primaryGenreName || '';
      var price = app.formattedPrice || '免费';
      var size = app.fileSizeBytes ? (app.fileSizeBytes / (1024 * 1024)).toFixed(0) + ' MB' : '';
      var screenshots = app.screenshotUrls || [];

      var stars = '';
      if (rating !== undefined && rating !== null) {
        var full = Math.floor(rating);
        var half = (rating - full) >= 0.5 ? 1 : 0;
        for (var i = 0; i < full; i++) { stars += '★'; }
        if (half) { stars += '☆'; }
        for (var i = full + half; i < 5; i++) { stars += '☆'; }
      }

      var html = '';
      html += '<div class="appstore-card-inner">';
      html += '  <div class="appstore-icon">';
      html += '    <img src="' + icon + '" alt="' + name + '" loading="lazy" onerror="this.onerror=null;this.src=\'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2280%22 height=%2280%22><rect fill=%22%23e9ecef%22 width=%2280%22 height=%2280%22 rx=%2216%22/><text x=%2240%22 y=%2248%22 text-anchor=%22middle%22 font-size=%2224%22>📱</text></svg>\'">';
      html += '  </div>';
      html += '  <div class="appstore-info">';
      html += '    <div class="appstore-name">' + name + '</div>';
      html += '    <div class="appstore-meta">';
      if (genre) html += '<span>' + genre + '</span>';
      if (price && price !== '免费') html += '<span class="appstore-dot">·</span><span>' + price + '</span>';
      html += '    </div>';
      if (stars) {
        html += '    <div class="appstore-rating">';
        html += '      <span class="appstore-stars">' + stars + '</span>';
        if (ratingCount) html += '      <span class="appstore-rating-count">(' + (ratingCount > 1000 ? (ratingCount/1000).toFixed(0) + 'k' : ratingCount) + ')</span>';
        html += '    </div>';
      }
      html += '    <div class="appstore-desc">' + shortDesc + '</div>';
      html += '    <a href="' + viewUrl + '" target="_blank" rel="noopener noreferrer" class="appstore-download-btn">';
      html += '      <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>';
      html += '      App Store 下载';
      html += '    </a>';
      html += '  </div>';
      html += '</div>';
      el.innerHTML = html;
    })
    .catch(function(err) {
      el.innerHTML = '<p style="color:red;">获取应用信息失败: ' + err.message + '</p>';
    });
})();
</script>
<h2 id="-系列文章与参考资料" class="headerLink">
    <a href="#-%e7%b3%bb%e5%88%97%e6%96%87%e7%ab%a0%e4%b8%8e%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-mark"></a>📚 系列文章与参考资料</h2><ul>
<li><a href="https://youngxhui.top/2026/05/sugarlite-kmp-migration/" target="_blank" rel="noopener noreferrer">第一篇：轻糖的 KMP 渐进式迁移实践</a></li>
<li><a href="https://youngxhui.top/2026/06/sugarlite-kmp-migration-2/" target="_blank" rel="noopener noreferrer">第二篇：轻糖的 KMP 渐进式迁移实践（二）：ViewModel、本地存储与平台抽象</a></li>
<li><a href="https://youngxhui.top/2026/08/sugarlite-kmp-migration-3/" target="_blank" rel="noopener noreferrer">第三篇：轻糖的 KMP 渐进式迁移实践（三）：KMPObservableViewModel 直连</a></li>
<li><a href="https://github.com/rickclephas/KMP-ObservableViewModel" target="_blank" rel="noopener noreferrer">KMP-ObservableViewModel</a></li>
</ul>
<hr>
<p><em>本文基于轻糖项目的真实事故排查过程撰写。</em></p>
]]></description></item></channel></rss>