<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>落叶挽歌</title>
  
  <subtitle>juliswang&#39;s 博客</subtitle>
  <link href="http://julis.wang/atom.xml" rel="self"/>
  
  <link href="http://julis.wang/"/>
  <updated>2026-06-01T11:36:24.373Z</updated>
  <id>http://julis.wang/</id>
  
  <author>
    <name>落叶挽歌</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>MediaCodec 全链路深度剖析(五)：从 C2Work 到 vendor 进程</title>
    <link href="http://julis.wang/2026/06/01/MediaCodec-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90-%E4%BA%94-%EF%BC%9A%E4%BB%8E-C2Work-%E5%88%B0-vendor-%E8%BF%9B%E7%A8%8B/"/>
    <id>http://julis.wang/2026/06/01/MediaCodec-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90-%E4%BA%94-%EF%BC%9A%E4%BB%8E-C2Work-%E5%88%B0-vendor-%E8%BF%9B%E7%A8%8B/</id>
    <published>2026-06-01T13:34:00.000Z</published>
    <updated>2026-06-01T11:36:24.373Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>系列导读</strong>：第四篇停在<code>CCodecBufferChannel::queueInputBufferInternal</code>里那一行<code>mComponent-&gt;queue(&amp;items)</code>——一个<code>C2Work</code>列表打过去，HAL 那边解完通过<code>onWorkDone</code>回来。这一篇接着拆开这条线：</p><ul><li><strong><code>C2Work</code>到底装了什么、为什么是这个形状？</strong></li><li><strong><code>mComponent-&gt;queue()</code>这一下从 App 进程到 vendor 进程到底走过哪些环节？</strong></li><li><strong>解出来的 YUV 是怎么跨进程零拷贝交回来的？</strong></li></ul><p>Codec2 的几个核心数据结构会先用一节快速过完，主线放在<code>C2Work</code>的来回与跨进程机制。</p></blockquote><h2 id="Codec2-数据结构速览"><a href="#Codec2-数据结构速览" class="headerlink" title="Codec2 数据结构速览"></a>Codec2 数据结构速览</h2><p>后面追<code>queue / onWorkDone</code>这条线时会反复用到这几个名字，这里先一张表对齐：</p><div class="table-container"><table><thead><tr><th>结构</th><th>一句话定义</th><th>用在哪</th></tr></thead><tbody><tr><td><code>C2Work</code></td><td>一次编解码任务的描述对象，HAL 唯一的输入单元</td><td><code>comp-&gt;queue()</code>传进去、<code>onWorkDone</code>回来</td></tr><tr><td><code>C2FrameData</code></td><td>一帧的数据载体——时间戳 + buffers + 配置更新 + flags</td><td><code>C2Work::input</code>和<code>worklet-&gt;output</code></td></tr><tr><td><code>C2Buffer</code></td><td>数据容器，分 Linear（码流 / PCM）和 Graphic（YUV）</td><td>装在<code>C2FrameData::buffers</code>里</td></tr><tr><td><code>C2Block</code></td><td>底层 DMA 内存的抽象，持有 ION/dma-buf fd</td><td><code>C2Buffer</code>内部组件</td></tr><tr><td><code>C2BlockPool</code></td><td>Block 分配器，决定内存来源</td><td>输入用 LINEAR pool，输出 Surface 时用 BUFFERQUEUE pool</td></tr><tr><td><code>C2Fence</code></td><td>硬件同步原语，对应一个 sync fd</td><td>跨硬件等”buffer 真的可读 / 可写”的信号</td></tr><tr><td><code>C2Param</code></td><td>统一的参数协议，所有配置项都用它表达</td><td><code>MediaFormat</code>翻成一组<code>C2Param</code>下发</td></tr></tbody></table></div><p>层级上<code>C2Work</code>套<code>C2FrameData</code>套<code>C2Buffer</code>套<code>C2Block</code>，越往里越接近物理内存。<code>C2BlockPool</code>在外侧管分配，<code>C2Fence</code>横在中间管时序，<code>C2Param</code>独立成另一条线管配置。</p><p>后面真正要追的就两条：<strong><code>C2Work</code>在<code>queue / onWorkDone</code>这条来回上是怎么填、怎么对回来的</strong>，以及<strong>它内部那几个<code>shared_ptr&lt;C2Buffer&gt;</code>是怎么跨进程不丢内存的</strong>。</p><h2 id="C2Work-长什么样"><a href="#C2Work-长什么样" class="headerlink" title="C2Work 长什么样"></a>C2Work 长什么样</h2><p>打开<code>C2Work.h</code>，结构体本身只有四五个字段：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">C2Work</span> &#123;</span><br><span class="line">    C2FrameData input;                                 <span class="comment">// 输入：一帧压缩数据</span></span><br><span class="line">    std::list&lt;std::unique_ptr&lt;C2Worklet&gt;&gt; worklets;    <span class="comment">// 输出占位</span></span><br><span class="line"></span><br><span class="line">    <span class="type">uint32_t</span>   workletsProcessed = <span class="number">0</span>;                  <span class="comment">// 完成的 worklet 数</span></span><br><span class="line">    <span class="type">c2_status_t</span> result = C2_OK;                        <span class="comment">// 处理结果</span></span><br><span class="line"></span><br><span class="line">    C2WorkOrdinalStruct input_ordinal;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">C2FrameData</span> &#123;</span><br><span class="line">    <span class="type">uint32_t</span> flags;                                       <span class="comment">// EOS / CODEC_CONFIG / DROP_FRAME ...</span></span><br><span class="line">    C2WorkOrdinalStruct ordinal;                          <span class="comment">// 时间戳 / 帧号</span></span><br><span class="line">    std::vector&lt;std::shared_ptr&lt;C2Buffer&gt;&gt; buffers;       <span class="comment">// 实际数据</span></span><br><span class="line">    std::vector&lt;std::unique_ptr&lt;C2Param&gt;&gt; configUpdate;   <span class="comment">// 动态配置（如 bitrate 调整）</span></span><br><span class="line">    std::vector&lt;std::shared_ptr&lt;C2InfoBuffer&gt;&gt; infoBuffers;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>第四篇<code>queueInputBufferInternal</code>那段填的就是这两个结构。先记三条，后面跨进程时还会回来用：</p><p><strong>输入和输出共用同一种容器<code>C2FrameData</code></strong>。<code>input</code>是一份<code>C2FrameData</code>，每个<code>worklet-&gt;output</code>也是一份<code>C2FrameData</code>。差别只在装的内容：input 装压缩码流（Linear <code>C2Buffer</code>），output 装解码 YUV（Graphic <code>C2Buffer</code>）。这种对称让 HAL 那边写代码很省事——同一套 buffer 处理逻辑两边都能用。</p><p><strong><code>worklets</code>是输出占位，不是预先填好的输出</strong>。CCodecBufferChannel 在<code>queueInputBufferInternal</code>里只<code>emplace_back(new C2Worklet)</code>塞了一个空 worklet 进去，里面什么都没有。HAL 解完一帧后，把 YUV <code>C2Buffer</code>填到<code>worklet-&gt;output.buffers</code>里再回传——这个”留位 → 回填”的设计是异步路径的关键。</p><p><strong><code>input_ordinal</code>三个字段各管各的</strong>：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">C2WorkOrdinalStruct</span> &#123;</span><br><span class="line">    <span class="type">c2_cntr64_t</span> timestamp;       <span class="comment">// PTS（纳秒），可以不单调（B 帧重排）</span></span><br><span class="line">    <span class="type">c2_cntr64_t</span> frameIndex;      <span class="comment">// 全局单调递增的帧号</span></span><br><span class="line">    <span class="type">c2_cntr64_t</span> customOrdinal;   <span class="comment">// 客户端自定义</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><code>timestamp</code>是 PTS，给 A/V 同步看的；<code>frameIndex</code>是 BufferChannel 自己维护的单调递增序号——HAL 处理完回调时只带这个 index，BufferChannel 拿它在内部映射表里查”这条 work 是哪一帧、对应哪个 input slot”。第四篇那行<code>mFrameIndex++</code>填的就是这个字段。<code>customOrdinal</code>这里就是 PTS 的副本，给上层做去重用。</p><p>为什么不直接拿<code>timestamp</code>认领？因为 B 帧场景下 PTS 不单调、可能重复（seek 后两帧 PTS 相同），靠它做主键会撞车。<code>frameIndex</code>保证一帧一个 ID，认领时不会出歧义。</p><h2 id="一次-queue-的进程内调用"><a href="#一次-queue-的进程内调用" class="headerlink" title="一次 queue 的进程内调用"></a>一次 queue 的进程内调用</h2><p><code>mComponent-&gt;queue()</code>这行后面，从 App 进程到 vendor 进程之间隔着两个明显的边界：一个是 HAL 客户端代理<code>Codec2Client</code>，一个是 Binder 内核调用。</p><p>两个边界各自承担一件不同的事：</p><ul><li><strong><code>Codec2Client</code>这一层</strong>做的是<strong>类型转换</strong>——把 App 进程内部用的 C++ 对象<code>C2Work</code>（带<code>std::list</code>、<code>std::shared_ptr&lt;C2Buffer&gt;</code>、虚表指针这些没法跨进程的东西）翻译成 HIDL/AIDL 定义的、纯数据的<code>WorkBundle</code>。这一步只在 App 进程内做，不涉及内核。</li><li><strong>Binder 这一层</strong>做的是<strong>搬运</strong>——把上面那个<code>WorkBundle</code>走 binder 驱动送到 vendor 进程，期间内核负责 fd 的复制和数据拷贝。</li></ul><p>为什么要拆成两层？因为 HIDL/AIDL 自动生成的 binder proxy 只认 IDL 里声明的纯数据类型，而<code>CCodecBufferChannel</code>手里拿的是 C++ 业务对象。中间这层<code>Codec2Client</code>就是”业务对象 ↔ IDL 数据”的翻译层——上层永远拿<code>C2Work</code>写代码，下层永远收<code>WorkBundle</code>传 binder，互不打扰。</p><p>先看 App 进程这一侧从 BufferChannel 走到代理为止的路径。</p><pre class="mermaid">flowchart TB    A["CCodecBufferChannel::queueInputBufferInternal<br/>构造 C2Work、填 frameIndex、切 C2Buffer 视图"]    B["std::list&lt;unique_ptr&lt;C2Work&gt;&gt; items"]    C["mComponent-&gt;queue(&items)<br/>(Codec2Client::Component 客户端代理)"]    D["objcpy(C2Work -&gt; WorkBundle)<br/>把 shared_ptr&lt;C2Buffer&gt; 序列化成可跨进程的形式"]    E["IComponent::queue(workBundle)<br/>(HIDL/AIDL proxy)"]    F["binder driver"]    A --> B --> C --> D --> E --> F</pre><p>最关键的一步是<code>objcpy</code>这个序列化函数，对应<code>Codec2Client::Component::queue</code>大致是这样：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="type">c2_status_t</span> Codec2Client::Component::<span class="built_in">queue</span>(</span><br><span class="line">        std::list&lt;std::unique_ptr&lt;C2Work&gt;&gt; *<span class="type">const</span> items) &#123;</span><br><span class="line">    <span class="comment">// 1. 把 C2Work 列表打包成跨进程能传的 WorkBundle</span></span><br><span class="line">    WorkBundle workBundle;</span><br><span class="line">    Status status = <span class="built_in">objcpy</span>(&amp;workBundle, *items, &amp;mBufferPoolSender);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. Binder 调用，把 workBundle 投给 vendor</span></span><br><span class="line">    Return&lt;Status&gt; transStatus = mBase-&gt;<span class="built_in">queue</span>(workBundle);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 清空原列表——所有权已经过去了</span></span><br><span class="line">    items-&gt;<span class="built_in">clear</span>();</span><br><span class="line">    <span class="keyword">return</span> <span class="comment">/* ... */</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>WorkBundle</code>是 HIDL/AIDL 自动生成的可序列化结构，对应<code>C2Work</code>但只装可跨进程传的字段（POD 数据 + handle）。<code>objcpy</code>做的事情就是把每个<code>C2Work</code>里的<code>shared_ptr&lt;C2Buffer&gt;</code>拆开、把底层<code>C2Block</code>的<code>native_handle</code>（也就是 ION/dma-buf 的 fd）抽出来塞进<code>WorkBundle</code>。</p><p><strong><code>std::shared_ptr&lt;C2Buffer&gt;</code>本身没法跨进程</strong>——它是 App 进程里的 C++ 引用计数对象。能跨过去的只有里面那个 fd。Binder 跨进程传 fd 时，内核会在目标进程里复制出一个新 fd 编号，但指向同一块物理内存。<code>WorkBundle</code>这一头是”原料清单”，到 vendor 那头<code>objcpy</code>再反向走一遍——根据 fd 重建一个本地的<code>C2Buffer</code>/<code>C2Block</code>对象。两边都拿着各自的<code>shared_ptr</code>管自己的引用计数，不互相干扰。</p><pre class="mermaid">flowchart LR    subgraph App["App 进程"]        direction TB        A1["C2Buffer"]        A2["C2Block"]        A3["C2Handle<br/>fd = 42"]        A1 --> A2 --> A3    end    subgraph Vendor["vendor 进程"]        direction TB        V1["C2Buffer"]        V2["C2Block"]        V3["C2Handle<br/>fd = 78"]        V1 --> V2 --> V3    end    DMA[("同一块<br/>ION / dma-buf")]    A3 -. Binder 传 fd .-> V3    A3 --> DMA    V3 --> DMA</pre><p>但只靠 fd 还有一个问题没解决：<strong>生产侧什么时候 free 这块内存</strong>？App 进程把<code>shared_ptr</code>一释放，本地引用计数归零，但 vendor 那边可能还在用。这事归 BufferPool 管。</p><h2 id="BufferPool：跨进程的引用计数"><a href="#BufferPool：跨进程的引用计数" class="headerlink" title="BufferPool：跨进程的引用计数"></a>BufferPool：跨进程的引用计数</h2><p><code>android.hardware.media.bufferpool@2.0</code>是一个独立的 HAL 服务，专门做一件事——<strong>让一块共享内存的引用计数能跨进程算清楚</strong>。</p><p>机制概括成一句：每块 buffer 有一个全局唯一的 BufferID，所有持有者通过 BufferPool 注册和注销，BufferPool 数到 0 才真正释放。</p><pre class="mermaid">sequenceDiagram    participant App as App 进程    participant Pool as BufferPool 服务    participant V as vendor 进程    V->>Pool: register(fd, BufferID=X)    V->>App: queue WorkBundle (含 BufferID=X)    App->>Pool: receive(BufferID=X)    Note over App: refcount(X) = 2    App->>App: 用户 releaseOutputBuffer    App->>Pool: postSendMessage(release, X)    Note over Pool: refcount(X) = 1    V->>Pool: postSendMessage(release, X)    Note over Pool: refcount(X) = 0 → 真正释放</pre><p>之所以要单独做这层，是因为 Binder 自己的 fd 复制语义不带”全局引用计数”概念——A 关 fd 不影响 B 那边的 fd，但物理内存到底还在不在用，只看 fd 是不够的（fd 能复制无数份）。BufferPool 就在这上面补了一层全局账本。</p><p>第四篇<code>queueInputBuffer</code>走到这里能看到的是：<code>releaseBuffer(buffer, &amp;c2buffer, /*release*/ false)</code>切出来的<code>c2buffer</code>一旦塞进<code>WorkBundle</code>，跨过 Binder 后 vendor 那边会收到一份”已注册的”<code>C2Buffer</code>——背后就是 BufferPool 在管着。</p><h2 id="跨进程之后：vendor-进程的处理"><a href="#跨进程之后：vendor-进程的处理" class="headerlink" title="跨进程之后：vendor 进程的处理"></a>跨进程之后：vendor 进程的处理</h2><p>前面三节分别讲了 App 进程内部的序列化、BufferPool 的引用计数、跨过 binder 之后的解封装——这些片段拼起来才是一帧完整的”App → vendor”路径。先把它们叠成一张端到端的时序图，再回来看 vendor 这一侧的细节：</p><pre class="mermaid">sequenceDiagram    participant CC as CCodecBufferChannel<br/>(App 进程)    participant CL as Codec2Client::Component<br/>(App 进程)    participant BD as binder driver<br/>(kernel)    participant ST as BnComponent stub<br/>(vendor 进程)    participant CI as ComponentImpl::queue<br/>(vendor 进程)    participant WT as vendor 工作线程    CC->>CC: 构造 C2Work、填 frameIndex<br/>切 C2Buffer 视图    CC->>CL: queue(&items)    CL->>CL: objcpy(C2Work → WorkBundle)<br/>抽出 native_handle (fd)    CL->>BD: IComponent::queue(workBundle)    BD->>BD: 复制 fd 到目标进程    BD->>ST: onTransact(QUEUE)    ST->>CI: Component::queue(workBundle)    CI->>CI: objcpy(WorkBundle → C2Work)<br/>按 fd 重建 C2Buffer    CI->>WT: queue_nb(入队后立即返回)    CI-->>BD: Status::OK    BD-->>CL: 同步返回    CL-->>CC: c2_status_t = C2_OK    Note over WT: 异步：取 work、调 VPU<br/>填 worklet->output、回调 onWorkDone</pre><p>图里的关键节点对应前面三节：<code>objcpy</code>两次（去程一次拆、vendor 一次装）来自”一次 queue 的进程内调用”；binder driver 复制 fd 的语义在”BufferPool”那节展开；<code>queue_nb</code>和异步工作线程就是这一节要讲的重点。</p><p>接着看 vendor 这一侧调用栈大致是：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[kernel: binder driver]</span><br><span class="line">    ↓</span><br><span class="line">BnComponent::onTransact          // HIDL/AIDL 自动生成的 stub</span><br><span class="line">    ↓</span><br><span class="line">Component::queue                 // HAL 接口实现层</span><br><span class="line">    ↓</span><br><span class="line">ComponentImpl::queue (vendor 实现) // 厂商私有逻辑</span><br><span class="line">    ↓</span><br><span class="line">C2Component::queue_nb            // 真正的组件入口（non-blocking）</span><br><span class="line">    ↓</span><br><span class="line">入队组件内部 work queue，立即返回</span><br></pre></td></tr></table></figure><p>注意最后是<code>queue_nb</code>——non-blocking。vendor 这边收到<code>WorkBundle</code>、反序列化回<code>C2Work</code>列表、塞进自己的内部队列，整条链就立即沿着 Binder 返回了。<strong>真正的解码动作发生在 vendor 进程的另一条工作线程上</strong>：那条线程从队列里取<code>C2Work</code>，调 VPU 驱动 ioctl，硬件解码完成后，从输出 BlockPool 里分配一块 Graphic Block，构造<code>C2Buffer</code>填到<code>worklet-&gt;output.buffers</code>，置<code>workletsProcessed = 1</code>，然后通过 Listener 把这条<code>C2Work</code>回传。</p><p>这个分裂——“<code>queue</code>同步返回 + 解码异步进行 + <code>onWorkDone</code>异步回调”——是第四篇里反复说的”<code>comp-&gt;queue()</code>是 fire-and-forget”在协议这一层的具体形状。</p><h2 id="onWorkDone-的回路"><a href="#onWorkDone-的回路" class="headerlink" title="onWorkDone 的回路"></a>onWorkDone 的回路</h2><p>输出方向有一个不太直觉的点：<strong><code>onWorkDone</code>这条回调链是 vendor 主动调 App，方向反过来</strong>。</p><pre class="mermaid">flowchart TB    V["vendor 进程<br/>解码完成、填好 worklet-&gt;output"]    L["IComponentListener::onWorkDone<br/>(server-&gt;client 回调，HIDL/AIDL 都支持)"]    K["binder driver"]    P["App 进程<br/>BnComponentListener::onTransact"]    O["objcpy(WorkBundle -&gt; C2Work)<br/>反序列化，重建 shared_ptr&lt;C2Buffer&gt;"]    C["CCodecBufferChannel::onWorkDone(workItems)"]    V --> L --> K --> P --> O --> C</pre><p>要让 vendor 能”反过来”调 App，初始化时 App 这一侧得先把一个<code>Listener</code>注册过去。第四篇<code>CCodec::start</code>里那条不起眼的注释”订阅事件”做的就是这件事——App 进程构造一个<code>BufferPoolSender + Listener</code>远端对象，通过 HIDL/AIDL 发到 vendor 那边，vendor 拿着这个 binder proxy 在解完一帧后回调。</p><p><strong>输出 buffer 的零拷贝分配</strong>也是这一步的关键。回想一下第四篇<code>CCodec::configure</code>第 6 步：<code>mChannel-&gt;setSurface(surface)</code>。它干的事是创建一个<code>BUFFERQUEUE</code> 类型的<code>C2BlockPool</code>，把 Surface 的<code>IGraphicBufferProducer</code>挂上去。这个 pool 的 handle 跨进程发到 vendor 之后，vendor 解码出每帧 YUV 时，从这个 pool 里<code>fetchGraphicBlock</code>——内部直接走到 SurfaceFlinger 的 BufferQueue，dequeue 出一个真正的<code>GraphicBuffer</code> slot。</p><p>也就是说，<strong>vendor 解码出来的那块 YUV 内存，物理上一开始就分配在 SurfaceFlinger 的 BufferQueue 里</strong>。<code>C2Buffer</code>只是它在 vendor 进程里的一个引用视图。<code>onWorkDone</code>回到 App 那一刻，BufferChannel 拿到的是同一块内存的另一个视图。等到<code>renderOutputBuffer</code>把它<code>queueBuffer</code>给 BufferQueue 时，Consumer 那边发现”这块 slot 我之前给出去过、现在又回来了”，直接 latch 即可——<strong>全程没有一次内存拷贝</strong>。</p><p>vendor 解码 → SurfaceFlinger 显示，三个进程之间走的是同一块物理内存的不同视图，靠 fd + BufferPool + BufferQueue 三套机制接力。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这一篇沿着第四篇结尾<code>mComponent-&gt;queue()</code>那行，跨过进程边界把整条协议链拆开来看：</p><ul><li><strong><code>C2Work</code>是 HAL 唯一的输入单元</strong>，<code>input + worklets</code>两段、各自一个<code>C2FrameData</code>，靠<code>frameIndex</code>认领回路。<code>worklets</code>是预留的输出占位，由 vendor 回填。</li><li><strong>跨进程靠的是<code>WorkBundle</code>序列化</strong>——<code>shared_ptr&lt;C2Buffer&gt;</code>本身不能传，能传的只有底层 ION/dma-buf 的<code>native_handle</code>。<code>objcpy</code>两端各做一次拆装，内存不动，引用计数靠 BufferPool 跨进程算。</li><li><strong>解码输出的零拷贝靠 BUFFERQUEUE pool</strong>：<code>configure</code>阶段把 Surface 的 BufferQueue 包成<code>C2BlockPool</code>发给 vendor，vendor 解码时直接从 BufferQueue 拿 slot，物理内存全程不动，三个进程拿的都是同一块内存的不同视图。</li><li><strong><code>onWorkDone</code>是 server→client 反向回调</strong>，依赖 App 注册的 Listener proxy。<code>queue</code>同步返回 + vendor 内部异步处理 + <code>onWorkDone</code>异步回传——这一对组合是 fire-and-forget 在协议层的具体形状。</li></ul><p>MediaCodec 整条管线在 native 这一层的”零拷贝”不是一个孤立技巧，是<code>C2BlockPool</code> + <code>BufferPool</code> + <code>BufferQueue</code>三套机制叠出来的——前者管”内存来自哪”，中间管”还有谁在用”，后者管”显示端怎么接”。三层都用 fd 作为跨进程的硬通货，C++ 对象只是各自进程里的视图。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;系列导读&lt;/strong&gt;：第四篇停在&lt;code&gt;CCodecBufferChannel::queueInputBufferInternal&lt;/code&gt;里那一行&lt;code&gt;mComponent-&amp;gt;queue(&amp;amp;ite</summary>
      
    
    
    
    
    <category term="MediaCodec" scheme="http://julis.wang/tags/MediaCodec/"/>
    
  </entry>
  
  <entry>
    <title>MediaCodec 全链路深度剖析(四)：CCodec 与 BufferChannel 的分工</title>
    <link href="http://julis.wang/2026/05/11/MediaCodec-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90-%E5%9B%9B-%EF%BC%9ACCodec-%E4%B8%8E-BufferChannel-%E7%9A%84%E5%88%86%E5%B7%A5/"/>
    <id>http://julis.wang/2026/05/11/MediaCodec-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90-%E5%9B%9B-%EF%BC%9ACCodec-%E4%B8%8E-BufferChannel-%E7%9A%84%E5%88%86%E5%B7%A5/</id>
    <published>2026-05-11T14:48:00.000Z</published>
    <updated>2026-05-11T15:01:08.010Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>系列导读</strong>：第三篇把视角停在了<code>MediaCodec.cpp</code>的状态机，所有动作最后都落在一个<code>sp&lt;CodecBase&gt; mCodec</code>上。这一篇接着往下走，回答两个问题：</p><ul><li><strong>mCodec 到底是 CCodec 还是 ACodec？谁来选、什么时候选？</strong></li><li><strong>进了 CCodec 以后，状态切换、buffer 进出、Surface 渲染——为什么要拆给两个类来做？</strong></li></ul><p>主角是<code>CCodec</code>与<code>CCodecBufferChannel</code>，加上贯穿全文的<code>BufferChannelBase</code>这个抽象。</p></blockquote><h2 id="CCodec-和-ACodec"><a href="#CCodec-和-ACodec" class="headerlink" title="CCodec 和 ACodec"></a>CCodec 和 ACodec</h2><p>第三篇里反复出现的<code>mCodec</code>，类型是抽象基类<code>sp&lt;CodecBase&gt;</code></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">// MediaCodec.h</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MediaCodec</span> : <span class="keyword">public</span> AHandler &#123;</span><br><span class="line">    sp&lt;CodecBase&gt; mCodec;     <span class="comment">// 状态机的所有动作最终都作用在它身上</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><code>CodecBase</code>在 AOSP 里有两个实现：旧路径的<code>ACodec</code>（OMX）和新路径的<code>CCodec</code>（Codec2）。具体走哪条由组件名决定：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">// MediaCodec.cpp::GetCodecBase（约 L2358）</span></span><br><span class="line"><span class="function">sp&lt;CodecBase&gt; <span class="title">MediaCodec::GetCodecBase</span><span class="params">(<span class="type">const</span> AString &amp;name, <span class="type">const</span> <span class="type">char</span> *owner)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (owner) &#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">strncmp</span>(owner, <span class="string">&quot;default&quot;</span>, <span class="number">8</span>) == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> ACodec;                <span class="comment">// 旧路径：OMX</span></span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="built_in">strncmp</span>(owner, <span class="string">&quot;codec2&quot;</span>, <span class="number">6</span>) == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">CreateCCodec</span>();            <span class="comment">// 新路径：Codec2</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (name.<span class="built_in">startsWithIgnoreCase</span>(<span class="string">&quot;c2.&quot;</span>))   <span class="keyword">return</span> <span class="built_in">CreateCCodec</span>();</span><br><span class="line">    <span class="keyword">if</span> (name.<span class="built_in">startsWithIgnoreCase</span>(<span class="string">&quot;omx.&quot;</span>))  <span class="keyword">return</span> <span class="keyword">new</span> ACodec;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>规则只有三条：</p><ul><li>名字以<code>c2.</code>开头（例如<code>c2.android.avc.decoder</code>）走<code>CCodec</code>；</li><li>名字以<code>omx.</code>开头走<code>ACodec</code>；</li><li><code>MediaCodecList</code>里的 owner 字段也能直接指定。</li></ul><p>Android 10 以后绝大多数 vendor 已经迁到 Codec2，本篇之后只看<code>CCodec</code>这条线。</p><h2 id="CCodec-结构"><a href="#CCodec-结构" class="headerlink" title="CCodec 结构"></a>CCodec 结构</h2><p>打开<code>mCodec</code>之后，里面也不是一个职责无所不包的<code>CCodec</code>——<code>MediaCodec</code>真正持有的引用其实有两条：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">// MediaCodec.h</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MediaCodec</span> : <span class="keyword">public</span> AHandler &#123;</span><br><span class="line">    sp&lt;CodecBase&gt;                       mCodec;          <span class="comment">// CCodec / ACodec</span></span><br><span class="line">    std::shared_ptr&lt;BufferChannelBase&gt;  mBufferChannel;  <span class="comment">// CCodecBufferChannel / ACodecBufferChannel</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// MediaCodec.cpp（约 L2482，进入 INITIALIZED 时）</span></span><br><span class="line">mBufferChannel = mCodec-&gt;<span class="built_in">getBufferChannel</span>();</span><br></pre></td></tr></table></figure><p><code>mBufferChannel</code>由<code>CCodec::getBufferChannel()</code>创建并返回，<code>CCodec</code>内部用成员<code>mChannel</code>持有同一个对象。Java 一侧通过 JNI 看到的”一个 codec 实例”，对应到 native 是<strong>两个对象、三条引用、一个工厂关系</strong>：</p><pre class="mermaid">flowchart LR    MC["MediaCodec<br/>(状态机)"]    CC["CCodec<br/>(控制面)"]    BC["CCodecBufferChannel<br/>(数据面)"]    HAL["Codec2 HAL<br/>(vendor 进程)"]    MC -- "mCodec<br/>start/stop/flush" --> CC    MC -- "mBufferChannel<br/>queue/render/discard" --> BC    CC -- "mChannel<br/>同一个实例" --> BC    CC -- "下发控制指令" --> HAL    BC -- "queue C2Work / 收 onWorkDone" --> HAL</pre><p><code>MediaCodec</code>不会通过<code>mCodec</code>间接搬 buffer——每帧<code>queueInputBuffer</code>直接打到<code>mBufferChannel</code>上；<code>start / stop</code>这种则走<code>mCodec</code>。两层在<code>MediaCodec</code>这里就分开了。</p><p>三个名字最容易混，先一次说清：</p><div class="table-container"><table><thead><tr><th>名字</th><th>角色</th></tr></thead><tbody><tr><td><code>BufferChannelBase</code></td><td>抽象接口，定义<code>queueInputBuffer / renderOutputBuffer / discardBuffer</code>等数据面方法</td></tr><tr><td><code>CCodecBufferChannel</code></td><td><code>BufferChannelBase</code>在 Codec2 路径下的具体实现</td></tr><tr><td><code>ACodecBufferChannel</code></td><td><code>BufferChannelBase</code>在 OMX 路径下的具体实现</td></tr></tbody></table></div><p><code>MediaCodec</code>持有的类型是抽象<code>BufferChannelBase</code>，所以同一段上层代码在 OMX/Codec2 两条路径上是通用的——这条对偶后面会用到。</p><p>回到”为什么要拆”。<code>CCodec</code>和<code>CCodecBufferChannel</code>的职责切得很干净：</p><div class="table-container"><table><thead><tr><th>类</th><th>职责</th></tr></thead><tbody><tr><td><code>CCodec</code></td><td>生命周期管理（allocate / configure / start / stop / release），不直接管理数据流</td></tr><tr><td><code>CCodecBufferChannel</code></td><td>数据流管理（input/output buffer 进出、Surface 对接、HDR 侧带数据）</td></tr></tbody></table></div><p>拆成两个类，背后有三条理由：</p><ol><li><strong>变化频率不同</strong>。生命周期事件少且粗（一次<code>start</code>、一次<code>stop</code>），buffer 流转高频且细（每帧两次：queue + dequeue）。两者放一起会让加锁、等待、回调逻辑互相挤占。</li><li><strong>线程模型不同</strong>。<code>CCodec</code>跑在<code>CCodecLooper</code>这条配置线程上；<code>CCodecBufferChannel</code>的<code>onWorkDone</code>来自 HAL 回调线程；<code>queueInputBuffer</code>来自 App 线程。三条线索塞进同一个类里很难管。</li><li><strong>可替换性</strong>。<code>MediaCodec</code>持有的是<code>BufferChannelBase</code>这个接口，OMX 路径换成<code>ACodecBufferChannel</code>上层不用动一行。</li></ol><p>打个比方：<code>CCodec</code>是发动机工厂的厂长，决定开工、停工、招人；<code>CCodecBufferChannel</code>是流水线，负责原料进入、成品产出。<code>MediaCodec</code>是总公司，下指令给厂长的同时，也要直接对接流水线的进出货。</p><h2 id="CCodec-的初始化四步"><a href="#CCodec-的初始化四步" class="headerlink" title="CCodec 的初始化四步"></a>CCodec 的初始化四步</h2><p>从被构造出来到能跑第一帧，关键是四步：选中、allocate、configure、start。</p><p><strong>Step 1：被<code>MediaCodec</code>选中。</strong> <code>GetCodecBase</code>那段已经讲完——名字以<code>c2.</code>开头时返回<code>CreateCCodec()</code>，此后第三篇状态机里所有<code>mCodec-&gt;xxx()</code>都作用在该实例上。</p><p><strong>Step 2：<code>CCodec::allocate</code>。</strong></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">CCodec::allocate</span><span class="params">(<span class="type">const</span> sp&lt;MediaCodecInfo&gt; &amp;codecInfo)</span> </span>&#123;</span><br><span class="line">    AString componentName = codecInfo-&gt;<span class="built_in">getCodecName</span>();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 获取 Codec2Client（单例，连接到 vendor 进程）</span></span><br><span class="line">    std::shared_ptr&lt;Codec2Client&gt; client;</span><br><span class="line">    std::shared_ptr&lt;Codec2Client::Component&gt; comp =</span><br><span class="line">        Codec2Client::<span class="built_in">CreateComponentByName</span>(</span><br><span class="line">            componentName, mClientListener, &amp;client);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 把组件句柄交给 BufferChannel</span></span><br><span class="line">    mChannel-&gt;<span class="built_in">setComponent</span>(comp);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 回调给 MediaCodec：onComponentAllocated</span></span><br><span class="line">    mCallback-&gt;<span class="built_in">onComponentAllocated</span>(componentName.<span class="built_in">c_str</span>());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Codec2 路径上有四个名字会在后面反复出现，这里先简单介绍：</p><ul><li><strong>C2 HAL</strong>：Android 8.0 以后编解码器实现搬出 mediaserver，住进独立 vendor 进程<code>android.hardware.media.c2</code>。这套跨进程接口就是 Codec2 HAL。</li><li><strong><code>Codec2Client</code></strong>：mediaserver 这一侧的 HAL 客户端代理，负责跨进程握手、查询 vendor 那边可用的 codec 列表。</li><li><strong><code>Codec2Client::Component</code></strong>：通过<code>Codec2Client</code>拿到的”远端句柄”，对应 vendor 进程里某个具体 codec。<code>CCodec</code>后续所有<code>start / queue / flush</code>都通过它转发过去。</li><li><strong><code>C2Component</code></strong>：上面那个句柄对应的、住在 vendor 进程里的真正实例，是干活的对象。</li></ul><p>知道这四个名字，Step 2 这一步做的事就清楚了：通过<code>Codec2Client</code>跨进程连上 vendor 的 C2 HAL 服务 → 让 vendor 按名字（如<code>c2.android.avc.decoder</code>）创建<code>C2Component</code> → mediaserver 这边拿回<code>Codec2Client::Component</code>句柄交给<code>mChannel</code>持有 → 回调<code>onComponentAllocated</code>，<code>MediaCodec</code>状态机推到<code>INITIALIZED</code>。</p><p><strong>Step 3：<code>CCodec::configure</code>。</strong> 大头工作集中在这里，一次调用要做 10 件事：</p><ol><li>把 Java <code>MediaFormat</code>翻成<code>C2Param</code>（几十个字段）；</li><li>写入 width / height / color format 等基础参数；</li><li>解析并设置色彩元数据（<code>C2StreamColorAspectsInfo</code>）；</li><li>解析 HDR 静态/动态元数据；</li><li>处理 SPS/PPS csd-0 / csd-1；</li><li>设置输出 Surface（<code>mChannel-&gt;setSurface</code>）；</li><li>反向查询组件实际配置（vendor 可能改写）；</li><li>创建输入/输出 BlockPool；</li><li>处理 low-latency / tunneling 等特殊模式；</li><li>回调<code>onComponentConfigured</code>。</li></ol><p>源码位置：<code>CCodec.cpp::configure</code>，约从 L1400 开始的 600 多行。这里只点一条最容易踩的：第 7 步会把组件回填后的格式重新写回<code>outputFormat</code>，所以 Java 侧<code>getOutputFormat()</code>拿到的不是用户原始<code>MediaFormat</code>，而是 vendor 协商后的版本。</p><p><strong>Step 4：<code>CCodec::start</code>。</strong></p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">CCodec::start</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 1. 启动组件（让 vendor 进程开始干活）</span></span><br><span class="line">    <span class="type">c2_status_t</span> err = comp-&gt;<span class="built_in">start</span>();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 启动 BufferChannel：分配 buffer pool、把 input/output 的 slot 备好</span></span><br><span class="line">    mChannel-&gt;<span class="built_in">start</span>(inputFormat, outputFormat, buffersBoundToCodec);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 订阅事件：C2Component 通过 Listener 回调 onWorkDone / onError / onTripped</span></span><br><span class="line">    <span class="comment">//    这三条回调走的是 BufferChannel 不是 CCodec</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4. 回调 MediaCodec</span></span><br><span class="line">    mCallback-&gt;<span class="built_in">onStartCompleted</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>到这一步，控制面（<code>CCodec</code>）的工作基本就交完了。后面每帧的 queue/dequeue/render 跟<code>CCodec</code>不再发生关系，<code>MediaCodec</code>直接打到<code>mBufferChannel</code>上。</p><h2 id="CCodecBufferChannel-结构"><a href="#CCodecBufferChannel-结构" class="headerlink" title="CCodecBufferChannel 结构"></a>CCodecBufferChannel 结构</h2><p><code>CCodecBufferChannel</code>内部状态分成<code>Input</code>和<code>Output</code>两块，各自带一把锁：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CCodecBufferChannel</span> : <span class="keyword">public</span> BufferChannelBase &#123;</span><br><span class="line">    std::shared_ptr&lt;Codec2Client::Component&gt; mComponent;   <span class="comment">// HAL 组件句柄</span></span><br><span class="line"></span><br><span class="line">    Mutexed&lt;Input&gt;  mInput;     <span class="comment">// 输入管道</span></span><br><span class="line">    Mutexed&lt;Output&gt; mOutput;    <span class="comment">// 输出管道</span></span><br><span class="line"></span><br><span class="line">    std::shared_ptr&lt;C2BlockPool&gt; mInputAllocator;</span><br><span class="line">    std::shared_ptr&lt;C2BlockPool&gt; mOutputSurfacePool;</span><br><span class="line"></span><br><span class="line">    std::<span class="type">atomic_uint64_t</span> mFrameIndex;   <span class="comment">// 单调递增帧号，HAL 回调用它来配对</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Input</span> &#123;</span><br><span class="line">    std::unique_ptr&lt;InputBuffers&gt; buffers;</span><br><span class="line">    <span class="type">size_t</span> numSlots;</span><br><span class="line">    std::shared_ptr&lt;LocalBufferPool&gt; bufferPool;</span><br><span class="line">    sp&lt;MemoryDealer&gt; memoryDealer;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Output</span> &#123;</span><br><span class="line">    std::unique_ptr&lt;OutputBuffers&gt; buffers;</span><br><span class="line">    <span class="type">size_t</span> numSlots;</span><br><span class="line">    sp&lt;Surface&gt; surface;</span><br><span class="line">    sp&lt;IGraphicBufferProducer&gt; bufferProducer;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>几个抽象先一一对上：</p><div class="table-container"><table><thead><tr><th>抽象</th><th>含义</th></tr></thead><tbody><tr><td><code>InputBuffers</code></td><td>当前有效的输入 buffer 集合（client 持有 + pending in HAL）</td></tr><tr><td><code>OutputBuffers</code></td><td>输出 buffer 集合</td></tr><tr><td><code>C2BlockPool</code></td><td>底层 buffer 分配器（SURFACE / BUFFERQUEUE / BASIC 三类）</td></tr><tr><td><code>mFrameIndex</code></td><td>每帧分配的唯一 ID，HAL 回来时凭它认领</td></tr></tbody></table></div><p><code>Input</code>和<code>Output</code>各自被<code>Mutexed&lt;&gt;</code>包了一层，是因为<code>queueInputBuffer</code>来自 App 线程，<code>onWorkDone</code>来自 HAL 回调线程，<code>renderOutputBuffer</code>来自 App 线程，三条入口可能并发踩到同一份 slot 表。</p><h2 id="输入路径：queueInputBuffer"><a href="#输入路径：queueInputBuffer" class="headerlink" title="输入路径：queueInputBuffer"></a>输入路径：queueInputBuffer</h2><p>App 调一帧<code>queueInputBuffer(index, ...)</code>后，进到<code>CCodecBufferChannel</code>：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">status_t</span> <span class="title">CCodecBufferChannel::queueInputBufferInternal</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">        sp&lt;MediaCodecBuffer&gt; buffer,</span></span></span><br><span class="line"><span class="params"><span class="function">        std::shared_ptr&lt;C2BlockPool&gt; encryptedBlockPool,</span></span></span><br><span class="line"><span class="params"><span class="function">        sp&lt;AMessage&gt; outputFormat)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="type">int64_t</span> timeUs;</span><br><span class="line">    buffer-&gt;<span class="built_in">meta</span>()-&gt;<span class="built_in">findInt64</span>(<span class="string">&quot;timeUs&quot;</span>, &amp;timeUs);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 构造 C2Work：Codec2 框架里&quot;一次编解码任务&quot;的描述对象，由 input（数据 + 时间戳 +</span></span><br><span class="line">    <span class="comment">//    序号 + 配置更新）和 worklets（HAL 回填的输出占位）两部分组成。</span></span><br><span class="line">    <span class="function">std::unique_ptr&lt;C2Work&gt; <span class="title">work</span><span class="params">(<span class="keyword">new</span> C2Work)</span></span>;</span><br><span class="line">    work-&gt;input.ordinal.timestamp     = timeUs;</span><br><span class="line">    work-&gt;input.ordinal.frameIndex    = mFrameIndex++;   <span class="comment">// 给 HAL 回调认领用</span></span><br><span class="line">    work-&gt;input.ordinal.customOrdinal = timeUs;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 把 MediaCodecBuffer 视图切到 C2Buffer 视图（同一块共享内存，零拷贝）</span></span><br><span class="line">    <span class="keyword">if</span> (eos) work-&gt;input.flags = C2FrameData::FLAG_END_OF_STREAM;</span><br><span class="line">    <span class="keyword">if</span> (csd) work-&gt;input.flags = C2FrameData::FLAG_CODEC_CONFIG;</span><br><span class="line"></span><br><span class="line">    std::shared_ptr&lt;C2Buffer&gt; c2buffer;</span><br><span class="line">    mInput.<span class="built_in">lock</span>()-&gt;buffers-&gt;<span class="built_in">releaseBuffer</span>(buffer, &amp;c2buffer, <span class="comment">/*release*/</span> <span class="literal">false</span>);</span><br><span class="line">    <span class="keyword">if</span> (c2buffer) work-&gt;input.buffers.<span class="built_in">push_back</span>(c2buffer);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 加一个空 worklet，HAL 处理完后会把输出回填进来</span></span><br><span class="line">    work-&gt;worklets.<span class="built_in">emplace_back</span>(<span class="keyword">new</span> C2Worklet);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4. 跨进程投递给 vendor 进程</span></span><br><span class="line">    std::list&lt;std::unique_ptr&lt;C2Work&gt;&gt; items;</span><br><span class="line">    items.<span class="built_in">push_back</span>(std::<span class="built_in">move</span>(work));</span><br><span class="line">    <span class="type">c2_status_t</span> err = mComponent-&gt;<span class="built_in">queue</span>(&amp;items);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> err == C2_OK ? OK : UNKNOWN_ERROR;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>四步里有三个细节决定了后续路径：</p><ul><li><strong><code>mFrameIndex++</code>给的就是配对凭证</strong>。HAL 回来时只带这个 frameIndex，BufferChannel 拿它和发出去时记下的映射表对一下，才能找回当时是哪个 input buffer、对应哪个 output slot。</li><li><strong><code>releaseBuffer</code>不释放内存</strong>。它做的事是把<code>MediaCodecBuffer</code>切成<code>C2Buffer</code>视图——底层是同一块 ION/dmabuf，<code>MediaCodecBuffer</code>是给 Java/Native 客户端读写用的视图，<code>C2Buffer</code>是给 HAL 跨进程用的视图。第二个参数<code>release=false</code>表示这次只是”借”出去给 HAL，不归还 slot。</li><li><strong><code>comp-&gt;queue()</code>是 fire-and-forget</strong>。Binder 调过去就返回，不等结果。HAL 处理完通过 Listener 回调<code>onWorkDone</code>，在另一条线程上。这条 fire-and-forget 是状态机能保持流畅的关键——<code>queueInputBuffer</code>不会被 vendor 慢解码拖住。</li></ul><h2 id="输出路径：onWorkDone"><a href="#输出路径：onWorkDone" class="headerlink" title="输出路径：onWorkDone"></a>输出路径：onWorkDone</h2><p>vendor 解完一帧，通过 Listener 回调到 BufferChannel：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">CCodecBufferChannel::onWorkDone</span><span class="params">(std::list&lt;std::unique_ptr&lt;C2Work&gt;&gt; workItems)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">auto</span> &amp;work : workItems) &#123;</span><br><span class="line">        <span class="built_in">handleWork</span>(std::<span class="built_in">move</span>(work), outputFormat, initData);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">feedInputBufferIfAvailable</span>();   <span class="comment">// 顺便看 input 还有没有空 slot 可以喂</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">CCodecBufferChannel::handleWork</span><span class="params">(std::unique_ptr&lt;C2Work&gt; work, ...)</span> </span>&#123;</span><br><span class="line">    <span class="type">const</span> <span class="keyword">auto</span> &amp;worklet = work-&gt;worklets.<span class="built_in">front</span>();</span><br><span class="line">    <span class="type">const</span> <span class="keyword">auto</span> &amp;output  = worklet-&gt;output;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (output.buffers.<span class="built_in">size</span>() &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">// 1. 拿到解码后的 C2Buffer（YUV graphic / 压缩 NAL linear）</span></span><br><span class="line">        std::shared_ptr&lt;C2Buffer&gt; buffer = output.buffers[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2. 反向切视图：注册到 OutputBuffers，分配 index 和 MediaCodecBuffer</span></span><br><span class="line">        sp&lt;MediaCodecBuffer&gt; outBuffer;</span><br><span class="line">        <span class="type">size_t</span> index;</span><br><span class="line">        mOutput.<span class="built_in">lock</span>()-&gt;buffers-&gt;<span class="built_in">registerBuffer</span>(buffer, &amp;index, &amp;outBuffer);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3. 回调上层</span></span><br><span class="line">        mCallback-&gt;<span class="built_in">onOutputBufferAvailable</span>(index, outBuffer);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里的<code>mCallback-&gt;onOutputBufferAvailable</code>容易和 Java 层的<code>MediaCodec.Callback.onOutputBufferAvailable</code>同名打架——两个不是同一个东西：</p><ul><li>这个<code>mCallback</code>类型是<code>BufferCallback</code>，是<code>BufferChannel → MediaCodec</code>的<strong>内部回调</strong>。无论同步还是异步模式都会触发。</li><li>它干的事只有一件：把<code>index / buffer</code>包成<code>kWhatDrainThisBuffer</code>消息，丢到<code>MediaCodec</code>自己的 Looper。</li></ul><p>同步与异步的分叉发生在<code>MediaCodec</code>处理<code>kWhatDrainThisBuffer</code>时：</p><div class="table-container"><table><thead><tr><th>模式</th><th>行为</th></tr></thead><tbody><tr><td><strong>异步</strong>（用户调过<code>setCallback</code>）</td><td>调<code>MediaCodec::onOutputBufferAvailable()</code> → 投<code>kWhatCallbackNotify</code> → 派发到 Java 端用户<code>Callback.onOutputBufferAvailable</code></td></tr><tr><td><strong>同步</strong>（用户在轮询<code>dequeueOutputBuffer</code>）</td><td>当前正好有线程阻塞在<code>dequeueOutputBuffer</code>就直接 reply 唤醒它；没有就把 index 暂存进可用队列，下一次<code>dequeueOutputBuffer</code>取走</td></tr></tbody></table></div><p>第三篇里讲过<code>dequeueOutputBuffer</code>怎么用<code>PostAndAwaitResponse</code>阻塞 + 条件变量唤醒——<code>onWorkDone</code>这条路径正好对接到那一头：HAL 回调线程把 buffer 写进可用队列后，给阻塞中的<code>dequeueOutputBuffer</code>线程发 reply，那边醒过来拿到 index 返回 Java。两层联动起来才是完整的同步语义。</p><h2 id="渲染路径：renderOutputBuffer"><a href="#渲染路径：renderOutputBuffer" class="headerlink" title="渲染路径：renderOutputBuffer"></a>渲染路径：renderOutputBuffer</h2><p>Java 侧调<code>releaseOutputBuffer(index, true)</code>走的就是这一段：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">status_t</span> <span class="title">CCodecBufferChannel::renderOutputBuffer</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="type">const</span> sp&lt;MediaCodecBuffer&gt; &amp;buffer, <span class="type">int64_t</span> timestampNs)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 反向切视图：从 MediaCodecBuffer 拿回 C2Buffer，并把这个 slot 还给 OutputBuffers</span></span><br><span class="line">    std::shared_ptr&lt;C2Buffer&gt; c2Buffer;</span><br><span class="line">    mOutput.<span class="built_in">lock</span>()-&gt;buffers-&gt;<span class="built_in">releaseBuffer</span>(buffer, &amp;c2Buffer, <span class="comment">/*release*/</span> <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 提取 HDR 元数据（C2StreamHdrStaticInfo / C2StreamHdr10PlusInfo）</span></span><br><span class="line">    HdrStaticInfo  hdrStatic;</span><br><span class="line">    HdrDynamicInfo hdrDynamic;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 从 C2 ColorAspects 读出 dataspace / crop / transform</span></span><br><span class="line">    android_dataspace dataSpace = <span class="comment">/* ... */</span>;</span><br><span class="line">    Rect              cropRect  = <span class="comment">/* ... */</span>;</span><br><span class="line">    <span class="type">uint32_t</span>          transform = <span class="comment">/* ... */</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4. 装进 BufferQueue 的 QueueBufferInput</span></span><br><span class="line">    <span class="function">IGraphicBufferProducer::QueueBufferInput <span class="title">input</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">        timestampNs,</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="comment">/*isAutoTimestamp=*/</span><span class="literal">false</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">        dataSpace,</span></span></span><br><span class="line"><span class="params"><span class="function">        cropRect,</span></span></span><br><span class="line"><span class="params"><span class="function">        NATIVE_WINDOW_SCALING_MODE_SCALE_TO_WINDOW,</span></span></span><br><span class="line"><span class="params"><span class="function">        transform,</span></span></span><br><span class="line"><span class="params"><span class="function">        Fence::NO_FENCE)</span></span>;     <span class="comment">// 也可能从 C2Fence 提取</span></span><br><span class="line">    input.<span class="built_in">setHdrMetadata</span>(hdrMetadata);</span><br><span class="line">    input.<span class="built_in">setSurfaceDamage</span>(<span class="built_in">Region</span>());</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 5. 投递给 Surface（也就是 BufferQueue 的 Producer 端）</span></span><br><span class="line">    IGraphicBufferProducer::QueueBufferOutput output;</span><br><span class="line">    mOutputSurface-&gt;<span class="built_in">queueBuffer</span>(slot, input, &amp;output);</span><br><span class="line">    <span class="keyword">return</span> OK;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这一步是 MediaCodec 系列与图形管线系列的衔接点：左手收 vendor 解出来的 YUV <code>GraphicBuffer</code>，右手以 Producer 身份投给 SurfaceFlinger。HDR/crop/transform/fence 这些 side band 看起来杂，背后逻辑只有一句——BufferQueue 那边的 Consumer 不读<code>C2Buffer</code>，所有 vendor 想传给显示链的元信息，都得在这一步翻译成<code>QueueBufferInput</code>字段。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本篇沿着第三篇结尾的<code>mCodec</code>，往下钻了一层，讲清三件事：</p><ul><li><strong>CCodec 是怎么被选中的</strong>——名字以<code>c2.</code>开头就走<code>CreateCCodec()</code>，否则走<code>ACodec</code>；<code>MediaCodecList</code>的 owner 字段也能直接指定。</li><li><strong>CCodec 内部为什么再拆出 BufferChannel</strong>——变化频率、线程模型、可替换性三条理由都指向”控制面和数据面分开”。<code>MediaCodec</code>同时持有<code>mCodec</code>和<code>mBufferChannel</code>，控制走<code>mCodec</code>，每帧数据直接走<code>mBufferChannel</code>。</li><li><strong>一帧解码在 BufferChannel 上的三段路径</strong>——<code>queueInputBuffer</code>把<code>MediaCodecBuffer</code>切成<code>C2Buffer</code>视图、打<code>frameIndex</code>后 fire-and-forget 投给 HAL；<code>onWorkDone</code>收到 HAL 回填、反向切回<code>MediaCodecBuffer</code>，再交给<code>MediaCodec</code> Looper 决定走同步还是异步路径；<code>renderOutputBuffer</code>把 HDR/crop/transform 翻成<code>QueueBufferInput</code>，以 Producer 身份接到 BufferQueue。</li></ul><p>一句话收束：<code>CCodec</code>管开关，<code>CCodecBufferChannel</code>管搬运，<code>BufferChannelBase</code>是它们之间和 OMX 老路径之间唯一不变的契约——MediaCodec 在 native 这一侧的”控制 / 数据 / 抽象”三角，到这里就齐了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;系列导读&lt;/strong&gt;：第三篇把视角停在了&lt;code&gt;MediaCodec.cpp&lt;/code&gt;的状态机，所有动作最后都落在一个&lt;code&gt;sp&amp;lt;CodecBase&amp;gt; mCodec&lt;/code&gt;上。这一篇接着往下走，</summary>
      
    
    
    
    <category term="技术文章" scheme="http://julis.wang/categories/technology/"/>
    
    
    <category term="MediaCodec" scheme="http://julis.wang/tags/MediaCodec/"/>
    
  </entry>
  
  <entry>
    <title>MediaCodec 全链路深度剖析(三)：Native 消息机制与 JNI 桥接</title>
    <link href="http://julis.wang/2026/05/10/MediaCodec-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90-%E4%B8%89-%EF%BC%9ANative-%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6%E4%B8%8E-JNI-%E6%A1%A5%E6%8E%A5/"/>
    <id>http://julis.wang/2026/05/10/MediaCodec-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90-%E4%B8%89-%EF%BC%9ANative-%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6%E4%B8%8E-JNI-%E6%A1%A5%E6%8E%A5/</id>
    <published>2026-05-10T01:16:00.000Z</published>
    <updated>2026-05-10T01:29:09.026Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>系列导读</strong>：第二篇结尾留下一个钩子——<em>真正的状态机在 Native 层，Java 侧只是个薄壳</em>。这一篇沿着 <code>codec.start()</code>、<code>dequeueOutputBuffer()</code> 这种”看上去同步”的调用一路下钻，回答一个问题：</p><p><strong>Java 的同步调用，在 JNI 之下到底是怎么落到 Native、又是怎么”原地等回结果”的？</strong></p><p>主角是 AOSP <code>foundation</code> 库里那套 ALooper / AHandler / AMessage，加上 <code>android_media_MediaCodec.cpp</code> 这一层 JNI 桥。本篇刻意只聚焦<strong>同步 API 这一条链</strong>——异步 Callback（<code>setCallback / onOutputBufferAvailable</code>）不进行介绍。</p></blockquote><h2 id="1-为什么-Native-不复用-Java-的-Looper"><a href="#1-为什么-Native-不复用-Java-的-Looper" class="headerlink" title="1. 为什么 Native 不复用 Java 的 Looper"></a>1. 为什么 Native 不复用 Java 的 Looper</h2><p>翻开 <code>MediaCodec.cpp</code>，<code>start()</code> 长这样：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">status_t</span> <span class="title">MediaCodec::start</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    sp&lt;AMessage&gt; msg = <span class="keyword">new</span> <span class="built_in">AMessage</span>(kWhatStart, <span class="keyword">this</span>);</span><br><span class="line">    sp&lt;AMessage&gt; response;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">PostAndAwaitResponse</span>(msg, &amp;response);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>三行代码，三个问题：</p><ul><li><code>AMessage</code> 不是 <code>android.os.Message</code>，那是什么？</li><li>“同步调用”为什么长得像异步？</li><li>系统已经有 Java Looper 了，为什么还要在 Native 再造一套？</li></ul><p>第三个问题先回答：AOSP 的 <code>foundation</code> 库早于 ART 存在，很多系统组件（<code>mediaserver</code>、<code>cameraserver</code>）跑在根本没有 JVM 的进程里。Java 的 Handler/Looper 需要 ART 支撑，Native 用不了。所以 AOSP 在 <code>frameworks/av/media/libstagefright/foundation/</code> 下维护了自己的一套，名字都加了 <code>A</code> 前缀：</p><div class="table-container"><table><thead><tr><th>AOSP C++</th><th>Java 对应</th><th>作用</th></tr></thead><tbody><tr><td><code>ALooper</code></td><td><code>Looper</code></td><td>消息队列 + 循环线程</td></tr><tr><td><code>AHandler</code></td><td><code>Handler</code></td><td>接收回调的对象</td></tr><tr><td><code>AMessage</code></td><td><code>Message</code></td><td>消息本身，带 what 和 KV 参数</td></tr></tbody></table></div><p>两套有一个关键差异：<code>AMessage</code> 不用 <code>arg1 / arg2 / obj</code>，用的是<strong>字典</strong>——<code>setInt32(&quot;index&quot;, 3)</code>/<code>setInt64(&quot;timeUs&quot;, 100)</code>/<code>setObject(&quot;buffer&quot;, buffer)</code>。好处是参数多了不用扩字段，坏处是取值要查表。</p><p>更重要的另一个差异：<strong><code>AMessage</code> 原生支持同步等待，Java Handler 不支持</strong>。这是 <code>PostAndAwaitResponse</code> 能存在的基础。</p><h2 id="2-CodecLooper：每个-codec-实例独享一条线程"><a href="#2-CodecLooper：每个-codec-实例独享一条线程" class="headerlink" title="2. CodecLooper：每个 codec 实例独享一条线程"></a>2. CodecLooper：每个 codec 实例独享一条线程</h2><p>每个 <code>MediaCodec</code> 实例在 Native 侧独享一个 Looper。初始化时会看到：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line">mCodecLooper = <span class="keyword">new</span> ALooper;</span><br><span class="line">mCodecLooper-&gt;<span class="built_in">setName</span>(<span class="string">&quot;CodecLooper&quot;</span>);</span><br><span class="line">mCodecLooper-&gt;<span class="built_in">start</span>(</span><br><span class="line">    <span class="comment">/* runOnCallingThread = */</span> <span class="literal">false</span>,   <span class="comment">// 起新线程</span></span><br><span class="line">    <span class="comment">/* canCallJava        = */</span> <span class="literal">false</span>,</span><br><span class="line">    ANDROID_PRIORITY_AUDIO);            <span class="comment">// 优先级 -16</span></span><br><span class="line">mCodecLooper-&gt;<span class="built_in">registerHandler</span>(<span class="keyword">this</span>);    <span class="comment">// MediaCodec 自己是 AHandler</span></span><br></pre></td></tr></table></figure><p>三个细节值得记：</p><div class="table-container"><table><thead><tr><th>参数/特性</th><th>含义</th><th>为什么这么设</th></tr></thead><tbody><tr><td><code>canCallJava = false</code></td><td>这条线程不期望调 Java</td><td>真正要跨进 Java 的是 <code>JMediaCodec</code> 自带的另一条线程，两者职责分离</td></tr><tr><td><code>ANDROID_PRIORITY_AUDIO</code>（-16）</td><td>比普通前台线程高</td><td>音视频同步容不下 Looper 线程被挤占</td></tr><tr><td>一实例一 Looper</td><td>不共享</td><td>同进程开 4 个解码器就有 4 个 <code>CodecLooper</code>，每个状态机一条线程</td></tr></tbody></table></div><h2 id="3-PostAndAwaitResponse：同步感从哪里来"><a href="#3-PostAndAwaitResponse：同步感从哪里来" class="headerlink" title="3. PostAndAwaitResponse：同步感从哪里来"></a>3. PostAndAwaitResponse：同步感从哪里来</h2><p><code>start()</code> 内部投了一条消息就返回了，外层却”同步”拿到了结果。机制是这样的：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 调用方：投完消息就在 token 上睡觉</span></span><br><span class="line"><span class="built_in">PostAndAwaitResponse</span>(msg, &amp;response);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 处理方：onMessageReceived 里干完活 postReply 唤醒对方</span></span><br><span class="line">response-&gt;<span class="built_in">postReply</span>(replyToken);</span><br></pre></td></tr></table></figure><h3 id="3-1-AReplyToken：一次调用的”信箱”"><a href="#3-1-AReplyToken：一次调用的”信箱”" class="headerlink" title="3.1 AReplyToken：一次调用的”信箱”"></a>3.1 AReplyToken：一次调用的”信箱”</h3><p>核心数据结构叫 <code>AReplyToken</code>，一个调用对应一个，只有三个字段：</p><div class="table-container"><table><thead><tr><th>字段</th><th>作用</th></tr></thead><tbody><tr><td><code>mReply</code></td><td>处理方填进来的回复，初始为空</td></tr><tr><td><code>mReplied</code></td><td>是否已回复，调用方循环检查的断言</td></tr><tr><td><code>mLooper</code></td><td>归属的 Looper，不参与同步</td></tr></tbody></table></div><p>token 自己<strong>不带锁</strong>。整个进程里所有 token 共用一把全局锁和一个全局条件变量，都挂在 <code>gLooperRoster</code> 单例上。唤醒时用 broadcast，被唤醒的线程自己检查 <code>mReplied</code> 是不是轮到自己。</p><h3 id="3-2-核心逻辑"><a href="#3-2-核心逻辑" class="headerlink" title="3.2 核心逻辑"></a>3.2 核心逻辑</h3><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 调用方</span></span><br><span class="line"><span class="function"><span class="type">status_t</span> <span class="title">ALooper::awaitResponse</span><span class="params">(<span class="type">const</span> sp&lt;AReplyToken&gt; &amp;replyToken, sp&lt;AMessage&gt; *response)</span> </span>&#123;</span><br><span class="line">    <span class="function">Mutex::Autolock <span class="title">autoLock</span><span class="params">(mRepliesLock)</span></span>;   <span class="comment">// ① 锁住&quot;回复&quot;专用锁</span></span><br><span class="line">    <span class="built_in">CHECK</span>(replyToken != <span class="literal">NULL</span>);                <span class="comment">// ② 校验 token 合法</span></span><br><span class="line">    <span class="keyword">while</span> (!replyToken-&gt;<span class="built_in">retrieveReply</span>(response)) &#123;   <span class="comment">// ③ 循环尝试取回复</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="function">Mutex::Autolock <span class="title">autoLock</span><span class="params">(mLock)</span></span>;  <span class="comment">// ④ 加另一把锁，检查 Looper 状态</span></span><br><span class="line">            <span class="keyword">if</span> (mThread == <span class="literal">NULL</span>) &#123;</span><br><span class="line">                <span class="keyword">return</span> -ENOENT;               <span class="comment">// ⑤ Looper 已停止 → 失败返回</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        mRepliesCondition.<span class="built_in">wait</span>(mRepliesLock); <span class="comment">// ⑥ 条件变量阻塞，等待被唤醒</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> OK;                                <span class="comment">// ⑦ 成功拿到回复</span></span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 处理方（在 Looper 线程 onMessageReceived 内）</span></span><br><span class="line"><span class="function"><span class="type">status_t</span> <span class="title">ALooper::postReply</span><span class="params">(<span class="type">const</span> sp&lt;AReplyToken&gt; &amp;replyToken, <span class="type">const</span> sp&lt;AMessage&gt; &amp;reply)</span> </span>&#123;</span><br><span class="line">    <span class="function">Mutex::Autolock <span class="title">autoLock</span><span class="params">(mRepliesLock)</span></span>;</span><br><span class="line">    <span class="type">status_t</span> err = replyToken-&gt;<span class="built_in">setReply</span>(reply);     </span><br><span class="line">    <span class="keyword">if</span> (err == OK) &#123;</span><br><span class="line">        mRepliesCondition.<span class="built_in">broadcast</span>();              <span class="comment">// 唤醒</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> err;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">status_t</span> <span class="title">AReplyToken::setReply</span><span class="params">(<span class="type">const</span> sp&lt;AMessage&gt; &amp;reply)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (mReplied) &#123;</span><br><span class="line">        <span class="built_in">ALOGE</span>(<span class="string">&quot;trying to post a duplicate reply&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span> -EBUSY;</span><br><span class="line">    &#125;</span><br><span class="line">    mReply = reply;</span><br><span class="line">    mReplied = <span class="literal">true</span>;</span><br><span class="line">    <span class="keyword">return</span> OK;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>两个细节：</p><ul><li><strong>broadcast 而不是 signal</strong>：一把条件变量看守所有 token，被唤醒的线程要自己检查 <code>mReplied</code>，不是自己就接着睡</li><li><strong>while 而不是 if</strong>：<code>Condition::wait</code> 从睡眠中返回，不代表”回复一定到了”——一方面 POSIX 允许虚假唤醒（没有任何 <code>signal/broadcast</code> 也可能自己醒），另一方面这里用的是 <code>broadcast</code>，别人收到回复时我也会被一起叫醒。所以必须用 <code>while</code> 包住 <code>wait</code>，醒来后重新检查 <code>mReplied</code>，没轮到自己就继续睡</li></ul><h3 id="3-3-一次-codec-start-的完整时序"><a href="#3-3-一次-codec-start-的完整时序" class="headerlink" title="3.3 一次 codec.start() 的完整时序"></a>3.3 一次 <code>codec.start()</code> 的完整时序</h3><pre class="mermaid">sequenceDiagram    participant App as App 线程    participant JNI as JNI (JMediaCodec)    participant Q as Looper Queue    participant L as CodecLooper 线程    App->>JNI: native_start()    JNI->>JNI: new AMessage(kWhatStart)    JNI->>JNI: createReplyToken + setReplyToken    JNI->>Q: msg.post()    JNI->>JNI: wait on mRepliesCondition    Q->>L: 取出 msg    L->>L: onMessageReceived: 推状态机到 STARTED    L->>L: postReply: 写信箱 + broadcast    L-->>JNI: 唤醒    JNI->>JNI: 读 mReply 得到 status_t    JNI-->>App: start() 返回 OK</pre><p>注意整条链路上<strong>没有任何一把锁保护 <code>mState</code></strong>：所有入口（App 线程、底层回调线程、Looper 自己）都只 <code>post</code> 消息，状态机只在 CodecLooper 这一条线程上读写——这是后面状态机能”无锁”的根本原因，第四篇会展开。</p><h2 id="4-JMediaCodec：JNI-桥的薄壳"><a href="#4-JMediaCodec：JNI-桥的薄壳" class="headerlink" title="4. JMediaCodec：JNI 桥的薄壳"></a>4. JMediaCodec：JNI 桥的薄壳</h2><p>Java 侧 <code>codec.start()</code> 走的不是直接下 Native，而是经过一层 <code>JMediaCodec</code>：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Java: codec.start()</span><br><span class="line">  └─ native_start()                          // JNI 入口</span><br><span class="line">       └─ JMediaCodec::start()               // 持有 sp&lt;MediaCodec&gt;</span><br><span class="line">            └─ mCodec-&gt;start()               // 真正的 Native 实现</span><br><span class="line">                 └─ PostAndAwaitResponse     // 跨 Looper 线程</span><br></pre></td></tr></table></figure><h3 id="4-1-三层对象：谁是谁"><a href="#4-1-三层对象：谁是谁" class="headerlink" title="4.1 三层对象：谁是谁"></a>4.1 三层对象：谁是谁</h3><p>讨论”<code>codec.start()</code> 在 JNI 之下发生了什么”之前，先把整条链路上<strong>到底有几个对象、各自归谁管</strong>画清楚——这张图后面整篇文章都会反复用到：</p><pre class="mermaid">%%{init: {'flowchart': {'subGraphTitleMargin': {'top': 14, 'bottom': 14}, 'nodeSpacing': 50, 'rankSpacing': 70}}}%%flowchart LR    subgraph J["<b>Java 层（ART）</b>"]        JM["MediaCodec.java<br/><i>mNativeContext: long</i>"]    end    subgraph B["<b>JNI 桥</b><br/>android_media_MediaCodec.cpp"]        JNI["JMediaCodec<br/><i>: public RefBase</i><br/>sp&lt;MediaCodec&gt; mCodec"]    end    subgraph N["<b>Native 层（libstagefright）</b>"]        MC["MediaCodec.cpp<br/><i>真正的状态机</i><br/>持有 CodecLooper"]    end    JM -. "mNativeContext<br/>存 JMediaCodec*" .-> JNI    JNI == "sp&lt;&gt; 强引用" ==> MC    classDef java fill:#BBDEFB,stroke:#1565C0,stroke-width:2px,color:#0D47A1    classDef jni fill:#FFF59D,stroke:#F57F17,stroke-width:2px,color:#3E2723    classDef native fill:#FFCC80,stroke:#E65100,stroke-width:2px,color:#BF360C    class JM java    class JNI jni    class MC native    style J fill:#E3F2FD,stroke:#1976D2,stroke-width:1.5px,color:#0D47A1    style B fill:#FFF9C4,stroke:#F9A825,stroke-width:1.5px,color:#5D4037    style N fill:#FFE0B2,stroke:#EF6C00,stroke-width:1.5px,color:#BF360C</pre><p>三个对象，一一对应不会变：</p><div class="table-container"><table><thead><tr><th>层</th><th>对象</th><th>归属</th><th>职责</th></tr></thead><tbody><tr><td>Java</td><td><code>MediaCodec.java</code></td><td>ART / GC 管理</td><td>用户面对的 API；只是个壳</td></tr><tr><td>JNI</td><td><code>JMediaCodec</code></td><td><code>sp&lt;&gt;</code> 引用计数管理</td><td>翻译 Java 调用 ↔ Native 调用</td></tr><tr><td>Native</td><td><code>MediaCodec.cpp</code></td><td>由 <code>JMediaCodec::mCodec</code> 强引用</td><td>真正的状态机、CodecLooper、<code>PostAndAwaitResponse</code></td></tr></tbody></table></div><p>绑定关系靠 Java 侧的一个 <code>private long mNativeContext</code> 字段——里面存的就是 <code>JMediaCodec*</code> 指针的整数值（64-bit 系统下 8 字节）。<code>native_setup</code> 时通过 <code>SetLongField</code> 写入；之后每个 JNI 入口的第一步都是 <code>getMediaCodec(env, thiz)</code> 把这个 long 强转回 <code>JMediaCodec*</code> 用：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line">sp&lt;JMediaCodec&gt; codec = <span class="built_in">getMediaCodec</span>(env, thiz);</span><br><span class="line"><span class="keyword">if</span> (codec == <span class="literal">NULL</span>) &#123;</span><br><span class="line">    <span class="built_in">throwExceptionAsNecessary</span>(env, INVALID_OPERATION);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br><span class="line">codec-&gt;<span class="built_in">start</span>();   <span class="comment">// 同步路径上 JNI 做的几乎就是这一件事</span></span><br></pre></td></tr></table></figure><p>整个 <code>android_media_MediaCodec.cpp</code> 里 95% 的 JNI 入口都是这个模板：取 <code>JMediaCodec</code> → 调一次方法 → 把 <code>status_t</code> 翻成 Java 异常或返回值。本篇主线讲的”Java 同步调用怎么落到 Native”，<strong>这一层就是承接点，但本身没有任何业务逻辑</strong>。</p><h3 id="4-2-唯一一把-sLock：与前文”无锁”形成对照"><a href="#4-2-唯一一把-sLock：与前文”无锁”形成对照" class="headerlink" title="4.2 唯一一把 sLock：与前文”无锁”形成对照"></a>4.2 唯一一把 sLock：与前文”无锁”形成对照</h3><p>第 3 节强调过：Native 那一层的 <code>mState</code> 是<strong>无锁</strong>的，依靠 CodecLooper 单线程串行避免竞争。但翻开 JNI 文件，会看到一把全局静态锁：</p><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> Mutex sLock;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">static</span> sp&lt;JMediaCodec&gt; <span class="title">getMediaCodec</span><span class="params">(JNIEnv *env, jobject thiz)</span> </span>&#123;</span><br><span class="line">    Mutex::Autolock _l(sLock);</span><br><span class="line">    <span class="keyword">return</span> (JMediaCodec*)env-&gt;<span class="built_in">GetLongField</span>(thiz, gFields.context);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">static</span> sp&lt;JMediaCodec&gt; <span class="title">setMediaCodec</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">        JNIEnv *env, jobject thiz, <span class="type">const</span> sp&lt;JMediaCodec&gt; &amp;codec)</span> </span>&#123;</span><br><span class="line">    Mutex::Autolock _l(sLock);</span><br><span class="line">    sp&lt;JMediaCodec&gt; old = (JMediaCodec*)env-&gt;<span class="built_in">GetLongField</span>(thiz, gFields.context);</span><br><span class="line">    <span class="keyword">if</span> (codec != <span class="literal">NULL</span>)  codec-&gt;<span class="built_in">incStrong</span>(thiz);</span><br><span class="line">    <span class="keyword">if</span> (old != <span class="literal">NULL</span>)    old-&gt;<span class="built_in">decStrong</span>(thiz);</span><br><span class="line">    env-&gt;<span class="built_in">SetLongField</span>(thiz, gFields.context, (jlong)codec.<span class="built_in">get</span>());</span><br><span class="line">    <span class="keyword">return</span> old;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为什么 Native 都做到无锁了，JNI 这一薄层反而要加锁？因为这两层面对的<strong>调用者根本不一样</strong>：</p><div class="table-container"><table><thead><tr><th>层</th><th>调用方</th><th>是否需要锁</th></tr></thead><tbody><tr><td>Native <code>MediaCodec.cpp</code> 的 <code>mState</code></td><td>只有 CodecLooper 一条线程能改</td><td>不需要</td></tr><tr><td>JNI <code>mNativeContext</code> 字段</td><td><strong>任意 Java 线程</strong>都能直接调 JNI 入口</td><td>必须加锁</td></tr></tbody></table></div><p>具体的并发场景就是 <code>release</code> vs 其他方法。假设没有 <code>sLock</code>，两条线程会这样交错：</p><pre class="mermaid">sequenceDiagram    participant A as 线程 A（release）    participant F as mNativeContext<br/>（Java 字段）    participant B as 线程 B（queueInputBuffer）    A->>F: 读出 old 指针    B->>F: 读出同一个 old 指针    A->>A: old->decStrong()<br/>JMediaCodec 析构    A->>F: 写入 nullptr    Note over B: B 手里的指针已经悬空    B->>B: codec->queueInputBuffer(...)<br/>use-after-free</pre><p>问题点很清楚：A 还没把字段清空、对象还没析构之前，B 已经把指针读走了；等 A 析构完，B 手上那份指针就成了野指针。</p><p><code>sLock</code> 把 <code>getMediaCodec</code> 和 <code>setMediaCodec</code> 串起来后，B 只可能看到两种结果：</p><ul><li><strong>要么</strong> A 还没开始：B 拿到一个<strong>完整的</strong> <code>sp&lt;JMediaCodec&gt;</code>，<code>sp&lt;&gt;</code> 一旦构造就持有强引用，A 那边的 <code>decStrong</code> 至多让引用计数减 1，对象不会在 B 手里被析构；</li><li><strong>要么</strong> A 已经做完：B 拿到 <code>nullptr</code>，提前返回错误。</li></ul><p>两种结果都安全，<strong>不会出现”拿到一个正在死的对象”</strong> 这种中间态。</p><p>注意这把锁<strong>只保护 <code>mNativeContext</code> 这一个 8 字节字段</strong>——一进一出粒度极小，对吞吐几乎没影响。真正的状态机仍然在下层用消息驱动维持无锁。可以这样记：</p><blockquote><p>JNI 这一层的 sLock，护的是”Java 对象 ↔ Native 对象的指针绑定”；<br>Native 那一层的无锁，靠的是”所有改动都走 Looper 串行”。<br>两层各管各的，组合起来就是 MediaCodec 那种”看上去多线程随便调，实际从不出竞争”的体验。</p></blockquote><p>（<code>JMediaCodec</code> 还承担第二件事——把 Native 事件反向转回 Java Callback——那是异步 API 的路径，涉及 Global Ref、<code>AttachCurrentThread</code>、<code>jmethodID</code> 缓存等一整套 JNI 跨语言回调技巧，本篇不展开。）</p><h2 id="5-一次完整闭环：dequeueOutputBuffer"><a href="#5-一次完整闭环：dequeueOutputBuffer" class="headerlink" title="5. 一次完整闭环：dequeueOutputBuffer"></a>5. 一次完整闭环：dequeueOutputBuffer</h2><p>看一次 <code>dequeueOutputBuffer</code> 这个<strong>带 timeout 的同步 API</strong> 的完整来回，把前面所有零件串起来：</p><pre class="mermaid">sequenceDiagram    participant App as App 线程    participant JNI as JMediaCodec    participant MC as MediaCodec.cpp    participant CL as CodecLooper    App->>JNI: dequeueOutputBuffer(timeout)    JNI->>MC: mCodec->dequeueOutputBuffer    MC->>CL: post kWhatDequeueOutputBuffer    MC->>MC: PostAndAwaitResponse 阻塞    Note over CL: 队列里没有可用 buffer 就先不回    Note right of CL: 底层数据准备好后<br/>另一条消息入队    CL->>CL: 把 buffer 放进 mAvailPortBuffers    CL->>CL: 取出之前 pending 的 dequeue msg 处理    CL->>MC: postReply（携带 index）    MC-->>JNI: 条件变量唤醒    JNI-->>App: 返回 index</pre><p>两条关键：</p><ul><li><strong>带 timeout 的同步等待</strong>：<code>PostAndAwaitResponse</code> 内部走条件变量的 timed wait——超时直接返回 <code>INFO_TRY_AGAIN_LATER</code>，不等到天荒地老</li><li><strong>消息按 FIFO 串行处理</strong>：dequeue msg 投早了没数据就先挂着，等数据到位的消息进来后被一并取出来回复。<strong>整条同步语义不依赖任何锁保护字段</strong>，全靠 CodecLooper 单线程串行 + token 唤醒</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本篇沿着一次”看上去同步”的 Java 调用下钻到 Native 侧，讲清了四件事：</p><ul><li><strong>AOSP 为什么自造 A 系列原语</strong>（ALooper / AHandler / AMessage），以及和 Java Handler 的关键差异——原生支持同步等待</li><li><strong>CodecLooper</strong> 如何用”每实例一条线程串行处理”换来状态机的天然无锁</li><li><strong>PostAndAwaitResponse</strong> 怎样通过 <code>AReplyToken</code> + 条件变量，把异步消息包装成 Java 看到的”同步调用”</li><li><strong>JMediaCodec / <code>sLock</code></strong> 为什么只锁 <code>mNativeContext</code> 一个字段，就能避免 <code>release</code> 与并发调用之间的 use-after-free</li></ul><p>一句话收束：<strong>下层 Looper 串行做到无锁，上层 JNI 只锁一个字段防悬空</strong>——这就是 MediaCodec 同步 API 在 Native 侧的全部秘密。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;系列导读&lt;/strong&gt;：第二篇结尾留下一个钩子——&lt;em&gt;真正的状态机在 Native 层，Java 侧只是个薄壳&lt;/em&gt;。这一篇沿着 &lt;code&gt;codec.start()&lt;/code&gt;、&lt;code&gt;dequeueOutpu</summary>
      
    
    
    
    
    <category term="MediaCodec" scheme="http://julis.wang/tags/MediaCodec/"/>
    
  </entry>
  
  <entry>
    <title>MediaCodec 全链路深度剖析(二)：Java 层 API 与状态机</title>
    <link href="http://julis.wang/2026/05/05/MediaCodec-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9AJava-%E5%B1%82-API-%E4%B8%8E%E7%8A%B6%E6%80%81%E6%9C%BA/"/>
    <id>http://julis.wang/2026/05/05/MediaCodec-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9AJava-%E5%B1%82-API-%E4%B8%8E%E7%8A%B6%E6%80%81%E6%9C%BA/</id>
    <published>2026-05-05T13:40:00.000Z</published>
    <updated>2026-05-07T13:59:43.984Z</updated>
    
    <content type="html"><![CDATA[<p><strong>系列导读</strong>：主系列从”一帧像素怎么走完六层架构”切入，偏底层。但在开挖之前，得先让读者<strong>站在 App 工程师的视角</strong>，把 <code>MediaCodec</code> 的 Java API 摸清楚——状态机有哪些、常用 API 各自扮演什么角色、哪些坑踩了会直接 crash。</p><h2 id="一句话理解-MediaCodec"><a href="#一句话理解-MediaCodec" class="headerlink" title="一句话理解 MediaCodec"></a>一句话理解 MediaCodec</h2><p><code>MediaCodec</code> 是一台<strong>带缓冲池的状态机</strong>：</p><ul><li><strong>状态机</strong>决定”你现在能做什么”——错了就抛 <code>IllegalStateException</code>；</li><li><strong>缓冲池</strong>是 App 与底层 codec 之间的传送带——你租一个 buffer、填数据、还回去；底层处理完再租一个 buffer、拿数据、你还回去。</li></ul><h2 id="MediaCodec-状态机"><a href="#MediaCodec-状态机" class="headerlink" title="MediaCodec 状态机"></a>MediaCodec 状态机</h2><p>用一张官方的图来展示 MediaCodec 的声明周期：<br><img src="https://developer.android.com/images/media/mediacodec_states.svg" alt=""></p><blockquote><p>当通过任一工厂方法创建编解码器时，编解码器处于未初始化状态。首先需要通过 configure(…) 完成配置，该方法会将其切换至已配置状态；随后调用 start()，便可进入执行状态。在执行状态下，你就可以通过前文所述的缓冲区队列操作来进行数据处理。</p><p>执行状态包含三个子状态：刷新态（Flushed）、运行态（Running）和流结束态（End-of-Stream）。调用 start() 后，解码器会立即进入刷新子状态，此时它持有所有缓冲区。一旦取出第一个输入缓冲区，编解码器就会切换至运行子状态，这也是其最主要的工作状态。当你向输入缓冲区带入流结束标记并送入队列时，编解码器会切换到流结束子状态。在此状态下，编解码器不再接收新的输入缓冲区，但仍会持续生成输出缓冲区，直到输出端数据流收尾完成。对于解码器而言，只要处于执行状态下，随时都可以调用 flush() 回到刷新子状态。</p><p>调用 stop() 可将编解码器回归未初始化状态，之后可重新对其进行配置。使用完毕后，必须调用 release() 释放编解码器资源。</p></blockquote><p>详细的内容可以查看<a href="https://developer.android.com/reference/android/media/MediaCodec#states">MediaCodec#states</a>一节。</p><h3 id="完整状态图"><a href="#完整状态图" class="headerlink" title="完整状态图"></a>完整状态图</h3><pre class="mermaid">stateDiagram-v2    [*] --> Uninitialized: new / createByXxx()    Uninitialized --> Configured: configure()    Uninitialized --> Released: release()    Configured --> Uninitialized: reset()    Configured --> Released: release()    Configured --> Executing: start()    state Executing {        [*] --> Flushed        Flushed --> Running: queueInputBuffer()        Running --> Running: queue/dequeue        Running --> EndOfStream: queueInputBuffer(EOS)        EndOfStream --> Flushed: flush()        Running --> Flushed: flush()        Flushed --> Flushed: flush()    }    Executing --> Uninitialized: stop()    Executing --> Released: release()    Executing --> Error: 任何调用失败    Error --> Uninitialized: reset()    Error --> Released: release()    Released --> [*]</pre><p>一个被忽略的细节：真正的状态机在 <strong>Native 层</strong> (<code>MediaCodec.cpp</code>)，Java 侧只是个”薄壳”。这也是为什么 <code>MediaCodec.CodecException</code> 会带上 <code>errorCode</code>——错误是从 native 甚至 HAL 层反射上来的。</p><hr><h2 id="一个最小闭环-MediaCodec-示例（解码-渲染到-Surface，同步模式）"><a href="#一个最小闭环-MediaCodec-示例（解码-渲染到-Surface，同步模式）" class="headerlink" title="一个最小闭环 MediaCodec 示例（解码 + 渲染到 Surface，同步模式）"></a>一个最小闭环 MediaCodec 示例（解码 + 渲染到 Surface，同步模式）</h2><p>先给个能跑的代码，脑子里有个整体印象再往下看，后续所有 native 的相关的解释都与下面的代码有关。</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> extractor = MediaExtractor().apply &#123; setDataSource(path) &#125;</span><br><span class="line"><span class="keyword">val</span> trackIdx = (<span class="number">0</span> until extractor.trackCount).first &#123;</span><br><span class="line">    extractor.getTrackFormat(it).getString(MediaFormat.KEY_MIME)!!.startsWith(<span class="string">&quot;video/&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">extractor.selectTrack(trackIdx)</span><br><span class="line"><span class="keyword">val</span> format = extractor.getTrackFormat(trackIdx)</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!).apply &#123;</span><br><span class="line">    configure(format, outputSurface, <span class="literal">null</span>, <span class="number">0</span>)   <span class="comment">// 渲染到 Surface</span></span><br><span class="line">    start()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> info = MediaCodec.BufferInfo()</span><br><span class="line"><span class="keyword">var</span> inputDone = <span class="literal">false</span></span><br><span class="line"><span class="keyword">val</span> startNs = System.nanoTime()</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">    <span class="comment">// 1. 喂输入</span></span><br><span class="line">    <span class="keyword">if</span> (!inputDone) &#123;</span><br><span class="line">        <span class="keyword">val</span> inIdx = codec.dequeueInputBuffer(<span class="number">10_000</span>)</span><br><span class="line">        <span class="keyword">if</span> (inIdx &gt;= <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">val</span> buf = codec.getInputBuffer(inIdx)!!</span><br><span class="line">            <span class="keyword">val</span> size = extractor.readSampleData(buf, <span class="number">0</span>)</span><br><span class="line">            <span class="keyword">if</span> (size &lt; <span class="number">0</span>) &#123;</span><br><span class="line">                codec.queueInputBuffer(inIdx, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, MediaCodec.BUFFER_FLAG_END_OF_STREAM)</span><br><span class="line">                inputDone = <span class="literal">true</span></span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                codec.queueInputBuffer(inIdx, <span class="number">0</span>, size, extractor.sampleTime, <span class="number">0</span>)</span><br><span class="line">                extractor.advance()</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 取输出</span></span><br><span class="line">    <span class="keyword">when</span> (<span class="keyword">val</span> outIdx = codec.dequeueOutputBuffer(info, <span class="number">10_000</span>)) &#123;</span><br><span class="line">        MediaCodec.INFO_TRY_AGAIN_LATER -&gt; &#123; <span class="comment">/* 继续转 */</span> &#125;</span><br><span class="line">        MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -&gt; &#123;</span><br><span class="line">            Log.d(TAG, <span class="string">&quot;output format = <span class="subst">$&#123;codec.outputFormat&#125;</span>&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> -&gt; <span class="keyword">if</span> (outIdx &gt;= <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment">// 按 PTS 精确送显，实现音画同步</span></span><br><span class="line">            <span class="keyword">val</span> renderAtNs = startNs + info.presentationTimeUs * <span class="number">1000L</span></span><br><span class="line">            codec.releaseOutputBuffer(outIdx, renderAtNs)</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != <span class="number">0</span>) <span class="keyword">break</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">codec.stop(); codec.release()</span><br><span class="line">extractor.release()</span><br></pre></td></tr></table></figure><p>不到 40 行，就是一个能用的硬解播放内核。编码器的骨架和它镜像——把 extractor 换成 GPU 画 Surface，把 releaseOutputBuffer 换成 MediaMuxer 写流。</p><hr><h2 id="API-全景：一个生命周期流水线"><a href="#API-全景：一个生命周期流水线" class="headerlink" title="API 全景：一个生命周期流水线"></a>API 全景：一个生命周期流水线</h2><p>把所有常用 API 按调用顺序串起来，就是下面这张流水线：</p><pre class="mermaid">flowchart TB    S1["<b>1. 创建</b><br/>createDecoderByType / createEncoderByType<br/>createByCodecName"]    S2["<b>2. 注册回调</b>（可选）<br/>setCallback<br/><i>异步模式必调，且必须在 configure 之前</i>"]    S3["<b>3. 配置</b><br/>configure(format, surface, crypto, flags)"]    S4["<b>4. 启动</b><br/>start()"]    subgraph LOOP["<b>5. 运行循环</b>"]        direction LR        IN["<b>喂输入</b><br/>dequeueInputBuffer<br/>↓<br/>getInputBuffer<br/>↓<br/>queueInputBuffer"]        OUT["<b>取输出</b><br/>dequeueOutputBuffer<br/>↓<br/>getOutputBuffer<br/>↓<br/>releaseOutputBuffer"]        MID["<b>中途可调</b><br/>flush · setParameters<br/>getOutputFormat<br/>signalEndOfInputStream"]        IN -.并行.- OUT        OUT -.-> MID    end    S6["<b>6. 清理</b><br/>stop() / reset() / release()"]    S1 --> S2 --> S3 --> S4 --> LOOP --> S6    classDef phase fill:#E3F2FD,stroke:#1976D2,stroke-width:2px,color:#000    classDef ioBox fill:#FFF9C4,stroke:#F9A825,stroke-width:1.5px,color:#000    classDef midBox fill:#FFE0B2,stroke:#EF6C00,stroke-width:1.5px,color:#000    classDef endBox fill:#FFCDD2,stroke:#C62828,stroke-width:2px,color:#000    class S1,S2,S3,S4 phase    class IN,OUT ioBox    class MID midBox    class S6 endBox</pre><h2 id="Flush-Stop-Reset-Release"><a href="#Flush-Stop-Reset-Release" class="headerlink" title="Flush / Stop / Reset / Release"></a>Flush / Stop / Reset / Release</h2><div class="table-container"><table><thead><tr><th>API</th><th style="text-align:center">丢 buffer</th><th style="text-align:center">保留 configure</th><th style="text-align:center">需要重新 configure</th><th style="text-align:center">之后还能用</th></tr></thead><tbody><tr><td><code>flush()</code></td><td style="text-align:center">✅</td><td style="text-align:center">✅</td><td style="text-align:center"></td><td style="text-align:center">✅ 直接 queue</td></tr><tr><td><code>stop()</code></td><td style="text-align:center">✅</td><td style="text-align:center"></td><td style="text-align:center">✅</td><td style="text-align:center">✅ configure + start</td></tr><tr><td><code>reset()</code></td><td style="text-align:center">✅</td><td style="text-align:center"></td><td style="text-align:center">✅</td><td style="text-align:center">✅ configure + start（Error 可调）</td></tr><tr><td><code>release()</code></td><td style="text-align:center">✅</td><td style="text-align:center"></td><td style="text-align:center">—</td><td style="text-align:center">❌ 对象已死</td></tr></tbody></table></div><hr><h2 id="同步-vs-异步模式"><a href="#同步-vs-异步模式" class="headerlink" title="同步 vs 异步模式"></a>同步 vs 异步模式</h2><div class="table-container"><table><thead><tr><th></th><th><strong>同步模式</strong></th><th><strong>异步模式</strong></th></tr></thead><tbody><tr><td>喂/取 buffer</td><td><code>dequeueInputBuffer</code> / <code>dequeueOutputBuffer</code> 轮询</td><td><code>onInputBufferAvailable</code> / <code>onOutputBufferAvailable</code> 回调</td></tr><tr><td>错误处理</td><td>catch <code>CodecException</code></td><td><code>onError(codec, e)</code> 回调</td></tr><tr><td>何时选</td><td>串行逻辑、能接受阻塞循环</td><td>低延迟播放、编码器管线、UI 友好</td></tr><tr><td>共同点</td><td>两种都要处理 <code>INFO_OUTPUT_FORMAT_CHANGED</code>（异步里是 <code>onOutputFormatChanged</code>）</td><td>同</td></tr></tbody></table></div><p><strong>规则很硬</strong>：setCallback 之后，同步的 dequeueXxx 就禁用了，调一次抛一次 IllegalStateException。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>回到开篇那句话——<strong>MediaCodec 是一台带缓冲池的状态机</strong>。</p><p><strong>状态机</strong>决定你”现在能调什么”：<code>configure → start → queue/dequeue → stop/release</code> 是主干，<code>flush</code> 在 Executing 内部腾挪，<code>reset/release</code> 负责兜底和终结。调错顺序 → <code>IllegalStateException</code>。<br><strong>缓冲池</strong>决定你”怎么交换数据”：<code>dequeue → get → queue/release</code> 是三步闭环，每租必还，PTS 单调，EOS 用 flag 而不是 API。忘了 release → buffer 枯竭卡死。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;strong&gt;系列导读&lt;/strong&gt;：主系列从”一帧像素怎么走完六层架构”切入，偏底层。但在开挖之前，得先让读者&lt;strong&gt;站在 App 工程师的视角&lt;/strong&gt;，把 &lt;code&gt;MediaCodec&lt;/code&gt; 的 Java API 摸清楚——状态机有哪</summary>
      
    
    
    
    
    <category term="MediaCodec" scheme="http://julis.wang/tags/MediaCodec/"/>
    
  </entry>
  
  <entry>
    <title>MediaCodec 全链路深度剖析(一)：开篇 —— 当你点下&quot;播放&quot;，究竟发生了什么？</title>
    <link href="http://julis.wang/2026/04/30/MediaCodec-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E5%BC%80%E7%AF%87-%E2%80%94%E2%80%94-%E5%BD%93%E4%BD%A0%E7%82%B9%E4%B8%8B-%E6%92%AD%E6%94%BE-%EF%BC%8C%E7%A9%B6%E7%AB%9F%E5%8F%91%E7%94%9F%E4%BA%86%E4%BB%80%E4%B9%88%EF%BC%9F/"/>
    <id>http://julis.wang/2026/04/30/MediaCodec-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E5%BC%80%E7%AF%87-%E2%80%94%E2%80%94-%E5%BD%93%E4%BD%A0%E7%82%B9%E4%B8%8B-%E6%92%AD%E6%94%BE-%EF%BC%8C%E7%A9%B6%E7%AB%9F%E5%8F%91%E7%94%9F%E4%BA%86%E4%BB%80%E4%B9%88%EF%BC%9F/</id>
    <published>2026-04-30T13:21:00.000Z</published>
    <updated>2026-05-02T02:45:54.045Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>系列导读</strong>：本系列将从 Kotlin/Java 应用层出发，一路向下，穿越 JNI、Native framework、Codec2、HAL、驱动，直至 SoC 内部的 VPU/GPU/NPU 硬件电路，完整拆解一帧视频从<strong>磁盘到屏幕</strong>的代码链路与数据流转。</p><p>本文是<strong>第 1 篇 · 开篇总览</strong>，不涉及具体代码细节，目的是建立一张「全景地图」，让你知道自己在下面哪一层、该往哪个方向挖。</p></blockquote><hr><h2 id="一、为什么要写这个系列"><a href="#一、为什么要写这个系列" class="headerlink" title="一、为什么要写这个系列"></a>一、为什么要写这个系列</h2><p>过去这些年，我对 <code>MediaCodec</code> 的理解基本停留在 <strong>API 使用层面</strong>——知道怎么在多个线程间（音频解码线程、视频解码线程、UI线程）串起整个流程，能理解与 MediaExtractor、AudioTrack、MediaMuxer 之间的协作，也了解 H264 的理论原理相关，但一旦有比较深入地问下去，我就答不上来了：</p><ul><li><code>queueInputBuffer</code> 之后，那段 H.264 码流到底经过了几个进程？</li><li>“硬解”到底是谁在解？<code>ACodec</code>、<code>CCodec</code>、<code>OMX</code>、<code>Codec2</code> 它们是什么关系、又是怎么演化出来的？</li><li>为什么 <code>Surface</code> 输出可以”零拷贝”？这个”零”是真的零，还是营销话术？</li><li><code>DMA-BUF</code>、<code>fence</code>、<code>Gralloc</code>、<code>BufferQueue</code> 这些名词我都听过，但它们到底怎么咬合在一起？</li><li>走到最底下，<code>VPU</code> 这颗硬件电路，是怎么被 App 的一行 Kotlin 代码”遥控”起来的？</li></ul><p>这些问题或者遇到疑难 bug（偶现花屏、特定机型掉帧、跨设备兼容性），没有底层视角，只能靠猜；遇到性能优化，没有全链路认知，也只能做一些表面功夫。</p><p>去翻市面上的 <code>MediaCodec</code> 文章，大多数是”十分钟入门”的——贴一段 demo 代码、列一下状态机、讲讲同步 vs 异步模式，就结束了。这些内容对刚入门的人有用，但对已经写了几年音视频的人，<strong>信息密度低到几乎为零，而且彼此之间高度同质化</strong>。<br>真正能把「App 代码 → JNI → Native framework → HAL → 驱动 → 硬件」这条链路一路贯通的文章比较少。毕竟很少人对这所有的方面都了解，做底层硬件的可能不太了解上层的应用，做应用的不太了解硬件底层的逻辑。好在当前 AI 的发展帮忙能够比较好地去学习了解这些问题了，本系列文章很多资源内容也是借助 AI 搜集整理资料的能力完成的。</p><p><strong>本系列试图做的事</strong>：不讲解如何使用 MediaCodec，只讲解原理相关。把安卓音视频碎片串成一条完整的链路，从 App 代码一行一行地跟踪到硬件寄存器，<strong>知其然更知其所以然</strong>。</p><hr><h3 id="适合读这个系列的人"><a href="#适合读这个系列的人" class="headerlink" title="适合读这个系列的人"></a>适合读这个系列的人</h3><div class="table-container"><table><thead><tr><th>你是谁？</th><th>会收获什么</th></tr></thead><tbody><tr><td>写短视频/直播/播放器的 App 工程师</td><td>理解 MediaCodec API 背后的”陷阱”来源，写出更稳定的代码</td></tr><tr><td>做音视频 SDK / 特效引擎的开发者</td><td>明白 Surface、BufferQueue、EGLImage 的真实语义，设计零拷贝管线</td></tr><tr><td>做系统 / ROM / HAL 的工程师</td><td>把 Codec2、OMX、Gralloc、Fence 这些散点连成系统视图</td></tr><tr><td>对底层原理有好奇心的爱好者</td><td>看到一颗 SoC 内部 CPU/GPU/VPU/NPU 是如何协作的</td></tr></tbody></table></div><p><strong>前置要求</strong>：</p><ul><li>对音视频编码基础了解，强烈推荐这个项目提供的内容 <a href="https://github.com/leandromoreira/digital_video_introduction">digital_video_introduction</a></li><li>能读懂中等难度的 C/C++</li><li>熟悉 Kotlin/Java 语法，了解 MediaCodec 的基本使用</li><li>对”线程”、”IPC”、”进程”有基本概念</li></ul><hr><h3 id="一个直击灵魂的提问"><a href="#一个直击灵魂的提问" class="headerlink" title="一个直击灵魂的提问"></a>一个直击灵魂的提问</h3><p>在正式展开之前，先做一个思想实验。假设你写了如下最朴素的播放代码：</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> extractor = MediaExtractor().apply &#123; setDataSource(<span class="string">&quot;/sdcard/demo.mp4&quot;</span>) &#125;</span><br><span class="line"><span class="keyword">val</span> format = extractor.getTrackFormat(videoTrackIndex)</span><br><span class="line"><span class="keyword">val</span> codec  = MediaCodec.createDecoderByType(<span class="string">&quot;video/avc&quot;</span>).apply &#123;</span><br><span class="line">    configure(format, surfaceView.holder.surface, <span class="literal">null</span>, <span class="number">0</span>)</span><br><span class="line">    start()</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// ... queueInputBuffer / releaseOutputBuffer ...</span></span><br></pre></td></tr></table></figure><p>点击运行，屏幕上出现了画面。</p><p><strong>问题来了</strong>：从你调用 <code>releaseOutputBuffer(index, true)</code> 那一刻算起，到这一帧的像素真正点亮 LCD 的某一行，中间发生了<strong>多少次进程切换</strong>、<strong>多少次内存拷贝</strong>、<strong>多少次 CPU ↔ GPU ↔ VPU 协同</strong>？</p><p>答案是：</p><blockquote><p><strong>0 次像素拷贝（理想情况）、3 次跨进程、7 次关键跳跃、至少 4 种硬件单元参与。</strong></p></blockquote><p>看不懂这些数字没关系——整个系列就是用来解释它们的。为什么要知道这些？</p><p>《SICP》里面的核心思想 “程序即数据，数据即程序，二者边界模糊，所有计算机程序存在的目的都是为了处理、转换或构造数据，最终达到对复杂系统进行抽象和模拟的目的”。对于我们来讲只要能理解各个数据的流向就能理解整个系统。</p><hr><h2 id="二、MediaCodec-的真实身份"><a href="#二、MediaCodec-的真实身份" class="headerlink" title="二、MediaCodec 的真实身份"></a>二、MediaCodec 的真实身份</h2><h3 id="4-1-它是什么？"><a href="#4-1-它是什么？" class="headerlink" title="4.1 它是什么？"></a>4.1 它是什么？</h3><p><strong>MediaCodec 不是一个解码器，而是 Android 为你提供的”访问解码/编码硬件”的统一 API 门面。</strong></p><blockquote><p>更准确地说：<code>MediaCodec</code> 是一个<strong>字节流的状态机 + 缓冲池的调度器</strong>。</p></blockquote><p>它自己<strong>不懂</strong> H.264，不懂 HEVC，不懂 AV1。它做的事情是：</p><ol><li>接管你塞进来的一段<strong>输入字节</strong>（可能是 H.264 的一个 NAL、也可能是 AAC 的一帧）；</li><li>把它交给真正懂这种格式的<strong>后端实现</strong>（ACodec/OMX 或 CCodec/Codec2）；</li><li>后端再通过 HAL 把数据推给<strong>硬件电路</strong>（VPU）或<strong>软件库</strong>（如 libavcodec）；</li><li>拿回解码好的 YUV，通过<strong>输出 Buffer</strong> 或 <strong>Surface</strong> 交还给你。</li></ol><h3 id="4-2-它不是什么？"><a href="#4-2-它不是什么？" class="headerlink" title="4.2 它不是什么？"></a>4.2 它不是什么？</h3><div class="table-container"><table><thead><tr><th>误解</th><th>事实</th></tr></thead><tbody><tr><td>“MediaCodec 自己做解码”</td><td>❌ 它只是门面，真正解码的是 VPU 或软件库</td></tr><tr><td>“createDecoderByType 就会调起硬解”</td><td>❌ Android 会按能力表挑选组件，可能挑到 <code>OMX.google.h264.decoder</code>（软解）</td></tr><tr><td>“硬解一定比软解快”</td><td>❌ 启动开销大，短视频首帧软解更快；长视频稳态才体现硬解优势</td></tr><tr><td>“Surface 输出没有拷贝”</td><td>⚠️ 准确说是「通常没有 CPU 可见的像素拷贝」，DMA 搬运和格式转换仍可能发生</td></tr></tbody></table></div><h3 id="4-3-一个比喻"><a href="#4-3-一个比喻" class="headerlink" title="4.3 一个比喻"></a>4.3 一个比喻</h3><p>把 MediaCodec 想象成<strong>麦当劳的点餐柜台</strong>：</p><ul><li>你（App）只管点一个”巨无霸”（送入 H.264 码流）</li><li>柜台（MediaCodec）把订单传到后厨（Codec2/ACodec）</li><li>后厨可能让<strong>烤箱</strong>（VPU）或者<strong>人工厨师</strong>（CPU 软解）来做</li><li>做好的汉堡（YUV 帧）通过传送带（BufferQueue）送到你面前</li><li>整个过程你看不见厨房内部，但知道下单的 API 长什么样</li></ul><p>系列后续的工作，就是<strong>掀开厨房的帘子</strong>，逐个工位讲清楚。</p><hr><h2 id="三、MediaCodec-全链路"><a href="#三、MediaCodec-全链路" class="headerlink" title="三、MediaCodec 全链路"></a>三、MediaCodec 全链路</h2><h3 id="六层架构速览"><a href="#六层架构速览" class="headerlink" title="六层架构速览"></a>六层架构速览</h3><pre class="mermaid">flowchart TB    subgraph L1["🟦 L1 · Java API"]        A1["📱 App 进程<br/>━━━━━━━━━━━━━━<br/>MediaCodec.java<br/>MediaExtractor · MediaMuxer<br/>Surface · SurfaceTexture"]    end    subgraph L2["🟩 L2 · JNI 桥接"]        B1["📱 App 进程<br/>━━━━━━━━━━━━━━<br/>android_media_MediaCodec.cpp<br/>JMediaCodec 包装对象<br/>ByteBuffer · Surface 跨界"]    end    subgraph L3["🟨 L3 · Native Framework"]        direction LR        C1["ACodec<br/>━━━━━━━━<br/>OMX 路径<br/>legacy"]        C0["⚙️ mediacodec 进程<br/>━━━━━━━━━━━━━━<br/>MediaCodec.cpp<br/>ALooper + AHandler<br/>状态机"]        C2["CCodec<br/>━━━━━━━━<br/>Codec2 路径<br/>modern"]        C0 -.-> C1        C0 -.-> C2    end    subgraph L4["🟧 L4 · HAL 层"]        direction LR        D1["🔧 vendor 进程<br/>━━━━━━━━━━━━<br/>OMX HAL<br/>IOMX.hal"]        D2["🔧 vendor 进程<br/>━━━━━━━━━━━━<br/>Codec2 HAL<br/>IComponentStore.aidl"]    end    subgraph L5["🟥 L5 · Kernel 驱动"]        E1["🐧 内核态<br/>━━━━━━━━━━━━━━━━━━━━<br/>V4L2 · ION · DMA-BUF · iommu"]    end    subgraph L6["🖤 L6 · SoC 硬件"]        direction LR        F1["💻 CPU<br/>━━━━<br/>控制<br/>软解"]        F2["🎬 VPU<br/>━━━━<br/>硬件<br/>编解码"]        F3["🎨 GPU<br/>━━━━<br/>OES 纹理<br/>Shader"]        F4["🧠 NPU/DSP<br/>━━━━━━<br/>AI 超分<br/>插帧"]        F5["📺 DPU/HWC<br/>━━━━━━<br/>屏幕合成"]    end    A1 --> B1    B1 --> C0    C1 --> D1    C2 --> D2    D1 --> E1    D2 --> E1    E1 --> F1    E1 --> F2    E1 --> F3    E1 --> F4    E1 --> F5    classDef l1 fill:#E3F2FD,stroke:#1976D2,stroke-width:2px,color:#000    classDef l2 fill:#E8F5E9,stroke:#388E3C,stroke-width:2px,color:#000    classDef l3 fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#000    classDef l4 fill:#FFE0B2,stroke:#EF6C00,stroke-width:2px,color:#000    classDef l5 fill:#F3E5F5,stroke:#6A1B9A,stroke-width:2px,color:#000    classDef l6 fill:#FFCDD2,stroke:#C62828,stroke-width:2px,color:#000    class A1 l1    class B1 l2    class C0,C1,C2 l3    class D1,D2 l4    class E1 l5    class F1,F2,F3,F4,F5 l6</pre><h3 id="各层职责一览"><a href="#各层职责一览" class="headerlink" title="各层职责一览"></a>各层职责一览</h3><div class="table-container"><table><thead><tr><th>层</th><th>进程</th><th>关键代码</th><th>本质职责</th></tr></thead><tbody><tr><td><strong>L1 Java API</strong></td><td>App 进程</td><td><code>MediaCodec.java</code></td><td>给 App 写代码的入口，状态机封装</td></tr><tr><td><strong>L2 JNI</strong></td><td>App 进程</td><td><code>android_media_MediaCodec.cpp</code></td><td>把 Java 对象翻译成 C++ 对象；持有 native 指针</td></tr><tr><td><strong>L3 Native Framework</strong></td><td><strong>mediacodec 进程</strong>（沙箱隔离）</td><td><code>MediaCodec.cpp</code>、<code>CCodec.cpp</code></td><td>真正的解码大脑，调度 Looper/Handler</td></tr><tr><td><strong>L4 HAL</strong></td><td><strong>vendor 进程</strong></td><td><code>c2.qti.*</code> / <code>c2.mtk.*</code> / <code>OMX.qcom.*</code></td><td>厂商私有实现，写寄存器、发中断</td></tr><tr><td><strong>L5 Kernel</strong></td><td>内核态</td><td>V4L2-M2M、ION、iommu</td><td>暴露硬件资源、管理 DMA-BUF</td></tr><tr><td><strong>L6 Hardware</strong></td><td>硅片</td><td>VPU / GPU / NPU / DPU</td><td>真正的电路，消耗电能产出像素</td></tr></tbody></table></div><p><strong>三个最昂贵的跨越</strong>（对性能影响最大）：</p><ol><li><strong>L2 → L3</strong> — <code>App 进程 → mediacodec 进程</code>（Binder IPC）</li><li><strong>L3 → L4</strong> — <code>mediacodec 进程 → vendor 进程</code>（HIDL/AIDL 跨进程）</li><li><strong>L4 → L5</strong> — 用户态到内核态（ioctl/syscall）</li></ol><blockquote><p><strong>一个隐藏知识点</strong>：Android 8.0（Treble）之后，MediaCodec 被拆到独立进程 <code>mediacodec</code> 中运行，这是为了应对 <strong>StageFright 漏洞</strong>（CVE-2015-1538）的历史教训——<strong>用进程沙箱换安全性</strong>，代价是多一次 Binder 调用。</p></blockquote><hr><h3 id="一帧数据的”七次跳跃”"><a href="#一帧数据的”七次跳跃”" class="headerlink" title="一帧数据的”七次跳跃”"></a>一帧数据的”七次跳跃”</h3><p>用一张”一帧生命周期”时序图来串联上面六层：</p><pre class="mermaid">sequenceDiagram    autonumber    participant App as App 线程    participant JNI as JNI 层    participant MC as mediacodec 进程    participant HAL as vendor HAL 进程    participant VPU as VPU 硬件    participant GPU as GPU    participant SF as SurfaceFlinger    participant HWC as HWC/DPU    App ->> JNI: 1. queueInputBuffer(H.264 NAL)    JNI ->> MC: 2. Binder IPC（kWhatQueueInputBuffer）    MC ->> HAL: 3. HIDL/AIDL（c2_queue）    HAL ->> VPU: 4. ioctl + DMA-BUF 描述符    Note over VPU: 硬件解码（几毫秒）<br/>产出 YUV 到 DMA-BUF    VPU -->> HAL: 5. 中断 + dma-fence    HAL -->> MC: 6. 输出 GraphicBuffer    MC -->> JNI: 7. onOutputBufferAvailable    JNI -->> App: releaseOutputBuffer(true)    App ->> GPU: (可选) 特效渲染到 FBO    GPU ->> SF: queueBuffer 到 BufferQueue    SF ->> HWC: 合成（GPU 或 DPU 直出）    HWC -->> App: VSYNC · 像素点亮屏幕</pre><p>对应的「<strong>7 次关键跳跃</strong>」是：</p><div class="table-container"><table><thead><tr><th>#</th><th>跳跃</th><th>机制</th><th>成本</th></tr></thead><tbody><tr><td>1</td><td>Kotlin → Java</td><td>字节码直接调用</td><td>几乎为 0</td></tr><tr><td>2</td><td>Java → Native</td><td>JNI</td><td>纳秒级</td></tr><tr><td>3</td><td>App 进程 → mediacodec 进程</td><td>Binder IPC</td><td><strong>微秒级</strong></td></tr><tr><td>4</td><td>mediacodec → vendor HAL</td><td>HIDL/AIDL</td><td><strong>微秒级</strong></td></tr><tr><td>5</td><td>vendor → kernel</td><td>ioctl</td><td>几微秒</td></tr><tr><td>6</td><td>kernel → 硬件电路</td><td>寄存器 + 中断</td><td>纳秒级触发，毫秒级执行</td></tr><tr><td>7</td><td>VPU 输出 → GPU/DPU 消费</td><td><strong>dma-fence + DMA-BUF 共享</strong></td><td><strong>零拷贝</strong>（理想）</td></tr></tbody></table></div><blockquote><p><strong>关键秘密</strong>：跳跃 3/4/5 是控制信号（很小，几百字节），跳跃 7 是像素数据（很大，Full HD 一帧 YUV420 约 3MB）——<strong>控制路径多次跨界没关系，像素路径一次拷贝都不能多</strong>。</p><p>这就是为什么 Android 花了十年打磨 <strong>Gralloc + BufferQueue + DMA-BUF + fence</strong> 这一整套”零拷贝基础设施”——我们会在 <strong>M6/M9</strong> 详细拆解。</p></blockquote><hr><h3 id="硬件协同：一颗-SoC-是如何”开派对”的"><a href="#硬件协同：一颗-SoC-是如何”开派对”的" class="headerlink" title="硬件协同：一颗 SoC 是如何”开派对”的"></a>硬件协同：一颗 SoC 是如何”开派对”的</h3><p>很多人以为视频播放只是”解码 → 显示”两步。真实情况要热闹得多：</p><pre class="mermaid">flowchart LR    MP4[MP4 文件] -->|demux| CPU1[CPU<br/>解封装]    CPU1 -->|H.264 NAL| VPU[VPU<br/>硬件解码]    VPU -->|YUV 帧| GPU[GPU<br/>特效/色彩空间转换]    GPU -->|RGB 纹理| NPU[NPU<br/>超分/插帧]    NPU -->|增强帧| DPU[DPU/HWC<br/>合成]    DPU -->|扫描行| LCD[📺 屏幕]    CPU2[CPU<br/>全局调度与 fence 等待] -. 控制 .- VPU    CPU2 -. 控制 .- GPU    CPU2 -. 控制 .- NPU    CPU2 -. 控制 .- DPU    style CPU1 fill:#E3F2FD,stroke:#1976D2    style CPU2 fill:#E3F2FD,stroke:#1976D2    style VPU fill:#FFCDD2,stroke:#C62828    style GPU fill:#FFF9C4,stroke:#F9A825    style NPU fill:#E8F5E9,stroke:#388E3C    style DPU fill:#FFE0B2,stroke:#EF6C00</pre><h3 id="五大硬件单元的「戏份」"><a href="#五大硬件单元的「戏份」" class="headerlink" title="五大硬件单元的「戏份」"></a>五大硬件单元的「戏份」</h3><div class="table-container"><table><thead><tr><th>硬件</th><th>典型代号</th><th>在视频链路里干什么</th><th>被谁调用</th></tr></thead><tbody><tr><td><strong>CPU</strong></td><td>ARM Cortex-A</td><td>demux、调度、fence 等待、软解 fallback</td><td>所有层都用</td></tr><tr><td><strong>VPU</strong></td><td>Qualcomm Venus / MTK VDEC / 三星 MFC</td><td><strong>硬件编解码主力</strong></td><td>Codec2 HAL → 驱动</td></tr><tr><td><strong>GPU</strong></td><td>Adreno / Mali / Xclipse</td><td>色彩空间转换、Shader 特效、UI 合成 fallback</td><td>OpenGL ES / Vulkan</td></tr><tr><td><strong>NPU/DSP</strong></td><td>Hexagon / APU / NPU</td><td>AI 超分、插帧、去噪、HDR 转换</td><td>NNAPI / 私有 SDK</td></tr><tr><td><strong>DPU/HWC</strong></td><td>MDSS / DISP</td><td><strong>免 GPU 直接合成</strong>，最省电</td><td>SurfaceFlinger → HWC2</td></tr></tbody></table></div><p><strong>关键洞察</strong>：</p><ul><li><strong>稳态播放时 CPU 几乎不干活</strong> — 因为 VPU→GPU→DPU 之间通过 DMA-BUF + fence 自治流转</li><li><strong>GPU 有三种角色</strong> — 特效处理（App 用）、色彩转换（SF 用）、合成 fallback（HWC 不能直出时兜底）</li><li><strong>NPU 是近 3 年才加入的新玩家</strong> — 超分、插帧是最典型的业务</li></ul><blockquote><p>这些内容将在 <strong>M9 硬件分工全景</strong> 中用两万字铺开讲，包括为什么 HWC 能直出 NV12 而 App 层却必须经过 GPU。</p></blockquote><hr><h3 id="解码与编码：两条相反的路"><a href="#解码与编码：两条相反的路" class="headerlink" title="解码与编码：两条相反的路"></a>解码与编码：两条相反的路</h3><div class="table-container"><table><thead><tr><th>方向</th><th>输入</th><th>处理</th><th>输出</th><th>谁来保存</th></tr></thead><tbody><tr><td><strong>解码（播放）</strong></td><td>H.264 码流</td><td>VPU 解码 → GPU 特效</td><td>YUV / RGB 帧</td><td>SurfaceFlinger 合成显示</td></tr><tr><td><strong>编码（录制/导出）</strong></td><td>YUV / RGB 帧</td><td>GPU 渲染 → VPU 编码</td><td>H.264 码流</td><td>MediaMuxer 封装进 MP4</td></tr></tbody></table></div><p>特效视频的核心链路是「<strong>解码 → 特效 → 编码</strong>」的一个完整闭环：</p><pre class="mermaid">flowchart LR    A[MP4 输入] --> B[MediaExtractor]    B --> C[MediaCodec 解码器<br/>硬解 → YUV]    C --> D[SurfaceTexture<br/>OES 外部纹理]    D --> E[GPU Shader<br/>LUT/滤镜/贴纸]    E --> F[MediaCodec 编码器<br/>输入 Surface]    F --> G[H.264 码流]    G --> H[MediaMuxer]    H --> I[MP4 输出]    style C fill:#FFF9C4,stroke:#F9A825    style F fill:#FFF9C4,stroke:#F9A825    style E fill:#FFCDD2,stroke:#C62828</pre><blockquote><p>关键技巧：<strong>编码器的输入 Surface + 解码器的输出 SurfaceTexture</strong> 构成一条纯 GPU 零拷贝管线，整个路径 CPU 几乎不参与像素搬运。<strong>这是现代短视频 App 性能的秘密武器</strong>。</p></blockquote><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><code>MediaCodec</code> 不是解码器，而是<strong>六层架构的门面</strong>：上层 API 一次调用，背后是 3 次跨进程、7 次关键跳跃、4 种硬件协同。其中前六跳传的是<strong>控制信号</strong>，最后一跳靠 <code>DMA-BUF + fence</code> 完成<strong>像素零拷贝</strong>——这正是 Android 花十年打磨出的护城河。看懂了这张地图，就能解释”硬解为何变软解”、”同样 1080p60 为何别人更省电”这类表层 API 问题。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;系列导读&lt;/strong&gt;：本系列将从 Kotlin/Java 应用层出发，一路向下，穿越 JNI、Native framework、Codec2、HAL、驱动，直至 SoC 内部的 VPU/GPU/NPU 硬件电路，完整拆解一帧视</summary>
      
    
    
    
    <category term="技术文章" scheme="http://julis.wang/categories/technology/"/>
    
    
    <category term="MediaCodec" scheme="http://julis.wang/tags/MediaCodec/"/>
    
  </entry>
  
  <entry>
    <title>如何构建一个好用的SDK</title>
    <link href="http://julis.wang/2026/02/04/%E5%A6%82%E4%BD%95%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%A5%BD%E7%94%A8%E7%9A%84SDK/"/>
    <id>http://julis.wang/2026/02/04/%E5%A6%82%E4%BD%95%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%A5%BD%E7%94%A8%E7%9A%84SDK/</id>
    <published>2026-02-04T12:53:23.000Z</published>
    <updated>2026-02-09T11:48:13.026Z</updated>
    
    <content type="html"><![CDATA[<p>如何去设计一个好用的SDK，自己做了很久的SDK，实际上并没有过多去思考过这个问题。最近对起总结一下，在回答这个问题之前，我们可以自己思考一下：对于接入者，怎样才算是一个好的SDK？对我自己而言，如果我要去接入一个SDK的话，可能会去考虑分别针对几个阶段：</p><p><strong>接入前：</strong> 有清晰的文档能告诉我这个SDK 的环境要求，比如 minSdkVersion compileSdkVersion 这些硬性配置，明确列出<strong>所有直接 / 间接依赖的第三方库及版本</strong>，以及包大小、申请权限等。</p><p><strong>接入中：</strong>接入需要足够简单，在 dependencies 中一行搞定；如果有初始化操作也尽量简洁，一行搞定；对应 SDK 调用 ，提供一个单例类或者某个接口，在这个类/接口里面能够看到最核心的 Api；使用的接口对各种异常处理有足够多的错误提示信息；针对输出日志能够有配置开关等。</p><p><strong>接入后：</strong> SDK 由于功能需要更新，需提供详尽的 CHANGELOG，接入者只用更新版本号；</p><p>上面是站在接入者的角度去考虑的，作为SDK的开发者则应该遵循以下的规则。</p><h2 id="先前原则"><a href="#先前原则" class="headerlink" title="先前原则"></a>先前原则</h2><p><strong>核心原则：以接入方体验为中心</strong></p><p>在开发一个SDK的时候，把接入者当作“小白”，不寄希望于他能点进SDK内部，去了解内部的实现；把接入者视作“懒人”，他不想做太多事，他赖得看你代码里面的各种注释，只想看一份接入文档就搞定所有事情。</p><p><strong>最小化原则：只暴露必要的内容</strong></p><p>对外仅暴露<strong>核心入口类 + 必要数据类 + 统一结果类</strong>，内部工具类、中间逻辑、私有方法全部标记为<code>private</code>（Java）/<code>internal</code>（Kotlin），杜绝接入方依赖内部逻辑。</p><p><strong>稳定性原则：避免崩溃，容错兜底</strong></p><p>不要将你的SDK视为一个可以随时更新的应用程序，而应将其看作一旦上线就无法实时更新的后端服务。避免崩溃是第一前提，但不可能万无一失，会由于设备兼容性问题，没法在上线将所有情况全考虑到，那么需要SDK能够有兜底操作，比如某个开关能够关闭掉该功能，或者针对特定机型Android系统等进行屏蔽（需服务端配合）。</p><h2 id="开发阶段"><a href="#开发阶段" class="headerlink" title="开发阶段"></a>开发阶段</h2><h3 id="详细的文档"><a href="#详细的文档" class="headerlink" title="详细的文档"></a>详细的文档</h3><p>接入需要清晰、详细的文档，了解如何充分发挥 SDK 的能力。一份全面的文档需要包含：</p><ul><li><p>SDK开发环境要求</p></li><li><p>SDK安装和配置的详细流程</p></li><li><p>核心 Api 使用的代码示例</p></li><li><p>有一个可进行索引的目录</p></li></ul><h3 id="安装和初始化"><a href="#安装和初始化" class="headerlink" title="安装和初始化"></a>安装和初始化</h3><p>使用<strong>Maven Publish Plugin</strong>将 SDK 发布到 Maven 仓库，配置<code>groupId</code>/<code>artifactId</code>/<code>version</code>，确保接入方仅需一行<code>implementation</code>即可引入；</p><p>避免在 AndroidManifest.xml 中配置冗余内容，如需一些动态配置，提供<strong>Gradle 插件自动注入</strong>这些能力，无需接入方手动修改。</p><p>初始化的时候一行搞定，比如：<code>XxxSdk.Builder().appKey(&quot;your_app_key&quot;).debugMode(true).build()</code></p><h3 id="极简-API-设计"><a href="#极简-API-设计" class="headerlink" title="极简 API 设计"></a>极简 API 设计</h3><p>设计良好的 API 确保接入者能够轻松集成并使用你的 SDK 功能，避免不必要的复杂性。用 Kochava SDK 工程总监 <a href="https://appdevelopermagazine.com/building-better-sdks/">Nathan Darst </a>的话说：</p><blockquote><p>The developer wants to solve a problem, and the API should facilitate solving this problem quickly and intuitively with as few lines of code as possible. A good API prioritizes and focuses on the most common use cases; it is not muddied up with unnecessary, ambiguous, redundant, or rarely used functionality likely to confuse the user.</p><p>开发者的核心诉求是解决实际问题，而 API 的设计初衷，应是助力开发者<strong>用最少的代码、最直观的方式，快速解决问题</strong>。一个优质的 API，会优先聚焦并打磨<strong>最常用的核心使用场景</strong>；不会掺杂无关、模糊、冗余或极少用到的功能 —— 这类功能只会让使用者产生困惑。</p></blockquote><p>更多的代码准则应参考谷歌官方：<a href="https://source.android.com/docs/setup/contribute/api-guidelines?hl=zh-cn#basics-documented">Android API 准则</a></p><h3 id="轻量级-SDK"><a href="#轻量级-SDK" class="headerlink" title="轻量级 SDK"></a>轻量级 SDK</h3><p>尽量减少外部依赖，尤其是核心功能方面。如果非得用第三方库（Gson、okhttp等），就把它们隔离出来，或者更好的是，让接入者选择排除或替换这些工具，或者参考<a href="https://insert-koin.io/docs/setup/koin/">koin</a>，采用模块化架构允许仅集成所需的组件。</p><h3 id="错误处理、调试、测试"><a href="#错误处理、调试、测试" class="headerlink" title="错误处理、调试、测试"></a>错误处理、调试、测试</h3><p>当出现错误时，清晰且可作的响应帮助接入者快速识别并修复问题，减少挫败感，节省集成和故障排除的时间。尽可能将详细的错误返回或者在日志里面输出出来，不要只展示“未知错误”、”-1”、“errros”这些表达不明确的错误。</p><p>除了明确的错误信息提示外，还需要提供一些详细详细日志，且可配置。以视频渲染SDK为例，由于有多个线程参与，还有 GPU 参与，断点调试几乎不太可能，如果在每一帧都打印日志的话，又会得到大量冗余的信息，所以这里就需要日志输出可配置化。</p><p>为了更好地测试你的库，建议使用 <a href="https://en.wikipedia.org/wiki/Dependency_injection">依赖注入</a>，这里以一份网络请求做对比-&gt;<a href="https://gist.github.com/VomPom/0571f87993c4512cb774257c234eccde">使用依赖注入和不使用依赖注入的两种实现对比</a></p><p>依赖注入通过解耦被测试类与具体依赖，实现依赖的灵活替换与精准模拟，让单元测试更可控、快速且用例间相互独立。</p><h3 id="前后的兼容性"><a href="#前后的兼容性" class="headerlink" title="前后的兼容性"></a>前后的兼容性</h3><p>当 SDK 更新破坏现有 API 实现时，接入者会面临不必要的返工，通过保持向后兼容性，使接入者能够无缝采用新的 SDK 版本，同时不影响他们的工作流程。实现这一目标的一种广泛采用的策略是<a href="https://semver.org/">语义版本控制（SemVer），</a> 它采用三部分版本控制方案，清晰传达变更的范围和影响：</p><p>以 4.3.2 为例，<strong>4 是主版本号</strong>，其变更代表 SDK 出现了向后不兼容的破坏性修改，需适配调整才能使用；<strong>3 是次版本号</strong>，其变更为新增兼容式功能 / 特性，无需改动原有代码即可升级；<strong>2 是补丁版本号</strong>，其变更仅为兼容式 bug 修复，是最无风险的小版本升级。</p><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>这里主要是一些琐碎点，或者能提供一些在SDK 开发过程中的帮助</p><h3 id="多利用语言特性"><a href="#多利用语言特性" class="headerlink" title="多利用语言特性"></a>多利用语言特性</h3><p>目前我遇到的利用 kotlin 特性最好的开源库是 <a href="https://github.com/InsertKoinIO/koin">koin</a>，使用了大量的高阶函数 &amp; Lambda 表达式，比如：高阶函数 / Lambda实现简洁的 DSL 模块定义（module { … }），委托属性实现<code>by inject()</code> 延迟获取依赖，协程支持协程作用域、挂起函数创建依赖等</p><p>演示代码片段：<a href="https://gist.github.com/VomPom/d1bfa7bb01baf0be148f970cb60dbd72">koin使用到 kotlin 的一些特性</a></p><h3 id="善于使用脚本"><a href="#善于使用脚本" class="headerlink" title="善于使用脚本"></a>善于使用脚本</h3><p>脚本可以帮助开发者实现很多重复工作，包括不限于：Android SDK 一键编译打包（AAR/JAR）、多版本编译（测试不同 Kotlin/AGP 版本）、自动更新 SDK 版本号、生成版本日志（结合 Git 提交记录）、测试自动化等，这里面有很多可以使用 Gradle脚本，有的也可使用 Shell 脚本。</p><p>其他持续补充……</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本人结合个人Android SDK开发经验，好用的SDK需围绕接入方体验、稳定性设计：接入前有清晰文档，接入中极简便捷，接入后无缝升级。开发者需遵循接入方体验为中心、最小化暴露、稳定性兜底三大原则，开发中做好文档、安装初始化、API设计等核心工作，善用Kotlin特性与自动化脚本。本文为个人经验总结，因SDK开发场景多样，不适用于所有情况，仅供学习参考。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><p><a href="https://newsletter.pragmaticengineer.com/p/building-great-sdks">Building great SDKs</a></p><p><a href="https://proandroiddev.com/sdk-development-the-good-the-bad-the-ugly-9e9ab2a81697?gi=80902e83600f">SDK Development; The Good, The Bad, The Ugly</a></p><p><a href="https://www.shakebugs.com/blog/sdk-design-best-practices/">SDK design best practices</a></p><p><a href="https://anil-gudigar.medium.com/mobile-sdk-development-guidelines-eccb276d4df4">Mobile SDK Development Guidelines</a></p><p><a href="https://juejin.cn/post/6864723217831952392">Android SDK开发艺术探索（一）开篇与设计</a> </p><p><a href="https://appdevelopermagazine.com/building-better-sdks/">Building Better SDKs</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;如何去设计一个好用的SDK，自己做了很久的SDK，实际上并没有过多去思考过这个问题。最近对起总结一下，在回答这个问题之前，我们可以自己思考一下：对于接入者，怎样才算是一个好的SDK？对我自己而言，如果我要去接入一个SDK的话，可能会去考虑分别针对几个阶段：&lt;/p&gt;
&lt;p&gt;&lt;</summary>
      
    
    
    
    <category term="思考总结" scheme="http://julis.wang/categories/thinking/"/>
    
    
    <category term="SDK" scheme="http://julis.wang/tags/SDK/"/>
    
  </entry>
  
  <entry>
    <title>JPEG压缩之DCT离散余弦变换</title>
    <link href="http://julis.wang/2025/11/04/JPEG%E5%8E%8B%E7%BC%A9%E4%B9%8BDCT%E7%A6%BB%E6%95%A3%E4%BD%99%E5%BC%A6%E5%8F%98%E6%8D%A2/"/>
    <id>http://julis.wang/2025/11/04/JPEG%E5%8E%8B%E7%BC%A9%E4%B9%8BDCT%E7%A6%BB%E6%95%A3%E4%BD%99%E5%BC%A6%E5%8F%98%E6%8D%A2/</id>
    <published>2025-11-04T14:25:17.000Z</published>
    <updated>2025-11-08T03:17:35.317Z</updated>
    
    <content type="html"><![CDATA[<p>在<a href="https://github.com/leandromoreira/digital_video_introduction.git">digital_video_introduction</a>的一节中讲解 <a href="https://github.com/leandromoreira/digital_video_introduction/blob/master/README-cn.md#%E8%A7%86%E9%A2%91%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8%E6%98%AF%E5%A6%82%E4%BD%95%E5%B7%A5%E4%BD%9C%E7%9A%84">视频编解码器是如何工作的？</a>提到了 DCT，此前没有太关注，最近有对相关的原理进行学习了解，本文对 DCT 离散余弦变换相关的内容进行整理总结。<br>JPEG 使用的是 <strong>二维 DCT</strong>，因为它处理的是图像（二维信号）,理解二维 DCT 的基础是一维 DCT。</p><h3 id="一维离散余弦变换"><a href="#一维离散余弦变换" class="headerlink" title="一维离散余弦变换"></a>一维离散余弦变换</h3><p>一维 DCT 将一个长度为 N 的信号序列（例如一行像素值）从空间域转换到频域。最常用的是 <strong>DCT-II</strong> 类型，这也是 JPEG 标准所使用的。</p><p><strong>正变换：</strong> 从空间域到频域</p><script type="math/tex; mode=display">F(u) = C(u) \sum_{x=0}^{N-1} f(x) \cdot \cos\left[\frac{\pi u (2x + 1)}{2N}\right]</script><p>其中：</p><ul><li><script type="math/tex">f(x)</script>  是输入信号在位置 <script type="math/tex">x</script> 的值（例如，像素亮度）。</li><li><script type="math/tex">F(u)</script>  是变换后得到的第 <script type="math/tex">u</script> 个频率分量（DCT 系数）。</li><li><script type="math/tex">N</script>  是信号的长度（在 JPEG 中，通常是 8）。</li><li><script type="math/tex">u</script> 是频率索引，<script type="math/tex">u = 0, 1, \dots, N-1</script>。</li><li><script type="math/tex">x</script> 是空间位置索引，<script type="math/tex">x = 0, 1, \dots, N-1</script>。</li><li><script type="math/tex">C(u)</script> 是一个归一化系数，定义为：</li></ul><script type="math/tex; mode=display">  C(u) = \begin{cases}  \sqrt{\frac{1}{N}} & \text{if } u = 0 \\  \sqrt{\frac{2}{N}} & \text{if } u > 0  \end{cases}</script><p><strong>关键点：</strong></p><ul><li>当 <script type="math/tex">u = 0</script> 时，<script type="math/tex">F(0)</script> 被称为 <strong>直流系数</strong>。它实际上是整个信号块的平均值。</li><li>当 <script type="math/tex">u > 0</script> 时，<script type="math/tex">F(u)</script> 被称为 <strong>交流系数</strong>。它们代表了信号中不同频率的振荡模式。<script type="math/tex">u</script> 越大，代表的频率越高。</li></ul><h3 id="二维离散余弦变换"><a href="#二维离散余弦变换" class="headerlink" title="二维离散余弦变换"></a>二维离散余弦变换</h3><p>JPEG 将图像分割成 8x8 的小块，然后对每个块独立进行二维 DCT。二维 DCT 可以看作先对每一行进行一维 DCT，然后再对每一列进行一维 DCT（顺序可互换）。</p><p><strong>正变换：</strong> 从空间域到频域</p><script type="math/tex; mode=display">F(u, v) = C(u) C(v) \sum_{x=0}^{N-1} \sum_{y=0}^{N-1} f(x, y) \cdot \cos\left[\frac{\pi u (2x + 1)}{2N}\right] \cdot \cos\left[\frac{\pi v (2y + 1)}{2N}\right]</script><p>其中：</p><ul><li><script type="math/tex">f(x, y)</script> 是 8x8 图像块中在位置 <script type="math/tex">(x, y)</script> 的像素值。在计算前，通常会先将像素值减去 128（即 -128 到 127 的范围），使其围绕零对称。</li><li><script type="math/tex">F(u, v)</script> 是变换后得到的在频率 <script type="math/tex">(u, v)</script> 上的 DCT 系数。</li><li><script type="math/tex">N = 8</script>（对于标准的 JPEG）。</li><li><script type="math/tex">u, v</script> 是频率索引，<script type="math/tex">u, v = 0, 1, \dots, 7</script>。</li><li><script type="math/tex">x, y</script> 是空间位置索引，<script type="math/tex">x, y = 0, 1, \dots, 7</script>。</li><li><script type="math/tex">C(u)</script> 和 <script type="math/tex">C(v)</script> 的定义与一维情况相同：</li></ul><script type="math/tex; mode=display">  C(u) = \begin{cases}  \sqrt{\frac{1}{8}} & \text{if } u = 0 \\  \sqrt{\frac{2}{8}} & \text{if } u > 0  \end{cases}</script><p>  同理于 <script type="math/tex">C(v)</script>。</p><p><strong>关键点：</strong></p><ul><li><script type="math/tex">F(0, 0)</script> 是 <strong>直流系数</strong>，代表整个 8x8 块的平均亮度。</li><li>所有其他的 <script type="math/tex">F(u, v)</script> 都是 <strong>交流系数</strong>。</li><li>系数 <script type="math/tex">F(u, v)</script> 的 <script type="math/tex">u</script> 和 <script type="math/tex">v</script> 值越大，代表在水平和垂直方向上的频率越高。</li><li>在变换后的 8x8 系数矩阵中，<strong>左上角是低频系数，右下角是高频系数</strong>。图像的大部分能量（信息）都集中在低频区域，这是 JPEG 能够实现高压缩比的关键。</li></ul><h3 id="逆离散余弦变换"><a href="#逆离散余弦变换" class="headerlink" title="逆离散余弦变换"></a>逆离散余弦变换</h3><p>为了从频域数据重建图像，需要使用逆 DCT。</p><p><strong>逆变换：</strong> 从频域回到空间域</p><script type="math/tex; mode=display">f(x, y) = \sum_{u=0}^{N-1} \sum_{v=0}^{N-1} C(u) C(v) F(u, v) \cdot \cos\left[\frac{\pi u (2x + 1)}{2N}\right] \cdot \cos\left[\frac{\pi v (2y + 1)}{2N}\right]</script><p>公式中的各项含义与正变换完全相同。</p><h3 id="简单理解"><a href="#简单理解" class="headerlink" title="简单理解"></a>简单理解</h3><p><strong>DCT的本质与核心直觉</strong></p><p>DCT可以看作离散傅里叶变换（DFT）的一种特殊形式，主要处理实数信号，并且有很好的能量集中特性。</p><p><strong>它的核心思想是</strong>：任何一个8x8的像素块，都可以看作是64种不同频率的标准余弦波（即“基础图案”）按照特定权重（也就是DCT系数）叠加而成的。<br><img src="https://ww2.mathworks.cn/help/images/basis8.gif" alt=""><br>在这些基础图案中：</p><ul><li><p>低频成分（对应于系数矩阵左上角，u和v值较小的部分）代表了图像块的大致轮廓和平滑变化的背景。</p></li><li><p>高频成分（对应于系数矩阵右下角，u和v值较大的部分）代表了图像块的细节、锐利边缘和纹理。</p></li></ul><p>图像的大部分视觉能量（信息） 通常都集中在低频区域。这意味著，我们往往只需要少数几个大的低频系数，就能大致描述出图像块的主要样貌。</p><p><strong>用二进制分解来类比DCT的能量集中</strong></p><script type="math/tex; mode=display">171=2^7+2^5+2^3+2^1+2^0</script><p>如果我们将所有的0系数也补上</p><script type="math/tex; mode=display">171=2^7+0*2^6+2^5+0*2^4+2^3+0*2^2+2^1+2^0</script><p>那么对应的系数数组为：[1,0,1,0,1,0,1,1]</p><p>如果我们简单将数组低索引称之为”低频”，高索引为”高频”,将数据压缩只包含”低频”系数<br>[1,0,0,0,0,0,0,0]=&gt;2^7=128 大概丢了25%的信息<br>[1,0,1,0,0,0,0,0]=&gt;2^7+2^5=160 大概丢了6%的信息<br>[1,0,1,0,1,0,0,0]=&gt;2^7+2^5=160 大概丢了1%的信息<br>……</p><p>可以看到“高频”信号对整个数据的影响比较小，这与DCT的思想不谋而合：我们保留对整体影响大的主要成分（低频系数），而舍弃或粗略表示那些影响细微的成分（高频系数），从而实现压缩。</p><h3 id="总结-JPEG-压缩的流程"><a href="#总结-JPEG-压缩的流程" class="headerlink" title="总结 JPEG 压缩的流程"></a>总结 JPEG 压缩的流程</h3><ol><li><strong>颜色空间转换</strong>：将图像从 RGB 转换到 YCbCr。</li><li><strong>分块</strong>：将每个分量（Y, Cb, Cr）图像分割成 8x8 的块。</li><li><strong>前向 DCT</strong>：对每个 8x8 块应用上述的二维 DCT 公式，得到频率系数。</li><li><strong>量化</strong>：将 DCT 系数除以一个对应的量化步长（来自量化表），并四舍五入到整数。<strong>这一步是有损的，是信息丢失的主要来源</strong>。高频系数通常会被量化为 0。</li><li><strong>熵编码</strong>：对量化后的系数进行 Zigzag 扫描、差分脉冲编码调制和霍夫曼编码，生成最终的 .jpg 文件。</li></ol><p>当解码时，过程是反过来的：熵解码 -&gt; 反量化 -&gt; 逆 DCT -&gt; 合并块 -&gt; 转换回 RGB。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><p> <a href="https://zhuanlan.zhihu.com/p/40356456">《影像算法解析——JPEG 压缩算法》</a><br> <a href="https://www.cnblogs.com/zxporz/p/16072580.html">《白话文理解DCT离散余弦变换》</a><br> <a href="https://ww2.mathworks.cn/help/images/discrete-cosine-transform.html">《离散余弦变换》</a><br> <strong>视频</strong><br> <a href="https://www.youtube.com/watch?v=0me3guauqOU">The Unreasonable Effectiveness of JPEG: A Signal Processing Approach</a><br>中文翻译版本<a href="https://www.bilibili.com/video/BV1bc411q7YG/?spm_id_from=333.1387.favlist.content.click&amp;vd_source=3fd1362ef2060381db6c532cb37979dc">《离散余弦变换可视化讲解》</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在&lt;a href=&quot;https://github.com/leandromoreira/digital_video_introduction.git&quot;&gt;digital_video_introduction&lt;/a&gt;的一节中讲解 &lt;a href=&quot;https://github.</summary>
      
    
    
    
    <category term="算法研究" scheme="http://julis.wang/categories/%E7%AE%97%E6%B3%95%E7%A0%94%E7%A9%B6/"/>
    
    
    <category term="数学" scheme="http://julis.wang/tags/%E6%95%B0%E5%AD%A6/"/>
    
  </entry>
  
  <entry>
    <title>H264码流结构理解整理</title>
    <link href="http://julis.wang/2025/09/15/H264%E7%A0%81%E6%B5%81%E7%BB%93%E6%9E%84%E7%90%86%E8%A7%A3%E6%95%B4%E7%90%86/"/>
    <id>http://julis.wang/2025/09/15/H264%E7%A0%81%E6%B5%81%E7%BB%93%E6%9E%84%E7%90%86%E8%A7%A3%E6%95%B4%E7%90%86/</id>
    <published>2025-09-15T13:31:00.000Z</published>
    <updated>2025-12-16T13:30:14.834Z</updated>
    
    <content type="html"><![CDATA[<p>本文将带你深入H.264文件的内部，从宏观到微观，逐一剖析其各个组成部分的作用、相互关系以及一些精妙的设计哲学。<br>在了解H264之前需要有以下的一些基础知识：</p><h2 id="宏观结构：从文件到帧"><a href="#宏观结构：从文件到帧" class="headerlink" title="宏观结构：从文件到帧"></a>宏观结构：从文件到帧</h2><p>一个H.264原始码流（<code>.h264</code>或<code>.264</code>文件）并不是一个简单的“视频文件”，它不包含音频、字幕等元信息。它是一个<strong>纯粹的、编码后的视频数据比特流</strong>。这个流的结构可以看作一个分层模型，如下图所示，理解这个结构是理解H.264的关键：</p><p><img src="https://www.hardening-consulting.com/images/h264_bitstream.png" alt=""></p><h3 id="网络抽象层单元-NAL-Unit"><a href="#网络抽象层单元-NAL-Unit" class="headerlink" title="网络抽象层单元 (NAL Unit)"></a>网络抽象层单元 (NAL Unit)</h3><p>H.264设计的一个核心思想是<strong>网络友好性</strong>。为了实现这一目标，整个码流被分割成一个个独立的包，称为 <strong>NAL Unit（网络抽象层单元）</strong>。每个NAL Unit都是一个自包含的数据包，包含一个头部和负载数据。这种设计使得H.流非常适合在容易产生包丢失和延迟的网络（如RTP/UDP）中传输，因为一个NAL Unit的丢失通常不会导致整个视频无法解码。</p><h3 id="关键概念：帧-Frame-与片-Slice"><a href="#关键概念：帧-Frame-与片-Slice" class="headerlink" title="关键概念：帧 (Frame) 与片 (Slice)"></a>关键概念：帧 (Frame) 与片 (Slice)</h3><p>在视频编码中，一<strong>帧（Frame）</strong> 通常对应一张静态图片。H.264对一帧图像进行编码后，其数据可能会被装进<strong>一个或多个NAL Unit</strong>中。</p><p>为什么是一或多个？这是因为一帧数据可以被分割成多个<strong>片（Slice）</strong>。每个Slice都是一个独立的编码单元，包含了一帧图像中的一部分宏块（Macroblock）。将一帧分割成多个Slice主要有两个好处：</p><ol><li><strong>错误恢复</strong>：在网络传输中，如果一个Slice丢失了，解码器仍然可以利用错误隐藏技术来近似恢复图像，而不是丢失整帧。</li><li><strong>并行处理</strong>：多个Slice可以并行编码或解码，提高效率。</li></ol><h2 id="微观结构：NAL-Unit的内部世界"><a href="#微观结构：NAL-Unit的内部世界" class="headerlink" title="微观结构：NAL Unit的内部世界"></a>微观结构：NAL Unit的内部世界</h2><p>现在，让我们打开一个NAL Unit，看看它里面到底有什么。</p><h3 id="NAL-Unit-Header（头部）"><a href="#NAL-Unit-Header（头部）" class="headerlink" title="NAL Unit Header（头部）"></a>NAL Unit Header（头部）</h3><p>每个NAL Unit都以一个1字节（可扩展为2字节）的头部开始。这个头部虽然小，但信息量巨大：</p><ul><li><strong>禁止位（F）</strong>：通常为0，如果为1表示该单元出错。</li><li><strong>重要性指示位（NRI）</strong>：表示这个NAL Unit的重要性。值越大，解码器越需要优先保护它（如SPS/PPS的NRI值最高）。</li><li><strong>类型（Type）</strong>：这是最关键的部分！它定义了该单元负载数据的类型。主要分为两大类：<ul><li><strong>VCL（视频编码层）单元</strong>：真正携带编码视频数据的单元（如Slice）。</li><li><strong>Non-VCL（非视频编码层）单元</strong>：携带元数据和控制信息的单元，是解码的“说明书”。</li></ul></li></ul><h3 id="NAL-Unit-Payload（负载）"><a href="#NAL-Unit-Payload（负载）" class="headerlink" title="NAL Unit Payload（负载）"></a>NAL Unit Payload（负载）</h3><p>负载部分的数据内容完全由头部中的<strong>类型（Type）</strong> 决定。</p><h4 id="关键的Non-VCL单元（元数据）"><a href="#关键的Non-VCL单元（元数据）" class="headerlink" title="关键的Non-VCL单元（元数据）"></a><strong>关键的Non-VCL单元（元数据）</strong></h4><p>这些单元不包含图像像素数据，但<strong>没有它们，VCL单元根本无法被解码</strong>。它们通常在视频流开始时发送一次，但如果解码器中途加入，也需要重新获取。</p><p>  <strong>SPS（序列参数集 - Type 7）</strong><br>       <strong>作用</strong>：包含了适用于<strong>整个视频序列</strong>的全局参数。它是解码器的“总纲”。<br>       <strong>包含信息</strong>：视频的档次、级别、分辨率（<code>pic_width_in_mbs_minus1</code>等）、帧率、色深、比特深度等。没有SPS，解码器连图像该解码成多大都不知道。</p><p>   <strong>PPS（图像参数集 - Type 8）</strong><br>      <strong>作用</strong>：包含了适用于<strong>一幅或多幅图像</strong>的解码参数。它更像是“章节细则”。<br>       <strong>包含信息</strong>：熵编码模式（CAVLC或CABAC）、量化参数等。PPS可以改变，从而在序列中实现不同的编码配置。</p><p>   <strong>IDR（即时解码刷新 - 属于VCL，但特殊）</strong><br>       <strong>作用</strong>：一个特殊的Slice（通常是I-Slice），它告诉解码器：“从这里开始，可以独立解码，不再需要参考之前的帧了。”<br>       <strong>意义</strong>：IDR帧是<strong>随机访问和 seeking 的关键点</strong>。当你拖动视频进度条时，播放器总是在寻找最近的IDR帧开始解码，因为它能清空之前的参考帧缓冲区，保证解码正确。</p><h4 id="VCL单元（核心数据）"><a href="#VCL单元（核心数据）" class="headerlink" title="VCL单元（核心数据）"></a><strong>VCL单元（核心数据）</strong></h4><p>这些单元携带了实际的压缩视频数据，即Slice。</p><p>  <strong>Slice Header（切片头）</strong><br>       每个Slice都有自己的头，其中包含了当前Slice解码所需的<strong>信息</strong>：<br>           引用哪个PPS（从而间接引用SPS）。<br>           帧类型（I, P, B）。<br>           量化参数。<br>           根据帧类型，包含运动向量预测所需的信息。</p><p>   <strong>Slice Data（切片数据）</strong><br>       这是压缩数据的核心，由一系列<strong>宏块（Macroblock）</strong> 组成。<br>       <strong>宏块</strong>通常是16x16像素的编码单元，它包含了：<br>           <strong>预测信息</strong>：对于I帧，是帧内预测模式；对于P/B帧，是运动向量（描述当前块是从参考帧的哪个位置移动过来的）。<br>           <strong>残差数据</strong>：经过预测后，当前块与预测块之间的差值。这部分数据会经过<strong>变换（DCT）、量化、熵编码（CAVLC/CABAC）</strong>，从而获得极高的压缩率。</p><h2 id="特殊设计点"><a href="#特殊设计点" class="headerlink" title="特殊设计点"></a>特殊设计点</h2><h3 id="3-1-参数集（SPS-PPS）机制"><a href="#3-1-参数集（SPS-PPS）机制" class="headerlink" title="3.1 参数集（SPS/PPS）机制"></a>3.1 参数集（SPS/PPS）机制</h3><p>这是H.264一个非常巧妙的设计。它将<strong>很少改变但至关重要的信息</strong>（SPS/PPS）与<strong>频繁变化的数据</strong>（Slice）分离开。<br>   <strong>优点一：鲁棒性</strong>：即使丢失了一些Slice，只要SPS/PPS还在，解码器就能继续工作。<br>   <strong>优点二：效率</strong>：无需在每一个Slice中都重复这些头部信息，大大节省了码流。<br>   <strong>优点三：灵活性</strong>：一个码流中可以存在多个PPS，并在不同场景下切换使用。</p><h3 id="3-2-I-P-B帧与GOP（图像组）"><a href="#3-2-I-P-B帧与GOP（图像组）" class="headerlink" title="3.2 I, P, B帧与GOP（图像组）"></a>3.2 I, P, B帧与GOP（图像组）</h3><p>   <strong>I帧（Intra）</strong>：自包含帧，仅使用本帧内的信息进行编码，不参考其他帧。它是压缩率最低但最关键的帧，是P帧和B帧的锚点。<br>   <strong>P帧（Predicted）</strong>：参考前面的I帧或P帧进行运动补偿预测编码，压缩率高于I帧。<br>   <strong>B帧（Bi-directional）</strong>：可以同时参考前面和后面的帧，获得最高的压缩率，但会带来编码延迟。<br>   一个<strong>GOP</strong>就是从上一个IDR帧到下一个IDR帧之前的所有帧序列。GOP长度越长，B/P帧越多，压缩率越高，但随机访问的间隔也越长。</p><h3 id="3-3-熵编码：CAVLC-与-CABAC"><a href="#3-3-熵编码：CAVLC-与-CABAC" class="headerlink" title="3.3 熵编码：CAVLC 与 CABAC"></a>3.3 熵编码：CAVLC 与 CABAC</h3><p>这是压缩过程中的最后一步，将数据转换为二进制码流。<br>   <strong>CAVLC（上下文自适应变长编码）</strong>：相对简单，压缩效率一般，用于Baseline等档次。<br>   <strong>CABAC（上下文自适应二进制算术编码）</strong>：非常复杂，但压缩效率比CAVLC高出10%-20%，是Main和High档次效率高的主要原因之一。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>H.264的结构是一个分层、模块化的杰作：<br>  <strong>整体</strong>：码流由一个个<strong>NAL Unit</strong>组成，适合网络传输。<br>  <strong>局部</strong>：NAL Unit分为<strong>VCL</strong>（携带Slice数据）和<strong>Non-VCL</strong>（携带SPS/PPS等元数据）。<br>  <strong>核心</strong>：Slice数据由<strong>宏块</strong>组成，宏块包含了<strong>预测信息</strong>和<strong>残差数据</strong>，通过预测和变换编码实现压缩。<br>  <strong>精妙设计</strong>：<strong>参数集分离</strong>、<strong>IDR帧</strong>、<strong>Slice划分</strong>和<strong>CABAC</strong>等特性共同造就了H.264在效率、鲁棒性和灵活性上的完美平衡。</p><p>理解H.264的结构，不仅能帮助我们更好地处理视频数据（如封装、传输、解码问题定位），更能让我们体会到工程师们在标准制定中的智慧和远见。尽管如今H.265/HEVC、AV1等更先进的编码器已经出现，但H.264的基本设计思想和结构仍然深刻地影响着它们。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;本文将带你深入H.264文件的内部，从宏观到微观，逐一剖析其各个组成部分的作用、相互关系以及一些精妙的设计哲学。&lt;br&gt;在了解H264之前需要有以下的一些基础知识：&lt;/p&gt;
&lt;h2 id=&quot;宏观结构：从文件到帧&quot;&gt;&lt;a href=&quot;#宏观结构：从文件到帧&quot; class=&quot;h</summary>
      
    
    
    
    
    <category term="音视频" scheme="http://julis.wang/tags/%E9%9F%B3%E8%A7%86%E9%A2%91/"/>
    
  </entry>
  
  <entry>
    <title>手写一个精简版Koin：深入理解依赖注入核心原理</title>
    <link href="http://julis.wang/2025/08/25/Koin-%E6%BA%90%E7%A0%81%E7%90%86%E8%A7%A3%E7%9B%B8%E5%85%B3/"/>
    <id>http://julis.wang/2025/08/25/Koin-%E6%BA%90%E7%A0%81%E7%90%86%E8%A7%A3%E7%9B%B8%E5%85%B3/</id>
    <published>2025-08-25T13:08:00.000Z</published>
    <updated>2025-08-25T13:44:34.603Z</updated>
    
    <content type="html"><![CDATA[<p>在现代 Android 应用开发中，依赖注入（Dependency Injection, DI）已成为构建松耦合、可测试代码的重要技术。Koin 作为一个轻量级的Kotlin依赖注入框架，因其简洁的DSL和易用性深受开发者喜爱。最近对其源码进行学习了解，通过手写一个极度精简的 Koin 核心代码，来透彻理解Koin的注册、解析和参数传递机制。</p><blockquote><p><strong>本文代码基于 Koin 源码思想实现，仅用于学习核心原理，并非 Koin官 方代码。</strong></p></blockquote><h2 id="核心概念与项目结构"><a href="#核心概念与项目结构" class="headerlink" title="核心概念与项目结构"></a>核心概念与项目结构</h2><p>下图是基于 koin 4.1 解析的 主要类UML图，可以比较清晰地看看各个类之间的关系<br><img src="https://cdn.julis.wang/blog/img/koin_uml.png"><br>power by <a href="https://www.mermaidchart.com/">mermaidchart</a></p><p>主要类：</p><ul><li><strong><code>KoinApplication</code></strong>: Koin启动的入口，负责初始化容器和加载模块。</li><li><strong><code>Koin</code></strong>: 核心容器，持有实例注册表 InstanceRegistry 和作用域注册表 ScopeRegistry。</li><li><strong><code>Module</code></strong>: 定义依赖的地方，存放了所有的 bean 定义 BeanDefinition 与 InstanceFactory。</li><li><strong><code>BeanDefinition</code></strong>: 对一个依赖项的定义，包括其类型、限定符、所属作用域以及创建它的 lambda 表达式。</li><li><strong><code>InstanceFactory</code></strong>: 负责根据 <code>BeanDefinition</code> 创建实例的核心工厂，分为 <code>SingleFactory</code> (单例)、<code>FactoryFactory</code> (工厂模式) 和 <code>ScopeFactory</code> (作用域内单例)。</li><li><strong><code>Scope</code></strong>: 作用域，用于管理特定生命周期内的实例。</li><li><strong><code>ParametersHolder</code></strong>: 参数容器，用于在获取实例时动态传递参数。</li></ul><h2 id="手写-koin-代码介绍"><a href="#手写-koin-代码介绍" class="headerlink" title="手写 koin 代码介绍"></a>手写 koin 代码介绍</h2><p>基于对源码的理解和参考，实现了 koin 的基本功能，整体分成三部分：简单 single 数据存取、包含 scope 能力、动态参数能力，分成三个文件夹，顺序123是基于前面带代码累加的。</p><p><strong>简单 single 数据存取</strong><br>代码实现在：<a href="https://github.com/VomPom/JProject/blob/master/app/src/main/java/wang/julis/jproject/example/source/koin/noScope1/KoinWithoutScope.kt">KoinWithoutScope.kt</a></p><p>这是一份最简单的代码，大概200行不到，基本上包含了 koin 的核心思想：启动时注册组件定义。解析时，先查作用域缓存，命中则直接返回。未命中则递归解析其依赖项，调用工厂函数创建实例，最后返回实例。</p><p>从这也能看出来 koin 的缺点：Koin 启动时 (startKoin) 需要将所有模块的定义 (BeanDefinition) 注册到容器中。实例数量过多会显著增加启动注册过程的耗时，影响应用启动速度。由于每个实例都会对应一个 BeanDefinition 以及 Factory ，内存占用会相应地上升。</p><p>整个流程简单来讲就是生成一个 map，通过 key 获取对于的数据。</p><p><strong>Scope 能力</strong><br>代码实现在：<a href="https://github.com/VomPom/JProject/blob/master/app/src/main/java/wang/julis/jproject/example/source/koin/scope2/KoinWithScope.kt">KoinWithScope.kt</a></p><p>这一份是在之前的能力上进行添加，此前将所有的数据都注册到 “root” 这个容器内，全局通用，但为了将不同作用域分开，需要引入 scope 的概念。</p><p>简单理解就是在通过 key 获取的 map 里面的数据的时候，这个 key 是有一定的规则的，核心逻辑在这里：<br> <figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"> <span class="keyword">inline</span> <span class="function"><span class="keyword">fun</span> <span class="title">indexKey</span><span class="params">(clazz: <span class="type">KClass</span>&lt;*&gt;, typeQualifier: <span class="type">String</span>?, scopeQualifier: <span class="type">String</span>)</span></span>: String &#123;</span><br><span class="line">    <span class="keyword">return</span> buildString &#123;</span><br><span class="line">        append(clazz.java.name)</span><br><span class="line">        append(<span class="string">&#x27;:&#x27;</span>)</span><br><span class="line">        append(typeQualifier ?: <span class="string">&quot;&quot;</span>)</span><br><span class="line">        append(<span class="string">&#x27;:&#x27;</span>)</span><br><span class="line">        append(scopeQualifier)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br> 不同的 scope 实际上也就是获取的 key 值的不同。</p><p><strong>动态参数能力</strong><br>代码实现在：<a href="https://github.com/VomPom/JProject/blob/master/app/src/main/java/wang/julis/jproject/example/source/koin/parameter3/KoinWithParameter.kt">KoinWithParameter.kt</a></p><p>最后在 scope 的基础上实现了一个比较重要的能力-动态参数能力，通过这个能力可以让有实例能够在运行的时候根据参数动态创建。这个能力也是像在安卓 Activity/Fragment 里面 viewmodel() 实现依赖注入的必要实现。 </p><p>简单理解就是在 get() 的时候将参数传入到获取实例的调用链中，在运行时执行注册的 Lambda 函数invoke时候将作为参数传递到构造方法中去。这里单独拎出来实现是因为这个参数传递影响到整个流程的逻辑，为了上上面的两个能力逻辑更简单清晰，单独在这一部分实现。</p><h2 id="Koin-的注册流程（Declaration）"><a href="#Koin-的注册流程（Declaration）" class="headerlink" title="Koin 的注册流程（Declaration）"></a>Koin 的注册流程（Declaration）</h2><p>注册是DI容器工作的第一步。通过 <code>startKoin</code> 和 <code>module</code> DSL来声明依赖。</p><h3 id="启动-Koin-与模块加载"><a href="#启动-Koin-与模块加载" class="headerlink" title="启动 Koin 与模块加载"></a>启动 Koin 与模块加载</h3><p>整个启动加载流程将 kotlin 的语法糖用到了极致，也就使得整个代码看起来是如此的简洁。</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> myApp = startKoin &#123;</span><br><span class="line">    modules(appModule)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义一个模块</span></span><br><span class="line"><span class="keyword">val</span> appModule = module &#123;</span><br><span class="line">    <span class="comment">// 注册一个单例，其构造需要一個 Int 参数</span></span><br><span class="line">    single &#123; (<span class="keyword">data</span>: <span class="built_in">Int</span>) -&gt; ComponentInt(<span class="keyword">data</span>) &#125;</span><br><span class="line">    <span class="comment">// 注册一个工厂（每次获取都是新实例），其构造需要 Int 和 Float 参数</span></span><br><span class="line">    factory &#123; (data1: <span class="built_in">Int</span>, data2: <span class="built_in">Float</span>) -&gt; ComponentIntFloat(data1, data2) &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>流程剖析：</strong></p><p><strong><code>startKoin</code></strong><br>这是一个顶级函数，它调用 <code>GlobalContext.startKoin</code>，创建并初始化一个 <code>KoinApplication</code> 对象。</p><p><strong><code>modules(...)</code></strong><br><code>KoinApplication</code> 的方法，它将传入的 <code>Module</code> 列表交给 <code>Koin</code> 实例的 <code>loadModels</code> 方法处理。</p><p><strong><code>module &#123; ... &#125;</code></strong><br>DSL函数，它创建一个 <code>Module</code> 对象，并执行其中的配置lambda。</p><p><strong><code>single/factory/scope</code></strong><br><code>Module</code> 的扩展函数。它们的作用是：</p><ul><li>使用 <code>_createDefinition</code> 将 lambda 表达式包装成一个 <code>BeanDefinition</code>对象。</li><li>使用 <code>_InstanceFactory</code> 将 <code>BeanDefinition</code> 包装成对应的 <code>InstanceFactory</code>。</li><li>调用 <code>indexPrimaryType</code>，生成一个<strong>唯一的Key</strong>（格式：<code>类名:限定符:作用域</code>），并将 <code>Factory</code> 存入 <code>Module.mappings</code> 这个 <code>HashMap</code> 中。</li></ul><p><strong>最终存储</strong><br><code>Koin</code> 的 <code>InstanceRegistry</code> 会遍历所有 <code>Module</code>，将它们 <code>mappings</code> 中的全部 <code>Factory</code> 都合并到自己的 <code>_instances</code>（一个 <code>ConcurrentHashMap</code>）中。</p><p>至此，所有依赖的定义都已注册到容器中，静待获取。</p><h2 id="Koin的实例获取流程（Retrieval）"><a href="#Koin的实例获取流程（Retrieval）" class="headerlink" title="Koin的实例获取流程（Retrieval）"></a>Koin的实例获取流程（Retrieval）</h2><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 获取无参依赖（普通方式）</span></span><br><span class="line"><span class="keyword">val</span> component = <span class="keyword">get</span>&lt;Component&gt;()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 通过 scope 作用域限定进行获取</span></span><br><span class="line"><span class="keyword">val</span> scope = koin.createScope(<span class="string">&quot;scope&quot;</span>, scopeQualifier)</span><br><span class="line"><span class="keyword">val</span> component = scope.<span class="keyword">get</span>&lt;Component&gt;()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 通过需要动态参数的获取</span></span><br><span class="line"><span class="keyword">val</span> componentWithArgs = <span class="keyword">get</span>&lt;ComponentInt&gt; &#123; parametersOf(<span class="number">42</span>) &#125;</span><br><span class="line"><span class="keyword">val</span> componentWithMultiArgs = <span class="keyword">get</span>&lt;ComponentIntFloat&gt; &#123; parametersOf(<span class="number">101</span>, <span class="number">3.14f</span>) &#125;</span><br></pre></td></tr></table></figure><h3 id="流程剖析"><a href="#流程剖析" class="headerlink" title="流程剖析"></a>流程剖析</h3><p><strong><code>Scope.get&lt;T&gt;</code></strong></p><p>这是 <code>Scope</code> 的一个扩展函数。它首先创建一个 <code>ResolutionContext</code>，封装了当前作用域、要解析的类型、限定符以及最重要的——<strong>参数持有器 <code>ParametersHolder</code></strong>（由 <code>parametersOf</code> 函数创建）。</p><p> <strong>解析上下文（ResolutionContext）</strong></p><p> 这个上下文对象包含了解析一个实例所需的所有信息。</p><p><strong>核心解析器（CoreResolver）</strong><br><code>get</code> 操作会委托给 <code>Koin</code> 的 <code>CoreResolver</code>进行处理。源码里面对于查找顺序有非常清晰的层次体现：</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="type">&lt;T&gt;</span> <span class="title">resolveFromContext</span><span class="params">(scope : <span class="type">Scope</span>, instanceContext: <span class="type">ResolutionContext</span>)</span></span>: T &#123;</span><br><span class="line">      <span class="keyword">return</span> resolveFromContextOrNull(scope,instanceContext) ?: throwNoDefinitionFound(instanceContext)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="type">&lt;T&gt;</span> <span class="title">resolveFromContextOrNull</span><span class="params">(scope : <span class="type">Scope</span>, instanceContext: <span class="type">ResolutionContext</span>, lookupParent : <span class="type">Boolean</span> = <span class="literal">true</span>)</span></span>: T? &#123;</span><br><span class="line">      <span class="keyword">return</span> resolveFromInjectedParameters(instanceContext)</span><br><span class="line">          ?: resolveFromRegistry(scope,instanceContext)</span><br><span class="line">          ?: resolveFromStackedParameters(scope,instanceContext)</span><br><span class="line">          ?: resolveFromScopeSource(scope,instanceContext)</span><br><span class="line">          ?: resolveFromScopeArchetype(scope,instanceContext)</span><br><span class="line">          ?: <span class="keyword">if</span> (lookupParent) resolveFromParentScopes(scope,instanceContext) <span class="keyword">else</span> <span class="literal">null</span></span><br><span class="line">          ?: resolveInExtensions(scope,instanceContext)</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p> <strong>查找工厂</strong></p><ul><li><code>Resolver</code> 会调用 <code>InstanceRegistry.resolveDefinition</code>。</li><li><p>该方法使用和注册时<strong>相同的算法</strong>生成Key（类名:限定符:作用域），然后从 <code>_instances</code> 中查找对应的 <code>InstanceFactory</code>。</p><p><strong>创建实例</strong></p></li><li>找到 <code>Factory</code> 后，调用其 <code>get(context: ResolutionContext)</code> 方法。</li><li><p><code>Factory</code> 会调用自己的 <code>create</code> 方法。<strong>关键一步来了</strong>：在 <code>create</code> 方法中，会执行 <code>BeanDefinition.definition.invoke(context.scope, parameters)</code>。这其实就是执行了之前注册的 lambda：<code>&#123; (data: Int) -&gt; ComponentInt(data) &#125;</code>。</p></li><li><p><strong>参数传递</strong>：这里的 <code>parameters</code> 就是在 <code>get</code> 时传入的 <code>ParametersHolder</code>。Lambda 的参数 <code>(data: Int)</code> 会从 <code>ParametersHolder</code> 中按顺序（或使用解构）取出值</p></li></ul><p><strong>返回实例</strong></p><p>工厂将创建好的实例返回给调用者。</p><p>对于 <code>SingleFactory</code>，它会将第一次创建出来的实例缓存起来，后续调用直接返回缓存实例。<code>FactoryFactory</code> 则每次都会执行 <code>create</code> 方法。</p><h2 id="其他技术"><a href="#其他技术" class="headerlink" title="其他技术"></a>其他技术</h2><h3 id="DslMarker-的作用"><a href="#DslMarker-的作用" class="headerlink" title="@DslMarker 的作用"></a>@DslMarker 的作用</h3><p>在实现的过程中发现如下图所示：koin 的代码有颜色分层，能比较清晰地看到各个 block 之间的差异，自己写的代码全部是白色。<br><img src="https://cdn.julis.wang/blog/img/koin_color_contrast.png"> </p><p>代码开头定义了三个注解：<code>@KoinApplicationDslMarker</code>, <code>@KoinDslMarker</code>, <code>@OptionDslMarker</code>。这是Kotlin DSL的<strong>安全卫士</strong>。<br><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="meta">@DslMarker</span></span><br><span class="line"><span class="keyword">annotation</span> <span class="keyword">class</span> <span class="title class_">KoinDslMarker</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@KoinDslMarker</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Module</span> &#123;</span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">single</span><span class="params">(...)</span></span> &#123; ... &#125; <span class="comment">// 这个single在DSL里</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@KoinDslMarker</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">KoinApplication</span> &#123;</span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">modules</span><span class="params">(...)</span></span> &#123; ... &#125; <span class="comment">// 这个modules在DSL里</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">test</span><span class="params">()</span></span> &#123;</span><br><span class="line">    startKoin &#123;</span><br><span class="line">        modules(...) <span class="comment">// 正确：在 KoinApplication 的 lambda 里</span></span><br><span class="line">        single &#123; ... &#125; <span class="comment">// 编译错误！@DslMarker 阻止了隐式地使用外部 Receiver (Module)</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p><code>@DslMarker</code> 实际的作用是防止在嵌套的DSL Lambda中，意外地调用到外层 Receiver 的方法，从而让DSL书写更加清晰和安全。代码颜色是由 IDE 提供的效果。在代码中加上几个注解之后，效果如图所示：</p><img src="https://cdn.julis.wang/blog/img/koin_color_annotation.png"> <p>跟 koin 的颜色不太一致，不过能明显看到代码有分层，应该是由于 koin 对 annotation 也有处理，这里没有再深入研究。</p><h3 id="2-优雅的参数传递与解构"><a href="#2-优雅的参数传递与解构" class="headerlink" title="2. 优雅的参数传递与解构"></a>2. 优雅的参数传递与解构</h3><p>这个逻辑复刻了Koin的动态参数特性。</p><ul><li><strong><code>ParametersHolder</code></strong>：一个轻量的参数容器，内部用一个 <code>List&lt;Any?&gt;</code> 存储参数。</li><li><strong><code>parametersOf</code></strong>：辅助函数，优雅地创建 <code>ParametersHolder</code>。</li><li><strong>解构声明（Destructuring Declaration）</strong>：<code>ParametersHolder</code> 重写了 <code>component1()</code> 到 <code>component5()</code> 操作符。这使得在定义lambda时，可以直接用 <code>(a: A, b: B)</code> 的形式来接收参数，而不是手动调用 <code>parameters.get&lt;X&gt;(0)</code>。</li></ul><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 注册端：看起来就像普通函数</span></span><br><span class="line">single &#123; (id: <span class="built_in">Int</span>, name: String) -&gt; User(id, name) &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取端：传递参数非常直观</span></span><br><span class="line"><span class="keyword">val</span> user = <span class="keyword">get</span>&lt;User&gt; &#123; parametersOf(<span class="number">123</span>, <span class="string">&quot;Julius&quot;</span>) &#125;</span><br></pre></td></tr></table></figure><p>这种设计极大地提升了API的简洁性和可读性。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过这个手写的迷你Koin，可以深刻地理解到，一个现代DI容器的核心无非是解决两个问题：</p><ol><li><strong>如何注册（Declaration）</strong>：通过DSL将依赖的创建方式（Lambda）以键值对的形式保存到一个全局的注册表中。</li><li><strong>如何获取（Retrieval）</strong>：根据请求的类型、限定符和作用域生成Key，从注册表中找到对应的创建工厂，并调用它来生成实例。支持通过参数容器实现动态传参。</li></ol><p>除此之外，诸如 <code>@DslMarker</code> 保证DSL安全、<strong>解构</strong>实现参数优雅传递，都是构建一个健壮、易用框架的关键技术。</p><p>虽然这个实现省略了Koin的许多高级功能（如完整的Scope生命周期管理、属性注入、Android特定支持等），但它已经囊括了最核心、最精妙的设计思想，再理解其他的模块也会简单很多。</p><p>实现的所有源码位于：<a href="https://github.com/VomPom/JProject/tree/master/app/src/main/java/wang/julis/jproject/example/source/koin">JProject/source/koin</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在现代 Android 应用开发中，依赖注入（Dependency Injection, DI）已成为构建松耦合、可测试代码的重要技术。Koin 作为一个轻量级的Kotlin依赖注入框架，因其简洁的DSL和易用性深受开发者喜爱。最近对其源码进行学习了解，通过手写一个极度精简</summary>
      
    
    
    
    <category term="技术文章" scheme="http://julis.wang/categories/technology/"/>
    
    
    <category term="koin" scheme="http://julis.wang/tags/koin/"/>
    
  </entry>
  
  <entry>
    <title>[Compose Multiplatform]跨平台博客应用实践</title>
    <link href="http://julis.wang/2025/07/28/Compose-Multiplatform-%E8%B7%A8%E5%B9%B3%E5%8F%B0%E5%8D%9A%E5%AE%A2%E5%BA%94%E7%94%A8%E5%AE%9E%E8%B7%B5/"/>
    <id>http://julis.wang/2025/07/28/Compose-Multiplatform-%E8%B7%A8%E5%B9%B3%E5%8F%B0%E5%8D%9A%E5%AE%A2%E5%BA%94%E7%94%A8%E5%AE%9E%E8%B7%B5/</id>
    <published>2025-07-28T12:28:00.000Z</published>
    <updated>2026-02-09T10:48:52.288Z</updated>
    
    <content type="html"><![CDATA[<h2 id="用-CMP-构建跨平台博客应用：一次-Kotlin-的全栈实践"><a href="#用-CMP-构建跨平台博客应用：一次-Kotlin-的全栈实践" class="headerlink" title="用 CMP 构建跨平台博客应用：一次 Kotlin 的全栈实践"></a>用 CMP 构建跨平台博客应用：一次 Kotlin 的全栈实践</h2><p>在追求高效开发的时代，跨平台技术已成为移动应用开发的主流选择，此前基于鸿蒙的开发平台开发 <a href="https://julis.wang/2025/05/16/%E9%B8%BF%E8%92%99-%E5%86%99%E4%BA%86%E4%B8%AA%E5%9F%BA%E4%BA%8EHexo%E5%8D%9A%E5%AE%A2%E7%9A%84%E9%B8%BF%E8%92%99App/">blog_harmony</a>，将自己博客文章进行展示。本文将介绍基于 <strong>CMP(Compose Multiplatform)</strong> 构建的开源博客应用 <a href="https://github.com/VomPom/blog_kmp">blog_kmp</a>，展示如何用 Kotlin 实现跨平台的应用开发。</p><h3 id="Compose-Multiplatform"><a href="#Compose-Multiplatform" class="headerlink" title="Compose Multiplatform"></a>Compose Multiplatform</h3><p>Compose Multiplatform 是 JetBrains 推出的声明式 UI 框架，基于 Jetpack Compose 扩展而来：</p><ul><li><strong>核心优势</strong>：用同一套 Kotlin 代码构建 Android、iOS、Desktop 和 Web 应用</li><li><strong>开发效率</strong>：实时预览、热重载加速开发迭代</li><li><strong>原生性能</strong>：通过 Skia 渲染引擎实现接近原生体验</li><li><strong>共享逻辑</strong>：业务逻辑、网络请求、状态管理可 100% 复用</li></ul><h3 id="项目架构与技术栈"><a href="#项目架构与技术栈" class="headerlink" title="项目架构与技术栈"></a>项目架构与技术栈</h3><p>blog_kmp 采用分层架构设计，核心模块包括：</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">shared/</span><br><span class="line">├── src/commonMain/kotlin/  # 共享业务逻辑</span><br><span class="line">│   ├── <span class="keyword">data</span>/               # 数据层</span><br><span class="line">│   ├── domain/             # 领域模型</span><br><span class="line">│   └── presentation/       # UI状态管理</span><br><span class="line">├── src/androidMain/        # Android 平台代码</span><br><span class="line">└── src/iosMain/            # iOS 平台适配</span><br><span class="line">├── composeApp</span><br><span class="line">│   ├── build.gradle.kts</span><br><span class="line">│   └── src</span><br><span class="line">│       ├── androidMain     # Android 平台代码</span><br><span class="line">│       ├── commonMain      # 共享业务逻辑</span><br><span class="line">│            ├── App.kt     # 界面展示入口</span><br><span class="line">│            ├── <span class="keyword">data</span>       # 数据层</span><br><span class="line">│            │   ├── api        # 网络请求</span><br><span class="line">│            │   ├── di         # koin 依赖注入</span><br><span class="line">│            │   ├── model      # model 数据</span><br><span class="line">│            │   └── repository # 数据缓存管理</span><br><span class="line">│            │</span><br><span class="line">│            ├── navigation  # 页面间导航管理</span><br><span class="line">│            ├── platform    # 通过对各个平台抽象的接口 </span><br><span class="line">│            └── ui          # 通用 UI 逻辑</span><br><span class="line">│</span><br><span class="line">│       ├── desktopMain     # Desktop 平台适配</span><br><span class="line">│       └── iosMain         # iOS 平台适配</span><br></pre></td></tr></table></figure><h2 id="功能预览"><a href="#功能预览" class="headerlink" title="功能预览"></a>功能预览</h2><h3 id="Android"><a href="#Android" class="headerlink" title="Android"></a>Android</h3><p><img src="https://cdn.julis.wang/github/blog_cmp/android.png" alt=""></p><h4 id="深色模式"><a href="#深色模式" class="headerlink" title="深色模式"></a>深色模式</h4><p><img src="https://cdn.julis.wang/github/blog_cmp/dark.png" alt=""></p><h3 id="iOS"><a href="#iOS" class="headerlink" title="iOS"></a>iOS</h3><p><img src="https://cdn.julis.wang/github/blog_cmp/ios.png" alt=""></p><h3 id="Desktop"><a href="#Desktop" class="headerlink" title="Desktop"></a>Desktop</h3><p><img src="https://cdn.julis.wang/github/blog_cmp/desktop.png" alt=""></p><h4 id="主要技术栈"><a href="#主要技术栈" class="headerlink" title="主要技术栈"></a>主要技术栈</h4><ol><li><p><strong>Ktor 客户端</strong> - 网络请求</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> httpClient = HttpClient &#123;</span><br><span class="line">    install(ContentNegotiation) &#123;</span><br><span class="line">        json(Json &#123; ignoreUnknownKeys = <span class="literal">true</span> &#125;)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">loadPosts</span><span class="params">()</span></span>: List&lt;Post&gt; = </span><br><span class="line">    httpClient.<span class="keyword">get</span>(<span class="string">&quot;https://cdn.julis/api/posts&quot;</span>).body()</span><br></pre></td></tr></table></figure></li><li><p><strong>DataStore</strong> - 跨平台数据库</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> dataKey = stringPreferencesKey(key)</span><br><span class="line"><span class="keyword">val</span> result = dataStore.<span class="keyword">data</span></span><br><span class="line">    .<span class="keyword">catch</span> &#123; exception -&gt;</span><br><span class="line">        <span class="comment">// dataStore.data throws an IOException when an error is encountered when reading data</span></span><br><span class="line">        <span class="keyword">if</span> (exception <span class="keyword">is</span> IOException) &#123;</span><br><span class="line">            emit(emptyPreferences())</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">throw</span> exception</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    .map &#123; preferences -&gt;</span><br><span class="line">        <span class="keyword">val</span> <span class="keyword">data</span>: String? = preferences[dataKey]</span><br><span class="line">        <span class="keyword">if</span> (<span class="keyword">data</span> == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="literal">null</span></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> (isJson) Json.decodeFromString&lt;T&gt;(<span class="keyword">data</span>) <span class="keyword">else</span> (<span class="keyword">data</span> <span class="keyword">as</span> T)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure></li><li><p><strong>Koin</strong> - 依赖注入</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> sharedModule = module &#123;</span><br><span class="line">    single&lt;PostRepository&gt; &#123; PostRepositoryImpl(<span class="keyword">get</span>()) &#125;</span><br><span class="line">    viewModel &#123; PostViewModel(<span class="keyword">get</span>()) &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p><strong>Kotlinx.Serialization</strong> - JSON 解析</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Serializable</span></span><br><span class="line"><span class="keyword">data</span> <span class="keyword">class</span> <span class="title class_">Post</span>(</span><br><span class="line">    <span class="keyword">val</span> id: String,</span><br><span class="line">    <span class="keyword">val</span> title: String,</span><br><span class="line">    <span class="keyword">val</span> content: String</span><br><span class="line">)</span><br></pre></td></tr></table></figure></li><li><p><strong>compose-webview-multiplatform</strong> - WebView 浏览器<br>使用的第三方开发<a href="https://github.com/KevinnZou/compose-webview-multiplatform">compose-webview-multiplatform</a>基于 <a href="https://github.com/chromiumembedded/java-cef">java-cef</a>开发，不过这个library 在 desktop 平台表现不是太好，待完善。</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> state = rememberWebViewState(postUrl)</span><br><span class="line"> WebView(state = state,modifier = Modifier.fillMaxSize())            </span><br></pre></td></tr></table></figure></li></ol><h3 id="平台特定实现"><a href="#平台特定实现" class="headerlink" title="平台特定实现"></a>平台特定实现</h3><p>UI 层面三端能够使用同一份代码，但为了体验，可能需要针对不同的设计，在桌面端可以设计更好地体验UI。这里避免不了 if-else 的UI逻辑，以及一些依赖各种系统的 api 需要单独实现，比如：深色模式监听、资源存储路径、系统信息、状态栏颜色等。</p><p><strong>Android 端</strong><br>Android 特定的功能结合使用起来非常的简单，毕竟都是有血缘关系的。可以使用 AndroidView 直接渲染原生的 UI 页面。</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">AndroidView(</span><br><span class="line">      modifier = Modifier.fillMaxSize(),</span><br><span class="line">      factory = &#123; context -&gt;</span><br><span class="line">          MyView(context) &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">      update = &#123; view -&gt;&#125;</span><br><span class="line">  )</span><br></pre></td></tr></table></figure><p><strong>iOS 端</strong><br>iOS端主要需要 XCode 进行配合，还需要关注开发者账号相关的信息等，其他与 Android 端实现没有太大的差异。</p><p><strong>桌面端</strong><br>利用 Compose Desktop 的窗口管理，可以实现窗口多开。<br><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">()</span></span> = application &#123;</span><br><span class="line">    Window(onCloseRequest = ::exitApplication) &#123;</span><br><span class="line">        DesktopAppTheme &#123; AppContent() &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h3 id="🚀-性能优化实践"><a href="#🚀-性能优化实践" class="headerlink" title="🚀 性能优化实践"></a>🚀 性能优化实践</h3><ol><li><p><strong>分页加载</strong>：实现懒加载防止长列表卡顿</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">LazyColumn &#123;</span><br><span class="line">    itemsIndexed(posts) &#123; _, post -&gt;</span><br><span class="line">        PostItem(post)</span><br><span class="line">    &#125;</span><br><span class="line">    item &#123; <span class="keyword">if</span> (loading) LoadingIndicator() &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p><strong>本地缓存</strong>：DataStore 离线存储 + Ktor 缓存策略</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">HttpClient &#123;</span><br><span class="line">    install(HttpCache) <span class="comment">// 启用 HTTP 缓存</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p><strong>图像处理</strong>：搭配 Coil 实现高效图片加载</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">AsyncImage(</span><br><span class="line">    modifier = Modifier.size(<span class="number">80.</span>dp)</span><br><span class="line">        .shadow(</span><br><span class="line">            elevation = <span class="number">5.</span>dp,</span><br><span class="line">            shape = CircleShape,</span><br><span class="line">            spotColor = Color.Black</span><br><span class="line">        )</span><br><span class="line">        .clip(CircleShape)</span><br><span class="line">        .clickable &#123; &#125;,</span><br><span class="line">    model = AppConfig.AVATAR,</span><br><span class="line">    contentDescription = AppConfig.AVATAR,</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h3 id="开发经验总结"><a href="#开发经验总结" class="headerlink" title="开发经验总结"></a>开发经验总结</h3></li><li><p><strong>UI界面</strong><br>使用 <a href="https://developer.android.com/compose">Compose</a> 进行界面布局开发，声明性编程范式相比于传统的 xml 布局开发，高效很多，使用也很方便。使用了这种方式，传统的 UI 开发方式再也回不去了。</p></li><li><p><strong>状态管理</strong><br>使用 <code>mutableStateOf</code> 实现响应式更新，或者使用 <code>derivedStateOf</code> 实现派生状态的处理。</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> pagIndex <span class="keyword">by</span> remember &#123; mutableStateOf(<span class="number">0</span>) &#125;</span><br><span class="line"><span class="keyword">var</span> errorState <span class="keyword">by</span> remember &#123; mutableStateOf&lt;String?&gt;(<span class="literal">null</span>) &#125;   </span><br><span class="line"><span class="keyword">val</span> themeState <span class="keyword">by</span> mineViewModel.appTheme.collectAsState()</span><br><span class="line"><span class="keyword">val</span> uiChecked <span class="keyword">by</span> remember(themeState) &#123; derivedStateOf &#123; themeState == ThemeConstants.DARK &#125; &#125;</span><br></pre></td></tr></table></figure></li></ol><ol><li><strong>导航</strong></li></ol><p>实现 <code>Compose Navigator</code> 统一路由管理<br><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> gotoDebug: () -&gt; <span class="built_in">Unit</span> = &#123;</span><br><span class="line">    navController.navigate(Routes.Debug())</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> goToPostDetail: (Post) -&gt; <span class="built_in">Unit</span> = &#123; it -&gt;</span><br><span class="line">    navController.navigate(Routes.PostDetail(title = it.title, it.url))</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure></p><ol><li><strong>Kotlin Flow</strong><br>简化异步编程，让网络请求的代码看起来更直观<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">loadAllPost</span><span class="params">()</span></span>: Flow&lt;List&lt;PostV2&gt;&gt; = load(<span class="string">&quot;allPosts&quot;</span>) &#123;</span><br><span class="line">    postApi.getAllPost()?.<span class="keyword">data</span> ?: emptyList()</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">getAllPost</span><span class="params">()</span></span>: SearchResponse? = request&lt;SearchResponse&gt;(getUrl(<span class="string">&quot;api/search.json&quot;</span>))</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">suspend</span> <span class="keyword">inline</span> <span class="function"><span class="keyword">fun</span> <span class="type">&lt;<span class="keyword">reified</span> T&gt;</span> <span class="title">request</span><span class="params">(url: <span class="type">String</span>)</span></span>: T? &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">try</span> &#123;</span><br><span class="line">        client.<span class="keyword">get</span>(url).body()</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e: Exception) &#123;</span><br><span class="line">        <span class="keyword">if</span> (e <span class="keyword">is</span> CancellationException) <span class="keyword">throw</span> e</span><br><span class="line">        e.printStackTrace()</span><br><span class="line">        <span class="literal">null</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ol><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>经过一番各种折腾，将很多在工作上无法使用的能力（Koin、Flow、DataStore……）都体验使用了一下，在业余的时间完成了基于博客文章构建的 App 在三个平台上的开发，实际上最初我也想搭建 WebJs 的平台的，后面删除掉了，因为涉及到 web 平台开发的各种库相比客户端少很多，兼容起来也比较费劲。KMP/CMP 这块技术确实是能很大地节省开发人力，多端使用同一份UI逻辑代码，部分逻辑也可以用 kotlin 统一进行封装，后续维护也会方便很多。但这里有个缺点就是涉及到的库所需要的 kotlin/Java 版本要求比较高，除非开发一些独立的 App，否则公司里的项目想基于这些技术去实现不太大可能。以及如果所需要的能力比较依赖与原生，比如音视频领域就有一定的局限性，总体来讲更适合偏交互业务的开发。</p><p><strong>项目源码</strong>: <a href="https://github.com/VomPom/blog_kmp">https://github.com/VomPom/blog_kmp</a>  </p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;用-CMP-构建跨平台博客应用：一次-Kotlin-的全栈实践&quot;&gt;&lt;a href=&quot;#用-CMP-构建跨平台博客应用：一次-Kotlin-的全栈实践&quot; class=&quot;headerlink&quot; title=&quot;用 CMP 构建跨平台博客应用：一次 Kotlin 的全栈实</summary>
      
    
    
    
    <category term="技术文章" scheme="http://julis.wang/categories/technology/"/>
    
    
    <category term="KMP" scheme="http://julis.wang/tags/KMP/"/>
    
  </entry>
  
  <entry>
    <title>[鸿蒙]写了个基于Hexo博客的鸿蒙App</title>
    <link href="http://julis.wang/2025/05/16/%E9%B8%BF%E8%92%99-%E5%86%99%E4%BA%86%E4%B8%AA%E5%9F%BA%E4%BA%8EHexo%E5%8D%9A%E5%AE%A2%E7%9A%84%E9%B8%BF%E8%92%99App/"/>
    <id>http://julis.wang/2025/05/16/%E9%B8%BF%E8%92%99-%E5%86%99%E4%BA%86%E4%B8%AA%E5%9F%BA%E4%BA%8EHexo%E5%8D%9A%E5%AE%A2%E7%9A%84%E9%B8%BF%E8%92%99App/</id>
    <published>2025-05-16T12:10:00.000Z</published>
    <updated>2026-02-09T10:49:11.605Z</updated>
    
    <content type="html"><![CDATA[<p>最近部门也在跟进<a href="https://www.harmonyos.com/">鸿蒙</a>平台的业务开发，自己主要是做 Android 开发，主要使用 Kotlin/Java 语言。，需要对新的开发平台和开发模式进行学习，在业余时间开了个项目练手，做了个基于 Hexo 博客内容开发的App。鸿蒙主要使用<a href="https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts">ArkTS语言</a>和<a href="https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkui">ArkUI框架</a>进行开发，有使用 <a href="https://developer.android.com/compose">Jetpack Compose</a> 和 JavaScript/TypeScript 的开发经验的话，上手会比较的轻松。本文主要介绍做的App功能以及对鸿蒙开发的一个总结。</p><h2 id="App-简介"><a href="#App-简介" class="headerlink" title="App 简介"></a>App 简介</h2><p>后台数据来自 <a href="https://hexo.io/">Hexo</a> 生成的博客文章，利用 <a href="https://github.com/ryanuo/hexo-generator-wxapi">hexo-generator-wxapi</a> 生成 api .json 文件，再利用 <a href="https://www.qiniu.com/">七牛云</a> 提供对图片和 .json 文件 CDN。</p><p>实现的功能</p><ul><li>博客列表分页加载</li><li>文章详情加载</li><li>文章按分类/标签展示</li><li>文章内容统计</li><li>深色/浅色模式切换</li><li>数据本地缓存</li></ul><h3 id="功能预览"><a href="#功能预览" class="headerlink" title="功能预览"></a>功能预览</h3><div class="table-container"><table><thead><tr><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody><tr><td><img src="https://cdn.julis.wang/github/blog_harmony/light_blog_list.jpeg"  alt="博客列表" /></td><td><img src="https://cdn.julis.wang/github/blog_harmony/light_stats.jpeg"  alt="统计" /></td><td><img src="https://cdn.julis.wang/github/blog_harmony/light_mine.jpeg" alt="个人" /></td><td><img src="https://cdn.julis.wang/github/blog_harmony/light_detail.jpeg"  alt="文章详情" /></td><td><img src="https://cdn.julis.wang/github/blog_harmony/light_category.jpeg"  alt="分类" /></td><td><img src="https://cdn.julis.wang/github/blog_harmony/light_tag.jpeg"  alt="标签" /></td></tr></tbody></table></div><h3 id="依赖项"><a href="#依赖项" class="headerlink" title="依赖项"></a>依赖项</h3><h4 id="Hexo"><a href="#Hexo" class="headerlink" title="Hexo"></a>Hexo</h4><ul><li><a href="https://hexo.io/">Hexo</a> 快速、简洁且高效的博客框架</li><li><a href="https://github.com/ryanuo/hexo-generator-wxapi">hexo-generator-wxapi</a> 用于将 Hexo 博客内容生成 api 风格的.json文件</li><li><a href="https://www.qiniu.com/">七牛云</a> 提供对图片和.json文件 CDN加速</li></ul><h4 id="HarmonyOS"><a href="#HarmonyOS" class="headerlink" title="HarmonyOS"></a>HarmonyOS</h4><ul><li><a href="https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts">ArkTS</a> ArkTS在TypeScript（简称TS）生态基础上做了进一步扩展，保持了TS的基本风格，同时通过规范定义强化开发期静态检查和分析，提升代码健壮性，并实现更好的程序执行稳定性和性能。</li><li><a href="https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkui">ArkUI</a>  ArkUI（方舟UI框架）为应用的UI开发提供了完整的基础设施，包括简洁的UI语法、丰富的UI功能（组件、布局、动画以及交互事件），以及实时界面预览工具等，可以支持开发者进行可视化界面开发。</li><li><a href="https://gitee.com/openharmony-sig/ohos_pull_to_refresh">ohos_pull_to_refresh</a> 列表加载/刷新控件(没有’No more’的状态)</li><li><a href="https://github.com/Tencent/MMKV">MMKV</a> 是基于 mmap 内存映射的 key-value 组件</li></ul><h2 id="鸿蒙开发总结"><a href="#鸿蒙开发总结" class="headerlink" title="鸿蒙开发总结"></a>鸿蒙开发总结</h2><h3 id="ArkTs-语言"><a href="#ArkTs-语言" class="headerlink" title="ArkTs 语言"></a>ArkTs 语言</h3><p>ArkTS 是 TypeScript 的超集，TypeScript 又是 JavaScript 的超集，所以对于基本数据类型使用的是 TypeScript 语法。他们三者的关系如下图所示：</p><p>  <img src="https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtybbs/713/408/959/0030086000713408959.20241009110308.85777546121432927171131630988896:50001231000000:2800:D599CEDEC4315A859E47A08CEC5D4D3E334431F82ABC9FD0E9B6AD0F91CD2FF5.png" width="80%" alt="ArkTS与TypeScript的关系" /></p><p>相关的差异可以参考社区话题讨论 <a href="https://developer.huawei.com/consumer/cn/forum/topic/0203163854317501934">ArkTS与Typescript的区别？</a></p><p>这里主要记录一下自己使用过程中踩过的坑：</p><h4 id="基本语言类型"><a href="#基本语言类型" class="headerlink" title="基本语言类型"></a>基本语言类型</h4><p>Number 和 number 是两个不同的类型，Number 是 JavaScript 中的一个全局对象，可以使用 new Number() 来创建一个 Number 对象。同理对于 String 和 string，Boolean 和 boolean 也是一样的，大写开头的是<strong>包装对象类型</strong>，小写的是<strong>原始类型</strong>，这点Java/kotlin也有类似的包装对象比较好理解，但 Object 居然也有大小写之区分相比难理解点，写代码的时候好几次忽略了这个事，<strong>Object</strong> 是所有对象的基类，object 表示非原始类型（即不是 number、string、boolean、symbol、null 或 undefined 的所有类型）。可以是任何对象、数组、函数、类实例等。<br><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">let obj: object;</span><br><span class="line">obj = &#123; a: 1 &#125;;         // ✅ 正确：普通对象</span><br><span class="line">obj = [1, 2, 3];        // ✅ 正确：数组</span><br><span class="line">obj = () =&gt; &#123;&#125;;         // ✅ 正确：函数</span><br><span class="line">obj = new Date();       // ✅ 正确：类实例</span><br><span class="line"></span><br><span class="line">obj = 42;               // ❌ 错误：原始类型 number</span><br><span class="line">obj = &quot;hello&quot;;          // ❌ 错误：原始类型 string</span><br></pre></td></tr></table></figure></p><div class="table-container"><table><thead><tr><th style="text-align:left">特性</th><th style="text-align:left"><code>ArkTS</code></th><th style="text-align:left"><code>JavaScript</code></th></tr></thead><tbody><tr><td style="text-align:left"><strong>允许的值</strong></td><td style="text-align:left">仅非原始类型（对象、数组等）</td><td style="text-align:left">任意类型（包括原始值）</td></tr><tr><td style="text-align:left"><strong>原始值处理</strong></td><td style="text-align:left">禁止使用原始值</td><td style="text-align:left">自动装箱（如 <code>42</code> → <code>Number</code>）</td></tr><tr><td style="text-align:left"><strong>使用场景</strong></td><td style="text-align:left">明确限制为非原始类型时</td><td style="text-align:left">极少使用（通常用 <code>unknown</code> 或具体类型替代）</td></tr></tbody></table></div><h4 id="Map-等集合类当作普通-JavaScript-对象来操作"><a href="#Map-等集合类当作普通-JavaScript-对象来操作" class="headerlink" title="Map 等集合类当作普通 JavaScript 对象来操作"></a>Map 等集合类当作普通 JavaScript 对象来操作</h4><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> map = <span class="keyword">new</span> <span class="title class_">Map</span>&lt;<span class="built_in">string</span>, <span class="built_in">object</span>&gt;();</span><br><span class="line">map[<span class="string">&quot;key&quot;</span>] = value;            <span class="comment">// ❌ 错误用法！</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(map.<span class="title function_">get</span>(<span class="string">&quot;biz&quot;</span>));   <span class="comment">// ❌ 输出 undefined</span></span><br></pre></td></tr></table></figure><p>最开始挺奇怪的 map 明明设置了值，但是对应的 map size 为0，遍历 map 也没有数据。后来才发现是这种方式 不会 触发 Map 的内部机制，而是绕过了 Map 的方法，直接操作对象的属性，赋值后，键值对 不会 被存入 Map 的真实存储中，而是作为对象的普通属性存在。正确的用法是：</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> map = <span class="keyword">new</span> <span class="title class_">Map</span>&lt;<span class="built_in">string</span>, <span class="built_in">object</span>&gt;();</span><br><span class="line">map.<span class="property">set</span>[<span class="string">&quot;key&quot;</span>] = value;        <span class="comment">//  ✅ 正确用法！</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(map.<span class="title function_">get</span>(<span class="string">&quot;biz&quot;</span>));   <span class="comment">//  ✅ 输出 value</span></span><br></pre></td></tr></table></figure><h4 id="struct-的困扰"><a href="#struct-的困扰" class="headerlink" title="struct 的困扰"></a>struct 的困扰</h4><p>在 js 里面是没有 <code>struct</code> 这个关键词的，从刚接触到现在它唯一的作用就是：和 <code>@Component</code>绑定声明一个UI控件。例如：</p><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">export</span> struct <span class="title class_">ToolBar</span>&#123;&#125;</span><br></pre></td></tr></table></figure><p><code>@Component</code> 和 <code>struct</code> 两则缺一不可，既然必须有 <code>@Component</code>来标注这是一个UI控件，为什么不能下面这样呢？能省掉一个关键字。<br><figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">ToolBar</span> &#123;&#125;</span><br></pre></td></tr></table></figure><br>同样困扰的人还有很多，这里有一份讨论<a href="https://developer.huawei.com/consumer/cn/forum/topic/0204135301248599134">定义组件时的stuct关键字是什么？</a><br>官方也有一份聊胜于无的介绍</p><blockquote><p>struct和class的区别是什么? </p><p>struct只在自定义组件中使用，@Component装饰的struct就是自定义组件，自定义组件和class是两个概念，自定义组件没有类型，也不能等同于class。如果开发者需要使用组件作为参数在组件之间传递，可以使用自定义占位节点。</p></blockquote><p>我猜测这样是为了省掉对<code>@Component</code>装饰器编译的工作量，如果使用 class 声明，那么声明的UI控件就有“面向对象”的能力，实际上只希望它是一个UI控件声明，不需要它有其他的能力。难道不能对 <code>@Component</code> 装饰过的对象收回“面向对象”的能力么？当然能啊，估计要做很多编译检查的事儿。另外，从开发理解的层面上来讲，它确实也已经不是”对象”了，它只是一个干巴巴的一个UI结构，所以干脆就搞了一个新的关键词 struct。</p><h3 id="ArkUI-框架"><a href="#ArkUI-框架" class="headerlink" title="ArkUI 框架"></a>ArkUI 框架</h3><p>整体框架使用的方式和 <a href="https://developer.android.com/compose">Jetpack Compose</a> 类似，都是声明式UI框架。compose 里面使用  <code>@Composable</code>来标记某个方法这个方法便成了<code>UI控件</code>，控件里面的状态管理使用 <code>remember</code>+ <code>mutableState</code>来控制。而 ArkUI 通过 @State、@Link、@Prop 等装饰器来控制。了解了这些个装饰器的用法，基本上就能理解 ArkUI 的开发流程了。</p><h4 id="构建-UI-的-Component-Builder"><a href="#构建-UI-的-Component-Builder" class="headerlink" title="构建 UI 的 @Component @Builder"></a>构建 UI 的 @Component @Builder</h4><p>@Component 和 @Builder 组合起来实现的差不多就是 Compose 里面使用  <code>@Composable</code> 装饰某个方法的作用，用于构建 UI 或可复用的逻辑单元。<br><strong>@Component</strong><br>用于创建一个自定义组件，组件可以包含独立的 UI 结构、状态管理和生命周期。</p><p><strong>@Builder</strong><br>定义可复用的 UI 片段，用于创建一个UI 构建函数，封装一段可复用的 UI 代码块。不是独立组件，而是嵌入到其他组件或布局中执行，主要作用是复用和逻辑隔离，例如：关于页面，里面的文本是差不多的样式，只是内容不一样，那么只需要保留一个 text 属性出来接收参数。或者某块UI比较复杂，可以抽离一部分UI成为一个独立的UI逻辑模块。</p><h3 id="构建-UI-的状态控制装饰器"><a href="#构建-UI-的状态控制装饰器" class="headerlink" title="构建 UI 的状态控制装饰器"></a>构建 UI 的状态控制装饰器</h3><p><strong>@State</strong><br>比较常用的装饰器，和 Compose 里面 remember+mutableStateOf 的作用差不多，对应的值改变之后，对相关的使用到该属性UI的地方进行刷新。</p><p><strong>@Prop</strong><br>@Prop 装饰的变量和父组件建立单向的同步关系，@Prop变量允许在本地修改，但修改后的变化不会同步回父组件。</p><p>也就是在某个 @Component 的组件内有一个 @State 装饰的属性，传递到子 @Component 组件 @Prop 修饰的属性。子控件对这个属性修改之后，父控件不会对这个改变感知，父控件UI不会改变。</p><p><strong>@Link</strong><br>子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。<br>跟 @Prop 的作用类似，不过是双向的，子控件对这个属性修改之后，父控件会感知这个变化，父控件UI会随着这个属性改变而改变。</p><p><strong>@BuilderParam</strong><br>主要用于动态注入 UI 构建逻辑（即 @Builder 函数），实现父组件向子组件传递可定制的 UI 片段，也就是向子控件传递 UI 参数。</p><p>基本上比较常用到的就这些，还有很多例如：@LocalBuilder @StorageLink @Styles等，都是为了解决开发过过程中遇到的问题，但是只要掌握了 ArkUI UI组件的声明周期和状态管理的基本原理理解其他装饰器还是比较简单的。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>总体开发体验下来，鸿蒙开发学习成本并不是特别高，比较快能上手，但设计的 api 更像一个缝合怪，且使用上不太收敛。很多库还需要再建设，例如音视频开发对应的支持库还不是特别成熟。不过，作为一个从头搞的生态来说能实现成这样已经很不错了，就像此前武磊登陆西甲，以及目前被看好的青年新星王钰栋，都是”自己的孩子”，需要迈出第一步。现在，很多公司也在适配鸿蒙了，期待未来能从 Android 跟 iOS 的生态中争夺出一片大市场。</p><p>项目源码：<a href="https://github.com/VomPom/blog_harmony">https://github.com/VomPom/blog_harmony</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近部门也在跟进&lt;a href=&quot;https://www.harmonyos.com/&quot;&gt;鸿蒙&lt;/a&gt;平台的业务开发，自己主要是做 Android 开发，主要使用 Kotlin/Java 语言。，需要对新的开发平台和开发模式进行学习，在业余时间开了个项目练手，做了个基于 H</summary>
      
    
    
    
    <category term="技术文章" scheme="http://julis.wang/categories/technology/"/>
    
    
    <category term="鸿蒙" scheme="http://julis.wang/tags/%E9%B8%BF%E8%92%99/"/>
    
  </entry>
  
  <entry>
    <title>KV-存储之mmkv</title>
    <link href="http://julis.wang/2025/03/30/KV-%E5%AD%98%E5%82%A8%E4%B9%8Bmmkv/"/>
    <id>http://julis.wang/2025/03/30/KV-%E5%AD%98%E5%82%A8%E4%B9%8Bmmkv/</id>
    <published>2025-03-30T03:38:00.000Z</published>
    <updated>2025-05-20T11:46:57.000Z</updated>
    
    <content type="html"><![CDATA[<p>在平时的业务中，需要用到轻量级存储业务中的数据（例如设置数据存储），绝大多数时候 Anroid 管法提供的 <a href="https://developer.android.com/reference/android/content/SharedPreferences">SharedPreferences</a>  组件就能实现，但针对一些需要高效的场景它就不那么使用了，不适合存储大量数据、多线程操作的不安全性、数据明文不安全性，以及不支持多进程之间的调用等各种问题。<br><a href="https://github.com/Tencent/MMKV">MMKV</a>的诞生就是为了解决以上的问题，本文主要对 MMKV 源码的学习知识点进行一些总结。</p><h2 id="核心设计与原理"><a href="#核心设计与原理" class="headerlink" title="核心设计与原理"></a>核心设计与原理</h2><p>在官方的开源工程中可以看到如下的一些介绍</p><blockquote><p>MMKV 是基于 mmap 内存映射的 key-value 组件，底层序列化/反序列化使用 protobuf 实现，性能高，稳定性强。从 2015 年中至今在微信上使用，其性能和稳定性经过了时间的验证。</p></blockquote><h3 id="传统I-O与-mmap"><a href="#传统I-O与-mmap" class="headerlink" title="传统I/O与 mmap"></a>传统I/O与 mmap</h3><p>mmap 这个是 mmkv 实现的核心，没有 mmap 那么就没有 mmkv。对于 <code>SharedPreferences</code>的实现来说，每次的数据更新都将操作本地文件，而本地文件的写入是通过传统的I/O实现。要理解两者的实现差异，需要先理解 Linux <strong>用户空间与内核空间</strong>设计。</p><h4 id="用户空间与内核空间"><a href="#用户空间与内核空间" class="headerlink" title="用户空间与内核空间"></a><strong>用户空间与内核空间</strong></h4><p>Linux的进程是相互独立的，一个进程是不能直接操作或者访问别一个进程空间的。每个进程空间还分为用户空间和内核（Kernel）空间，相当于把Kernel和上层的应用程序抽像的隔离开。</p><p><strong>用户空间</strong>和<strong>内核空间</strong>，用户空间是用户程序代码运行的地方，内核空间是内核代码运行的地方。为了安全，它们是隔离的，即使用户的程序崩溃了，内核也不受影响。</p><p>这里有两个隔离，一个进程间是相互隔离的，二是进程内有用户空间和内核空间的隔离。</p><p>进程间，用户空间的数据不可共享，所以用户空间 = 不可共享空间<br>进程间，内核空间的数据可共享，所以内核空间 = 可共享空间，所以Linux系统的内存通常是MemFree+Cache<br>所有进程共用1个内核空间。</p><h4 id="传统I-O读写流程"><a href="#传统I-O读写流程" class="headerlink" title="传统I/O读写流程"></a><strong>传统I/O读写流程</strong></h4><p>常规文件读写操作（调用read/fread等函数）过程如下：</p><ul><li><p>进程发起读写文件请求。</p></li><li><p>内核通过查找进程文件符表，定位到内核已打开文件集上的文件信息，从而找到此文件的<code>inode</code>。</p></li><li><p><code>inode</code> 在 <code>address_space</code> 上查找要请求的文件页是否已经缓存在页缓存中。如果存在，则直接返回这片文件页的内容。</p></li><li><p>如果不存在，则通过 <code>inode</code> 定位到文件磁盘地址，将数据从磁盘复制到页缓存。之后再次发起读页面过程，进而将页缓存中的数据发给用户进程。</p><blockquote><p><strong>什么是 inode</strong> ?</p><p>全称为 index node，既<strong>存储文件元信息的区域</strong>，中文译名“索引节点”。<br>包含：文件权限、文件拥有者的UID、文件的大小等等。</p><img src="https://cdn.julis.wang/blog/img/ee519ba873acf3f80fd4ccec86ed72e7.png"></blockquote></li></ul><p>总结来说，常规文件操作为了提高读写效率和保护磁盘，使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中，由于页缓存处在内核空间，不能被用户进程直接寻址访问，所以还需要将页缓存中的数据页再次拷贝到用户空间中。这样，通过了两次数据拷贝过程，才能完成<strong>进程</strong>对<strong>文件</strong>内容的访问。</p><h4 id="mmap基本概念和原理"><a href="#mmap基本概念和原理" class="headerlink" title="mmap基本概念和原理"></a><strong>mmap基本概念和原理</strong></h4><p>内存映射（mmap），就是<strong>将文件的磁盘扇区映射到进程的虚拟内存空间</strong>的过程，即将一个文件映射到进程的虚拟空间，实现文件磁盘地址和进程虚拟空间中一段虚拟地址的一一对应关系。实现这样的映射关系后，进程就可以采用指针的方式读写操作这一段内存，而系统会自动回写脏页面到对应的文件磁盘上，即完成了对文件的操作而不必再调用read,write等系统调用函数。</p><img src="https://cdn.julis.wang/blog/img/mmap_1.png"><p>由上图可知，进程的虚拟地址空间，由多个虚拟内存区域构成。每个虚拟内存区域都是进程在虚拟地址空间中的一个同质区间，即具有同样特性的连续地址范围。上图中所示的text数据段（代码段）、初始数据段、BSS数据段、堆、栈和内存映射，都是一个独立的虚拟内存区域。内存映射的地址空间处在堆栈之间的空余部分。</p><p>linux内核使用 <code>vm_area_struc</code>t 结构来表示一个独立的虚拟内存区域，由于每个不同质的虚拟内存区域功能和内部机制都不同，因此一个进程使用多个 <code>vm_area_struct</code> 结构来分别表示不同类型的虚拟内存区域。各个 <code>vm_area_struct</code> 结构使用链表或者树形结构链接，方便进程快速访问，如下图所示：</p><img src="https://cdn.julis.wang/blog/img/mmap_2_1.png"><p><code>vm_area_struct</code> 结构中包含区域起始和终止地址以及其他相关信息。这样，进程对某一虚拟内存区域的任何操作需要用要的信息，都可以从 <code>vm_area_struct</code> 中获得。mmap函数就是要创建一个新的 <code>vm_area_struct</code> 结构，并将其与文件的物理磁盘地址相连。</p><p>mmap内存映射的实现过程，总的来说可以分为三个阶段：</p><p><strong>阶段一：进程启动映射过程，并在虚拟地址空间中为映射创建虚拟映射区域</strong></p><ul><li>进程在用户空间调用mmap库函数</li></ul><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> *<span class="title">mmap</span><span class="params">(<span class="type">void</span> *addr, <span class="type">size_t</span> length, <span class="type">int</span> prot, <span class="type">int</span> flags, <span class="type">int</span> fd, <span class="type">off_t</span> offset)</span></span>;</span><br></pre></td></tr></table></figure><p><code>addr</code>：指定映射的虚拟内存地址，可以设置为 NULL，让内核自动选择合适的虚拟内存地址</p><p><code>length</code>：映射的长度。</p><p><code>prot</code>：映射内存的保护模式，可选值如下：  </p><p><code>flags</code>：指定映射的类型</p><p><code>fd</code>：进行映射的文件句柄。</p><p><code>offset</code>：文件偏移量（从文件的何处开始映射）</p><ul><li><p>在当前进程的虚拟地址空间中，寻找一段空闲的满足要求的连续的虚拟地址</p></li><li><p>为此虚拟区分配一个 <code>vm_area_struct</code> 结构，接着对这个结构的各个域进行了初始化</p></li><li><p>将新创建的虚拟区结构 <code>vm_area_struct</code> 对象插入到进程的虚拟地址区域链表/树中</p></li></ul><p><strong>阶段二：调用内核空间的mmap函数（不同于用户空间函数），实现文件物理地址和进程虚拟地址的一一映射关系</strong></p><ul><li>为映射分配了新的虚拟地址区域后，通过待映射的文件指针，在文件描述符表中找到对应的文件描述符，通过文件描述符，链接到内核“已打开文件集”中该文件的文件结构体（struct file），每个文件结构体维护着和这个已打开文件相关各项信息。</li><li><p>为映射分配了新的虚拟地址区域后，通过待映射的文件指针，在文件描述符表中找到对应的文件描述符，通过文件描述符，链接到内核“已打开文件集”中该文件的文件结构体（struct file），每个文件结构体维护着和这个已打开文件相关各项信息。</p></li><li><p>通过该文件的文件结构体，链接到 <code>file_operations</code> 模块，调用内核mmap函数，其原型为：</p></li></ul><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">int mmap(struct file *filp, struct vm_area_struct *vma) //不同于用户空间mmap库函数</span><br></pre></td></tr></table></figure><ul><li><p>内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。</p></li><li><p>通过 <code>remap_pfn_range</code> 函数建立页表，即实现了文件地址和虚拟地址区域的映射关系。此时，这片虚拟地址并没有任何数据关联到物理内存(主存)中。</p></li></ul><blockquote><p>主存</p><p>主存储器（Main memory），简称主存。是计算机硬件的一个重要部件，其作用是存放指令和数据，并能由中央处理器（CPU）直接随机存取</p></blockquote><p><strong>阶段三：进程发起对这片映射地址空间的访问，引发缺页异常，实现文件内容到主存（物理内存）的拷贝</strong></p><blockquote><p>前两个阶段仅在于创建虚拟区间并完成地址映射，但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时</p></blockquote><ul><li><p>进程的读或写操作访问虚拟地址空间这一段映射地址，通过查询页表，发现这一段地址并不在物理页上。因为目前只建立了地址映射，真正的硬盘数据还没有拷贝到内存中，因此引发缺页异常。</p></li><li><p>缺页异常进行一系列判断，确定无非法操作后，内核发起请求调页过程。</p></li><li><p>调页过程先在交换缓存空间（swap cache）中寻找需要访问的内存页，如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。</p></li><li><p>之后进程即可对这片主存进行读或者写的操作，如果写操作改变了其内容，一定时间后系统会自动回写脏页面到对应磁盘地址，也即完成了写入到文件的过程。</p></li></ul><blockquote><p>修改过的脏页面并不会立即更新回文件中，而是有一段时间的延迟，可以调用<code>msync()</code>来强制同步, 这样所写的内容就能立即保存到文件里了</p></blockquote><p>常规文件操作需要从磁盘到内核空间页缓存再到用户空间主存的两次数据拷贝。而mmap文件映射，只需要从磁盘到用户空间主存的一次数据拷贝过程。mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程，因此 mmap 效率更高。</p><p>以上是 mmap 的基本概念和原理，搞明白了这些才能看明白整个 mmkv 里面的逻辑处理</p><h3 id="mmkv-一次-put-的流程"><a href="#mmkv-一次-put-的流程" class="headerlink" title="mmkv 一次 put 的流程"></a>mmkv 一次 put 的流程</h3><p>mmkv初始化比较简单，主要涉及到一些配置的初始化，文件夹创建等，其中最重要的逻辑 mmap 调用被封装到一个 <code>MemoryFile</code>到对象里面 </p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">MemoryFile::mmap</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">auto</span> oldPtr = m_ptr;</span><br><span class="line">    <span class="keyword">auto</span> mode = m_readOnly ? PROT_READ : (PROT_READ | PROT_WRITE);</span><br><span class="line">    m_ptr = (<span class="type">char</span> *) ::<span class="built_in">mmap</span>(m_ptr, m_size, mode, MAP_SHARED, m_diskFile.m_fd, <span class="number">0</span>);</span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>主要记录一下一次 put 任务的流程，以 <code>mmkv.putInt(&quot;int&quot;, 1)</code>为例，进过 JNI 的调用到了</p><p><strong>native-birdge.cpp</strong></p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function">MMKV_JNI jboolean <span class="title">encodeInt</span><span class="params">(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint value)</span> </span>&#123;</span><br><span class="line">    MMKV *kv = <span class="built_in">reinterpret_cast</span>&lt;MMKV *&gt;(handle);</span><br><span class="line">    <span class="keyword">if</span> (kv &amp;&amp; oKey) &#123;</span><br><span class="line">        string key = <span class="built_in">jstring2string</span>(env, oKey);</span><br><span class="line">        <span class="keyword">return</span> (jboolean) kv-&gt;<span class="built_in">set</span>((<span class="type">int32_t</span>) value, key);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> (jboolean) <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>进入了<strong>MMVK.cpp</strong>的 </p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">MMKV::set</span><span class="params">(<span class="type">int32_t</span> value, MMKVKey_t key, <span class="type">uint32_t</span> expireDuration)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">isKeyEmpty</span>(key)) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">size_t</span> size = <span class="built_in">mmkv_unlikely</span>(m_enableKeyExpire) ? Fixed32Size + <span class="built_in">pbInt32Size</span>(value) : <span class="built_in">pbInt32Size</span>(value);</span><br><span class="line">    <span class="function">MMBuffer <span class="title">data</span><span class="params">(size)</span></span>;</span><br><span class="line">    <span class="function">CodedOutputData <span class="title">output</span><span class="params">(data.getPtr(), size)</span></span>;</span><br><span class="line">    output.<span class="built_in">writeInt32</span>(value);</span><br><span class="line">    <span class="comment">// ... 省略一些校验逻辑</span></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">setDataForKey</span>(std::<span class="built_in">move</span>(data), key);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这一步主要是准备一下数据，并使用 <code>MMBuffer</code> <code>CodedOutputData</code>将写入的数据进行一次包装（不仅仅是 key-value，还有数据size等等），实际调用在<code>setDataForKey</code></p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">MMKV::setDataForKey</span><span class="params">(MMBuffer &amp;&amp;data, MMKVKey_t key, <span class="type">bool</span> isDataHolder)</span> </span>&#123;</span><br><span class="line">    <span class="built_in">checkLoadData</span>(); <span class="comment">// 状态同步相关的逻辑</span></span><br><span class="line">    <span class="keyword">if</span> (m_crypter) &#123;</span><br><span class="line">      <span class="comment">// ... 省略加密的处理逻辑</span></span><br><span class="line">    &#125; <span class="keyword">else</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">auto</span> itr = m_dic-&gt;<span class="built_in">find</span>(key);</span><br><span class="line">        <span class="keyword">if</span> (itr != m_dic-&gt;<span class="built_in">end</span>()) &#123;</span><br><span class="line">            <span class="comment">// compare data before appending to file</span></span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">isCompareBeforeSetEnabled</span>()) &#123;</span><br><span class="line">                <span class="keyword">auto</span> basePtr = (<span class="type">uint8_t</span> *) (m_file-&gt;<span class="built_in">getMemory</span>()) + Fixed32Size;</span><br><span class="line">                MMBuffer oldValueData = itr-&gt;second.<span class="built_in">toMMBuffer</span>(basePtr);</span><br><span class="line">                <span class="keyword">if</span> (isDataHolder) &#123;</span><br><span class="line">                    <span class="function">CodedInputData <span class="title">inputData</span><span class="params">(oldValueData.getPtr(), oldValueData.length())</span></span>;</span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        <span class="comment">// read extra holder header bytes and to real MMBuffer</span></span><br><span class="line">                        oldValueData = CodedInputData::<span class="built_in">readRealData</span>(oldValueData);</span><br><span class="line">                        <span class="keyword">if</span> (oldValueData == data) &#123;</span><br><span class="line">                            <span class="comment">// MMKVInfo(&quot;[key] %s, set the same data&quot;, key.c_str());</span></span><br><span class="line">                            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125; <span class="built_in">catch</span> (std::exception &amp;exception) &#123;</span><br><span class="line">                        <span class="built_in">MMKVWarning</span>(<span class="string">&quot;compareBeforeSet exception: %s&quot;</span>, exception.<span class="built_in">what</span>());</span><br><span class="line">                    &#125; <span class="built_in">catch</span> (...) &#123;</span><br><span class="line">                        <span class="built_in">MMKVWarning</span>(<span class="string">&quot;compareBeforeSet fail&quot;</span>);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                     ...</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="type">bool</span> onlyOneKey = !<span class="built_in">isMultiProcess</span>() &amp;&amp; m_dic-&gt;<span class="built_in">size</span>() == <span class="number">1</span>;</span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">mmkv_likely</span>(!m_enableKeyExpire)) &#123;</span><br><span class="line">                ...</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                KVHolderRet_t ret;</span><br><span class="line">                <span class="keyword">if</span> (onlyOneKey) &#123;</span><br><span class="line">                    ret = <span class="built_in">overrideDataWithKey</span>(data, key, isDataHolder);</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    ret = <span class="built_in">appendDataWithKey</span>(data, key, isDataHolder);</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">if</span> (!ret.first) &#123;</span><br><span class="line">                    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                itr = m_dic-&gt;<span class="built_in">find</span>(key);</span><br><span class="line">                <span class="keyword">if</span> (itr != m_dic-&gt;<span class="built_in">end</span>()) &#123;</span><br><span class="line">                    itr-&gt;second = std::<span class="built_in">move</span>(ret.second);</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="comment">// in case filterExpiredKeys() is triggered</span></span><br><span class="line">                    m_dic-&gt;<span class="built_in">emplace</span>(key, std::<span class="built_in">move</span>(ret.second));</span><br><span class="line">                    <span class="built_in">mmkv_retain_key</span>(key);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">           ...</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    m_hasFullWriteback = <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里面的代码逻辑很长，做了很多 if-else 的逻辑，最终走向两个大分支：</p><p>key 是新增的走 <code>appendDataWithKey</code></p><p>key 将会覆盖原来的将会走 <code>overrideDataWithKey</code></p><p>有这两个分支，主要是因为 mmkv 存储采用的  <a href="https://protobuf.com.cn/">protobuf 协议</a>，另外有一个很重要的方法也在这里执行了：<code>checkLoadData();</code>  安卓里面的多进程实现，将需要这里的一些逻辑，在 mmkv多进程原理篇进行讲解。</p><p><code>appendDataWithKey</code> 转换为 <code>MMBuffer</code>并继续向下执行</p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function">KVHolderRet_t <span class="title">MMKV::appendDataWithKey</span><span class="params">(<span class="type">const</span> MMBuffer &amp;data, MMKVKey_t key, <span class="type">bool</span> isDataHolder)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">auto</span> keyData = <span class="built_in">MMBuffer</span>((<span class="type">void</span> *) key.<span class="built_in">data</span>(), key.<span class="built_in">size</span>(), MMBufferNoCopy);</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">doAppendDataWithKey</span>(data, keyData, isDataHolder, <span class="built_in">static_cast</span>&lt;<span class="type">uint32_t</span>&gt;(keyData.<span class="built_in">length</span>()));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>doAppendDataWithKey</code> 里面的代码也很长，不过也就只做一件事：将k-v值写入到文件里面做准备，真正的写入逻辑在 <code>m_output-&gt;writeData(keyData);</code>，这里先后调用了两次 <code>writeData</code>,是先写入key再写入了 value。</p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function">KVHolderRet_t</span></span><br><span class="line"><span class="function"><span class="title">MMKV::doAppendDataWithKey</span><span class="params">(<span class="type">const</span> MMBuffer &amp;data, <span class="type">const</span> MMBuffer &amp;keyData, <span class="type">bool</span> isDataHolder, <span class="type">uint32_t</span> originKeyLength)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">auto</span> isKeyEncoded = (originKeyLength &lt; keyData.<span class="built_in">length</span>());</span><br><span class="line">    <span class="keyword">auto</span> keyLength = <span class="built_in">static_cast</span>&lt;<span class="type">uint32_t</span>&gt;(keyData.<span class="built_in">length</span>());</span><br><span class="line">    <span class="keyword">auto</span> valueLength = <span class="built_in">static_cast</span>&lt;<span class="type">uint32_t</span>&gt;(data.<span class="built_in">length</span>());</span><br><span class="line">    <span class="keyword">if</span> (isDataHolder) &#123;</span><br><span class="line">        valueLength += <span class="built_in">pbRawVarint32Size</span>(valueLength);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// size needed to encode the key</span></span><br><span class="line">    <span class="type">size_t</span> size = isKeyEncoded ? keyLength : (keyLength + <span class="built_in">pbRawVarint32Size</span>(keyLength));</span><br><span class="line">    <span class="comment">// size needed to encode the value</span></span><br><span class="line">    size += valueLength + <span class="built_in">pbRawVarint32Size</span>(valueLength);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">SCOPED_LOCK</span>(m_exclusiveProcessLock);</span><br><span class="line"></span><br><span class="line">    <span class="type">bool</span> hasEnoughSize = <span class="built_in">ensureMemorySize</span>(size);</span><br><span class="line">    <span class="keyword">if</span> (!hasEnoughSize || !<span class="built_in">isFileValid</span>()) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">make_pair</span>(<span class="literal">false</span>, <span class="built_in">KeyValueHolder</span>());</span><br><span class="line">    &#125;</span><br><span class="line">...</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (isKeyEncoded) &#123;</span><br><span class="line">            m_output-&gt;<span class="built_in">writeRawData</span>(keyData);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            m_output-&gt;<span class="built_in">writeData</span>(keyData);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (isDataHolder) &#123;</span><br><span class="line">            m_output-&gt;<span class="built_in">writeRawVarint32</span>((<span class="type">int32_t</span>) valueLength);</span><br><span class="line">        &#125;</span><br><span class="line">        m_output-&gt;<span class="built_in">writeData</span>(data); <span class="comment">// note: write size of data</span></span><br><span class="line">    &#125; </span><br><span class="line">    ...</span><br><span class="line">    m_actualSize += size;</span><br><span class="line">    <span class="built_in">updateCRCDigest</span>(ptr, size);</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">make_pair</span>(<span class="literal">true</span>, <span class="built_in">KeyValueHolder</span>(originKeyLength, valueLength, offset));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>writeData</code> 进行了两步先写入数据的 <strong>长度信息</strong>，再写入真实的数据，这里还是因为  <a href="https://protobuf.com.cn/">protobuf 协议</a>设计相关</p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">CodedOutputData::writeData</span><span class="params">(<span class="type">const</span> MMBuffer &amp;value)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>-&gt;<span class="built_in">writeRawVarint32</span>((<span class="type">int32_t</span>) value.<span class="built_in">length</span>());</span><br><span class="line">    <span class="keyword">this</span>-&gt;<span class="built_in">writeRawData</span>(value);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最终走到了<code>writeRawData</code> 关键代码</p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">CodedOutputData::writeRawData</span><span class="params">(<span class="type">const</span> MMBuffer &amp;data)</span> </span>&#123;</span><br><span class="line">    <span class="type">size_t</span> numberOfBytes = data.<span class="built_in">length</span>();</span><br><span class="line">    <span class="keyword">if</span> (m_position + numberOfBytes &gt; m_size) &#123;</span><br><span class="line">       ...</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="built_in">memcpy</span>(m_ptr + m_position, data.<span class="built_in">getPtr</span>(), numberOfBytes);</span><br><span class="line">    m_position += numberOfBytes;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>核心逻辑使用 <code>memcpy</code> 将数据直接通过 memcpy 直接在内存层面进行拷贝，而这里的 <code>m_ptr</code>就是最开始通过<code>mmap</code>创建出来的指针！！到这里一次写入基本上就结束了。</p><h3 id="mmkv-一次-get-的流程"><a href="#mmkv-一次-get-的流程" class="headerlink" title="mmkv 一次 get 的流程"></a>mmkv 一次 get 的流程</h3><p>依然先通过 JNI走到</p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function">MMKV_JNI jint <span class="title">decodeInt</span><span class="params">(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint defaultValue)</span> </span>&#123;</span><br><span class="line">    MMKV *kv = <span class="built_in">reinterpret_cast</span>&lt;MMKV *&gt;(handle);</span><br><span class="line">    <span class="keyword">if</span> (kv &amp;&amp; oKey) &#123;</span><br><span class="line">        string key = <span class="built_in">jstring2string</span>(env, oKey);</span><br><span class="line">        <span class="keyword">return</span> (jint) kv-&gt;<span class="built_in">getInt32</span>(key, defaultValue);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> defaultValue;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>再到 mmkv getInt32</p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">int32_t</span> <span class="title">MMKV::getInt32</span><span class="params">(MMKVKey_t key, <span class="type">int32_t</span> defaultValue, <span class="type">bool</span> *hasValue)</span> </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">    <span class="built_in">SCOPED_LOCK</span>(m_lock);</span><br><span class="line">    <span class="built_in">SCOPED_LOCK</span>(m_sharedProcessLock);</span><br><span class="line">    <span class="keyword">auto</span> data = <span class="built_in">getDataForKey</span>(key);</span><br><span class="line">    <span class="keyword">if</span> (data.<span class="built_in">length</span>() &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="function">CodedInputData <span class="title">input</span><span class="params">(data.getPtr(), data.length())</span></span>;</span><br><span class="line">            <span class="keyword">if</span> (hasValue != <span class="literal">nullptr</span>) &#123;</span><br><span class="line">                *hasValue = <span class="literal">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> input.<span class="built_in">readInt32</span>();</span><br><span class="line">        &#125; </span><br><span class="line">        ...</span><br><span class="line">    <span class="keyword">return</span> defaultValue;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>getRawDataForKey</code>方法，主要有两个分支，一种是加密逻辑，另一种是非加密逻辑，但他们流程都差不多从一个  map 里面根据 key 获取一个对象（这个对象暂时并不是 get 最终的返回值），那这个 map 是从哪里来的呢？</p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function">MMBuffer <span class="title">MMKV::getRawDataForKey</span><span class="params">(MMKVKey_t key)</span> </span>&#123;</span><br><span class="line">    <span class="built_in">checkLoadData</span>();</span><br><span class="line"><span class="meta">#<span class="keyword">ifndef</span> MMKV_DISABLE_CRYPT</span></span><br><span class="line">    <span class="keyword">if</span> (m_crypter) &#123;</span><br><span class="line">        <span class="keyword">auto</span> itr = m_dicCrypt-&gt;<span class="built_in">find</span>(key);</span><br><span class="line">        <span class="keyword">if</span> (itr != m_dicCrypt-&gt;<span class="built_in">end</span>()) &#123;</span><br><span class="line">            <span class="keyword">auto</span> basePtr = (<span class="type">uint8_t</span> *) (m_file-&gt;<span class="built_in">getMemory</span>()) + Fixed32Size;</span><br><span class="line">            <span class="keyword">return</span> itr-&gt;second.<span class="built_in">toMMBuffer</span>(basePtr, m_crypter);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">auto</span> itr = m_dic-&gt;<span class="built_in">find</span>(key);</span><br><span class="line">        <span class="keyword">if</span> (itr != m_dic-&gt;<span class="built_in">end</span>()) &#123;</span><br><span class="line">            <span class="keyword">auto</span> basePtr = (<span class="type">uint8_t</span> *) (m_file-&gt;<span class="built_in">getMemory</span>()) + Fixed32Size;</span><br><span class="line">            <span class="keyword">return</span> itr-&gt;second.<span class="built_in">toMMBuffer</span>(basePtr);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    MMBuffer nan;</span><br><span class="line">    <span class="keyword">return</span> nan;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从源码里面溯源<code>m_dicCrypt</code>和 <code>m_dic</code> 是在 MMKV 初始化的时候生成的，主要逻辑在 <code>MMKV_IO .cpp</code>里面的 <code>loadFromFile</code>方法内：</p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">MMKV::loadFromFile</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="built_in">loadMetaInfoAndCheck</span>();</span><br><span class="line">...</span><br><span class="line">    <span class="keyword">if</span> (!m_file-&gt;<span class="built_in">isFileValid</span>()) &#123;</span><br><span class="line">        m_file-&gt;<span class="built_in">reloadFromFile</span>(m_expectedCapacity);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (!m_file-&gt;<span class="built_in">isFileValid</span>()) &#123;</span><br><span class="line">        <span class="built_in">MMKVError</span>(<span class="string">&quot;file [%s] not valid&quot;</span>, m_path.<span class="built_in">c_str</span>());</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="type">bool</span> loadFromFile = <span class="literal">false</span>, needFullWriteback = <span class="literal">false</span>;</span><br><span class="line">        <span class="built_in">checkDataValid</span>(loadFromFile, needFullWriteback);</span><br><span class="line">        ...</span><br><span class="line">        <span class="keyword">auto</span> ptr = (<span class="type">uint8_t</span> *) m_file-&gt;<span class="built_in">getMemory</span>();</span><br><span class="line">        <span class="comment">// loading</span></span><br><span class="line">        <span class="keyword">if</span> (loadFromFile &amp;&amp; m_actualSize &gt; <span class="number">0</span>) &#123;</span><br><span class="line">...</span><br><span class="line">            <span class="function">MMBuffer <span class="title">inputBuffer</span><span class="params">(ptr + Fixed32Size, m_actualSize, MMBufferNoCopy)</span></span>;</span><br><span class="line">            <span class="keyword">if</span> (m_crypter) &#123;</span><br><span class="line">                <span class="built_in">clearDictionary</span>(m_dicCrypt);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="built_in">clearDictionary</span>(m_dic);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (needFullWriteback) &#123;</span><br><span class="line"><span class="meta">#<span class="keyword">ifndef</span> MMKV_DISABLE_CRYPT</span></span><br><span class="line">                <span class="keyword">if</span> (m_crypter) &#123;</span><br><span class="line">                    MiniPBCoder::<span class="built_in">greedyDecodeMap</span>(*m_dicCrypt, inputBuffer, m_crypter);</span><br><span class="line">                &#125; <span class="keyword">else</span></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line">                &#123;</span><br><span class="line">                    MiniPBCoder::<span class="built_in">greedyDecodeMap</span>(*m_dic, inputBuffer);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="meta">#<span class="keyword">ifndef</span> MMKV_DISABLE_CRYPT</span></span><br><span class="line">                <span class="keyword">if</span> (m_crypter) &#123;</span><br><span class="line">                    MiniPBCoder::<span class="built_in">decodeMap</span>(*m_dicCrypt, inputBuffer, m_crypter);</span><br><span class="line">                &#125; <span class="keyword">else</span></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line">                &#123;</span><br><span class="line">                    MiniPBCoder::<span class="built_in">decodeMap</span>(*m_dic, inputBuffer);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            m_output = <span class="keyword">new</span> <span class="built_in">CodedOutputData</span>(ptr + Fixed32Size, m_file-&gt;<span class="built_in">getFileSize</span>() - Fixed32Size);</span><br><span class="line">            m_output-&gt;<span class="built_in">seek</span>(m_actualSize);</span><br><span class="line">            <span class="keyword">if</span> (needFullWriteback) &#123;</span><br><span class="line">                <span class="built_in">fullWriteback</span>();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// file not valid or empty, discard everything</span></span><br><span class="line">            <span class="built_in">SCOPED_LOCK</span>(m_exclusiveProcessLock);</span><br><span class="line"></span><br><span class="line">            m_output = <span class="keyword">new</span> <span class="built_in">CodedOutputData</span>(ptr + Fixed32Size, m_file-&gt;<span class="built_in">getFileSize</span>() - Fixed32Size);</span><br><span class="line">            <span class="keyword">if</span> (m_actualSize &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="built_in">writeActualSize</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="literal">nullptr</span>, IncreaseSequence);</span><br><span class="line">                <span class="built_in">sync</span>(MMKV_SYNC);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="built_in">writeActualSize</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="literal">nullptr</span>, KeepSequence);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">...</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    m_needLoadFromFile = <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>总统来说就是在初始化的时候就会将基于<code>protobuf</code>协议的本地文件里面的数据加载到内存，并将其放在一个 map 内，方便后续使用。</p><p>回到 <code>int32_t MMKV::getInt32()</code>通过 <code>getDataForKey(key)</code>获取到一个<code>MMBuffer</code>对象，并通过 <strong>CodedInputData</strong>进行反序列化操作，读取 <strong>Varint32</strong> 的 <strong>valueSize</strong> 值，随后不断循环通过 <strong>CodedInputData</strong>  读取到<strong>value</strong> 值。</p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">int32_t</span> <span class="title">MMKV::getInt32</span><span class="params">(MMKVKey_t key, <span class="type">int32_t</span> defaultValue, <span class="type">bool</span> *hasValue)</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">auto</span> data = <span class="built_in">getDataForKey</span>(key);</span><br><span class="line">    <span class="keyword">if</span> (data.<span class="built_in">length</span>() &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="function">CodedInputData <span class="title">input</span><span class="params">(data.getPtr(), data.length())</span></span>;</span><br><span class="line">            <span class="keyword">if</span> (hasValue != <span class="literal">nullptr</span>) &#123;</span><br><span class="line">                *hasValue = <span class="literal">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> input.<span class="built_in">readInt32</span>();</span><br><span class="line">        &#125; </span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br><span class="line">  ...</span><br><span class="line">    <span class="keyword">return</span> defaultValue;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="mmkv-与-SharedPreferences"><a href="#mmkv-与-SharedPreferences" class="headerlink" title="mmkv 与 SharedPreferences"></a>mmkv 与 SharedPreferences</h2><p>以下是 <strong>MMKV</strong> 与 <strong>SharedPreferences</strong> 的优劣势对比总结，结合性能、安全性、功能支持等核心维度进行分析：</p><h3 id="性能对比"><a href="#性能对比" class="headerlink" title="性能对比"></a><strong>性能对比</strong></h3><div class="table-container"><table><thead><tr><th style="text-align:left"><strong>维度</strong></th><th style="text-align:left"><strong>SharedPreferences</strong></th><th style="text-align:left"><strong>MMKV</strong></th></tr></thead><tbody><tr><td style="text-align:left"><strong>读写速度</strong></td><td style="text-align:left">慢（同步 I/O，多次数据拷贝）</td><td style="text-align:left">快（<code>mmap</code> 零拷贝，内存直接操作）</td></tr><tr><td style="text-align:left"><strong>线程安全</strong></td><td style="text-align:left">需自行加锁（<code>apply()</code> 异步写入仍有风险）</td><td style="text-align:left">内置多线程锁（文件锁 + 内存锁）</td></tr><tr><td style="text-align:left"><strong>大数据量支持</strong></td><td style="text-align:left">性能急剧下降（全量 XML 解析/序列化）</td><td style="text-align:left">高效（增量更新，Protobuf 编码）</td></tr></tbody></table></div><h3 id="安全性与稳定性"><a href="#安全性与稳定性" class="headerlink" title="安全性与稳定性"></a><strong>安全性与稳定性</strong></h3><div class="table-container"><table><thead><tr><th style="text-align:left"><strong>维度</strong></th><th style="text-align:left"><strong>SharedPreferences</strong></th><th style="text-align:left"><strong>MMKV</strong></th></tr></thead><tbody><tr><td style="text-align:left"><strong>数据加密</strong></td><td style="text-align:left">无（明文存储）</td><td style="text-align:left">支持 AES-128/AES-256 加密</td></tr><tr><td style="text-align:left"><strong>崩溃恢复</strong></td><td style="text-align:left">可能因异常导致 XML 损坏</td><td style="text-align:left">通过 CRC 校验 + 备份文件保障完整性</td></tr><tr><td style="text-align:left"><strong>系统版本适配</strong></td><td style="text-align:left">部分版本有 ANR 问题（如 <code>apply()</code>）</td><td style="text-align:left">无系统级兼容性问题</td></tr></tbody></table></div><h3 id="功能支持"><a href="#功能支持" class="headerlink" title="功能支持"></a><strong>功能支持</strong></h3><div class="table-container"><table><thead><tr><th style="text-align:left"><strong>维度</strong></th><th style="text-align:left"><strong>SharedPreferences</strong></th><th style="text-align:left"><strong>MMKV</strong></th></tr></thead><tbody><tr><td style="text-align:left"><strong>多进程</strong></td><td style="text-align:left">不支持（跨进程数据不同步）</td><td style="text-align:left">支持（通过文件锁 + <code>mmap</code> 共享内存）</td></tr><tr><td style="text-align:left"><strong>数据类型</strong></td><td style="text-align:left">仅支持基本类型（int/String 等）</td><td style="text-align:left">支持基本类型、二进制数据（MMBuffer）</td></tr><tr><td style="text-align:left"><strong>加密存储</strong></td><td style="text-align:left">明文存储（XML）</td><td style="text-align:left">支持 AES 加密（可选）</td></tr><tr><td style="text-align:left"><strong>增量更新</strong></td><td style="text-align:left">全量写入（即使只改一个键值）</td><td style="text-align:left">仅追加新数据，定期整理</td></tr></tbody></table></div><p>从上面的对比看看，mmkv 在很多层面都是领先 SharedPreferences 的，那么 mmkv 是否有缺陷呢？答案是有的。</p><blockquote><p>任何的操作系统、任何的软件，在往磁盘写数据的过程中如果发生了意外——例如程序崩溃，或者断电关机——磁盘里的文件就会以这种写了一半的、不完整的形式被保留。写了一半的数据怎么用啊？没法用，这就是文件的损坏。这种问题是不可能避免的，MMKV 虽然由于底层机制的原因，在程序崩溃的时候不会影响数据往磁盘的写入，但断电关机之类的操作系统级别的崩溃，MMKV 就没办法了，文件照样会损坏。对于这种文件损坏，SharedPreferences 和 DataStore 的应对方式是在每次写入新数据之前都对现有文件做一次自动备份，这样在发生了意外出现了文件损坏之后，它们就会把备份的数据恢复过来；而 MMKV，没有这种自动的备份和恢复，那么当文件发生了损坏，数据就丢了，之前保存的各种信息只能被重置。也就是说，MMKV 是唯一会丢数据的方案。</p></blockquote><p>在 mmkv 里面有 <a href="https://info.support.huawei.com/info-finder/encyclopedia/zh/CRC.html">CRC</a> 校验，如果不通过的话，将会废弃掉之前所有的数据。在 mmkv 里面也有人反馈：<a href="https://github.com/Tencent/MMKV/issues/729">https://github.com/Tencent/MMKV/issues/729</a> 在写入的过程中因为一些特殊情况写入失败，会导致本地的文件损坏且不可recovery。</p><p>那有什么办法避免这个问题呢？有大佬开源另一个 KV 框架 <a href="https://github.com/BillyWei01/FastKV">FastKV</a>对这个问题进行了处理，采用通过double-write等方法确保数据的完整性，原理是数据依次写入A/B两个文件，如果写入A过程中崩溃，B仍是完整的，如果A完整写入了，则B写入时崩溃也不要紧。这种实现方式理论上是不错的，不太清楚 mmkv 为什么没有采取这样的逻辑。不过这个库并没有经过大量业务进行验证，只能作为一个学习的方案先看看。</p><p>另外谷歌已经开发了新的KV存储框架<a href="https://cloud.google.com/datastore/docs/concepts/overview?hl=zh-cn">DataStore</a>，<code>SharedPreferences</code>也将渐渐地退出历史的舞台了。不过 DataStore 的性能目前仍然没有 mmkv 的好。关于这三者的比较可以查看： <a href="https://juejin.cn/post/7112268981163016229">《Android 的键值对存储有没有最优解？》</a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这篇文章深入剖析了 <strong>MMKV</strong>（腾讯开源的高性能键值存储组件）的核心设计与实现原理，重点对比了传统 I/O 与 <code>mmap</code> 内存映射的差异，并详细分析了 MMKV 的读写流程以及和 SharedPreferences 的各方面对比。</p><p><strong>参考</strong></p><p><a href="https://juejin.cn/post/7112268981163016229">《Android 的键值对存储有没有最优解？》</a></p><p><a href="https://yangjie2.github.io/2021/11/14/mmap%E5%8E%9F%E7%90%86%E4%B8%8E%E5%BA%94%E7%94%A8/">《mmap原理与应用》</a></p><p><a href="https://blog.csdn.net/zhanglh046/article/details/115603788">《文件内存映射和传统I/O机制》</a></p><p><a href="https://blog.csdn.net/luo_boke/article/details/109311432">Android 内存映射mmap浅谈</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在平时的业务中，需要用到轻量级存储业务中的数据（例如设置数据存储），绝大多数时候 Anroid 管法提供的 &lt;a href=&quot;https://developer.android.com/reference/android/content/SharedPreferences&quot;</summary>
      
    
    
    
    
    <category term="mmap" scheme="http://julis.wang/tags/mmap/"/>
    
  </entry>
  
  <entry>
    <title>RetroFit2 源码学习相关</title>
    <link href="http://julis.wang/2025/03/17/Learn-from-RetroFit/"/>
    <id>http://julis.wang/2025/03/17/Learn-from-RetroFit/</id>
    <published>2025-03-17T12:28:00.000Z</published>
    <updated>2025-05-20T11:46:57.000Z</updated>
    
    <content type="html"><![CDATA[<p>研究 <a href="https://github.com/square/retrofit">retrofit</a> 目标：理解动态代理、注解、反射、学习它所用到的设计模式，达到自己能手写它的核心实现。</p><p>最近终于有点精力能够去研究研究源码了， 真的是写的一个非常好的的开源库，以前刚接触安卓的时候扒拉过相关的源码，但是随着工作了几年之后，经验的积累，让我对源码里面的东西能够体会更深刻，自己也尝试去手写里面的核心实现，看完源码对整体的架构理解了之后，以为自己能很顺利的写下来，实则不然。<br>知识还是需要知行合一，这篇文章主要记录 <a href="https://github.com/square/retrofit">retrofit</a>  的一些知识点。</p><h3 id="retrofit-的设计模式"><a href="#retrofit-的设计模式" class="headerlink" title="retrofit 的设计模式"></a>retrofit 的设计模式</h3><p>retrofit 里面中使用了多种设计模式，以实现其灵活、可扩展和高性能的特性：</p><div class="table-container"><table><thead><tr><th style="text-align:left">设计模式</th><th style="text-align:left">应用场景</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left"><strong>建造者模式</strong></td><td style="text-align:left"><code>Retrofit.Builder</code></td><td style="text-align:left">灵活配置 Retrofit 实例</td></tr><tr><td style="text-align:left"><strong>工厂模式</strong></td><td style="text-align:left"><code>Converter.Factory</code>、<code>CallAdapter.Factory</code></td><td style="text-align:left">创建 Converter 和 CallAdapter 实例</td></tr><tr><td style="text-align:left"><strong>动态代理模式</strong></td><td style="text-align:left">接口方法转换为 HTTP 请求</td><td style="text-align:left">运行时生成接口代理对象</td></tr><tr><td style="text-align:left"><strong>适配器模式</strong></td><td style="text-align:left"><code>CallAdapter</code></td><td style="text-align:left">将 <code>Call</code> 适配为其他类型</td></tr><tr><td style="text-align:left"><strong>装饰器模式</strong></td><td style="text-align:left"><code>OkHttp</code> 拦截器</td><td style="text-align:left">增强 HTTP 请求和响应的功能</td></tr><tr><td style="text-align:left"><strong>观察者模式</strong></td><td style="text-align:left">与 <code>RxJava</code> 或 <code>LiveData</code> 结合</td><td style="text-align:left">实现异步数据流的订阅和通知</td></tr><tr><td style="text-align:left"><strong>策略模式</strong></td><td style="text-align:left"><code>Converter</code> 和 <code>CallAdapter</code> 选择</td><td style="text-align:left">动态选择数据转换或调用适配策略</td></tr><tr><td style="text-align:left"><strong>单例模式</strong></td><td style="text-align:left"><code>Retrofit</code> 实例共享</td><td style="text-align:left">确保全局只有一个 Retrofit 实例</td></tr><tr><td style="text-align:left"><strong>模板方法模式</strong></td><td style="text-align:left"><code>Call</code> 的实现</td><td style="text-align:left">定义 HTTP 请求的执行流程</td></tr></tbody></table></div><p><strong>retrofit 的动态代理模式</strong></p><p>retrofit 用了诸多的设计模式，其中最经典的莫过于动态代理模式了，在了解 retrofit 之前，我一直以为这样的网络请求形式是最直观的，参考以前写的<a href="https://julis.wang/2019/05/13/%E5%9F%BA%E4%BA%8EVolley%E6%A1%86%E6%9E%B6%E7%9A%84%E8%BF%94%E5%9B%9E%E6%95%B0%E6%8D%AE%E7%9A%84%E8%8C%83%E5%9E%8B%E5%A4%84%E7%90%86/">基于Volley框架的返回数据的范型处理</a></p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">Request.<span class="keyword">get</span>(</span><br><span class="line">    url = url,</span><br><span class="line">    params = param,</span><br><span class="line">    listener = <span class="keyword">object</span> : OnRequestListener&lt;Data&gt; &#123;</span><br><span class="line">        <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onSuccess</span><span class="params">(commonData: <span class="type">CommonData</span>?, <span class="keyword">data</span>: <span class="type">Data</span>?)</span></span> &#123;&#125;</span><br><span class="line">        <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onFailure</span><span class="params">(errorCode: <span class="type">Int</span>, errorMessage: <span class="type">String</span>?)</span></span> &#123;&#125;</span><br><span class="line">    &#125;)</span><br></pre></td></tr></table></figure><p>以为这样很直观，逻辑也很清晰，实则 代码冗余，回调嵌套，如果有多个连续的请求，代码会变得难以维护，而 retrofit 搭配上协程能这样实现：</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> <span class="keyword">data</span> = apiService.getXXX(params)</span><br></pre></td></tr></table></figure><p>简单到不能再简单，<code>动态代理</code>功不可没，上面的 apiService 是一个接口，由：<code>retrofit.create(ApiInterface::class.java)</code> 生成其实例，动态代理其核心实现：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> &lt;T&gt; T <span class="title function_">create</span><span class="params">(<span class="keyword">final</span> Class&lt;T&gt; service)</span> &#123;</span><br><span class="line">    validateServiceInterface(service);</span><br><span class="line">    <span class="keyword">return</span> (T)</span><br><span class="line">        Proxy.newProxyInstance(</span><br><span class="line">            service.getClassLoader(),</span><br><span class="line">            <span class="keyword">new</span> <span class="title class_">Class</span>&lt;?&gt;[] &#123;service&#125;,</span><br><span class="line">            <span class="keyword">new</span> <span class="title class_">InvocationHandler</span>() &#123;</span><br><span class="line">              <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">Platform</span> <span class="variable">platform</span> <span class="operator">=</span> Platform.get();</span><br><span class="line">              <span class="keyword">private</span> <span class="keyword">final</span> Object[] emptyArgs = <span class="keyword">new</span> <span class="title class_">Object</span>[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line">              <span class="meta">@Override</span></span><br><span class="line">              <span class="keyword">public</span> <span class="meta">@Nullable</span> Object <span class="title function_">invoke</span><span class="params">(Object proxy, Method method, <span class="meta">@Nullable</span> Object[] args)</span> <span class="keyword">throws</span> Throwable &#123;</span><br><span class="line">                <span class="comment">// ....</span></span><br><span class="line">                <span class="keyword">return</span> platform.isDefaultMethod(method)</span><br><span class="line">                    ? platform.invokeDefaultMethod(method, service, proxy, args)</span><br><span class="line">                    : loadServiceMethod(method).invoke(args);</span><br><span class="line">              &#125;</span><br><span class="line">            &#125;);</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>由<code>loadServiceMethod(method).invoke(args)</code> 负责将接口方法（通过 Java 反射获取的 <code>Method</code> 对象）解析并转换为一个可执行的 HTTP 请求。</p><p><code>Proxy.newProxyInstance</code> 方法，参数：</p><ul><li><p>ClassLoader loader 用于加载代理类的类加载器。</p></li><li><p>Class&lt;?&gt;[] interfaces 代理类需要实现的接口数组，代理对象将实现这些接口，并拦截对这些接口方法的调用。只能代理实现了接口的类，不能代理没有接口的类。</p></li><li><p>InvocationHandler h<br>调用处理器，负责处理代理对象上的方法调用。每次调用代理对象的方法时，都会调用 <code>InvocationHandler</code> 的 <code>invoke</code> 方法。对于 Retrofit 的接口我们并没有去“实现”它的方法，所有的逻辑都由<code>` retrofit.create()</code>方法里面返回的 <code>InvocationHandler</code>实现的 <code>invoke</code>方法实现的。</p></li></ul><h3 id="核心实现逻辑"><a href="#核心实现逻辑" class="headerlink" title="核心实现逻辑"></a>核心实现逻辑</h3><h4 id="协程的支持"><a href="#协程的支持" class="headerlink" title="协程的支持"></a><strong>协程的支持</strong></h4><p>Retrofit 支持多种异步编程模型，包括回调、RxJava 和协程等，这里主要记录一下对协程的支持。普通方法和异步逻辑的分叉在：</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> (!isKotlinSuspendFunction) &#123;</span><br><span class="line">      <span class="keyword">return</span> new CallAdapted&lt;&gt;(requestFactory, callFactory, responseConverter, callAdapter);</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (continuationWantsResponse) &#123;</span><br><span class="line">      <span class="comment">//noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.</span></span><br><span class="line">      <span class="keyword">return</span> (HttpServiceMethod&lt;ResponseT, ReturnT&gt;)</span><br><span class="line">          new SuspendForResponse&lt;&gt;(</span><br><span class="line">              requestFactory,</span><br><span class="line">              callFactory,</span><br><span class="line">              responseConverter,</span><br><span class="line">              (CallAdapter&lt;ResponseT, Call&lt;ResponseT&gt;&gt;) callAdapter);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">//noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.</span></span><br><span class="line"><span class="comment">//...</span></span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>上面代码关键变量<code>isKotlinSuspendFunction</code> ，用于判断是否为协程方法（suspend修饰），判断逻辑很简单，只需要判定方法最后一个参数是否为<code>Continuation.class</code> 即可。这里的分叉逻辑都继承自<code>HttpServiceMethod&lt;T&gt;</code>实现 <code>ReturnT adapt(Call&lt;ResponseT&gt; call, Object[] args)</code>这个抽象方法，这也是 retrofit 使用 <strong>适配器模式</strong>的地方，把不同的调用方式进行统一。对于协程方式的调用有实现：</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">protected</span> Object adapt(Call&lt;ResponseT&gt; call, Object[] args) &#123;</span><br><span class="line">      call = callAdapter.adapt(call);</span><br><span class="line"></span><br><span class="line">      <span class="comment">//noinspection unchecked Checked by reflection inside RequestFactory.</span></span><br><span class="line">      Continuation&lt;Response&lt;ResponseT&gt;&gt; continuation =</span><br><span class="line">          (Continuation&lt;Response&lt;ResponseT&gt;&gt;) args[args.length - <span class="number">1</span>];</span><br><span class="line"></span><br><span class="line">      <span class="comment">// See SuspendForBody for explanation about this try/catch.</span></span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> KotlinExtensions.awaitResponse(call, continuation);</span><br><span class="line">      &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        <span class="keyword">return</span>.suspendAndThrow(e, continuation);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="type">&lt;T&gt;</span> Call<span class="type">&lt;T&gt;</span>.<span class="title">awaitResponse</span><span class="params">()</span></span>: Response&lt;T&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> suspendCancellableCoroutine &#123; continuation -&gt;</span><br><span class="line">    continuation.invokeOnCancellation &#123;</span><br><span class="line">      cancel()</span><br><span class="line">    &#125;</span><br><span class="line">    enqueue(<span class="keyword">object</span> : Callback&lt;T&gt; &#123;</span><br><span class="line">      <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onResponse</span><span class="params">(call: <span class="type">Call</span>&lt;<span class="type">T</span>&gt;, response: <span class="type">Response</span>&lt;<span class="type">T</span>&gt;)</span></span> &#123;</span><br><span class="line">        continuation.resume(response)</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onFailure</span><span class="params">(call: <span class="type">Call</span>&lt;<span class="type">T</span>&gt;, t: <span class="type">Throwable</span>)</span></span> &#123;</span><br><span class="line">        continuation.resumeWithException(t)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里就一切都明朗了，实现了 <code>Call</code>的扩展方法，这里的 <code>Call</code>并不是 <code>okhttp3.Call</code>，它只是 retrofit  <code>okhttp3.Call</code>为方便框架整体逻辑的处理而定义的，比如 retrofit 的 <code>Call</code> 是泛型化的，可以直接返回解析后的对象，<code>enqueue</code>同理。</p><p><code>suspendCancellableCoroutine</code>方法是实现协程方法的关键，它可以将基于回调的异步操作封装成一个挂起函数，怎么理解呢？对 扩展方法<code>awaitResponse</code>反编译可以看到方法定义是这样的：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> Object <span class="title function_">await</span><span class="params">(<span class="meta">@NotNull</span> Call $<span class="built_in">this</span>$await, <span class="meta">@NotNull</span> Continuation $completion)</span> </span><br></pre></td></tr></table></figure><p>其实这里跟定义一个 <code>listener</code>去监听方法的回调有点像，这个方法改写成 <code>listener</code>的实现话大概就是这样：</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="type">&lt;T&gt;</span> Call<span class="type">&lt;T&gt;</span>.<span class="title">awaitResponse</span><span class="params">(listener:<span class="type">Listener</span>&lt;<span class="type">T</span>&gt;)</span></span>: Response&lt;T&gt; &#123;</span><br><span class="line">        enqueue(<span class="keyword">object</span> : Callback&lt;T&gt; &#123;</span><br><span class="line">            <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onResponse</span><span class="params">(call: <span class="type">Call</span>&lt;<span class="type">T</span>&gt;, response: <span class="type">Response</span>&lt;<span class="type">T</span>&gt;)</span></span> &#123;</span><br><span class="line">                Listener.resume(response)</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onFailure</span><span class="params">(call: <span class="type">Call</span>&lt;<span class="type">T</span>&gt;, t: <span class="type">Throwable</span>)</span></span> &#123;</span><br><span class="line">                Listener.resumeWithException(t)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可看到改造实现需要传递一个 <code>listener</code>，哪这个 <code>listener</code>是什么？前面其有如何判断一个方法是否为协程的方法的逻辑：判定方法最后一个参数是否为<code>Continuation.class</code> 即可。这里的  <code>listener</code> 其实可以等价于 一个 <code>Continuation</code>实例，kotlin 的协程库帮我们实现了对应的封装，对于使用我们不会直观地感受<code>Continuation</code>的存在，实际它贯穿整个协程。关于协程这里不再赘述，可以查看 <a href="https://juejin.cn/post/7142743424670629895?searchId=202503230943390124BC33C1668EC4B62B">《带着问题分析Kotlin协程原理》</a>了解。</p><h3 id="返回数据格式的解析"><a href="#返回数据格式的解析" class="headerlink" title="返回数据格式的解析"></a><strong>返回数据格式的解析</strong></h3><p>对于<strong>Converter</strong>,在协程和普通方法调用分叉逻辑的前面点：</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">Converter&lt;ResponseBody, ResponseT&gt; responseConverter = createResponseConverter(retrofit, method, responseType);</span><br></pre></td></tr></table></figure><p><code>createResponseConverter</code>之后一路走到</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> &lt;T&gt; Converter&lt;ResponseBody, T&gt; nextResponseBodyConverter(</span><br><span class="line">    <span class="meta">@Nullable</span> Converter.Factory skipPast, Type type, Annotation[] annotations) &#123;</span><br><span class="line">  Objects.requireNonNull(type, <span class="string">&quot;type == null&quot;</span>);</span><br><span class="line">  Objects.requireNonNull(annotations, <span class="string">&quot;annotations == null&quot;</span>);</span><br><span class="line"></span><br><span class="line">  int start = converterFactories.indexOf(skipPast) + <span class="number">1</span>;</span><br><span class="line">  <span class="keyword">for</span> (int i = start, count = converterFactories.size(); i &lt; count; i++) &#123;</span><br><span class="line">    Converter&lt;ResponseBody, ?&gt; converter =</span><br><span class="line">        converterFactories.<span class="keyword">get</span>(i).responseBodyConverter(type, annotations, <span class="keyword">this</span>);</span><br><span class="line">    <span class="keyword">if</span> (converter != <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="comment">//noinspection unchecked</span></span><br><span class="line">      <span class="keyword">return</span> (Converter&lt;ResponseBody, T&gt;) converter;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>converterFactories</code> 的值就是在 retrofit 初始化的时候进行使用  <code>public Builder addConverterFactory(Converter.Factory factory)</code>添加的值。可以看到是按添加到<code>List&lt;Converter.Factory&gt; converterFactories</code>里面的顺序进行选择的，默认<code>GsonConverterFactory</code>实现了利用 <code>Gson</code>进行数据转化 ，如果我们自己实现<code>Converter.Factory</code>的接口的话，那么可以根据一定的规则判断是否要返回我们自定义的 <code>Converter</code>，如果不需要使用就返回 null，会自动匹配下一个能使用的 <code>Converter</code>。注意这里并不会因为前一个  <code>Converter</code> 解析失败而自动尝试使用下一个<code>Converter</code>（当然，你可以在自定义的<code>Converter</code>里面做类似这样的尝试策略）。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><ul><li><p>这篇文章深入剖析了 <strong>Retrofit</strong> 框架的核心设计模式、动态代理机制、协程支持以及数据解析逻辑，通过源码分析和手写实现，帮助读者更好地理解 Retrofit 的工作原理，并强调了理论与实践结合的重要性。</p></li><li><p>为加深对 retrofit 的理解，可以尝试手写核心实现，自己尝试的的代码在 <a href="https://github.com/VomPom/JProject/tree/master/app/src/main/java/wang/julis/jproject/example/source/retrofit2/learn/vmfit">vmfit</a> </p></li><li><p>附一张 retrofit 的全流程图，来源：<a href="https://cloud.tencent.com/developer/article/1683334">https://cloud.tencent.com/developer/article/1683334</a></p></li></ul><img src="https://cdn.julis.wang/blog/img/ru5ssbhumq.jpeg">]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;研究 &lt;a href=&quot;https://github.com/square/retrofit&quot;&gt;retrofit&lt;/a&gt; 目标：理解动态代理、注解、反射、学习它所用到的设计模式，达到自己能手写它的核心实现。&lt;/p&gt;
&lt;p&gt;最近终于有点精力能够去研究研究源码了， 真的是写的一</summary>
      
    
    
    
    <category term="技术文章" scheme="http://julis.wang/categories/technology/"/>
    
    
    <category term="Android" scheme="http://julis.wang/tags/Android/"/>
    
  </entry>
  
  <entry>
    <title>Android屏幕刷新机制</title>
    <link href="http://julis.wang/2025/02/24/Android%E5%B1%8F%E5%B9%95%E5%88%B7%E6%96%B0%E6%9C%BA%E5%88%B6/"/>
    <id>http://julis.wang/2025/02/24/Android%E5%B1%8F%E5%B9%95%E5%88%B7%E6%96%B0%E6%9C%BA%E5%88%B6/</id>
    <published>2025-02-24T02:49:00.000Z</published>
    <updated>2025-05-20T11:46:57.000Z</updated>
    
    <content type="html"><![CDATA[<p>最近在研究 Android 屏幕显示与渲染相关的内容，平时经常看到这些类 <code>ViewRootImpl</code>、<code>Choreographer</code>、<code>Surface</code> 、 <code>SurfaceFlinger</code>等，知道它们都用于屏幕渲染相关，但对它们细节了解较少，相关的文章也比较多，不需要自己完全重新再编写一份，于是对相关内容进行一个总结,<br>主要来源：<a href="https://juejin.cn/post/6863756420380196877">《Android屏幕刷新机制—VSyncChoreographer 全面理解》</a>，这篇博客是我认为是目前看到过最好的一篇，文章由浅入深比较好理解。不过文章里面图片链接资源已经失效，为以后复习相关知识点，在此将其整理删除冗余内容，并对图片资源进行更新。</p><h2 id="一、背景和疑问"><a href="#一、背景和疑问" class="headerlink" title="一、背景和疑问"></a><strong>一、背景和疑问</strong></h2><p>在Android中，当我们谈到 <strong>布局优化</strong>、<strong>卡顿优化</strong> 时，通常都知道 需要减少布局层级、减少主线程耗时操作，这样可以减少<strong>丢帧</strong>。如果丢帧比较严重，那么界面可能会有明显的卡顿感。我们知道 通常手机刷新是每秒60次，即每隔16.6ms刷新一次。 问题来了：</p><ol><li><strong>丢帧</strong>(掉帧) ，是说 这一帧延迟显示 还是丢弃不再显示 ？</li><li>布局层级较多/主线程耗时 是如何造成 丢帧的呢？</li><li>16.6ms刷新一次 是啥意思？是每16.6ms都走一次 measure/layout/draw ？</li><li>measure/layout/draw 走完，界面就立刻刷新了吗?</li><li>如果界面没动静止了，还会刷新吗？</li><li>可能你知道<strong>VSYNC</strong>，这个具体指啥？在屏幕刷新中如何工作的？</li><li>可能你还听过屏幕刷新使用 <strong>双缓存</strong>、<strong>三缓存</strong>，这又是啥意思呢？</li><li>可能你还听过神秘的<strong>Choreographer</strong>，这又是干啥的？</li></ol><h2 id="二、显示系统基础知识"><a href="#二、显示系统基础知识" class="headerlink" title="二、显示系统基础知识"></a><strong>二、显示系统基础知识</strong></h2><p>在一个典型的显示系统中，一般包括CPU、GPU、Display三个部分， CPU负责计算帧数据，把计算好的数据交给GPU，GPU会对图形数据进行渲染，渲染好后放到buffer(图像缓冲区)里存起来，然后Display（屏幕或显示器）负责把buffer里的数据呈现到屏幕上。如下图：</p><img src="https://cdn.julis.wang/blog/img/0nq54q5jtq.jpeg"><p>单缓存，从缓存映射到屏幕。</p><h3 id="2-1-基础概念"><a href="#2-1-基础概念" class="headerlink" title="2.1 基础概念"></a><strong>2.1 基础概念</strong></h3><ul><li><strong>屏幕刷新频率</strong> 一秒内屏幕刷新的次数（一秒内显示了多少帧的图像），单位 Hz（赫兹），如常见的 60 Hz。<strong>刷新频率取决于硬件的固定参数</strong>（不会变的）。</li><li><strong>逐行扫描</strong> 显示器并不是一次性将画面显示到屏幕上，而是从左到右边，从上到下逐行扫描，顺序显示整屏的一个个像素点，不过这一过程快到人眼无法察觉到变化。以 60 Hz 刷新率的屏幕为例，这一过程即 1000 / 60 ≈ 16ms。</li><li><strong>帧率</strong> （Frame Rate） 表示 <strong>GPU 在一秒内绘制操作的帧数</strong>，单位 fps。例如在电影界采用 24 帧的速度足够使画面运行的非常流畅。而 Android 系统则采用更加流程的 60 fps，即每秒钟GPU最多绘制 60 帧画面。帧率是动态变化的，例如当画面静止时，GPU 是没有绘制操作的，屏幕刷新的还是buffer中的数据，即GPU最后操作的帧数据。</li><li><strong>画面撕裂</strong>（tearing） 一个屏幕内的数据来自2个不同的帧，画面会出现撕裂感，如下图</li></ul><img src="https://cdn.julis.wang/blog/img/xxm0lvzypa.jpeg"><p>明显看出画面错位的位置，这就是画面撕裂。</p><h3 id="2-2-双缓存"><a href="#2-2-双缓存" class="headerlink" title="2.2 双缓存"></a><strong>2.2 双缓存</strong></h3><h5 id="2-2-1-画面撕裂-原因"><a href="#2-2-1-画面撕裂-原因" class="headerlink" title="2.2.1  画面撕裂 原因"></a><strong>2.2.1  画面撕裂 原因</strong></h5><p>屏幕刷新频是固定的，比如每16.6ms从buffer取数据显示完一帧，理想情况下帧率和刷新频率保持一致，即每绘制完成一帧，显示器显示一帧。但是CPU/GPU写数据是不可控的，所以会出现buffer里有些数据根本没显示出来就被重写了，即buffer里的数据可能是来自不同的帧的， 当屏幕刷新时，此时它并不知道buffer的状态，因此从buffer抓取的帧并不是完整的一帧画面，即出现画面撕裂。</p><p>简单说就是Display在显示的过程中，buffer内数据被CPU/GPU修改，导致画面撕裂。</p><h5 id="2-2-2-双缓存"><a href="#2-2-2-双缓存" class="headerlink" title="2.2.2  双缓存"></a><strong>2.2.2  双缓存</strong></h5><p>那咋解决画面撕裂呢？答案是使用 双缓存。</p><p>由于图像绘制和屏幕读取 使用的是同个buffer，所以屏幕刷新时可能读取到的是不完整的一帧画面。</p><p><strong>双缓存</strong>，让绘制和显示器拥有各自的buffer：GPU 始终将完成的一帧图像数据写入到 <strong>Back Buffer</strong>，而显示器使用 <strong>Frame Buffer</strong>，当屏幕刷新时，Frame Buffer 并不会发生变化，当Back buffer准备就绪后，它们才进行交换。如下图：</p><img src="https://cdn.julis.wang/blog/img/q2vukxpyvq.jpeg"><p>双缓存，CPU/GPU写数据到Back Buffer，显示器从Frame Buffer取数据</p><h5 id="2-2-3-VSync"><a href="#2-2-3-VSync" class="headerlink" title="2.2.3  VSync"></a><strong>2.2.3  VSync</strong></h5><p>问题又来了：什么时候进行两个buffer的交换呢？</p><p>假如是 Back buffer准备完成一帧数据以后就进行，那么如果此时屏幕还没有完整显示上一帧内容的话，肯定是会出问题的。看来只能是等到屏幕处理完一帧数据后，才可以执行这一操作了。</p><p>当扫描完一个屏幕后，设备需要重新回到第一行以进入下一次的循环，此时有一段时间空隙，称为VerticalBlanking Interval(VBI)。那，这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新，也就避免了交换过程中出现 screen tearing的状况。</p><p><strong>VSync</strong>(垂直同步)是VerticalSynchronization的简写，它利用VBI时期出现的vertical sync pulse（垂直同步脉冲）来保证双缓冲在最佳时间点才进行交换。另外，交换是指各自的内存地址，可以认为该操作是瞬间完成。</p><p>所以说V-sync这个概念并不是Google首创的，它在早年的PC机领域就已经出现了。</p><h2 id="三、Android屏幕刷新机制"><a href="#三、Android屏幕刷新机制" class="headerlink" title="三、Android屏幕刷新机制"></a><strong>三、Android屏幕刷新机制</strong></h2><h3 id="3-1-Android4-1之前的问题"><a href="#3-1-Android4-1之前的问题" class="headerlink" title="3.1 Android4.1之前的问题"></a><strong>3.1 Android4.1之前的问题</strong></h3><p>具体到Android中，在Android4.1之前，屏幕刷新也遵循 上面介绍的 双缓存+VSync 机制。如下图：</p><img src="https://cdn.julis.wang/blog/img/1ax0mz0nu1.jpeg"><p>双缓存会在VSync脉冲时交换，但CPU/GPU绘制是随机的</p><p>以时间的顺序来看下将会发生的过程：</p><ol><li>Display显示第0帧数据，此时CPU和<a href="https://cloud.tencent.com/solution/render?from_column=20065&amp;from=20065">GPU渲染</a>第1帧画面，且在Display显示下一帧前完成</li><li>因为渲染及时，Display在第0帧显示完成后，也就是第1个VSync后，缓存进行交换，然后正常显示第1帧</li><li>接着第2帧开始处理，是直到第2个VSync快来前才开始处理的。</li><li>第2个VSync来时，由于第2帧数据还没有准备就绪，缓存没有交换，显示的还是第1帧。这种情况被Android开发组命名为“Jank”，即发生了<strong>丢帧</strong>。</li><li>当第2帧数据准备完成后，它并不会马上被显示，而是要等待下一个VSync 进行缓存交换再显示。</li></ol><p>所以总的来说，就是屏幕平白无故地多显示了一次第1帧。</p><p>原因是 第2帧的CPU/GPU计算 没能在VSync信号到来前完成 。</p><p>我们知道，<strong>双缓存的交换 是在Vsyn到来时进行，交换后屏幕会取Frame buffer内的新数据，而实际 此时的Back buffer 就可以供GPU准备下一帧数据了。如果 Vsyn到来时  CPU/GPU就开始操作的话，是有完整的16.6ms的，这样应该会基本避免jank的出现了</strong>（除非CPU/GPU计算超过了16.6ms）。  那如何让 CPU/GPU计算在 Vsyn到来时进行呢？</p><h3 id="3-2-drawing-with-VSync"><a href="#3-2-drawing-with-VSync" class="headerlink" title="3.2 drawing with VSync"></a><strong>3.2 drawing with VSync</strong></h3><p>为了优化显示性能，Google在Android 4.1系统中对Android Display系统进行了重构，实现了Project Butter（黄油工程）：系统在收到VSync pulse后，将马上开始下一帧的渲染。即<strong>一旦收到VSync通知（16ms触发一次），CPU和GPU 才立刻开始计算然后把数据写入buffer</strong>。如下图：</p><img src="https://cdn.julis.wang/blog/img/uuqflxwo53.jpeg"><p>VSync脉冲到来：双缓存交换，且开始CPU/GPU绘制 CPU/GPU根据VSYNC信号同步处理数据，可以让CPU/GPU有完整的16ms时间来处理数据，减少了jank。</p><p>一句话总结，<strong>VSync同步使得CPU/GPU充分利用了16.6ms时间，减少jank。</strong></p><p>问题又来了，如果界面比较复杂，CPU/GPU的处理时间较长 超过了16.6ms呢？如下图：</p><img src="https://cdn.julis.wang/blog/img/po2jd1h7u8.jpeg"><p>虽然CPU/GPU开始在VSync，但超过16.6ms</p><ol><li>在第二个时间段内，但却因 GPU 还在处理 B 帧，缓存没能交换，导致 A 帧被重复显示。</li><li>而B完成后，又因为缺乏VSync pulse信号，它只能等待下一个signal的来临。于是在这一过程中，有一大段时间是被浪费的。</li><li>当下一个VSync出现时，CPU/GPU马上执行操作（A帧），且缓存交换，相应的显示屏对应的就是B。这时看起来就是正常的。只不过由于执行时间仍然超过16ms，导致下一次应该执行的缓冲区交换又被推迟了——如此循环反复，便出现了越来越多的“Jank”。</li></ol><p><strong>为什么 CPU 不能在第二个 16ms 处理绘制工作呢？</strong></p><p>原因是只有两个 buffer，Back buffer正在被GPU用来处理B帧的数据， Frame buffer的内容用于Display的显示，这样两个buffer都被占用，CPU 则无法准备下一帧的数据。那么，如果再提供一个buffer，CPU、GPU 和显示设备都能使用各自的buffer工作，互不影响。</p><h3 id="3-3-三缓存"><a href="#3-3-三缓存" class="headerlink" title="3.3 三缓存"></a><strong>3.3 三缓存</strong></h3><p><strong>三缓存</strong>就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区，这样可以最大限度的利用空闲时间，带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。</p><img src="https://cdn.julis.wang/blog/img/ldq7oda57p.jpeg"><p>三缓存</p><ol><li>第一个Jank，是不可避免的。但是在第二个 16ms 时间段，CPU/GPU 使用 <strong>第三个 Buffer</strong> 完成C帧的计算，虽然还是会多显示一次 A 帧，但后续显示就比较顺畅了，有效避免 Jank 的进一步加剧。</li><li>注意在第3段中，A帧的计算已完成，但是在第4个vsync来的时候才显示，如果是双缓冲，那在第三个vynsc就可以显示了。</li></ol><p><strong>三缓冲有效利用了等待vysnc的时间，减少了jank，但是带来了延迟。</strong> 所以，是不是 Buffer 越多越好呢？这个是否定的，Buffer 正常还是两个，当出现 Jank 后三个足以。</p><p>以上就是Android屏幕刷新的原理了。</p><h2 id="四、Choreographer"><a href="#四、Choreographer" class="headerlink" title="四、Choreographer"></a><strong>四、Choreographer</strong></h2><h3 id="4-1-概述"><a href="#4-1-概述" class="headerlink" title="4.1 概述"></a><strong>4.1 概述</strong></h3><p>上面讲到，Google在Android 4.1系统中对Android Display系统进行了优化：在收到VSync pulse后，将马上开始下一帧的渲染。即<strong>一旦收到VSync通知，CPU和GPU就立刻开始计算然后把数据写入buffer</strong>。本节就来讲 “drawing with VSync” 的实现——<strong>Choreographer</strong>。</p><ul><li>Choreographer，意为 舞蹈编导、编舞者。在这里就是指 对CPU/GPU绘制的指导—— 收到VSync信号 才开始绘制，保证绘制拥有完整的16.6ms，避免绘制的随机性。</li><li>Choreographer，是一个Java类，包路径android.view.Choreographer。类注释是“协调动画、输入和绘图的计时”。</li><li>通常 应用层不会直接使用Choreographer，而是使用更高级的API，例如动画和View绘制相关的ValueAnimator.start()、View.invalidate()等。</li><li>业界一般通过Choreographer来监控应用的帧率。</li></ul><h3 id="4-2-源码分析"><a href="#4-2-源码分析" class="headerlink" title="4.2 源码分析"></a><strong>4.2 源码分析</strong></h3><p>学习 Choreographer 可以帮助理解 每帧运行的原理，也可加深对 Handler机制、View绘制流程的理解，这样再去做UI优化、卡顿优化，思路会更清晰。</p><p>好了，下面开始源码分析了~</p><h5 id="4-2-1-入口-和-实例创建"><a href="#4-2-1-入口-和-实例创建" class="headerlink" title="4.2.1 入口 和 实例创建"></a><strong>4.2.1 入口 和 实例创建</strong></h5><p>在<a href="https://juejin.cn/post/7076274407416528909">《Window和WindowManager》</a>、<a href="https://blog.csdn.net/allen_xu_2012_new/article/details/131167564">《Activity的启动过程详解》</a>中介绍过，Activity启动 走完onResume方法后，会进行<strong>window的添加</strong>。window添加过程会 调用ViewRootImpl的setView()方法，setView()方法会调用requestLayout()方法来请求绘制布局，requestLayout()方法内部又会走到scheduleTraversals()方法，最后会走到performTraversals()方法，接着到了我们熟知的测量、布局、绘制三大流程了。</p><p>另外，查看源码发现，当我们使用 ValueAnimator.start()、View.invalidate()时，最后也是走到ViewRootImpl的scheduleTraversals()方法。（View.invalidate()内部会循环获取ViewParent直到ViewRootImpl的invalidateChildInParent()方法，然后走到scheduleTraversals()，可自行查看源码 ）</p><p>即 <strong>所有UI的变化都是走到ViewRootImpl的scheduleTraversals()方法。</strong></p><p>那么问题又来了，scheduleTraversals() 到 performTraversals() 中间 经历了什么呢？是立刻执行吗？答案很显然是否定的，根据我们上面的介绍，在VSync信号到来时才会执行绘制，即performTraversals()方法。下面来瞅瞅这是如何实现的：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">//ViewRootImpl.java</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">scheduleTraversals</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!mTraversalScheduled) &#123;</span><br><span class="line">        <span class="comment">//此字段保证同时间多次更改只会刷新一次，例如TextView连续两次setText(),也只会走一次绘制流程</span></span><br><span class="line">        mTraversalScheduled = <span class="literal">true</span>;</span><br><span class="line">        <span class="comment">//添加同步屏障，屏蔽同步消息，保证VSync到来立即执行绘制</span></span><br><span class="line">        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();</span><br><span class="line">        <span class="comment">//mTraversalRunnable是TraversalRunnable实例，最终走到run()，也即doTraversal();</span></span><br><span class="line">        mChoreographer.postCallback(</span><br><span class="line">                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, <span class="literal">null</span>);</span><br><span class="line">        <span class="keyword">if</span> (!mUnbufferedInputDispatch) &#123;</span><br><span class="line">            scheduleConsumeBatchedInput();</span><br><span class="line">        &#125;</span><br><span class="line">        notifyRendererOfFramePending();</span><br><span class="line">        pokeDrawLockIfNeeded();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">TraversalRunnable</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">        doTraversal();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">final</span> <span class="type">TraversalRunnable</span> <span class="variable">mTraversalRunnable</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">TraversalRunnable</span>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">doTraversal</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (mTraversalScheduled) &#123;</span><br><span class="line">        mTraversalScheduled = <span class="literal">false</span>;</span><br><span class="line">        <span class="comment">//移除同步屏障</span></span><br><span class="line">        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);</span><br><span class="line">        ...</span><br><span class="line">        <span class="comment">//开始三大绘制流程</span></span><br><span class="line">        performTraversals();</span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>主要有以下逻辑：</p><ol><li>首先使用mTraversalScheduled字段保证同时间多次更改只会刷新一次，例如TextView连续两次setText()，也只会走一次绘制流程。</li><li>然后把当前线程的<a href="https://cloud.tencent.com/product/message-queue-catalog?from_column=20065&amp;from=20065">消息队列</a>Queue添加了<strong>同步屏障</strong>，这样就屏蔽了正常的同步消息，保证VSync到来后立即执行绘制，而不是要等前面的同步消息。后面会具体分析同步屏障和异步消息的代码逻辑。</li><li>调用了mChoreographer.postCallback()方法，发送一个会在下一帧执行的回调，即<strong>在下一个VSync到来时会执行TraversalRunnable—&gt;doTraversal()—-&gt;performTraversals()—&gt;绘制流程</strong>。</li></ol><p>接下来，就是分析的重点——Choreographer。我们先看它的实例mChoreographer，是在ViewRootImpl的构造方法内使用Choreographer.getInstance()创建：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">Choreographer mChoreographer;</span><br><span class="line"></span><br><span class="line"><span class="comment">//ViewRootImpl实例是在添加window时创建</span></span><br><span class="line"><span class="keyword">public</span> <span class="title function_">ViewRootImpl</span><span class="params">(Context context, Display display)</span> &#123;</span><br><span class="line">    ...</span><br><span class="line">    mChoreographer = Choreographer.getInstance();</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们先来看看Choreographer.getInstance()：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> Choreographer <span class="title function_">getInstance</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> sThreadInstance.get();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> ThreadLocal&lt;Choreographer&gt; sThreadInstance =</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">ThreadLocal</span>&lt;Choreographer&gt;() &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> Choreographer <span class="title function_">initialValue</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">Looper</span> <span class="variable">looper</span> <span class="operator">=</span> Looper.myLooper();</span><br><span class="line">        <span class="keyword">if</span> (looper == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">//当前线程要有looper，Choreographer实例需要传入</span></span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;The current thread must have a looper!&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">Choreographer</span> <span class="variable">choreographer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Choreographer</span>(looper, VSYNC_SOURCE_APP);</span><br><span class="line">        <span class="keyword">if</span> (looper == Looper.getMainLooper()) &#123;</span><br><span class="line">            mMainInstance = choreographer;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> choreographer;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>看到这里 如你对Handler机制中looper比较熟悉的话，应该知道 Choreographer和Looper一样 是线程单例的。且当前线程要有looper，Choreographer实例需要传入。接着看看Choreographer构造方法：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="title function_">Choreographer</span><span class="params">(Looper looper, <span class="type">int</span> vsyncSource)</span> &#123;</span><br><span class="line">    mLooper = looper;</span><br><span class="line">    <span class="comment">//使用当前线程looper创建 mHandler</span></span><br><span class="line">    mHandler = <span class="keyword">new</span> <span class="title class_">FrameHandler</span>(looper);</span><br><span class="line">    <span class="comment">//USE_VSYNC 4.1以上默认是true，表示 具备接受VSync的能力，这个接受能力就是FrameDisplayEventReceiver</span></span><br><span class="line">    mDisplayEventReceiver = USE_VSYNC</span><br><span class="line">            ? <span class="keyword">new</span> <span class="title class_">FrameDisplayEventReceiver</span>(looper, vsyncSource)</span><br><span class="line">            : <span class="literal">null</span>;</span><br><span class="line">    mLastFrameTimeNanos = Long.MIN_VALUE;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 计算一帧的时间，Android手机屏幕是60Hz的刷新频率，就是16ms</span></span><br><span class="line">    mFrameIntervalNanos = (<span class="type">long</span>)(<span class="number">1000000000</span> / getRefreshRate());</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建一个链表类型CallbackQueue的数组，大小为5，</span></span><br><span class="line">    <span class="comment">//也就是数组中有五个链表，每个链表存相同类型的任务：输入、动画、遍历绘制等任务（CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL）</span></span><br><span class="line">    mCallbackQueues = <span class="keyword">new</span> <span class="title class_">CallbackQueue</span>[CALLBACK_LAST + <span class="number">1</span>];</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt;= CALLBACK_LAST; i++) &#123;</span><br><span class="line">        mCallbackQueues[i] = <span class="keyword">new</span> <span class="title class_">CallbackQueue</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// b/68769804: For low FPS experiments.</span></span><br><span class="line">    setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, <span class="number">1</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码中都有注释，创建了一个mHandler、VSync事件接收器mDisplayEventReceiver、任务链表数组mCallbackQueues。FrameHandler、FrameDisplayEventReceiver、CallbackQueue后面会一一说明。</p><h5 id="4-2-2-安排任务—postCallback"><a href="#4-2-2-安排任务—postCallback" class="headerlink" title="4.2.2 安排任务—postCallback"></a><strong>4.2.2 安排任务—postCallback</strong></h5><p>回头看mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)方法，注意到第一个参数是CALLBACK_TRAVERSAL，表示回调任务的类型，共有以下5种类型：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">//输入事件，首先执行</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">CALLBACK_INPUT</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"><span class="comment">//动画，第二执行</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">CALLBACK_ANIMATION</span> <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"><span class="comment">//插入更新的动画，第三执行</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">CALLBACK_INSETS_ANIMATION</span> <span class="operator">=</span> <span class="number">2</span>;</span><br><span class="line"><span class="comment">//绘制，第四执行</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">CALLBACK_TRAVERSAL</span> <span class="operator">=</span> <span class="number">3</span>;</span><br><span class="line"><span class="comment">//提交，最后执行，</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">CALLBACK_COMMIT</span> <span class="operator">=</span> <span class="number">4</span>;</span><br></pre></td></tr></table></figure><p>五种类型任务对应存入对应的CallbackQueue中，每当收到 VSYNC 信号时，Choreographer 将首先处理 INPUT 类型的任务，然后是 ANIMATION 类型，最后才是 TRAVERSAL 类型。</p><p>postCallback()内部调用postCallbackDelayed()，接着又调用postCallbackDelayedInternal()，来瞅瞅：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">postCallbackDelayedInternal</span><span class="params">(<span class="type">int</span> callbackType,</span></span><br><span class="line"><span class="params">        Object action, Object token, <span class="type">long</span> delayMillis)</span> &#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">synchronized</span> (mLock) &#123;</span><br><span class="line">        <span class="comment">// 当前时间</span></span><br><span class="line">        <span class="keyword">final</span> <span class="type">long</span> <span class="variable">now</span> <span class="operator">=</span> SystemClock.uptimeMillis();</span><br><span class="line">        <span class="comment">// 加上延迟时间</span></span><br><span class="line">        <span class="keyword">final</span> <span class="type">long</span> <span class="variable">dueTime</span> <span class="operator">=</span> now + delayMillis;</span><br><span class="line">        <span class="comment">//取对应类型的CallbackQueue添加任务</span></span><br><span class="line">        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (dueTime &lt;= now) &#123;</span><br><span class="line">            <span class="comment">//立即执行</span></span><br><span class="line">            scheduleFrameLocked(now);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">//延迟运行，最终也会走到scheduleFrameLocked()</span></span><br><span class="line">            <span class="type">Message</span> <span class="variable">msg</span> <span class="operator">=</span> mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);</span><br><span class="line">            msg.arg1 = callbackType;</span><br><span class="line">            msg.setAsynchronous(<span class="literal">true</span>);</span><br><span class="line">            mHandler.sendMessageAtTime(msg, dueTime);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先取对应类型的CallbackQueue添加任务，action就是mTraversalRunnable，token是null。<strong>CallbackQueue的addCallbackLocked()就是把 dueTime、action、token组装成CallbackRecord后 存入CallbackQueue的下一个节点</strong>，具体代码比较简单，不再跟进。</p><p>然后注意到如果没有延迟会执行scheduleFrameLocked()方法，有延迟就会使用 mHandler发送MSG_DO_SCHEDULE_CALLBACK消息，并且注意到 <strong>使用msg.setAsynchronous(true)把消息设置成异步</strong>，这是因为前面设置了同步屏障，只有异步消息才会执行。我们看下mHandler的对这个消息的处理：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">FrameHandler</span> <span class="keyword">extends</span> <span class="title class_">Handler</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">FrameHandler</span><span class="params">(Looper looper)</span> &#123;</span><br><span class="line">        <span class="built_in">super</span>(looper);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">handleMessage</span><span class="params">(Message msg)</span> &#123;</span><br><span class="line">        <span class="keyword">switch</span> (msg.what) &#123;</span><br><span class="line">            <span class="keyword">case</span> MSG_DO_FRAME:</span><br><span class="line">                <span class="comment">// 执行doFrame,即绘制过程</span></span><br><span class="line">                doFrame(System.nanoTime(), <span class="number">0</span>);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> MSG_DO_SCHEDULE_VSYNC:</span><br><span class="line">                <span class="comment">//申请VSYNC信号，例如当前需要绘制任务时</span></span><br><span class="line">                doScheduleVsync();</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> MSG_DO_SCHEDULE_CALLBACK:</span><br><span class="line">                <span class="comment">//需要延迟的任务，最终还是执行上述两个事件</span></span><br><span class="line">                doScheduleCallback(msg.arg1);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>直接使用doScheduleCallback方法，看看：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">void</span> <span class="title function_">doScheduleCallback</span><span class="params">(<span class="type">int</span> callbackType)</span> &#123;</span><br><span class="line">    <span class="keyword">synchronized</span> (mLock) &#123;</span><br><span class="line">        <span class="keyword">if</span> (!mFrameScheduled) &#123;</span><br><span class="line">            <span class="keyword">final</span> <span class="type">long</span> <span class="variable">now</span> <span class="operator">=</span> SystemClock.uptimeMillis();</span><br><span class="line">            <span class="keyword">if</span> (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) &#123;</span><br><span class="line">                scheduleFrameLocked(now);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>发现也是走到这里，即延迟运行最终也会走到scheduleFrameLocked()，跟进看看：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">scheduleFrameLocked</span><span class="params">(<span class="type">long</span> now)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!mFrameScheduled) &#123;</span><br><span class="line">        mFrameScheduled = <span class="literal">true</span>;</span><br><span class="line">        <span class="comment">//开启了VSYNC</span></span><br><span class="line">        <span class="keyword">if</span> (USE_VSYNC) &#123;</span><br><span class="line">            <span class="keyword">if</span> (DEBUG_FRAMES) &#123;</span><br><span class="line">                Log.d(TAG, <span class="string">&quot;Scheduling next frame on vsync.&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">//当前执行的线程，是否是mLooper所在线程</span></span><br><span class="line">            <span class="keyword">if</span> (isRunningOnLooperThreadLocked()) &#123;</span><br><span class="line">                <span class="comment">//申请 VSYNC 信号</span></span><br><span class="line">                scheduleVsyncLocked();</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="comment">// 若不在，就用mHandler发送消息到原线程，最后还是调用scheduleVsyncLocked方法</span></span><br><span class="line">                <span class="type">Message</span> <span class="variable">msg</span> <span class="operator">=</span> mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);</span><br><span class="line">                msg.setAsynchronous(<span class="literal">true</span>);<span class="comment">//异步</span></span><br><span class="line">                mHandler.sendMessageAtFrontOfQueue(msg);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// 如果未开启VSYNC则直接doFrame方法（4.1后默认开启）</span></span><br><span class="line">            <span class="keyword">final</span> <span class="type">long</span> <span class="variable">nextFrameTime</span> <span class="operator">=</span> Math.max(</span><br><span class="line">                    mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);</span><br><span class="line">            <span class="keyword">if</span> (DEBUG_FRAMES) &#123;</span><br><span class="line">                Log.d(TAG, <span class="string">&quot;Scheduling next frame in &quot;</span> + (nextFrameTime - now) + <span class="string">&quot; ms.&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="type">Message</span> <span class="variable">msg</span> <span class="operator">=</span> mHandler.obtainMessage(MSG_DO_FRAME);</span><br><span class="line">            msg.setAsynchronous(<span class="literal">true</span>);<span class="comment">//异步</span></span><br><span class="line">            mHandler.sendMessageAtTime(msg, nextFrameTime);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol><li>如果系统未开启 VSYNC 机制，此时直接发送 MSG_DO_FRAME 消息到 FrameHandler。注意查看上面贴出的 FrameHandler 代码，此时直接执行 doFrame 方法。</li><li>Android 4.1 之后系统默认开启 VSYNC，在 Choreographer 的构造方法会创建一个 FrameDisplayEventReceiver，scheduleVsyncLocked 方法将会通过它申请 VSYNC 信号。</li><li>isRunningOnLooperThreadLocked 方法，其内部根据 Looper 判断是否在原线程，否则发送消息到 FrameHandler。最终还是会调用 scheduleVsyncLocked 方法申请 VSYNC 信号。</li></ol><p>到这里，<strong>FrameHandler的作用很明显里了：发送异步消息（因为前面设置了同步屏障）。有延迟的任务发延迟消息、不在原线程的发到原线程、没开启VSYNC的直接走 doFrame 方法取执行绘制。</strong></p><h5 id="4-2-3-申请和接受VSync"><a href="#4-2-3-申请和接受VSync" class="headerlink" title="4.2.3 申请和接受VSync"></a><strong>4.2.3 申请和接受VSync</strong></h5><p>好了， 接着就看 scheduleVsyncLocked 方法是如何申请 VSYNC 信号的。猜测肯定申请 VSYNC 信号后，信号到来时也是走doFrame() 方法，doFrame()后面再看。先跟进scheduleVsyncLocked():</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">scheduleVsyncLocked</span><span class="params">()</span> &#123;</span><br><span class="line">    mDisplayEventReceiver.scheduleVsync();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>很简单，调用mDisplayEventReceiver的scheduleVsync()方法，mDisplayEventReceiver是Choreographer构造方法中创建，是FrameDisplayEventReceiver 的实例。FrameDisplayEventReceiver是 DisplayEventReceiver 的子类，DisplayEventReceiver 是一个 abstract class：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="title function_">DisplayEventReceiver</span><span class="params">(Looper looper, <span class="type">int</span> vsyncSource)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (looper == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">&quot;looper must not be null&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    mMessageQueue = looper.getQueue();</span><br><span class="line">    <span class="comment">// 注册VSYNC信号监听者</span></span><br><span class="line">    mReceiverPtr = nativeInit(<span class="keyword">new</span> <span class="title class_">WeakReference</span>&lt;DisplayEventReceiver&gt;(<span class="built_in">this</span>), mMessageQueue,</span><br><span class="line">            vsyncSource);</span><br><span class="line"></span><br><span class="line">    mCloseGuard.open(<span class="string">&quot;dispose&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 DisplayEventReceiver 的构造方法会通过 JNI 创建一个 IDisplayEventConnection 的 VSYNC 的监听者。</p><p>FrameDisplayEventReceiver的scheduleVsync()就是在 DisplayEventReceiver中：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">scheduleVsync</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (mReceiverPtr == <span class="number">0</span>) &#123;</span><br><span class="line">        Log.w(TAG, <span class="string">&quot;Attempted to schedule a vertical sync pulse but the display event &quot;</span></span><br><span class="line">                + <span class="string">&quot;receiver has already been disposed.&quot;</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 申请VSYNC中断信号，会回调onVsync方法</span></span><br><span class="line">        nativeScheduleVsync(mReceiverPtr);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么scheduleVsync()就是使用native方法nativeScheduleVsync()去申请VSYNC信号。这个native方法就看不了了，只需要知道<strong>VSYNC信号的接受回调是onVsync()</strong>，我们直接看onVsync()：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 接收到VSync脉冲时 回调</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> timestampNanos VSync脉冲的时间戳</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> frame 帧号码，自增</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@UnsupportedAppUsage</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onVsync</span><span class="params">(<span class="type">long</span> timestampNanos, <span class="type">long</span> physicalDisplayId, <span class="type">int</span> frame)</span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>具体实现是在FrameDisplayEventReceiver中：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">FrameDisplayEventReceiver</span> <span class="keyword">extends</span> <span class="title class_">DisplayEventReceiver</span></span><br><span class="line">        <span class="keyword">implements</span> <span class="title class_">Runnable</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">boolean</span> mHavePendingVsync;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">long</span> mTimestampNanos;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> mFrame;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">FrameDisplayEventReceiver</span><span class="params">(Looper looper, <span class="type">int</span> vsyncSource)</span> &#123;</span><br><span class="line">        <span class="built_in">super</span>(looper, vsyncSource);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onVsync</span><span class="params">(<span class="type">long</span> timestampNanos, <span class="type">long</span> physicalDisplayId, <span class="type">int</span> frame)</span> &#123;</span><br><span class="line">        <span class="comment">// Post the vsync event to the Handler.</span></span><br><span class="line">        <span class="comment">// The idea is to prevent incoming vsync events from completely starving</span></span><br><span class="line">        <span class="comment">// the message queue.  If there are no messages in the queue with timestamps</span></span><br><span class="line">        <span class="comment">// earlier than the frame time, then the vsync event will be processed immediately.</span></span><br><span class="line">        <span class="comment">// Otherwise, messages that predate the vsync event will be handled first.</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">now</span> <span class="operator">=</span> System.nanoTime();</span><br><span class="line">        <span class="keyword">if</span> (timestampNanos &gt; now) &#123;</span><br><span class="line">            Log.w(TAG, <span class="string">&quot;Frame time is &quot;</span> + ((timestampNanos - now) * <span class="number">0.000001f</span>)</span><br><span class="line">                    + <span class="string">&quot; ms in the future!  Check that graphics HAL is generating vsync &quot;</span></span><br><span class="line">                    + <span class="string">&quot;timestamps using the correct timebase.&quot;</span>);</span><br><span class="line">            timestampNanos = now;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (mHavePendingVsync) &#123;</span><br><span class="line">            Log.w(TAG, <span class="string">&quot;Already have a pending vsync event.  There should only be &quot;</span></span><br><span class="line">                    + <span class="string">&quot;one at a time.&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            mHavePendingVsync = <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        mTimestampNanos = timestampNanos;</span><br><span class="line">        mFrame = frame;</span><br><span class="line">        <span class="comment">//将本身作为runnable传入msg， 发消息后 会走run()，即doFrame()，也是异步消息</span></span><br><span class="line">        <span class="type">Message</span> <span class="variable">msg</span> <span class="operator">=</span> Message.obtain(mHandler, <span class="built_in">this</span>);</span><br><span class="line">        msg.setAsynchronous(<span class="literal">true</span>);</span><br><span class="line">        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">        mHavePendingVsync = <span class="literal">false</span>;</span><br><span class="line">        doFrame(mTimestampNanos, mFrame);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>onVsync()中，将接收器本身作为runnable传入异步消息msg，并使用mHandler发送msg，最终执行的就是doFrame()方法了。</p><p>注意一点是，<strong>onVsync()方法中只是使用mHandler发送消息到MessageQueue中，不一定是立刻执行，如何MessageQueue中前面有较为耗时的操作，那么就要等完成，才会执行本次的doFrame()</strong>。</p><h5 id="4-2-4-doFrame"><a href="#4-2-4-doFrame" class="headerlink" title="4.2.4 doFrame"></a><strong>4.2.4 doFrame</strong></h5><p>和上面猜测一样，申请VSync信号接收到后确实是走 doFrame()方法，那么就来看看Choreographer的doFrame()：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">void</span> <span class="title function_">doFrame</span><span class="params">(<span class="type">long</span> frameTimeNanos, <span class="type">int</span> frame)</span> &#123;</span><br><span class="line">    <span class="keyword">final</span> <span class="type">long</span> startNanos;</span><br><span class="line">    <span class="keyword">synchronized</span> (mLock) &#123;</span><br><span class="line">        <span class="keyword">if</span> (!mFrameScheduled) &#123;</span><br><span class="line">            <span class="keyword">return</span>; <span class="comment">// no work to do</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        ...</span><br><span class="line">        <span class="comment">// 预期执行时间</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">intendedFrameTimeNanos</span> <span class="operator">=</span> frameTimeNanos;</span><br><span class="line">        startNanos = System.nanoTime();</span><br><span class="line">        <span class="comment">// 超时时间是否超过一帧的时间（这是因为MessageQueue虽然添加了同步屏障，但是还是有正在执行的同步任务，导致doFrame延迟执行了）</span></span><br><span class="line">        <span class="keyword">final</span> <span class="type">long</span> <span class="variable">jitterNanos</span> <span class="operator">=</span> startNanos - frameTimeNanos;</span><br><span class="line">        <span class="keyword">if</span> (jitterNanos &gt;= mFrameIntervalNanos) &#123;</span><br><span class="line">            <span class="comment">// 计算掉帧数</span></span><br><span class="line">            <span class="keyword">final</span> <span class="type">long</span> <span class="variable">skippedFrames</span> <span class="operator">=</span> jitterNanos / mFrameIntervalNanos;</span><br><span class="line">            <span class="keyword">if</span> (skippedFrames &gt;= SKIPPED_FRAME_WARNING_LIMIT) &#123;</span><br><span class="line">                <span class="comment">// 掉帧超过30帧打印Log提示</span></span><br><span class="line">                Log.i(TAG, <span class="string">&quot;Skipped &quot;</span> + skippedFrames + <span class="string">&quot; frames!  &quot;</span></span><br><span class="line">                        + <span class="string">&quot;The application may be doing too much work on its main thread.&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">final</span> <span class="type">long</span> <span class="variable">lastFrameOffset</span> <span class="operator">=</span> jitterNanos % mFrameIntervalNanos;</span><br><span class="line">            ...</span><br><span class="line">            frameTimeNanos = startNanos - lastFrameOffset;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        ...</span><br><span class="line"></span><br><span class="line">        mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);</span><br><span class="line">        <span class="comment">// Frame标志位恢复</span></span><br><span class="line">        mFrameScheduled = <span class="literal">false</span>;</span><br><span class="line">        <span class="comment">// 记录最后一帧时间</span></span><br><span class="line">        mLastFrameTimeNanos = frameTimeNanos;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// 按类型顺序 执行任务</span></span><br><span class="line">        Trace.traceBegin(Trace.TRACE_TAG_VIEW, <span class="string">&quot;Choreographer#doFrame&quot;</span>);</span><br><span class="line">        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);</span><br><span class="line"></span><br><span class="line">        mFrameInfo.markInputHandlingStart();</span><br><span class="line">        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);</span><br><span class="line"></span><br><span class="line">        mFrameInfo.markAnimationsStart();</span><br><span class="line">        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);</span><br><span class="line">        doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);</span><br><span class="line"></span><br><span class="line">        mFrameInfo.markPerformTraversalsStart();</span><br><span class="line">        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);</span><br><span class="line"></span><br><span class="line">        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        AnimationUtils.unlockAnimationClock();</span><br><span class="line">        Trace.traceEnd(Trace.TRACE_TAG_VIEW);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面都有注释了很好理解，接着看任务的具体执行doCallbacks 方法：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">void</span> <span class="title function_">doCallbacks</span><span class="params">(<span class="type">int</span> callbackType, <span class="type">long</span> frameTimeNanos)</span> &#123;</span><br><span class="line">    CallbackRecord callbacks;</span><br><span class="line">    <span class="keyword">synchronized</span> (mLock) &#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> <span class="type">long</span> <span class="variable">now</span> <span class="operator">=</span> System.nanoTime();</span><br><span class="line">        <span class="comment">// 根据指定的类型CallbackkQueue中查找到达执行时间的CallbackRecord</span></span><br><span class="line">        callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);</span><br><span class="line">        <span class="keyword">if</span> (callbacks == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        mCallbacksRunning = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//提交任务类型</span></span><br><span class="line">        <span class="keyword">if</span> (callbackType == Choreographer.CALLBACK_COMMIT) &#123;</span><br><span class="line">            <span class="keyword">final</span> <span class="type">long</span> <span class="variable">jitterNanos</span> <span class="operator">=</span> now - frameTimeNanos;</span><br><span class="line">            <span class="keyword">if</span> (jitterNanos &gt;= <span class="number">2</span> * mFrameIntervalNanos) &#123;</span><br><span class="line">                <span class="keyword">final</span> <span class="type">long</span> <span class="variable">lastFrameOffset</span> <span class="operator">=</span> jitterNanos % mFrameIntervalNanos</span><br><span class="line">                        + mFrameIntervalNanos;</span><br><span class="line">                <span class="keyword">if</span> (DEBUG_JANK) &#123;</span><br><span class="line">                    Log.d(TAG, <span class="string">&quot;Commit callback delayed by &quot;</span> + (jitterNanos * <span class="number">0.000001f</span>)</span><br><span class="line">                            + <span class="string">&quot; ms which is more than twice the frame interval of &quot;</span></span><br><span class="line">                            + (mFrameIntervalNanos * <span class="number">0.000001f</span>) + <span class="string">&quot; ms!  &quot;</span></span><br><span class="line">                            + <span class="string">&quot;Setting frame time to &quot;</span> + (lastFrameOffset * <span class="number">0.000001f</span>)</span><br><span class="line">                            + <span class="string">&quot; ms in the past.&quot;</span>);</span><br><span class="line">                    mDebugPrintNextFrameTimeDelta = <span class="literal">true</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                frameTimeNanos = now - lastFrameOffset;</span><br><span class="line">                mLastFrameTimeNanos = frameTimeNanos;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// 迭代执行队列所有任务</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">CallbackRecord</span> <span class="variable">c</span> <span class="operator">=</span> callbacks; c != <span class="literal">null</span>; c = c.next) &#123;</span><br><span class="line">            <span class="comment">// 回调CallbackRecord的run，其内部回调Callback的run</span></span><br><span class="line">            c.run(frameTimeNanos);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        <span class="keyword">synchronized</span> (mLock) &#123;</span><br><span class="line">            mCallbacksRunning = <span class="literal">false</span>;</span><br><span class="line">            <span class="keyword">do</span> &#123;</span><br><span class="line">                <span class="keyword">final</span> <span class="type">CallbackRecord</span> <span class="variable">next</span> <span class="operator">=</span> callbacks.next;</span><br><span class="line">                <span class="comment">//回收CallbackRecord</span></span><br><span class="line">                recycleCallbackLocked(callbacks);</span><br><span class="line">                callbacks = next;</span><br><span class="line">            &#125; <span class="keyword">while</span> (callbacks != <span class="literal">null</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>主要内容就是取对应任务类型的队列，遍历队列执行所有任务，执行任务是 CallbackRecord的 run 方法：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">CallbackRecord</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> CallbackRecord next;</span><br><span class="line">    <span class="keyword">public</span> <span class="type">long</span> dueTime;</span><br><span class="line">    <span class="keyword">public</span> Object action; <span class="comment">// Runnable or FrameCallback</span></span><br><span class="line">    <span class="keyword">public</span> Object token;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@UnsupportedAppUsage</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">(<span class="type">long</span> frameTimeNanos)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (token == FRAME_CALLBACK_TOKEN) &#123;</span><br><span class="line">            <span class="comment">// 通过postFrameCallback 或 postFrameCallbackDelayed，会执行这里</span></span><br><span class="line">            ((FrameCallback)action).doFrame(frameTimeNanos);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">//取出Runnable执行run()</span></span><br><span class="line">            ((Runnable)action).run();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>前面看到mChoreographer.postCallback传的token是null，所以取出action，就是Runnable，执行run()，这里的action就是 ViewRootImpl 发起的绘制任务mTraversalRunnable了，那么<strong>这样整个逻辑就闭环了</strong>。</p><p>那么 啥时候 token == FRAME_CALLBACK_TOKEN 呢？答案是Choreographer的postFrameCallback()方法：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">postFrameCallback</span><span class="params">(FrameCallback callback)</span> &#123;</span><br><span class="line">    postFrameCallbackDelayed(callback, <span class="number">0</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">postFrameCallbackDelayed</span><span class="params">(FrameCallback callback, <span class="type">long</span> delayMillis)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (callback == <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">&quot;callback must not be null&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//也是走到是postCallbackDelayedInternal，并且注意是CALLBACK_ANIMATION类型，</span></span><br><span class="line">    <span class="comment">//token是FRAME_CALLBACK_TOKEN，action就是FrameCallback</span></span><br><span class="line">    postCallbackDelayedInternal(CALLBACK_ANIMATION,</span><br><span class="line">            callback, FRAME_CALLBACK_TOKEN, delayMillis);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">FrameCallback</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doFrame</span><span class="params">(<span class="type">long</span> frameTimeNanos)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到postFrameCallback()传入的是FrameCallback实例，接口FrameCallback只有一个doFrame()方法。并且也是走到postCallbackDelayedInternal，FrameCallback实例作为action传入，token则是FRAME_CALLBACK_TOKEN，并且任务是CALLBACK_ANIMATION类型。</p><p><strong>Choreographer的postFrameCallback()通常用来计算丢帧情况</strong>，使用方式如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">      <span class="comment">//Application.java</span></span><br><span class="line">       <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onCreate</span><span class="params">()</span> &#123;</span><br><span class="line">           <span class="built_in">super</span>.onCreate();</span><br><span class="line">           <span class="comment">//在Application中使用postFrameCallback</span></span><br><span class="line">           Choreographer.getInstance().postFrameCallback(<span class="keyword">new</span> <span class="title class_">FPSFrameCallback</span>(System.nanoTime()));</span><br><span class="line">       &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FPSFrameCallback</span> <span class="keyword">implements</span> <span class="title class_">Choreographer</span>.FrameCallback &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">TAG</span> <span class="operator">=</span> <span class="string">&quot;FPS_TEST&quot;</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">long</span> <span class="variable">mLastFrameTimeNanos</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">long</span> mFrameIntervalNanos;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">FPSFrameCallback</span><span class="params">(<span class="type">long</span> lastFrameTimeNanos)</span> &#123;</span><br><span class="line">        mLastFrameTimeNanos = lastFrameTimeNanos;</span><br><span class="line">        mFrameIntervalNanos = (<span class="type">long</span>)(<span class="number">1000000000</span> / <span class="number">60.0</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doFrame</span><span class="params">(<span class="type">long</span> frameTimeNanos)</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//初始化时间</span></span><br><span class="line">        <span class="keyword">if</span> (mLastFrameTimeNanos == <span class="number">0</span>) &#123;</span><br><span class="line">            mLastFrameTimeNanos = frameTimeNanos;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">long</span> <span class="variable">jitterNanos</span> <span class="operator">=</span> frameTimeNanos - mLastFrameTimeNanos;</span><br><span class="line">        <span class="keyword">if</span> (jitterNanos &gt;= mFrameIntervalNanos) &#123;</span><br><span class="line">            <span class="keyword">final</span> <span class="type">long</span> <span class="variable">skippedFrames</span> <span class="operator">=</span> jitterNanos / mFrameIntervalNanos;</span><br><span class="line">            <span class="keyword">if</span>(skippedFrames&gt;<span class="number">30</span>)&#123;</span><br><span class="line">                <span class="comment">//丢帧30以上打印日志</span></span><br><span class="line">                Log.i(TAG, <span class="string">&quot;Skipped &quot;</span> + skippedFrames + <span class="string">&quot; frames!  &quot;</span></span><br><span class="line">                        + <span class="string">&quot;The application may be doing too much work on its main thread.&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        mLastFrameTimeNanos=frameTimeNanos;</span><br><span class="line">        <span class="comment">//注册下一帧回调</span></span><br><span class="line">        Choreographer.getInstance().postFrameCallback(<span class="built_in">this</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="4-2-5-小结"><a href="#4-2-5-小结" class="headerlink" title="4.2.5 小结"></a><strong>4.2.5 小结</strong></h5><p>使用Choreographer的postCallback()、postFrameCallback() 作用理解：发送任务 存队列中，监听VSync信号，当前VSync到来时 会使用mHandler发送异步message，这个message的Runnable就是队列中的所有任务。</p><p>好了，Choreographer整个代码逻辑都讲完了，引用《Android 之 Choreographer 详细分析》的流程图：</p><p>原文流程图为：<a href="https://i-blog.csdnimg.cn/blog_migrate/5ff22e98afde4ff780f8a291d1081619.png">Android 之 Choreographer</a>，但并不是很形象，引用另一张流程图：</p><img src="https://cdn.julis.wang/blog/img/aab4273a0af898dcc9bb0fdcc0447b5a.png"><h2 id="六、疑问解答"><a href="#六、疑问解答" class="headerlink" title="六、疑问解答"></a><strong>六、疑问解答</strong></h2><ol><li><strong>丢帧</strong>(掉帧) ，是说 这一帧延迟显示 还是丢弃不再显示 ？ 答：延迟显示，因为缓存交换的时机只能等下一个VSync了。</li><li>布局层级较多/主线程耗时 是如何造成 丢帧的呢？ 答：布局层级较多/主线程耗时 会影响CPU/GPU的执行时间，大于16.6ms时只能等下一个VSync了。</li><li>16.6ms刷新一次 是啥意思？是每16.6ms都走一次 measure/layout/draw ？ 答：屏幕的固定刷新频率是60Hz，即16.6ms。不是每16.6ms都走一次 measure/layout/draw，而是有绘制任务才会走，并且绘制时间间隔是取决于布局复杂度及主线程耗时。</li><li>measure/layout/draw 走完，界面就立刻刷新了吗? 答：不是。measure/layout/draw 走完后 会在VSync到来时进行缓存交换和刷新。</li><li>如果界面没动静止了，还会刷新吗？ 答：屏幕会固定没16.6ms刷新，但CPU/GPU不走绘制流程。见下面的SysTrace图。</li><li>可能你知道<strong>VSYNC</strong>，这个具体指啥？在屏幕刷新中如何工作的？ 答：当扫描完一个屏幕后，设备需要重新回到第一行以进入下一次的循环，此时会出现的vertical sync pulse（垂直同步脉冲）来保证双缓冲在最佳时间点才进行交换。并且Android4.1后 CPU/GPU的绘制是在VSYNC到来时开始。</li><li>可能你还听过屏幕刷新使用 <strong>双缓存</strong>、<strong>三缓存</strong>，这又是啥意思呢？ 答：双缓存是Back buffer、Frame buffer，用于解决画面撕裂。三缓存增加一个Back buffer，用于减少Jank。</li><li>可能你还听过神秘的<strong>Choreographer</strong>，这又是干啥的？ 答：用于实现——“CPU/GPU的绘制是在VSYNC到来时开始”。</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近在研究 Android 屏幕显示与渲染相关的内容，平时经常看到这些类 &lt;code&gt;ViewRootImpl&lt;/code&gt;、&lt;code&gt;Choreographer&lt;/code&gt;、&lt;code&gt;Surface&lt;/code&gt; 、 &lt;code&gt;SurfaceFlinger&lt;/co</summary>
      
    
    
    
    <category term="技术文章" scheme="http://julis.wang/categories/technology/"/>
    
    
    <category term="安卓" scheme="http://julis.wang/tags/Android/"/>
    
  </entry>
  
  <entry>
    <title>关于 pthread_key_t 导致的 Android Crash 的探索</title>
    <link href="http://julis.wang/2024/11/10/%E5%85%B3%E4%BA%8E-pthread-key-t-%E5%AF%BC%E8%87%B4%E7%9A%84-Android-Crash-%E7%9A%84%E6%8E%A2%E7%B4%A2/"/>
    <id>http://julis.wang/2024/11/10/%E5%85%B3%E4%BA%8E-pthread-key-t-%E5%AF%BC%E8%87%B4%E7%9A%84-Android-Crash-%E7%9A%84%E6%8E%A2%E7%B4%A2/</id>
    <published>2024-11-10T08:11:00.000Z</published>
    <updated>2025-05-20T11:46:57.000Z</updated>
    
    <content type="html"><![CDATA[<p>此前我负责的 SDK 已集成多个司内业务，一切运行正常，最近在接入到一些游戏项目中的时候发现存在比较多关于 <strong>libc.so</strong> 的 crash，在游戏中某个场景会使用SDK 进行逻辑处理，在部分手机会在短时间就直接 Crash，且集中在性能比较好的手机中。经过一番折腾，最后被定位在了一个跟 SDK 没有什么关系的地方：<code>pthread_key_t</code></p><h2 id="Crash-表现"><a href="#Crash-表现" class="headerlink" title="Crash 表现"></a>Crash 表现</h2><p>在 Crash 上报平台中收到诸多的 Crash 上报，调用的形式多种多样，异常名都是<code>signal 6 (SIGABRT)</code></p><p>但崩溃调用栈最终都停留在<br>  <code>/apex/com.android.runtime/lib64/bionic/libc.so pc (abort+168)</code></p><p>以及中间都会经过：<br><code>/apex/com.android.runtime/lib64/bionic/libc.so (pthread_once+136)</code></p><h3 id="难以复现的问题"><a href="#难以复现的问题" class="headerlink" title="难以复现的问题"></a>难以复现的问题</h3><p>由于我们的项目依赖于其他业务的SDK，最终的 SDK 打包合并在 Unity 的游戏中，我们不能直接使用游戏侧代码逻辑进行编译打包进行调试，这为问题的排查增大了一定的难度，只能在 Unity 的 demo 工程具体的表现为：</p><p>1、部分性能好的手机（如小米14 pro）才会出现 Crash，而且在对应的游戏中必现，有些游戏又不会复现</p><p>2、SDK里面同样的代码逻辑在测试 App 工程中完全不会复现</p><p>3、SDK里面同样的代码逻辑在 Unity 测试游戏 demo 中也完全不会复现</p><p>4、使用了业务方（游戏侧）的 Unity 的各种配置，依然没有复现</p><p>5、崩溃栈中有涉及到 <strong>thread</strong> 相关的关键词，怀疑是线程相关问题，但在原生层开辟N个线程也没有复现</p><p>6、其他各种尝试都没有复现：开辟大量内存、Unity 与 Android 调用方式调整……</p><h2 id="解决线索与方案"><a href="#解决线索与方案" class="headerlink" title="解决线索与方案"></a>解决线索与方案</h2><p>一开始是怀疑业务方的环境与 SDK 运行环境有冲突，毕竟 SDK 已经在诸多业务中上线并正常运行了很久，不应该是 SDK 本身代码逻辑不对导致的才对。但没过多久，我们在另一个业务中也发现了这个问题，那说明并不是一个游戏环境导致。</p><p>解决问题直接看对应的崩溃栈，其崩溃栈都是使用相关的组件导致的 Crash，询问了相关的开发大佬之后并没有得到解决办法，原因是我们使用的版本相对较老，经历了比较久的迭代，逻辑改掉了很多。二是有可能这个问题在新版本中已经修复掉了。于是我们进行了一大波改造升级，经过一段时间后，再次集成到业务方，原以为这个问题就此解决了，调用了一下创编 SDK 之后依然 crash，此时心拔凉拔凉……<br>但这时候比较能确定的是，这个 crash 跟依赖的SDK 没有直接关系，可能是由其他的环境问题什么。</p><h3 id="问题线索-pthread-key"><a href="#问题线索-pthread-key" class="headerlink" title="问题线索 pthread_key"></a>问题线索 pthread_key</h3><p>在最开始的排查问题过程中一直在关注在环境的差异上面，经过一番折腾依然没有效果，方向错误了，于是再次回到 Crash 栈中来，在崩溃栈中都含有：<code>pthread_once</code>、<code>emutls_get_address</code>、<code>cxa_get_globals</code>、<code>emutls_init</code>相关的关键词，由于平时完全没有接触过这几个函数，对他们的了解比较少。但经过一番搜索之后，他们都有提到一个关键的术语：<strong>TLS (thread-local storage)</strong></p><p>以及对几个函数调用的源码进行查看，发现这几个函数最终涉及到的都是 <code>pthread</code>  使用或者创建相关的</p><p>其中在 cs.android的 <a href="https://cs.android.com/android/platform/superproject/main/+/main:external/compiler-rt/lib/builtins/emutls.c?q=emutls_init&amp;ss=android%2Fplatform%2Fsuperproject%2Fmain">emutls.c</a> 源码里有：<br><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">static</span> <span class="type">void</span> <span class="title">emutls_init</span><span class="params">(<span class="type">void</span>)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">pthread_key_create</span>(&amp;emutls_pthread_key, emutls_key_destructor) != <span class="number">0</span>)</span><br><span class="line"><span class="built_in">abort</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>这里基本上可以和崩溃栈对应上了，正是这里执行的 <code>abort()</code>，那么原因是否是由 <code>pthread_key_create()</code>引起的呢？继续对 <code>pthread_key_create</code> 研究，原来在 Bionic 中，能够被开发者所使用的 Pthread Key 数量，是 <code>PTHREAD_KEYS_MAX</code> 宏所定义的 128 个。</p><p>那我们遇到的问题是否也是同一个问题呢？得到答案最好的方式是验证，想办法做一个验证，用代码把系统能提供的 pthread_key 耗尽然后再使用我们创编SDK的功能，使用如下代码创建 <code>PTHREAD_KEYS_MAX</code>个 <code>pthread_key_t</code>，再直接使用创编 SDK，果不其然 Crash了，而且 crash 栈与上报的数据比较的一致（没有完全一致，毕竟一些场景还是会有点差异）。</p><p>以下的代码会耗尽目前程序中的 key，只创建 pthread_key，而不释放掉</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">available_key</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; PTHREAD_KEYS_MAX; i++) &#123;</span><br><span class="line">        <span class="type">pthread_key_t</span> key;</span><br><span class="line">        <span class="type">int</span> result = pthread_key_create(&amp;key, detachDestructor);</span><br><span class="line">        <span class="keyword">if</span> (result == JNI_OK) &#123;</span><br><span class="line">            __android_log_print(ANDROID_LOG_ERROR, <span class="string">&quot;--julis&quot;</span>, <span class="string">&quot;create thread key Success&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            __android_log_print(ANDROID_LOG_ERROR, <span class="string">&quot;--julis&quot;</span>, <span class="string">&quot;create thread key failed&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从打印的日志里面看，在 Unity demo App 里面大概创建到 60 多的时候就创建失败了，也就是说Unity 本身可能就使用了很多 key，留给应用层开发的就只有几十个 key 了。</p><p>虽然尝试是Crash了，但怎么能证明这个就是导致业务方 Crash 就是这个原因呢？以及怎么解释有的手机为什么会Crash，有的手机不会Crash呢？</p><p>我们继续，从目前的推论来看，我们的创编SDK需要使用 <code>pthread_key_t</code>, 可能数量不够了，也就说创编SDK需要使用一定数量的key，那我们将刚才代码里面的<code>i &lt; PTHREAD_KEYS_MAX;</code> 进行调整，我们预留足够的 key 空间给创编SDK，<code>i &lt; target_number;</code> 于是在之前 crash 的手机和未 crash 的手机做了一次对比。</p><p>以下是对部手机的测试结果，记录日志前面的数字就是代码里面的 <code>target_number</code></p><img src="https://cdn.julis.wang/blog/img/j51nlsfd.jpg"><p>从对比结果看，两部手机他们可以供应用层使用的 key 的数量是不同的，之前会 crash 的手机它可以使用的 key 明显是少于之前未 crash 手机的数量的，这也就能解释为什么有的手机为什么会 crash，有的手机不会 crash 了。以及，可以推测出来创编SDK使用了5个key左右。</p><p>这里提一下在解决问题之初，我们发现 crash 的手机基本上都是市面上比较好的手机，且手机的 GPU 都集中于 Adreno 比较新的型号，一度误以为是相关底层 SDK 未进行兼容性适配导致。为什么性能更好的手机使用的 <code>pthread_key_t</code> 会更多？猜测可能是好的手机 Unity 运行相关的东西或者优化(这里的优化指的是游戏特效或者功能玩法)更多，所以消耗的资源就更多一点，当然这里只是个人猜测，具体原因还需要深入了解。</p><p>还剩下一个问题：业务方的 App 为什么会Crash？于是将上面的 <code>available_key()</code>方法进行一次包装，并将其打包集成进游戏侧测试，从日志里面看到留给我们创编SDK使用的 key 只有3个了！而我们的 SDK 需要5个左右，问题原因基本就是这个了，那如何解决呢？</p><h3 id="方案解决"><a href="#方案解决" class="headerlink" title="方案解决"></a>方案解决</h3><p>究其根本原因是 Android 系统的 <code>pthread_key_t</code> 的使用数量的限制，那么最直接的解决方式那就是降低对 <code>pthread_key_t</code> 的使用，但是由于我们依赖使用其他地方的 SDK，对其项目直接优化更改可能成本相对较高，直接修改源码解决的话一时半会儿无法解决。这里先对 <code>pthread_key_t</code> 数量限制相关的问题进行一些研究总结：</p><p>在 Android 官方源码 <a href="https://android.googlesource.com/platform/bionic/+/master/libc/include/pthread.h">pthread.h#pthread_key_create()</a> 里面有提到：</p><blockquote><p>There is a limit of <code>PTHREAD_KEYS_MAX</code> keys per process, but most callers should just use the C or C++ <code>thread_local</code> storage specifier anyway. When targeting new enough OS versions, the compiler will automatically use ELF TLS; when targeting old OS versions the emutls implementation will multiplex pthread keys behind the scenes, using one per library rather than one per thread-local variable. If you are implementing the runtime for a different language, you should consider similar implementation choices and avoid a direct one-to-one mapping from thread locals to pthread keys.<br>Returns 0 on success and returns an error number on failure.<br><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">int pthread_key_create(pthread_key_t* _Nonnull **key_ptr, void (* _Nullable **key_destructor)(void* _Nullable));</span><br></pre></td></tr></table></figure></p></blockquote><p>可以看到官方建议使用 <code>thread_local</code> 去实现 TLS，以及在新的系统版本中会使用 <code>ELF TLS</code> 对 <code>pthread_key_t</code> 将不直接依赖，<br>但条件相对比较高，参考官方更新：需要 miniSDK&gt;29 和NDK r26</p><blockquote><p>ELF TLS (Available for API level &gt;= 29)<br>Android supports <a href="https://android.googlesource.com/platform/bionic/+/HEAD/docs/elf-tls.md">ELF TLS</a> starting at API level 29. Since NDK r26, clang will automatically enable ELF TLS for <code>minSdkVersion 29</code> or higher. Otherwise, the existing emutls implementation (which uses <code>pthread_key_create()</code> behind the scenes) will continue to be used. This means that convenient C/C++ thread-local syntax is available at any API level; at worst it will perform similarly to “roll your own” thread locals using <code>pthread_key_create()</code> but at best you’ll get the performance benefit of ELF TLS, and the NDK will take care of the details.</p></blockquote><p>最后我们的解决方式是依据上面 <code>pthread_key_create</code> 提到的</p><blockquote><p>There is a limit of <code>PTHREAD_KEYS_MAX</code> keys per process…..</p></blockquote><p>重点是：<strong>per process</strong>，每个进程有 <code>PTHREAD_KEYS_MAX</code>,这个<code>PTHREAD_KEYS_MAX</code>被定义在 <a href="https://android.googlesource.com/platform/bionic/+/refs/heads/main/libc/include/limits.h">limits.h</a>  现在的 Android 基本上都是定义为128。那那我们将我们的SDK 使用的时候放在一个单独的进程不就ok了？事实是的，由于我们的SDK向业务只是提供一个 素材输入=&gt;视频输出的功能，中间过程是一个黑盒，那么这个场景使用多进程是完全OK的，使用多进程还有一个好处就是能与游戏进程相独立，尽量减少两者之间的依赖。但多进程也带来了一些门槛，但这相比与改渲染 SDK 底层的源码来说是相对简单很多的，最终经过一番折腾我们将创编SDK得渲染放在了一个单独的进程，后试验运行在之前 Crash 过的游戏业务上一切正常。</p><h3 id="pthread-key-检测工具"><a href="#pthread-key-检测工具" class="headerlink" title="pthread_key 检测工具"></a>pthread_key 检测工具</h3><p>为了以后接入其他游戏前不再发生类似的Crash问题，在接入业务前做一些技术评估，<code>pthread_key_t</code> 可用数量可能也需要成为一个考量指标，可用数的不同，可能需要不同的技术方案，我专门写了一个小工具，可方便查询业务项目目前使用了多少 pthread_key_t，能帮助项目排查当前问题是否是由于 <code>pthread_key_t</code> 占满导致的相关问题。</p><p>不过我更想做一个能够检测项目里面有消耗过 pthread_key_t 的地方，将其 hook 住，打印出来对应的调用栈，这样就能方便业务排查。未来，随着 Android 业务的复杂化，这种问题可能会变成更多大型项目将会遇到。调研发现 Tencent 对外开源项目 <a href="https://github.com/Tencent/matrix?tab=readme-ov-file">Tencent/matrix</a> 已经有针对 pthread_key 做了相关的hook，业务侧也可以直接使用 matrix 进行检测，但其项目相对比较庞大，以及使用的方式较复杂。于是将其精简到一个小工具内，整体大小只有1MB 不到。</p><p>源码地址：<a href="https://github.com/VomPom/PthreadKeyDetect">PthreadKeyDetect</a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文主要记录了i创作SDK出现大佬了关于 <code>libc.so</code> 的 Crash，经过调查，问题被定位在 <code>pthread_key_t</code> 资源耗尽的问题上，并对其进行了相关研究，最后并解决了该问题的过程。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><p><a href="https://android.googlesource.com/platform/bionic/+/HEAD/android-changes-for-ndk-developers.md#elf-tls-available-for-api-level-29">Android linker changes for NDK developers</a></p><p><a href="https://github.com/android/ndk/issues/789">thread specific key leakage</a></p><p><a href="https://juejin.cn/post/6987921143487283236">pthread_key_create用法导致的崩溃修复</a></p><p><a href="https://github.com/flutter/flutter/issues/127079">Crash issue caused by pthread_key_create failed: 11 when integrating Flutter into our project #127079</a></p><p><a href="https://muc.lists.netbsd.tech.userlevel.narkive.com/gFAi2gse/increase-pthread-keys-max">Increase PTHREAD_KEYS_MAX</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;此前我负责的 SDK 已集成多个司内业务，一切运行正常，最近在接入到一些游戏项目中的时候发现存在比较多关于 &lt;strong&gt;libc.so&lt;/strong&gt; 的 crash，在游戏中某个场景会使用SDK 进行逻辑处理，在部分手机会在短时间就直接 Crash，且集中在性能比较</summary>
      
    
    
    
    <category term="技术文章" scheme="http://julis.wang/categories/technology/"/>
    
    
    <category term="Android" scheme="http://julis.wang/tags/Android/"/>
    
  </entry>
  
  <entry>
    <title>[译]软件开发人员的常青技能</title>
    <link href="http://julis.wang/2024/07/09/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E4%BA%BA%E5%91%98%E7%9A%84%E5%B8%B8%E9%9D%92%E6%8A%80%E8%83%BD/"/>
    <id>http://julis.wang/2024/07/09/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E4%BA%BA%E5%91%98%E7%9A%84%E5%B8%B8%E9%9D%92%E6%8A%80%E8%83%BD/</id>
    <published>2024-07-08T23:27:19.000Z</published>
    <updated>2026-02-09T10:49:55.671Z</updated>
    
    <content type="html"><![CDATA[<p>最近在 Github 看到这一篇将程序员一直需要使用的非技术核心能力进行了总结，深受里面内容的启发，语言、框架都是会过时的，但有些技能无论是什么语言或者框架都是通用的，如果要在这个行业持续深根，那么这些非技术能力是必备的且实用的。本文在原文上进行翻译，并对文中提到部分专业术语进行了解释，以及对指向外部链接的文档内容作了一些简单的概述，希望能帮助到查看此文档的人。</p><p>原文地址：<a href="https://github.com/romenrg/evergreen-skills-developers.git">evergreen-skills-developers</a></p><p>中英双文地址：<a href="https://github.com/VomPom/evergreen-skills-developers/blob/master/README_en_cn.md">[译]evergreen-skills-developers</a></p><h2 id="原文翻译："><a href="#原文翻译：" class="headerlink" title="原文翻译："></a>原文翻译：</h2><p>这个仓库包括了一份“常青技能”清单，这份清单应该可以作为对技术精湛的软件工程师/开发者客观评价。</p><p>这份工作的是为了在招聘软件开发者/工程师时，提供一个替代的技术面试的方案。文档关注的是软开发最佳实践、跨框架原则和通用的技能；而不是我们在行业中经常看到的语言层面，或者特定技术框架的内容。</p><p>编程语言不断进化，公司也不断改变他们的技术栈，框架很快就会过时，有经验的工程师使用搜索引擎能在几分钟就能解决语法相关的问题。因此，在面试候选人时关注这些方面是否有意义呢？</p><p>另一方面，技术框架以外的原理和非技术的技能是在谷歌上查不到的，这些技能是“常青”的，并且对工程师的表现有巨大的影响。这些更能反映出软件开发者/工程师为团队带来的真正价值。</p><p>这个仓库是基于以下文章的一个衍生作品：”<a href="https://www.romenrg.com/blog/2018/12/29/what-makes-a-great-software-engineer">是什么造就了一位伟大的软件工程师</a>“。</p><p>这是一个正在进行中的工作。重要的知识可能缺失，现有的条目可能可以改进，更好的分组策略也可能被发现。因此，任何贡献（即PR或问题）都是受欢迎的。请随时按照<a href="https://gptx.woa.com/CONTRIBUTING.md">贡献指南</a>提出修改建议。</p><h2 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h2><ul><li><p><a href="#非技术技能">非技术技能</a></p><ul><li><a href="#核心技能（又称“软技能”）">核心技能</a><ul><li><a href="#交流">交流</a></li><li><a href="#团队">团队</a></li></ul></li><li><a href="#创新和自我管理技能">创新和自我管理技能</a><ul><li><a href="#开发流程">开发流程</a></li><li><a href="#问题解决能力">问题解决能力</a></li><li><a href="#心态">心态</a></li></ul></li></ul></li><li><p><a href="#技能能力">技能能力</a></p><ul><li><a href="#通用技术能力">通用技术能力</a></li><li><a href="#编程准则">编程准则</a><ul><li><a href="#数据结构">数据结构</a> </li><li><a href="#代码整洁">代码整洁</a></li><li><a href="#源码管理">源码管理</a></li><li><a href="#技术合作">技术合作</a></li><li><a href="#DevOps实践">DevOps实践</a></li><li><a href="通用技术知识">通用技术知识</a><ul><li><a href="#语言理论知识">语言理论知识</a></li><li><a href="#优化">优化</a></li><li><a href="#并发">并发</a>   </li></ul></li></ul></li></ul></li><li><p><a href="#特定领域技术知识">特定领域技术知识</a></p><ul><li><p><a href="#前端开发">前端开发</a></p></li><li><p><a href="#后端开发">后端开发</a></p></li><li><p><a href="#架构">架构</a></p></li><li><p><a href="#基础建设">基础建设</a></p></li><li><p><a href="#安全">安全</a></p></li></ul></li></ul><h2 id="非技术技能"><a href="#非技术技能" class="headerlink" title="非技术技能"></a>非技术技能</h2><p>以下非技术能力可能是开发者最重要的能力。尽管一个人可能具备很强的技术能力，但在公司中没有良好的沟通、团队合作态度、开发流程、解决问题的能力和学习的心态的话，一切会变得非常糟糕。</p><h3 id="核心技能（又称“软技能”）"><a href="#核心技能（又称“软技能”）" class="headerlink" title="核心技能（又称“软技能”）"></a>核心技能（又称“软技能”）</h3><h4 id="交流"><a href="#交流" class="headerlink" title="交流"></a>交流</h4><ul><li>遵循邮件使用的最佳策略(例： <a href="https://www.grammarly.com/blog/email-etiquette-rules-to-know/">some e-mail etiquette rules</a>)</li></ul><ul><li><p>遵循沟通的最佳策略 (e.g. <a href="https://slack.com/intl/en-es/help/articles/115000769927-Use-threads-to-organize-discussions-">use threads to organize discussions</a> and <a href="https://blog.rescuetime.com/slack-focus-guide/">other best-practices from Slack</a>)</p><p>两份链接指向的 slack 的一则使用文档和一份 slack 使用技巧文档</p></li><li><p><a href="https://jaxenter.com/aaaand-gone-true-cost-interruptions-128741.html">最小化干扰</a></p><p>链接指向的文章是一篇关于程序员在工作中，因被其他事项而中断程序开发的影响，一般人，在工作过程中断打扰后大约需要23分钟才能恢复到之前的状态，而程序员需要更久，文中强调了工作中断对程序员工作效率和心情的影响，并讨论了有计划和非计划性中断的不同影响。</p></li><li><p>保持礼貌</p></li></ul><h4 id="团队"><a href="#团队" class="headerlink" title="团队"></a>团队</h4><ul><li><p><a href="https://simpleprogrammer.com/empathy-software-developers">练习同理心</a></p></li><li><p>保持谦逊和低调</p></li><li><p>做一个积极倾听的人</p></li><li><p>做一个好的导师</p></li><li><p>知识分享</p></li><li><p>得有见地</p></li></ul><h3 id="创新和自我管理技能"><a href="#创新和自我管理技能" class="headerlink" title="创新和自我管理技能"></a>创新和自我管理技能</h3><h4 id="开发流程"><a href="#开发流程" class="headerlink" title="开发流程"></a>开发流程</h4><ul><li><p>了解<a href="https://agilemanifesto.org/principles.html">《敏捷开发原则》</a></p></li><li><p>适应迭代和增量开发</p></li><li><p>自组织的能力</p><p>指的是个体或系统能够自发地、无需外部强制指挥，根据内部规则和相互作用来组织自身结构和行为的能力。这种能力在多个层面都有体现，包括个人自我管理、团队协作以及更广泛的社会和生态系统</p></li><li><p>避免产生错误的预估（比如：工时预估）</p></li><li><p>关注优先级和业务价值</p></li></ul><h4 id="问题解决能力"><a href="#问题解决能力" class="headerlink" title="问题解决能力"></a>问题解决能力</h4><ul><li><p>使用科学方法(<a href="https://en.wikipedia.org/wiki/Scientific_method">Scientific Method</a>)</p><blockquote><p>科学方法是一种有系统地寻求知识的程序，涉及了以下三个步骤：问题的认知与表述、实验数据的收集、假说的构成与测试。</p></blockquote></li><li><p>检索能力</p></li><li><p>横向思维</p><blockquote><p>横向思维，指使用间接的、具有创造力的、不是一望而知的推理方式来解决问题</p></blockquote></li><li><p>抽象化能力</p></li><li><p>创造力</p></li><li><p><a href="http://en.wikipedia.org/wiki/5_Whys">五问法</a></p><blockquote><p>五问法关键所在就是，鼓励解决问题的人要努力避开主观或自负的假设和逻辑陷阱，从结果着手，沿着因果关系链条，顺藤摸瓜，穿越不同的抽象层面，直至找出原有问题的根本原因。简而言之，就是鼓励解决问题的人要有“打破砂锅问到底”的精神。</p></blockquote></li><li><p>风险管理</p></li></ul><h4 id="心态"><a href="#心态" class="headerlink" title="心态"></a>心态</h4><ul><li><p>不要害怕变化</p></li><li><p>敢于失败</p></li><li><p>终生学习</p></li><li><p><a href="https://en.wikipedia.org/wiki/Critical_thinking">批判性思维</a> （保持理性，质疑决定，“让事实说话”）</p></li></ul><h2 id="技能能力"><a href="#技能能力" class="headerlink" title="技能能力"></a>技能能力</h2><h3 id="通用技术能力"><a href="#通用技术能力" class="headerlink" title="通用技术能力"></a>通用技术能力</h3><p>有一些技术知识是永恒的，对任何软件工程师都有关，尽管他们将要从事的具体领域各不相同。为了深入了解他们的资历并了解他们的工程实践有多扎实，你可以和他们就编程原理、数据结构、清晰的代码、源代码管理、技术协作或者DevOps实践等主题进行交谈。如果这些基础扎实，他们可能能够毫无问题地学习你们特定领域的东西。</p><h4 id="编程准则"><a href="#编程准则" class="headerlink" title="编程准则"></a>编程准则</h4><ul><li><p>基本流程结构和逻辑代数</p></li><li><p>面向对象编程</p></li><li><p><a href="https://en.wikipedia.org/wiki/SOLID">SOLID</a>, <a href="https://en.wikipedia.org/wiki/GRASP_(object-oriented_design">GRASP</a>)面向对象设计</p><blockquote><p><strong>SOLID</strong>（单一功能、开闭原则、里氏替换、接口隔离以及依赖反转）</p></blockquote></li></ul><blockquote><div class="table-container"><table><thead><tr><th style="text-align:center"></th><th></th><th></th></tr></thead><tbody><tr><td style="text-align:center">S</td><td><a href="https://zh.wikipedia.org/wiki/单一功能原则">单一功能原则</a></td><td>认为“对象应该仅具有一种单一功能”的概念。</td></tr><tr><td style="text-align:center">O</td><td><a href="https://zh.wikipedia.org/wiki/开闭原则">开闭原则</a></td><td>认为“软件应该是对于扩展开放的，但是对于修改封闭的”的概念。</td></tr><tr><td style="text-align:center">L</td><td><a href="https://zh.wikipedia.org/wiki/里氏替换原则">里氏替换原则</a></td><td>认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”的概念。参考契约式设计。</td></tr><tr><td style="text-align:center">I</td><td><a href="https://zh.wikipedia.org/wiki/接口隔离原则">接口隔离原则</a></td><td>认为“多个特定客户端接口要好于一个宽泛用途的接口”的概念。</td></tr><tr><td style="text-align:center">D</td><td><a href="https://zh.wikipedia.org/wiki/依赖反转原则">依赖反转原则</a></td><td>认为一个方法应该遵从“依赖于抽象而不是一个实例”的概念。 依赖注入是该原则的一种实现方式。</td></tr></tbody></table></div><p><strong>GRASP</strong>中提到的模式和原则包括有控制器（controller）、创建者（creator）、中介（indirection）、信息专家（information expert）、低耦合性（low coupling）、高内聚性（high cohesion）、多态（polymorphism）、保护变化（protected variations）和纯虚构（pure Fabrication）[2]</p></blockquote><ul><li><p>函数式编程（纯函数、不变性、递归……）</p></li><li><p><a href="http://amzotti.github.io/programming%20paradigms/2015/02/13/what-is-the-difference-between-procedural-function-imperative-and-declarative-programming-paradigms/">声明式与命令式编程</a></p><blockquote><p>声明式和命令式编程范例只不过是描述在不同抽象层次上编码的流行词。声明式编程关注的是“做什么，而不是如何做”，而命令式编程则关注的是“如何做，而不是做什么”。声明式编程是在比命令式编程更高的抽象层次上进行编程。两者都有其适用的地方，例如在网页开发中使用框架时需要声明式编程，而在设计算法和其他底层需求时则需要命令式编程。</p></blockquote></li></ul><h4 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h4><ul><li><p>基本数据结构（基本类型、数组、矩阵、对象…）</p></li><li><p>缓存和 memoization</p><p>memoization 没有一个很好的词能翻译，大概意思就是通过存储函数调用的结果，并在再次使用相同输入调用函数时直接返回已存储的结果，从而加速计算逻辑。斐波那契数列就是一个使用 memoization 的例子</p></li><li><p>Hash codes、 tokens、编码（比如 Base64）</p></li><li><p><a href="https://stackoverflow.com/a/80113/1213497">栈与堆内存</a> </p><p>链接指向一则在 <a href="https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap/80113#80113">stackoverflow</a>  提出堆栈相关的诸多疑问，最高数回答解释了堆栈两种内存分配方式的基本概念、操作方式和性能差异，其中栈内存分配方式由于其后进先出的特性和近距离的存取模式，使得其在内存分配和回收上更加高效；而堆内存分配方式由于其动态和灵活的特性，对内存的管理相对复杂，但能够满足更多的内存需求</p></li></ul><h4 id="代码整洁"><a href="#代码整洁" class="headerlink" title="代码整洁"></a>代码整洁</h4><ul><li><p>懂得命名对代码的可读性的重要性</p></li><li><p>避免过长的方法和类，确保职责被划分到各个方法或者类中</p></li><li><p>遵循约定来管理项目结构</p></li><li><p>将复杂的布尔条件提取到命名良好的函数中</p></li><li><p>尽量编写尽可能自解释的代码（即通过阅读代码就能容易理解代码的功能）</p></li><li><p>良好的命名和轻量的文档而不是行内注释</p><p><a href="https://www.codeproject.com/Articles/872073/Code-Comments-are-Lies">代码注释通常可能会误导人</a>，因为它们经常被用作一种捷径，用来解释一段混乱的代码块的功能，而不是投入时间去重构它以提高其可读性。</p><p>链接的文章主张编写清晰、自解释和可维护的代码，而不是过度依赖注释，同时也承认在某些特殊情况下，注释是有其必要性和价值的。</p></li><li><p>将文档编写为代码，理想情况下与代码一起，以便于维护（例如，在仓库中的“docs”文件夹中的 markdown 文件）</p></li><li><p>使用文档来描述“为什么”和“怎么做”（例如，目标、用例、组件、高级架构概述等）</p></li><li><p>在面向对象编程中，组合优于继承</p></li><li><p><a href="https://semver.org/">Follow 语义化</a></p></li><li><p>了解TDD及其实践（例如，“红色，绿色，重构”）</p><blockquote><p><strong>TDD</strong>(测试驱动开发)是戴两顶帽子思考的开发方式：先戴上实现功能的帽子，在测试的辅助下，快速实现其功能；再戴上测试驱动开发的帽子，在测试的保护下，通过去除冗余的代码，提高代码品质。测试驱动着整个开发过程：首先，驱动代码的设计和功能的实现；其后，驱动代码的再设计和重构。</p><ul><li><p>红色：首先编写一个针对新功能的测试用例，此时由于功能尚未实现，测试用例将无法通过（失败，显示红色）</p></li><li><p>绿色：接下来编写功能代码，使得测试用例能够通过（成功，显示绿色）。在这个阶段，重点是让测试通过，而不是编写完美的代码。</p></li><li><p>重构：在测试用例通过后，对功能代码进行优化和重构，提高代码质量，同时确保测试用例仍然能够通过。</p></li></ul></blockquote></li></ul><h4 id="源码管理能力"><a href="#源码管理能力" class="headerlink" title="源码管理能力"></a>源码管理能力</h4><ul><li><p>CVS（控制版本系统）/ SCM（源代码管理）基础知识：分支、标签、集中式与分散式等</p></li><li><p>SCM与仓库管理/托管的区别（即<a href="https://stackoverflow.com/a/13321586">Git与GitHub之间的区别</a>）</p></li><li><p>理解版本化的重要性</p></li><li><p>Commit 最佳实践</p><ul><li><p><a href="https://lucasr.org/2011/01/29/micro-commits/">微提交</a> /原子提交，良好的描述等</p></li><li><p><a href="https://www.conventionalcommits.org/en/v1.0.0/">常规提交</a></p></li></ul></li><li><p>功能分支（短期）</p></li><li><p>基于主干的开发</p></li><li><p>依赖管理（包管理器的重要性，依赖地狱的风险等）</p></li></ul><h4 id="技术合作"><a href="#技术合作" class="headerlink" title="技术合作"></a>技术合作</h4><ul><li><p><a href="https://blog.github.com/2015-01-21-how-to-write-the-perfect-pull-request/">代码 review 最佳实践</a></p><ul><li>一句话来说就是：在执行代码审查时关注相关部分。目的是学习，而不是指责。</li></ul></li><li><p><a href="https://martinfowler.com/articles/on-pair-programming.html">结对编程</a></p><p>这篇文章主要讨论了结对编程（Pair Programming）的相关主题，包括其风格、时间管理、轮换策略、日常规划、物理环境设置、远程配对等方面。还探讨了结对编程的好处和挑战，以及如何说服管理者和同事采用这种方法。此外，文章还涉及了一些与配对编程相关的细节和常见问题</p></li></ul><h4 id="DevOps-实践"><a href="#DevOps-实践" class="headerlink" title="DevOps 实践"></a>DevOps 实践</h4><ul><li><p>自动化构建</p></li><li><p>构件仓库和镜像注册表</p></li><li><p>编写自动化测试</p></li><li><p>单元、集成和端到端（e2e）测试之间的区别</p></li><li><p>测试金字塔</p></li><li><p>持续集成</p></li><li><p>持续交付与持续部署</p></li><li><p>功能 Flag 和功能开关</p></li></ul><h4 id="通用技术知识"><a href="#通用技术知识" class="headerlink" title="通用技术知识"></a>通用技术知识</h4><h5 id="语言理论知识"><a href="#语言理论知识" class="headerlink" title="语言理论知识"></a>语言理论知识</h5><ul><li><p>正则表达式（regex）</p></li><li><p>编译型与解释型语言</p></li><li><p><a href="https://medium.com/@cpave3/understanding-types-static-vs-dynamic-strong-vs-weak-88a4e1f0ed5f">动态与静态 &amp; 弱类型与强类型语言类型</a> </p></li></ul><h5 id="优化"><a href="#优化" class="headerlink" title="优化"></a>优化</h5><ul><li><p>懒加载</p></li><li><p><a href="https://en.wikipedia.org/wiki/Profiling_(computer_programming">性能分析</a>)</p></li></ul><h5 id="并发"><a href="#并发" class="headerlink" title="并发"></a>并发</h5><ul><li><p>竞态条件</p></li><li><p>死锁</p></li><li><p>互斥</p></li></ul><h3 id="特定领域技术知识"><a href="#特定领域技术知识" class="headerlink" title="特定领域技术知识"></a>特定领域技术知识</h3><p>在某些情况下，您可能希望工程师已经了解某些特定领域，例如前端、后端、架构、基础设施或安全方面。在这些情况下，还有一些跨框架的概念和原则，可用于推动针对每个领域的特定技术知识的内容。</p><h4 id="前端开发"><a href="#前端开发" class="headerlink" title="前端开发"></a>前端开发</h4><ul><li><p>API通信（不同的架构标准，数据如何传输…）</p></li><li><p>DOM（定义，理解，虚拟DOM…）</p></li><li><p>浏览器事件</p></li><li><p>响应式设计（目的，优点，渐进增强…）</p></li><li><p>客户端渲染（CSR）与服务器端渲染（SSR）</p></li><li><p>分页</p></li><li><p>状态管理（相关问题，无状态方法…）</p></li><li><p>MVC 和相关的衍生品</p></li><li><p>WebSockets 网络通信协议</p></li></ul><h4 id="后端开发"><a href="#后端开发" class="headerlink" title="后端开发"></a>后端开发</h4><ul><li><p>API设计（不同的架构标准，数据如何传输…）</p></li><li><p><a href="https://en.wikipedia.org/wiki/Message_broker">消息代理</a></p></li><li><p>关系型数据库（它们是如何工作的，基本概念…）</p></li><li><p>非关系型数据库</p></li><li><p>数据库设计</p></li><li><p>ORM（对象关系映射）</p></li><li><p>批处理进程 / 定时任务</p></li><li><p>会话处理</p></li><li><p><a href="https://lti.umuc.edu/contentadaptor/topics/byid/db0a8c4f-f738-4674-9f60-b75323cdb07f">错误处理、审查、日志记录</a></p></li></ul><h4 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h4><ul><li><p>API</p><ul><li><p>标准协议：REST / SOAP </p></li><li><p>安全性（例如拦截机器人，控制账户接管攻击等）</p></li><li><p>针对第三方服务故障的弹性橱窗（例如断路器）</p></li></ul></li><li><p>外部可配置化</p></li><li><p><a href="https://www.romenrg.com/blog/2019/12/31/everything-as-code/">万物皆代码（即配置即代码，基础设施即代码，文档即代码…）</a></p></li><li><p>单体应用与微服务</p></li><li><p>领域驱动设计（DDD）</p></li><li><p>六边形架构</p></li><li><p>服务 Mesh</p></li><li><p>相关的互联网协议及其用法（如 HTTP, HTTPS, TCP, UDP, LDAP, SSH, SMTP…）</p></li><li><p><a href="https://en.wikipedia.org/wiki/Data_modeling">数据建模</a></p></li></ul><h4 id="基础设施"><a href="#基础设施" class="headerlink" title="基础设施"></a>基础设施</h4><ul><li><p>虚拟机与容器</p></li><li><p>进程与线程</p></li><li><p>控制器-代理/主副本模式</p></li><li><p>C/S模式</p></li><li><p>IAAS, PAAS, SASS</p></li><li><p>Web服务器</p></li><li><p>反向代理</p></li><li><p>负载均衡</p></li><li><p>冗余</p></li><li><p>延迟</p></li><li><p>监控</p></li><li><p><a href="https://docs.honeycomb.io/learning-about-observability/intro-to-observability/">可监控性</a></p></li></ul><h4 id="安全"><a href="#安全" class="headerlink" title="安全"></a>安全</h4><ul><li><p>身份和访问管理（IAM）</p><ul><li><p>认证（JWT, SSO）</p></li><li><p>授权（RBAC, ABAC）</p></li><li><p>公钥密码系统（例如RSA）</p></li><li><p>加密协议（TLS, SSL）</p></li><li><p>最小权限原则</p></li><li><p>DoS / DDoS</p></li><li><p>SQL 注入</p></li><li><p>中间人攻击</p></li><li><p>XSS（跨站脚本攻击） 和 CSRF（跨站请求伪造）</p></li></ul></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近在 Github 看到这一篇将程序员一直需要使用的非技术核心能力进行了总结，深受里面内容的启发，语言、框架都是会过时的，但有些技能无论是什么语言或者框架都是通用的，如果要在这个行业持续深根，那么这些非技术能力是必备的且实用的。本文在原文上进行翻译，并对文中提到部分专业术</summary>
      
    
    
    
    <category term="思考总结" scheme="http://julis.wang/categories/thinking/"/>
    
    
  </entry>
  
  <entry>
    <title>UTF-8字符编码相关</title>
    <link href="http://julis.wang/2024/04/11/%E5%85%B3%E4%BA%8EWindows%E4%B8%AD%E6%96%87%E5%AD%97%E7%AC%A6%E4%B9%B1%E7%A0%81/"/>
    <id>http://julis.wang/2024/04/11/%E5%85%B3%E4%BA%8EWindows%E4%B8%AD%E6%96%87%E5%AD%97%E7%AC%A6%E4%B9%B1%E7%A0%81/</id>
    <published>2024-04-11T06:23:00.000Z</published>
    <updated>2025-05-20T11:46:57.000Z</updated>
    
    <content type="html"><![CDATA[<p>最近在 Windows 上开发一些逻辑的时候遇到一些关于中文的坑，中文路径会乱码，是由于 Window 系统默认的编码格式是 <strong>GBK</strong>，而传入的参数编码格式是 <strong>UTF-8</strong>，导致整个程序出错。后续使用了<code>`MultiByteToWideChar</code> 和<code>WideCharToMultiByte</code> 方法对编码进行一次改变，从而避免了这个问题的产生。但不了解相关原因，经过一番学习，对相关的概念进行一些简单的总结，并对一些 api  的实现源码进行分析。</p><h3 id="ASCII-码"><a href="#ASCII-码" class="headerlink" title="ASCII 码"></a>ASCII 码</h3><p> ASCII ( American Standard Code for Information Interchange)<br> 256个符号，从 00000000 到 11111111    </p><h3 id="ANSI"><a href="#ANSI" class="headerlink" title="ANSI"></a>ANSI</h3><p>ANSI（American National Standards Institute，美国国家标准协会）编码：ANSI 编码是一种基于 8 位的字符编码。它包含了 128 个美国英语字符和其他 128 个特殊字符，共 256 个字符。ANSI 编码主要用于表示英语字符，但它的局限性在于无法表示其他语言的字符。为了解决这个问题，各国家和地区分别制定了自己的 ANSI 编码标准，但这又引入了新的问题，即不同编码之间的互不兼容。</p><p>​       美国和西欧：Windows-1252<br>​       中文（简体）：GB2312 或 GBK<br>​       中文（繁体）：Big5<br>​       日文：Shift-JIS<br>​       韩文：EUC-KR   </p><h3 id="Unicode"><a href="#Unicode" class="headerlink" title="Unicode"></a>Unicode</h3><p>为了解决字符编码之间的兼容性问题，Unicode 标准应运而生。Unicode 是一种包含世界上大多数字符的编码方案，它为每个字符分配一个唯一的数字（称为码点），无论在任何平台、程序或语言中，都可以表示这些字符。Unicode 有多种实现方式，如 UTF-8、UTF-16 和 UTF-32。UTF-8 是最常用的 Unicode 实现方式，它是一种变长编码，可以使用 1 到 4 个字节来表示一个字符，这使得它在存储和传输方面更加高效</p><p>  “FE FF” 是 Unicode 字符串的字节顺序标记（Byte Order Mark，简称 BOM），用于表示字符串的字节顺序<br>  Unicode Little-Endian，”FF FE”<br>  Unicode Big-Endian，”FE FF”</p><h3 id="UTF-8"><a href="#UTF-8" class="headerlink" title="UTF-8"></a>UTF-8</h3><p>UTF-8 是 Unicode 的实现方式之一  ，是一种变长编码，它使用 1 到 4 个字节（8 位）来表示一个字符</p><p><strong>单字节</strong>   所有的ASCII 字符<br><strong>二字节</strong>  带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要二个字节编码                </p><p><strong>三字节</strong> 基本等同于GBK，含21000多个汉字 </p><p><strong>四字节</strong> 中日韩超大字符集里面的汉字，有5万多个</p><p><strong>UTF-8编码对照表</strong></p><div class="table-container"><table><thead><tr><th>Unicode 符号范围   (十六进制)</th><th>UTF-8编码方式（二进制）</th></tr></thead><tbody><tr><td>0000 0000 ~ 0000 007F</td><td>0xxxxxxx</td></tr><tr><td>0000 0080 ~ 0000 07FF</td><td>110xxxxx 10xxxxxx</td></tr><tr><td>0000 0800 ~ 0000 FFFF</td><td>1110xxxx 10xxxxxx 10xxxxxx</td></tr><tr><td>0001 0000 ~ 0010 FFFF</td><td>11110xxx 10xxxxxx 10xxxxxx 10xxxxxx</td></tr></tbody></table></div><h3 id="源码阅读：Java-String-toUtf8"><a href="#源码阅读：Java-String-toUtf8" class="headerlink" title="源码阅读：Java String toUtf8"></a>源码阅读：Java String toUtf8</h3><p> Java 的 String 默认用 UTF-16 存储数据，String 类的方法<code>.getBytes(StandardCharsets.UTF_8)</code> 将指定的字符集将字符串编码为 byte 序列，并将结果存储到一个新的 byte 数组中。</p><p>其主要逻辑在:<a href="https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/CharsetUtils.java#46">CharsetUtils.java#toUtf8Bytes</a></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">native</span> <span class="type">byte</span>[] toUtf8Bytes(String s, <span class="type">int</span> offset, <span class="type">int</span> length);</span><br></pre></td></tr></table></figure><p>对应的最终实现：<a href="https://android.googlesource.com/platform/libcore/+/3e8abdd9bdca823a635aac3adacf71ef227b18e1/luni/src/main/native/java_nio_charset_Charsets.cpp#183">java_nio_charset_Charsets.cpp#Charsets_toUtf8Bytes</a> </p><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="type">static</span> jbyteArray <span class="title">Charsets_toUtf8Bytes</span><span class="params">(JNIEnv* env, jclass, jcharArray javaChars, jint offset, jint length)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// ....此处省略 一些检查逻辑</span></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> end = offset + length;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = offset; i &lt; end; ++i) &#123;</span><br><span class="line">        jint ch = chars[i];</span><br><span class="line">        <span class="keyword">if</span> (ch &lt; <span class="number">0x80</span>) &#123;</span><br><span class="line">            <span class="comment">// 单字节直接放进去</span></span><br><span class="line">            <span class="keyword">if</span> (!out.<span class="built_in">append</span>(ch)) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (ch &lt; <span class="number">0x800</span>) &#123;</span><br><span class="line">            <span class="comment">// 双字节</span></span><br><span class="line">            <span class="keyword">if</span> (!out.<span class="built_in">append</span>((ch &gt;&gt; <span class="number">6</span>) | <span class="number">0xc0</span>) || !out.<span class="built_in">append</span>((ch &amp; <span class="number">0x3f</span>) | <span class="number">0x80</span>)) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="built_in">U16_IS_SURROGATE</span>(ch)) &#123;</span><br><span class="line">          <span class="comment">// ....此处省略 UTF-16 代理字符串相关的逻辑</span></span><br><span class="line">            ch = <span class="built_in">U16_GET_SUPPLEMENTARY</span>(high, low);</span><br><span class="line">            <span class="comment">// 四字节 </span></span><br><span class="line">            jbyte b1 = (ch &gt;&gt; <span class="number">18</span>) | <span class="number">0xf0</span>;</span><br><span class="line">            jbyte b2 = ((ch &gt;&gt; <span class="number">12</span>) &amp; <span class="number">0x3f</span>) | <span class="number">0x80</span>;</span><br><span class="line">            jbyte b3 = ((ch &gt;&gt; <span class="number">6</span>) &amp; <span class="number">0x3f</span>) | <span class="number">0x80</span>;</span><br><span class="line">            jbyte b4 = (ch &amp; <span class="number">0x3f</span>) | <span class="number">0x80</span>;</span><br><span class="line">            <span class="keyword">if</span> (!out.<span class="built_in">append</span>(b1) || !out.<span class="built_in">append</span>(b2) || !out.<span class="built_in">append</span>(b3) || !out.<span class="built_in">append</span>(b4)) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// 三字节.</span></span><br><span class="line">            jbyte b1 = (ch &gt;&gt; <span class="number">12</span>) | <span class="number">0xe0</span>;</span><br><span class="line">            jbyte b2 = ((ch &gt;&gt; <span class="number">6</span>) &amp; <span class="number">0x3f</span>) | <span class="number">0x80</span>;</span><br><span class="line">            jbyte b3 = (ch &amp; <span class="number">0x3f</span>) | <span class="number">0x80</span>;</span><br><span class="line">            <span class="keyword">if</span> (!out.<span class="built_in">append</span>(b1) || !out.<span class="built_in">append</span>(b2) || !out.<span class="built_in">append</span>(b3)) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> out.<span class="built_in">toByteArray</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>整体的逻辑非常的好理解：判断输入值的区间，并分成单双三四字节的处理逻辑，其中有处理 UTF-16 代理字符串相关的逻辑此处忽略，可以了解<a href="https://learn.microsoft.com/zh-cn/windows/win32/intl/surrogates-and-supplementary-characters">代理项和增补字符</a>。对应单字节符号处理，直接将原始值返回即可，其他的字节就一个一个地获取，这里分析一下对于双字节的逻辑处理。获取第一个字节的逻辑为：<code>(ch &gt;&gt; 6) | 0xc0</code>第二个字节逻辑为 <code>(ch &amp; 0x3f) | 0x80</code> </p><ul><li><p><code>(ch &gt;&gt; 6) | 0xc0</code></p><p>第一个字节的前两位是 <code>11</code>（十六进制中的 <code>0xc0</code>），后面的 5 位是 Unicode 码点的高 5 位</p></li><li><p><code>(ch &amp; 0x3f) | 0x80</code></p><p>第二个字节的前两位是 <code>10</code>（十六进制中的 <code>0x80</code>），后面的 6 位是 Unicode 码点的低 6 位</p></li></ul><p>举例，希腊符号  <code>ε</code>(epsilon) 在 UTF-8 编码里面是用双字节表示， Unicode 为 <code>0x03B5</code>  对应二进制数据：<code>0000001110110101</code>，计算流程如下所示：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"># ε 0x03B5 to UTF-8 </span><br><span class="line"></span><br><span class="line"># 第一个字节 (ch &gt;&gt; 6) | 0xc0</span><br><span class="line">0000001110110101 &gt;&gt; 6</span><br><span class="line">      0000001110 | 0xc0 (11000000)</span><br><span class="line">        11000000</span><br><span class="line">             ||</span><br><span class="line">        11001110</span><br><span class="line">            0xCE</span><br><span class="line"></span><br><span class="line"># 第二个字节 (ch &amp; 0x3f) | 0x80</span><br><span class="line">0000001110110101 &amp; 0x3f (111111)</span><br><span class="line">          111111</span><br><span class="line">          110101 | 0x80 (10000000)</span><br><span class="line">        10000000</span><br><span class="line">              ||</span><br><span class="line">        10110101</span><br><span class="line">            0xB5</span><br></pre></td></tr></table></figure><p>从而计算出  <code>ε</code> 对应的 UTF-8 Encoding为<code>0xCE 0xB5</code></p><h3 id="“锟斤拷”和“烫”"><a href="#“锟斤拷”和“烫”" class="headerlink" title="“锟斤拷”和“烫”"></a>“锟斤拷”和“烫”</h3><p><code>“锟斤拷”</code>通常发生在UTF-8 到 GBK 编码的转换中，在 UTF-8 编码中，”0xEF 0xBF 0xBD” 是一个特殊的字符，表示 REPLACEMENT CHARACTER（替换字符），当解码器在解码字节序列时遇到无法识别的字节或无效的编码时，通常会用 REPLACEMENT CHARACTER（U+FFFD）替换这些无效的字节 ，”0xEF 0xBF 0xBD” 在 GBK 里面则编码成 “锟斤拷”。</p><p><code>“烫”</code> 则是由于在 Windows 操作系统中，开发者在使用调试器调试程序时，会发现未初始化的内存通常会被填充为0xCC，而”0xCC” 在 GBK 里面则编码成“烫”。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>本文主要讨论了字符编码的一些基本概念和原理，包括 ASCII、ANSI、Unicode 和 UTF-8 编码，文章分析了 Java String 类的<code>.getBytes(StandardCharsets.UTF_8)</code>方法的实现源码，解释了将 Unicode 字符串转换为 UTF-8 编码字节序列的过程，最后介绍了一下 “锟斤拷”和”烫”为什么会被展示。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近在 Windows 上开发一些逻辑的时候遇到一些关于中文的坑，中文路径会乱码，是由于 Window 系统默认的编码格式是 &lt;strong&gt;GBK&lt;/strong&gt;，而传入的参数编码格式是 &lt;strong&gt;UTF-8&lt;/strong&gt;，导致整个程序出错。后续使用了&lt;cod</summary>
      
    
    
    
    
    <category term="UTF-8" scheme="http://julis.wang/tags/UTF-8/"/>
    
  </entry>
  
  <entry>
    <title>实现一个自定义 FFmpeg Filter</title>
    <link href="http://julis.wang/2024/03/07/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%87%AA%E5%AE%9A%E4%B9%89FFmpeg-Filter/"/>
    <id>http://julis.wang/2024/03/07/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%87%AA%E5%AE%9A%E4%B9%89FFmpeg-Filter/</id>
    <published>2024-03-07T02:58:00.000Z</published>
    <updated>2025-12-22T03:35:09.445Z</updated>
    
    <content type="html"><![CDATA[<p>此前在做  ffmpeg+某个第三库作为 filter 的集成，第三库是做AE特效相关的，与 ffmpeg 结合能让视频渲染效果大大提升。整体流程将第三方库作为 ffmpeg 的一个filter 形式进行结合，其中就涉及到 ffmpeg 的 filter 开发，本文即 对ffmpeg 的滤镜开发流程作一个总结。本文以实现一个视频垂直翻转的 filter 为例，ffmpeg 源码基于<a href="https://github.com/FFmpeg/FFmpeg/tree/release/6.1">FFmpeg6.1</a> </p><h2 id="实现自定义-Filter-流程"><a href="#实现自定义-Filter-流程" class="headerlink" title="实现自定义 Filter 流程"></a>实现自定义 Filter 流程</h2><ul><li><p>编写 filter.c 文件</p><p>一般视频滤镜以 vf_ 为前缀，视频滤镜以 af_ 为前缀，放在libavfilter目录下，参考其他 filter 代码逻辑，模块化配置相关参数，本文例以 vf_flip.c 实现视频的上下翻转</p></li><li><p>在 <code>libavfilter/allfilters.c</code> 注册</p><p>例如：extern const AVFilter ff_vf_flip;  <code>ff_vf_flip</code>就是在 <code>vf_flip.c</code>的 filter 注册名称</p></li><li><p>修改 <code>libavfilter/Makefile</code> 添加编译配置： </p><p>例如：OBJS-$(CONFIG_FLIP_FILTER)                   += vf_flip.o</p></li><li><p>编译打包</p></li></ul><h2 id="编写-filter-c-文件"><a href="#编写-filter-c-文件" class="headerlink" title="编写 filter.c 文件"></a>编写 filter.c 文件</h2><h3 id="AVFilter主体"><a href="#AVFilter主体" class="headerlink" title="AVFilter主体"></a>AVFilter主体</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">AVFilter</span> &#123;</span></span><br><span class="line">  <span class="type">const</span> <span class="type">char</span> *name;</span><br><span class="line">  <span class="type">const</span> <span class="type">char</span> *description;</span><br><span class="line">  <span class="type">const</span> AVFilterPad *inputs;</span><br><span class="line">  <span class="type">const</span> AVFilterPad *outputs;</span><br><span class="line">  <span class="type">const</span> AVClass *priv_class;</span><br><span class="line">  <span class="type">int</span> flags;</span><br><span class="line">  <span class="type">int</span> (*preinit)(AVFilterContext *ctx);</span><br><span class="line">  <span class="type">int</span> (*init)(AVFilterContext *ctx);</span><br><span class="line">  <span class="type">int</span> (*init_dict)(AVFilterContext *ctx, AVDictionary **options);</span><br><span class="line">  <span class="type">void</span> (*uninit)(AVFilterContext *ctx);</span><br><span class="line">  <span class="type">int</span> (*query_formats)(AVFilterContext *);</span><br><span class="line">  <span class="type">int</span> priv_size;   </span><br><span class="line">  <span class="type">int</span> flags_internal; </span><br><span class="line">  <span class="class"><span class="keyword">struct</span> <span class="title">AVFilter</span> *<span class="title">next</span>;</span></span><br><span class="line">  <span class="type">int</span> (*process_command)(AVFilterContext *, <span class="type">const</span> <span class="type">char</span> *cmd, <span class="type">const</span> <span class="type">char</span> *arg, <span class="type">char</span> *res, <span class="type">int</span> res_len, <span class="type">int</span> flags);</span><br><span class="line">  <span class="type">int</span> (*init_opaque)(AVFilterContext *ctx, <span class="type">void</span> *opaque);</span><br><span class="line">  <span class="type">int</span> (*activate)(AVFilterContext *ctx);</span><br><span class="line">&#125; AVFilter;</span><br></pre></td></tr></table></figure><p>具体里面的属性作用可以参考：<a href="https://www.cnblogs.com/TaigaCon/p/10171464.html">[ffmpeg] 定制滤波器</a>，可以根据需求实现里面的相关函数，接下来以一个最简单的 Filter 和一个较复杂一点的 Filter 举例。</p><h3 id="最简单的-AVFilter"><a href="#最简单的-AVFilter" class="headerlink" title="最简单的 AVFilter"></a>最简单的 AVFilter</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">const</span> AVClass *<span class="class"><span class="keyword">class</span>;</span></span><br><span class="line">&#125; NoopContext;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> <span class="title function_">filter_frame</span><span class="params">(AVFilterLink *link, AVFrame *frame)</span> &#123;</span><br><span class="line">    av_log(<span class="literal">NULL</span>, AV_LOG_INFO, <span class="string">&quot;filter frame pts:%lld\n&quot;</span>, frame-&gt;pts);</span><br><span class="line">    NoopContext *noopContext = link-&gt;dst-&gt;priv;</span><br><span class="line">    <span class="keyword">return</span> ff_filter_frame(link-&gt;dst-&gt;outputs[<span class="number">0</span>], frame);</span><br><span class="line">&#125;</span><br><span class="line"><span class="type">static</span> <span class="type">const</span> AVFilterPad noop_inputs[] = &#123;</span><br><span class="line">        &#123;</span><br><span class="line">                .name         = <span class="string">&quot;default&quot;</span>,</span><br><span class="line">                .type         = AVMEDIA_TYPE_VIDEO,</span><br><span class="line">                .filter_frame = filter_frame,</span><br><span class="line">        &#125;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="type">static</span> <span class="type">const</span> AVFilterPad noop_outputs[] = &#123;</span><br><span class="line">        &#123;</span><br><span class="line">                .name = <span class="string">&quot;default&quot;</span>,</span><br><span class="line">                .type = AVMEDIA_TYPE_VIDEO,</span><br><span class="line">        &#125;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="type">const</span> AVFilter ff_vf_noop = &#123;</span><br><span class="line">        .name          = <span class="string">&quot;noop&quot;</span>,</span><br><span class="line">        .description   = NULL_IF_CONFIG_SMALL(<span class="string">&quot;Pass the input video unchanged.&quot;</span>),</span><br><span class="line">        .priv_size     = <span class="keyword">sizeof</span>(NoopContext),</span><br><span class="line">        FILTER_INPUTS(noop_inputs),</span><br><span class="line">        FILTER_OUTPUTS(noop_outputs),</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>命令行运行：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">ffmpeg -i test.mp4 -vf &quot;noop&quot; noop.mp4</span><br></pre></td></tr></table></figure><p> 正常输出文件（对原片没有做任何更改）,这个 filter 的作用是将输入的视频帧不做任何处理地传递给下一个过滤器，在处理每帧的时候会打印处理的 PTS，麻雀虽小五脏俱全，它包含了一个 AVFilter 基础的结构：</p><ol><li><p><strong><code>NoopContext</code></strong></p><p>这是一个简单的结构体，包含一个指向 AVClass 的指针。在这个例子中，实际上没有使用到 NoopContext 结构体的任何成员，因为这个过滤器没有需要存储的私有数据。</p></li><li><p><strong><code>filter_frame</code></strong> </p><p>这个函数的作用是处理输入的视频帧。在这个例子中，它只是打印帧的 PTS（Presentation Time Stamp，显示时间戳）并将帧传递给下一个过滤器，不对帧做任何修改。</p></li><li><p><strong><code>noop_inputs</code> 和 <code>noop_outputs</code></strong></p><p>这两个数组定义了过滤器的输入和输出 Pad。在这个例子中，输入 Pad 类型为 AVMEDIA_TYPE_VIDEO，并关联了 <code>filter_frame</code> 函数。输出 Pad 也是 AVMEDIA_TYPE_VIDEO 类型，但没有关联任何函数，因为输出直接由 <code>filter_frame</code> 函数处理。</p></li><li><p><strong><code>ff_vf_noop</code></strong></p><p>这是一个 AVFilter 结构体实例，包含了过滤器的名称、描述、私有数据大小以及输入和输出 Pad。在这个例子中，过滤器的名称为 “noop”，描述为 “Pass the input video unchanged.”，这也就是在执行：<code>ffmpeg -filters</code> 看到的 Filter描述内容。</p></li></ol><p>接下来看一个稍微复杂的一个 AVFilter，实现一个视频的上下翻转</p><h3 id="复杂一点的-AVFilter"><a href="#复杂一点的-AVFilter" class="headerlink" title="复杂一点的 AVFilter"></a>复杂一点的 AVFilter</h3><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">FlipContext</span> &#123;</span></span><br><span class="line">    <span class="type">const</span> AVClass *<span class="class"><span class="keyword">class</span>;</span></span><br><span class="line">    <span class="type">int</span> duration;</span><br><span class="line">&#125; FlipContext;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> OFFSET(x) offsetof(FlipContext, x)</span></span><br><span class="line"><span class="type">static</span> <span class="type">const</span> AVOption flip_options[] = &#123;</span><br><span class="line">        &#123;<span class="string">&quot;duration&quot;</span>, <span class="string">&quot;set flip duration&quot;</span>, OFFSET(duration), AV_OPT_TYPE_INT, &#123;.i64 = <span class="number">0</span>&#125;, <span class="number">0</span>, INT_MAX, .flags = AV_OPT_FLAG_FILTERING_PARAM&#125;,</span><br><span class="line">        &#123;<span class="literal">NULL</span>&#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> av_cold <span class="type">int</span> <span class="title function_">flip_init</span><span class="params">(AVFilterContext *ctx)</span> &#123;</span><br><span class="line">    FlipContext *context = ctx-&gt;priv;</span><br><span class="line">    av_log(<span class="literal">NULL</span>, AV_LOG_ERROR, <span class="string">&quot;Input duration: %d.\n&quot;</span>, context-&gt;duration);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> av_cold <span class="type">void</span> <span class="title function_">flip_uninit</span><span class="params">(AVFilterContext *ctx)</span> &#123;</span><br><span class="line">    FlipContext *context = ctx-&gt;priv;</span><br><span class="line">    <span class="comment">// no-op 本例无需释放滤镜实例分配的内存、关闭文件、资源等</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 对输入的 AVFrame 进行翻转</span></span><br><span class="line"><span class="type">static</span> AVFrame *<span class="title function_">flip_frame</span><span class="params">(AVFilterContext *ctx, AVFrame *in_frame)</span> &#123;</span><br><span class="line"> AVFilterLink *inlink = ctx-&gt;inputs[<span class="number">0</span>];</span><br><span class="line">    FlipContext *s = ctx-&gt;priv;</span><br><span class="line">    <span class="type">int64_t</span> pts = in_frame-&gt;pts;</span><br><span class="line">    <span class="comment">// 将时间戳（pts）转化以秒为单位的时间戳</span></span><br><span class="line">    <span class="type">float</span> time_s = TS2T(pts, inlink-&gt;time_base);</span><br><span class="line">    <span class="keyword">if</span> (time_s &gt; s-&gt;duration) &#123;</span><br><span class="line">        <span class="comment">// 超过对应的时间则直接输出in_frame</span></span><br><span class="line">        <span class="keyword">return</span> in_frame;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 创建输出帧并分配内存</span></span><br><span class="line">    AVFrame *out_frame = av_frame_alloc();</span><br><span class="line">    <span class="keyword">if</span> (!out_frame) &#123;</span><br><span class="line">        av_frame_free(&amp;in_frame);</span><br><span class="line">        <span class="keyword">return</span> out_frame;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 设置输出帧的属性</span></span><br><span class="line">    out_frame-&gt;format = in_frame-&gt;format;</span><br><span class="line">    out_frame-&gt;width = in_frame-&gt;width;</span><br><span class="line">    out_frame-&gt;height = in_frame-&gt;height;</span><br><span class="line">    out_frame-&gt;pts = in_frame-&gt;pts;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 分配输出帧的数据缓冲区</span></span><br><span class="line">    <span class="type">int</span> ret = av_frame_get_buffer(out_frame, <span class="number">32</span>);</span><br><span class="line">    <span class="keyword">if</span> (ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">        av_frame_free(&amp;in_frame);</span><br><span class="line">        av_frame_free(&amp;out_frame);</span><br><span class="line">        <span class="keyword">return</span> out_frame;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 这个示例仅适用于 YUV 格式的视频。对于其他格式（如 RGB）</span></span><br><span class="line">    <span class="comment">// 翻转输入帧的数据到输出帧</span></span><br><span class="line">    <span class="comment">// 翻转了 Y 分量，然后翻转了 U 和 V 分量</span></span><br><span class="line">    <span class="comment">//</span></span><br><span class="line">    <span class="type">uint8_t</span> *src_y = in_frame-&gt;data[<span class="number">0</span>];</span><br><span class="line">    <span class="type">uint8_t</span> *src_u = in_frame-&gt;data[<span class="number">1</span>];</span><br><span class="line">    <span class="type">uint8_t</span> *src_v = in_frame-&gt;data[<span class="number">2</span>];</span><br><span class="line">    <span class="type">uint8_t</span> *dst_y = out_frame-&gt;data[<span class="number">0</span>] + (in_frame-&gt;height - <span class="number">1</span>) * out_frame-&gt;linesize[<span class="number">0</span>];</span><br><span class="line">    <span class="type">uint8_t</span> *dst_u = out_frame-&gt;data[<span class="number">1</span>] + (in_frame-&gt;height / <span class="number">2</span> - <span class="number">1</span>) * out_frame-&gt;linesize[<span class="number">1</span>];</span><br><span class="line">    <span class="type">uint8_t</span> *dst_v = out_frame-&gt;data[<span class="number">2</span>] + (in_frame-&gt;height / <span class="number">2</span> - <span class="number">1</span>) * out_frame-&gt;linesize[<span class="number">2</span>];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; in_frame-&gt;height; i++) &#123;</span><br><span class="line">        <span class="built_in">memcpy</span>(dst_y, src_y, in_frame-&gt;width);</span><br><span class="line">        src_y += in_frame-&gt;linesize[<span class="number">0</span>];</span><br><span class="line">        dst_y -= out_frame-&gt;linesize[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (i &lt; in_frame-&gt;height / <span class="number">2</span>) &#123;</span><br><span class="line">            <span class="built_in">memcpy</span>(dst_u, src_u, in_frame-&gt;width / <span class="number">2</span>);</span><br><span class="line">            <span class="built_in">memcpy</span>(dst_v, src_v, in_frame-&gt;width / <span class="number">2</span>);</span><br><span class="line">            src_u += in_frame-&gt;linesize[<span class="number">1</span>];</span><br><span class="line">            src_v += in_frame-&gt;linesize[<span class="number">2</span>];</span><br><span class="line">            dst_u -= out_frame-&gt;linesize[<span class="number">1</span>];</span><br><span class="line">            dst_v -= out_frame-&gt;linesize[<span class="number">2</span>];</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> out_frame;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> <span class="title function_">activate</span><span class="params">(AVFilterContext *ctx)</span> &#123;</span><br><span class="line">    AVFilterLink *inlink = ctx-&gt;inputs[<span class="number">0</span>];</span><br><span class="line">    AVFilterLink *outlink = ctx-&gt;outputs[<span class="number">0</span>];</span><br><span class="line">    AVFrame *in_frame = <span class="literal">NULL</span>;</span><br><span class="line">    AVFrame *out_frame = <span class="literal">NULL</span>;</span><br><span class="line">    <span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line">    <span class="comment">// 获取输入帧</span></span><br><span class="line">    ret = ff_inlink_consume_frame(inlink, &amp;in_frame);</span><br><span class="line">    <span class="keyword">if</span> (ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> ret;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 如果有输入帧，进行翻转处理</span></span><br><span class="line">    <span class="keyword">if</span> (in_frame) &#123;</span><br><span class="line">        <span class="comment">// 对输出帧进行上下翻转处理</span></span><br><span class="line">        out_frame = flip_frame(ctx, in_frame);</span><br><span class="line">        <span class="comment">// 将处理后的帧放入输出缓冲区</span></span><br><span class="line">        ret = ff_filter_frame(outlink, out_frame);</span><br><span class="line">        <span class="keyword">if</span> (ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            av_frame_free(&amp;out_frame);</span><br><span class="line">            <span class="keyword">return</span> ret;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 如果没有输入帧，尝试请求一个新的输入帧</span></span><br><span class="line">    <span class="keyword">if</span> (!in_frame) &#123;</span><br><span class="line">        ff_inlink_request_frame(inlink);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> status;</span><br><span class="line">    <span class="type">int64_t</span> pts;</span><br><span class="line">    ret = ff_inlink_acknowledge_status(inlink, &amp;status, &amp;pts);</span><br><span class="line">    <span class="keyword">if</span> (ret &lt; <span class="number">0</span>)</span><br><span class="line">        <span class="keyword">return</span> ret;</span><br><span class="line">    <span class="keyword">if</span> (status == AVERROR_EOF) &#123;</span><br><span class="line">        <span class="comment">// 输入链接已经结束，设置输出链接的状态为 EOF</span></span><br><span class="line">        ff_outlink_set_status(outlink, AVERROR_EOF, pts);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">AVFILTER_DEFINE_CLASS(flip);</span><br><span class="line"><span class="type">static</span> <span class="type">const</span> AVFilterPad flip_inputs[] = &#123;</span><br><span class="line">        &#123;</span><br><span class="line">                .name = <span class="string">&quot;default&quot;</span>,</span><br><span class="line">                .type = AVMEDIA_TYPE_VIDEO,</span><br><span class="line">        &#125;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="type">static</span> <span class="type">const</span> AVFilterPad flip_outputs[] = &#123;</span><br><span class="line">        &#123;</span><br><span class="line">                .name = <span class="string">&quot;default&quot;</span>,</span><br><span class="line">                .type = AVMEDIA_TYPE_VIDEO,</span><br><span class="line">        &#125;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="type">const</span> AVFilter ff_vf_flip = &#123;</span><br><span class="line">        .name = <span class="string">&quot;flip&quot;</span>,</span><br><span class="line">        .description = NULL_IF_CONFIG_SMALL(<span class="string">&quot;Flip the input video.&quot;</span>),</span><br><span class="line">        .priv_size = <span class="keyword">sizeof</span>(FlipContext),</span><br><span class="line">        .priv_class = &amp;flip_class,</span><br><span class="line">        .activate      = activate,</span><br><span class="line">        .init = flip_init,</span><br><span class="line">        .uninit = flip_uninit,</span><br><span class="line">        FILTER_INPUTS(flip_inputs),</span><br><span class="line">        FILTER_OUTPUTS(flip_outputs),</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>命令行运行：</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">ffmpeg -i test.mp4 -filter_complex &quot;[0:v]flip=duration=5[out];&quot; -map &quot;[out]&quot; flip.mp4</span><br></pre></td></tr></table></figure><p> 得到渲染好的视频，前5s是上下翻转的，后面的内容正常。</p><p>相比于最简单的 AVFilter 多了几个实现：</p><ol><li><p><strong><code>AVOption flip_options</code></strong></p><p>用于设置翻转持续时间的选项，外部命令配置可选输入<code>duration=5</code>，会自动对数据合法性进行校验。参数类型为 <code>AV_OPT_TYPE_INT</code>，默认值为 0，取值范围为 0 到 <code>INT_MAX</code>。<code>.flags</code> 设置为 <code>AV_OPT_FLAG_FILTERING_PARAM</code>，表示这是一个过滤参数。</p></li><li><p><strong><code>.priv_class</code></strong>  </p><p>配置的<code>flip_class</code>实际是通过 <code>AVFILTER_DEFINE_CLASS(flip);</code> 宏实现的一个声明：见：<a href="https://github.com/FFmpeg/FFmpeg/blob/release/6.1/libavfilter/internal.h#L311">internal.h#AVFILTER_DEFINE_CLASS_EXT</a></p></li><li><p><strong><code>init</code>&amp; <code>uninit</code></strong></p><p>滤镜在初始化或者释放资源的时候将会调用</p></li><li><p><strong><code>activate</code></strong></p><p>这个函数首先获取输入帧，然后调用 <code>flip_frame</code> 函数进行翻转操作，并将处理后的帧放入输出链接。如果没有输入帧，它会请求一个新的输入帧。最后，它会确认输入链接的状态，并根据需要设置输出链接的状态。</p></li></ol><p>这个例子相比最简单的 filter 使用了 <code>activate</code> 函数 用于帧渲染，而不是使用 <code>filter_frame</code>去渲染，这两个方法有什么区别于联系呢？查看：<a href="##filter_frame(">filter_frame和activate方法</a>和activate()函数)</p><p>也能通过 <code>filter_frame</code>实现，对代码部分逻辑更新更改：</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">const</span> AVFilterPad flip_inputs[] = &#123;</span><br><span class="line">        &#123;</span><br><span class="line">                .name = <span class="string">&quot;default&quot;</span>,</span><br><span class="line">                .type = AVMEDIA_TYPE_VIDEO,</span><br><span class="line">                .filter_frame = filter_frame, <span class="comment">//添加filter_frame 实现</span></span><br><span class="line">        &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">const</span> AVFilter ff_vf_flip = &#123;</span><br><span class="line">       ……</span><br><span class="line">        .priv_class = &amp;flip_class,</span><br><span class="line">       <span class="comment">// .activate      = activate,</span></span><br><span class="line">        .init = flip_init,</span><br><span class="line">       ……</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> <span class="title function_">filter_frame</span><span class="params">(AVFilterLink *inlink, AVFrame *in)</span> &#123;</span><br><span class="line">    AVFilterContext *ctx = inlink-&gt;dst;</span><br><span class="line">    FlipContext *s = ctx-&gt;priv;</span><br><span class="line">    AVFilterLink *outlink = ctx-&gt;outputs[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line">    <span class="type">int64_t</span> pts = in-&gt;pts;</span><br><span class="line">    <span class="comment">// 将时间戳（pts）转化以秒为单位的时间戳</span></span><br><span class="line">    <span class="type">float</span> time_s = TS2T(pts, inlink-&gt;time_base);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (time_s &gt; s-&gt;duration) &#123;</span><br><span class="line">        <span class="comment">// 超过对应的时间则直接输出in_frame</span></span><br><span class="line">        <span class="keyword">return</span> ff_filter_frame(outlink, in);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        av_log(<span class="literal">NULL</span>, AV_LOG_ERROR, <span class="string">&quot;time_s s: %f.\n&quot;</span>, time_s);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    AVFrame *out = flip_frame(ctx, in);</span><br><span class="line">    <span class="comment">// 释放输入帧</span></span><br><span class="line">    av_frame_free(&amp;in);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 将输出帧传递给下一个滤镜</span></span><br><span class="line">    <span class="keyword">return</span> ff_filter_frame(outlink, out);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>命令行运行，得到的输出结果是一样的。</p><h2 id="filter-frame-和activate-函数"><a href="#filter-frame-和activate-函数" class="headerlink" title="filter_frame()和activate()函数"></a>filter_frame()和activate()函数</h2><p>对于这点查了相关资料，看看源码相关的实现</p><p>参考：<a href="https://www.ffmpeg.org/doxygen/5.0/filter__design_8txt.html">https://www.ffmpeg.org/doxygen/5.0/filter__design_8txt.html</a></p><blockquote><p>The purpose of these rules is to ensure that frames flow in the filter graph without getting stuck and accumulating somewhere. Simple filters that output one frame for each input frame should not have to worry about it. There are two design for filters:one using the  <a href="https://www.ffmpeg.org/doxygen/5.0/vsink__nullsink_8c.html#aaa9a0e0f9de1464941d86a984cf77d37">filter_frame</a>() and <a href="https://www.ffmpeg.org/doxygen/5.0/vsrc__mptestsrc_8c.html#a72949c8fcad3f201712a3569fc6888cb">request_frame</a>() callbacks and the other using the activate() callback. The design using filter_frame() and request_frame() is legacy, but it is suitable for filters that have a single input and process one frame at a time. New filters with several inputs, that treat several frames at a time or that require a special treatment at EOF should probably use the design using activate(). activate ———— This method is called when something must be done in a filter</p></blockquote><p>大意，实现滤镜有两种实现方式：</p><ul><li><p><strong><code>filter_frame()</code></strong></p><p>可以被认为是历史遗留产物。在早期的 AVFilter 设计中，<code>filter_frame()</code> 和 <code>request_frame()</code> 是主要用于处理输入帧和请求输出帧的回调函数。这种设计适用于简单的过滤器，例如单输入且每次处理一个帧的过滤器。</p></li><li><p><strong><code>activate()</code></strong></p><p>随着 ffmpeg 和 AVFilter 的发展，处理需求变得越来越复杂，例如需要处理多个输入、一次处理多个帧或在文件结束（EOF）时进行特殊处理等。为了满足这些需求，引入了 <code>activate()</code> 函数，它提供了更灵活和强大的处理能力。因此，虽然 <code>filter_frame()</code> 在某些简单场景下仍然可以使用，但对于新的或复杂的过滤器，建议使用 <code>activate()</code> 函数。</p></li></ul><p>如果两个方法都实现了，那他们谁会先执行呢？</p><p>对应的源码处理逻辑： <a href="https://github.com/FFmpeg/FFmpeg/blob/release/6.1/libavfilter/avfilter.c#L1322">avfilter.c</a></p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">ff_filter_activate</span><span class="params">(AVFilterContext *filter)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> ret;</span><br><span class="line">……</span><br><span class="line">    ret = filter-&gt;filter-&gt;activate ? filter-&gt;filter-&gt;activate(filter) :</span><br><span class="line">          ff_filter_activate_default(filter);</span><br><span class="line">  ……</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果配置了activate() 函数则执行，否则执行 ff_filter_activate_default()-&gt;ff_filter_frame_to_filter()-&gt;ff_filter_frame_framed() 最终执行到配置的 filter_frame() 方法。</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span> <span class="title function_">ff_filter_frame_framed</span><span class="params">(AVFilterLink *link, AVFrame *frame)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> (*filter_frame)(AVFilterLink *, AVFrame *);</span><br><span class="line">    AVFilterContext *dstctx = link-&gt;dst;</span><br><span class="line">    AVFilterPad *dst = link-&gt;dstpad;</span><br><span class="line">    <span class="type">int</span> ret;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!(filter_frame = dst-&gt;filter_frame))</span><br><span class="line">        filter_frame = default_filter_frame;</span><br><span class="line">    ……</span><br><span class="line">    ret = filter_frame(link, frame);  <span class="comment">// 最终调用到的地方</span></span><br><span class="line">    link-&gt;frame_count_out++;</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">fail:</span><br><span class="line">    ……</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文介绍了 FFmpeg 滤镜开发的整体流程，如何编写 filter.c 文件，并以一个最简单的 AVFilter 和一个较为复杂的 AVFilter 为例，解析了滤镜开发的具体步骤和代码实现，并介绍了 filter_frame() 和 activate() 函数的区别与联系。</p><p>在滤镜开发过程中，需要注意的是，filter_frame() 和 activate() 函数的使用取决于滤镜的复杂性。对于简单的滤镜，可以使用 filter_frame() 函数；而对于需要处理多个输入、一次处理多个帧或在文件结束（EOF）时进行特殊处理的复杂滤镜，建议使用 activate() 函数。</p><p>文中的源码可以查看：<a href="https://github.com/VomPom/FFmpeg/commit/9176f58ae60e0b70e5708b25017f374deac9fae7">add most simplest  AVFilter and a simple video flip filter.</a></p><h3 id="参考："><a href="#参考：" class="headerlink" title="参考："></a>参考：</h3><p><a href="https://www.cnblogs.com/TaigaCon/p/10171464.html">https://www.cnblogs.com/TaigaCon/p/10171464.html</a></p><p><a href="https://www.cnblogs.com/ranson7zop/p/7728639.html">https://www.cnblogs.com/ranson7zop/p/7728639.html</a></p><p><a href="https://www.ffmpeg.org/doxygen/5.0/filter__design_8txt.html">https://www.ffmpeg.org/doxygen/5.0/filter__design_8txt.html</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;此前在做  ffmpeg+某个第三库作为 filter 的集成，第三库是做AE特效相关的，与 ffmpeg 结合能让视频渲染效果大大提升。整体流程将第三方库作为 ffmpeg 的一个filter 形式进行结合，其中就涉及到 ffmpeg 的 filter 开发，本文即 对f</summary>
      
    
    
    
    <category term="技术文章" scheme="http://julis.wang/categories/technology/"/>
    
    
    <category term="FFmpeg" scheme="http://julis.wang/tags/FFmpeg/"/>
    
    <category term="音视频" scheme="http://julis.wang/tags/%E9%9F%B3%E8%A7%86%E9%A2%91/"/>
    
  </entry>
  
  <entry>
    <title>RecyclerView自定义LayoutManager从0到1实践</title>
    <link href="http://julis.wang/2023/10/31/%E8%87%AA%E5%AE%9A%E4%B9%89LayoutManager%E4%BB%8E0%E5%88%B01%E5%AE%9E%E8%B7%B5/"/>
    <id>http://julis.wang/2023/10/31/%E8%87%AA%E5%AE%9A%E4%B9%89LayoutManager%E4%BB%8E0%E5%88%B01%E5%AE%9E%E8%B7%B5/</id>
    <published>2023-10-31T11:19:00.000Z</published>
    <updated>2025-05-20T11:46:57.000Z</updated>
    
    <content type="html"><![CDATA[<p>此前大部分涉及到 RecyclerView 页面的 LayoutManager基本上用系统提供的 LinearLayoutManager 、GridLayoutManager 就能解决，但在一些特殊场景上还是需要我们自定义  LayoutManager。之前基本上没有自己写过，在网上看各种源码各种文章，刚开始花了好多时间去理解整体流程，因为它们都给我一种非常非常复杂的感觉，包括相关的博客文章也是。经过一段时间摸索，也慢慢能理解为什么要那么复杂了，这的确不是特别容易入门。所以对整体的流程进行了一个拆解，尽量原子化一点，对自己学习的一个总结，也希望能帮助到一部分人能对  LayoutManager 入门。</p><p>本文最终实现一个简单的 LinearLayoutManager（只支持 VERTICAL）方向，适合对 LayoutManager 整体流程的学习与理解，整体代码分为多个文件，每个文件都是对前一段代码的补充，方便理解，整体项目源码已提交 Github: <a href="https://github.com/VomPom/LayoutManagerGradually">LayoutManagerGradually</a>，代码里面写了很多很多注释，如果不想浪费时间，可以直接看代码运行，跳过这篇文章，把每一个 LayoutManager 都跑一下体验结合代码看看。</p><h2 id="自定义-LayoutManager-的必要元素"><a href="#自定义-LayoutManager-的必要元素" class="headerlink" title="自定义 LayoutManager 的必要元素"></a>自定义 LayoutManager 的必要元素</h2><ul><li><p>继承 <code>RecyclerView.LayoutManager</code> 并实现 <code>generateDefaultLayoutParams()</code>方法</p></li><li><p>重写<code>onLayoutChildren</code> 第一次数据填充的时候数据添加</p></li><li><p>重写 <code>canScrollHorizontally()</code> 和<code>canScrollVertically()</code>方法设定支持滑动的方向</p></li><li><p>重写 <code>scrollHorizontallyBy()</code>和<code>scrollVerticallyBy()</code>方法，在滑动的时候对屏幕以外的 View 进行回收，以及填充即将滑动进入屏幕范围内的 View 进行填充</p></li><li><p>重写 <code>scrollToPosition()</code>和<code>smoothScrollToPosition()</code>方法支持</p></li></ul><p>其中<code>onLayoutChildren</code> 和 <code>scrollHorizontallyBy/scrollVerticallyBy</code> 是最核心且最复杂的方法，这里稍微拎出来讲一下</p><h3 id="onLayoutChildren"><a href="#onLayoutChildren" class="headerlink" title="onLayoutChildren"></a>onLayoutChildren</h3><p>这个方法类似于自定义 ViewGroup 的 onLayout() 方法，RecyclerView 的 LayoutManager.onLayoutChildren 在以下几个时机会被触发：</p><ul><li>当 <code>RecyclerView</code> 首次附加到窗口时</li><li>当<code>Adapter</code>  的数据集发生变化</li><li>当 <code>RecyclerView</code> 被 执行 <code>RequetLayout</code>的时候</li><li>当 <code>LayoutManager</code> 发生变化时</li></ul><h3 id="scrollHorizontallyBy-scrollVerticallyBy"><a href="#scrollHorizontallyBy-scrollVerticallyBy" class="headerlink" title="scrollHorizontallyBy/scrollVerticallyBy"></a>scrollHorizontallyBy/scrollVerticallyBy</h3><p>方法的主要作用包括：</p><ol><li><p>更新 ItemView 的位置：根据传入的垂直滚动距离（dy 参数），更新子视图在屏幕上的位置。通常调用 <code>offsetChildrenVertical</code> 方法。</p></li><li><p>回收不可见的 ItemView：在滚动过程中，一些 ItemView 可能会离开屏幕，变得不可见。<code>scrollVerticallyBy</code> 方法需要负责回收这些子视图并将它们放入回收池，以便稍后复用。</p></li><li><p>添加新的 ItemView：在滚动过程中，新的 ItemView 可能需要显示在屏幕上。<code>scrollVerticallyBy</code> 方法需要从回收池中获取可复用的视图并将它们添加到屏幕上。这通常涉及到调用 <code>RecyclerView.Recycler</code> 的 <code>getViewForPosition</code> 方法。</p></li><li><p>返回实际滚动距离：由于 ItemView 的数量有限，滚动可能会受到限制。例如，当滚动到列表顶部或底部时，滚动可能会停止。在这种情况下，实际滚动的距离可能会小于传入的 <code>dy</code> 参数。<code>scrollVerticallyBy</code> 方法需要返回实际滚动的距离，以便 <code>RecyclerView</code> 可以正确地更新滚动条和触发滚动事件。</p></li></ol><p>概念就简单讲这么多， talk is cheap show me the code，直接看代码理解会比较深刻</p><h2 id="逐步实现"><a href="#逐步实现" class="headerlink" title="逐步实现"></a>逐步实现</h2><p>要实现一个可用的 LayoutManger 通常我们需要实现以下流程</p><ul><li>数据填充并且只需要填充屏幕范围内的 ItemView</li><li>回收掉屏幕以外的 ItemView</li><li>屏幕外 ItemView 再回到屏幕后，需要重新填充</li><li>对滑动边界边界进行处理</li><li>对 scrollToPosition 和 smoothScrollToPosition进行支持</li></ul><p>我们不用一上来就实现最终的效果，而是一步一步来，看看 LayoutManger 是怎么渐渐地变化，最终能跑起来的。</p><h3 id="0-最简单的-LayoutManager"><a href="#0-最简单的-LayoutManager" class="headerlink" title="0 最简单的 LayoutManager"></a>0 最简单的 LayoutManager</h3><p>代码查看：<a href="https://github.com/VomPom/LayoutManagerGradually/tree/main/layoutmanager/src/main/java/com/julis/layoutmanager/series/MostSimpleLayoutManager.kt">MostSimpleLayoutManager</a>，我们关注 <code>onLayoutChildren</code> 方法:</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onLayoutChildren</span><span class="params">(recycler: <span class="type">RecyclerView</span>.<span class="type">Recycler</span>, state: <span class="type">RecyclerView</span>.<span class="type">State</span>?)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 垂直方向的偏移量</span></span><br><span class="line">    <span class="keyword">var</span> offsetTop = <span class="number">0</span></span><br><span class="line">    <span class="comment">// 实际业务中最好不要这样一次性加载所有的数据，这里只是最简单地演示一下整体是如何工作的</span></span><br><span class="line">    <span class="keyword">for</span> (itemIndex <span class="keyword">in</span> <span class="number">0</span> until itemCount) &#123;</span><br><span class="line">        <span class="comment">// 从适配器获取与给定位置关联的视图</span></span><br><span class="line">        <span class="keyword">val</span> itemView = recycler.getViewForPosition(itemIndex)</span><br><span class="line">        <span class="comment">// 将视图添加到 RecyclerView 中</span></span><br><span class="line">        addView(itemView)</span><br><span class="line">        <span class="comment">// 测量并布局视图</span></span><br><span class="line">        measureChildWithMargins(itemView, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line">        <span class="comment">// 拿到宽高（包括ItemDecoration）</span></span><br><span class="line">        <span class="keyword">val</span> width = getDecoratedMeasuredWidth(itemView)</span><br><span class="line">        <span class="keyword">val</span> height = getDecoratedMeasuredHeight(itemView)</span><br><span class="line">        <span class="comment">// 对要添加的子 View 进行布局</span></span><br><span class="line">        layoutDecorated(itemView, <span class="number">0</span>, offsetTop, width, offsetTop + height)</span><br><span class="line">        offsetTop += height</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码主要演示了，如何利用<code>addView</code> <code>layoutDecorated</code>等方法，将 ItemView 添加到 RecyclerView 上。代码可见是 将所有的 ItemView（即使它在屏幕上不可见）一次性全部加载到了 RecyclerView上， 这里一般不这么做，只是这里这里只是最简单地演示一下整体是如何工作的。</p><p>运行在手机上能看到这样的效果：Item数据已经被全部添加到界面上了，并且各个方向的滑动都支持。</p><img src="https://cdn.julis.wang/blog/img/layoutmanager_gradually_0.gif?imageView2/2/w/300"><h3 id="1-更合理的数据添加方式"><a href="#1-更合理的数据添加方式" class="headerlink" title="1 更合理的数据添加方式"></a>1 更合理的数据添加方式</h3><p>代码查看：<a href="https://github.com/VomPom/LayoutManagerGradually/tree/main/layoutmanager/src/main/java/com/julis/layoutmanager/series/LinearLayoutManager1">LinearLayoutManager1.kt</a></p><p>对最开始的代码进行优化，只在屏幕范围内的区域进行数据的添加，这样就不需要一次性将所有数据就添加上去，如果 Adapter 的 ItemCount 足够巨大，for all addView 的话，很容易就 OOM。</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onLayoutChildren</span><span class="params">(recycler: <span class="type">RecyclerView</span>.<span class="type">Recycler</span>, state: <span class="type">RecyclerView</span>.<span class="type">State</span>)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 垂直方向上的的空间大小</span></span><br><span class="line">    <span class="keyword">var</span> remainSpace = height - paddingTop</span><br><span class="line">    <span class="comment">//垂直方向的偏移量</span></span><br><span class="line">    <span class="keyword">var</span> offsetTop = <span class="number">0</span></span><br><span class="line">    <span class="keyword">var</span> currentPosition = <span class="number">0</span></span><br><span class="line">    <span class="keyword">while</span> (remainSpace &gt; <span class="number">0</span> &amp;&amp; currentPosition &lt; state.itemCount) &#123;</span><br><span class="line">        <span class="comment">// 从适配器获取与给定位置关联的视图</span></span><br><span class="line">        <span class="keyword">val</span> itemView = recycler.getViewForPosition(currentPosition)</span><br><span class="line">        <span class="comment">// 将视图添加到 RecyclerView 中</span></span><br><span class="line">        addView(itemView)</span><br><span class="line">        <span class="comment">// 测量并布局视图</span></span><br><span class="line">        measureChildWithMargins(itemView, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line">        <span class="comment">// 拿到宽高（包括ItemDecoration）</span></span><br><span class="line">        <span class="keyword">val</span> itemWidth = getDecoratedMeasuredWidth(itemView)</span><br><span class="line">        <span class="keyword">val</span> itemHeight = getDecoratedMeasuredHeight(itemView)</span><br><span class="line">        <span class="comment">// 对要添加的子 View 进行布局</span></span><br><span class="line">        layoutDecorated(itemView, <span class="number">0</span>, offsetTop, itemWidth, offsetTop + itemHeight)</span><br><span class="line">        offsetTop += itemHeight</span><br><span class="line">        currentPosition++</span><br><span class="line">        <span class="comment">// 可用空间减少</span></span><br><span class="line">        remainSpace -= itemHeight</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-对屏幕外的View回收"><a href="#2-对屏幕外的View回收" class="headerlink" title="2 对屏幕外的View回收"></a>2 对屏幕外的View回收</h3><p>代码查看：<a href="https://github.com/VomPom/LayoutManagerGradually/tree/main/layoutmanager/src/main/java/com/julis/layoutmanager/series/LinearLayoutManager2.kt">LinearLayoutManager2</a></p><p>RecylerView 没有 recycler 怎么行呢？当 RecylerView 的 ItemView 滑出屏幕后我们需要对齐进行回收，实现的话需要在 <code>scrollVerticallyBy</code>中，比较复杂的逻辑就是怎么去判断：ItemView 在屏幕以外，最后利用：<code>removeAndRecycleView</code>方法进行回收</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">scrollVerticallyBy</span><span class="params">(dy: <span class="type">Int</span>, recycler: <span class="type">RecyclerView</span>.<span class="type">Recycler</span>, state: <span class="type">RecyclerView</span>.<span class="type">State</span>?)</span></span>: <span class="built_in">Int</span> &#123;</span><br><span class="line">      <span class="comment">// 在这里处理上下的滚动逻辑，dy 表示滚动的距离</span></span><br><span class="line">      <span class="comment">// 平移所有子视图</span></span><br><span class="line">      offsetChildrenVertical(-dy)</span><br><span class="line">      <span class="comment">// 如果实际滚动距离与 dy 相同，返回 dy；如果未滚动，返回 0</span></span><br><span class="line">      recycleInvisibleView(dy, recycler)</span><br><span class="line">      <span class="keyword">return</span> dy</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 回收掉在界面上看不到的 ItemView</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> dy</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> recycler</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">recycleInvisibleView</span><span class="params">(dy: <span class="type">Int</span>, recycler: <span class="type">RecyclerView</span>.<span class="type">Recycler</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">val</span> totalSpace = orientationHelper.totalSpace</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 将要回收View的集合</span></span><br><span class="line">    <span class="keyword">val</span> recycleViews = hashSetOf&lt;View&gt;()</span><br><span class="line">    <span class="comment">// 从下往上滑</span></span><br><span class="line">    <span class="keyword">if</span> (dy &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">for</span> (i <span class="keyword">in</span> <span class="number">0</span> until childCount) &#123;</span><br><span class="line">            <span class="keyword">val</span> child = getChildAt(i)!!</span><br><span class="line">            <span class="comment">// 从下往上滑从最上面的 item 开始计算</span></span><br><span class="line">            <span class="keyword">val</span> top = getDecoratedTop(child)</span><br><span class="line">            <span class="comment">// 判断最顶部的 item 是否已经完全不可见，如何可见，那说明底下的 item 也是可见</span></span><br><span class="line">            <span class="keyword">val</span> height = top - getDecoratedBottom(child)</span><br><span class="line">            <span class="keyword">if</span> (height - top &lt; <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="keyword">break</span></span><br><span class="line">            &#125;</span><br><span class="line">            recycleViews.add(child)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (dy &lt; <span class="number">0</span>) &#123;   <span class="comment">// 从上往下滑</span></span><br><span class="line">        <span class="keyword">for</span> (i <span class="keyword">in</span> childCount - <span class="number">1</span> downTo <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">val</span> child = getChildAt(i)!!</span><br><span class="line">            <span class="comment">// 从上往下滑从最底部的 item 开始计算</span></span><br><span class="line">            <span class="keyword">val</span> bottom = getDecoratedBottom(child)</span><br><span class="line">            <span class="comment">// 判断最底部的 item 是否已经完全不可见，如何可见，那说明上面的 item 也是可见</span></span><br><span class="line">            <span class="keyword">val</span> height = bottom - getDecoratedTop(child)</span><br><span class="line">            <span class="keyword">if</span> (bottom - totalSpace &lt; height) &#123;</span><br><span class="line">                <span class="keyword">break</span></span><br><span class="line">            &#125;</span><br><span class="line">            recycleViews.add(child)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 真正把 View 移除掉的逻辑</span></span><br><span class="line">    <span class="keyword">for</span> (view <span class="keyword">in</span> recycleViews) &#123;</span><br><span class="line">        <span class="comment">// [removeAndRecycleView]</span></span><br><span class="line">        <span class="comment">// 用于从视图层次结构中删除某个视图，并将其资源回收，以便在需要时重新利用</span></span><br><span class="line">        removeAndRecycleView(view, recycler)</span><br><span class="line">    &#125;</span><br><span class="line">    recycleViews.clear()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行在手机上能看到这样的效果：滑出屏幕外的ItemView 被回收掉了</p><img src="https://cdn.julis.wang/blog/img/layoutmanager_gradually_2.gif?imageView2/2/w/300"><h3 id="3-向上滑动的时View的填充"><a href="#3-向上滑动的时View的填充" class="headerlink" title="3 向上滑动的时View的填充"></a>3 向上滑动的时View的填充</h3><p>代码查看：<a href="https://github.com/VomPom/LayoutManagerGradually/tree/main/layoutmanager/src/main/java/com/julis/layoutmanager/series/LinearLayoutManager3.kt">LinearLayoutManager3</a></p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">scrollVerticallyBy</span><span class="params">(dy: <span class="type">Int</span>, recycler: <span class="type">RecyclerView</span>.<span class="type">Recycler</span>, state: <span class="type">RecyclerView</span>.<span class="type">State</span>?)</span></span>: <span class="built_in">Int</span> &#123;</span><br><span class="line">    <span class="comment">// 填充 view</span></span><br><span class="line">    fillView(dy, recycler)</span><br><span class="line">    <span class="comment">// 移动 view</span></span><br><span class="line">    offsetChildrenVertical(-dy)</span><br><span class="line">    <span class="comment">// 回收 View</span></span><br><span class="line">    recycleInvisibleView(dy, recycler)</span><br><span class="line">    <span class="keyword">return</span> dy</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 填充重新进入屏幕内的 ItemView</span></span><br><span class="line"><span class="comment"> *     getChildCount():childCount-&gt; 当前屏幕内RecyclerView展示的 ItemView 数量</span></span><br><span class="line"><span class="comment"> *     getItemCount():itemCount-&gt; 最大的 ItemView 数量，也就是 Adapter 传递的数据的数量</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">fillView</span><span class="params">(dy: <span class="type">Int</span>, recycler: <span class="type">RecyclerView</span>.<span class="type">Recycler</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">val</span> verticalSpace = orientationVerticalHelper.totalSpace</span><br><span class="line">    <span class="keyword">var</span> remainSpace = <span class="number">0</span></span><br><span class="line">    <span class="keyword">var</span> nextFillPosition = <span class="number">0</span></span><br><span class="line">    <span class="comment">//垂直方向的偏移量</span></span><br><span class="line">    <span class="keyword">var</span> offsetTop = <span class="number">0</span></span><br><span class="line">    <span class="keyword">var</span> offsetLeft = <span class="number">0</span></span><br><span class="line">    <span class="comment">// 从下往上滑，那么需要向底部添加数据</span></span><br><span class="line">    <span class="keyword">if</span> (dy &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">val</span> anchorView = getChildAt(childCount - <span class="number">1</span>) ?: <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">val</span> anchorPosition = getPosition(anchorView)</span><br><span class="line">        <span class="keyword">val</span> anchorBottom = getDecoratedBottom(anchorView)</span><br><span class="line">        <span class="keyword">val</span> anchorLeft = getDecoratedLeft(anchorView)</span><br><span class="line">        remainSpace = verticalSpace - anchorBottom</span><br><span class="line">        <span class="comment">// 垂直可用的数据为&lt;0，意外着这时候屏幕底部的位置刚好在最底部的 ItemView 上，还需要向上滑动一点点...我们才能添加 View</span></span><br><span class="line">        <span class="keyword">if</span> (remainSpace &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        &#125;</span><br><span class="line">        nextFillPosition = anchorPosition + <span class="number">1</span></span><br><span class="line">        offsetTop = anchorBottom</span><br><span class="line">        offsetLeft = anchorLeft</span><br><span class="line">        <span class="keyword">if</span> (nextFillPosition &gt;= itemCount) &#123;</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (dy &lt; <span class="number">0</span>) &#123;  <span class="comment">// 从上往下滑，那么需要向顶部添加数据</span></span><br><span class="line">        <span class="comment">//no-op 暂时不实现从上往下滑的底部数据填充</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (remainSpace &gt; <span class="number">0</span> &amp;&amp; nextFillPosition &lt; itemCount) &#123;</span><br><span class="line">        <span class="comment">// 从适配器获取与给定位置关联的视图</span></span><br><span class="line">        <span class="keyword">val</span> itemView = recycler.getViewForPosition(nextFillPosition)</span><br><span class="line">        <span class="comment">// 将视图添加到 RecyclerView 中</span></span><br><span class="line">        addView(itemView)</span><br><span class="line">        <span class="comment">// 测量并布局视图</span></span><br><span class="line">        measureChildWithMargins(itemView, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line">        <span class="comment">// 拿到宽高（包括ItemDecoration）</span></span><br><span class="line">        <span class="keyword">val</span> itemWidth = getDecoratedMeasuredWidth(itemView)</span><br><span class="line">        <span class="keyword">val</span> itemHeight = getDecoratedMeasuredHeight(itemView)</span><br><span class="line">        <span class="comment">// 对要添加的子 View 进行布局，相比onLayoutChildren 里面的实现添加了：offsetLeft（因为我们没有禁止掉 左右的滑动）</span></span><br><span class="line">        <span class="comment">// 试着把 offsetLeft 改成0，也就是最原始的样子，然后左右上下滑滑，你会有意外收获</span></span><br><span class="line">        layoutDecorated(itemView, offsetLeft, offsetTop, itemWidth + offsetLeft, offsetTop + itemHeight)</span><br><span class="line">        offsetTop += itemHeight</span><br><span class="line">        nextFillPosition++</span><br><span class="line">        <span class="comment">// 可用空间减少</span></span><br><span class="line">        remainSpace -= itemHeight</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行在手机上能看到这样的效果：向上滑动的时候，底部陆续有元素填充，但向下滑动的时候没有填充数据</p><img src="https://cdn.julis.wang/blog/img/layoutmanager_gradually_3.gif?imageView2/2/w/300"><h3 id="4-两个方向的View填充"><a href="#4-两个方向的View填充" class="headerlink" title="4 两个方向的View填充"></a>4 两个方向的View填充</h3><p>代码查看：<a href="https://github.com/VomPom/LayoutManagerGradually/tree/main/layoutmanager/src/main/java/com/julis/layoutmanager/series/LinearLayoutManager4.kt">LinearLayoutManager4</a></p><p>补齐从上往下滑之后添加的逻辑</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">fillView</span><span class="params">(dy: <span class="type">Int</span>, recycler: <span class="type">RecyclerView</span>.<span class="type">Recycler</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">val</span> verticalSpace = orientationVerticalHelper.totalSpace</span><br><span class="line">    <span class="keyword">var</span> remainSpace = <span class="number">0</span></span><br><span class="line">    <span class="keyword">var</span> nextFillPosition = <span class="number">0</span></span><br><span class="line">    <span class="comment">//垂直方向的偏移量</span></span><br><span class="line">    <span class="keyword">var</span> offsetTop = <span class="number">0</span></span><br><span class="line">    <span class="keyword">var</span> offsetLeft = <span class="number">0</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 从下往上滑，那么需要向底部添加数据</span></span><br><span class="line">    <span class="keyword">if</span> (dy &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        ……</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (dy &lt; <span class="number">0</span>) &#123;  <span class="comment">// 从上往下滑，那么需要向顶部添加数据</span></span><br><span class="line">        <span class="keyword">val</span> anchorView = getChildAt(<span class="number">0</span>) ?: <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">val</span> anchorPosition = getPosition(anchorView)</span><br><span class="line">        <span class="keyword">val</span> anchorTop = getDecoratedTop(anchorView)</span><br><span class="line">        offsetLeft = getDecoratedLeft(anchorView)</span><br><span class="line">        remainSpace = anchorTop</span><br><span class="line">        <span class="comment">// 垂直可用的数据为&lt;0，意外着这时候屏幕顶部的位置刚好在最底部的 ItemView 上，还需要向下滑动一点点...我们才能添加 View</span></span><br><span class="line">        <span class="keyword">if</span> (anchorTop &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        &#125;</span><br><span class="line">        nextFillPosition = anchorPosition - <span class="number">1</span></span><br><span class="line">        <span class="keyword">if</span> (nextFillPosition &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">val</span> itemHeight = getDecoratedMeasuredHeight(anchorView)</span><br><span class="line">        <span class="comment">// 新的布局的itemView 的顶部位置应该以 anchorTop - itemHeight 开始</span></span><br><span class="line">        offsetTop = anchorTop - itemHeight</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (remainSpace &gt; <span class="number">0</span> &amp;&amp;</span><br><span class="line">        ((nextFillPosition &lt; itemCount) &amp;&amp; (nextFillPosition &gt;= <span class="number">0</span>))</span><br><span class="line">    ) &#123;</span><br><span class="line">        <span class="comment">// 从适配器获取与给定位置关联的视图</span></span><br><span class="line">        <span class="keyword">val</span> itemView = recycler.getViewForPosition(nextFillPosition)</span><br><span class="line">        <span class="comment">// 将视图添加到 RecyclerView 中k，从顶部添加的话，需要加到最前的位置</span></span><br><span class="line">        <span class="keyword">if</span> (dy &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            addView(itemView)</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            addView(itemView, <span class="number">0</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        ……</span><br><span class="line">        <span class="keyword">if</span> (dy &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            offsetTop += itemHeight</span><br><span class="line">            nextFillPosition++</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            offsetTop -= itemHeight</span><br><span class="line">            nextFillPosition--</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 可用空间减少</span></span><br><span class="line">        remainSpace -= itemHeight</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>运行在手机上能看到这样的效果：向上或者滑动的时候，底部陆续都有元素填充</p><img src="https://cdn.julis.wang/blog/img/layoutmanager_gradually_4.gif?imageView2/2/w/300"><h3 id="5-对顶部和底部滑动边界处理"><a href="#5-对顶部和底部滑动边界处理" class="headerlink" title="5 对顶部和底部滑动边界处理"></a>5 对顶部和底部滑动边界处理</h3><p>代码查看：<a href="https://github.com/VomPom/LayoutManagerGradually/tree/main/layoutmanager/src/main/java/com/julis/layoutmanager/series/LinearLayoutManager5.kt">LinearLayoutManager5</a></p><p>对于前面的实现会发现会：不停地下滑或者上滑会留出来巨大的空白。这里对填充 View 的逻辑进行改造，需要进行边界检测。</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">scrollVerticallyBy</span><span class="params">(dy: <span class="type">Int</span>, recycler: <span class="type">RecyclerView</span>.<span class="type">Recycler</span>, state: <span class="type">RecyclerView</span>.<span class="type">State</span>?)</span></span>: <span class="built_in">Int</span> &#123;</span><br><span class="line">    <span class="comment">// 填充 view</span></span><br><span class="line">    <span class="keyword">val</span> adjustedDy = fillView(dy, recycler)</span><br><span class="line">    <span class="comment">// 移动 view</span></span><br><span class="line">    offsetChildrenVertical(-adjustedDy)</span><br><span class="line">    <span class="comment">// 回收 View</span></span><br><span class="line">    recycleInvisibleView(adjustedDy, recycler)</span><br><span class="line">    <span class="comment">// 由于需要对边界进行限制，所以需要对原始的 dy 进行修正，这里不再直接返回 dy</span></span><br><span class="line">    <span class="keyword">return</span> adjustedDy</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这里的整体注释我写在了代码里面，可以看图稍微理解一下，以向上滑动为例：假设这一次滑动的距离非常非常大(想象成10000像素)，如果直接滑动的话，我们有50个元素，每个元素高度100像素，最大高度也只有50x100=5000，那么滑动后一定会留下大量空区域。需要对当前传入的这 10000 像素做调整：只给到可滑动的最大距离，如果不能滑动了就返回0。</p><img src="https://cdn.julis.wang/blog/img/5_scroll_limit.png"><p>运行在手机上能看到这样的效果：向上或者滑动的时候，达到最大的位置时候是不能再滑动的。</p><img src="https://cdn.julis.wang/blog/img/layoutmanager_gradually_5.gif?imageView2/2/w/300"><h3 id="6-实现-scrollToPosition"><a href="#6-实现-scrollToPosition" class="headerlink" title="6 实现 scrollToPosition"></a>6 实现 scrollToPosition</h3><p>代码查看：<a href="https://github.com/VomPom/LayoutManagerGradually/tree/main/layoutmanager/src/main/java/com/julis/layoutmanager/series/LinearLayoutManager6.kt">LinearLayoutManager6</a></p><p>到这里这个 LinearLayoutManager 看着已经能正常运行了，但一般还需要支持<code>scrollToPosition</code> 和 <code>smoothScrollToPositio</code></p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">var</span> mPendingScrollPosition = RecyclerView.NO_POSITION</span><br><span class="line"></span><br><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">scrollToPosition</span><span class="params">(position: <span class="type">Int</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">super</span>.scrollToPosition(position)</span><br><span class="line">    <span class="keyword">if</span> (position &lt; <span class="number">0</span> || position &gt;= itemCount) &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    mPendingScrollPosition = position</span><br><span class="line">    requestLayout()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onLayoutChildren</span><span class="params">(recycler: <span class="type">RecyclerView</span>.<span class="type">Recycler</span>, state: <span class="type">RecyclerView</span>.<span class="type">State</span>)</span></span> &#123;</span><br><span class="line">    ……</span><br><span class="line">    <span class="keyword">var</span> currentPosition = <span class="number">0</span></span><br><span class="line">    <span class="keyword">if</span> (mPendingScrollPosition != RecyclerView.NO_POSITION) &#123;</span><br><span class="line">        currentPosition = mPendingScrollPosition</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (remainSpace &gt; <span class="number">0</span> &amp;&amp; currentPosition &lt; state.itemCount) &#123;</span><br><span class="line">      …… <span class="comment">// 填充View 的逻辑</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>scrollToPosition</code> 的实现比较简单，如上代码所示：在 <code>scrollToPosition</code>  的时候记录一次目标position，再 requestLayout 一波，还记得之前有提到过：<code>onLayoutChildren</code> 会在 <code>requestLayout</code> 的时候调用一次，于是再将<code>onLayoutChildren</code>逻辑改写，不再从第0个元素开始，而是从目标位置进行布局。</p><p>运行在手机上能看到这样的效果：点击 scrollTo30 将会滑动到 第30个位置。</p><img src="https://cdn.julis.wang/blog/img/layoutmanager_gradually_6.gif?imageView2/2/w/300"><h3 id="7-实现-smoothScrollToPosition"><a href="#7-实现-smoothScrollToPosition" class="headerlink" title="7 实现 smoothScrollToPosition"></a>7 实现 smoothScrollToPosition</h3><p>代码查看：<a href="https://github.com/VomPom/LayoutManagerGradually/tree/main/layoutmanager/src/main/java/com/julis/layoutmanager/series/LinearLayoutManager7.kt">LinearLayoutManager7</a>  </p><p>要实现自定义的 smoothScrollToPosition 动画效果，这一块如果要完全自己实现的话比较复杂，可以直接使用系统提供的 LinearSmoothScroller改造,也可以继承 RecyclerView.SmoothScroller 自定义，也可以完全不使用 SmoothScroller， 照着 SmoothScroller 的实现使用类似 ValueAnimator 自定义动画，添加动画 UpdateListener，在 onAnimationUpdate 的时候动态计算布局从而实现滑动动画,这里拿 LinearSmoothScroller 举例:</p><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">smoothScrollToPosition</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">    recyclerView: <span class="type">RecyclerView</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">    state: <span class="type">RecyclerView</span>.<span class="type">State</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">    position: <span class="type">Int</span></span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (position &gt;= itemCount || position &lt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">val</span> scroller: LinearSmoothScroller = <span class="keyword">object</span> : LinearSmoothScroller(recyclerView.context) &#123;</span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * 这个方法用于计算滚动到目标位置所需的滚动向量。滚动向量是一个二维向量，包含水平和垂直方向上的滚动距离</span></span><br><span class="line"><span class="comment">         *</span></span><br><span class="line"><span class="comment">         * <span class="doctag">@param</span> targetPosition 滑动的目标位置</span></span><br><span class="line"><span class="comment">         * <span class="doctag">@return</span>  返回一个 PointF 对象，表示滚动向量。</span></span><br><span class="line"><span class="comment">         *              PointF.x 表示水平方向上的滚动距离，</span></span><br><span class="line"><span class="comment">         *              PointF.y 表示垂直方向上的滚动距离</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">computeScrollVectorForPosition</span><span class="params">(targetPosition: <span class="type">Int</span>)</span></span>: PointF &#123;</span><br><span class="line">            <span class="comment">// 查找到屏幕里显示的第 1 个元素与</span></span><br><span class="line">            <span class="keyword">val</span> firstChildPos = getPosition(getChildAt(<span class="number">0</span>)!!)</span><br><span class="line">            <span class="keyword">val</span> direction = <span class="keyword">if</span> (targetPosition &lt; firstChildPos) -<span class="number">1</span> <span class="keyword">else</span> <span class="number">1</span></span><br><span class="line">            <span class="comment">// x 左右滑动，由于我们只实现了垂直的滑动，所以 x方向为0即可</span></span><br><span class="line">            <span class="comment">// 整数代表正向移动，负数代表反向移动，这里的数值大小不重要，源码里面最终都会 normalize 归一化处理</span></span><br><span class="line">            <span class="keyword">return</span> PointF(<span class="number">0f</span>, direction.toFloat())</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * 计算每像素速度</span></span><br><span class="line"><span class="comment">         *</span></span><br><span class="line"><span class="comment">         * <span class="doctag">@param</span> displayMetrics</span></span><br><span class="line"><span class="comment">         * <span class="doctag">@return</span> 返回每一像素的耗时，单位ms，假设返回值是1.0 代表着：1ms 内会滑动 1像素，1s会滑动1000像素</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">calculateSpeedPerPixel</span><span class="params">(displayMetrics: <span class="type">DisplayMetrics</span>?)</span></span>: <span class="built_in">Float</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">super</span>.calculateSpeedPerPixel(displayMetrics)</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * 滑动速度的插值（实现滑动速度随着滑动时间的变化）</span></span><br><span class="line"><span class="comment">         *</span></span><br><span class="line"><span class="comment">         * <span class="doctag">@param</span> dx</span></span><br><span class="line"><span class="comment">         * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">calculateTimeForDeceleration</span><span class="params">(dx: <span class="type">Int</span>)</span></span>: <span class="built_in">Int</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">super</span>.calculateTimeForDeceleration(dx)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 很多方法可以使用，不再一一列举</span></span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">    scroller.targetPosition = position</span><br><span class="line">    <span class="comment">// 执行默认动画的逻辑</span></span><br><span class="line">    startSmoothScroll(scroller)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行在手机上能看到这样的效果：点击 smoothScrollTo30 将会有个动画效果滑动到第30个位置。</p><img src="https://cdn.julis.wang/blog/img/layoutmanager_gradually_7.gif?imageView2/2/w/300"><p>以上基本上一个自定义 LayoutManager 的雏形就已经完成了，虽然只实现了一个方向的滑动，但是其原理都是一样的，剩下的就是各种细节的打磨了，可以加各种自己想要的效果，比如：指定位置 放大一定的系数，或者更炫酷的滑动动画…</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文主要整理了自定义 LayoutManager 的必要元素，以及其核心方法 scrollHorizontallyBy/scrollVerticallyBy、onLayoutChildren 的作用与调用时机，接下对实现一个简单的 LinearLayoutManger 进行逻辑拆解，从最简单的不滑动回收和填充以及不含滑动边界检测，到最终一个具备基本功能的 LayoutManger</p><p>源码：<a href="https://github.com/VomPom/LayoutManagerGradually">https://github.com/VomPom/LayoutManagerGradually</a></p><p>参考：</p><p><a href="https://juejin.cn/post/6870770285247725581?searchId=202310181005138A6D82B1DEE9C47A9797#heading-23">《看完这篇文章你还不会自定义LayoutManager，我吃X！》</a></p><p><a href="https://github.com/MycroftWong/FlowLayoutManager/blob/master/LayoutManager%E5%88%86%E6%9E%90%E4%B8%8E%E5%AE%9E%E8%B7%B5.md">《/LayoutManager分析与实践》</a></p><p><a href="https://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/">Building a RecyclerView LayoutManager – Part 1</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;此前大部分涉及到 RecyclerView 页面的 LayoutManager基本上用系统提供的 LinearLayoutManager 、GridLayoutManager 就能解决，但在一些特殊场景上还是需要我们自定义  LayoutManager。之前基本上没有自己写</summary>
      
    
    
    
    
    <category term="技术文章" scheme="http://julis.wang/tags/technology/"/>
    
  </entry>
  
  <entry>
    <title>Android 基于 J2V8 运行 JavasScript  实践</title>
    <link href="http://julis.wang/2023/09/30/Android-J2V8-%E5%AE%9E%E8%B7%B5/"/>
    <id>http://julis.wang/2023/09/30/Android-J2V8-%E5%AE%9E%E8%B7%B5/</id>
    <published>2023-09-30T13:11:00.000Z</published>
    <updated>2025-05-20T11:46:57.000Z</updated>
    
    <content type="html"><![CDATA[<p>V8 引擎是由 Google 开源的 JavaScript 引擎，Chrome 就是基于 V8 开发，V8 是跨平台的，J2V8 基于 V8 进行开发，使得 js 代码能够在 Android 平台上脱离 WebView 运行。目前，也有很多关于 Android J2V8 的文章，不过讲解不是特别细（可能也是我太菜了，看完了之后，依然遇到很多问题），自己在调研的过程中遇到很多坑，所以这里记录一下，本文主要记录整个 J2V8 框架的使用方法，以及一些坑。</p><h2 id="一、Webpack-打包"><a href="#一、Webpack-打包" class="headerlink" title="一、Webpack 打包"></a>一、Webpack 打包</h2><p>通常业务逻辑的 js 文件是有多个的，我们需要借助一些打包工具将多个文件打包成一个 js 文件供 J2V8 使用，我们可以使用 Gulp、Webpack、Browserify，本文主要讲 Webpack 的使用。<br>主要流程如下：</p><p><strong>编写基础逻辑并通过 <code>module.exports</code> 对外部提供</strong></p><p><strong>编写 <code>index.js</code> 入口文件</strong><br><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  simpleFunc, complexFunc</span><br><span class="line">&#125;;</span><br><span class="line"></span><br></pre></td></tr></table></figure><br><strong> 编写<code>webpack.config</code>打包配置</strong><br><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">entry</span>: <span class="string">&#x27;./src/example/index.js&#x27;</span>,</span><br><span class="line">  <span class="attr">output</span>: &#123;</span><br><span class="line">    <span class="attr">library</span>: <span class="string">&#x27;libExample&#x27;</span>,                 <span class="comment">// j2v8 加载该lib</span></span><br><span class="line">    <span class="attr">path</span>: path.<span class="title function_">resolve</span>(__dirname, <span class="string">&#x27;dist&#x27;</span>),</span><br><span class="line">    <span class="attr">filename</span>: <span class="string">&#x27;example.js&#x27;</span>,                <span class="comment">// 导出指定命名的 js 文件 </span></span><br><span class="line">  &#125;,</span><br><span class="line">  ...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></p><p><strong>执行 <code>webpack</code> 打包命令</strong><br><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">./node_modules/.bin/webpack --config webpack.config.js</span><br></pre></td></tr></table></figure></p><h2 id="二、运行-JavaScript"><a href="#二、运行-JavaScript" class="headerlink" title="二、运行 JavaScript"></a>二、运行 JavaScript</h2><p>到这里我们已经有一份通过 Webpack 打包好的 js 文件了，要在 j2v8 中运行 JavaScript 文件，使用以下步骤：</p><p><strong>1、创建一个 V8 实例</strong><br><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">V8 v8 = V8.createV8Runtime();</span><br></pre></td></tr></table></figure><br><strong>2、读取 JavaScript 文件</strong><br><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> scriptStr = String(Files.readAllBytes(Paths.<span class="keyword">get</span>(<span class="string">&quot;example.js&quot;</span>)))</span><br></pre></td></tr></table></figure><br><strong>3、在 V8 实例中执行 JavaScript 代码</strong><br><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line">v8.executeScript(scriptStr);</span><br></pre></td></tr></table></figure><br>这一步已经让整个 js 文件运行起来，但我们还不能调用我们的方法</p><p><strong>4、读取指定模块</strong></p><p>由于是通过 Webpack 打包，在 Webpack 的 <code>output.library</code> 配置，选项用于将打包后的代码作为一个库(library)暴露出去，以便其他应用程序或模块可以使用它。<br><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> rootLib =v8.getObject(libName); <span class="comment">// 这里的 libName 就是 output.library 配置的名字</span></span><br></pre></td></tr></table></figure><br>如果是访问模块的导出对象中的子对象，那么继续：<br><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">val</span> subLib =rootLib.getObject(subLibName); <span class="comment">// 这里的 subLibName 是 index 文件中 module.exports 里面的模块名</span></span><br></pre></td></tr></table></figure><br> 如果子对象还有子对象，继续<code>.getObject</code> 即可</p><p><strong>5、运行指定方法</strong></p><p>接下来就简单了，直接通过如下方法执行 js 中的指定方法<br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">executeVoidFunction</span><span class="params">(String name, V8Array parameters)</span></span><br><span class="line"><span class="keyword">public</span> String <span class="title function_">executeStringFunction</span><span class="params">(String name, V8Array parameters)</span> </span><br><span class="line"><span class="keyword">public</span> <span class="type">double</span> <span class="title function_">executeDoubleFunction</span><span class="params">(String name, V8Array parameters)</span> </span><br><span class="line"><span class="keyword">public</span> <span class="type">int</span> <span class="title function_">executeIntegerFunction</span><span class="params">(String name, V8Array parameters)</span></span><br><span class="line">……</span><br></pre></td></tr></table></figure></p><p><code>V8Object</code> 提供了很多数据格式调用，不过都差不多，主要是在返回值那里帮你实现了数据的转化，如果不想用转化好的格式，希望自己来操作的话，使用<code>public V8Object executeObjectFunction()</code> 拿到返回值，自己去转化即可</p><p><strong>6、释放资源</strong></p><p>由于 V8 运行消耗较多的资源，执行结束的时候要将在过程中创建的所有的资源释放，避免导致内存泄漏。<br>V8提供了close方法，如果只使用 v8.close() 进行释放，或者未关闭过程中有用到 v8 runtime 的变量都会报如下错误，正确的做法是将所有资源进行关闭。</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">java.lang.IllegalStateException: <span class="number">3</span> Object(s) still exist in runtime</span><br></pre></td></tr></table></figure><h2 id="三、进阶"><a href="#三、进阶" class="headerlink" title="三、进阶"></a>三、进阶</h2><p>通过以上的方式已经能执行很多逻辑了，但在实践过过程中发现：如何 js 的返回值是 Promise 的话不会等到最终的结果给我们，而是直接返回了一个 Promise 对象，以及看不到 <code>console.log</code> 打印的日志…… 诸如此类的问题需要解决，这里主要讲讲这两种方法的实现。</p><p><strong>注册 Native 插件</strong></p><p>J2V8 是一个基于 V8 引擎的 Java 库，它允许在 Java 中执行 JavaScript 代码。由于 J2V8 是在 Java 中运行的，它没有直接访问浏览器或控制台的能力，因此无法直接使用 console.log 函数来输出日志，总结 <strong>J2V8 不能实现以下功能：</strong></p><blockquote><ul><li>浏览器 API：j2v8 是在 Java 中运行的，因此无法直接访问浏览器 API，如 DOM、BOM 等。这意味着 j2v8 无法直接操作网页内容、处理事件等</li><li>文件系统访问：j2v8 在 Java 中运行，无法直接访问文件系统。如果需要访问文件系统，需要使用 Java 提供的文件操作 API。</li><li>定时器：JavaScript 中有多种定时器函数，如 setTimeout、setInterval 等，可以在指定时间后执行代码。但 j2v8 无法实现这些定时器函数，因为它无法直接访问系统的计时器。</li><li>Web Worker：Web Worker 是 JavaScript 中的一个特殊对象，可以在后台线程中执行代码，以避免阻塞主线程。但 j2v8 无法实现 Web Worker，因为它无法直接访问操作系统的线程。</li><li>Node.js API：j2v8 主要是为了在 Java 中执行浏览器端的 JavaScript 代码而设计的，因此无法直接访问 Node.js API。如果需要在 Java 中执行 Node.js 代码，可以考虑使用 Nashorn 等其他工具。</li></ul></blockquote><p>这里是 <code>console.log</code>的一个简单实现：</p><p><code>V8Object</code> 是 J2V8 中的一个类，它代表了一个 JavaScript 对象，对于 <code>console.log</code> 我们可以将 <code>console</code> 看作一个对象，其有一个叫 <code>log</code> 的方法，要实现在 js 中打印日志到 Android Studio 控制台，如下即可：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ConsolePlugin</span> &#123;</span><br><span class="line">    </span><br><span class="line">    fun <span class="title function_">log</span><span class="params">(message: Any)</span> &#123;</span><br><span class="line">        Log.d(<span class="string">&quot;ConsolePlugin&quot;</span>, message.toString())</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fun <span class="title function_">register</span><span class="params">(v8: V8)</span> &#123;</span><br><span class="line">        <span class="type">val</span> <span class="variable">v8Console</span> <span class="operator">=</span> V8Object(v8)</span><br><span class="line">        <span class="comment">// 第一个 log 表示 在 Java 中该方法的名字，第二个 log 表示在 JavaScript 中调用的名字 </span></span><br><span class="line">        v8Console.registerJavaMethod(<span class="built_in">this</span>, <span class="string">&quot;log&quot;</span>, <span class="string">&quot;log&quot;</span>, arrayOf&lt;Class&lt;*&gt;&gt;(Any::class.java))</span><br><span class="line">        v8Console.setWeak()</span><br><span class="line">        <span class="comment">// 将含有叫&quot;log&quot;方法的一个对象加到运行环境中，该对象被命名为 &quot;console&quot;</span></span><br><span class="line">        v8.add(<span class="string">&quot;console&quot;</span>, v8Console)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">ConsolePlugin().register(v8)</span><br></pre></td></tr></table></figure><p>具体代码可参考:<a href="https://github.com/VomPom/J2V8_tutorial">J2V8_tutorial</a></p><h4 id="执行返回值是-Promise-类型的方法"><a href="#执行返回值是-Promise-类型的方法" class="headerlink" title="执行返回值是 Promise 类型的方法"></a>执行返回值是 Promise 类型的方法</h4><p>之前将的方法调用都是返回数据为基础类型，由于在 Java/kotlin 中没有<code>Promise</code>类型的方法，所以对于 <code>Promise</code> 方法我们需要进行一些特殊处理，我们通过使用 <code>CountDownLatch</code> 可以来实现一个 “异步变同步” 的操作，我们需要考虑的是如何接受到 <code>resolve</code> <code>rejcet</code>的调用，js 中 Promise 的方法使用如下：<br><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="title class_">PromiseMethod</span>().<span class="title function_">then</span>(<span class="function">(<span class="params">result</span>)=&gt;</span>&#123;</span><br><span class="line">    <span class="comment">// success got result</span></span><br><span class="line">  &#125;).<span class="title function_">catch</span>(<span class="function">(<span class="params">e</span>)=&gt;</span>&#123;</span><br><span class="line">    <span class="comment">// error...</span></span><br><span class="line">  &#125;);</span><br><span class="line"></span><br></pre></td></tr></table></figure><br>在 J2V8中一样的实现</p><p><strong>获取返回的 Promise 对象</strong><br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="type">val</span> <span class="variable">promiseObj</span> <span class="operator">=</span> v8.executeFunction(functionName, v8Array) as V8Object</span><br></pre></td></tr></table></figure><br><strong>执行 Promise 对象的 then 和 catch 方法 </strong><br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">jsPromise.apply &#123;</span><br><span class="line">        <span class="type">val</span> <span class="variable">onResolveParameter</span> <span class="operator">=</span> V8Array(v8).push(onResolve)</span><br><span class="line">        <span class="type">val</span> <span class="variable">onRejectParameter</span> <span class="operator">=</span> V8Array(v8).push(onReject)</span><br><span class="line">        executeVoidFunction(<span class="string">&quot;then&quot;</span>, onResolveParameter)</span><br><span class="line">        executeVoidFunction(<span class="string">&quot;catch&quot;</span>, onRejectParameter)</span><br><span class="line">        ....</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><br>其中 onResolve<br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="type">val</span> <span class="variable">onResolve</span> <span class="operator">=</span> V8Function(jsRuntime) &#123; receiver, parameters -&gt;</span><br><span class="line">        ……</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><br>具体代码可参考:<a href="https://github.com/VomPom/J2V8_tutorial">J2V8_tutorial</a></p><h2 id="四、总结"><a href="#四、总结" class="headerlink" title="四、总结"></a>四、总结</h2><p>以上基本上能解决大部分 Android 调用 js的代码逻辑了，这里对整体执行的流程进行一个总结</p><p>1、通过 webpack 对多个 .js 文件打包<br>2、初始化 V8 环境并加载 .js 文件<br>3、注册 Java 方法，供 js 进行调用<br>4、读取指定的模板<br>5、执行目标 js 方法，并释放 v8 执行过程中产生的资源</p><h3 id="踩过的一些坑"><a href="#踩过的一些坑" class="headerlink" title="踩过的一些坑"></a>踩过的一些坑</h3><p>1、<code>java.lang.UnsupportedOperationException: StartNodeJS Not Supported.</code></p><p>这个库有一个 <code>NodeJS.createNodeJS()</code>方法，以为是完美结合 NodeJs 的，查了下不太支持 Android，不过也有人提出解决方法：<a href="https://stackoverflow.com/questions/42574824/how-to-use-nodejs-in-android-using-j2v8">https://stackoverflow.com/questions/42574824/how-to-use-nodejs-in-android-using-j2v8</a></p><p>2、<code>java.lang.IllegalStateException: 3 Object(s) still exist in runtime</code></p><p>这是调用 `v8.close`` 总是会遇到的问题，一定需要确保使用了 v8 Runtime 过程变量有被释放掉，可能有时候不知道具体哪个变量没有被释放</p><p>3、<code>setTimeout、setInterval</code> 无效</p><p>这是我最开始遇到的问题，简单想着“既然能执行js代码，那 setTimeout、setInterval 这些方法都是 js 最普通的方法应该没问题吧”，如果有一些平时在 js 很常见的操作如果无法执行，最好 check 一下 J2V8 是否支持</p><p>4、Undefined 相关</p><p>虽然源码里面通过了一个 Undefined 的类，但是不能直接使用，如果方法返回的 Undefined，通过 <code>V8Object</code> 的 <code>isUndefined()</code> 去判断</p><h3 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h3><p>[1]J2V8 <a href="https://eclipsesource.com/blogs/tutorials/getting-started-with-j2v8/">https://eclipsesource.com/blogs/tutorials/getting-started-with-j2v8/</a></p><p>[2] Registering Java Callbacks with J2V8 <a href="https://eclipsesource.com/blogs/2015/06/06/registering-java-callbacks-with-j2v8/">https://eclipsesource.com/blogs/2015/06/06/registering-java-callbacks-with-j2v8/</a></p><p>[3] Simple JS in Node.js <a href="https://yenhuang.gitbooks.io/android-development-note/content/wrap-js-library/simple-js-with-nodejs.html">https://yenhuang.gitbooks.io/android-development-note/content/wrap-js-library/simple-js-with-nodejs.html</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;V8 引擎是由 Google 开源的 JavaScript 引擎，Chrome 就是基于 V8 开发，V8 是跨平台的，J2V8 基于 V8 进行开发，使得 js 代码能够在 Android 平台上脱离 WebView 运行。目前，也有很多关于 Android J2V8 的</summary>
      
    
    
    
    <category term="技术文章" scheme="http://julis.wang/categories/technology/"/>
    
    
    <category term="JavaScript" scheme="http://julis.wang/tags/JavaScript/"/>
    
  </entry>
  
</feed>
