<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title><![CDATA[邹成卓的个人网站]]></title>
  <subtitle><![CDATA[Chengzhuo Zou's personal website]]></subtitle>
  <link href="/atom.xml" rel="self"/>
  <link href="https://www.zoucz.com/"/>
  <updated>2024-01-30T18:10:01.000Z</updated>
  <id>https://www.zoucz.com/</id>
  
  <author>
    <name><![CDATA[邹成卓]]></name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title><![CDATA[react（三） 跨端应用框架]]></title>
    <link href="https://www.zoucz.com/blog/2024/01/27/682a7050-bf98-11ee-8eb9-6929c410dc79/"/>
    <id>https://www.zoucz.com/blog/2024/01/27/682a7050-bf98-11ee-8eb9-6929c410dc79/</id>
    <published>2024-01-27T11:52:59.000Z</published>
    <updated>2024-01-30T18:10:01.000Z</updated>
    <content type="html"><![CDATA[<p>前面提到，react 的核心模块是可以做到平台无关的，在做 fiber 树渲染的时候，再根据需求选择在浏览器上渲染 DOM，还是在服务端渲染生成 html 字符，或者是在其它实现 HostConfig 协议的场景实现任意端的渲染。  </p>
<p>在服务端渲染时， nodejs 天然提供了 js 运行环境，同时服务端也不需要真正渲染出 UI，只需要给出 html 字符串，或者 html 流就行，真正的渲染仍然交给浏览器。和在nodejs服务端的运行不同，终端上运行时，要解决的问题更多，包括如何渲染 UI、如何运行 JS，JS 如何与终端通信等。  </p>
<p>本文从 UI 渲染、JS运行时、JS和终端通信机制 等方面，描述 React Native 的实现原理，并简单介绍其它几款和 react 相关的跨端渲染框架。  </p>
<h1 id="1-_React_Native"><a href="#1-_React_Native" class="headerlink" title="1. React Native"></a>1. React Native</h1><p><a href="https://github.com/facebook/react-native" target="_blank" rel="external">https://github.com/facebook/react-native</a>  </p>
<h2 id="UI_u6E32_u67D3"><a href="#UI_u6E32_u67D3" class="headerlink" title="UI渲染"></a>UI渲染</h2><p>React Native 使用原生平台的 UI 组件进行渲染，它将 JavaScript 中的 React 组件映射到相应的原生 UI 组件。这种方式能让整体渲染体验达到接近原生的效果，同时能在一定程度上将代码在多端复用（仍然存在一些小的细节需要不同平台各自适配）。    </p>
<p>渲染环节涉及三个线程。  </p>
<ul>
<li>UI 线程（主线程）：唯一可以操作宿主视图的线程。负责</li>
<li>JavaScript 线程：这是执行 React 渲染阶段的地方。</li>
<li>后台线程：专门用于布局的线程。</li>
</ul>
<p>实现跨端 UI 渲染的过程   </p>
<p><img src="https://zoucz.com/blogimgs/682a7050-bf98-11ee-8eb9-6929c410dc79.md/2730cc84919b190b115196bbac1326d9.jpg" alt="image.png">  </p>
<p>其中，Fabric 是 React Native 新架构的渲染系统。其核心原理是在 C++ 层统一更多的渲染逻辑，提升与宿主平台（host platforms）互操作性，并为 React Native 解锁更多能力。其研发始于 2018 年。从 2021 年开始， Facebook App 中的 React Native 启用了新的渲染器。  </p>
<p><img src="https://zoucz.com/blogimgs/682a7050-bf98-11ee-8eb9-6929c410dc79.md/f0d97e95707f6f8011fdc05a3e180950.jpg" alt="image.png"></p>
<p>事件触发 Fabric 渲染的过程，渲染可以分为三个步骤  </p>
<ul>
<li>渲染（Render）：在 JavaScript 中，React 执行那些产品逻辑代码创建 React 元素树（React Element Trees）。然后在 C++ 中，用 React 元素树创建 React 影子树（React Shadow Tree）。</li>
<li>提交（Commit）：在 React 影子树完全创建后，渲染器会触发一次提交。这会将 React 元素树和新创建的 React 影子树的提升为“下一棵要挂载的树”。 这个过程中也包括了布局信息计算。</li>
<li>挂载（Mount）：React 影子树有了布局计算结果后，它会被转化为一个宿主视图树（Host View Tree）。</li>
</ul>
<p><img src="https://zoucz.com/blogimgs/682a7050-bf98-11ee-8eb9-6929c410dc79.md/81597a745d455a24d012741bdd01386c.jpg" alt="image.png"></p>
<p><a href="https://reactnative.cn/architecture/render-pipeline" target="_blank" rel="external">https://reactnative.cn/architecture/render-pipeline</a>   </p>
<h2 id="JS__u5F15_u64CE"><a href="#JS__u5F15_u64CE" class="headerlink" title="JS 引擎"></a>JS 引擎</h2><p>根据版本和运行环境不同， React Native 可能使用三种 JS 引擎  </p>
<ul>
<li>从 React Native 0.70 版本开始，默认使用 Hermes 引擎，它是专门为 React Native 而优化的一个开源 JavaScript 引擎。</li>
<li>如果 Hermes 被禁用或是较早的 React Native 版本，则会使用JavaScriptCore，也就是 Safari 所使用的 JavaScript 引擎。但是在 iOS 上 JavaScriptCore 并没有使用即时编译技术（JIT），因为在 iOS 中应用无权拥有可写可执行的内存页（因此无法动态生成代码）。</li>
<li>在使用 Chrome 调试时，所有的 JavaScript 代码都运行在 Chrome 中，并且通过 WebSocket 与原生代码通信。此时的运行环境是V8 引擎。（社区也有提供可以在生产环境中使用的react-native-v8)</li>
</ul>
<h2 id="JS_u548C_u7EC8_u7AEF_u901A_u4FE1"><a href="#JS_u548C_u7EC8_u7AEF_u901A_u4FE1" class="headerlink" title="JS和终端通信"></a>JS和终端通信</h2><p>JS 和终端通信有属性传递、事件、JsBrige 等多种方式。  </p>
<ul>
<li>原生组件属性、RN组件属性互相传递</li>
<li>原生组件到RN组件的事件通信</li>
<li>RN组件到原生组件通过 JsBrige 直接调用方法。</li>
<li>新架构的 JSI（JavaScript Interface） ，JS 和 C++ 互相持有对象引用并调用方法</li>
</ul>
<p>值得注意的是，这些通信都是异步的，以提高性能。  </p>
<p>参考：  </p>
<ul>
<li><a href="https://reactnative.cn/docs/communication-android" target="_blank" rel="external">https://reactnative.cn/docs/communication-android</a></li>
<li><a href="https://reactnative.cn/docs/communication-ios" target="_blank" rel="external">https://reactnative.cn/docs/communication-ios</a></li>
<li><a href="https://reactnative.cn/docs/the-new-architecture/why" target="_blank" rel="external">https://reactnative.cn/docs/the-new-architecture/why</a> </li>
</ul>
<h1 id="2-__u5176_u5B83_u652F_u6301_react__u7684_u8DE8_u7AEF_u6846_u67B6"><a href="#2-__u5176_u5B83_u652F_u6301_react__u7684_u8DE8_u7AEF_u6846_u67B6" class="headerlink" title="2. 其它支持 react 的跨端框架"></a>2. 其它支持 react 的跨端框架</h1><h2 id="Taro"><a href="#Taro" class="headerlink" title="Taro"></a>Taro</h2><p><a href="https://github.com/nervjs/taro" target="_blank" rel="external">https://github.com/nervjs/taro</a>  </p>
<p>Taro 是一个开放式跨端跨框架解决方案，支持使用 React/Vue/Nerv 等框架来开发 微信 / 京东 / 百度 / 支付宝 / 字节跳动 / QQ / 飞书 小程序 / H5 / RN 等应用。  </p>
<p><img src="https://zoucz.com/blogimgs/682a7050-bf98-11ee-8eb9-6929c410dc79.md/e51d1ff89243f458088126c3a5cac729.jpg" alt="image.png"></p>
<p>来源：<a href="https://www.w3.org/2022/09/hangzhou/miniapps/slides/jiajian.pdf" target="_blank" rel="external">https://www.w3.org/2022/09/hangzhou/miniapps/slides/jiajian.pdf</a>  </p>
<h2 id="Hippy"><a href="#Hippy" class="headerlink" title="Hippy"></a>Hippy</h2><p><a href="https://github.com/Tencent/Hippy" target="_blank" rel="external">https://github.com/Tencent/Hippy</a>  </p>
<p>Hippy 是腾讯开源的一款跨端渲染框架，支持 react 和 vue。从底层进行了大量优化，在启动速度、可复用列表组件、渲染效率、动画速度、网络通信等方面都提供了业内顶尖的性能表现。  </p>
<p>Hippy 3.0 的升级中，业务层上不再局限于 JS 驱动，也支持切换其它任意 DSL 语言进行驱动；在渲染层中，渲染引擎除了支持现有原生（Native）渲染之外，还可以选择其他渲染 Renderer，如 Flutter(Voltron) 渲染。  </p>
<p><img src="https://zoucz.com/blogimgs/682a7050-bf98-11ee-8eb9-6929c410dc79.md/62ef08f1c54de5317a11057a566f4ce1.jpg" alt="image.png"></p>
]]></content>
    <summary type="html">
    <![CDATA[<p>前面提到，react 的核心模块是可以做到平台无关的，在做 fiber 树渲染的时候，再根据需求选择在浏览器上渲染 DOM，还是在服务端渲染生成 html 字符，或者是在其它实现 HostConfig 协议的场景实现任意端的渲染。  </p>
<p>在服务端渲染时， nod]]>
    </summary>
    
      <category term="react" scheme="https://www.zoucz.com/blog/tags/react/"/>
    
      <category term="前端开发" scheme="https://www.zoucz.com/blog/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
      <category term="前端开发" scheme="https://www.zoucz.com/blog/categories/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[react（二）react 服务端渲染]]></title>
    <link href="https://www.zoucz.com/blog/2024/01/18/62c58d70-b610-11ee-8eb9-6929c410dc79/"/>
    <id>https://www.zoucz.com/blog/2024/01/18/62c58d70-b610-11ee-8eb9-6929c410dc79/</id>
    <published>2024-01-18T14:46:38.000Z</published>
    <updated>2024-01-30T15:00:09.000Z</updated>
    <content type="html"><![CDATA[<p>react 的核心模块是可以做到平台无关的，在做 fiber 树渲染的时候，再根据需求选择在浏览器上渲染 DOM，还是在服务端渲染生成 html 字符，或者是在其它实现 HostConfig 协议的场景实现任意端的渲染。  </p>
<p>react 的服务端渲染（ssr）和 ejs 的区别在于，除了在服务端渲染出 html 外，还支持服务端的渲染产物在客户端无缝绑定 react 在客户端的能力，即同构。  </p>
<p>ssr 能在 SEO 和 首屏速度方面带来一定收益，但是同时也会给代码的复杂性和维护成本上带来负面影响，需要根据项目实际情况做好权衡。这里简单总结一下 ssr 过程中可能会遇到的问题和处理方案，内容结构见下图。  </p>
<p><img src="https://zoucz.com/blogimgs/62c58d70-b610-11ee-8eb9-6929c410dc79.md/4d77290328e2bd261cb9884a58da79a5.jpg" alt="image.png"></p>
<h1 id="1-__u57FA_u7840_u7684_ssr__u5B9E_u73B0"><a href="#1-__u57FA_u7840_u7684_ssr__u5B9E_u73B0" class="headerlink" title="1. 基础的 ssr 实现"></a>1. 基础的 ssr 实现</h1><p>react 官方 API 中关于 ssr 的部分提供了很简单的几个 API <a href="https://react.dev/reference/react-dom/server" target="_blank" rel="external">https://react.dev/reference/react-dom/server</a>  </p>
<p>实际项目中，ssr 还有很多问题要处理，但是 react 只关注组件渲染本身，这里首先看一下几个官方 API 的简单使用。  </p>
<ul>
<li>renderToString 将 React 树渲染为一个 HTML 字符串</li>
<li>renderToPipeableStream 将 react 组件渲染为 nodejs 的可读流</li>
<li>renderToReadableStream 将 react 组件渲染为 Web 可读流</li>
<li>renderToStaticMarkup 将非交互的 React 组件树渲染成 HTML 字符串</li>
<li>renderToStaticNodeStream 将非交互的 React 组件树渲染成 nodejs 的可读流<h2 id="renderToString"><a href="#renderToString" class="headerlink" title="renderToString"></a>renderToString</h2>看方法名称就比较好理解了，在渲染步骤中，react 不再是根据 fiber 树创建 dom 节点，而是生成 html 字符串。<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> Koa from <span class="string">'koa'</span>;</span><br><span class="line"><span class="keyword">import</span> Router from <span class="string">'@koa/router'</span>;</span><br><span class="line"><span class="keyword">import</span> React from <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; renderToString &#125;from<span class="string">"react-dom/server"</span>;</span><br><span class="line"><span class="keyword">import</span> App from <span class="string">'./App'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> SERVER_PORT = <span class="number">8002</span>;</span><br><span class="line"><span class="keyword">const</span> app = <span class="keyword">new</span> Koa();</span><br><span class="line"><span class="keyword">const</span> router = <span class="keyword">new</span> Router();</span><br><span class="line"></span><br><span class="line">router.get(<span class="string">'/'</span>, (ctx, next) =&gt;&#123;</span><br><span class="line">    <span class="keyword">const</span> content = renderToString(&lt;App/&gt;);</span><br><span class="line">    ctx.body=content;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">app.use(router.routes());</span><br><span class="line">app.listen(SERVER_PORT, () =&gt; &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(`server is started on port $&#123;SERVER_PORT&#125; ...`);</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>
</li>
</ul>
<h2 id="renderToPipeableStream"><a href="#renderToPipeableStream" class="headerlink" title="renderToPipeableStream"></a>renderToPipeableStream</h2><p>react18 中提供了 lazy、Suspense，配合 renderToPipeableStream 方法，实现流式内容下发的效果，加快首屏展示速度。<br>使用 renderToString 生成 html 时，可能有部分组件依赖其它资源加载生成。如果等待所有组件渲染完成再输出内容，速度会变得很慢。如果使用上面的 lazy + Suspense 方案，renderToString 甚至只能输出 fallback 的内容，不支持加载后的结果渲染。<br>SlowComponent.tsx<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">import React, &#123; lazy &#125; from 'react';</span><br><span class="line"></span><br><span class="line">const SlowComponent = lazy(async () =&gt; &#123;</span><br><span class="line">    await new Promise&lt;void&gt;((resolve) =&gt; &#123;</span><br><span class="line">      setTimeout(() =&gt; &#123;</span><br><span class="line">        resolve()</span><br><span class="line">      &#125;, 2000)</span><br><span class="line">    &#125;)</span><br><span class="line">    return &#123; default: () =&gt; &lt;p&gt;a slow component&lt;/p&gt; &#125;;</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">export default SlowComponent;</span><br></pre></td></tr></table></figure></p>
<p>App.tsx<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">import React, &#123; Suspense &#125; from 'react';</span><br><span class="line">import SlowComponent from './SlowComponent';</span><br><span class="line">import './App.css';</span><br><span class="line"></span><br><span class="line">const App = function() &#123;</span><br><span class="line">    return &lt;&gt;</span><br><span class="line">        &lt;p className='reactApp'&gt;react app&lt;/p&gt;</span><br><span class="line">        &lt;Suspense fallback=&#123;&lt;p&gt;loading...&lt;/p&gt;&#125;&gt;</span><br><span class="line">            &lt;SlowComponent /&gt;</span><br><span class="line">        &lt;/Suspense&gt;</span><br><span class="line">    &lt;/&gt;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">export default App;</span><br></pre></td></tr></table></figure></p>
<p>server.tsx<br><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> Koa from <span class="string">'koa'</span>;</span><br><span class="line"><span class="keyword">import</span> Router from <span class="string">'@koa/router'</span>;</span><br><span class="line"><span class="keyword">import</span> React from <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; renderToString, renderToPipeableStream &#125;from<span class="string">"react-dom/server"</span>;</span><br><span class="line"><span class="keyword">import</span> App from <span class="string">'./App'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> SERVER_PORT = <span class="number">8002</span>;</span><br><span class="line"><span class="keyword">const</span> app = <span class="keyword">new</span> Koa();</span><br><span class="line"><span class="keyword">const</span> router = <span class="keyword">new</span> Router();</span><br><span class="line"></span><br><span class="line">router.get(<span class="string">'/'</span>, async (ctx, next) =&gt;&#123;</span><br><span class="line">    await <span class="keyword">new</span> Promise&lt;<span class="built_in">void</span>&gt;((resolve, reject) =&gt; &#123;</span><br><span class="line">        <span class="keyword">const</span> readableSteam = renderToPipeableStream(&lt;App /&gt;, &#123;</span><br><span class="line">            onShellReady() &#123;</span><br><span class="line">                ctx.respond = <span class="literal">false</span>;</span><br><span class="line">                ctx.res.statusCode = <span class="number">200</span>;</span><br><span class="line">                ctx.response.set(<span class="string">'content-type'</span>, <span class="string">'text/html'</span>);</span><br><span class="line">                readableSteam.pipe(ctx.res);</span><br><span class="line">            &#125;,</span><br><span class="line">            onError(err) &#123;</span><br><span class="line">                reject(err);</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><span class="line"></span><br><span class="line">app.use(router.routes());</span><br><span class="line">app.listen(SERVER_PORT, () =&gt; &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(`server is started on port $&#123;SERVER_PORT&#125; ...`);</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure></p>
<p>这样就能实现首屏内容快速加载，耗时内容流式下发的效果。  </p>
<p><img src="https://zoucz.com/blogimgs/62c58d70-b610-11ee-8eb9-6929c410dc79.md/6f06fccd92447bfa3de49c62c7de79ca.jpg" alt="image.png"></p>
<p>慢组件加载完成后  </p>
<p><img src="https://zoucz.com/blogimgs/62c58d70-b610-11ee-8eb9-6929c410dc79.md/f5ecff278cbb765493760e1004325d6d.jpg" alt="image.png"></p>
<h2 id="renderToReadableStream"><a href="#renderToReadableStream" class="headerlink" title="renderToReadableStream"></a>renderToReadableStream</h2><p>同 reanderToPipeableStream，只是返回的是 Web 可读流，而不是 nodejs 的可读流，一般在 Deno 或支持 Web Streams 的运行时中使用。</p>
<h2 id="renderToStaticMarkup"><a href="#renderToStaticMarkup" class="headerlink" title="renderToStaticMarkup"></a>renderToStaticMarkup</h2><p>和 renderToString 类似，只是其生成的内容无法在客户端水合（后面会提到），适合纯展示类型静态页。</p>
<h2 id="renderToStaticNodeStream"><a href="#renderToStaticNodeStream" class="headerlink" title="renderToStaticNodeStream"></a>renderToStaticNodeStream</h2><p>和 renderToStaticMarkup 类似，其生成的内容是一个 node 可读流，支持 Suspend，但是无法再客户端水合。</p>
<h1 id="2-__u540C_u6784_u4E0E_u6C34_u5408"><a href="#2-__u540C_u6784_u4E0E_u6C34_u5408" class="headerlink" title="2. 同构与水合"></a>2. 同构与水合</h1><p>在上面示例的基础上，添加一个点击交互组件，客户端渲染，组件正常工作。服务端渲染，点击后没有反应。  </p>
<p><img src="https://zoucz.com/blogimgs/62c58d70-b610-11ee-8eb9-6929c410dc79.md/b630bfdfa2eedf4f4563206ada84f014.jpg" alt="image.png"></p>
<p>查看服务端渲染返回的内容，也确实没有任何脚本内容。<br>修改 client 渲染的入口，client 端的 react 将会连接到内部有 domNode 的 HTML 上，然后接管其中的 domNode，这个操作称为“水合”。  </p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> root = hydrateRoot(rootContainer, &lt;App /&gt;);</span><br></pre></td></tr></table></figure>
<p>修改 client 打包配置，让 client 的 output 输出为一个 client.js。  </p>
<p>此时可以将 client.js 的 url 直接插入服务端渲染输出的 html 中，或者通过 renderToPipeableStream 提供的 bootstrapScripts 选项来设置，下面是一个通过 bootstrapScripts 来设置的示例：  </p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">app.use(KoaStatic(path.resolve(__dirname, <span class="string">'../client/'</span>)));</span><br><span class="line"></span><br><span class="line">router.get(<span class="string">'/ssr'</span>, async (ctx, next) =&gt;&#123;</span><br><span class="line">    <span class="comment">// const content = renderToString(&lt;App/&gt;);</span></span><br><span class="line">    <span class="comment">// ctx.body=content;</span></span><br><span class="line">    await <span class="keyword">new</span> Promise&lt;<span class="built_in">void</span>&gt;((resolve, reject) =&gt; &#123;</span><br><span class="line">        <span class="keyword">const</span> readableSteam = renderToPipeableStream(&lt;App /&gt;, &#123;</span><br><span class="line">            bootstrapScripts: [<span class="string">'client.js'</span>],</span><br><span class="line">            onShellReady() &#123;</span><br><span class="line">                ctx.respond = <span class="literal">false</span>;</span><br><span class="line">                ctx.res.statusCode = <span class="number">200</span>;</span><br><span class="line">                ctx.response.set(<span class="string">'content-type'</span>, <span class="string">'text/html'</span>);</span><br><span class="line">                readableSteam.pipe(ctx.res);</span><br><span class="line">            &#125;,</span><br><span class="line">            onError(err) &#123;</span><br><span class="line">                reject(err);</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><img src="https://zoucz.com/blogimgs/62c58d70-b610-11ee-8eb9-6929c410dc79.md/265c1c52603d12c0cfd03879a9b2b000.jpg" alt="image.png"></p>
<p>可以看到 ssr 返回的代码中附加了客户端的脚本。此时如果服务端渲染的 dom 结构和客户端渲染的结构不同，会导致 hydrate 时无法找到目标 dom，无法正确接管，无法达到正确效果。  </p>
<p>注意：实际生产环境比这种情况要复杂，可能存在 hash 命名或者代码/公共库/运行时拆分，需要结合打包工具插件生成 assetsMap 来获取打包后的资源。  </p>
<h1 id="3-__u8DEF_u7531_u5904_u7406"><a href="#3-__u8DEF_u7531_u5904_u7406" class="headerlink" title="3. 路由处理"></a>3. 路由处理</h1><h3 id="ssr_u8DEF_u7531_u652F_u6301"><a href="#ssr_u8DEF_u7531_u652F_u6301" class="headerlink" title="ssr路由支持"></a>ssr路由支持</h3><p>客户端渲染时，一般可以通过 hash 或者 pushState 来实现单页应用路由，而这俩在服务端都无法使用。<br>react-router-dom 提供了 StaticRouter 来实现服务端渲染的路由。  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">router.get(/^\/ssr.*/gim, async (ctx, next) =&gt;&#123;</span><br><span class="line">    const data = &lt;&gt;&lt;html&gt;&lt;head&gt;&lt;title&gt;react ssr&lt;/title&gt;&lt;/head&gt;&lt;body&gt;&lt;div id="root"&gt;</span><br><span class="line">        &lt;StaticRouter location=&#123;ctx.request.path&#125;&gt;</span><br><span class="line">            &lt;App /&gt;</span><br><span class="line">            &#123; routes &#125;</span><br><span class="line">        &lt;/StaticRouter&gt;</span><br><span class="line">    &lt;/div&gt;&lt;/body&gt;&lt;/html&gt;&lt;/&gt;;</span><br><span class="line">    await new Promise&lt;void&gt;((resolve, reject) =&gt; &#123;</span><br><span class="line">        const readableSteam = renderToPipeableStream(data, &#123;</span><br><span class="line">            // bootstrapScripts: ['/client.js'],</span><br><span class="line">            onShellReady() &#123;</span><br><span class="line">                ctx.respond = false;</span><br><span class="line">                ctx.res.statusCode = 200;</span><br><span class="line">                ctx.response.set('content-type', 'text/html');</span><br><span class="line">                readableSteam.pipe(ctx.res);</span><br><span class="line">            &#125;,</span><br><span class="line">            onError(err) &#123;</span><br><span class="line">                reject(err);</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>注意点：</p>
<ul>
<li>必须使用 StaticRouter</li>
<li>后端需要自己处理 router 请求路径的问题<h3 id="u8DEF_u7531_u540C_u6784"><a href="#u8DEF_u7531_u540C_u6784" class="headerlink" title="路由同构"></a>路由同构</h3>此时，ssr 应用已经支持了后端路由，在页面上通过 Link 切换路由时，可以看到应用内容随路由变化。但是路由每次切换时，都是一次刷新页面。  </li>
</ul>
<p>此时在客户端使用 BrowserRouter 实现同样的路由逻辑，使用 bootstrapScripts 开启客户端水合，即可对路由逻辑进行同构。  </p>
<p>服务端：<br><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="keyword">const</span> readableSteam = renderToPipeableStream(data, &#123;</span><br><span class="line">    bootstrapScripts: [<span class="string">'/client.js'</span>],</span><br><span class="line">    ...</span><br><span class="line">&#125;);</span><br><span class="line">...</span><br></pre></td></tr></table></figure></p>
<p>客户端：<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">import React, &#123; StrictMode &#125; from 'react';</span><br><span class="line">import &#123; createRoot, hydrateRoot &#125; from 'react-dom/client'</span><br><span class="line">import &#123; BrowserRouter &#125; from 'react-router-dom';</span><br><span class="line">import routes from './Router';</span><br><span class="line">import App from './App'</span><br><span class="line">import './index.css'</span><br><span class="line"></span><br><span class="line">const rootContainer = document.querySelector('#root');</span><br><span class="line">if (rootContainer) &#123;</span><br><span class="line">    hydrateRoot(rootContainer, &lt;StrictMode&gt;</span><br><span class="line">        &lt;BrowserRouter&gt;</span><br><span class="line">            &lt;App /&gt;</span><br><span class="line">            &#123; routes &#125;</span><br><span class="line">        &lt;/BrowserRouter&gt;</span><br><span class="line">    &lt;/StrictMode&gt;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>注意点：</p>
<ul>
<li>因为server端对路由增加了支持，要小心 bootstrapScripts 解析路径不受影响，否则会导致拉取脚本时拿到 ssr 返回的 html 结果而导致 hydrate 失败</li>
<li>客户端需要使用 BrowserRouter 而不是 HashRouter，否则会导致从子路由进入应用时 hydrate 失败</li>
</ul>
<h1 id="4-__u6837_u5F0F_u5904_u7406"><a href="#4-__u6837_u5F0F_u5904_u7406" class="headerlink" title="4. 样式处理"></a>4. 样式处理</h1><p>在“基础的 ssr 实现”示例中， App 组件引入了样式，在客户端渲染时，通过 style-loader 或者 MiniCssExtractPlugin.loader 可以让样式生效，但是使用服务端渲染时，样式无效了。  </p>
<p>对于不同方式的样式，ssr 时也需要做不同的处理。  </p>
<h2 id="u540C_u6784_u65F6_u7531_u5BA2_u6237_u7AEF_u8BBE_u7F6E"><a href="#u540C_u6784_u65F6_u7531_u5BA2_u6237_u7AEF_u8BBE_u7F6E" class="headerlink" title="同构时由客户端设置"></a>同构时由客户端设置</h2><p>在“同构与水合”、“路由处理” 的示例中，客户端打包时选择用 style-loader 将样式附加到页面中，所以一旦水合成功，页面样式就设置成功了。  </p>
<p>这种方式会导致 ssr 直出的时候实际上是丢失样式的，直到客户端第二次渲染完成后，样式才设置上。对于比较复杂的页面，会有样式闪烁的问题。  </p>
<h2 id="u4F7F_u7528_isomorphic-style-loader"><a href="#u4F7F_u7528_isomorphic-style-loader" class="headerlink" title="使用 isomorphic-style-loader"></a>使用 isomorphic-style-loader</h2><p>isomorphic-style-loader 可以像客户端的 style-loader 一样，在 ssr 时向 html 中插入样式。<br>项目地址：<a href="https://github.com/kriasoft/isomorphic-style-loader" target="_blank" rel="external">https://github.com/kriasoft/isomorphic-style-loader</a><br>组件中<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">import React from 'react'</span><br><span class="line">import withStyles from 'isomorphic-style-loader/withStyles'</span><br><span class="line">import s from './App.scss'</span><br><span class="line"></span><br><span class="line">function App(props, context) &#123;</span><br><span class="line">  return (</span><br><span class="line">    &lt;div className=&#123;s.root&#125;&gt;</span><br><span class="line">      &lt;h1 className=&#123;s.title&#125;&gt;Hello, world!&lt;/h1&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">  )</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">export default withStyles(s)(App)</span><br></pre></td></tr></table></figure></p>
<p>服务端<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">import StyleContext from 'isomorphic-style-loader/StyleContext'</span><br><span class="line">import App from './App.js'</span><br><span class="line">...</span><br><span class="line">server.get('*', (req, res, next) =&gt; &#123;</span><br><span class="line">  const css = new Set() // CSS for all rendered React components</span><br><span class="line">  const insertCss = (...styles) =&gt; styles.forEach(style =&gt; css.add(style._getCss()))</span><br><span class="line">  const body = ReactDOM.renderToString(</span><br><span class="line">    &lt;StyleContext.Provider value=&#123;&#123; insertCss &#125;&#125;&gt;</span><br><span class="line">      &lt;App /&gt;</span><br><span class="line">    &lt;/StyleContext.Provider&gt;</span><br><span class="line">  )</span><br><span class="line">  const html = `&lt;!doctype html&gt;</span><br><span class="line">    &lt;html&gt;</span><br><span class="line">      &lt;head&gt;</span><br><span class="line">        &lt;script src="client.js" defer&gt;&lt;/script&gt;</span><br><span class="line">        &lt;style&gt;$&#123;[...css].join('')&#125;&lt;/style&gt;</span><br><span class="line">      &lt;/head&gt;</span><br><span class="line">      &lt;body&gt;</span><br><span class="line">        &lt;div id="root"&gt;$&#123;body&#125;&lt;/div&gt;</span><br><span class="line">      &lt;/body&gt;</span><br><span class="line">    &lt;/html&gt;`</span><br><span class="line">  res.status(200).send(html)</span><br><span class="line">&#125;)</span><br><span class="line">...</span><br></pre></td></tr></table></figure></p>
<p>客户端 hydrate 时<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">import StyleContext from 'isomorphic-style-loader/StyleContext'</span><br><span class="line">import App from './App.js'</span><br><span class="line"></span><br><span class="line">const insertCss = (...styles) =&gt; &#123;</span><br><span class="line">  const removeCss = styles.map(style =&gt; style._insertCss())</span><br><span class="line">  return () =&gt; removeCss.forEach(dispose =&gt; dispose())</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">ReactDOM.hydrate(</span><br><span class="line">  &lt;StyleContext.Provider value=&#123;&#123; insertCss &#125;&#125;&gt;</span><br><span class="line">    &lt;App /&gt;</span><br><span class="line">  &lt;/StyleContext.Provider&gt;,</span><br><span class="line">  document.getElementById('root')</span><br><span class="line">)</span><br></pre></td></tr></table></figure></p>
<p>最终生成的 html<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&lt;html&gt;</span><br><span class="line">  &lt;head&gt;</span><br><span class="line">...</span><br><span class="line">    &lt;style type="text/css"&gt;</span><br><span class="line">      .App_root_Hi8 &#123; padding: 10px &#125;</span><br><span class="line">      .App_title_e9Q &#123; color: red &#125;</span><br><span class="line">    &lt;/style&gt;</span><br><span class="line">  &lt;/head&gt;</span><br><span class="line">...</span><br><span class="line">&lt;/html&gt;</span><br></pre></td></tr></table></figure></p>
<h2 id="u6253_u5305_u5DE5_u5177_u83B7_u53D6_u8D44_u6E90_u8868"><a href="#u6253_u5305_u5DE5_u5177_u83B7_u53D6_u8D44_u6E90_u8868" class="headerlink" title="打包工具获取资源表"></a>打包工具获取资源表</h2><p>这种方式是 react 官方文档在流式 ssr 中推荐的一种方式，我感觉这是最贴近生产，侵入性也是相对较小的一种方式了。<br>服务端：<br><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 你需要从你的打包构建工具中获取这个 JSON。</span></span><br><span class="line"><span class="keyword">const</span> assetMap = &#123;</span><br><span class="line">  <span class="string">'styles.css'</span>: <span class="string">'/styles.123456.css'</span>,</span><br><span class="line">  <span class="string">'main.js'</span>: <span class="string">'/main.123456.js'</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">app.use(<span class="string">'/'</span>, (request, response) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; pipe &#125; = renderToPipeableStream(&lt;App assetMap=&#123;assetMap&#125; /&gt;, &#123;</span><br><span class="line">    <span class="comment">// 注意: 由于这些数据并非用户生成，所以使用 stringify 是安全的。</span></span><br><span class="line">    bootstrapScriptContent: `<span class="built_in">window</span>.assetMap = $&#123;<span class="built_in">JSON</span>.stringify(assetMap)&#125;;`,</span><br><span class="line">    bootstrapScripts: [assetMap[<span class="string">'main.js'</span>]],</span><br><span class="line">    onShellReady() &#123;</span><br><span class="line">      response.setHeader(<span class="string">'content-type'</span>, <span class="string">'text/html'</span>);</span><br><span class="line">      pipe(response);</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>
<p>设置 bootstrapScriptContent 是为了让客户端也能获取到一样的数据，以支持同构。<br>客户端：<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">// index.tsx</span><br><span class="line">import &#123; hydrateRoot &#125; from 'react-dom/client';</span><br><span class="line">import App from './App.js';</span><br><span class="line"></span><br><span class="line">hydrateRoot(document, &lt;App assetMap=&#123;window.assetMap&#125; /&gt;);</span><br><span class="line"></span><br><span class="line">// App.tsx</span><br><span class="line">export default function App(&#123; assetMap &#125;) &#123;</span><br><span class="line">  return (</span><br><span class="line">    &lt;html&gt;</span><br><span class="line">      &lt;head&gt;</span><br><span class="line">        ...</span><br><span class="line">        &lt;link rel="stylesheet" href=&#123;assetMap['styles.css']&#125;&gt;&lt;/link&gt;</span><br><span class="line">        ...</span><br><span class="line">      &lt;/head&gt;</span><br><span class="line">      ...</span><br><span class="line">    &lt;/html&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<h1 id="5-__u72B6_u6001_u548C_u6570_u636E_u8BF7_u6C42"><a href="#5-__u72B6_u6001_u548C_u6570_u636E_u8BF7_u6C42" class="headerlink" title="5. 状态和数据请求"></a>5. 状态和数据请求</h1><h2 id="redux__u7684_ssr__u540C_u6784"><a href="#redux__u7684_ssr__u540C_u6784" class="headerlink" title="redux 的 ssr 同构"></a>redux 的 ssr 同构</h2><p>在组件中使用 redux 维护全局状态，并在路由页面中异步加载数据<br>store.tsx<br><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createAsyncThunk, createSlice, configureStore, combineReducers &#125; from <span class="string">'@reduxjs/toolkit'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; TypedUseSelectorHook, useSelector, useDispatch &#125; from <span class="string">'react-redux'</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 命名空间、全局状态初始值</span></span><br><span class="line"><span class="keyword">const</span> <span class="keyword">namespace</span> = <span class="string">'global'</span>;</span><br><span class="line"><span class="keyword">const</span> initialState = &#123;</span><br><span class="line">    page1Data: <span class="string">'loading...'</span>,</span><br><span class="line">    page2Data: <span class="string">'loading...'</span>,</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">export</span> <span class="keyword">const</span> fetchPage1Data = createAsyncThunk(</span><br><span class="line">    `$&#123;<span class="keyword">namespace</span>&#125;/fetchPage1Data`,</span><br><span class="line">    async (dataName: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">        <span class="keyword">const</span> res = await <span class="keyword">new</span> Promise((resolve, reject) =&gt; &#123;</span><br><span class="line">            <span class="built_in">console</span>.log(<span class="string">'fetch data1...'</span>)</span><br><span class="line">            setTimeout(() =&gt; &#123;</span><br><span class="line">                resolve(`remote page1 data of $&#123;dataName&#125;`)</span><br><span class="line">            &#125;, <span class="number">1000</span>)</span><br><span class="line">        &#125;)</span><br><span class="line">        <span class="keyword">return</span> &#123; page1Data: res &#125;;</span><br><span class="line">    &#125;,</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> fetchPage2Data = createAsyncThunk(</span><br><span class="line">    `$&#123;<span class="keyword">namespace</span>&#125;/fetchPage2Data`,</span><br><span class="line">    async (dataName: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">        <span class="keyword">const</span> res = await <span class="keyword">new</span> Promise((resolve, reject) =&gt; &#123;</span><br><span class="line">            <span class="built_in">console</span>.log(<span class="string">'fetch data2...'</span>)</span><br><span class="line">            setTimeout(() =&gt; &#123;</span><br><span class="line">                resolve(`remote page2 data of $&#123;dataName&#125;`)</span><br><span class="line">            &#125;, <span class="number">1000</span>)</span><br><span class="line">        &#125;)</span><br><span class="line">        <span class="keyword">return</span> &#123; page2Data: res &#125;;</span><br><span class="line">    &#125;,</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建带有命名空间的reducer</span></span><br><span class="line"><span class="keyword">const</span> globalInfoSlice = createSlice(&#123;</span><br><span class="line">    name: <span class="keyword">namespace</span>,</span><br><span class="line">    initialState,</span><br><span class="line">    reducers: &#123;</span><br><span class="line">      clearData: (state) =&gt; &#123;</span><br><span class="line">        state.page1Data = <span class="string">''</span>;</span><br><span class="line">        state.page2Data = <span class="string">''</span>;</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 处理异步reducer</span></span><br><span class="line">    extraReducers: (builder) =&gt; &#123;</span><br><span class="line">      builder</span><br><span class="line">        .addCase(fetchPage1Data.fulfilled, (state, &#123; payload &#125;) =&gt; &#123;</span><br><span class="line">          <span class="keyword">if</span> (!payload) <span class="keyword">return</span>;</span><br><span class="line">          state.page1Data = payload.page1Data as <span class="built_in">string</span>;</span><br><span class="line">        &#125;)</span><br><span class="line">        .addCase(fetchPage2Data.fulfilled, (state, &#123; payload &#125;) =&gt; &#123;</span><br><span class="line">            <span class="keyword">if</span> (!payload) <span class="keyword">return</span>;</span><br><span class="line">            state.page2Data = payload.page2Data as <span class="built_in">string</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"></span><br><span class="line"><span class="comment">// 封装 hook</span></span><br><span class="line"><span class="keyword">const</span> reducer = combineReducers(&#123;</span><br><span class="line">    global: globalInfoSlice.reducer,</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> store = configureStore(&#123;</span><br><span class="line">    reducer,</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> &#123; clearData &#125; = globalInfoSlice.actions;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> selectGlobal = (state: RootState) =&gt; state.global;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> RootState = ReturnType&lt;<span class="keyword">typeof</span> store.getState&gt;;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> AppDispatch = <span class="keyword">typeof</span> store.dispatch;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> useAppDispatch = () =&gt; useDispatch&lt;AppDispatch&gt;();</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> useAppSelector: TypedUseSelectorHook&lt;RootState&gt; = useSelector;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> store;</span><br></pre></td></tr></table></figure></p>
<p>路由页面中加载数据、使用数据<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">import React, &#123; useEffect &#125; from 'react';</span><br><span class="line">import &#123; useAppSelector, useAppDispatch, fetchPage1Data &#125; from './store';</span><br><span class="line"></span><br><span class="line">const RoutePage1 = function() &#123;</span><br><span class="line">    const dispatch = useAppDispatch();</span><br><span class="line">    const page1Data = useAppSelector((state) =&gt; state.global.page1Data);</span><br><span class="line">    useEffect(() =&gt; &#123;</span><br><span class="line">        dispatch(fetchPage1Data('data1'))</span><br><span class="line">    &#125;, [])</span><br><span class="line">    return &lt;&gt;</span><br><span class="line">        &lt;p&gt;content of route 1&lt;/p&gt;</span><br><span class="line">        &lt;p&gt;remote data of page1 from redux: &#123;page1Data&#125;&lt;/p&gt;</span><br><span class="line">    &lt;/&gt;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">export default RoutePage1;</span><br></pre></td></tr></table></figure></p>
<p>entry 入口 client.tsx<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">import React, &#123; StrictMode &#125; from 'react';</span><br><span class="line">import &#123; createRoot, hydrateRoot &#125; from 'react-dom/client'</span><br><span class="line">import &#123; BrowserRouter, HashRouter &#125; from 'react-router-dom';</span><br><span class="line">import &#123; Provider &#125; from 'react-redux';</span><br><span class="line">import store from './store';</span><br><span class="line">import routes from './Router';</span><br><span class="line">import App from './App'</span><br><span class="line"></span><br><span class="line">import './index.css'</span><br><span class="line"></span><br><span class="line">const rootContainer = document.querySelector('#root');</span><br><span class="line">if (rootContainer) &#123;</span><br><span class="line">    const root = createRoot(rootContainer);</span><br><span class="line">    root.render(</span><br><span class="line">        &lt;StrictMode&gt;</span><br><span class="line">            &lt;Provider store=&#123;store&#125;&gt;</span><br><span class="line">                &lt;HashRouter&gt;</span><br><span class="line">                    &lt;App /&gt;</span><br><span class="line">                    &#123; routes &#125;</span><br><span class="line">                &lt;/HashRouter&gt;</span><br><span class="line">            &lt;/Provider&gt;</span><br><span class="line">        &lt;/StrictMode&gt;</span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>启动客户端应用调试，切换到对应路由后，能正常加载数据、显示数据。<br>修改服务端渲染入口， server.tsx<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">import &#123; Provider &#125; from 'react-redux';</span><br><span class="line">import store from './store';</span><br><span class="line">...</span><br><span class="line">&lt;Provider store=&#123;store&#125;&gt;</span><br><span class="line">    &lt;StaticRouter location=&#123;ctx.request.path&#125;&gt;</span><br><span class="line">        &lt;App /&gt;</span><br><span class="line">        &#123; routes &#125;</span><br><span class="line">    &lt;/StaticRouter&gt;</span><br><span class="line">&lt;/Provider&gt;</span><br><span class="line">...</span><br></pre></td></tr></table></figure></p>
<p>客户端 client.tsx 修改为水合并重新打包<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">hydrateRoot(rootContainer, &lt;StrictMode&gt;</span><br><span class="line">        &lt;Provider store=&#123;store&#125;&gt;</span><br><span class="line">            &lt;BrowserRouter&gt;</span><br><span class="line">                &lt;App /&gt;</span><br><span class="line">                &#123; routes &#125;</span><br><span class="line">            &lt;/BrowserRouter&gt;</span><br><span class="line">        &lt;/Provider&gt;</span><br><span class="line">    &lt;/StrictMode&gt;)</span><br><span class="line">...</span><br></pre></td></tr></table></figure></p>
<h2 id="u670D_u52A1_u7AEF_u5F02_u6B65_u8BF7_u6C42"><a href="#u670D_u52A1_u7AEF_u5F02_u6B65_u8BF7_u6C42" class="headerlink" title="服务端异步请求"></a>服务端异步请求</h2><p>上一步中，已经成功实现了同构的 redux 状态管理。  </p>
<p><img src="https://zoucz.com/blogimgs/62c58d70-b610-11ee-8eb9-6929c410dc79.md/b7f5bbbe3349112990af8fdb8576f414.jpg" alt="image.png"></p>
<p>然而我们观察页面展示效果和源码，可以发现 ssr 返回的数据其实是 redux 中的初始数据，真正的数据加载还是在客户端水合后，组件渲染后触发的。  </p>
<p>想要 ssr 直出异步数据加载后的 html，思路也比较简单，就是找到当前路由，然后执行它的数据加载逻辑，加载完毕后再渲染返回就行了。  </p>
<p>可以使用 react-router-config 模块提供的 matchRoutes 获取当前的路由。为此，我们需要将路由改造成 matchRoutes 需要的 RouteConfig 格式。  </p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="interface"><span class="keyword">interface</span> RouteConfig </span>&#123;</span><br><span class="line">    key?: React.Key | <span class="literal">undefined</span>;</span><br><span class="line">    location?: Location | <span class="literal">undefined</span>;</span><br><span class="line">    component?: React.ComponentType&lt;RouteConfigComponentProps&lt;<span class="built_in">any</span>&gt;&gt; | React.ComponentType | <span class="literal">undefined</span>;</span><br><span class="line">    path?: <span class="built_in">string</span> | <span class="built_in">string</span>[] | <span class="literal">undefined</span>;</span><br><span class="line">    exact?: <span class="built_in">boolean</span> | <span class="literal">undefined</span>;</span><br><span class="line">    strict?: <span class="built_in">boolean</span> | <span class="literal">undefined</span>;</span><br><span class="line">    routes?: RouteConfig[] | <span class="literal">undefined</span>;</span><br><span class="line">    render?: ((props: RouteConfigComponentProps&lt;<span class="built_in">any</span>&gt;) =&gt; React.ReactNode) | <span class="literal">undefined</span>;</span><br><span class="line">    [propName: <span class="built_in">string</span>]: <span class="built_in">any</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>路由定义  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line">// Router.tsx</span><br><span class="line">...</span><br><span class="line">const routes = [</span><br><span class="line">    &#123;</span><br><span class="line">      path: '/ssr/route1',</span><br><span class="line">      component: &lt;RoutePage1 /&gt;,</span><br><span class="line">      exact: true,</span><br><span class="line">      // 供服务端调用</span><br><span class="line">      loadData: RoutePage1.loadData,</span><br><span class="line">      key: 'route1'</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      path: '/ssr/route2',</span><br><span class="line">      component: &lt;RoutePage2 /&gt;,</span><br><span class="line">      exact: true,</span><br><span class="line">      loadData: RoutePage2.loadData,</span><br><span class="line">      key: 'route2'</span><br><span class="line">    &#125;</span><br><span class="line">];</span><br><span class="line">// server.tsx 服务端路由</span><br><span class="line">...</span><br><span class="line">&lt;Provider store=&#123;store&#125;&gt;</span><br><span class="line">    &lt;StaticRouter location=&#123;ctx.request.path&#125;&gt;</span><br><span class="line">        &lt;App /&gt;</span><br><span class="line">        &lt;Routes&gt;</span><br><span class="line">        &#123;routes.map((route) =&gt; &#123;</span><br><span class="line">            return &lt;Route path=&#123;route.path as string&#125; key=&#123;route.key&#125; element=&#123;route.component as ReactNode&#125;/&gt;;</span><br><span class="line">        &#125;)&#125;</span><br><span class="line">        &lt;/Routes&gt;</span><br><span class="line">    &lt;/StaticRouter&gt;</span><br><span class="line">&lt;/Provider&gt;</span><br><span class="line">...</span><br><span class="line">// client.tsx 客户端水合</span><br><span class="line">...</span><br><span class="line">hydrateRoot(rootContainer, &lt;StrictMode&gt;</span><br><span class="line">    &lt;Provider store=&#123;store&#125;&gt;</span><br><span class="line">        &lt;BrowserRouter&gt;</span><br><span class="line">            &lt;App /&gt;</span><br><span class="line">            &lt;Routes&gt;</span><br><span class="line">            &#123;routes.map((route) =&gt; &#123;</span><br><span class="line">                return &lt;Route path=&#123;route.path as string&#125; key=&#123;route.key&#125; element=&#123;route.component as ReactNode&#125;/&gt;;</span><br><span class="line">            &#125;)&#125;</span><br><span class="line">            &lt;/Routes&gt;</span><br><span class="line">        &lt;/BrowserRouter&gt;</span><br><span class="line">    &lt;/Provider&gt;</span><br><span class="line">&lt;/StrictMode&gt;)</span><br><span class="line">...</span><br></pre></td></tr></table></figure>
<p>服务端判断路由并加载数据<br>(在我使用的 <a href="mailto:react-router@6.x" target="_blank" rel="external">react-router@6.x</a> 和 <a href="mailto:react-router-config@5.x" target="_blank" rel="external">react-router-config@5.x</a> 版本下，matchRoutes 有BUG会报错：TypeError: pathname.match is not a function。这里替换为自己的简单实现)<br><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取当前路由，若有匹配的路由，则先加载数据，以做到服务端 ssr 直出最终的 html</span></span><br><span class="line"><span class="comment">// const matchedRoutes = matchRoutes(routes, ctx.request.path);</span></span><br><span class="line"><span class="keyword">const</span> matchedRoutes = routes.filter(route =&gt; route.path === ctx.request.path);</span><br><span class="line"><span class="keyword">const</span> loadDataRequests = matchedRoutes.filter(item =&gt; item.loadData).map(item =&gt; item.loadData(store));</span><br><span class="line">await Promise.all(loadDataRequests);</span><br></pre></td></tr></table></figure></p>
<p><img src="https://zoucz.com/blogimgs/62c58d70-b610-11ee-8eb9-6929c410dc79.md/508ac5f3fa67796070e116017668e16d.jpg" alt="image.png"></p>
<p>此时可以观察到几个现象</p>
<ul>
<li>服务端返回的 html 已经是符合预期的加载好数据之后的内容了</li>
<li>客户端在展示服务端加载结果后一瞬间又会切换到初始值，并重新走一遍加载逻辑</li>
<li>控制台中有 hydrate 过程的异常报错</li>
</ul>
<h2 id="u6CE8_u6C34_u8131_u6C34"><a href="#u6CE8_u6C34_u8131_u6C34" class="headerlink" title="注水脱水"></a>注水脱水</h2><p>上面的现象其实比较好理解，客户端在水合结束后，会立即开始运行客户端代码，即渲染 store 中的初始值，并在渲染完毕后去拉取远程数据，导致一次无意义的数据请求。  </p>
<p>有一种比较简单的思路，在服务端渲染时，把加载好的数据通过 script 标签写入到window中，客户端从 window 中拿到初始数据后用于初始化 store。这个过程被称为服务端注水、客户端脱水。  </p>
<p>服务端注水  </p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// server.tsx</span></span><br><span class="line">await <span class="keyword">new</span> Promise&lt;<span class="built_in">void</span>&gt;((resolve, reject) =&gt; &#123;</span><br><span class="line">        <span class="keyword">const</span> readableSteam = renderToPipeableStream(data, &#123;</span><br><span class="line">            bootstrapScripts: [<span class="string">'/client.js'</span>],</span><br><span class="line">            bootstrapScriptContent: `<span class="built_in">window</span>.context = &#123;state: $&#123;<span class="built_in">JSON</span>.stringify (store.getState())&#125;&#125;`,</span><br><span class="line">            onShellReady() &#123;</span><br><span class="line">                ctx.respond = <span class="literal">false</span>;</span><br><span class="line">                ctx.res.statusCode = <span class="number">200</span>;</span><br><span class="line">                ctx.response.set(<span class="string">'content-type'</span>, <span class="string">'text/html'</span>);</span><br><span class="line">                readableSteam.pipe(ctx.res);</span><br><span class="line">            &#125;,</span><br><span class="line">            onError(err) &#123;</span><br><span class="line">                reject(err);</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>客户端脱水<br><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// store.tsx</span></span><br><span class="line"><span class="comment">// 命名空间、全局状态初始值</span></span><br><span class="line"><span class="keyword">const</span> <span class="keyword">namespace</span> = <span class="string">'global'</span>;</span><br><span class="line"><span class="keyword">let</span> initialState = &#123;</span><br><span class="line">    page1Data: <span class="string">'loading...'</span>,</span><br><span class="line">    page2Data: <span class="string">'loading...'</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">declare</span> <span class="keyword">const</span> <span class="built_in">window</span>: Window &amp; &#123; context: &#123; state: <span class="built_in">any</span>&#125; &#125;;</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">typeof</span> <span class="built_in">window</span> !== <span class="string">'undefined'</span>) &#123;</span><br><span class="line">    initialState = <span class="built_in">window</span>.context?.state?.global || initialState;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>这样就能从 window 中获取服务端请求好的数据了。但是此时客户端组件渲染完成后仍然会去加载一次数据，可以在客户端增加一些判断，不要重复加载数据。<br><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// RoutePage1.tsx</span></span><br><span class="line">useEffect(() =&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (page1Data === <span class="string">'loading...'</span>) dispatch(fetchPage1Data(<span class="string">'data1'</span>))</span><br><span class="line">&#125;, [])</span><br></pre></td></tr></table></figure></p>
<p>这样就完成了一个无冗余请求的 redux 同构应用。  </p>
<p><img src="https://zoucz.com/blogimgs/62c58d70-b610-11ee-8eb9-6929c410dc79.md/f6514fd4831dc3977d6110c142df5674.jpg" alt="image.png">  </p>
<h1 id="6-__u57FA_u4E8E_next-js__u7684_ssr"><a href="#6-__u57FA_u4E8E_next-js__u7684_ssr" class="headerlink" title="6. 基于 next.js 的 ssr"></a>6. 基于 next.js 的 ssr</h1><p>前面提到的内容会给代码带来不小的复杂度和维护成本，生产环境下也可以选择一些比较成熟的开源库来实现，如 next.js。  </p>
<p>next.js 封装了基本的 ssr 实现、css支持、服务端数据加载等（可以让组件通过提供 getServerSideProps 方法来实现服务端的数据加载）。  </p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">Page</span>(<span class="params">&#123; data &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// Render data...</span></span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// This gets called on every request</span></span><br><span class="line"><span class="keyword">export</span> async <span class="function"><span class="keyword">function</span> <span class="title">getServerSideProps</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// Fetch data from external API</span></span><br><span class="line">  <span class="keyword">const</span> res = await fetch(`https:<span class="comment">//.../data`)</span></span><br><span class="line">  <span class="keyword">const</span> data = await res.json()</span><br><span class="line"> </span><br><span class="line">  <span class="comment">// Pass data to the page via props</span></span><br><span class="line">  <span class="keyword">return</span> &#123; props: &#123; data &#125; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>其它细节这里不再赘述，可参考文档 <a href="https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props。" target="_blank" rel="external">https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props。</a></p>
]]></content>
    <summary type="html">
    <![CDATA[<p>react 的核心模块是可以做到平台无关的，在做 fiber 树渲染的时候，再根据需求选择在浏览器上渲染 DOM，还是在服务端渲染生成 html 字符，或者是在其它实现 HostConfig 协议的场景实现任意端的渲染。  </p>
<p>react 的服务端渲染（ssr）]]>
    </summary>
    
      <category term="react" scheme="https://www.zoucz.com/blog/tags/react/"/>
    
      <category term="前端开发" scheme="https://www.zoucz.com/blog/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
      <category term="前端开发" scheme="https://www.zoucz.com/blog/categories/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[react（一）从编码要点到原理]]></title>
    <link href="https://www.zoucz.com/blog/2024/01/14/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e/"/>
    <id>https://www.zoucz.com/blog/2024/01/14/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e/</id>
    <published>2024-01-14T09:14:20.000Z</published>
    <updated>2024-01-18T14:25:43.000Z</updated>
    <content type="html"><![CDATA[<h1 id="1-__u7F16_u7801_u8981_u70B9"><a href="#1-__u7F16_u7801_u8981_u70B9" class="headerlink" title="1. 编码要点"></a>1. 编码要点</h1><p>react 中有一些不符合直觉，容易忽略或者误解的点，想要编写出高质量的 react 代码，通读文档还是很有必要的。这部分主要内容来自官方文档（ <a href="https://react.dev/" target="_blank" rel="external">https://react.dev/</a> ），官方文档中给出了非常多的示例代码，以及最佳实践和常见错误的示例。 </p>
<p>结合我以往编码经验和官方文档描述，从 JSX 、状态管理、副作用 等方面，记录一些 react 编码要点。</p>
<h2 id="1-1_JSX"><a href="#1-1_JSX" class="headerlink" title="1.1 JSX"></a>1.1 JSX</h2><h3 id="1-1-1__u7EC4_u4EF6_u547D_u540D_u9650_u5236"><a href="#1-1-1__u7EC4_u4EF6_u547D_u540D_u9650_u5236" class="headerlink" title="1.1.1 组件命名限制"></a>1.1.1 组件命名限制</h3><p>这算是个入门级的潜规则，组件的名称必须以大写字母开头。如果组件名不是以大写字母开头的，react dom 不会把它识别为一个组件。  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">myButton</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="title">button</span>&gt;</span></span><br><span class="line">      I'm a button</span><br><span class="line">    <span class="tag">&lt;/<span class="title">button</span>&gt;</span></span><br><span class="line">  )</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> App = <span class="function"><span class="keyword">function</span> <span class="title">MyApp</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="title">div</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">h1</span>&gt;</span>Welcome to my app<span class="tag">&lt;/<span class="title">h1</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">myButton</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br><span class="line">  )</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/67e1cde8a8a267347acdcf13023f11e4.jpg" alt="image.png"></p>
<h3 id="1-1-2__u7981_u6B62_u5D4C_u5957_u5B9A_u4E49_u7EC4_u4EF6"><a href="#1-1-2__u7981_u6B62_u5D4C_u5957_u5B9A_u4E49_u7EC4_u4EF6" class="headerlink" title="1.1.2 禁止嵌套定义组件"></a>1.1.2 禁止嵌套定义组件</h3><p>组件可以嵌套渲染其他组件，但是禁止嵌套定义组件。这一点单独拿出来说，是因为 react 编译时运行时均不认为这是一个错误，然而这样做会导致运行时性能问题和体验BUG。  </p>
<p>① 性能问题 —— 重复渲染  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">ParentComponent</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">ChildComponent</span>(<span class="params"></span>)</span>&#123;...&#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;&gt;</span>...<span class="tag">&lt;<span class="title">ChildComponent</span> /&gt;</span>...<span class="tag">&lt;/&gt;</span></span><br><span class="line">&#125;</span></span><br></pre></td></tr></table></figure>
<p>上面的写法中，父组件每次重新渲染时，都会重新定义子组件，进而导致每次返回的 JSX 组件中，子组件部分都是一个全新组件。渲染、提交DOM时，会将其当做一个全新的渲染节点，生成新的 DOM 节点。  </p>
<p>② 体验 BUG —— 子组件状态丢失  </p>
<p>有时候如果子组件本身没有状态，我们并不容易发现重复渲染的性能问题。  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> App = <span class="function"><span class="keyword">function</span> <span class="title">MyApp</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [counter, setCounter] = useState(<span class="number">0</span>);</span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">ChildComponent</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> id = useId()</span><br><span class="line">    <span class="keyword">const</span> [content, setContent] = useState(<span class="string">''</span>);</span><br><span class="line">    <span class="keyword">return</span> (<span class="xml"><span class="tag">&lt;&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">p</span>&gt;</span>component id is: &#123;id&#125;, content is: &#123;content&#125;<span class="tag">&lt;/<span class="title">p</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">input</span> <span class="attribute">value</span>=<span class="value">&#123;content&#125;</span> <span class="attribute">onChange</span>=<span class="value">&#123;(e)=</span>&gt;</span>&#123; setContent(e.target.value) &#125;&#125; /&gt;</span><br><span class="line">    <span class="tag">&lt;/&gt;</span>)</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="title">div</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">h1</span>&gt;</span>Welcome to my app<span class="tag">&lt;/<span class="title">h1</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">button</span> <span class="attribute">onClick</span>=<span class="value">&#123;()</span> =&gt;</span> &#123;setCounter(counter + 1)&#125;&#125;&gt;counter: &#123;counter&#125;<span class="tag">&lt;/<span class="title">button</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">ChildComponent</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br><span class="line">  )</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>如果子组件中有状态，则在父组件重新渲染时，会触发子组件的重新定义重新渲染生成，其状态会丢失，运行上面的代码还可以看到子组件的 id 在不断发生变化。  </p>
<p>③ 体验 BUG —— 焦点丢失  </p>
<p>当子组件嵌套定义时，它可以访问到父组件的状态，如果直接在子组件中使用和修改父组件的状态，还可能引入因重新渲染而丢失焦点的体验问题。  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> App = <span class="function"><span class="keyword">function</span> <span class="title">MyApp</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [counter, setCounter] = useState(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">const</span> [content, setContent] = useState(<span class="string">''</span>);</span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">ChildComponent</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> id = useId()</span><br><span class="line">    <span class="keyword">return</span> (<span class="xml"><span class="tag">&lt;&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">p</span>&gt;</span>component id is: &#123;id&#125;, content is: &#123;content&#125;<span class="tag">&lt;/<span class="title">p</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">input</span> <span class="attribute">value</span>=<span class="value">&#123;content&#125;</span> <span class="attribute">onChange</span>=<span class="value">&#123;(e)=</span>&gt;</span>&#123; setContent(e.target.value) &#125;&#125; /&gt;</span><br><span class="line">    <span class="tag">&lt;/&gt;</span>)</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="title">div</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">h1</span>&gt;</span>Welcome to my app<span class="tag">&lt;/<span class="title">h1</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">button</span> <span class="attribute">onClick</span>=<span class="value">&#123;()</span> =&gt;</span> &#123;setCounter(counter + 1)&#125;&#125;&gt;counter: &#123;counter&#125;<span class="tag">&lt;/<span class="title">button</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">ChildComponent</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br><span class="line">  )</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>例如上面的写法，组件貌似可以正常工作，但是每输入一个字符，输入框的焦点就会丢失。原因就是嵌套定义带来的子组件重新渲染问题。  </p>
<p>解决方案非常简单，如上面②中的示例，将子组件挪到外面定义就行了；③中的示例，子组件本身没有状态，可以把它挪到外面定义，或者当做函数调用而不是子组件使用 {ChildComponent()}。  </p>
<h3 id="1-1-3_html__u4E0D_u662F_JSX"><a href="#1-1-3_html__u4E0D_u662F_JSX" class="headerlink" title="1.1.3 html 不是 JSX"></a>1.1.3 html 不是 JSX</h3><p>看起来我们可以直接在 JSX 中使用 html 的标签， 但是要注意的是 html 并不能直接当 JSX 使用。  </p>
<p>标签属性和内联style属性需要以驼峰命名法编写。  </p>
<p>例如，html<br><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="title">ul</span> <span class="attribute">style</span>=<span class="value">"background-color: black"</span>&gt;</span></span><br></pre></td></tr></table></figure></p>
<p>在 JSX 组件里应该写成    </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;ul style=&#123;&#123; backgroundColor: <span class="string">'black'</span> &#125;&#125;&gt;</span><br></pre></td></tr></table></figure>
<p>需要避免 JSX 属性和 html 属性冲突。<br>由于 class 是一个保留字，所以在 React 中需要用 className 来代替。  </p>
<p>更多细节可以使用 html-to-jsx 转换器探索。 <a href="https://transform.tools/html-to-jsx。" target="_blank" rel="external">https://transform.tools/html-to-jsx。</a>  </p>
<h3 id="1-1-4__u6761_u4EF6_u6E32_u67D3_u65F6_u7684__26amp_3B_26amp_3B__u5199_u6CD5"><a href="#1-1-4__u6761_u4EF6_u6E32_u67D3_u65F6_u7684__26amp_3B_26amp_3B__u5199_u6CD5" class="headerlink" title="1.1.4 条件渲染时的 &amp;&amp; 写法"></a>1.1.4 条件渲染时的 &amp;&amp; 写法</h3><p>条件渲染时，切勿将数字放在 &amp;&amp; 左侧，0将会被作为文本渲染  </p>
<h3 id="1-1-5__u5217_u8868_u6E32_u67D3_u65F6_u7684_key"><a href="#1-1-5__u5217_u8868_u6E32_u67D3_u65F6_u7684_key" class="headerlink" title="1.1.5 列表渲染时的 key"></a>1.1.5 列表渲染时的 key</h3><ul>
<li>不要把数组项的索引当作 key 值来用</li>
<li>不要使用随机数当做 key 使用<br>这两种方式都会导致渲染性能下降。  </li>
</ul>
<h3 id="1-1-6__u4E25_u683C_u6A21_u5F0F_u4E0E_u7EAF_u51FD_u6570"><a href="#1-1-6__u4E25_u683C_u6A21_u5F0F_u4E0E_u7EAF_u51FD_u6570" class="headerlink" title="1.1.6 严格模式与纯函数"></a>1.1.6 严格模式与纯函数</h3><p>纯函数的特征：</p>
<ul>
<li>只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。</li>
<li>输入相同，则输出相同。给定相同的输入，纯函数应总是返回相同的结果。</li>
</ul>
<p>简单来说就是纯函数不会修改外部的变量或对象，也不会引用外部会发生变化的变量/对象。   </p>
<p>下面是一个非纯函数组件  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> guest = <span class="number">0</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Cup</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// Bad: changing a preexisting variable!</span></span><br><span class="line">  guest = guest + <span class="number">1</span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="title">h2</span>&gt;</span>Tea cup for guest #&#123;guest&#125;<span class="tag">&lt;/<span class="title">h2</span>&gt;</span>;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">TeaSet</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">Cup</span> /&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">Cup</span> /&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">Cup</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/&gt;</span></span><br><span class="line">  )</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>这个组件在每次渲染时都会让外部变量加1，有很多不可控因素会影响这个组件的渲染，例如父组件重新渲染带来的子组件重新渲染等，导致最终的结果不可控。    </p>
<p>基于非纯函数的特性，react 在开发环境下引入的 StrictMode 来帮助检测这些问题，Strict 模式下，每个组件会被渲染两次，这样上面的组件显示的结果就是 2、4、6 而不是 1、2、3。  </p>
<h2 id="1-2__u72B6_u6001_u7BA1_u7406"><a href="#1-2__u72B6_u6001_u7BA1_u7406" class="headerlink" title="1.2 状态管理"></a>1.2 状态管理</h2><h3 id="1-2-1__u72B6_u6001_u7684_u672C_u8D28"><a href="#1-2-1__u72B6_u6001_u7684_u672C_u8D28" class="headerlink" title="1.2.1 状态的本质"></a>1.2.1 状态的本质</h3><p>状态在 react 内部的实现，类似一个组件实例绑定的数组，下面是一段示例代码帮助我们理解  </p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> componentHooks = [];</span><br><span class="line"><span class="keyword">let</span> currentHookIndex = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// useState 在 React 中是如何工作的（简化版）</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useState</span>(<span class="params">initialState</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">let</span> pair = componentHooks[currentHookIndex];</span><br><span class="line">  <span class="keyword">if</span> (pair) &#123;</span><br><span class="line">    <span class="comment">// 这不是第一次渲染</span></span><br><span class="line">    <span class="comment">// 所以 state pair 已经存在</span></span><br><span class="line">    <span class="comment">// 将其返回并为下一次 hook 的调用做准备</span></span><br><span class="line">    currentHookIndex++;</span><br><span class="line">    <span class="keyword">return</span> pair;</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">// 所以新建一个 state pair 然后存储它</span></span><br><span class="line">  pair = [initialState, setState];</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">setState</span>(<span class="params">nextState</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 当用户发起 state 的变更，</span></span><br><span class="line">    <span class="comment">// 把新的值放入 pair 中</span></span><br><span class="line">    pair[<span class="number">0</span>] = nextState;</span><br><span class="line">    updateDOM();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 存储这个 pair 用于将来的渲染</span></span><br><span class="line">  <span class="comment">// 并且为下一次 hook 的调用做准备</span></span><br><span class="line">  componentHooks[currentHookIndex] = pair;</span><br><span class="line">  currentHookIndex++;</span><br><span class="line">  <span class="keyword">return</span> pair;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>详见文档：react.dev/learn/state-a-components-memory#giving-a-component-multiple-state-variables  </p>
<p>这段代码演示了组件是如何记忆状态变量，并在每次重新渲染时返回变量的。  </p>
<p>值得注意的是，组件的状态由 react 保存，且对于每个组件实例（即渲染节点），状态都是隔离且私有的。  </p>
<h3 id="1-2-2__u72B6_u6001_u66F4_u65B0_uFF08_u4E0D_u53EF_u53D8_u5BF9_u8C61_uFF09"><a href="#1-2-2__u72B6_u6001_u66F4_u65B0_uFF08_u4E0D_u53EF_u53D8_u5BF9_u8C61_uFF09" class="headerlink" title="1.2.2 状态更新（不可变对象）"></a>1.2.2 状态更新（不可变对象）</h3><p>组件的状态应该是一个不可变对象，即不能在组件内部直接给状态变量赋值、修改状态变量的属性/元素等。  </p>
<p>① 不能直接在组件中修改状态变量或者给状态变量赋值  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Component</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">  <span class="keyword">let</span> [counter, setCounter] = useState(<span class="number">0</span>);</span><br><span class="line">  <span class="comment">// 这样赋值修改的是当前作用域的值，不会对存储在组件对象内部的状态值造成任何影响</span></span><br><span class="line">  counter = <span class="number">1</span>;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">let</span> [obj, setObj] = useState(&#123;&#125;);</span><br><span class="line">  <span class="comment">// 这样赋值修改会影响状态，但是无法正确触发组件重新渲染</span></span><br><span class="line">  obj.a = <span class="number">1</span>;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">let</span> [arr, setArr] = useState([]);</span><br><span class="line">  <span class="comment">// 这样赋值修改会影响状态，但是无法正确触发组件重新渲染</span></span><br><span class="line">  arr[<span class="number">3</span>] = <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>② 如果 state 变量是一个对象时，不能只更新它的属性。  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Component</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">  <span class="keyword">let</span> [obj, setObj] = useState(&#123;a:<span class="number">1</span>, b:<span class="number">2</span>&#125;);</span><br><span class="line">  <span class="comment">// 这样会导致 obj 整体变为 &#123;a: 3&#125;，b属性丢失</span></span><br><span class="line">  setObj(&#123;a: <span class="number">3</span>&#125;);</span><br><span class="line">  <span class="comment">// 正确做法</span></span><br><span class="line">  setObj(&#123;...obj, a: <span class="number">3</span>&#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>③ 不能使用状态对象或数组本身修改后更新状态  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Component</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">  <span class="keyword">let</span> [obj, setObj] = useState(&#123;a:<span class="number">1</span>, b:<span class="number">2</span>&#125;);</span><br><span class="line">  <span class="comment">// 这样的状态修改不能正常触发重新渲染</span></span><br><span class="line">  obj.a = <span class="number">3</span>;</span><br><span class="line">  setObj(obj);</span><br><span class="line">  <span class="comment">// 正确做法，创建一个新对象</span></span><br><span class="line">  setObj(&#123;...obj, a: <span class="number">3</span>&#125;);</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">let</span> [arr, setArr] = useState([<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]);</span><br><span class="line">  <span class="comment">// 这样的状态修改不能正常触发重新渲染</span></span><br><span class="line">  arr.push(<span class="number">4</span>);</span><br><span class="line">  setArr(arr);</span><br><span class="line">  <span class="comment">// 正确做法，创建一个新数组</span></span><br><span class="line">  setArr([...arr, <span class="number">4</span>]);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>react 使用 Object.is 来比较状态是否发生变化，因而直接修改状态本身，react 是感知不到状态变化的，在更新渲染树时也就不会更新渲染节点。  </p>
<p>使用不可变对象库可以简化写法，参考：</p>
<ul>
<li><a href="https://github.com/immerjs/immer" target="_blank" rel="external">https://github.com/immerjs/immer</a></li>
<li><a href="https://github.com/immerjs/use-immer" target="_blank" rel="external">https://github.com/immerjs/use-immer</a></li>
<li><a href="https://github.com/tnfe/limu" target="_blank" rel="external">https://github.com/tnfe/limu</a></li>
</ul>
<h3 id="1-2-3__u91CD_u65B0_u6E32_u67D3_u65F6_u7684_u72B6_u6001_u4FDD_u7559_u548C_u91CD_u7F6E"><a href="#1-2-3__u91CD_u65B0_u6E32_u67D3_u65F6_u7684_u72B6_u6001_u4FDD_u7559_u548C_u91CD_u7F6E" class="headerlink" title="1.2.3 重新渲染时的状态保留和重置"></a>1.2.3 重新渲染时的状态保留和重置</h3><p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/a0b873e095d980e6c5dfa0817208dd3b.jpg" alt="image.png"></p>
<p>react 从组件中创建渲染树，并基于渲染树创建真正的 DOM 树。中间的部分就是根据组件渲染得到的渲染树。   </p>
<p>当向一个组件添加状态时，看起来状态是保存在组件内。但实际上，状态是由 React 保存的。React 通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来。  </p>
<p>也就是说，只有当在树中相同的位置渲染相同的组件时，React 才会一直保留着组件的 state。  </p>
<p>值得注意的是，react 在生成渲染树时，每个花括号内的渲染结果都会被当做一个渲染节点，下面的两个 JSX 示例中   </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&lt;div&gt;</span><br><span class="line">  &#123;isFancy ? (</span><br><span class="line">    &lt;Counter isFancy=&#123;true&#125; /&gt; </span><br><span class="line">  ) : (</span><br><span class="line">    &lt;Counter isFancy=&#123;false&#125; /&gt; </span><br><span class="line">  )&#125;</span><br><span class="line">&lt;/div&gt;</span><br><span class="line">...</span><br><span class="line">&lt;div&gt;</span><br><span class="line">  &#123;isFancy ? &lt;Counter isFancy=&#123;true&#125; /&gt; : null &#125;</span><br><span class="line">  &#123;isFancy ? null : &lt;Counter isFancy=&#123;false&#125; /&gt; &#125;</span><br><span class="line"></span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure>
<p>前面的写法会被 react 解析成一个渲染节点，isFancy 发生变化时，这个渲染节点返回的总是 Counter 组件，Counter 组件对应的状态变量保留；后面的写法则会被解析为两个渲染节点，分别根据情况返回 Counter 组件或者 null，此时 isFancy 发生变化时，表达式返回的组件不同，状态不会保留。  </p>
<p>这两种写法渲染效果从表面上看是一致的，但是内部的状态逻辑完全不同，值得注意。  </p>
<h3 id="1-2-4__u4F7F_u7528_reducer__u6574_u5408_u72B6_u6001_u66F4_u65B0"><a href="#1-2-4__u4F7F_u7528_reducer__u6574_u5408_u72B6_u6001_u66F4_u65B0" class="headerlink" title="1.2.4 使用 reducer 整合状态更新"></a>1.2.4 使用 reducer 整合状态更新</h3><p>由 useState 切换到 useReducer 可以将复杂的状态更新逻辑整合到 reducer 函数中。    </p>
<p>reducer 函数的作用用一句话来描述就是：接收当前的 state 和一个 action，返回经过 action 处理后的 state。   </p>
<p>reducer 函数一般可以支持多种 action，即把不同的更新逻辑都整合到各种 action 中。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useReducer &#125; <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> AddTask <span class="keyword">from</span> <span class="string">'./AddTask.js'</span>;</span><br><span class="line"><span class="keyword">import</span> TaskList <span class="keyword">from</span> <span class="string">'./TaskList.js'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">TaskApp</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [tasks, dispatch] = useReducer(tasksReducer, initialTasks);</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">handleAddTask</span>(<span class="params">text</span>) </span>&#123;</span><br><span class="line">    dispatch(&#123;</span><br><span class="line">      type: <span class="string">'added'</span>,</span><br><span class="line">      id: nextId++,</span><br><span class="line">      text: text,</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">handleChangeTask</span>(<span class="params">task</span>) </span>&#123;</span><br><span class="line">    dispatch(&#123;</span><br><span class="line">      type: <span class="string">'changed'</span>,</span><br><span class="line">      task: task,</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">handleDeleteTask</span>(<span class="params">taskId</span>) </span>&#123;</span><br><span class="line">    dispatch(&#123;</span><br><span class="line">      type: <span class="string">'deleted'</span>,</span><br><span class="line">      id: taskId,</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">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">h1</span>&gt;</span>布拉格的行程安排<span class="tag">&lt;/<span class="title">h1</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">AddTask</span> <span class="attribute">onAddTask</span>=<span class="value">&#123;handleAddTask&#125;</span> /&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="title">TaskList</span></span><br><span class="line">        <span class="attribute">tasks</span>=<span class="value">&#123;tasks&#125;</span></span><br><span class="line">        <span class="attribute">onChangeTask</span>=<span class="value">&#123;handleChangeTask&#125;</span></span><br><span class="line">        <span class="attribute">onDeleteTask</span>=<span class="value">&#123;handleDeleteTask&#125;</span></span><br><span class="line">      /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/&gt;</span></span><br><span class="line">  )</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">tasksReducer</span>(<span class="params">tasks, action</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">switch</span> (action.type) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">'added'</span>: &#123;</span><br><span class="line">      <span class="keyword">return</span> [</span><br><span class="line">        ...tasks,</span><br><span class="line">        &#123;</span><br><span class="line">          id: action.id,</span><br><span class="line">          text: action.text,</span><br><span class="line">          done: <span class="literal">false</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">      ];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">'changed'</span>: &#123;</span><br><span class="line">      <span class="keyword">return</span> tasks.map((t) =&gt; &#123;</span><br><span class="line">        <span class="keyword">if</span> (t.id === action.task.id) &#123;</span><br><span class="line">          <span class="keyword">return</span> action.task;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          <span class="keyword">return</span> t;</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">case</span> <span class="string">'deleted'</span>: &#123;</span><br><span class="line">      <span class="keyword">return</span> tasks.filter((t) =&gt; t.id !== action.id);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">default</span>: &#123;</span><br><span class="line">      <span class="keyword">throw</span> <span class="built_in">Error</span>(<span class="string">'未知 action: '</span> + action.type);</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="keyword">let</span> nextId = <span class="number">3</span>;</span><br><span class="line"><span class="keyword">const</span> initialTasks = [</span><br><span class="line">  &#123;id: <span class="number">0</span>, text: <span class="string">'参观卡夫卡博物馆'</span>, done: <span class="literal">true</span>&#125;,</span><br><span class="line">  &#123;id: <span class="number">1</span>, text: <span class="string">'看木偶戏'</span>, done: <span class="literal">false</span>&#125;,</span><br><span class="line">  &#123;id: <span class="number">2</span>, text: <span class="string">'打卡列侬墙'</span>, done: <span class="literal">false</span>&#125;</span><br><span class="line">];</span><br></pre></td></tr></table></figure></p>
<p>参考 <a href="http://react.dev/learn/extracting-state-logic-into-a-reducer#step-3-use-the-reducer-from-your-component" target="_blank" rel="external">http://react.dev/learn/extracting-state-logic-into-a-reducer#step-3-use-the-reducer-from-your-component</a>  </p>
<h3 id="1-2-5__u4F7F_u7528_props__u4F20_u9012_u72B6_u6001"><a href="#1-2-5__u4F7F_u7528_props__u4F20_u9012_u72B6_u6001" class="headerlink" title="1.2.5 使用 props 传递状态"></a>1.2.5 使用 props 传递状态</h3><p>下面是一个官网提供的典型示例，若两个同级子组件想要共享一个状态，可以将状态提升到它们的父组件中，然后通过 props 向下传递状态。  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/e2339163ec44ac6d1478c7910d553538.jpg" alt="image.png"></p>
<p>变量提升，并通过 props 向子组件传递状态  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/5a4195895420f08ae0f2e2fc1763b7dc.jpg" alt="image.png"></p>
<h3 id="1-2-6__u4F7F_u7528_context__u4F20_u9012_u72B6_u6001"><a href="#1-2-6__u4F7F_u7528_context__u4F20_u9012_u72B6_u6001" class="headerlink" title="1.2.6 使用 context 传递状态"></a>1.2.6 使用 context 传递状态</h3><p>使用 props 时，状态需要逐级透传，当组件数量多结构复杂时，这样效率会非常低，而且让组件的结构变得复杂难以维护。  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/95bcfcd76419b6c88a8c6d083f7e72f7.jpg" alt="image.png">  </p>
<p>而使用 context 可以跨层级传递状态   </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/8be209fbec196413b5b4c69aa2b9b10d.jpg" alt="image.png"></p>
<p>一般用于传递这类数据：</p>
<ul>
<li>站点主体</li>
<li>登录用户信息</li>
<li>页面路由</li>
<li>全局状态管理<h3 id="1-2-7__u5F71_u54CD_u6E32_u67D3_u7684_u56E0_u7D20_u603B_u7ED3"><a href="#1-2-7__u5F71_u54CD_u6E32_u67D3_u7684_u56E0_u7D20_u603B_u7ED3" class="headerlink" title="1.2.7 影响渲染的因素总结"></a>1.2.7 影响渲染的因素总结</h3>总结一下，当一个组件的渲染函数被调用时，除了它自身内部的局部变量和引用的外部变量（如浏览器API返回的外部状态数据等），还有这些可变因素会影响渲染结果</li>
<li>state</li>
<li>props</li>
<li>context</li>
</ul>
<h2 id="1-3__u526F_u4F5C_u7528"><a href="#1-3__u526F_u4F5C_u7528" class="headerlink" title="1.3 副作用"></a>1.3 副作用</h2><h3 id="1-3-1__u4EC0_u4E48_u65F6_u5019_u4F7F_u7528"><a href="#1-3-1__u4EC0_u4E48_u65F6_u5019_u4F7F_u7528" class="headerlink" title="1.3.1 什么时候使用"></a>1.3.1 什么时候使用</h3><p>react 中的逻辑分为这几类  </p>
<ul>
<li>渲染逻辑代码：组件的主体代码部分，一个返回 JSX 组件的纯函数</li>
<li>用户事件处理程序：特征组件内部，用于处理用户操作（如点击或者输入）引起的引起的“副作用”（它们改变了程序的状态）。</li>
<li>用渲染本身引起的副作用：如组件渲染后需要连接到服务器，或者需要加载数据等。<br>值得注意的是，useEffect 的执行时机是 渲染完成、屏幕更新后的 commit 阶段运行。</li>
</ul>
<h3 id="1-3-2__u89E6_u53D1_u7C7B_u578B"><a href="#1-3-2__u89E6_u53D1_u7C7B_u578B" class="headerlink" title="1.3.2 触发类型"></a>1.3.2 触发类型</h3><p>useEffect 有无依赖参数、空数组依赖、非空数组依赖 三种情况  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 每次渲染完成后都执行</span></span><br><span class="line">useEffect(() =&gt; &#123;&#125;)</span><br><span class="line"><span class="comment">// 仅第一次渲染完成后执行</span></span><br><span class="line">useEffect(() =&gt; &#123;&#125;, [])</span><br><span class="line"><span class="comment">// 依赖的变量 a / b 中任意一个发生变化后执行</span></span><br><span class="line">useEffect(() =&gt; &#123;&#125;, [a, b])</span><br></pre></td></tr></table></figure>
<p>其中第三种方式要注意，在 useEffect 中依赖的任何可变对象，都需要在后面的依赖数组中传入，否则 react 会报错。  </p>
<h3 id="1-3-3_useEffect__u5F15_u8D77_u7684_u6B7B_u5FAA_u73AF"><a href="#1-3-3_useEffect__u5F15_u8D77_u7684_u6B7B_u5FAA_u73AF" class="headerlink" title="1.3.3 useEffect 引起的死循环"></a>1.3.3 useEffect 引起的死循环</h3><p>useEffect 会在每次渲染后执行，或者每次渲染后依赖发生变化后执行，下面的写法会导致每次 useEffect 执行后状态被改变，又触发重新渲染，导致死循环。  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line">useEffect(() =&gt; &#123;</span><br><span class="line">  setCount(count + <span class="number">1</span>);</span><br><span class="line">&#125;);</span><br><span class="line"><span class="comment">//...</span></span><br><span class="line">useEffect(() =&gt; &#123;</span><br><span class="line">  setCount(count + <span class="number">1</span>);</span><br><span class="line">&#125;, [count]);</span><br></pre></td></tr></table></figure>
<h3 id="1-3-4__u591A_u79CD_u4E0D_u5E94_u8BE5_u4F7F_u7528_useEffect__u7684_u60C5_u51B5"><a href="#1-3-4__u591A_u79CD_u4E0D_u5E94_u8BE5_u4F7F_u7528_useEffect__u7684_u60C5_u51B5" class="headerlink" title="1.3.4 多种不应该使用 useEffect 的情况"></a>1.3.4 多种不应该使用 useEffect 的情况</h3><p>参考文档 <a href="https://react.dev/learn/you-might-not-need-an-effect" target="_blank" rel="external">https://react.dev/learn/you-might-not-need-an-effect</a><br>官方给出的场景非常多，就不一一列举了，感觉也记不住。   </p>
<p>我觉得要不最佳实践就别记黑名单了，记白名单吧：  </p>
<ul>
<li>组件初始化时的服务器连接操作</li>
<li>组件初始化时的数据加载操作等</li>
<li>一次渲染完毕后的其它操作如修改 DOM 等</li>
</ul>
<h3 id="1-3-5_useEffect__u7684_u751F_u547D_u5468_u671F"><a href="#1-3-5_useEffect__u7684_u751F_u547D_u5468_u671F" class="headerlink" title="1.3.5 useEffect 的生命周期"></a>1.3.5 useEffect 的生命周期</h3><p>Effect 与组件有不同的生命周期。组件可以挂载、更新或卸载。Effect 只能做两件事：开始同步某些东西，然后停止同步它。如果 Effect 依赖于随时间变化的 props 和 state，这个循环可能会发生多次。  </p>
<p>例如下面的 useEffect  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> serverUrl = <span class="string">'https://localhost:1234'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">ChatRoom</span>(<span class="params">&#123; roomId &#125;</span>) </span>&#123;</span><br><span class="line">  useEffect(() =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> connection = createConnection(serverUrl, roomId);</span><br><span class="line">    connection.connect();</span><br><span class="line">    <span class="keyword">return</span> () =&gt; &#123;</span><br><span class="line">      connection.disconnect();</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;, [roomId]);</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>在 组件挂载 → roomId 发生变化 → 组件卸载，的过程中，useEffect 可能经历下面的生命周期  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ChatRoom &#32452;&#20214;&#30340;&#21464;&#21270;&#36807;&#31243;&#65306;&#10;&#10;ChatRoom &#32452;&#20214;&#25346;&#36733;&#65292;roomId &#35774;&#32622;&#20026; &#34;general&#34;&#10;ChatRoom &#32452;&#20214;&#26356;&#26032;&#65292;roomId &#35774;&#32622;&#20026; &#34;travel&#34;&#10;ChatRoom &#32452;&#20214;&#26356;&#26032;&#65292;roomId &#35774;&#32622;&#20026; &#34;music&#34;&#10;ChatRoom &#32452;&#20214;&#21368;&#36733;&#10;&#10;Effect &#25191;&#34892;&#20102;&#19981;&#21516;&#30340;&#25805;&#20316;&#65306;&#10;Effect &#36830;&#25509;&#21040;&#20102; &#34;general&#34; &#32842;&#22825;&#23460;&#10;Effect &#26029;&#24320;&#20102;&#19982; &#34;general&#34; &#32842;&#22825;&#23460;&#30340;&#36830;&#25509;&#65292;&#24182;&#36830;&#25509;&#21040;&#20102; &#34;travel&#34; &#32842;&#22825;&#23460;&#10;Effect &#26029;&#24320;&#20102;&#19982; &#34;travel&#34; &#32842;&#22825;&#23460;&#30340;&#36830;&#25509;&#65292;&#24182;&#36830;&#25509;&#21040;&#20102; &#34;music&#34; &#32842;&#22825;&#23460;&#10;Effect &#26029;&#24320;&#20102;&#19982; &#34;music&#34; &#32842;&#22825;&#23460;&#30340;&#36830;&#25509;</span><br></pre></td></tr></table></figure>
<h2 id="1-4_hooks"><a href="#1-4_hooks" class="headerlink" title="1.4 hooks"></a>1.4 hooks</h2><h3 id="1-4-1__u73B0_u6709_hooks"><a href="#1-4-1__u73B0_u6709_hooks" class="headerlink" title="1.4.1 现有 hooks"></a>1.4.1 现有 hooks</h3><p>参考文档：<a href="https://react.dev/reference/react/hooks" target="_blank" rel="external">https://react.dev/reference/react/hooks</a><br>目前 react 提供了内置的 useState、useContext、useRef、useEffect 等常用 hook，以及</p>
<ul>
<li>性能优化相关hook： useMemo、useCallback、useTransition、useDeferredValue</li>
<li>资源相关 hook：use</li>
<li>其它 hook：useDebugValue、useId、useSyncExternalStore<h3 id="1-4-2__u81EA_u5B9A_u4E49_hooks"><a href="#1-4-2__u81EA_u5B9A_u4E49_hooks" class="headerlink" title="1.4.2 自定义 hooks"></a>1.4.2 自定义 hooks</h3>像组件有一个命名规范一样，自定义 hooks 也有要遵守的命名规范。</li>
<li>React 组件名称必须以大写字母开头，比如 StatusBar 和 SaveButton。React 组件还需要返回一些 React 能够显示的内容，比如一段 JSX。</li>
<li>Hook 的名称必须以 use 开头，然后紧跟一个大写字母，如内置的 useState 或者自定义的 useOnlineStatus 。Hook 可以返回任意值。<br>需要注意的是，自定义 Hook 共享的是状态逻辑，而不是状态本身。<br>参考官方文档 <a href="https://react.dev/learn/reusing-logic-with-custom-hooks#custom-hooks-let-you-share-stateful-logic-not-state-itself" target="_blank" rel="external">https://react.dev/learn/reusing-logic-with-custom-hooks#custom-hooks-let-you-share-stateful-logic-not-state-itself</a> 中的示例。  </li>
</ul>
<p>使用自定义 hook 的方式：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">StatusBar</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> isOnline = useOnlineStatus();</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SaveButton</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> isOnline = useOnlineStatus();</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>可以看做是和在组件中各自写重复逻辑代码一样，两个组件中的状态是私有独立，互不干扰的。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">StatusBar</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [isOnline, setIsOnline] = useState(<span class="literal">true</span>);</span><br><span class="line">  useEffect(() =&gt; &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;, []);</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SaveButton</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [isOnline, setIsOnline] = useState(<span class="literal">true</span>);</span><br><span class="line">  useEffect(() =&gt; &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;, []);</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<h2 id="1-5__u5176_u5B83_u80FD_u529B"><a href="#1-5__u5176_u5B83_u80FD_u529B" class="headerlink" title="1.5 其它能力"></a>1.5 其它能力</h2><h3 id="1-5-1__u8BB0_u5FC6_u5316"><a href="#1-5-1__u8BB0_u5FC6_u5316" class="headerlink" title="1.5.1 记忆化"></a>1.5.1 记忆化</h3><p>参考：<a href="http://react.dev/reference/react/useMemo、http://react.dev/reference/react/useCallback" target="_blank" rel="external">http://react.dev/reference/react/useMemo、http://react.dev/reference/react/useCallback</a><br>可以将数据、组件、函数缓存到 react 中，当依赖项没有发生变化时，整个生命周期中数据或组件或函数不再重新生成。  </p>
<p>① 使用 useMemo 缓存数据 </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useMemo &#125; <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">TodoList</span>(<span class="params">&#123; todos, tab &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> visibleTodos = useMemo(</span><br><span class="line">    () =&gt; filterTodos(todos, tab),</span><br><span class="line">    [todos, tab]</span><br><span class="line">  );</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>当依赖的 todos，tab 未发生变化时，TodoList 的多次重新渲染不会导致 filterTodos 的重新计算。<br>适用场景：数据计算量比较大时，缓存数据  </p>
<p>② 使用 useCallback / useMemo 缓存函数  </p>
<p>使用 useMemo 的写法  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">Page</span>(<span class="params">&#123; productId, referrer &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> handleSubmit = useMemo(() =&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> (orderDetails) =&gt; &#123;</span><br><span class="line">      post(<span class="string">'/product/'</span> + productId + <span class="string">'/buy'</span>, &#123;</span><br><span class="line">        referrer,</span><br><span class="line">        orderDetails</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;, [productId, referrer]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="title">Form</span> <span class="attribute">onSubmit</span>=<span class="value">&#123;handleSubmit&#125;</span> /&gt;</span>;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>使用 useCallback 的写法，和上面的完全一样，只是为了简化写法  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">Page</span>(<span class="params">&#123; productId, referrer &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> handleSubmit = useCallback((orderDetails) =&gt; &#123;</span><br><span class="line">    post(<span class="string">'/product/'</span> + productId + <span class="string">'/buy'</span>, &#123;</span><br><span class="line">      referrer,</span><br><span class="line">      orderDetails</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;, [productId, referrer]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="title">Form</span> <span class="attribute">onSubmit</span>=<span class="value">&#123;handleSubmit&#125;</span> /&gt;</span>;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>③ 使用 memo 缓存组件  </p>
<p>默认情况下，当一个组件重新渲染时，React 会递归地重新渲染它的所有子组件。可以通过将它包装在 memo 中，这样当它的 props 跟上一次渲染相同的时候它就会跳过本次渲染。    </p>
<p>不使用 memo 的情况  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">TodoList</span>(<span class="params">&#123; todos, tab, theme &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// 每当主题发生变化时，这将是一个不同的数组……</span></span><br><span class="line">  <span class="keyword">const</span> visibleTodos = filterTodos(todos, tab);</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="title">div</span> <span class="attribute">className</span>=<span class="value">&#123;theme&#125;</span>&gt;</span></span><br><span class="line">      &#123;/* ... 所以List的props永远不会一样，每次都会重新渲染 */&#125;</span><br><span class="line">      <span class="tag">&lt;<span class="title">List</span> <span class="attribute">items</span>=<span class="value">&#123;visibleTodos&#125;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br><span class="line">  )</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>使用 memo 的情况  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">TodoList</span>(<span class="params">&#123; todos, tab, theme &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="comment">// 告诉 React 在重新渲染之间缓存你的计算结果...</span></span><br><span class="line">  <span class="keyword">const</span> visibleTodos = useMemo(</span><br><span class="line">    () =&gt; filterTodos(todos, tab),</span><br><span class="line">    [todos, tab] <span class="comment">// ...所以只要这些依赖项不变...</span></span><br><span class="line">  );</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="xml"><span class="tag">&lt;<span class="title">div</span> <span class="attribute">className</span>=<span class="value">&#123;theme&#125;</span>&gt;</span></span><br><span class="line">      &#123;/* ... List 也就会接受到相同的 props 并且会跳过重新渲染 */&#125;</span><br><span class="line">      <span class="tag">&lt;<span class="title">List</span> <span class="attribute">items</span>=<span class="value">&#123;visibleTodos&#125;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br><span class="line">  )</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>总结：记忆化只要适用于下面的场景  </p>
<ul>
<li>跳过代价昂贵的数据重新计算：useMemo(数据, [依赖])</li>
<li>跳过组件的重新渲染：memo(组件, [依赖])</li>
<li>记忆另一个 hook 的依赖，防止重复触发：useXXX(xxx, [ useMemo(yyy) ])</li>
<li>记忆一个函数：useMemo(  () =&gt; fn, [依赖] ) / useCallback(fn, [依赖])</li>
</ul>
<h3 id="1-5-2_ref"><a href="#1-5-2_ref" class="headerlink" title="1.5.2 ref"></a>1.5.2 ref</h3><p>当你希望组件“记住”某些信息，但又不想让这些信息 触发新的渲染 时，你可以使用 ref 。  </p>
<p>① 什么是 ref  </p>
<p>ref 可以看做是一个不支持 setState 的状态对象：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// React 内部</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useRef</span>(<span class="params">initialValue</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [ref, unused] = useState(&#123; current: initialValue &#125;);</span><br><span class="line">  <span class="keyword">return</span> ref;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>从这里可以看出，ref 对象是一个不可变对象，通过 Object.js 判断两次渲染间的 ref 变量，结果永远是 true。</p>
<p>② 使用 ref 来缓存 DOM  </p>
<p>当你将 ref 放在像 <code>&lt;input /&gt;</code> 这样输出浏览器元素的内置组件上时，React 会将该 ref 的 current 属性设置为相应的 DOM 节点（例如浏览器中实际的 <code>&lt;input /&gt;</code> ）。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useRef &#125; <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">const</span> myRef = useRef(<span class="literal">null</span>);</span><br><span class="line"><span class="xml"><span class="tag">&lt;<span class="title">div</span> <span class="attribute">ref</span>=<span class="value">&#123;myRef&#125;</span>&gt;</span></span></span><br></pre></td></tr></table></figure></p>
<p>③ 允许 JSX 组件接收 ref  </p>
<p>如前面所说，只有浏览器元素的内置组件才支持通过 ref 属性设置 dom，如果想要 JSX 组件也能接收 ref，可以使用 forwardRef 将组件进行包装。<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> MyInput = forwardRef((props, ref) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="title">input</span> &#123;<span class="attribute">...props</span>&#125; <span class="attribute">ref</span>=<span class="value">&#123;ref&#125;</span> /&gt;</span>;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></p>
<p>④ ref 只暴露 dom 的部分能力  </p>
<p>可以使用 useImperativeHandle 只暴露 DOM 的一部分能力，而不是全部  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> MyInput = forwardRef((props, ref) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> realInputRef = useRef(<span class="literal">null</span>);</span><br><span class="line">  useImperativeHandle(ref, () =&gt; (&#123;</span><br><span class="line">    <span class="comment">// 只暴露 focus，没有别的</span></span><br><span class="line">    focus() &#123;</span><br><span class="line">      realInputRef.current.focus();</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;));</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="title">input</span> &#123;<span class="attribute">...props</span>&#125; <span class="attribute">ref</span>=<span class="value">&#123;realInputRef&#125;</span> /&gt;</span>;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<h3 id="1-5-3__u61D2_u52A0_u8F7D_lazy"><a href="#1-5-3__u61D2_u52A0_u8F7D_lazy" class="headerlink" title="1.5.3 懒加载 lazy"></a>1.5.3 懒加载 lazy</h3><p>参考：<a href="http://react.dev/reference/react/lazy" target="_blank" rel="external">http://react.dev/reference/react/lazy</a>  </p>
<p>react 的懒加载机制：  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; lazy &#125; <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> MarkdownPreview = lazy(() =&gt; <span class="keyword">import</span>(<span class="string">'./MarkdownPreview.js'</span>));</span><br><span class="line">&lt;Suspense fallback=&#123;<span class="xml"><span class="tag">&lt;<span class="title">Loading</span> /&gt;</span>&#125;&gt;</span><br><span class="line">  <span class="tag">&lt;<span class="title">h2</span>&gt;</span>Preview<span class="tag">&lt;/<span class="title">h2</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="title">MarkdownPreview</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="title">Suspense</span>&gt;</span></span></span><br></pre></td></tr></table></figure>
<p>实现了懒加载模块，以及加载中时的替代组件展示。  </p>
<h3 id="1-5-4_useSyncExternalStore__u8BA2_u9605_u5916_u90E8_u6570_u636E"><a href="#1-5-4_useSyncExternalStore__u8BA2_u9605_u5916_u90E8_u6570_u636E" class="headerlink" title="1.5.4 useSyncExternalStore 订阅外部数据"></a>1.5.4 useSyncExternalStore 订阅外部数据</h3><p>当一个 react 组件想订阅外部来源的数据时，可以用 useEffect 的方式  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useOnlineStatus</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// 不理想：在 Effect 中手动订阅 store</span></span><br><span class="line">  <span class="keyword">const</span> [isOnline, setIsOnline] = useState(<span class="literal">true</span>);</span><br><span class="line">  useEffect(() =&gt; &#123;</span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">updateState</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">      setIsOnline(navigator.onLine);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    updateState();</span><br><span class="line"></span><br><span class="line">    <span class="built_in">window</span>.addEventListener(<span class="string">'online'</span>, updateState);</span><br><span class="line">    <span class="built_in">window</span>.addEventListener(<span class="string">'offline'</span>, updateState);</span><br><span class="line">    <span class="keyword">return</span> () =&gt; &#123;</span><br><span class="line">      <span class="built_in">window</span>.removeEventListener(<span class="string">'online'</span>, updateState);</span><br><span class="line">      <span class="built_in">window</span>.removeEventListener(<span class="string">'offline'</span>, updateState);</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;, []);</span><br><span class="line">  <span class="keyword">return</span> isOnline;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">ChatIndicator</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> isOnline = useOnlineStatus();</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>可以使用 useSyncExternalStore 来实现上面的逻辑  </p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">subscribe</span>(<span class="params">callback</span>) </span>&#123;</span><br><span class="line">  <span class="built_in">window</span>.addEventListener(<span class="string">'online'</span>, callback);</span><br><span class="line">  <span class="built_in">window</span>.addEventListener(<span class="string">'offline'</span>, callback);</span><br><span class="line">  <span class="keyword">return</span> () =&gt; &#123;</span><br><span class="line">    <span class="built_in">window</span>.removeEventListener(<span class="string">'online'</span>, callback);</span><br><span class="line">    <span class="built_in">window</span>.removeEventListener(<span class="string">'offline'</span>, callback);</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">useOnlineStatus</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// ✅ 非常好：用内置的 Hook 订阅外部 store</span></span><br><span class="line">  <span class="keyword">return</span> useSyncExternalStore(</span><br><span class="line">    subscribe, <span class="comment">// 只要传递的是同一个函数，React 不会重新订阅</span></span><br><span class="line">    () =&gt; navigator.onLine, <span class="comment">// 如何在客户端获取值</span></span><br><span class="line">    () =&gt; <span class="literal">true</span> <span class="comment">// 如何在服务端获取值</span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">ChatIndicator</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> isOnline = useOnlineStatus();</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="1-5-5__u5E76_u53D1_u4EFB_u52A1"><a href="#1-5-5__u5E76_u53D1_u4EFB_u52A1" class="headerlink" title="1.5.5 并发任务"></a>1.5.5 并发任务</h3><p>在 v18 版本中， react 提供了 useDeferredValue、 startTransition 来执行并发渲染的操作，以提高复杂渲染过程的用户体验，其原理是利用渲染的空闲时间来执行复杂的渲染操作，后面会详细描述。  </p>
<h2 id="1-6__u5916_u90E8_u72B6_u6001_u7BA1_u7406_u5E93"><a href="#1-6__u5916_u90E8_u72B6_u6001_u7BA1_u7406_u5E93" class="headerlink" title="1.6 外部状态管理库"></a>1.6 外部状态管理库</h2><p>前面描述了 react 内置的状态管理能力，而在复杂的实际项目中，有一些问题需要解决，因而业界诞生了非常多的状态管理库，来优化 react 项目中的状态管理。他们主要解决下面的问题：  </p>
<ul>
<li>组件间状态共享：在大型应用中，有时候需要在多个组件之间共享状态，使用外部状态管理库可以方便地在组件间共享状态，而不需要通过层层传递 props。</li>
<li>状态管理复杂度：随着应用规模的增长，状态管理变得越来越复杂。使用外部状态管理库可以将状态管理与组件逻辑分离，使得代码更加清晰、易于维护。</li>
<li>时间旅行调试：一些外部状态管理库提供了时间旅行功能，可以方便地回溯状态变化过程，帮助开发者更快地定位问题。</li>
<li>中间件支持：外部状态管理库可以支持中间件，使得开发者可以方便地扩展状态管理功能，例如异步操作、日志记录等。</li>
</ul>
<p>一些常见的外部状态管理库：  </p>
<table>
<thead>
<tr>
<th>类型</th>
<th>库</th>
<th>描述</th>
<th>缺点</th>
</tr>
</thead>
<tbody>
<tr>
<td>单向数据流</td>
<td>Redux</td>
<td>一个基于 Flux 架构的状态管理库，通过单一数据源和纯函数（reducer）来管理状态。它解决了状态共享、状态管理复杂度、时间旅行调试等问题。常用的中间件有 redux-thunk、redux-saga、redux-logger 等</td>
<td>学习曲线较高，需要理解一定的概念（如 action、reducer、store 等）。代码冗余，需要编写大量样板代码。对于一些简单的状态管理场景，使用 Redux 可能会显得过于繁琐。</td>
</tr>
<tr>
<td>单向数据流</td>
<td>Zustand</td>
<td>一个轻量级的状态管理库，通过 hooks API 来管理状态。它解决了状态共享、状态管理复杂度等问题，同时具有较低的学习成本。</td>
<td>相对较小，功能可能不如 Redux 和 MobX 丰富。社区和生态相对较小，可能在一些特定场景下缺乏支持。</td>
</tr>
<tr>
<td>响应式</td>
<td>MobX</td>
<td>一个基于观察者模式的状态管理库，通过可观察对象（observable）和自动追踪（autorun）来管理状态。它解决了状态共享、状态管理复杂度等问题，相较于 Redux，它的学习曲线更低，代码更简洁。</td>
<td>隐式依赖，由于使用观察者模式，可能导致难以追踪的数据流和副作用。不支持时间旅行调试，与 Redux 相比，调试功能较弱。</td>
</tr>
<tr>
<td>原子状态</td>
<td>Recoil</td>
<td>Facebook 开源的一款状态管理库，它使用原子（atoms）和选择器（selectors）来管理状态。Recoil解决了组件间状态共享、状态管理复杂度等问题，同时与 React 更紧密地集成。</td>
<td>相对较新，社区和生态尚未完全成熟。</td>
</tr>
<tr>
<td>Hooks</td>
<td>hox</td>
<td>一个基于 React Hooks 的轻量级状态管理库，它将状态和操作封装在自定义 Hook 中，实现了状态共享和逻辑复用。</td>
<td>相对较新，社区和生态相对较小；对于大型应用的状态管理需求，可能不如 Redux 和 MobX。</td>
</tr>
</tbody>
</table>
<h1 id="2-__u6846_u67B6_u539F_u7406_u5206_u6790"><a href="#2-__u6846_u67B6_u539F_u7406_u5206_u6790" class="headerlink" title="2. 框架原理分析"></a>2. 框架原理分析</h1><p>日常编码，我们参考 react 官方文档，已经可以写出足够好的代码了。如果想要更深入的了解，则需要对 react 的实现原理进行分析。  </p>
<p>这部分主内容主要来源于 <a href="https://github.com/7kms/react-illustration-series" target="_blank" rel="external">https://github.com/7kms/react-illustration-series</a> 图解 react 核心逻辑，按应用启动、渲染流程、任务管理、用户接口（状态和副作用）、hook 的顺序进行描述。  </p>
<h2 id="2-1__u542F_u52A8_u6D41_u7A0B"><a href="#2-1__u542F_u52A8_u6D41_u7A0B" class="headerlink" title="2.1 启动流程"></a>2.1 启动流程</h2><p>react 有三种启动模式：</p>
<ul>
<li>legacy 模式，是 react17 版本中的默认启动模式，只能进入同步工作循环，无法使用可中断渲染特性。启动方式：ReactDOM.render(<app>, rootNode)</app></li>
<li>Blocking 模式，是一个过渡版本，实现了部分 concurrent 模式的特性。启动方式：ReactDOM.createBlockingRoot(rootNode).render(<app>)</app></li>
<li>Concurrent 模式，支持时间分片、可中断渲染的模式。启动方式：ReactDOM.createRoot(rootNode).render(<app>)</app></li>
</ul>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/57d9661686c0b12f573ca68665ca585f.jpg" alt="image.png"></p>
<p>上面的三种启动方式都是从 react-dom 包发起，调用 react-reconciler 包。整个启动的过程概述：  </p>
<ul>
<li>【react-dom】入口 render/createRoot/createBlockingRoot </li>
<li>【react-dom】创建 ReactDOMRoot / ReactDOMBlockingRoot 对象，提供 render / umount 方法</li>
<li>【react-reconciler】创建 fiberRoot 对象，保存 fiber 构建过程中依赖的全局状态，保存为 ReactDOMRoot 对象的 _internalRoot 属性</li>
<li>创建 HostRootFiber 对象， react 的首个 fiber 对象，作为 fiberRoot 的 current 属性</li>
</ul>
<p>三种模式的初始化后对象引用关系：  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/50cb34e2fcc1c18c37be2229d2fbe648.jpg" alt="image.png">  </p>
<h2 id="2-2__u6E32_u67D3_u6D41_u7A0B"><a href="#2-2__u6E32_u67D3_u6D41_u7A0B" class="headerlink" title="2.2 渲染流程"></a>2.2 渲染流程</h2><p>应用启动完成后，react 即进入 react-reconciler 包调用 updateContainer 函数，执行 fiber 树的构造工作，并最终将 JSX 转换为真实 DOM 节点，渲染出用户界面。  </p>
<p>函数调用栈大致是</p>
<ul>
<li>updateContainer → scheduleUpdateOnFiber → performSyncWorkOnRoot（legacy模式 &amp;&amp; 首次创建）</li>
<li>updateContainer → scheduleUpdateOnFiber → ensureRootIsScheduled（concurrent 或非首次启动）→ 回调 →  performSyncWorkOnRoot / performConcurrentWorkOnRoot</li>
</ul>
<h3 id="fiber__u6811_u6784_u9020"><a href="#fiber__u6811_u6784_u9020" class="headerlink" title="fiber 树构造"></a>fiber 树构造</h3><ul>
<li>步骤一：所有采用jsx语法书写的节点, 都会被编译器转换, 最终会以React.createElement(…)的方式, 创建出来一个与之对应的ReactElement对象</li>
<li>步骤二：通过ReactElement对象创建对应的fiber对象, 多个fiber对象构成了一棵fiber树</li>
<li>步骤三：以fiber树 为数据模型构造最终的 DOM 树，触发最终的 UI 渲染</li>
</ul>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/09c713dfd0027cf46d9009cd23febdd8.jpg" alt="image.png"></p>
<p>ReactElement 树和 fiber 树的构造过程都是一个深度优先遍历的过程，这里只引用 ReactElement 树的构造构成，如下  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/fae8a3a89e6326367adccc4bb962082e.jpg" alt="image.png"></p>
<h3 id="fiber__u6811_u66F4_u65B0"><a href="#fiber__u6811_u66F4_u65B0" class="headerlink" title="fiber 树更新"></a>fiber 树更新</h3><p>有 3 种常见方式可以主动触发 fiber 树的重新构造：  </p>
<ul>
<li>Class组件中调用setState</li>
<li>Function组件中调用hook对象暴露出的dispatchAction（使用 useState ）</li>
<li>在container节点上重复调用render</li>
</ul>
<p>fiber树的构造过程, 就是把ReactElement转换成fiber树的过程. 在这个过程中, 内存里会同时存在 2 棵fiber树:  </p>
<ul>
<li>其一: 代表当前界面的fiber树(已经被展示出来, 挂载到fiberRoot.current上). 如果是初次构造(初始化渲染), 页面还没有渲染, 此时界面对应的 fiber 树为空(fiberRoot.current = null).</li>
<li>其二: 正在构造的fiber树(即将展示出来, 挂载到HostRootFiber.alternate上, 正在构造的节点称为workInProgress). 当构造完成之后, 重新渲染页面, 最后切换fiberRoot.current = workInProgress, 使得fiberRoot.current重新指向代表当前界面的fiber树<br>构造过程中，当前界面的 HostRootFiber 的 alternate 属性指向更新中的 Fiber 树实例  </li>
</ul>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/68be5735d9aeae218083e1920c0bc504.jpg" alt="image.png"></p>
<p>构造完成后，切换 FiberRoot 的 current，指向更新完成的 HostRootFiber  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/efb952ab48034be7785f11018f80eeb7.jpg" alt="image.png">  </p>
<p>diff 算法：更新 fiber 树的过程中，将旧的 fiber 对象与新的 ReactElement 对象相比较，给需要新增,移动,和删除的节点设置相应的 flag。  </p>
<p>单节点比较：</p>
<ul>
<li>如果是新增节点, 直接新建 fiber, 没有多余的逻辑</li>
<li>对比节点，如果key和type都相同(即: ReactElement.key === Fiber.key 且 Fiber.elementType === ReactElement.type), 则复用，否则新建  </li>
</ul>
<p>可迭代节点比较（如数组类型等）：  </p>
<ul>
<li>第一次循环: 遍历最长公共序列(key 相同), 公共序列的节点都视为可复用<ul>
<li>如果newChildren序列被遍历完, 那么oldFiber序列中剩余节点都视为删除(打上Deletion标记)</li>
<li>如果oldFiber序列被遍历完, 那么newChildren序列中剩余节点都视为新增(打上Placement标记)</li>
</ul>
</li>
<li>第二次循环: 遍历剩余非公共序列, 优先复用 oldFiber 序列中的节点<ul>
<li>在对比更新阶段(非初次创建fiber, 此时shouldTrackSideEffects被设置为 true). 第二次循环遍历完成之后, oldFiber序列中没有匹配上的节点都视为删除(打上Deletion标记)</li>
</ul>
</li>
</ul>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/03757285e4dbd564b1537c5612eae32f.jpg" alt="image.png"></p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/73b31c08565bd5fd3507e420551bd150.jpg" alt="image.png"></p>
<h3 id="fiber__u6811_u6E32_u67D3"><a href="#fiber__u6811_u6E32_u67D3" class="headerlink" title="fiber 树渲染"></a>fiber 树渲染</h3><p>fiber 树的整个渲染逻辑都在commitRoot 函数中，渲染上屏过程大致做了下面的一些步骤。  </p>
<ul>
<li>commitBeforeMutationEffects dom 变更之前, 处理副作用队列中带有Snapshot,Passive标记的fiber节点<ul>
<li>处理Snapshot标记</li>
<li>处理Passive标记</li>
</ul>
</li>
<li>commitMutationEffects dom 变更, 界面得到更新. 处理副作用队列中带有Placement, Update, Deletion, Hydrating标记的fiber节点，最终调用appendChild, commitUpdate, removeChild这些react-dom包中的函数. 它们是HostConfig协议(源码在 ReactDOMHostConfig.js 中)中规定的标准函数, 调用后界面会得到更新。<ul>
<li>新增DOM: 函数调用栈 commitPlacement -&gt; insertOrAppendPlacementNode -&gt; appendChild</li>
<li>更新DOM: 函数调用栈 commitWork -&gt; commitUpdate</li>
<li>删除DOM: 函数调用栈 commitDeletion -&gt; removeChild</li>
</ul>
</li>
<li>commitLayoutEffectsdom 变更后, 处理副作用队列中带有Update | Callback标记的fiber节点<ul>
<li>对于ClassComponent节点, 调用生命周期函数componentDidMount或componentDidUpdate, 调用update.callback回调函数.</li>
<li>对于HostComponent节点, 如有Update标记, 需要设置一些原生状态(如: focus等)<br>渲染完成后，还要做一些清理工作</li>
</ul>
</li>
<li>清除副作用队列</li>
<li>检测更新<ul>
<li>在整个渲染过程中, 有可能产生新的update(比如在componentDidMount函数中, 再次调用setState()).</li>
<li>如果是常规(异步)任务, 不用特殊处理, 调用ensureRootIsScheduled确保任务已经注册到调度中心即可.</li>
<li>如果是同步任务, 则主动调用flushSyncCallbackQueue(无需再次等待 scheduler 调度), 再次进入 fiber 树构造循环</li>
</ul>
</li>
</ul>
<h2 id="2-3__u4EFB_u52A1_u7BA1_u7406"><a href="#2-3__u4EFB_u52A1_u7BA1_u7406" class="headerlink" title="2.3 任务管理"></a>2.3 任务管理</h2><p>前面描述的渲染流程，fiber 树更新的整个过程中，涉及到大量的 fiber 树构造、dom生成等工作。react 实现了一套任务执行流程和任务调度机制来高效完成这些工作，具体代码在 react-reconciler、scheduler 包中实现。  </p>
<h3 id="reconciler__u8FD0_u4F5C_u6D41_u7A0B"><a href="#reconciler__u8FD0_u4F5C_u6D41_u7A0B" class="headerlink" title="reconciler 运作流程"></a>reconciler 运作流程</h3><p>react-reconciler包的主要作用, 将主要功能分为 4 个方面:  </p>
<ul>
<li>输入 scheduleUpdateOnFiber: 暴露api函数(如: scheduleUpdateOnFiber), 供给其他包(如react包)调用<ul>
<li>不经过调度, 直接进行fiber构造.</li>
<li>注册调度任务, 经过Scheduler包的调度, 间接进行fiber构造</li>
</ul>
</li>
<li>注册调度任务 ensureRootIsScheduled: 与调度中心(scheduler包)交互, 注册调度任务task, 等待任务回调<ul>
<li>判断是否需要注册新的调度(如果无需新的调度, 会退出函数)</li>
<li>注册调度任务，等待调度中心执行回调 performSyncWorkOnRoot或performConcurrentWorkOnRoot</li>
</ul>
</li>
<li>执行任务回调 performSyncWorkOnRoot / performConcurrentWorkOnRoot: 在内存中构造出fiber树, 同时与与渲染器(react-dom)交互, 在内存中创建出与fiber对应的DOM节点<ul>
<li>fiber 树构造</li>
<li>异常处理: 有可能 fiber 构造过程中出现异常</li>
<li>调用输出</li>
</ul>
</li>
<li>输出 commitRoot: 与渲染器(react-dom)交互, 渲染DOM节点，即前面描述的“fiber 树渲染” 中的工作<ul>
<li>commitBeforeMutationEffects</li>
<li>commitMutationEffects</li>
<li>commitLayoutEffects</li>
</ul>
</li>
</ul>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/2674229d7fa25e353fd0a6183f0de867.jpg" alt="image.png"></p>
<h3 id="scheduler__u8C03_u5EA6_u539F_u7406"><a href="#scheduler__u8C03_u5EA6_u539F_u7406" class="headerlink" title="scheduler 调度原理"></a>scheduler 调度原理</h3><p>reconciler 运作的流程中，很多地方都涉及任务调度工作，任务调度决定了整个 react 应用的运行效率。  </p>
<p>scheduler 模块提供了两类能力。   </p>
<ul>
<li>调度相关: 请求或取消调度<ul>
<li>requestHostCallback  请求调度</li>
<li>cancelHostCallback  取消调度</li>
<li>requestHostTimeout  请求延时调度</li>
<li>cancelHostTimeout  取消延时调度</li>
</ul>
</li>
<li>时间切片(time slicing)相关: 执行时间分割, 让出主线程(把控制权归还浏览器, 浏览器可以处理用户输入, UI 绘制等紧急任务)<ul>
<li>getCurrentTime: 获取当前时间</li>
<li>shouldYieldToHost: 是否让出主线程</li>
<li>requestPaint: 请求绘制</li>
<li>forceFrameRate: 强制设置 yieldInterval(从源码中的引用来看, 算一个保留函数, 其他地方没有用到)</li>
</ul>
</li>
</ul>
<p>requestHostCallback 中，利用 MessageChannel / setTimeout 实现了类似 window.requestIdleCallback 的能力，以支持中断当前任务队列让出CPU的功能。 其中，shouldYieldToHost 让出 CPU 的判定条件是:  </p>
<ul>
<li>currentTime &gt;= deadline: 只有时间超过deadline之后才会让出主线程(其中deadline = currentTime + yieldInterval).<ul>
<li>yieldInterval默认是5ms, 只能通过forceFrameRate函数来修改</li>
<li>如果一个task运行时间超过5ms, 下一个task执行之前, 会把控制权归还浏览器.</li>
</ul>
</li>
<li>navigator.scheduling.isInputPending(): 这 facebook 官方贡献给 Chromium 的 api, 现在已经列入 W3C 标准, 用于判断是否有输入事件(包括: input 框输入事件, 点击事件等)</li>
</ul>
<p>整个调度过程的核心流程：  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/e9b334770b5e2c36d669cb34ebaada61.jpg" alt="image.png">  </p>
<h3 id="scheduler__u4F18_u5148_u7EA7_u548C_u4EFB_u52A1_u961F_u5217"><a href="#scheduler__u4F18_u5148_u7EA7_u548C_u4EFB_u52A1_u961F_u5217" class="headerlink" title="scheduler 优先级和任务队列"></a>scheduler 优先级和任务队列</h3><p>scheduler 内部实现了两种优先级机制。  </p>
<ul>
<li>与fiber构造过程相关的优先级(如fiber.updateQueue,fiber.lanes)都使用LanePriority</li>
<li>与scheduler调度中心相关的优先级使用SchedulerPriority</li>
</ul>
<p>为了能协同调度中心(scheduler包)和 fiber 树构造(react-reconciler包)中对优先级的使用, 则需要转换SchedulerPriority和LanePriority, 通过ReactPriorityLevel进行转换。  </p>
<p>其中 LanePriority 是基于位运算实现的优先级保存和比对:  </p>
<ul>
<li>可以使用的比特位一共有 31 位(js的位运算最高支持到int32，减去一个符号位)</li>
<li>共定义了18 种车道(Lane/Lanes)变量, 每一个变量占有 1 个或多个比特位, 分别定义为Lane和Lanes类型.</li>
<li>每一种车道(Lane/Lanes)都有对应的优先级, 所以源码中定义了 18 种优先级(LanePriority).</li>
<li>占有低位比特位的Lane变量对应的优先级越高<ul>
<li>最高优先级为SyncLanePriority对应的车道为SyncLane = 0b0000000000000000000000000000001.</li>
<li>最低优先级为OffscreenLanePriority对应的车道为OffscreenLane = 0b1000000000000000000000000000000</li>
</ul>
</li>
</ul>
<p>具体执行任务时, scheduler 内部定义了 2 个数组taskQueue和timerQueue, 它们都是按优先级以最小堆的形式进行存储, 这样就能保证以O(1)的时间复杂度, 取到数组顶端的对象(优先级最高的 task)  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/10161a5f7172ce664e460daf4c6c9836.jpg" alt="image.png"></p>
<h3 id="u65F6_u95F4_u5207_u7247_u548C_u53EF_u4E2D_u65AD_u6E32_u67D3"><a href="#u65F6_u95F4_u5207_u7247_u548C_u53EF_u4E2D_u65AD_u6E32_u67D3" class="headerlink" title="时间切片和可中断渲染"></a>时间切片和可中断渲染</h3><p>时间切片原理：  </p>
<p>消费任务队列的过程中, 可以消费1~n个 task, 甚至清空整个 queue. 但是在每一次具体执行task.callback之前都要进行超时检测, 如果超时可以立即退出循环并等待下一次调用。  </p>
<p>可中断渲染原理：  </p>
<p>在时间切片的基础之上, 如果单个task.callback执行时间就很长(假设 200ms). 就需要task.callback自己能够检测是否超时, 所以在 fiber 树构造过程中, 每构造完成一个单元, 都会检测一次超时(源码链接), 如遇超时就退出fiber树构造循环, 并返回一个新的回调函数(就是此处的continuationCallback)并等待下一次回调继续未完成的fiber树构造。  </p>
<p>react18.2中，提供了 useDeferredValue、useTransition 来进行并发渲染操作，其内部就应用了可中断渲染的功能。参考示例：  </p>
<ul>
<li><a href="https://zh-hans.react.dev/reference/react/useTransition" target="_blank" rel="external">https://zh-hans.react.dev/reference/react/useTransition</a></li>
<li><a href="https://zh-hans.react.dev/reference/react/useDeferredValue#examples" target="_blank" rel="external">https://zh-hans.react.dev/reference/react/useDeferredValue#examples</a></li>
</ul>
<p>尝试多段连续输入触发渲染，观察是否使用可中断渲染的表现：  </p>
<p>普通渲染  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/bb5c93c0a4c2e48c70dfa3bee75580de.jpg" alt="image.png">  </p>
<p>可中断渲染  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/c1e4436e7a5cca2fc4fabd320c7024f1.jpg" alt="image.png">  </p>
<h2 id="2-4__u72B6_u6001_u548C_u526F_u4F5C_u7528"><a href="#2-4__u72B6_u6001_u548C_u526F_u4F5C_u7528" class="headerlink" title="2.4 状态和副作用"></a>2.4 状态和副作用</h2><p>一个 fiber 节点中，影响最终渲染结果的属性可以分为状态类属性和副作用类属性。  </p>
<ul>
<li>状态类: 在renderRootSync[Concurrent]阶段, 为子节点提供确定的输入数据, 直接影响子节点的生成</li>
<li>副作用类: 在commitRoot阶段, 如果fiber被标记有副作用, 则副作用相关函数会被(同步/异步)调用</li>
</ul>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> Fiber = &#123;|</span><br><span class="line">  <span class="comment">// 1. fiber节点自身状态相关</span></span><br><span class="line">  pendingProps: <span class="built_in">any</span>,</span><br><span class="line">  memoizedProps: <span class="built_in">any</span>,</span><br><span class="line">  updateQueue: mixed,</span><br><span class="line">  memoizedState: <span class="built_in">any</span>,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. fiber节点副作用(Effect)相关</span></span><br><span class="line">  flags: Flags,</span><br><span class="line">  subtreeFlags: Flags, </span><br><span class="line">  deletions: <span class="built_in">Array</span>&lt;Fiber&gt; | <span class="literal">null</span>,</span><br><span class="line">  nextEffect: Fiber | <span class="literal">null</span>,</span><br><span class="line">  firstEffect: Fiber | <span class="literal">null</span>,</span><br><span class="line">  lastEffect: Fiber | <span class="literal">null</span>,</span><br><span class="line">|&#125;;</span><br></pre></td></tr></table></figure>
<h3 id="u72B6_u6001_u76F8_u5173_u5C5E_u6027"><a href="#u72B6_u6001_u76F8_u5173_u5C5E_u6027" class="headerlink" title="状态相关属性"></a>状态相关属性</h3><ul>
<li>fiber.pendingProps: 输入属性, 从ReactElement对象传入的 props. 它和fiber.memoizedProps比较可以得出属性是否变动.</li>
<li>fiber.memoizedProps: 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中. 向下生成子节点之前叫做pendingProps, 生成子节点之后会把pendingProps赋值给memoizedProps用于下一次比较.pendingProps和memoizedProps比较可以得出属性是否变动.</li>
<li>fiber.updateQueue: 存储update更新对象的队列, 每一次发起更新, 都需要在该队列上创建一个update对象.</li>
<li>fiber.memoizedState: 上一次生成子节点之后保持在内存中的局部状态.</li>
</ul>
<p>它们在fiber树构造阶段, 直接影响子节点的生成。  </p>
<h3 id="u526F_u4F5C_u7528_u76F8_u5173_u5C5E_u6027"><a href="#u526F_u4F5C_u7528_u76F8_u5173_u5C5E_u6027" class="headerlink" title="副作用相关属性"></a>副作用相关属性</h3><ul>
<li>fiber.flags: 标志位, 表明该fiber节点有副作用(在 react-reconciler/src/ReactFiberFlags.js 中定义了28种副作用).</li>
<li>fiber.nextEffect: 单向链表, 指向下一个副作用 fiber节点.</li>
<li>fiber.firstEffect: 单向链表, 指向第一个副作用 fiber 节点.</li>
<li>fiber.lastEffect: 单向链表, 指向最后一个副作用 fiber 节点.</li>
</ul>
<p>副作用是一个动态功能, 由于它的调用时机是在fiber树渲染阶段, 故它拥有更多的能力, 能轻松获取突变前快照, 突变后的DOM节点等. 甚至通过调用api发起新的一轮fiber树构造, 进而改变更多的状态, 引发更多的副作用。  </p>
<p>使用副作用 hook useEffect(function(){}, [])时，其中的函数是异步执行的, 因为它经过了调度中心。  </p>
<h2 id="2-5_hook"><a href="#2-5_hook" class="headerlink" title="2.5 hook"></a>2.5 hook</h2><p>hook 是一串挂载在 fiber 上的链表  </p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> Hook = &#123;|</span><br><span class="line">  memoizedState: <span class="built_in">any</span>, <span class="comment">// 当前状态</span></span><br><span class="line">  baseState: <span class="built_in">any</span>, <span class="comment">// 基状态</span></span><br><span class="line">  baseQueue: Update&lt;<span class="built_in">any</span>, <span class="built_in">any</span>&gt; | <span class="literal">null</span>, <span class="comment">// 基队列</span></span><br><span class="line">  queue: UpdateQueue&lt;<span class="built_in">any</span>, <span class="built_in">any</span>&gt; | <span class="literal">null</span>, <span class="comment">// 更新队列</span></span><br><span class="line">  next: Hook | <span class="literal">null</span>, <span class="comment">// next指针</span></span><br><span class="line">|&#125;;</span><br></pre></td></tr></table></figure>
<p>通过调用 mountWorkInProgressHook 函数，可以创建一个 Hook 对象，将其挂载到 fiber.memoizedState 链表结构上，并返回。  </p>
<p>并且在多次渲染之间，基于双缓冲技术对 hook 链表进行克隆，但是 hook 对象内部的状态和队列仍然共享，以保持状态不丢失。  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/c56e610101e840497c05b902d8e0adf8.jpg" alt="image.png"></p>
<h3 id="u72B6_u6001_hook"><a href="#u72B6_u6001_hook" class="headerlink" title="状态 hook"></a>状态 hook</h3><p>使用状态类 hook，如 useState 或者 useReducer 时，实际上就是调用 mountWorkInProgressHook 创建挂载 hook 到 fiber 上，并返回其内部状态和更新函数。  </p>
<p>创建 hook：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">function mountState&lt;S&gt;(</span><br><span class="line">  initialState: (() =&gt; S) | S,</span><br><span class="line">): [S, Dispatch&lt;BasicStateAction&lt;S&gt;&gt;] &#123;</span><br><span class="line">  // 1. 创建hook</span><br><span class="line">  const hook = mountWorkInProgressHook();</span><br><span class="line">  if (typeof initialState === 'function') &#123;</span><br><span class="line">    initialState = initialState();</span><br><span class="line">  &#125;</span><br><span class="line">  // 2. 初始化hook的属性</span><br><span class="line">  // 2.1 设置 hook.memoizedState/hook.baseState</span><br><span class="line">  // 2.2 设置 hook.queue</span><br><span class="line">  hook.memoizedState = hook.baseState = initialState;</span><br><span class="line">  const queue = (hook.queue = &#123;</span><br><span class="line">    pending: null,</span><br><span class="line">    dispatch: null,</span><br><span class="line">    // queue.lastRenderedReducer是内置函数</span><br><span class="line">    lastRenderedReducer: basicStateReducer,</span><br><span class="line">    lastRenderedState: (initialState: any),</span><br><span class="line">  &#125;);</span><br><span class="line">  // 2.3 设置 hook.dispatch</span><br><span class="line">  const dispatch: Dispatch&lt;BasicStateAction&lt;S&gt;&gt; = (queue.dispatch =</span><br><span class="line">    (dispatchAction.bind(null, currentlyRenderingFiber, queue): any));</span><br><span class="line"></span><br><span class="line">  // 3. 返回[当前状态, dispatch函数]</span><br><span class="line">  return [hook.memoizedState, dispatch];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/3e50c2d7f51143553b99389a5896495b.jpg" alt="image.png"></p>
<p>更新状态时，执行的操作是   </p>
<ul>
<li>创建update对象</li>
<li>将update对象添加到hook.queue.pending环形链表 → 调度更新</li>
<li>调用scheduleUpdateOnFiber, 进入reconciler 运作流程中的输入阶段</li>
</ul>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/721187388b9a8e96b1749fac5bcc868b.jpg" alt="image.png"></p>
<p>最终更新结果  </p>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/799a36cc62b49f72b4ce6f2b6b5a07c0.jpg" alt="image.png"></p>
<h3 id="u526F_u4F5C_u7528_hook"><a href="#u526F_u4F5C_u7528_hook" class="headerlink" title="副作用 hook"></a>副作用 hook</h3><p>创建副作用hook：  </p>
<p>和状态 hook 类似，创建 effect hook 的步骤是下面几步  </p>
<ul>
<li>创建hook</li>
<li>设置workInProgress的副作用标记: flags |= fiberFlags</li>
<li>创建effect(在pushEffect中), 挂载到hook.memoizedState上, 即 hook.memoizedState = effect<br>创建完毕后，fiber、hook、effect 三者的引用关系如下：</li>
</ul>
<p><img src="https://zoucz.com/blogimgs/4ce8eac0-b2bd-11ee-95cb-3556e1632a5e.md/d7028c65faeada51d786fa52c00c2909.jpg" alt="image.png">  </p>
<p>处理副作用 hook 回调：  </p>
<p>在commitRootImpl函数中，处理不同 flag 的副作用 hook 回调  </p>
<ul>
<li>commitBeforeMutationEffects：dom 变更之前，处理副作用队列中带有 Passive 标记的 effect</li>
<li>commitMutationEffects：dom 变更, 界面得到更新</li>
<li>commitLayoutEffects：dom 变更后，处理副作用队列中带有 Layout 标记的 effect</li>
</ul>
]]></content>
    <summary type="html">
    <![CDATA[<h1 id="1-__u7F16_u7801_u8981_u70B9"><a href="#1-__u7F16_u7801_u8981_u70B9" class="headerlink" title="1. 编码要点"></a>1. 编码要点</h1><p>react 中有一些]]>
    </summary>
    
      <category term="react" scheme="https://www.zoucz.com/blog/tags/react/"/>
    
      <category term="前端开发" scheme="https://www.zoucz.com/blog/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
      <category term="前端开发" scheme="https://www.zoucz.com/blog/categories/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[容器资源判断对 MKL / FFmpeg 等软件性能的影响]]></title>
    <link href="https://www.zoucz.com/blog/2024/01/03/02f7de00-aa23-11ee-95cb-3556e1632a5e/"/>
    <id>https://www.zoucz.com/blog/2024/01/03/02f7de00-aa23-11ee-95cb-3556e1632a5e/</id>
    <published>2024-01-03T10:29:44.000Z</published>
    <updated>2024-01-03T18:41:00.000Z</updated>
    <content type="html"><![CDATA[<h1 id="1-_u80CC_u666F"><a href="#1-_u80CC_u666F" class="headerlink" title="1.背景"></a>1.背景</h1><p>有时候我们很难精准的评估一份程序在运行时的性能表现。  </p>
<p>要分析问题得出结论，往往需要控制变量，而性能问题的变量往往会非常多。  </p>
<ul>
<li>同一程序在不同主频/架构的CPU上或者使用不同指令集运行，CPU占用可能相差很大  </li>
<li>同一程序在同一运行环境下，使用不同的输入数据，CPU占用、内存占用相差都可能很大</li>
<li>同一程序在同一运行环境下，使用相同的输入数据，程序自身使用不同的启动选项，CPU占用、内存占用相差都可能很大</li>
</ul>
<p>这些变量在某些比较复杂的工具软件上尤为明显。官方给出的参考 benchmark，往往只是一个或几个主流平台和主流使用场景下的数据。  </p>
<p>实际项目中会遇到很多不一样的场景，当我们自己使用时的性能数据和官方性能数据有出入时，有可能会下意识的认为这就是前面提到的变量差异带来的，正常的性能差异。   </p>
<p>本文记录一下容器环境逻辑核数判断对一些程序性能的影响。造成这种影响的原因是非常简单的，但是可能具有一定普遍性，且很容易因为前面所述的性能问题复杂性而被忽略，所以单独拿出来说一说。</p>
<h1 id="2-_u5BB9_u5668_u73AF_u5883_u903B_u8F91_u6838_u8D44_u6E90_u5224_u65AD_u95EE_u9898"><a href="#2-_u5BB9_u5668_u73AF_u5883_u903B_u8F91_u6838_u8D44_u6E90_u5224_u65AD_u95EE_u9898" class="headerlink" title="2.容器环境逻辑核资源判断问题"></a>2.容器环境逻辑核资源判断问题</h1><p>容器化部署的时代，很多场景下，我们的应用不再是直接运行在物理机/虚拟机上，而容器中通过 <code>/proc/cpuinfo</code> 获取逻辑核数，或者通过  </p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"><span class="function"><span class="keyword">long</span> <span class="title">sysconf</span><span class="params">(<span class="keyword">int</span> name)</span></span>; <span class="comment">// _SC_NPROCESSORS_ONLN</span></span><br><span class="line">...</span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/sysinfo.h&gt;</span></span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">get_nprocs</span><span class="params">(<span class="keyword">void</span>)</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">get_nprocs_conf</span><span class="params">(<span class="keyword">void</span>)</span></span>;</span><br><span class="line">...</span><br></pre></td></tr></table></figure>
<p>这些 posix 接口函数来获取逻辑核数，返回的都是宿主机的核数。  </p>
<p>有一些软件/库中，会内置一种自动配置选项的操作：获取默认的逻辑核数，然后根据逻辑核数初始化最佳的工作线程数。  </p>
<p>这种操作在非容器时代，能让程序使用代价降低，在内部自动做好最佳性能选项配置。然而在容器部署的时代，就可能因为错用了宿主机的核数，导致了大量的线程切换开销、多余缓存数据、锁竞争等问题，出现反向优化的情况。</p>
<h1 id="3-_u903B_u8F91_u6838_u6570_u5224_u65AD_u95EE_u9898_u5BF9_u6027_u80FD_u7684_u5F71_u54CD"><a href="#3-_u903B_u8F91_u6838_u6570_u5224_u65AD_u95EE_u9898_u5BF9_u6027_u80FD_u7684_u5F71_u54CD" class="headerlink" title="3.逻辑核数判断问题对性能的影响"></a>3.逻辑核数判断问题对性能的影响</h1><h2 id="3-1_MKL__u8BA1_u7B97_u7EBF_u7A0B_u6570"><a href="#3-1_MKL__u8BA1_u7B97_u7EBF_u7A0B_u6570" class="headerlink" title="3.1 MKL 计算线程数"></a>3.1 MKL 计算线程数</h2><h3 id="u73B0_u8C61"><a href="#u73B0_u8C61" class="headerlink" title="现象"></a>现象</h3><p>服务中在使用 tensorflow / libtorch 等库做深度学习模型的前向推理运算时，若使用 CPU 模式，则它们的底层都会使用 MKL 库来做矩阵运算。  </p>
<p>MKL库内置了一个逻辑，会根据运行环境的逻辑核数自动设置其内部 OpenMP 的计算线程数。若容器被分配了 16 核，实际宿主机 80 核，那么按 MKL 的原本设计思路，应该默认设置为 16 核，能达到最佳性能表现，实际上给设置为了 80 线程，导致计算性能反而下降。  </p>
<p>当设置不当时，带来的影响：  </p>
<ul>
<li>压满 CPU 时，前向推理运算最大并发数比实际最佳性能要低很多</li>
<li>单次推理任务耗时比实际最佳性能长很多</li>
</ul>
<p>由于不同类型的模型推理表现本来就存在很大的性能差异，且 CPU 推理并发性能也好，推理速度也好，确实比 GPU 模式都要差很多，所以很容易认为这就是 CPU 下的真实表现，而漏掉问题。</p>
<h3 id="u89E3_u6CD5"><a href="#u89E3_u6CD5" class="headerlink" title="解法"></a>解法</h3><p>在容器中运行时，不再让 MKL 通过自动检测的逻辑核数自动设置线程数，而是主动  </p>
<ul>
<li>通过 MKL_NUM_THREADS 环境变量设置 MKL 线程数</li>
<li>通过 mkl-set-num-threads 方法设置 MKL 线程数</li>
</ul>
<p>参考文档：  <a href="https://www.intel.com/content/www/us/en/docs/onemkl/developer-reference-c/2024-0/mkl-set-num-threads.html" target="_blank" rel="external">https://www.intel.com/content/www/us/en/docs/onemkl/developer-reference-c/2024-0/mkl-set-num-threads.html</a></p>
<h2 id="3-2_ffmpeg__u5DE5_u4F5C_u7EBF_u7A0B_u6570"><a href="#3-2_ffmpeg__u5DE5_u4F5C_u7EBF_u7A0B_u6570" class="headerlink" title="3.2 ffmpeg 工作线程数"></a>3.2 ffmpeg 工作线程数</h2><h3 id="u73B0_u8C61-1"><a href="#u73B0_u8C61-1" class="headerlink" title="现象"></a>现象</h3><p>ffmpeg 在做视频编码时，默认会自动根据运行环境的逻辑核数，设置 <code>-thread</code> 参数所指定的工作线程数。  </p>
<p>当设置不当时，带来的影响：  </p>
<ul>
<li>慢速编码（输入帧较慢）时，ffmpeg进程内存占用过大。</li>
<li>正常速度编码时（25fps），CPU 占用偏大</li>
</ul>
<p>由于 ffmpeg 编码输出视频时，输入数据规格、输出数据规格、各种选项参数非常繁杂，没有完全控制变量对比的情况下，一时间有点不好察觉到性能上的不对劲。  </p>
<p>例如我这边遇到的一个场景下：  </p>
<ul>
<li>40逻辑核机器，容器分配16核，做4K视频的编码输出，每 1.3s 输入一帧数据。ffmpeg 进程工作线程数 122，内存占用 5.1G；修改 -threads 参数为 8 后，ffmpeg 进程工作线程数 22，内存占用 1.6G，编码速度不变。</li>
<li>80逻辑核机器，容器分配4核，做2K视频的编码输出，每秒输入 25 帧数据。ffmpeg 进程工作线程数 200+，CPU占用4.5 核；修改 -threads 参数为 4 后，ffmpeg 进程工作线程数 10，CPU占用 4 核，编码速度不变。</li>
</ul>
<h3 id="u89E3_u6CD5-1"><a href="#u89E3_u6CD5-1" class="headerlink" title="解法"></a>解法</h3><p>手动根据实际情况设置工作线程数。  </p>
<h2 id="3-3__u666E_u904D_u6027"><a href="#3-3__u666E_u904D_u6027" class="headerlink" title="3.3 普遍性"></a>3.3 普遍性</h2><p>除了上面列举的两个之外，很多软件会根据运行环境的逻辑核心数自动配置选项以优化多处理器下的性能，如 Nginx、Apache HTTP Server、Mysql、Golang 等。  </p>
<p>它们利用系统的逻辑核数来对内部的一些选项进行默认配置，在容器中运行时往往不但达不到最优性，反而会起一些反作用，但是又不会影响程序的正常运行，性能也不会下降到不可用的地步，因而不太容易发现。  </p>
<p>所以我推测这种情况具有一定普遍性，值得注意。  </p>
<h1 id="4-__u5BB9_u5668_u4E2D_u6B63_u786E_u5224_u65AD_u8D44_u6E90"><a href="#4-__u5BB9_u5668_u4E2D_u6B63_u786E_u5224_u65AD_u8D44_u6E90" class="headerlink" title="4. 容器中正确判断资源"></a>4. 容器中正确判断资源</h1><p>可以使用 lxcfs 获取真实资源配置  </p>
<p>此文不再赘述，参考此文：<a href="https://cloud.tencent.com/developer/article/1807333" target="_blank" rel="external">https://cloud.tencent.com/developer/article/1807333</a>  </p>
]]></content>
    <summary type="html">
    <![CDATA[<h1 id="1-_u80CC_u666F"><a href="#1-_u80CC_u666F" class="headerlink" title="1.背景"></a>1.背景</h1><p>有时候我们很难精准的评估一份程序在运行时的性能表现。  </p>
<p>要分析问题得]]>
    </summary>
    
      <category term="性能" scheme="https://www.zoucz.com/blog/tags/%E6%80%A7%E8%83%BD/"/>
    
      <category term="后台开发" scheme="https://www.zoucz.com/blog/categories/%E5%90%8E%E5%8F%B0%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[linux 零拷贝相关方法梳理和代码调试]]></title>
    <link href="https://www.zoucz.com/blog/2023/12/31/93b6dc00-a7c3-11ee-95cb-3556e1632a5e/"/>
    <id>https://www.zoucz.com/blog/2023/12/31/93b6dc00-a7c3-11ee-95cb-3556e1632a5e/</id>
    <published>2023-12-31T10:01:33.000Z</published>
    <updated>2024-01-18T14:32:33.000Z</updated>
    <content type="html"><![CDATA[<p>零拷贝（Zero-Copy）是一种可以在计算机系统中，通过减少 CPU 拷贝数据次数、绕开内核进行直接 IO 等方式，提高系统 IO 性能的一种技术。</p>
<h1 id="u901A_u8FC7_DMA__u51CF_u5C11_CPU__u62F7_u8D1D_u6B21_u6570"><a href="#u901A_u8FC7_DMA__u51CF_u5C11_CPU__u62F7_u8D1D_u6B21_u6570" class="headerlink" title="通过 DMA 减少 CPU 拷贝次数"></a>通过 DMA 减少 CPU 拷贝次数</h1><p>减少拷贝次数的原理主要是通过减少数据拷贝的次数，减少CPU的使用，提高数据传输的效率。在传统的数据传输过程中，数据通常需要在用户空间和内核空间之间进行至少两次的拷贝，这就会消耗大量的CPU资源。而零拷贝技术则是通过一些特殊的技术手段，尽可能地减少数据的拷贝次数，从而提高数据传输的效率。  </p>
<h2 id="DMA_uFF08Direct_Memory_Access_uFF09"><a href="#DMA_uFF08Direct_Memory_Access_uFF09" class="headerlink" title="DMA（Direct Memory Access）"></a>DMA（Direct Memory Access）</h2><p>直接内存访问是一种可以让某些硬件子系统（例如高速磁盘驱动器、图形卡）在不涉及CPU的情况下，直接访问内存的技术，从而实现零拷贝。  </p>
<h3 id="u76F4_u63A5_u4F7F_u7528_CPU__u7684_IO"><a href="#u76F4_u63A5_u4F7F_u7528_CPU__u7684_IO" class="headerlink" title="直接使用 CPU 的 IO"></a>直接使用 CPU 的 IO</h3><p>普通的 IO 方式，CPU 参与了全部 IO 的工作，并存在数据从内核态到用户态拷贝的过程。  </p>
<p><img src="https://zoucz.com/blogimgs/93b6dc00-a7c3-11ee-95cb-3556e1632a5e.md/01b62593eee67d138de878015b1a41f1.jpg" alt="image.png"></p>
<h3 id="CPU_+_DMA__u7684_IO"><a href="#CPU_+_DMA__u7684_IO" class="headerlink" title="CPU + DMA 的 IO"></a>CPU + DMA 的 IO</h3><p>目前支持 DMA 的硬件包括：网卡、声卡、显卡、磁盘控制器等。  </p>
<p><img src="https://zoucz.com/blogimgs/93b6dc00-a7c3-11ee-95cb-3556e1632a5e.md/43ae8111a407019ce281cb40e1df045a.jpg" alt="image.png"></p>
<p>加上 DMA 设备后，CPU 参与的环节有所减少，但是整个 IO 过程扔存在内核态到用户态的拷贝过程</p>
<p><img src="https://zoucz.com/blogimgs/93b6dc00-a7c3-11ee-95cb-3556e1632a5e.md/3e332453dd4b883d92d925846bbe59dd.jpg" alt="image.png"></p>
<p>操作系统提供了一些方法，能进一步减少 CPU 参与的 IO 环节，让数据不再从内核缓冲区的内存页拷贝到用户缓冲区，减少数据拷贝的次数。  </p>
<h2 id="mmap"><a href="#mmap" class="headerlink" title="mmap"></a>mmap</h2><h3 id="u6982_u5FF5_u548C_u9002_u7528_u573A_u666F"><a href="#u6982_u5FF5_u548C_u9002_u7528_u573A_u666F" class="headerlink" title="概念和适用场景"></a>概念和适用场景</h3><p><img src="https://zoucz.com/blogimgs/93b6dc00-a7c3-11ee-95cb-3556e1632a5e.md/2cc48669dfae5fbb22dd91a02d9de4cd.jpg" alt="image.png"></p>
<p>该函数可以将一个文件或者其它对象映射进内存，通过对这块内存的读写操作，程序可以实现对文件或者其它对象的读写操作。这样就避免了数据在用户空间和内核空间之间的拷贝，从而减少了一次从内核态到用户态的拷贝过程。  </p>
<p>mmap适用的场景：  </p>
<ul>
<li>文件I/O性能要求较高的场景：mmap将文件映射到内存，可以避免数据在用户空间和内核空间之间的拷贝，从而提高文件I/O性能。</li>
<li>大文件处理：对于大文件，使用mmap可以避免一次性将整个文件读入内存，减少内存占用，提高处理效率。</li>
<li>多个进程共享内存的场景：mmap可以用于实现进程间通信（IPC），通过将一个文件映射到多个进程的内存空间，实现进程间数据共享。</li>
<li>内存数据库：mmap可以用于实现内存数据库，将数据库文件映射到内存，提高数据访问速度。</li>
</ul>
<p>Redis、Nginx、Lucene、LevelDB、SQLite 等应用中在一些场景下使用了 mmap 来优化 IO 效率。  </p>
<h3 id="u793A_u4F8B_u4EE3_u7801"><a href="#u793A_u4F8B_u4EE3_u7801" class="headerlink" title="示例代码"></a>示例代码</h3><p>下面是两段示例代码，来对比使用 mmap 和普通 IO 的性能差异。  </p>
<p>构造测试数据，向磁盘中写入一个文件 test_data.txt，1GB 大小，每行 100~200 个字节的文本字符。  </p>
<figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;time.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> FILE_SIZE (<span class="number">1024</span> * <span class="number">1024</span> * <span class="number">1024</span>)</span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> MIN_LINE_SIZE <span class="number">100</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> MAX_LINE_SIZE <span class="number">200</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">char</span> <span class="title">random_char</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">int</span> r = rand() % <span class="number">62</span>;</span><br><span class="line">    <span class="keyword">if</span> (r &lt; <span class="number">10</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">'0'</span> + r;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (r &lt; <span class="number">36</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">'A'</span> + r - <span class="number">10</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">'a'</span> + r - <span class="number">36</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="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">int</span> fd;</span><br><span class="line">    <span class="keyword">size_t</span> written_bytes = <span class="number">0</span>;</span><br><span class="line">    srand(time(<span class="literal">NULL</span>));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建并打开文件</span></span><br><span class="line">    fd = open(<span class="string">"test_data.txt"</span>, O_CREAT | O_WRONLY | O_TRUNC, <span class="number">0644</span>);</span><br><span class="line">    <span class="keyword">if</span> (fd &lt; <span class="number">0</span>) &#123;</span><br><span class="line">        perror(<span class="string">"open error"</span>);</span><br><span class="line">        <span class="built_in">exit</span>(EXIT_FAILURE);</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">while</span> (written_bytes &lt; FILE_SIZE) &#123;</span><br><span class="line">        <span class="keyword">int</span> line_size = MIN_LINE_SIZE + rand() % (MAX_LINE_SIZE - MIN_LINE_SIZE + <span class="number">1</span>);</span><br><span class="line">        <span class="keyword">char</span> buffer[MAX_LINE_SIZE + <span class="number">1</span>]; <span class="comment">// 多出来的一个字节用于存放换行符</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; line_size; ++i) &#123;</span><br><span class="line">            buffer[i] = random_char();</span><br><span class="line">        &#125;</span><br><span class="line">        buffer[line_size] = <span class="string">'\n'</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">ssize_t</span> to_write = line_size + <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">ssize_t</span> remaining = FILE_SIZE - written_bytes;</span><br><span class="line">        <span class="keyword">if</span> (to_write &gt; remaining) &#123;</span><br><span class="line">            to_write = remaining;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">ssize_t</span> written = write(fd, buffer, to_write);</span><br><span class="line">        <span class="keyword">if</span> (written &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            perror(<span class="string">"write error"</span>);</span><br><span class="line">            <span class="built_in">exit</span>(EXIT_FAILURE);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        written_bytes += written;</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">    close(fd);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>使用 mmap 读取文件并统计行数：  </p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/mman.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/stat.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;time.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span> *argv[])</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (argc != <span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Usage: %s &lt;file&gt;\n"</span>, argv[<span class="number">0</span>]);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">int</span> fd = open(argv[<span class="number">1</span>], O_RDONLY);</span><br><span class="line">    <span class="keyword">if</span> (fd &lt; <span class="number">0</span>) &#123;</span><br><span class="line">        perror(<span class="string">"open"</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">struct</span> stat sb;</span><br><span class="line">    <span class="keyword">if</span> (fstat(fd, &amp;sb) == -<span class="number">1</span>) &#123;</span><br><span class="line">        perror(<span class="string">"fstat"</span>);</span><br><span class="line">        close(fd);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">char</span> *file_data = (<span class="keyword">char</span>*)mmap(<span class="literal">NULL</span>, sb.st_size, PROT_READ, MAP_PRIVATE, fd, <span class="number">0</span>);</span><br><span class="line">    <span class="keyword">if</span> (file_data == MAP_FAILED) &#123;</span><br><span class="line">        perror(<span class="string">"mmap"</span>);</span><br><span class="line">        close(fd);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">clock_t</span> start = clock();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">size_t</span> line_count = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">char</span> *next = <span class="literal">NULL</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">char</span> *current = file_data; current - file_data &lt; sb.st_size; current = next + <span class="number">1</span>) &#123;</span><br><span class="line">        next = (<span class="keyword">char</span>*)<span class="built_in">memchr</span>(current, <span class="string">'\n'</span>, sb.st_size - (current - file_data));</span><br><span class="line">        <span class="keyword">if</span> (next) &#123;</span><br><span class="line">            line_count++;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</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"></span><br><span class="line">    <span class="keyword">clock_t</span> end = clock();</span><br><span class="line"></span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"Line count: %zu\n"</span>, line_count);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"Time taken: %lf ms\n"</span>, (<span class="keyword">double</span>)(end - start) / CLOCKS_PER_SEC * <span class="number">1000</span>);</span><br><span class="line"></span><br><span class="line">    munmap(file_data, sb.st_size);</span><br><span class="line">    close(fd);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>使用普通IO读取文件并统计行数：  </p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/stat.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;time.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span> *argv[])</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (argc != <span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Usage: %s &lt;file&gt;\n"</span>, argv[<span class="number">0</span>]);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    FILE *fp = fopen(argv[<span class="number">1</span>], <span class="string">"r"</span>);</span><br><span class="line">    <span class="keyword">if</span> (!fp) &#123;</span><br><span class="line">        perror(<span class="string">"fopen"</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">clock_t</span> start = clock();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 对文件数据进行处理，这里逐行读取文件内容并计算行数</span></span><br><span class="line">    <span class="keyword">size_t</span> line_count = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">char</span> *line = <span class="literal">NULL</span>;</span><br><span class="line">    <span class="keyword">size_t</span> line_length = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">ssize_t</span> read_length;</span><br><span class="line">    <span class="keyword">while</span> ((read_length = getline(&amp;line, &amp;line_length, fp)) != -<span class="number">1</span>) &#123;</span><br><span class="line">        line_count++;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">clock_t</span> end = clock();</span><br><span class="line"></span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"Line count: %zu\n"</span>, line_count);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"Time taken: %lf ms\n"</span>, (<span class="keyword">double</span>)(end - start) / CLOCKS_PER_SEC * <span class="number">1000</span>);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">free</span>(line);</span><br><span class="line">    fclose(fp);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>测试结果：  </p>
<p><img src="https://zoucz.com/blogimgs/93b6dc00-a7c3-11ee-95cb-3556e1632a5e.md/f47d6c3f5743e449092533659aa13683.jpg" alt="image.png"></p>
<h2 id="sendfile"><a href="#sendfile" class="headerlink" title="sendfile"></a>sendfile</h2><h3 id="u6982_u5FF5_u548C_u9002_u7528_u573A_u666F-1"><a href="#u6982_u5FF5_u548C_u9002_u7528_u573A_u666F-1" class="headerlink" title="概念和适用场景"></a>概念和适用场景</h3><p><img src="https://zoucz.com/blogimgs/93b6dc00-a7c3-11ee-95cb-3556e1632a5e.md/002ab6fdd28ab7d43aa7927a28420a65.jpg" alt="image.png"></p>
<p>sendfile 相当于实现了 mmap + write 的功能。该函数可以在两个文件描述符之间直接传输数据，避免了数据在用户空间和内核空间之间的拷贝，从而减少了一次从内核态到用户态的拷贝。  </p>
<p>sendfile适用的场景：  </p>
<ul>
<li>文件服务器：在文件服务器中，常需要将磁盘上的文件发送给客户端。使用sendfile方法可以直接将数据从磁盘传输到套接字，避免了多余的数据拷贝操作，提高了文件传输的性能。</li>
<li>代理服务器：代理服务器需要将客户端的请求转发给目标服务器，并将目标服务器的响应返回给客户端。使用sendfile方法可以在套接字之间直接传输数据，提高了代理服务器的性能。</li>
<li>Web服务器：Web服务器需要将磁盘上的静态文件发送给客户端。使用sendfile方法可以提高文件传输的性能，降低服务器的资源消耗。</li>
<li>数据库服务器：数据库服务器需要将数据文件中的数据发送给客户端。使用sendfile方法可以提高数据传输的效率，降低服务器的资源消耗。  </li>
</ul>
<p>Nginx、Apache、Haproxy 等项目使用 sendfile 来提高文件传输性能。   </p>
<h3 id="u793A_u4F8B_u4EE3_u7801-1"><a href="#u793A_u4F8B_u4EE3_u7801-1" class="headerlink" title="示例代码"></a>示例代码</h3><p>下面是两段测试代码，来测试 sendfile 和普通 IO 的性能差异，仍然使用上面创建的测试文件</p>
<p>使用 sendfile 读取文件并发送:  </p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;arpa/inet.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;netinet/in.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/sendfile.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/socket.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/stat.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/time.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span>* argv[])</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (argc != <span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Usage: %s &lt;file&gt;\n"</span>, argv[<span class="number">0</span>]);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">int</span> server_fd, client_fd, file_fd;</span><br><span class="line">    <span class="keyword">struct</span> sockaddr_in server_addr, client_addr;</span><br><span class="line">    <span class="keyword">socklen_t</span> client_addr_len = <span class="keyword">sizeof</span>(client_addr);</span><br><span class="line">    <span class="keyword">struct</span> stat file_stat;</span><br><span class="line">    <span class="keyword">off_t</span> offset = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">ssize_t</span> sent_bytes = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">struct</span> timeval start, end;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建TCP套接字</span></span><br><span class="line">    server_fd = socket(AF_INET, SOCK_STREAM, <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="built_in">memset</span>(&amp;server_addr, <span class="number">0</span>, <span class="keyword">sizeof</span>(server_addr));</span><br><span class="line">    server_addr.sin_family = AF_INET;</span><br><span class="line">    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);</span><br><span class="line">    server_addr.sin_port = htons(<span class="number">12345</span>);</span><br><span class="line">    bind(server_fd, (<span class="keyword">struct</span> sockaddr *)&amp;server_addr, <span class="keyword">sizeof</span>(server_addr));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 监听连接</span></span><br><span class="line">    listen(server_fd, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 接受客户端连接</span></span><br><span class="line">    client_fd = accept(server_fd, (<span class="keyword">struct</span> sockaddr *)&amp;client_addr, &amp;client_addr_len);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 打开要发送的文件</span></span><br><span class="line">    file_fd = open(argv[<span class="number">1</span>], O_RDONLY);</span><br><span class="line">    fstat(file_fd, &amp;file_stat);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 使用sendfile发送文件</span></span><br><span class="line">    gettimeofday(&amp;start, <span class="literal">NULL</span>);</span><br><span class="line">    <span class="keyword">while</span> (((sent_bytes = sendfile(client_fd, file_fd, &amp;offset, file_stat.st_size)) &gt; <span class="number">0</span>) &amp;&amp; (file_stat.st_size &gt; <span class="number">0</span>)) &#123;</span><br><span class="line">        file_stat.st_size -= sent_bytes;</span><br><span class="line">    &#125;</span><br><span class="line">    gettimeofday(&amp;end, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 计算并输出耗时</span></span><br><span class="line">    <span class="keyword">long</span> elapsed_ms = (end.tv_sec - start.tv_sec) * <span class="number">1000</span> + (end.tv_usec - start.tv_usec) / <span class="number">1000</span>;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"sendfile: %ld ms\n"</span>, elapsed_ms);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 关闭文件和套接字</span></span><br><span class="line">    close(file_fd);</span><br><span class="line">    close(client_fd);</span><br><span class="line">    close(server_fd);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>使用普通 IO 读取文件并发送：</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;arpa/inet.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;netinet/in.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/socket.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/stat.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/time.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> BUFFER_SIZE <span class="number">8192</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span>* argv[])</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (argc != <span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Usage: %s &lt;file&gt;\n"</span>, argv[<span class="number">0</span>]);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">int</span> server_fd, client_fd, file_fd;</span><br><span class="line">    <span class="keyword">struct</span> sockaddr_in server_addr, client_addr;</span><br><span class="line">    <span class="keyword">socklen_t</span> client_addr_len = <span class="keyword">sizeof</span>(client_addr);</span><br><span class="line">    <span class="keyword">struct</span> stat file_stat;</span><br><span class="line">    <span class="keyword">char</span> buffer[BUFFER_SIZE];</span><br><span class="line">    <span class="keyword">ssize_t</span> read_bytes, sent_bytes;</span><br><span class="line">    <span class="keyword">struct</span> timeval start, end;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建TCP套接字</span></span><br><span class="line">    server_fd = socket(AF_INET, SOCK_STREAM, <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="built_in">memset</span>(&amp;server_addr, <span class="number">0</span>, <span class="keyword">sizeof</span>(server_addr));</span><br><span class="line">    server_addr.sin_family = AF_INET;</span><br><span class="line">    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);</span><br><span class="line">    server_addr.sin_port = htons(<span class="number">12345</span>);</span><br><span class="line">    bind(server_fd, (<span class="keyword">struct</span> sockaddr *)&amp;server_addr, <span class="keyword">sizeof</span>(server_addr));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 监听连接</span></span><br><span class="line">    listen(server_fd, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 接受客户端连接</span></span><br><span class="line">    client_fd = accept(server_fd, (<span class="keyword">struct</span> sockaddr *)&amp;client_addr, &amp;client_addr_len);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 打开要发送的文件</span></span><br><span class="line">    file_fd = open(argv[<span class="number">1</span>], O_RDONLY);</span><br><span class="line">    fstat(file_fd, &amp;file_stat);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 使用普通I/O发送文件</span></span><br><span class="line">    gettimeofday(&amp;start, <span class="literal">NULL</span>);</span><br><span class="line">    <span class="keyword">while</span> ((read_bytes = read(file_fd, buffer, BUFFER_SIZE)) &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        sent_bytes = <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">while</span> (sent_bytes &lt; read_bytes) &#123;</span><br><span class="line">            <span class="keyword">ssize_t</span> sent = send(client_fd, buffer + sent_bytes, read_bytes - sent_bytes, <span class="number">0</span>);</span><br><span class="line">            <span class="keyword">if</span> (sent &lt; <span class="number">0</span>) &#123;</span><br><span class="line">                perror(<span class="string">"send error"</span>);</span><br><span class="line">                <span class="built_in">exit</span>(EXIT_FAILURE);</span><br><span class="line">            &#125;</span><br><span class="line">            sent_bytes += sent;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    gettimeofday(&amp;end, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 计算并输出耗时</span></span><br><span class="line">    <span class="keyword">long</span> elapsed_ms = (end.tv_sec - start.tv_sec) * <span class="number">1000</span> + (end.tv_usec - start.tv_usec) / <span class="number">1000</span>;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"regular IO: %ld ms\n"</span>, elapsed_ms);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 关闭文件和套接字</span></span><br><span class="line">    close(file_fd);</span><br><span class="line">    close(client_fd);</span><br><span class="line">    close(server_fd);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>客户端：  </p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;arpa/inet.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;netinet/in.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/socket.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> BUFFER_SIZE <span class="number">8192</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">int</span> client_fd;</span><br><span class="line">    <span class="keyword">struct</span> sockaddr_in server_addr;</span><br><span class="line">    <span class="keyword">char</span> buffer[BUFFER_SIZE];</span><br><span class="line">    <span class="keyword">ssize_t</span> received_bytes;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建TCP套接字</span></span><br><span class="line">    client_fd = socket(AF_INET, SOCK_STREAM, <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="built_in">memset</span>(&amp;server_addr, <span class="number">0</span>, <span class="keyword">sizeof</span>(server_addr));</span><br><span class="line">    server_addr.sin_family = AF_INET;</span><br><span class="line">    server_addr.sin_addr.s_addr = inet_addr(<span class="string">"127.0.0.1"</span>);</span><br><span class="line">    server_addr.sin_port = htons(<span class="number">12345</span>);</span><br><span class="line">    connect(client_fd, (<span class="keyword">struct</span> sockaddr *)&amp;server_addr, <span class="keyword">sizeof</span>(server_addr));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 接收文件</span></span><br><span class="line">    <span class="keyword">while</span> ((received_bytes = recv(client_fd, buffer, BUFFER_SIZE, <span class="number">0</span>)) &gt; <span class="number">0</span>) &#123;</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"></span><br><span class="line">    <span class="comment">// 关闭套接字</span></span><br><span class="line">    close(client_fd);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>测试结果：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sendfile: 221 ms&#10;regular IO: 499 ms</span><br></pre></td></tr></table></figure>
<h2 id="splice"><a href="#splice" class="headerlink" title="splice"></a>splice</h2><h3 id="u6982_u5FF5_u548C_u9002_u7528_u573A_u666F-2"><a href="#u6982_u5FF5_u548C_u9002_u7528_u573A_u666F-2" class="headerlink" title="概念和适用场景"></a>概念和适用场景</h3><p><img src="https://zoucz.com/blogimgs/93b6dc00-a7c3-11ee-95cb-3556e1632a5e.md/f874e9f92a1de74832a4663867ae5400.jpg" alt="image.png"></p>
<p>和 sendfile 一样，该函数可以将数据从一个文件描述符移动到另一个文件描述符，而不需要将数据从内核态拷贝到用户态，减少了一次拷贝过程。  </p>
<h3 id="u548C_sendfile__u7684_u5DEE_u5F02"><a href="#u548C_sendfile__u7684_u5DEE_u5F02" class="headerlink" title="和 sendfile 的差异"></a>和 sendfile 的差异</h3><p>splice 和 sendfile 都是用于优化数据传输的 Linux 系统调用，它们之间有一定的联系，但也有一些区别。  </p>
<p>功能上的差异</p>
<ul>
<li><p>sendfile 主要用于在两个文件描述符之间传输数据，特别是将文件数据发送到网络套接字。它可以在内核空间直接完成数据传输，避免了用户空间和内核空间之间的数据拷贝，从而提高了性能。然而，<code>sendfile</code>的一个限制是，它只能用于普通文件和套接字之间的数据传输。</p>
</li>
<li><p>splice 是一个更通用的数据传输系统调用，它可以在任意类型的文件描述符之间传输数据，包括普通文件、设备文件、套接字等。 splice 的工作原理是将数据从一个文件描述符移动到另一个文件描述符，而不需要将数据拷贝到用户空间。这样，它可以在内核空间完成数据传输，提高性能。与 sendfile 相比， splice 的优势在于它可以处理更多类型的文件描述符，更加灵活。</p>
</li>
</ul>
<p>数据传输方式差异：</p>
<ul>
<li><p>sendfile 在内核空间直接将数据从一个文件描述符传输到另一个文件描述符，不需要通过用户空间的缓冲区。sendfile适用于将磁盘上的文件发送到网络套接字的场景，例如文件服务器、Web服务器等。</p>
</li>
<li><p>splice 通过管道（pipe）在文件描述符之间传输数据。首先，它将数据从一个文件描述符移动到管道，然后将数据从管道移动到另一个文件描述符。这样，数据在内核空间完成传输，避免了用户空间和内核空间之间的数据拷贝。<code>splice</code>适用于各种类型的文件描述符之间的数据传输，例如设备文件、套接字等。</p>
</li>
</ul>
<h3 id="u793A_u4F8B_u4EE3_u7801-2"><a href="#u793A_u4F8B_u4EE3_u7801-2" class="headerlink" title="示例代码"></a>示例代码</h3><p>使用 splice 读取文件并发送  </p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;arpa/inet.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;netinet/in.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/socket.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/stat.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/time.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> BUFFER_SIZE <span class="number">8192</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span>* argv[])</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (argc != <span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Usage: %s &lt;file&gt;\n"</span>, argv[<span class="number">0</span>]);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">int</span> server_fd, client_fd, file_fd;</span><br><span class="line">    <span class="keyword">struct</span> sockaddr_in server_addr, client_addr;</span><br><span class="line">    <span class="keyword">socklen_t</span> client_addr_len = <span class="keyword">sizeof</span>(client_addr);</span><br><span class="line">    <span class="keyword">struct</span> stat file_stat;</span><br><span class="line">    <span class="keyword">int</span> pipe_fds[<span class="number">2</span>];</span><br><span class="line">    <span class="keyword">ssize_t</span> splice_bytes;</span><br><span class="line">    <span class="keyword">struct</span> timeval start, end;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建TCP套接字</span></span><br><span class="line">    server_fd = socket(AF_INET, SOCK_STREAM, <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="built_in">memset</span>(&amp;server_addr, <span class="number">0</span>, <span class="keyword">sizeof</span>(server_addr));</span><br><span class="line">    server_addr.sin_family = AF_INET;</span><br><span class="line">    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);</span><br><span class="line">    server_addr.sin_port = htons(<span class="number">12345</span>);</span><br><span class="line">    bind(server_fd, (<span class="keyword">struct</span> sockaddr *)&amp;server_addr, <span class="keyword">sizeof</span>(server_addr));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 监听连接</span></span><br><span class="line">    listen(server_fd, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 接受客户端连接</span></span><br><span class="line">    client_fd = accept(server_fd, (<span class="keyword">struct</span> sockaddr *)&amp;client_addr, &amp;client_addr_len);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 打开要发送的文件</span></span><br><span class="line">    file_fd = open(argv[<span class="number">1</span>], O_RDONLY);</span><br><span class="line">    fstat(file_fd, &amp;file_stat);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建管道</span></span><br><span class="line">    pipe(pipe_fds);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 使用splice发送文件</span></span><br><span class="line">    gettimeofday(&amp;start, <span class="literal">NULL</span>);</span><br><span class="line">    <span class="keyword">while</span> ((splice_bytes = splice(file_fd, <span class="literal">NULL</span>, pipe_fds[<span class="number">1</span>], <span class="literal">NULL</span>, BUFFER_SIZE, SPLICE_F_MOVE)) &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        splice(pipe_fds[<span class="number">0</span>], <span class="literal">NULL</span>, client_fd, <span class="literal">NULL</span>, splice_bytes, SPLICE_F_MOVE);</span><br><span class="line">    &#125;</span><br><span class="line">    gettimeofday(&amp;end, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 计算并输出耗时</span></span><br><span class="line">    <span class="keyword">long</span> elapsed_ms = (end.tv_sec - start.tv_sec) * <span class="number">1000</span> + (end.tv_usec - start.tv_usec) / <span class="number">1000</span>;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"splice: %ld ms\n"</span>, elapsed_ms);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 关闭文件和套接字</span></span><br><span class="line">    close(file_fd);</span><br><span class="line">    close(client_fd);</span><br><span class="line">    close(server_fd);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>使用普通 IO 读取文件  </p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;arpa/inet.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;netinet/in.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/socket.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/stat.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/time.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> BUFFER_SIZE <span class="number">8192</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span>* argv[])</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (argc != <span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Usage: %s &lt;file&gt;\n"</span>, argv[<span class="number">0</span>]);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">int</span> server_fd, client_fd, file_fd;</span><br><span class="line">    <span class="keyword">struct</span> sockaddr_in server_addr, client_addr;</span><br><span class="line">    <span class="keyword">socklen_t</span> client_addr_len = <span class="keyword">sizeof</span>(client_addr);</span><br><span class="line">    <span class="keyword">struct</span> stat file_stat;</span><br><span class="line">    <span class="keyword">char</span> buffer[BUFFER_SIZE];</span><br><span class="line">    <span class="keyword">ssize_t</span> read_bytes, sent_bytes;</span><br><span class="line">    <span class="keyword">struct</span> timeval start, end;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建TCP套接字</span></span><br><span class="line">    server_fd = socket(AF_INET, SOCK_STREAM, <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="built_in">memset</span>(&amp;server_addr, <span class="number">0</span>, <span class="keyword">sizeof</span>(server_addr));</span><br><span class="line">    server_addr.sin_family = AF_INET;</span><br><span class="line">    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);</span><br><span class="line">    server_addr.sin_port = htons(<span class="number">12345</span>);</span><br><span class="line">    bind(server_fd, (<span class="keyword">struct</span> sockaddr *)&amp;server_addr, <span class="keyword">sizeof</span>(server_addr));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 监听连接</span></span><br><span class="line">    listen(server_fd, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 接受客户端连接</span></span><br><span class="line">    client_fd = accept(server_fd, (<span class="keyword">struct</span> sockaddr *)&amp;client_addr, &amp;client_addr_len);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 打开要发送的文件</span></span><br><span class="line">    file_fd = open(argv[<span class="number">1</span>], O_RDONLY);</span><br><span class="line">    fstat(file_fd, &amp;file_stat);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 使用普通I/O发送文件</span></span><br><span class="line">    gettimeofday(&amp;start, <span class="literal">NULL</span>);</span><br><span class="line">    <span class="keyword">while</span> ((read_bytes = read(file_fd, buffer, BUFFER_SIZE)) &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        sent_bytes = <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">while</span> (sent_bytes &lt; read_bytes) &#123;</span><br><span class="line">            <span class="keyword">ssize_t</span> sent = send(client_fd, buffer + sent_bytes, read_bytes - sent_bytes, <span class="number">0</span>);</span><br><span class="line">            <span class="keyword">if</span> (sent &lt; <span class="number">0</span>) &#123;</span><br><span class="line">                perror(<span class="string">"send error"</span>);</span><br><span class="line">                <span class="built_in">exit</span>(EXIT_FAILURE);</span><br><span class="line">            &#125;</span><br><span class="line">            sent_bytes += sent;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    gettimeofday(&amp;end, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 计算并输出耗时</span></span><br><span class="line">    <span class="keyword">long</span> elapsed_ms = (end.tv_sec - start.tv_sec) * <span class="number">1000</span> + (end.tv_usec - start.tv_usec) / <span class="number">1000</span>;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"regular IO: %ld ms\n"</span>, elapsed_ms);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 关闭文件和套接字</span></span><br><span class="line">    close(file_fd);</span><br><span class="line">    close(client_fd);</span><br><span class="line">    close(server_fd);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>测试结果：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">splice: 224 ms&#10;regular IO: 473 ms</span><br></pre></td></tr></table></figure>
<h1 id="Direct_IO"><a href="#Direct_IO" class="headerlink" title="Direct IO"></a>Direct IO</h1><h2 id="u6982_u5FF5"><a href="#u6982_u5FF5" class="headerlink" title="概念"></a>概念</h2><p>Direct IO 是一种在文件系统层面实现的绕过内核缓存的技术。Direct IO 仍然使用文件系统，但是在读写文件时，数据直接从磁盘传输到用户空间，而不经过内核缓存。  </p>
<p>需要注意的是，不经过内核缓存，对于单次的文件读写来说，性能反而会下降。在某些大文件高并发读写场景下，可以通过开启 Direct IO 配合 aio，减小 IO 过程中的 CPU 和内存开销，从而提升整体性能。</p>
<h2 id="u793A_u4F8B_u4EE3_u7801-3"><a href="#u793A_u4F8B_u4EE3_u7801-3" class="headerlink" title="示例代码"></a>示例代码</h2><p>仍然使用前面创建的 test_data.txt 做测试。测试单次文件读写的性能，可以发现实际上 directio 比普通的 IO 速度是更慢的。  </p>
<p>使用 direct io 读取文件  </p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/stat.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;time.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;errno.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="preprocessor">#<span class="keyword">ifndef</span> O_DIRECT</span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> O_DIRECT <span class="number">040000</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> BUF_SIZE <span class="number">1024</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span> *argv[])</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (argc != <span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Usage: %s &lt;file&gt;\n"</span>, argv[<span class="number">0</span>]);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">int</span> fd = open(argv[<span class="number">1</span>], O_RDONLY | O_DIRECT);</span><br><span class="line">    <span class="keyword">if</span> (fd &lt; <span class="number">0</span>) &#123;</span><br><span class="line">        perror(<span class="string">"open"</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">char</span> *buf;</span><br><span class="line">    <span class="keyword">if</span> (posix_memalign((<span class="keyword">void</span> **)&amp;buf, BUF_SIZE, BUF_SIZE) != <span class="number">0</span>) &#123;</span><br><span class="line">        perror(<span class="string">"posix_memalign"</span>);</span><br><span class="line">        close(fd);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">clock_t</span> start = clock();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">size_t</span> total_bytes = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">ssize_t</span> read_length;</span><br><span class="line">    <span class="keyword">while</span> ((read_length = read(fd, buf, BUF_SIZE)) &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        total_bytes += read_length;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">clock_t</span> end = clock();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (read_length &lt; <span class="number">0</span>) &#123;</span><br><span class="line">        perror(<span class="string">"read"</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"Total bytes: %zu\n"</span>, total_bytes);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"Time taken: %lf ms\n"</span>, (<span class="keyword">double</span>) (end - start) / CLOCKS_PER_SEC * <span class="number">1000</span>);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">free</span>(buf);</span><br><span class="line">    close(fd);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>使用普通 io 读取文件  </p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/stat.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;time.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> BUF_SIZE <span class="number">1024</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span> *argv[])</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (argc != <span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Usage: %s &lt;file&gt;\n"</span>, argv[<span class="number">0</span>]);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    FILE *fp = fopen(argv[<span class="number">1</span>], <span class="string">"r"</span>);</span><br><span class="line">    <span class="keyword">if</span> (!fp) &#123;</span><br><span class="line">        perror(<span class="string">"fopen"</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">char</span> buf[BUF_SIZE];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">clock_t</span> start = clock();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">size_t</span> total_bytes = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">ssize_t</span> read_length;</span><br><span class="line">    <span class="keyword">while</span> ((read_length = fread(buf, <span class="number">1</span>, BUF_SIZE, fp)) &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        total_bytes += read_length;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">clock_t</span> end = clock();</span><br><span class="line"></span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"Total bytes: %zu\n"</span>, total_bytes);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"Time taken: %lf ms\n"</span>, (<span class="keyword">double</span>) (end - start) / CLOCKS_PER_SEC * <span class="number">1000</span>);</span><br><span class="line"></span><br><span class="line">    fclose(fp);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>测试结果：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Direct IO:&#10;Total bytes: 1073741824&#10;Time taken: 9720.000000 ms&#10;&#10;regular IO:&#10;Total bytes: 1073741824&#10;Time taken: 250.000000 ms</span><br></pre></td></tr></table></figure>
<h1 id="u5176_u5B83_IO__u4F18_u5316_u76F8_u5173_u6280_u672F"><a href="#u5176_u5B83_IO__u4F18_u5316_u76F8_u5173_u6280_u672F" class="headerlink" title="其它 IO 优化相关技术"></a>其它 IO 优化相关技术</h1><h2 id="u5199_u65F6_u590D_u5236"><a href="#u5199_u65F6_u590D_u5236" class="headerlink" title="写时复制"></a>写时复制</h2><p>Linux的写时复制（Copy-on-Write）机制是指在进程创建子进程或者读取文件内容时，并不会立即将数据完全复制到新的地址空间中。而是通过共享同一段物理内存来提高效率。只有当其中一个进程对该部分数据进行修改时，才会真正发生复制操作。</p>
<p>这种技术可以避免大量无用的数据复制，节省了系统开销。</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/mman.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/wait.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="keyword">size_t</span> data_size = <span class="number">4096</span>;</span><br><span class="line">    <span class="keyword">int</span> status;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 父子进程都能访问到的内存</span></span><br><span class="line">    <span class="keyword">char</span> shared_data[] = <span class="string">"Hello, copy-on-write!"</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建子进程</span></span><br><span class="line">    <span class="keyword">pid_t</span> pid = fork();</span><br><span class="line">    <span class="keyword">if</span> (pid &lt; <span class="number">0</span>) &#123;</span><br><span class="line">        perror(<span class="string">"fork error"</span>);</span><br><span class="line">        <span class="built_in">exit</span>(EXIT_FAILURE);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (pid == <span class="number">0</span>) &#123; <span class="comment">// 子进程</span></span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Child process: initial data: %s\n"</span>, shared_data);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 这将触发写时复制，因为我们试图修改一个只读的内存页</span></span><br><span class="line">        shared_data[<span class="number">0</span>] = <span class="string">'h'</span>;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Child process: modified data: %s\n"</span>, shared_data);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 退出子进程</span></span><br><span class="line">        <span class="built_in">exit</span>(EXIT_SUCCESS);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123; <span class="comment">// 父进程</span></span><br><span class="line">        <span class="comment">// 等待子进程结束</span></span><br><span class="line">        waitpid(pid, &amp;status, <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="built_in">printf</span>(<span class="string">"Parent process: data: %s\n"</span>, shared_data);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>测试结果：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Child process: initial data: Hello, copy-on-write!&#10;Child process: modified data: hello, copy-on-write!&#10;Parent process: data: Hello, copy-on-write!</span><br></pre></td></tr></table></figure>
<h2 id="RDMA_uFF08Remote_Direct_Memory_Access_uFF09"><a href="#RDMA_uFF08Remote_Direct_Memory_Access_uFF09" class="headerlink" title="RDMA（Remote Direct Memory Access）"></a>RDMA（Remote Direct Memory Access）</h2><p>RDMA（ Remote Direct Memory Access ）意为远程直接地址访问，通过RDMA，本端节点可以“直接”访问远端节点的内存。所谓直接，指的是可以像访问本地内存一样，绕过传统以太网复杂的TCP/IP网络协议栈读写远端内存，而这个过程对端是不感知的，而且这个读写过程的大部分工作是由硬件而不是软件完成的。  </p>
<p>需要专门的硬件（网卡）支持。     </p>
<p>参考: <a href="https://zhuanlan.zhihu.com/p/138874738" target="_blank" rel="external">https://zhuanlan.zhihu.com/p/138874738</a> </p>
<h2 id="DPDK_28Data_Plane_Development_Kit_29"><a href="#DPDK_28Data_Plane_Development_Kit_29" class="headerlink" title="DPDK(Data Plane Development Kit)"></a>DPDK(Data Plane Development Kit)</h2><p>Data Plane Development Kit 是运行在用户空间上利用自身提供的数据平面库来收发数据包，绕过了 Linux 内核协议栈对数据包处理过程。在收到数据包时，经DPDK重载的网卡驱动不会通过中断通知CPU，而是直接将数据包存入内存，交付应用层软件通过DPDK提供的接口来直接处理，这样节省了大量的CPU中断时间和内存拷贝时间。    </p>
<p>参考: <a href="https://zhuanlan.zhihu.com/p/632045322" target="_blank" rel="external">https://zhuanlan.zhihu.com/p/632045322</a>  </p>
<h2 id="SPDK"><a href="#SPDK" class="headerlink" title="SPDK"></a>SPDK</h2><p>SPDK(Storage Performance Development Kit)，包含一套驱动程序，以及一整套端到端的存储参考架构。SPDK的目标是能够把硬件平台的计算、网络、存储（基于 NVME 的高读写性能 ssd 磁盘）的最新性能进展充分发挥出来。自芯片而上进行设计优化，SPDK 已展示出超高的性能指标。</p>
<p>它的高性能实际上来自于两项核心技术：第一个是用户态运行，第二个是轮询模式驱动。</p>
<p>参考： <a href="https://zhuanlan.zhihu.com/p/646710218" target="_blank" rel="external">https://zhuanlan.zhihu.com/p/646710218</a></p>
<p>参考文档：</p>
<ul>
<li><a href="https://strikefreedom.top/archives/linux-io-and-zero-copy#toc-head-19" target="_blank" rel="external">https://strikefreedom.top/archives/linux-io-and-zero-copy#toc-head-19</a></li>
<li><a href="https://www.joshbialkowski.com/posts/2018/linux_zero_copy/linux-zero-copy.html" target="_blank" rel="external">https://www.joshbialkowski.com/posts/2018/linux_zero_copy/linux-zero-copy.html</a></li>
<li><a href="https://juejin.cn/post/7083793623594041375" target="_blank" rel="external">https://juejin.cn/post/7083793623594041375</a></li>
<li><a href="https://www.guanngxu.com/25MtObxIW/" target="_blank" rel="external">https://www.guanngxu.com/25MtObxIW/</a></li>
<li><a href="https://juejin.cn/post/7002034457007882277" target="_blank" rel="external">https://juejin.cn/post/7002034457007882277</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/366707663" target="_blank" rel="external">https://zhuanlan.zhihu.com/p/366707663</a></li>
<li><a href="https://www.cnblogs.com/lightdb/p/12182523.html" target="_blank" rel="external">https://www.cnblogs.com/lightdb/p/12182523.html</a></li>
</ul>
]]></content>
    <summary type="html">
    <![CDATA[<p>零拷贝（Zero-Copy）是一种可以在计算机系统中，通过减少 CPU 拷贝数据次数、绕开内核进行直接 IO 等方式，提高系统 IO 性能的一种技术。</p>
<h1 id="u901A_u8FC7_DMA__u51CF_u5C11_CPU__u62F7_u8D1D_u6B]]>
    </summary>
    
      <category term="共享内存" scheme="https://www.zoucz.com/blog/tags/%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98/"/>
    
      <category term="零拷贝" scheme="https://www.zoucz.com/blog/tags/%E9%9B%B6%E6%8B%B7%E8%B4%9D/"/>
    
      <category term="后台开发" scheme="https://www.zoucz.com/blog/categories/%E5%90%8E%E5%8F%B0%E5%BC%80%E5%8F%91/"/>
    
      <category term="linux" scheme="https://www.zoucz.com/blog/categories/%E5%90%8E%E5%8F%B0%E5%BC%80%E5%8F%91/linux/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[linux下的编译、链接、加载执行]]></title>
    <link href="https://www.zoucz.com/blog/2023/12/23/5544b600-a0e7-11ee-b717-b77abb79b472/"/>
    <id>https://www.zoucz.com/blog/2023/12/23/5544b600-a0e7-11ee-b717-b77abb79b472/</id>
    <published>2023-12-23T05:40:52.000Z</published>
    <updated>2024-01-18T14:31:55.000Z</updated>
    <content type="html"><![CDATA[<p>最近在工作中遇到了几次动态库链接报错的问题。  </p>
<p>一次是 import 两个 python 模块，必须将一个模块放到前面加载，另一个放到后面加载，不然会报找不到版本的错误。  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ImportError: /usr/lib64/libstdc++.so.6: version &#39;CXXABI_1.3.9&#39; not found (required by......</span><br></pre></td></tr></table></figure>
<p>另一次是先加载了 c++ 版本的 libtorch 库，然后在同一进程中尝试 import python 版本的 libtorch，报符号未定义的错误，通过将其中一个放到子进程中运行可以解决。  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ImportError:/root/miniconda3/lib/python3.9/site-&#10;packages/torch/lib/libtorch_cuda_cpp.so: undefined symbol: _ZTIN4c10d4WorkE</span><br></pre></td></tr></table></figure>
<p>这些报错以前也经常遇到，解决也比较简单，无非就是升级库版本，或者设置正确的库加载路径。不过这些版本错误、符号未定义的含义和底层原理是什么样的呢？带着问题看了《程序员的自我修养》中，linux下的编译链接、可执行文件加载执行相关的内容，整理一些比较常用的内容出来，方便记忆、加深理解。</p>
<h1 id="1-__u7F16_u8BD1_u94FE_u63A5_u5404_u9636_u6BB5_u5B9A_u4E49"><a href="#1-__u7F16_u8BD1_u94FE_u63A5_u5404_u9636_u6BB5_u5B9A_u4E49" class="headerlink" title="1. 编译链接各阶段定义"></a>1. 编译链接各阶段定义</h1><p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/7e890862e49d52a566be88eb622be074.jpg" alt="image.png"><br>gcc 编译过程分解</p>
<h2 id="1-1__u9884_u7F16_u8BD1"><a href="#1-1__u9884_u7F16_u8BD1" class="headerlink" title="1.1 预编译"></a>1.1 预编译</h2><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gcc &#8211;E hello.c &#8211;o hello.i / cpp hello.c &#62; hello.i</span><br></pre></td></tr></table></figure>
<p>预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。比如“#include”、“#define”等。  </p>
<h2 id="1-2__u7F16_u8BD1"><a href="#1-2__u7F16_u8BD1" class="headerlink" title="1.2 编译"></a>1.2 编译</h2><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gcc &#8211;S hello.i &#8211;o hello.s</span><br></pre></td></tr></table></figure>
<p>编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件。  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/a054c3318e5d18ca1311945ae62a77ca.jpg" alt="image.png"></p>
<p>编译过程：<br>词法分析（得到标记） →<br>语法分析（得到语法书） →<br>语义分析（静态语义分析） →<br>机器无关的中间语言生成（源代码级别优化，编译器前端优化） →<br>目标代码生成与优化（目标机器代码生成、目标代码优化器，编译器后端优化）  </p>
<h2 id="1-3__u6C47_u7F16"><a href="#1-3__u6C47_u7F16" class="headerlink" title="1.3 汇编"></a>1.3 汇编</h2><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">as hello.s &#8211;o hello.o / gcc &#8211;c hello.c &#8211;o hello.o</span><br></pre></td></tr></table></figure>
<p>汇编器是将汇编代码转变成机器可以执行的指令，每一个汇编语句几乎都对应一条机器指令。<br>所以汇编器的汇编过程相对于编译器来讲比较简单，它没有复杂的语法，也没有语义，也不需要做指令优化，只是根据汇编指令和机器指令的对照表一一翻译就可以了，“汇编”这个名字也来源于此。</p>
<h2 id="1-4__u94FE_u63A5"><a href="#1-4__u94FE_u63A5" class="headerlink" title="1.4 链接"></a>1.4 链接</h2><p>最早期的计算机程序是打在纸带上的：  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/0c9685e5a53cde79b19c01a278a0a870.jpg" alt="image.png">  </p>
<p>这个程序的第一条指令就是一条跳转指令，它的目的地址是第5条指令（注意，第5条指令的绝对地址是4）。  </p>
<p>一条纸带就相当于一份代码文件，当一个程序由多个纸带（多份代码）组成，要把他们拼接成一张纸条以加载执行时，就需要考虑如何重新分配命令和变量空间、如何重新定位数据位置等问题，这个工作就是链接。  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/7695513f2b5696b362e1dcc504a29eb3.jpg" alt="image.png"></p>
<h1 id="2-__u76EE_u6807_u6587_u4EF6_u5206_u6790"><a href="#2-__u76EE_u6807_u6587_u4EF6_u5206_u6790" class="headerlink" title="2. 目标文件分析"></a>2. 目标文件分析</h1><p>objdump、readelf、nm命令<br>readelf 命令包含了 objdump 和 nm 命令，更全  </p>
<h2 id="2-1__u76EE_u6807_u6587_u4EF6_u7ED3_u6784_u548C_u5185_u5BB9"><a href="#2-1__u76EE_u6807_u6587_u4EF6_u7ED3_u6784_u548C_u5185_u5BB9" class="headerlink" title="2.1 目标文件结构和内容"></a>2.1 目标文件结构和内容</h2><p>源码<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* </span><br><span class="line"> * SimpleSection.c</span><br><span class="line"> * </span><br><span class="line"> * Linux:</span><br><span class="line"> *   gcc -c SimpleSection.c</span><br><span class="line"> *</span><br><span class="line"> * Windows:</span><br><span class="line"> *   cl SimpleSection.c /c /Za</span><br><span class="line"> */</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">printf</span><span class="params">( <span class="keyword">const</span> <span class="keyword">char</span>* format, ... )</span></span>;</span><br><span class="line"><span class="keyword">int</span> global_init_var = <span class="number">84</span>;</span><br><span class="line"><span class="keyword">int</span> global_uninit_var;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">func1</span><span class="params">( <span class="keyword">int</span> i )</span> </span><br><span class="line"></span>&#123;</span><br><span class="line">  <span class="built_in">printf</span>( <span class="string">"%d\n"</span>,  i );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">void</span>)</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">int</span> static_var = <span class="number">85</span>;</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">int</span> static_var2;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">int</span> a = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">int</span> b; </span><br><span class="line"></span><br><span class="line">    func1( static_var + static_var2 + a + b );</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> a; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>编译  gcc –c SimpleSection.c<br>binutils的工具objdump可以用来查看各种目标文件的结构和内容<br>objdump -h SimpleSection.o  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SimpleSection.o:     file format elf32-i386&#10;&#10;Sections:&#10;&#10;Idx Name        Size      VMA         LMA       File off  Algn&#10;  0 .text       0000005b  00000000  00000000  00000034  2**2&#10;                CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE&#10;  1 .data       00000008  00000000  00000000  00000090  2**2&#10;                CONTENTS, ALLOC, LOAD, DATA&#10;  2 .bss        00000004  00000000  00000000  00000098  2**2&#10;                ALLOC&#10;  3 .rodata     00000004  00000000  00000000  00000098  2**0&#10;                CONTENTS, ALLOC, LOAD, READONLY, DATA&#10;  4 .comment    0000002a  00000000  00000000  0000009c  2**0&#10;                CONTENTS, READONLY&#10;  5 .note.GNU-stack 00000000  00000000  00000000  000000c6  2**0&#10;                CONTENTS, READONLY</span><br></pre></td></tr></table></figure>
<p>段名称含义</p>
<ul>
<li>.text: 代码段</li>
<li>.data: 数据段</li>
<li>.bss: 未初始化的数据段  Block Started by Symbo</li>
<li>.rodata 只读数据段 read only data</li>
<li>.comment 注释段</li>
<li>.note.GNU-stack 堆栈提示段</li>
</ul>
<p>列名称含义</p>
<ul>
<li>Size：表示该段（section）在文件中的大小，以字节为单位。</li>
<li>VMA（Virtual Memory Address）：表示该段在虚拟内存中的起始地址。当程序加载到内存中运行时，该段将被映射到这个虚拟地址</li>
<li>LMA（Load Memory Address）：表示该段在加载到物理内存时的起始地址。通常情况下，LMA 和 VMA 是相同的，但在某些特殊情况下，例如运行在 ROM 中的程序，它们可能会有所不同。</li>
<li>File off：表示该段在目标文件中的偏移量，即从文件开始到该段开始的字节数。</li>
<li>Algn（Alignment）：表示该段在内存中的对齐方式。对齐是为了提高内存访问性能，通常以 2 的幂次方表示。例如，22 表示 4 字节对齐，20 表示无需对齐。例如32位系统一次读取4个字节，char a, int b两个变量，未对齐时  abbbbc，要读取两次才能读到4字节b变量，对齐后 a—bbbbc，浪费了3个字节，但是可以一次性读取到 b 变量，速度快。</li>
</ul>
<p>每个段的第二行含义</p>
<ul>
<li>CONTENTS：表示该段在文件中存在，我们可以看到BSS段没有“CONTENTS”，表示它实际上在ELF文件中不存在内容</li>
<li>“.note.GNU-stack”段虽然有“CONTENTS”，但它的长度为0，这是个很古怪的段，我们暂且忽略它，认为它在ELF文件中也不存在</li>
<li>实际存在的也就是“.text”、“.data”、“.rodata”和“.comment”这4个段</li>
</ul>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/9e05e056a470e09dc514150b702bc463.jpg" alt="image.png"></p>
<h3 id="u4EE3_u7801_u6BB5"><a href="#u4EE3_u7801_u6BB5" class="headerlink" title="代码段"></a>代码段</h3><p>objdump的“-s”参数可以将所有段的内容以十六进制的方式打印出来，“-d”参数可以将所有包含指令的段反汇编。我们将objdump输出中关于代码段的内容提取出来，分析一下关于代码段的内容<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ objdump -s -d SimpleSection.o&#10;&#8230;&#8230;&#10;Contents of section .text:&#10; 0000 5589e583 ec088b45 08894424 04c70424  U......E..D$...$&#10; 0010 00000000 e8fcffff ffc9c38d 4c240483  ............L$..&#10; 0020 e4f0ff71 fc5589e5 5183ec14 c745f401  ...q.U..Q....E..&#10; 0030 0000008b 15040000 00a10000 00008d04  ................&#10; 0040 020345f4 0345f889 0424e8fc ffffff8b  ..E..E...$......&#10; 0050 45f483c4 14595d8d 61fcc3             E....Y].a..  &#10;&#8230;&#8230;&#10;00000000 &#60;func1&#62;:&#10;   0:   55                    push   %ebp&#10;   1:   89 e5                 mov    %esp,%ebp&#10;   3:   83 ec 08                  sub    $0x8,%esp&#10;   6:   8b 45 08                  mov    0x8(%ebp),%eax&#10;   9:   89 44 24 04             mov    %eax,0x4(%esp)&#10;   d:   c7 04 24 00 00 00 00  movl   $0x0,(%esp)&#10;  14:   e8 fc ff ff ff        call   15 &#60;func1+0x15&#62;&#10;  19:   c9                        leave&#10;  1a:   c3                        ret&#10;&#10;0000001b &#60;main&#62;:&#10;  1b:   8d 4c 24 04             lea  0x4(%esp),%ecx&#10;  1f:   83 e4 f0                and    $0xfffffff0,%esp&#10;  22:   ff 71 fc                pushl  -0x4(%ecx)&#10;  25:   55                        push   %ebp&#10;  26:   89 e5                   mov    %esp,%ebp&#10;  28:   51                        push   %ecx&#10;  29:   83 ec 14                sub    $0x14,%esp&#10;  2c:   c7 45 f4 01 00 00 00  movl   $0x1,-0xc(%ebp)&#10;  33:   8b 15 04 00 00 00     mov  0x4,%edx&#10;  39:   a1 00 00 00 00          mov  0x0,%eax&#10;  3e:   8d 04 02                lea    (%edx,%eax,1),%eax&#10;  41:   03 45 f4                add[&#8230;]</span><br></pre></td></tr></table></figure></p>
<p>Contents of section .text”就是.text的数据以十六进制方式打印出来的内容，总共0x5b字节，跟前面我们了解到的“.text”段长度相符合，最左面一列是偏移量，中间4列是十六进制内容，最右面一列是.text段的ASCII码形式。对照下面的反汇编结果，可以很明显地看到，.text段里所包含的正是SimpleSection.c里两个函数func1()和main()的指令。.text段的第一个字节“0x55”就是“func1()”函数的第一条“push %ebp”指令，而最后一个字节0xc3正是main()函数的最后一条指令“ret”。</p>
<h3 id="u6570_u636E_u6BB5_u548C_u53EA_u8BFB_u6570_u636E_u6BB5_u3001BSS_u6BB5"><a href="#u6570_u636E_u6BB5_u548C_u53EA_u8BFB_u6570_u636E_u6BB5_u3001BSS_u6BB5" class="headerlink" title="数据段和只读数据段、BSS段"></a>数据段和只读数据段、BSS段</h3><p>data段保存的是那些已经初始化了的全局静态变量和局部静态变量<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ objdump -x -s -d SimpleSection.o</span><br></pre></td></tr></table></figure></p>
<h3 id="u5176_u5B83_u5E38_u7528_u6BB5"><a href="#u5176_u5B83_u5E38_u7528_u6BB5" class="headerlink" title="其它常用段"></a>其它常用段</h3><p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/f1d8c2a30f6df26531db477a8e636a99.jpg" alt="image.png"></p>
<h3 id="size"><a href="#size" class="headerlink" title="size"></a>size</h3><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">size SimpleSection.o</span><br></pre></td></tr></table></figure>
<p>查看各个段的长度<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">text  data  bss  dec  hex  filename&#10;   95     8    4  107   6b  SimpleSection.o</span><br></pre></td></tr></table></figure></p>
<h2 id="2-2_ELF_u6587_u4EF6_u7ED3_u6784"><a href="#2-2_ELF_u6587_u4EF6_u7ED3_u6784" class="headerlink" title="2.2 ELF文件结构"></a>2.2 ELF文件结构</h2><p>ELF文件全称：Executable and Linkable Format<br><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/4dc3f30741d3c6d59f64d0f09f6e6524.jpg" alt="image.png">  </p>
<ul>
<li>ELF Header 文件头 ，描述整个文件的基本属性，比如ELF文件版本、目标机器型号、程序入口地址等<ul>
<li>text ~ other sections  各个段的数据</li>
</ul>
</li>
<li>Section header table 段表，描述了ELF文件包含的所有段的信息，比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性<ul>
<li>除了代码段数据段等段的描述信息外，还包含符号表、字符串表、段名字符串表、重定位表等<h3 id="elf__u6587_u4EF6_u5934"><a href="#elf__u6587_u4EF6_u5934" class="headerlink" title="elf 文件头"></a>elf 文件头</h3><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$readelf &#8211;h SimpleSection.o&#10;ELF Header:&#10;  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00&#10;  Class:                              ELF32&#10;  Data:                             2&#39;s complement, little endian&#10;  Version:                          1 (current)&#10;  OS/ABI:                             UNIX - System V&#10;  ABI Version:                    0&#10;  Type:                               REL (Relocatable file)&#10;  Machine:                            Intel 80386&#10;  Version:                        0x1&#10;  Entry point address:            0x0&#10;  Start of program headers:       0 (bytes into file)&#10;  Start of section headers:       280 (bytes into file)&#10;  Flags:                          0x0&#10;  Size of this header:          52 (bytes)&#10;  Size of program headers:        0 (bytes)&#10;  Number of program headers:      0&#10;  Size of section headers:        40 (bytes)&#10;  Number of section headers:      11&#10;  Section header string table index:  8</span><br></pre></td></tr></table></figure>
</li>
</ul>
</li>
</ul>
<p>ELF的文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。 </p>
<p>ELF文件结构及相关常数被定义在“/usr/include/elf.h”里，因为ELF文件在各种平台下都通用，ELF文件有32位版本和64位版本。它的文件头结构也有这两种版本，分别叫做“Elf32_Ehdr”和“Elf64_Ehdr”。  </p>
<h3 id="u6BB5_u8868"><a href="#u6BB5_u8868" class="headerlink" title="段表"></a>段表</h3><p>段表数据结构<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="keyword">struct</span></span><br><span class="line">&#123;</span><br><span class="line">  Elf64_Word    sh_name;                <span class="comment">/* Section name (string tbl index) */</span></span><br><span class="line">  Elf64_Word    sh_type;                <span class="comment">/* Section type */</span></span><br><span class="line">  Elf64_Xword   sh_flags;               <span class="comment">/* Section flags */</span></span><br><span class="line">  Elf64_Addr    sh_addr;                <span class="comment">/* Section virtual addr at execution */</span></span><br><span class="line">  Elf64_Off     sh_offset;              <span class="comment">/* Section file offset */</span></span><br><span class="line">  Elf64_Xword   sh_size;                <span class="comment">/* Section size in bytes */</span></span><br><span class="line">  Elf64_Word    sh_link;                <span class="comment">/* Link to another section */</span></span><br><span class="line">  Elf64_Word    sh_info;                <span class="comment">/* Additional section information */</span></span><br><span class="line">  Elf64_Xword   sh_addralign;           <span class="comment">/* Section alignment */</span></span><br><span class="line">  Elf64_Xword   sh_entsize;             <span class="comment">/* Entry size if section holds table */</span></span><br><span class="line">&#125; Elf64_Shdr;</span><br></pre></td></tr></table></figure></p>
<p>查看段表<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ readelf -S SimpleSection.o&#10;There are 11 section headers, starting at offset 0x118:&#10;&#10;Section Headers:&#10; [Nr] Name          Type      Addr     Off    Size   ES Flg Lk Inf Al&#10; [ 0]               NULL      00000000 000000 000000 00 0   0  0&#10; [ 1] .text         PROGBITS  00000000 000034 00005b 00 AX  0  0   4&#10; [ 2] .rel.text     REL       00000000 000428 000028 08     9  1   4&#10; [ 3] .data         PROGBITS  00000000 000090 000008 00 WA  0  0   4&#10; [ 4] .bss          NOBITS    00000000 000098 000004 00 WA  0  0   4&#10; [ 5] .rodata       PROGBITS  00000000 000098 000004 00 A   0  0   1&#10; [ 6] .comment        PROGBITS  00000000 00009c 00002a 00 0   0  1&#10; [ 7] .note.GNU-stack PROGBITS  00000000 0000c6 000000 00 0   0  1&#10; [ 8] .shstrtab   STRTAB    00000000 0000c6 000051 00 0   0  1&#10; [ 9] .symtab       SYMTAB    00000000 0002d0 0000f0 10     10 10   4&#10; [10] .strtab       STRTAB    00000000 0003c0 000066 00 0   0  1&#10;Key to Flags:&#10;  W (write), A (alloc), X (execute), M (merge), S (strings)&#10;  I (info), L (link order), G (group), x (unknown)&#10;  O (extra OS processing required) o (OS specific), p (processor specific)</span><br></pre></td></tr></table></figure></p>
<p>结构描述  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/b8e0f05ccf66e9208d65b35b915132f1.jpg" alt="image.png">  </p>
<p>段的类型  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/e7408583051d3e646295d3c8a9869c57.jpg" alt="image.png">  </p>
<h3 id="symtab__u7B26_u53F7_u8868"><a href="#symtab__u7B26_u53F7_u8868" class="headerlink" title=".symtab 符号表"></a>.symtab 符号表</h3><p>后面细说</p>
<h3 id="rel-text_/_-rel-data__u91CD_u5B9A_u4F4D_u8868"><a href="#rel-text_/_-rel-data__u91CD_u5B9A_u4F4D_u8868" class="headerlink" title=".rel.text / .rel.data 重定位表"></a>.rel.text / .rel.data 重定位表</h3><p>链接器在处理目标文件时，须要对目标文件中某些部位进行重定位，即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件的重定位表里面，对于每个须要重定位的代码段或数据段，都会有一个相应的重定位表。</p>
<h3 id="strtab__u5B57_u7B26_u4E32_u8868"><a href="#strtab__u5B57_u7B26_u4E32_u8868" class="headerlink" title=".strtab 字符串表"></a>.strtab 字符串表</h3><p>ELF文件中用到了很多字符串，比如段名、变量名等。因为字符串的长度往往是不定的，所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表，然后使用字符串在表中的偏移来引用字符串。  </p>
<h2 id="2-3__u7B26_u53F7"><a href="#2-3__u7B26_u53F7" class="headerlink" title="2.3 符号"></a>2.3 符号</h2><h3 id="u67E5_u770B_u76EE_u6807_u6587_u4EF6_u7684_u7B26_u53F7_u8868"><a href="#u67E5_u770B_u76EE_u6807_u6587_u4EF6_u7684_u7B26_u53F7_u8868" class="headerlink" title="查看目标文件的符号表"></a>查看目标文件的符号表</h3><p>在链接中，我们将函数和变量统称为符号（Symbol），函数名或变量名就是符号名（Symbol Name）。<br>用“nm”来查看“SimpleSection.o”的符号结果如下：<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ nm SimpleSection.o&#10;00000000 T func1&#10;00000000 D global_init_var&#10;00000004 C global_uninit_var&#10;0000001b T main&#10;U printf&#10;00000004 d static_var.1286&#10;00000000 b static_var2.1287</span><br></pre></td></tr></table></figure></p>
<h3 id="u7B26_u53F7_u8868_u6570_u636E_u7ED3_u6784"><a href="#u7B26_u53F7_u8868_u6570_u636E_u7ED3_u6784" class="headerlink" title="符号表数据结构"></a>符号表数据结构</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="keyword">struct</span> &#123;</span><br><span class="line">    Elf32_Word st_name;</span><br><span class="line">    Elf32_Addr st_value;</span><br><span class="line">    Elf32_Word st_size;</span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">char</span> st_info;</span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">char</span> st_other;</span><br><span class="line">    Elf32_Half st_shndx;</span><br><span class="line">&#125; Elf32_Sym;</span><br></pre></td></tr></table></figure>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/f660ecc8b84411f01278fa2fc47d6295.jpg" alt="image.png">  </p>
<h3 id="u4F7F_u7528_readelf__u67E5_u770B_u5B8C_u6574_u7B26_u53F7_u8868"><a href="#u4F7F_u7528_readelf__u67E5_u770B_u5B8C_u6574_u7B26_u53F7_u8868" class="headerlink" title="使用 readelf 查看完整符号表"></a>使用 readelf 查看完整符号表</h3><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">readelf &#8211;s SimpleSection.o&#10;&#10;Symbol table &#39;.symtab&#39; contains 15 entries:&#10;   Num:    Value  Size  Type    Bind   Vis      Ndx Name&#10;     0: 00000000  0   NOTYPE  LOCAL  DEFAULT  UND &#10;     1: 00000000  0   FILE    LOCAL  DEFAULT  ABS SimpleSection.c&#10;     2: 00000000  0   SECTION LOCAL  DEFAULT    1 &#10;     3: 00000000  0   SECTION LOCAL  DEFAULT    3 &#10;     4: 00000000  0   SECTION LOCAL  DEFAULT    4 &#10;     5: 00000000  0   SECTION LOCAL  DEFAULT    5 &#10;     6: 00000000    4   OBJECT  LOCAL  DEFAULT    4 static_var2.1534&#10;     7: 00000004    4   OBJECT  LOCAL  DEFAULT    3 static_var.1533&#10;     8: 00000000  0   SECTION LOCAL  DEFAULT    7 &#10;     9: 00000000  0   SECTION LOCAL  DEFAULT    6 &#10;    10: 00000000    4   OBJECT  GLOBAL DEFAULT    3 global_init_var&#10;    11: 00000000  27  FUNC    GLOBAL DEFAULT    1 func1&#10;    12: 00000000  0   NOTYPE  GLOBAL DEFAULT  UND printf&#10;    13: 0000001b  64  FUNC    GLOBAL DEFAULT    1 main&#10;    14: 00000004    4   OBJECT  GLOBAL DEFAULT  COM global_uninit_var</span><br></pre></td></tr></table></figure>
<p>对于变量和函数来说，符号值就是它们的地址。除了函数和变量之外，还存在其他几种不常用到的符号。我们将符号表中所有的符号进行分类，它们有可能是下面这些类型中的一种：  </p>
<ul>
<li>定义在本目标文件的全局符号，可以被其他目标文件引用。比如SimpleSection.o里面的“func1”、“main”和“global_init_var”</li>
<li>段名，这种符号往往由编译器产生，它的值就是该段的起始地址。比如SimpleSection.o里面的“.text”、“.data”等。</li>
<li>局部符号，这类符号只在编译单元内部可见。比如SimpleSection.o里面的“static_var”和“static_var2”。调试器可以使用这些符号来分析程序或崩溃时的核心转储文件。这些局部符号对于链接过程没有作用，链接器往往也忽略它们。</li>
<li>行号信息，即目标文件指令与源代码中代码行的对应关系，它也是可选的<h3 id="u7279_u6B8A_u7B26_u53F7"><a href="#u7279_u6B8A_u7B26_u53F7" class="headerlink" title="特殊符号"></a>特殊符号</h3></li>
<li>__executable_start，该符号为程序起始地址，注意，不是入口地址，是程序的最开始的地址。</li>
<li>__etext或_etext或etext，该符号为代码段结束地址，即代码段最末尾的地址。</li>
<li>_edata或edata，该符号为数据段结束地址，即数据段最末尾的地址。</li>
<li>_end或end，该符号为程序结束地址。  </li>
</ul>
<p>这些符号可以在代码中使用，比如给打印出来。  </p>
<h3 id="u7B26_u53F7_u4FEE_u9970"><a href="#u7B26_u53F7_u4FEE_u9970" class="headerlink" title="符号修饰"></a>符号修饰</h3><p>简单下划线修饰：_foo、<em>foo</em>  </p>
<p>c++修饰规则  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/7678f2b7d817626f84678aa81d352f19.jpg" alt="image.png">  </p>
<p>所有的符号都以“_Z”开头，对于嵌套的名字（在名称空间或在类里面的），后面紧跟“N”，然后是各个名称空间和类的名字，每个名字前是名字字符串长度，再以“E”结尾。比如N::C::func经过名称修饰以后就是_ZN1N1C4funcE。  </p>
<h3 id="extern__u201CC_u201D"><a href="#extern__u201CC_u201D" class="headerlink" title="extern “C”"></a>extern “C”</h3><p>被这个包含的符号不会给加修饰<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#ifdef __cplusplus&#10;extern &#34;C&#34; &#123;&#10;#endif&#10; &#10;void *memset (void *, int, size_t);&#10;&#10;#ifdef __cplusplus&#10;&#125;&#10;#endif</span><br></pre></td></tr></table></figure></p>
<p>可以用 __cplusplus 让c++程序可以链接到c的 memset，同时也能让不支持 extern “C” 的 c 语言引入这个头文件</p>
<h2 id="2-4__u8C03_u8BD5_u4FE1_u606F"><a href="#2-4__u8C03_u8BD5_u4FE1_u606F" class="headerlink" title="2.4 调试信息"></a>2.4 调试信息</h2><p>在GCC编译时加上“-g”参数，编译器就会在产生的目标文件里面加上调试信息，我们通过readelf等工具可以看到，目标文件里多了很多“debug”相关的段。<br>调试信息在目标文件和可执行文件中占用很大的空间，往往比程序的代码和数据本身大好几倍，所以当我们开发完程序并要将它发布的时候，须要把这些对于用户没有用的调试信息去掉，以节省大量的空间。在Linux下，我们可以使用“strip”命令来去掉ELF文件中的调试信息：<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$strip foo</span><br></pre></td></tr></table></figure></p>
<h1 id="3-__u9759_u6001_u94FE_u63A5"><a href="#3-__u9759_u6001_u94FE_u63A5" class="headerlink" title="3. 静态链接"></a>3. 静态链接</h1><h2 id="3-1__u7A7A_u95F4_u548C_u5730_u5740_u5206_u914D"><a href="#3-1__u7A7A_u95F4_u548C_u5730_u5740_u5206_u914D" class="headerlink" title="3.1 空间和地址分配"></a>3.1 空间和地址分配</h2><h3 id="u7B80_u5355_u6309_u5E8F_u53E0_u52A0"><a href="#u7B80_u5355_u6309_u5E8F_u53E0_u52A0" class="headerlink" title="简单按序叠加"></a>简单按序叠加</h3><p>最简单的想法：把各个目标文件直接拼接在一起<br>存在的问题：空间浪费，每个段即使只有1字节，也可能占用一个内存页的空间，如4096字节  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/61908056294611a458512f5738f4ee28.jpg" alt="image.png">  </p>
<h3 id="u76F8_u4F3C_u6BB5_u5408_u5E76"><a href="#u76F8_u4F3C_u6BB5_u5408_u5E76" class="headerlink" title="相似段合并"></a>相似段合并</h3><p>第一步 空间与地址分配 扫描所有的输入目标文件，并且获得它们的各个段的长度、属性和位置，并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来，统一放到一个全局符号表。这一步中，链接器将能够获得所有输入目标文件的段长度，并且将它们合并，计算出输出文件中各个段合并后的长度与位置，并建立映射关系。  </p>
<p>第二步 符号解析与重定位 使用上面第一步中收集到的所有信息，读取输入文件中段的数据、重定位信息，并且进行符号解析与重定位、调整代码中的地址等。事实上第二步是链接过程的核心，特别是重定位过程。  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/a3273a616f1a28f28de7b62ecc19fc4c.jpg" alt="image.png">  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ld a.o b.o -e main -o ab</span><br></pre></td></tr></table></figure>
<p>链接后的结果  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ objdump -h a.o&#10;Sections:&#10;Idx Name          Size    VMA       LMA       File off  Algn&#10;  0 .text       00000034  00000000  00000000  00000034  2**2&#10;                CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE&#10;  1 .data       00000000  00000000  00000000  00000068  2**2&#10;                CONTENTS, ALLOC, LOAD, DATA&#10;  2 .bss        00000000  00000000  00000000  00000068  2**2&#10;                ALLOC&#10;...&#10;$ objdump -h b.o&#10;...&#10;Sections:&#10;Idx Name        Size      VMA       LMA       File off  Algn&#10;  0 .text       0000003e  00000000  00000000  00000034  2**2&#10;                CONTENTS, ALLOC, LOAD, READONLY, CODE&#10;  1 .data       00000004  00000000  00000000  00000074  2**2&#10;                CONTENTS, ALLOC, LOAD, DATA&#10;  2 .bss        00000000  00000000  00000000  00000078  2**2&#10;                ALLOC&#10;...&#10;$objdump &#8211;h ab&#10;...&#10;Sections:&#10;Idx Name          Size      VMA       LMA       File off  Algn&#10;  0 .text       00000072  08048094  08048094  00000094  2**2&#10;                  CONTENTS, ALLOC, LOAD, READONLY, CODE&#10;  1 .data       00000004  08049108  08049108  00000108  2**2&#10;                  CONTENTS, ALLOC, LOAD, DATA</span><br></pre></td></tr></table></figure>
<p>VMA表示Virtual Memory Address，即虚拟地址，LMA表示Load Memory Address，即加载地址，正常情况下这两个值应该是一样的，但是在有些嵌入式系统中，特别是在那些程序放在ROM的系统中时，LMA和VMA是不相同的。这里我们只要关注VMA即可。  </p>
<p>链接前后的程序中所使用的地址已经是程序在进程中的虚拟地址，即我们关心上面各个段中的VMA（Virtual Memory Address）和Size，而忽略文件偏移（File off）。  </p>
<p>我们可以看到，在链接之前，目标文件中的所有段的VMA都是0，因为虚拟空间还没有被分配，所以它们默认都为0。等到链接之后，可执行文件“ab”中的各个段都被分配到了相应的虚拟地址。这里的输出程序“ab”中，“.text”段被分配到了地址0x08048094，大小为0x72字节；“.data”段从地址0x08049108开始，大小为4字节。如下图所示。  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/5a5acc74bf524dcdc4bc916cd46522dd.jpg" alt="image.png">  </p>
<h2 id="3-2__u7B26_u53F7_u7684_u89E3_u6790_u548C_u91CD_u5B9A_u4F4D"><a href="#3-2__u7B26_u53F7_u7684_u89E3_u6790_u548C_u91CD_u5B9A_u4F4D" class="headerlink" title="3.2 符号的解析和重定位"></a>3.2 符号的解析和重定位</h2><h3 id="u91CD_u5B9A_u4F4D"><a href="#u91CD_u5B9A_u4F4D" class="headerlink" title="重定位"></a>重定位</h3><p>根据段表中的重定位表，对链接后目标文件中依赖外部目标文件的变量和指令的方式进行调整，指向真正的虚拟地址中的位置。<br>重定位前：  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/0beac198c8987e0372c687a06a2410fb.jpg" alt="image.png">  </p>
<p>重定位后：  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/98869456d7f80edb10e5a94c064279b2.jpg" alt="image.png">  </p>
<h3 id="u7B26_u53F7_u89E3_u6790"><a href="#u7B26_u53F7_u89E3_u6790" class="headerlink" title="符号解析"></a>符号解析</h3><p>在我们通常的观念里，之所以要链接是因为我们目标文件中用到的符号被定义在其他目标文件，所以要将它们链接起来。比如我们直接使用ld来链接“a.o”，而不将“b.o”作为输入。链接器就会发现shared和swap两个符号没有被定义，没有办法完成链接工作：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ld a.o&#10;a.o: In function `main&#39;:&#10;a.c:(.text+0x1c): undefined reference to `shared&#39;&#10;a.c:(.text+0x27): undefined reference to `swap</span><br></pre></td></tr></table></figure>
<p>导致这个问题的原因很多，最常见的一般都是链接时缺少了某个库，或者输入目标文件路径不正确或符号的声明与定义不一样。  </p>
<p>重定位过程也伴随着符号的解析过程，每个目标文件都可能定义一些符号，也可能引用到定义在其他目标文件的符号。重定位的过程中，每个重定位的入口都是对一个符号的引用，那么当链接器须要对某个符号的引用进行重定位时，它就要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件的符号表组成的全局符号表，找到相应的符号后进行重定位。  </p>
<h2 id="3-3_c++_u76F8_u5173"><a href="#3-3_c++_u76F8_u5173" class="headerlink" title="3.3 c++相关"></a>3.3 c++相关</h2><h3 id="u5168_u5C40_u6784_u9020/_u6790_u6784"><a href="#u5168_u5C40_u6784_u9020/_u6790_u6784" class="headerlink" title="全局构造/析构"></a>全局构造/析构</h3><p>程序的一些特定的操作必须在main函数之前被执行，还有一些操作必须在main函数之后被执行，其中很具有代表性的就是C++的全局对象的构造和析构函数。因此ELF文件还定义了两种特殊的段。  </p>
<p>.init 该段里面保存的是可执行指令，它构成了进程的初始化代码。因此，当一个程序开始运行时，在main函数被调用之前，Glibc的初始化部分安排执行这个段的中的代码。  </p>
<p>.fini 该段保存着进程终止代码指令。因此，当一个程序的main函数正常退出时，Glibc会安排执行这个段中的代码。<br>这两个段.init和.fini的存在有着特别的目的，如果一个函数放到.init段，在main函数执行前系统就会执行它。同理，假如一个函数放到.fint段，在main函数返回后该函数就会被执行。利用这两个特性，C++的全局构造和析构函数就由此实现。  </p>
<h3 id="ABI_u63A5_u53E3"><a href="#ABI_u63A5_u53E3" class="headerlink" title="ABI接口"></a>ABI接口</h3><p>如果要使两个编译器编译出来的目标文件能够相互链接，那么这两个目标文件必须满足下面这些条件：采用同样的目标文件格式、拥有同样的符号修饰标准、变量的内存分布方式相同、函数的调用方式相同，等等。  </p>
<p>其中我们把符号修饰标准、变量内存布局、函数调用方式等这些跟可执行代码二进制兼容性相关的内容称为ABI（Application Binary Interface）。  </p>
<h2 id="3-4__u9759_u6001_u5E93_u94FE_u63A5"><a href="#3-4__u9759_u6001_u5E93_u94FE_u63A5" class="headerlink" title="3.4 静态库链接"></a>3.4 静态库链接</h2><p>一个静态库可以看做一组目标文件的集合，用下面的命令可以查看静态库的目标文件组成，后面会细说静态库链接过程。<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ar -t libc.a&#10;init-first.o&#10;libc-start.o&#10;sysdep.o&#10;version.o&#10;check_fds.o&#10;libc-tls.o&#10;......</span><br></pre></td></tr></table></figure></p>
<p>通过静态链接生成一个可执行文件，除了程序本身的目标代码，还有一堆系统的静态库需要被链接进去，以支持程序的加载执行。  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/c93816ec0bed9968f627d8e89d711abb.jpg" alt="image.png"></p>
<h1 id="4-__u53EF_u6267_u884C_u6587_u4EF6_u7684_u88C5_u8F7D_u4E0E_u8FDB_u7A0B"><a href="#4-__u53EF_u6267_u884C_u6587_u4EF6_u7684_u88C5_u8F7D_u4E0E_u8FDB_u7A0B" class="headerlink" title="4. 可执行文件的装载与进程"></a>4. 可执行文件的装载与进程</h1><h2 id="4-1__u865A_u62DF_u5730_u5740_u7A7A_u95F4"><a href="#4-1__u865A_u62DF_u5730_u5740_u7A7A_u95F4" class="headerlink" title="4.1 虚拟地址空间"></a>4.1 虚拟地址空间</h2><p>硬件决定了地址空间的最大理论上限，即硬件的寻址空间大小，比如32位的硬件平台决定了虚拟地址空间的地址为 0 到 2^32-1，即0x00000000～0xFFFFFFFF，也就是我们常说的4 GB虚拟空间大小；而64位的硬件平台具有64位寻址能力，它的虚拟地址空间达到了264字节，即0x0000000000000000～0xFFFFFFFFFFFFFFFF，总共17 179 869 184 GB，这个寻址能力从现在来看，几乎是无限的。  </p>
<p>32位硬件平台虚拟地址空间的一种分配示例如下  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/1d9a57a8a6e9efb5ac88ebfb97cfb6e7.jpg" alt="image.png">  </p>
<p>通过 PAE（Physical Address Extension）等方式，可以在32位平台将地址位扩大到36位，从而访问64G的内存，不过一般情况下只有操作系统感知这种做法。  </p>
<p>应用程序可以分配一块内存区出来转门用于内存映射，如 linux 下的 mmap 技术来使用大于 4G 的空间。   </p>
<h2 id="4-2__u88C5_u8F7D_u65B9_u5F0F"><a href="#4-2__u88C5_u8F7D_u65B9_u5F0F" class="headerlink" title="4.2 装载方式"></a>4.2 装载方式</h2><h3 id="u8986_u76D6_u88C5_u5165"><a href="#u8986_u76D6_u88C5_u5165" class="headerlink" title="覆盖装入"></a>覆盖装入</h3><p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/4db96a9b15bb1c0b3fdf98fe78b18dd4.jpg" alt="image.png"></p>
<p>覆盖装入的方法把挖掘内存潜力的任务交给了程序员，程序员在编写程序的时候必须手工将程序分割成若干块，然后编写一个小的辅助代码来管理这些模块何时应该驻留内存而何时应该被替换掉。这个小的辅助代码就是所谓的覆盖管理器（Overlay Manager）。  </p>
<h3 id="u9875_u6620_u5C04"><a href="#u9875_u6620_u5C04" class="headerlink" title="页映射"></a>页映射</h3><p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/809c8858acce31d6cfd02dd48bece07f.jpg" alt="image.png"></p>
<p>与覆盖装入的原理相似，页映射也不是一下子就把程序的所有数据和指令都装入内存，而是将内存和所有磁盘中的数据和指令按照“页（Page）”为单位划分成若干个页，以后所有的装载和操作的单位就是页。  </p>
<h2 id="4-3__u4ECE_u64CD_u4F5C_u7CFB_u7EDF_u89D2_u5EA6_u770B_u53EF_u6267_u884C_u6587_u4EF6_u7684_u88C5_u8F7D"><a href="#4-3__u4ECE_u64CD_u4F5C_u7CFB_u7EDF_u89D2_u5EA6_u770B_u53EF_u6267_u884C_u6587_u4EF6_u7684_u88C5_u8F7D" class="headerlink" title="4.3 从操作系统角度看可执行文件的装载"></a>4.3 从操作系统角度看可执行文件的装载</h2><h3 id="u8FDB_u7A0B_u7684_u5EFA_u7ACB"><a href="#u8FDB_u7A0B_u7684_u5EFA_u7ACB" class="headerlink" title="进程的建立"></a>进程的建立</h3><p>从操作系统的角度来看，一个进程最关键的特征是它拥有独立的虚拟地址空间，这使得它有别于其他进程。<br>创建一个进程，然后装载相应的可执行文件并且执行，在有虚拟存储的情况下，上述过程最开始只需要做三件事情：  </p>
<ul>
<li>创建一个独立的虚拟地址空间。</li>
<li>读取可执行文件头，并且建立虚拟空间与可执行文件的映射关系。</li>
<li>将CPU的指令寄存器设置成可执行文件的入口地址，启动运行。</li>
</ul>
<h2 id="4-4__u8FDB_u7A0B_u865A_u62DF_u7A7A_u95F4_u5206_u5E03"><a href="#4-4__u8FDB_u7A0B_u865A_u62DF_u7A7A_u95F4_u5206_u5E03" class="headerlink" title="4.4 进程虚拟空间分布"></a>4.4 进程虚拟空间分布</h2><h3 id="ELF_u6587_u4EF6_u94FE_u63A5_u89C6_u56FE_u548C_u6267_u884C_u89C6_u56FE"><a href="#ELF_u6587_u4EF6_u94FE_u63A5_u89C6_u56FE_u548C_u6267_u884C_u89C6_u56FE" class="headerlink" title="ELF文件链接视图和执行视图"></a>ELF文件链接视图和执行视图</h3><p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/cd8ecc2a428e13256a915559638c22f8.jpg" alt="image.png"></p>
<p>对于相同权限的段，把它们合并到一起当作一个段进行映射。比如有两个段分别叫“.text”和“.init”，它们包含的分别是程序的可执行代码和初始化代码，并且它们的权限相同，都是可读并且可执行的。假设.text为4 097字节，.init为512字节，这两个段分别映射的话就要占用三个页面，但是，如果将它们合并成一起映射的话只须占用两个页面。  </p>
<p>ELF可执行文件引入了一个概念叫做“Segment”，上面合并后的映射就叫做一个“Segment”，包含一个或多个属性类似的ELF段（“Section”）。  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/c4547dda1eb9dc3f022c17bef6a58d5a.jpg" alt="image.png">  </p>
<p>ELF可执行文件中有一个专门的数据结构叫做程序头表（Program Header Table）用来保存“Segment”的信息。因为ELF目标文件不需要被装载，所以它没有程序头表，而ELF的可执行文件和共享库文件都有。跟段表结构一样，程序头表也是一个结构体数组，它的结构体如下：  </p>
<figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="keyword">struct</span> &#123;</span><br><span class="line">    Elf32_Word p_type;</span><br><span class="line">    Elf32_Off p_offset;</span><br><span class="line">    Elf32_Addr p_vaddr;</span><br><span class="line">    Elf32_Addr p_paddr;</span><br><span class="line">    Elf32_Word p_filesz;</span><br><span class="line">    Elf32_Word p_memsz;</span><br><span class="line">    Elf32_Word p_flags;</span><br><span class="line">    Elf32_Word p_align;</span><br><span class="line">&#125; Elf32_Phdr;</span><br></pre></td></tr></table></figure>
<p>Elf32_Phdr结构体的几个成员与前面我们使用“readelf –l”打印文件头表显示的结果一一对应。Elf32_Phdr结构的各个成员的基本含义如下  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/081f62d8cbd2ff92892b79610bf774f4.jpg" alt="image.png">  </p>
<h3 id="u5806_u548C_u6808"><a href="#u5806_u548C_u6808" class="headerlink" title="堆和栈"></a>堆和栈</h3><p>在操作系统里面，VMA除了被用来映射可执行文件中的各个“Segment”以外，它还可以有其他的作用，操作系统通过使用VMA来对进程的地址空间进行管理。我们知道进程在执行的时候它还需要用到栈（Stack）、堆（Heap）等空间，事实上它们在进程的虚拟空间中的表现也是以VMA的形式存在的，很多情况下，一个进程中的栈和堆分别都有一个对应的VMA。在Linux下，我们可以通过查看“/proc”来查看进程的虚拟空间分布：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ./SectionMapping.elf &#38;&#10;[1] 21963&#10;$ cat /proc/21963/maps&#10;08048000-080b9000 r-xp 00000000 08:01 2801887    ./SectionMapping.elf&#10;080b9000-080bb000 rwxp 00070000 08:01 2801887    ./SectionMapping.elf&#10;080bb000-080de000 rwxp 080bb000 00:00 0          [heap]&#10;bf7ec000-bf802000 rw-p bf7ec000 00:00 0          [stack]&#10;ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]&#10;```  &#10;&#10;&#31532;&#19968;&#21015;&#26159;VMA&#30340;&#22320;&#22336;&#33539;&#22260;&#65307;&#31532;&#20108;&#21015;&#26159;VMA&#30340;&#26435;&#38480;&#65292;&#8220;r&#8221;&#34920;&#31034;&#21487;&#35835;&#65292;&#8220;w&#8221;&#34920;&#31034;&#21487;&#20889;&#65292;&#8220;x&#8221;&#34920;&#31034;&#21487;&#25191;&#34892;&#65292;&#8220;p&#8221;&#34920;&#31034;&#31169;&#26377;&#65288;COW, Copy on Write&#65289;&#65292;&#8220;s&#8221;&#34920;&#31034;&#20849;&#20139;&#12290;&#31532;&#19977;&#21015;&#26159;&#20559;&#31227;&#65292;&#34920;&#31034;VMA&#23545;&#24212;&#30340;Segment&#22312;&#26144;&#20687;&#25991;&#20214;&#20013;&#30340;&#20559;&#31227;&#65307;&#31532;&#22235;&#21015;&#34920;&#31034;&#26144;&#20687;&#25991;&#20214;&#25152;&#22312;&#35774;&#22791;&#30340;&#20027;&#35774;&#22791;&#21495;&#21644;&#27425;&#35774;&#22791;&#21495;&#65307;&#31532;&#20116;&#21015;&#34920;&#31034;&#26144;&#20687;&#25991;&#20214;&#30340;&#33410;&#28857;&#21495;&#12290;&#26368;&#21518;&#19968;&#21015;&#26159;&#26144;&#20687;&#25991;&#20214;&#30340;&#36335;&#24452;&#12290;  &#10;&#10;&#21487;&#20197;&#30475;&#21040;&#36827;&#31243;&#20013;&#26377;5&#20010;VMA&#65292;&#21482;&#26377;&#21069;&#20004;&#20010;&#26159;&#26144;&#23556;&#21040;&#21487;&#25191;&#34892;&#25991;&#20214;&#20013;&#30340;&#20004;&#20010;Segment&#12290;&#21478;&#22806;&#19977;&#20010;&#27573;&#30340;&#25991;&#20214;&#25152;&#22312;&#35774;&#22791;&#20027;&#35774;&#22791;&#21495;&#21644;&#27425;&#35774;&#22791;&#21495;&#21450;&#25991;&#20214;&#33410;&#28857;&#21495;&#37117;&#26159;0&#65292;&#21017;&#34920;&#31034;&#23427;&#20204;&#27809;&#26377;&#26144;&#23556;&#21040;&#25991;&#20214;&#20013;&#65292;&#36825;&#31181;VMA&#21483;&#20570;&#21311;&#21517;&#34394;&#25311;&#20869;&#23384;&#21306;&#22495;&#65288;Anonymous Virtual Memory Area&#65289;&#12290;  &#10;&#10;&#26377;&#20004;&#20010;&#21306;&#22495;&#20998;&#21035;&#26159;&#22534;&#65288;Heap&#65289;&#21644;&#26632;&#65288;Stack&#65289;&#65292;&#23427;&#20204;&#30340;&#22823;&#23567;&#20998;&#21035;&#20026;140 KB&#21644;88 KB&#12290;&#36825;&#20004;&#20010;VMA&#20960;&#20046;&#22312;&#25152;&#26377;&#30340;&#36827;&#31243;&#20013;&#23384;&#22312;&#65292;&#25105;&#20204;&#22312;C&#35821;&#35328;&#31243;&#24207;&#37324;&#38754;&#26368;&#24120;&#29992;&#30340;malloc()&#20869;&#23384;&#20998;&#37197;&#20989;&#25968;&#23601;&#26159;&#20174;&#22534;&#37324;&#38754;&#20998;&#37197;&#30340;&#65292;&#22534;&#30001;&#31995;&#32479;&#24211;&#31649;&#29702;&#12290;&#26632;&#19968;&#33324;&#20063;&#21483;&#20570;&#22534;&#26632;&#65292;&#25105;&#20204;&#30693;&#36947;&#27599;&#20010;&#32447;&#31243;&#37117;&#26377;&#23646;&#20110;&#33258;&#24049;&#30340;&#22534;&#26632;&#65292;&#23545;&#20110;&#21333;&#32447;&#31243;&#30340;&#31243;&#24207;&#26469;&#35762;&#65292;&#36825;&#20010;VMA&#22534;&#26632;&#23601;&#20840;&#37117;&#24402;&#23427;&#20351;&#29992;&#12290;&#10;&#19968;&#20010;&#24120;&#35265;&#30340;&#36827;&#31243;&#30340;&#34394;&#25311;&#31354;&#38388;&#65306;  &#10;&#10;![image.png](https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/d27f9929eced110716f29feb72afc32d.jpg)  &#10;&#10;## 4.5 linux&#20869;&#26680;&#35013;&#36733;ELF&#30340;&#36807;&#31243;&#10;&#22312;Linux&#31995;&#32479;&#30340;bash&#19979;&#36755;&#20837;&#19968;&#20010;&#21629;&#20196;&#25191;&#34892;&#26576;&#20010;ELF&#31243;&#24207;&#26102;&#65292;bash&#36827;&#31243;&#20250;&#35843;&#29992;fork()&#31995;&#32479;&#35843;&#29992;&#21019;&#24314;&#19968;&#20010;&#26032;&#30340;&#36827;&#31243;&#65292;&#28982;&#21518;&#26032;&#30340;&#36827;&#31243;&#35843;&#29992;execve()&#31995;&#32479;&#35843;&#29992;&#25191;&#34892;&#25351;&#23450;&#30340;ELF&#25991;&#20214;&#65292;&#21407;&#20808;&#30340;bash&#36827;&#31243;&#32487;&#32493;&#36820;&#22238;&#31561;&#24453;&#21018;&#25165;&#21551;&#21160;&#30340;&#26032;&#36827;&#31243;&#32467;&#26463;&#65292;&#28982;&#21518;&#32487;&#32493;&#31561;&#24453;&#29992;&#25143;&#36755;&#20837;&#21629;&#20196;&#12290;&#10;```c&#10;int execve(const char *filename, char *const argv[], char *const envp[]);</span><br></pre></td></tr></table></figure>
<p>三个参数分别是被执行的程序文件名、执行参数和环境变量。Glibc对execvp()系统调用进行了包装，提供了execl()、execlp()、execle()、execv()和execvp()等5个不同形式的exec系列API。<br>下面是一个简单的使用fork()和execlp()实现的minibash：<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;sys/types.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">char</span> buf[<span class="number">1024</span>] = &#123;<span class="number">0</span>&#125;;</span><br><span class="line">    <span class="keyword">pid_t</span> pid;</span><br><span class="line">    <span class="keyword">while</span>(<span class="number">1</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"minibash$"</span>);</span><br><span class="line">        <span class="built_in">scanf</span>(<span class="string">"%s"</span>, buf);</span><br><span class="line">        pid = fork();</span><br><span class="line">        <span class="keyword">if</span>(pid == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span>(execlp(buf, <span class="number">0</span> ) &lt; <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="built_in">printf</span>(<span class="string">"exec error\n"</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span>(pid &gt; <span class="number">0</span>)&#123;</span><br><span class="line">            <span class="keyword">int</span> status;</span><br><span class="line">            waitpid(pid,&amp;status,<span class="number">0</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="built_in">printf</span>(<span class="string">"fork error %d\n"</span>,pid);</span><br><span class="line">        &#125;</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></pre></td></tr></table></figure></p>
<h1 id="5-_u52A8_u6001_u94FE_u63A5"><a href="#5-_u52A8_u6001_u94FE_u63A5" class="headerlink" title="5.动态链接"></a>5.动态链接</h1><h2 id="5-1__u52A8_u6001_u94FE_u63A5_u7684_u597D_u5904"><a href="#5-1__u52A8_u6001_u94FE_u63A5_u7684_u597D_u5904" class="headerlink" title="5.1 动态链接的好处"></a>5.1 动态链接的好处</h2><p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/98024542b72dc4d1bf4de468fddc51ee.jpg" alt="image.png"></p>
<p>上面是静态链接程序的磁盘空间和内存空间使用情况，磁盘中每个程序中都保存了一个 Lib.o 的文件，运行时在内存中每个进程了加载了一份 Lib.o 的文件。   </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/d50aa6fc41ce9f4fa79b11514f00d76a.jpg" alt="image.png">  </p>
<p>对比使用动态链接的程序，磁盘中只保存了一份 Lib.o，运行时多个程序的进程也是共享的一份 Lib.o。  </p>
<p>动态链接的方式可以减少程序升级成本、节省内存空间、减少物理页面的换入换出，也可以增加CPU缓存的命中率。  </p>
<p>在Linux中，常用的C语言库的运行库glibc，它的动态链接形式的版本保存在“/lib”目录下，文件名叫做“libc.so”。整个系统只保留一份C语言库的动态链接文件“libc.so”，而所有的C语言编写的、动态链接的程序都可以在运行时使用它。当程序被装载的时候，系统的动态链接器会将程序所需要的所有动态链接库（最基本的就是libc.so）装载到进程的地址空间，并且将程序中所有未决议的符号绑定到相应的动态链接库中，并进行重定位工作。  </p>
<h2 id="5-2__u52A8_u6001_u94FE_u63A5_u7B80_u4ECB"><a href="#5-2__u52A8_u6001_u94FE_u63A5_u7B80_u4ECB" class="headerlink" title="5.2 动态链接简介"></a>5.2 动态链接简介</h2><h3 id="u52A8_u6001_u94FE_u63A5_u8FC7_u7A0B_u7684_u793A_u4F8B"><a href="#u52A8_u6001_u94FE_u63A5_u8FC7_u7A0B_u7684_u793A_u4F8B" class="headerlink" title="动态链接过程的示例"></a>动态链接过程的示例</h3><p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/8b068be36d9a08d04a49e55c49d617ee.jpg" alt="image.png"></p>
<p>对于静态链接的可执行文件来说，整个进程只有一个文件要被映射，那就是可执行文件本身，但是对于动态链接来说，除了可执行文件本身之外，还有它所依赖的共享目标文件。  </p>
<p>可以查看一个进程的虚拟地址空间分布：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#8220;$./Program1 &#38;&#10;[1] 12985&#10;Printing from Lib.so 1&#10;$ cat /proc/12985/maps&#10;08048000-08049000 r-xp 00000000 08:01 1343432    ./Program1&#10;08049000-0804a000 rwxp 00000000 08:01 1343432    ./Program1&#10;b7e83000-b7e84000 rwxp b7e83000 00:00 0&#10;b7e84000-b7fc8000 r-xp 00000000 08:01 1488993    /lib/tls/i686/cmov/libc-2.6.1.so&#10;b7fc8000-b7fc9000 r-xp 00143000 08:01 1488993    /lib/tls/i686/cmov/libc-2.6.1.so&#10;b7fc9000-b7fcb000 rwxp 00144000 08:01 1488993    /lib/tls/i686/cmov/libc-2.6.1.so&#10;b7fcb000-b7fce000 rwxp b7fcb000 00:00 0&#10;b7fd8000-b7fd9000 rwxp b7fd8000 00:00 0&#10;b7fd9000-b7fda000 r-xp 00000000 08:01 1343290    ./Lib.so&#10;b7fda000-b7fdb000 rwxp 00000000 08:01 1343290    ./Lib.so&#10;b7fdb000-b7fdd000 rwxp b7fdb000 00:00 0&#10;b7fdd000-b7ff7000 r-xp 00000000 08:01 1455332    /lib/ld-2.6.1.so&#10;b7ff7000-b7ff9000 rwxp 00019000 08:01 1455332    /lib/ld-2.6.1.so&#10;bf965000-bf97b000 rw-p bf965000 00:00 0          [stack]&#10;ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]&#10;$ kill 12985&#10;[1]+  Terminated              ./Program1</span><br></pre></td></tr></table></figure>
<p>可以看到，整个进程虚拟地址空间中，多出了几个文件的映射。Lib.so与Program1一样，它们都是被操作系统用同样的方法映射至进程的虚拟地址空间，只是它们占据的虚拟地址和长度不同。  </p>
<h3 id="u5730_u5740_u65E0_u5173_u4EE3_u7801"><a href="#u5730_u5740_u65E0_u5173_u4EE3_u7801" class="headerlink" title="地址无关代码"></a>地址无关代码</h3><p>希望程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变，实现的基本想法就是把指令中那些需要被修改的部分分离出来，跟数据部分放在一起，这样指令部分就可以保持不变，而数据部分可以在每个进程中拥有一个副本。这种方案就是目前被称为地址无关代码（PIC, Position-independent Code）的技术。  </p>
<p>分析模块中各种类型的地址引用方式，把共享对象模块中的地址引用按照是否为跨模块分成两类：模块内部引用和模块外部引用；按照不同的引用方式又可以分为指令引用和数据访问，这样就得到了4种情况：  </p>
<ul>
<li>第一种是模块内部的函数调用、跳转等。</li>
<li>第二种是模块内部的数据访问，比如模块中定义的全局变量、静态变量。</li>
<li>第三种是模块外部的函数调用、跳转等。</li>
<li>第四种是模块外部的数据访问，比如其他模块中定义的全局变量。</li>
</ul>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/59e4a1bea97cd7fa604be357c02680eb.jpg" alt="image.png">  </p>
<p>对于模块外部的地址引用方式，ELF的做法是在数据段里面建立一个指向这些变量的指针数组，也被称为全局偏移表（Global Offset Table，GOT），当代码需要引用该全局变量时，可以通过GOT中相对应的项间接引用  </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/622b3b00e708efddcea26cc3f3aafedc.jpg" alt="image.png">  </p>
<p>可以使用objdump来查看GOT的位置：<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ objdump -h pic.so&#10;...&#10;17 .got       00000010  000015d0  000015d0  000005d0  2**2&#10;                  CONTENTS, ALLOC, LOAD, DATA&#10;...</span><br></pre></td></tr></table></figure></p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/13a4c1e723d1a996d220e6506ff09442.jpg" alt="image.png"></p>
<h3 id="u5EF6_u8FDF_u7ED1_u5B9A"><a href="#u5EF6_u8FDF_u7ED1_u5B9A" class="headerlink" title="延迟绑定"></a>延迟绑定</h3><p>在一个程序运行过程中，可能很多函数在程序执行完时都不会被用到，比如一些错误处理函数或者是一些用户很少用到的功能模块等，如果一开始就把所有函数都链接好实际上是一种浪费。所以ELF采用了一种叫做延迟绑定（Lazy Binding）的做法，基本的思想就是当函数第一次被用到时才进行绑定（符号查找、重定位等），如果没有用到则不进行绑定。所以程序开始执行时，模块间的函数调用都没有进行绑定，而是需要用到时才由动态链接器来负责绑定。这样的做法可以大大加快程序的启动速度，特别有利于一些有大量函数引用和大量模块的程序。  </p>
<p>ELF使用PLT（Procedure Linkage Table）的方法来实现延迟绑定。  </p>
<h2 id="5-3__u52A8_u6001_u94FE_u63A5_u76F8_u5173_u7ED3_u6784"><a href="#5-3__u52A8_u6001_u94FE_u63A5_u76F8_u5173_u7ED3_u6784" class="headerlink" title="5.3 动态链接相关结构"></a>5.3 动态链接相关结构</h2><p>在映射完可执行文件之后，操作系统会先启动一个动态链接器（Dynamic Linker）。<br>在Linux下，动态链接器ld.so实际上是一个共享对象，操作系统同样通过映射的方式将它加载到进程的地址空间中。操作系统在加载完动态链接器之后，就将控制权交给动态链接器的入口地址（与可执行文件一样，共享对象也有入口地址）。当动态链接器得到控制权之后，它开始执行一系列自身的初始化操作，然后根据当前的环境参数，开始对可执行文件进行动态链接工作。当所有动态链接工作完成以后，动态链接器会将控制权转交到可执行文件的入口地址，程序开始正式执行。</p>
<h3 id="interp__u6BB5"><a href="#interp__u6BB5" class="headerlink" title=".interp 段"></a>.interp 段</h3><p>并不是所有系统的动态链接器都位于 /lib/ld.so，.interp 段中可以查看<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ objdump -s a.out&#10;&#10;a.out:     file format elf32-i386&#10;Contents of section .interp:&#10; 8048114 2f6c6962 2f6c642d 6c696e75 782e736f  /lib/ld-linux.so&#10; 8048124 2e3200</span><br></pre></td></tr></table></figure></p>
<h3 id="dynamic__u6BB5"><a href="#dynamic__u6BB5" class="headerlink" title=".dynamic 段"></a>.dynamic 段</h3><p>这个段里面保存了动态链接器所需要的基本信息，比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="keyword">struct</span> &#123;</span><br><span class="line">    Elf32_Sword d_tag;</span><br><span class="line">    <span class="keyword">union</span> &#123;</span><br><span class="line">        Elf32_Word d_val;</span><br><span class="line">        Elf32_Addr d_ptr;</span><br><span class="line">    &#125; d_un;</span><br><span class="line">&#125; Elf32_Dyn;</span><br></pre></td></tr></table></figure></p>
<p>使用readelf工具可以查看“.dynamic”段的内容<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">readelf -d Lib.so</span><br></pre></td></tr></table></figure></p>
<p>使用 ldd 命令可以查看一个可执行程序或者一个 动态共享库依赖于哪些动态共享库<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ldd Program1&#10;        linux-gate.so.1 =&#62;  (0xffffe000)&#10;        ./Lib.so (0xb7f62000)&#10;        libc.so.6 =&#62; /lib/tls/i686/cmov/libc.so.6 (0xb7e0d000)&#10;        /lib/ld-linux.so.2 (0xb7f66000)</span><br></pre></td></tr></table></figure></p>
<h3 id="dynsym__u52A8_u6001_u7B26_u53F7_u8868"><a href="#dynsym__u52A8_u6001_u7B26_u53F7_u8868" class="headerlink" title=".dynsym 动态符号表"></a>.dynsym 动态符号表</h3><p>与“.symtab”不同的是，“.dynsym”只保存了与动态链接相关的符号，对于那些模块内部的符号，比如模块私有变量则不保存。很多时候动态链接的模块同时拥有“.dynsym”和“.symtab”两个表，“.symtab”中往往保存了所有符号，包括“.dynsym”中的符号。<br>与“.symtab”类似，动态符号表也需要一些辅助的表，比如用于保存符号名的字符串表。静态链接时叫做符号字符串表“.strtab”（String Table），在这里就是动态符号字符串表“.dynstr”（Dynamic String Table）；由于动态链接下，我们需要在程序运行时查找符号，为了加快符号的查找过程，往往还有辅助的符号哈希表（“.hash”）。我们可以用readelf工具来查看ELF文件的动态符号表及它的哈希表：<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$readelf -sD Lib.so&#10;......</span><br></pre></td></tr></table></figure></p>
<h3 id="u52A8_u6001_u94FE_u63A5_u91CD_u5B9A_u4F4D_u8868"><a href="#u52A8_u6001_u94FE_u63A5_u91CD_u5B9A_u4F4D_u8868" class="headerlink" title="动态链接重定位表"></a>动态链接重定位表</h3><p>动态链接的可执行文件使用的是PIC方法，但这不能改变它需要重定位的本质。对于动态链接来说，如果一个共享对象不是以PIC模式编译的，那么它也是需要在装载时被重定位的。  </p>
<p>如果一个共享对象是PIC模式编译的，对于使用PIC技术的可执行文件或共享对象来说，虽然它们的代码段不需要重定位（因为地址无关），但是数据段还包含了绝对地址的引用，因为代码段中绝对地址相关的部分被分离了出来，变成了GOT，而GOT实际上是数据段的一部分。除了GOT以外，数据段还可能包含绝对地址引用。  </p>
<p>在静态链接中，目标文件里面包含有专门用于表示重定位信息的重定位表，比如“.rel.text”表示是代码段的重定位表，“.rel.data”是数据段的重定位表。  </p>
<p>动态链接的文件中，也有类似的重定位表分别叫做“.rel.dyn”和“.rel.plt”，它们分别相当于 “.rel.text”和“.rel.data”。“.rel.dyn”实际上是对数据引用的修正，它所修正的位置位于“.got”以及数据段；而“.rel.plt”是对函数引用的修正，它所修正的位置位于“.got.plt”。我们可以使用readelf来查看一个动态链接的文件的重定位表：</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">readelf -r Lib.so</span><br></pre></td></tr></table></figure>
<h3 id="u52A8_u6001_u94FE_u63A5_u65F6_u8FDB_u7A0B_u5806_u6808_u521D_u59CB_u5316_u4FE1_u606F"><a href="#u52A8_u6001_u94FE_u63A5_u65F6_u8FDB_u7A0B_u5806_u6808_u521D_u59CB_u5316_u4FE1_u606F" class="headerlink" title="动态链接时进程堆栈初始化信息"></a>动态链接时进程堆栈初始化信息</h3><p>可执行文件有几个段（“Segment”）、每个段的属性、程序的入口地址（因为动态链接器到时候需要把控制权交给可执行文件）等。  </p>
<h2 id="5-4__u52A8_u6001_u94FE_u63A5_u7684_u6B65_u9AA4_u548C_u5B9E_u73B0"><a href="#5-4__u52A8_u6001_u94FE_u63A5_u7684_u6B65_u9AA4_u548C_u5B9E_u73B0" class="headerlink" title="5.4 动态链接的步骤和实现"></a>5.4 动态链接的步骤和实现</h2><h3 id="u52A8_u6001_u94FE_u63A5_u5668_u81EA_u4E3E"><a href="#u52A8_u6001_u94FE_u63A5_u5668_u81EA_u4E3E" class="headerlink" title="动态链接器自举"></a>动态链接器自举</h3><p>动态链接器本身也是一个共享对象，但是事实上它有一些特殊性。首先动态链接器本身不可以依赖于其他任何共享对象；其次是动态链接器本身所需要的全局和静态变量的重定位工作由它本身完成。  </p>
<p>对于第二个条件，动态链接器必须在启动时有一段非常精巧的代码可以完成这项艰巨的工作而同时又不能用到全局和静态变量。这种具有一定限制条件的启动代码往往被称为自举（Bootstrap）。  </p>
<h3 id="u88C5_u8F7D_u5171_u4EAB_u5BF9_u8C61"><a href="#u88C5_u8F7D_u5171_u4EAB_u5BF9_u8C61" class="headerlink" title="装载共享对象"></a>装载共享对象</h3><p>完成基本自举以后，动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表当中，我们可以称它为全局符号表（Global Symbol Table）。  </p>
<h3 id="u76F8_u540C_u7B26_u53F7_u7684_u88C5_u8F7D"><a href="#u76F8_u540C_u7B26_u53F7_u7684_u88C5_u8F7D" class="headerlink" title="相同符号的装载"></a>相同符号的装载</h3><p>当有两个不同的模块定义了同一个符号，Linux下的动态链接器是这样处理的：它定义了一个规则，那就是当一个符号需要被加入全局符号表时，如果相同的符号名已经存在，则后加入的符号被忽略。装载顺序是按照广度优先的顺序进行的。<br>（我理解在同一个动态库的不同版本之间，也存在这个问题）。  </p>
<h3 id="u91CD_u5B9A_u4F4D_u548C_u521D_u59CB_u5316"><a href="#u91CD_u5B9A_u4F4D_u548C_u521D_u59CB_u5316" class="headerlink" title="重定位和初始化"></a>重定位和初始化</h3><p>当上面的步骤完成之后，链接器开始重新遍历可执行文件和每个共享对象的重定位表，将它们的GOT/PLT中的每个需要重定位的位置进行修正。  </p>
<p>重定位完成之后，如果某个共享对象有“.init”段，那么动态链接器会执行“.init”段中的代码，用以实现共享对象特有的初始化过程，比如最常见的，共享对象中的C++的全局/静态对象的构造就需要通过“.init”来初始化。相应地，共享对象中还可能有“.finit”段，当进程退出时会执行“.finit”段中的代码，可以用来实现类似C++全局对象析构之类的操作。  </p>
<h3 id="Linux_u52A8_u6001_u94FE_u63A5_u5668_u5B9E_u73B0"><a href="#Linux_u52A8_u6001_u94FE_u63A5_u5668_u5B9E_u73B0" class="headerlink" title="Linux动态链接器实现"></a>Linux动态链接器实现</h3><p>动态链接器是个非常特殊的共享对象，它不仅是个共享对象，还是个可执行的程序，可以直接在命令行下面运行  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/lib/ld-linux.so.2</span><br></pre></td></tr></table></figure>
<p>其实Linux的内核在执行execve()时不关心目标ELF文件是否可执行（文件头e_type是ET_EXEC还是ET_DYN），它只是简单按照程序头表里面的描述对文件进行装载然后把控制权转交给ELF入口地址（没有“.interp”就是ELF文件的e_entry；如果有“.interp”的话就是动态链接器的e_entry）。这样我们就很好理解为什么动态链接器本身可以作为可执行程序运行，这也从一个侧面证明了共享库和可执行文件实际上没什么区别，除了文件头的标志位和扩展名有所不同之外，其他都是一样的。Windows系统中的EXE和DLL也是类似的区别，DLL也可以被当作程序来运行，Windows提供了一个叫做rundll32.exe的工具可以把一个DLL当作可执行文件运行。  </p>
<p>具体逻辑这里不细展开。</p>
<h2 id="5-5__u663E_u5F0F_u8FD0_u884C_u65F6_u94FE_u63A5"><a href="#5-5__u663E_u5F0F_u8FD0_u884C_u65F6_u94FE_u63A5" class="headerlink" title="5.5  显式运行时链接"></a>5.5  显式运行时链接</h2><p>支持动态链接的系统往往都支持一种更加灵活的模块加载方式，叫做显式运行时链接（Explicit Run-time Linking），有时候也叫做运行时加载。也就是让程序自己在运行时控制加载指定的模块，并且可以在不需要该模块时将其卸载。从前面我们了解到的来看，如果动态链接器可以在运行时将共享模块装载进内存并且可以进行重定位等操作，那么这种运行时加载在理论上也是很容易实现的。而且一般的共享对象不需要进行任何修改就可以进行运行时装载，这种共享对象往往被叫做动态装载库（Dynamic Loading Library），其实本质上它跟一般的共享对象没什么区别，只是程序开发者使用它的角度不同。  </p>
<ul>
<li>dlopen() 函数用来打开一个动态库，并将其加载到进程的地址空间，完成初始化过程</li>
<li>dlsym() 函数基本上是运行时装载的核心部分，我们可以通过这个函数找到所需要的符号</li>
<li>dlclose()的作用跟dlopen()刚好相反，它的作用是将一个已经加载的模块卸载</li>
<li>调用dlopen()、dlsym()或dlclose()以后，可以调用dlerror()函数来判断上一次调用是否成功</li>
</ul>
<h1 id="6-_linux__u5171_u4EAB_u5E93_u7684_u7EC4_u7EC7"><a href="#6-_linux__u5171_u4EAB_u5E93_u7684_u7EC4_u7EC7" class="headerlink" title="6. linux 共享库的组织"></a>6. linux 共享库的组织</h1><h2 id="6-1__u5171_u4EAB_u5E93_u7248_u672C_u517C_u5BB9_u6027"><a href="#6-1__u5171_u4EAB_u5E93_u7248_u672C_u517C_u5BB9_u6027" class="headerlink" title="6.1 共享库版本兼容性"></a>6.1 共享库版本兼容性</h2><h3 id="u5171_u4EAB_u5E93_u517C_u5BB9_u6027"><a href="#u5171_u4EAB_u5E93_u517C_u5BB9_u6027" class="headerlink" title="共享库兼容性"></a>共享库兼容性</h3><p>下图描述了各种操作导致ABI接口兼容性变化的情况：    </p>
<p><img src="https://zoucz.com/blogimgs/5544b600-a0e7-11ee-b717-b77abb79b472.md/5904ca6755a2411b8573019001187321.jpg" alt="image.png">  </p>
<h3 id="u7248_u672C_u547D_u540D"><a href="#u7248_u672C_u547D_u540D" class="headerlink" title="版本命名"></a>版本命名</h3><p>libname.so.x.y.z  </p>
<p>最前面使用前缀“lib”、中间是库的名字和后缀“.so”，最后面跟着的是三个数字组成的版本号。“x”表示主版本号（Major Version Number），“y”表示次版本号（Minor Version Number），“z”表示发布版本号（Release Version Number）。  </p>
<p>一般不同主版本号之间不兼容；高版本次版本号兼容低版本次版本号；发布版本号之间互相兼容。  </p>
<h3 id="SO-NAME"><a href="#SO-NAME" class="headerlink" title="SO-NAME"></a>SO-NAME</h3><p>每个共享库都有一个对应的“SO-NAME”，这个SO-NAME即共享库的文件名去掉次版本号和发布版本号，保留主版本号。比如一个共享库叫做libfoo.so.2.6.1，那么它的SO-NAME即libfoo.so.2。很明显，“SO-NAME”规定了共享库的接口，“SO-NAME”的两个相同共享库，次版本号大的兼容次版本号小的。  </p>
<p>Linux中提供了一个工具叫做“ldconfig”，当系统中安装或更新一个共享库时，就需要运行这个工具，它会遍历所有的默认共享库目录，比如/lib、/usr/lib等，然后更新所有的软链接，使它们指向最新版的共享库；如果安装了新的共享库，那么ldconfig会为其创建相应的软链接。  </p>
<h2 id="6-2__u7B26_u53F7_u7248_u672C_uFF08_u517C_u5BB9_u6027_uFF09"><a href="#6-2__u7B26_u53F7_u7248_u672C_uFF08_u517C_u5BB9_u6027_uFF09" class="headerlink" title="6.2 符号版本（兼容性）"></a>6.2 符号版本（兼容性）</h2><p>SO-NAME 解决不了次版本号低版本不兼容高版本的问题。  </p>
<p>Linux下的Glibc从版本2.1之后开始支持一种叫做基于符合的版本机制（Symbol Versioning）的方案。这个方案的基本思路是让每个导出和导入的符号都有一个相关联的版本号，它的实际做法类似于名称修饰的方法。  </p>
<p>与以往简单地将某个共享库的版本号重新命名不同（比如将libfoo.so.1.2升级到libfoo.so.1.3），当我们将libfoo.so.1.2升级至1.3时，仍然保持libfoo.so.1这个SO-NAME，但是给在1.3这个新版中添加的那些全局符号打上一个标记，比如“VERS_1.3”。那么，如果一个共享库每一次次版本号升级，我们都能给那些在新的次版本号中添加的全局符号打上相应的标记，就可以清楚地看到共享库中的每个符号都拥有相应的标签，比如“VERS_1.1”、“VERS_1.2”、“VERS_1.3”、“VERS_1.4”。  </p>
<p>在动态库次版本不兼容的时候，可能看到类似如下错误：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./main&#10;./main: ./lib.so: version `VERS_1.2&#39; not found (required by ./main)</span><br></pre></td></tr></table></figure>
<p>可以用 strings 命令查看共享库支持的符号版本  </p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">strings /lib64/libc.so<span class="number">.6</span> | grep GLIBC</span><br><span class="line">GLIBC_2<span class="number">.2</span><span class="number">.5</span></span><br><span class="line">GLIBC_2<span class="number">.2</span><span class="number">.6</span></span><br><span class="line">GLIBC_2<span class="number">.3</span></span><br><span class="line">......</span><br></pre></td></tr></table></figure>
<h2 id="6-3__u5171_u4EAB_u5E93_u7CFB_u7EDF_u8DEF_u5F84"><a href="#6-3__u5171_u4EAB_u5E93_u7CFB_u7EDF_u8DEF_u5F84" class="headerlink" title="6.3 共享库系统路径"></a>6.3 共享库系统路径</h2><p>目前大多数包括Linux在内的开源操作系统都遵守一个叫做FHS（File Hierarchy Standard）的标准，这个标准规定了一个系统中的系统文件应该如何存放，包括各个目录的结构、组织和作用，这有利于促进各个开源操作系统之间的兼容性。共享库作为系统中重要的文件，它们的存放方式也被FHS列入了规定范围。FHS规定，一个系统中主要有两个存放共享库的位置，它们分别如下：  </p>
<ul>
<li>/lib，这个位置主要存放系统最关键和基础的共享库，比如动态链接器、C语言运行库、数学库等，这些库主要是那些/bin和/sbin下的程序所需要用到的库，还有系统启动时需要的库。</li>
<li>/usr/lib，这个目录下主要保存的是一些非系统运行时所需要的关键性的共享库，主要是一些开发时用到的共享库，这些共享库一般不会被用户的程序或shell脚本直接用到。这个目录下面还包含了开发时可能会用到的静态库、目标文件等。</li>
<li>/usr/local/lib，这个目录用来放置一些跟操作系统本身并不十分相关的库，主要是一些第三方的应用程序的库。比如我们在系统中安装了python语言的解释器，那么与它相关的共享库可能会被放到/usr/local/lib/python，而它的可执行文件可能被放到/usr/local/bin下。GNU的标准推荐第三方的程序应该默认将库安装到/usr/local/lib下。  </li>
</ul>
<h2 id="6-4__u5171_u4EAB_u5E93_u67E5_u627E_u8FC7_u7A0B"><a href="#6-4__u5171_u4EAB_u5E93_u67E5_u627E_u8FC7_u7A0B" class="headerlink" title="6.4 共享库查找过程"></a>6.4 共享库查找过程</h2><p>动态链接的模块所依赖的模块路径保存在“.dynamic”段里面，由DT_NEED类型的项表示。动态链接器对于模块的查找有一定的规则：  </p>
<ul>
<li>如果DT_NEED里面保存的是绝对路径，那么动态链接器就按照这个路径去查找；</li>
<li>如果DT_NEED里面保存的是相对路径，那么动态链接器会在/lib、/usr/lib和由/etc/ld.so.conf配置文件指定的目录中查找共享库。为了程序的可移植性和兼容性，共享库的路径往往是相对的。  </li>
</ul>
<p>ld.so.conf是一个文本配置文件，它可能包含其他的配置文件，这些配置文件中存放着目录信息。  </p>
<p>如果动态链接器在每次查找共享库时都去遍历这些目录，那将会非常耗费时间。所以Linux系统中都有一个叫做ldconfig的程序，这个程序的作用是为共享库目录下的各个共享库创建、删除或更新相应的SO-NAME（即相应的符号链接），这样每个共享库的SO-NAME就能够指向正确的共享库文件；并且这个程序还会将这些SO-NAME收集起来，集中存放到/etc/ld.so.cache文件里面，并建立一个SO-NAME的缓存。  </p>
<p>很多软件包的安装程序在往系统里面安装共享库以后都会调用ldconfig。  </p>
<h2 id="6-5__u73AF_u5883_u53D8_u91CF"><a href="#6-5__u73AF_u5883_u53D8_u91CF" class="headerlink" title="6.5 环境变量"></a>6.5 环境变量</h2><h3 id="LD_LIBRARY_PATH"><a href="#LD_LIBRARY_PATH" class="headerlink" title="LD_LIBRARY_PATH"></a>LD_LIBRARY_PATH</h3><p>动态链接器会按照下列顺序依次装载或查找共享对象（目标文件）：  </p>
<ul>
<li>由环境变量LD_LIBRARY_PATH指定的路径。</li>
<li>由路径缓存文件/etc/ld.so.cache指定的路径。</li>
<li>默认共享库目录，先/usr/lib，然后/lib。</li>
</ul>
<h3 id="LD_PRELOAD"><a href="#LD_PRELOAD" class="headerlink" title="LD_PRELOAD"></a>LD_PRELOAD</h3><p>系统中另外还有一个环境变量叫做LD_PRELOAD，这个文件中我们可以指定预先装载的一些共享库甚或是目标文件。  </p>
<h3 id="LD_DEBUG"><a href="#LD_DEBUG" class="headerlink" title="LD_DEBUG"></a>LD_DEBUG</h3><p>这个变量可以打开动态链接器的调试功能，当我们设置这个变量时，动态链接器会在运行时打印出各种有用的信息。  </p>
<p>可以设置值：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#8220;files&#8221;&#65306;&#26174;&#31034;&#31243;&#24207;&#20381;&#36182;&#20110;&#21738;&#20010;&#20849;&#20139;&#24211;&#24182;&#19988;&#25353;&#29031;&#20160;&#20040;&#27493;&#39588;&#35013;&#36733;&#21644;&#21021;&#22987;&#21270;&#65292;&#20849;&#20139;&#24211;&#35013;&#36733;&#26102;&#30340;&#22320;&#22336;&#31561;&#12290;&#10;&#8220;bindings&#8221;&#26174;&#31034;&#21160;&#24577;&#38142;&#25509;&#30340;&#31526;&#21495;&#32465;&#23450;&#36807;&#31243;&#12290;&#10;&#8220;libs&#8221;&#26174;&#31034;&#20849;&#20139;&#24211;&#30340;&#26597;&#25214;&#36807;&#31243;&#12290;&#10;&#8220;versions&#8221;&#26174;&#31034;&#31526;&#21495;&#30340;&#29256;&#26412;&#20381;&#36182;&#20851;&#31995;&#12290;&#10;&#8220;reloc&#8221;&#26174;&#31034;&#37325;&#23450;&#20301;&#36807;&#31243;&#12290;&#10;&#8220;symbols&#8221;&#26174;&#31034;&#31526;&#21495;&#34920;&#26597;&#25214;&#36807;&#31243;&#12290;&#10;&#8220;statistics&#8221;&#26174;&#31034;&#21160;&#24577;&#38142;&#25509;&#36807;&#31243;&#20013;&#30340;&#21508;&#31181;&#32479;&#35745;&#20449;&#24687;&#12290;</span><br></pre></td></tr></table></figure></p>
<h2 id="6-6__u5171_u4EAB_u5E93_u7684_u521B_u5EFA_u548C_u5B89_u88C5"><a href="#6-6__u5171_u4EAB_u5E93_u7684_u521B_u5EFA_u548C_u5B89_u88C5" class="headerlink" title="6.6 共享库的创建和安装"></a>6.6 共享库的创建和安装</h2><h3 id="u521B_u5EFA"><a href="#u521B_u5EFA" class="headerlink" title="创建"></a>创建</h3><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$gcc &#8211;shared &#8211;Wl,-soname,my_soname &#8211;o library_name source_files library_files</span><br></pre></td></tr></table></figure>
<p>如果我们不使用-soname来指定共享库的SO-NAME，那么该共享库默认就没有SO-NAME，即使用ldconfig更新SO-NAME的软链接时，对该共享库也没有效果。  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">strip libfoo.so</span><br></pre></td></tr></table></figure>
<p>去除符号和调试信息以后的文件往往比之前要小很多，一般只有原来的一半大小，甚至不到一半。除了使用“strip”工具，我们还可以使用ld的“-s”和“-S”参数，使得链接器生成输出文件时就不产生符号信息。  </p>
<p>“-s”和“-S”的区别是：“-S”消除调试符号信息，而“-s”消除所有符号信息。我们也可以在gcc中通过“-Wl,-s”和“-Wl,-S”给ld传递这两个参数。  </p>
<h3 id="u5B89_u88C5"><a href="#u5B89_u88C5" class="headerlink" title="安装"></a>安装</h3><p>最简单的办法就是将共享库复制到某个标准的共享库目录，如/lib、/usr/lib等，然后运行ldconfig即可。  </p>
<h3 id="u5171_u4EAB_u5E93_u6784_u9020_u548C_u6790_u6784_u51FD_u6570"><a href="#u5171_u4EAB_u5E93_u6784_u9020_u548C_u6790_u6784_u51FD_u6570" class="headerlink" title="共享库构造和析构函数"></a>共享库构造和析构函数</h3><p>GCC提供了一种共享库的构造函数，只要在函数声明时加上“attribute((constructor))”的属性，即指定该函数为共享库构造函数，拥有这种属性的函数会在共享库加载时被执行，即在程序的main函数之前执行。如果我们使用dlopen()打开共享库，共享库构造函数会在dlopen()返回之前被执行。  </p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> __attribute__((constructor)) init_function(<span class="keyword">void</span>);</span><br><span class="line"><span class="keyword">void</span> __attribute__((destructor))  fini_function (<span class="keyword">void</span>);</span><br></pre></td></tr></table></figure>
<h1 id="7-__u56DE_u5F52_u95EE_u9898"><a href="#7-__u56DE_u5F52_u95EE_u9898" class="headerlink" title="7. 回归问题"></a>7. 回归问题</h1><h2 id="7-1_version_not_found__u95EE_u9898"><a href="#7-1_version_not_found__u95EE_u9898" class="headerlink" title="7.1 version not found 问题"></a>7.1 version not found 问题</h2><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">import torch&#10;import skimage&#10;&#10;ImportError: /usr/lib64/libstdc++.so.6: version `CXXABI_1.3.9&#39; not found (required by /root/miniconda3/lib/python3.9/site-packages/scipy/sparse/_sparsetools.cpython-39-x86_64-linux-gnu.so)</span><br></pre></td></tr></table></figure>
<p>通过 strings 命令查看 /usr/lib64/libstdc++.so.6 的符号版本 </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(base) [root@79579837774f /]# strings /usr/lib64/libstdc++.so.6|grep CXXABI&#10;CXXABI_1.3&#10;CXXABI_1.3.1&#10;CXXABI_1.3.2&#10;CXXABI_1.3.3&#10;CXXABI_1.3.4&#10;CXXABI_1.3.5&#10;CXXABI_1.3.6&#10;CXXABI_1.3.7&#10;CXXABI_TM_1</span><br></pre></td></tr></table></figure>
<p>那为什么先加载 skimage 就没问题呢？  </p>
<p>通过 ldd 命令或者 readelf -d 命令可以查看报错的库的动态库依赖情况<br>ldd 命令  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(base) [root@79579837774f /]# ldd /root/miniconda3/lib/python3.9/site-packages/scipy/sparse/_sparsetools.cpython-39-x86_64-linux-gnu.so&#10;        linux-vdso.so.1 =&#62;  (0x00007ffc679ea000)&#10;        /$LIB/libonion.so =&#62; /lib64/libonion.so (0x00007f3124f7e000)&#10;        libstdc++.so.6 =&#62; /root/miniconda3/lib/python3.9/site-packages/scipy/sparse/../../../../libstdc++.so.6 (0x00007f31248e4000)&#10;        libgcc_s.so.1 =&#62; /root/miniconda3/lib/python3.9/site-packages/scipy/sparse/../../../../libgcc_s.so.1 (0x00007f3124f63000)&#10;        libc.so.6 =&#62; /usr/lib64/libc.so.6 (0x00007f3124516000)&#10;        libdl.so.2 =&#62; /usr/lib64/libdl.so.2 (0x00007f3124312000)&#10;        libm.so.6 =&#62; /usr/lib64/libm.so.6 (0x00007f3124010000)&#10;        /lib64/ld-linux-x86-64.so.2 (0x00007f3124e65000)</span><br></pre></td></tr></table></figure>
<p>readelf -d 命令  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(base) [root@79579837774f /]# readelf -d /root/miniconda3/lib/python3.9/site-packages/scipy/sparse/_sparsetools.cpython-39-x86_64-linux-gnu.so&#10;&#10;Dynamic section at offset 0x39bc58 contains 27 entries:&#10;  Tag        Type                         Name/Value&#10; 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]&#10; 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]&#10; 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]&#10; 0x000000000000000f (RPATH)              Library rpath: [$ORIGIN/../../../..]&#10;......</span><br></pre></td></tr></table></figure>
<p>可以看到它依赖的库是 miniconda3 目录下自带的一个 libstdc++.so，<code>/root/miniconda3/lib/libstdc++.so.6</code>，查看它的符号 </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(base) [root@79579837774f /]# strings /root/miniconda3/lib/libstdc++.so.6|grep CXXABI&#10;......&#10;CXXABI_1.3.9&#10;......</span><br></pre></td></tr></table></figure>
<p>可以看到果然是有报错的符号版本的。  </p>
<p>也就是说操作系统的 libstdc++.so 库和 miniconda3 的 libstdc++.so 主版本相同，都是 6，所以动态库加载时是可以互相兼容的， 但是次版本号不一致，导致低版本不兼容高版本。若先引入 libtorch，则会加载低次版本的 libstdc++.so，导致依赖高此版本号的 skimage 模块不兼容。先引入 skimage，则 skimage 和 libtorch 都是兼容的，可以正常工作。</p>
<h2 id="7-2_undefined_symbol__u95EE_u9898"><a href="#7-2_undefined_symbol__u95EE_u9898" class="headerlink" title="7.2 undefined symbol 问题"></a>7.2 undefined symbol 问题</h2><p>背景是业务中有一些 c++ 的 so 库，依赖了 c++ 版本的 torch / cuda 相关的 so 库，这些 c++ 版本的 torch / cuda 相关的 so 是由业务自行编译的。  </p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> ctypes</span><br><span class="line">ctypes.cdll.LoadLibrary(<span class="string">"/lib_one_cpp_lib_about_libtorch.so"</span>)</span><br><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"></span><br><span class="line">ImportError: /root/miniconda3/lib/python3<span class="number">.9</span>/site-packages/torch/lib/libtorch_cuda_cpp.so: undefined symbol: _ZTIN4c10d4WorkE</span><br></pre></td></tr></table></figure>
<p>先引入依赖了 libtorch 的c++共享库，再通过引入 pyhton 版本的 torch 模块，发现后引入的报符号未定义错误<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">import</span> ctypes</span><br><span class="line">ctypes.cdll.LoadLibrary(<span class="string">"/lib_one_cpp_lib_about_libtorch.so"</span>)</span><br><span class="line"></span><br><span class="line">OSError: xxxx.so: undefined symbol: _ZNK3c104Type14isSubtypeOfExtERKSt10shared_ptrIS0_EPSo</span><br></pre></td></tr></table></figure></p>
<p>同样会报服务错误。 c++ 版本的 torch / cuda 相关库是业务自行编译的版本，其 SO-NAME 和 python 版本的一样，但是 ABI 接口不能保证和 python 版本 torch 模块的兼容。进程根据 SO-NAME 加载 so 库时，只会加载先引入的那个，后引入的因为依赖的 SO-NAME 已经在前面映射过了，会直接复用。那么有依赖后加载库的地方，符号解析就会发生错误。 </p>
]]></content>
    <summary type="html">
    <![CDATA[<p>最近在工作中遇到了几次动态库链接报错的问题。  </p>
<p>一次是 import 两个 python 模块，必须将一个模块放到前面加载，另一个放到后面加载，不然会报找不到版本的错误。  </p>
<figure class="highlight"><table><tr>]]>
    </summary>
    
      <category term="c++" scheme="https://www.zoucz.com/blog/tags/c/"/>
    
      <category term="编译" scheme="https://www.zoucz.com/blog/tags/%E7%BC%96%E8%AF%91/"/>
    
      <category term="c++" scheme="https://www.zoucz.com/blog/categories/c/"/>
    
      <category term="编译" scheme="https://www.zoucz.com/blog/categories/c/%E7%BC%96%E8%AF%91/"/>
    
      <category term="后台开发" scheme="https://www.zoucz.com/blog/categories/c/%E7%BC%96%E8%AF%91/%E5%90%8E%E5%8F%B0%E5%BC%80%E5%8F%91/"/>
    
      <category term="linux" scheme="https://www.zoucz.com/blog/categories/c/%E7%BC%96%E8%AF%91/%E5%90%8E%E5%8F%B0%E5%BC%80%E5%8F%91/linux/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[Opentelemetry 可观测性规范（三）—— OTEL 核心源码梳理]]></title>
    <link href="https://www.zoucz.com/blog/2023/12/17/a833c170-9c8d-11ee-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/12/17/a833c170-9c8d-11ee-9fa0-5dbc93f9d3ee/</id>
    <published>2023-12-17T03:37:52.000Z</published>
    <updated>2023-12-17T10:38:11.000Z</updated>
    <content type="html"><![CDATA[<p>本文基于 Opentelemetry JS 语言的实现，梳理了其对 OTEL 的具体实现，方便深入理解 OTEL 的工作原理，便于日常项目中的可观测性数据上报和自定义的三方 instrumentation 组件开发。  </p>
<h1 id="1-_API"><a href="#1-_API" class="headerlink" title="1. API"></a>1. API</h1><h2 id="1-1_API__u7ED3_u6784_u68B3_u7406"><a href="#1-1_API__u7ED3_u6784_u68B3_u7406" class="headerlink" title="1.1 API 结构梳理"></a>1.1 API 结构梳理</h2><p>API 模块提供了 context、metrics、trace、propagation 等核心功能的 API 定义，以及它们的默认实现。  </p>
<p><img src="https://zoucz.com/blogimgs/a833c170-9c8d-11ee-9fa0-5dbc93f9d3ee.md/6b3010613b2f7b38961e33dae6f5d37f.jpg" alt="image.png">  </p>
<h2 id="1-2_API__u5177_u4F53_u5B9E_u73B0"><a href="#1-2_API__u5177_u4F53_u5B9E_u73B0" class="headerlink" title="1.2 API 具体实现"></a>1.2 API 具体实现</h2><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#9500;&#9472;&#9472; api&#10;&#9474;   &#9500;&#9472;&#9472; context.ts # &#32500;&#25252;&#19968;&#20010; ContextAPI &#21333;&#20363;&#23545;&#35937; context&#65292;&#25552;&#20379;&#20840;&#23616; context &#30340;&#35774;&#32622;&#12289;&#35775;&#38382;&#31561;&#26041;&#27861;&#10;&#9474;   &#9500;&#9472;&#9472; diag.ts    # &#32500;&#25252;&#19968;&#20010; DiagAPI &#21333;&#20363;&#23545;&#35937; diag&#65292;&#25552;&#20379;&#20840;&#23616;&#35786;&#26029;&#26085;&#24535;&#30340;&#31649;&#29702;&#10;&#9474;   &#9500;&#9472;&#9472; metrics.ts # &#32500;&#25252;&#19968;&#20010; MetricsAPI &#21333;&#20363;&#23545;&#35937; metrics&#65292;&#25552;&#20379;&#20840;&#23616; MeterProvider &#31649;&#29702;&#12289;Meter &#21019;&#24314;&#21151;&#33021;&#10;&#9474;   &#9500;&#9472;&#9472; propagation.ts # &#32500;&#25252;&#19968;&#20010; PropagationAPI &#21333;&#20363;&#23545;&#35937; propagation&#10;&#9;&#9;       # &#25552;&#20379;&#20840;&#23616; propagator &#30340;&#35774;&#32622;&#12289;inject&#12289;extract &#26041;&#27861;&#12289;Baggage &#30456;&#20851;&#26041;&#27861;&#10;&#9474;   &#9492;&#9472;&#9472; trace.ts   # &#32500;&#25252;&#19968;&#20010; TraceAPI &#21333;&#20363;&#23545;&#35937; trace&#65292;&#25552;&#20379;&#20840;&#23616; TracerProvider &#30340;&#31649;&#29702;&#12289;Tracer&#21019;&#24314;&#21151;&#33021;&#10;&#9500;&#9472;&#9472; baggage        #&#12304;&#20855;&#20307;&#23454;&#29616;&#12305;baggage&#65292;&#30001; propagation.ts &#35843;&#29992;&#10;&#9474;   &#9500;&#9472;&#9472; context-helpers.ts # &#25805;&#20316; baggage &#22312; context &#20013;&#35835;&#20889;&#30340;&#24037;&#20855;&#20989;&#25968;&#10;&#9474;   &#9500;&#9472;&#9472; internal&#10;&#9474;   &#9474;   &#9500;&#9472;&#9472; baggage-impl.ts # baggage &#20855;&#20307;&#23454;&#29616;&#10;&#9474;   &#9474;   &#9492;&#9472;&#9472; symbol.ts # &#25968;&#25454;&#31867;&#22411; symbol &#26631;&#35760;&#10;&#9474;   &#9500;&#9472;&#9472; types.ts      # Baggage &#30456;&#20851;&#25509;&#21475;&#23450;&#20041;&#65292;baggage-impl.ts &#23454;&#29616;&#20102; Baggage &#25509;&#21475;&#10;&#9474;   &#9492;&#9472;&#9472; utils.ts      # &#25552;&#20379;&#20986;&#21475;&#26041;&#27861; createBaggage&#12289;baggageEntryMetadataFromString&#10;&#9500;&#9472;&#9472; common            # &#19968;&#20123;&#22522;&#30784;&#31867;&#22411;&#23450;&#20041;&#10;&#9474;   &#9500;&#9472;&#9472; Attributes.ts # &#23646;&#24615;&#31867;&#22411;&#10;&#9474;   &#9500;&#9472;&#9472; Exception.ts  # &#24322;&#24120;&#31867;&#22411;&#10;&#9474;   &#9492;&#9472;&#9472; Time.ts       # &#26102;&#38388;&#31867;&#22411;&#10;&#9500;&#9472;&#9472; context           # context &#30456;&#20851;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; NoopContextManager.ts # &#40664;&#35748;&#30340; NoopContextManager implements types.ContextManager&#10;&#9;&#9;&#9;      # &#31532;&#19968;&#27425; active &#26102;&#36820;&#22238; ROOT_CONTEXT&#10;&#9474;   &#9500;&#9472;&#9472; context.ts            # &#40664;&#35748;&#30340; BaseContext implements Context&#65292;&#25552;&#20379;&#19968;&#20010;&#23454;&#20363;&#20316;&#20026; ROOT_CONTEXT&#10;&#9474;   &#9492;&#9472;&#9472; types.ts              # Context&#12289;ContextManager &#25509;&#21475;&#23450;&#20041;&#10;&#9500;&#9472;&#9472; diag                      # &#35786;&#26029;&#26085;&#24535;&#30456;&#20851;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; ComponentLogger.ts    # &#26085;&#24535;&#32452;&#20214; logger&#10;&#9474;   &#9500;&#9472;&#9472; consoleLogger.ts      # &#25511;&#21046;&#21488; logger&#10;&#9474;   &#9500;&#9472;&#9472; internal&#10;&#9474;   &#9474;   &#9500;&#9472;&#9472; logLevelLogger.ts # &#26681;&#25454;&#29616;&#26377; logger &#21019;&#24314;&#38480;&#21046;&#36755;&#20986;&#32423;&#21035;&#30340; logger&#10;&#9474;   &#9474;   &#9492;&#9472;&#9472; noopLogger.ts     # &#26410;&#35774;&#32622;&#20219;&#20309; logger &#26102;&#30340; no operation logger&#65292;&#21861;&#20063;&#19981;&#24178;&#10;&#9474;   &#9492;&#9472;&#9472; types.ts              # DiagLogger&#12289;DiagLoggerApi &#25509;&#21475;&#23450;&#20041;&#10;&#9500;&#9472;&#9472; internal # &#20869;&#37096;&#20844;&#20849;&#33021;&#21147;&#27169;&#22359;&#10;&#9474;   &#9500;&#9472;&#9472; global-utils.ts # &#26681;&#25454; platform &#33719;&#21462;&#24182;&#32500;&#25252;&#19968;&#20010;&#20840;&#23616;&#23545;&#35937;&#10;&#9;&#9;&#9;# &#21547; DiagLogger&#12289;TracerProvider&#12289;ContextManager&#12289;&#10;&#9;&#9;        # MeterProvider&#12289;TextMapPropagator&#10;&#9;&#9;&#9;# &#24182;&#25552;&#20379; get&#12289;register&#12289;unregister &#31561;&#20989;&#25968;&#10;&#9474;   &#9492;&#9472;&#9472; semver.ts # &#25552;&#20379;&#19968;&#20010;semver&#29256;&#26412;&#20860;&#23481;&#24615;&#21028;&#26029;&#20989;&#25968;&#65292;&#36755;&#20837;&#29256;&#26412;&#21495;&#65292;&#36755;&#20986;&#24403;&#21069;api&#26159;&#21542;&#25903;&#25345;&#27492;&#29256;&#26412;&#21495;&#10;&#9500;&#9472;&#9472; metrics                   # metrics &#30456;&#20851;&#25509;&#21475;&#23450;&#20041;&#21644;&#40664;&#35748;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; Meter.ts              # Meter &#25509;&#21475;&#23450;&#20041;&#65292;&#21547;&#21508;&#31867;&#25351;&#26631;&#30340;&#21019;&#24314;&#31561;&#26041;&#27861;&#23450;&#20041;&#10;&#9474;   &#9500;&#9472;&#9472; MeterProvider.ts      # MeterProvider &#25509;&#21475;&#23450;&#20041;&#65292;&#37324;&#36793;&#23601;&#19968;&#20010; getMeter &#26041;&#27861;&#10;&#9474;   &#9500;&#9472;&#9472; Metric.ts             # &#21508;&#31867;&#25351;&#26631;&#21644;&#25351;&#26631;&#36873;&#39033;&#23450;&#20041;&#65292;&#22914; Histogram&#12289;Counter &#31561;&#10;&#9474;   &#9500;&#9472;&#9472; NoopMeter.ts          # &#40664;&#35748;&#30340; Meter &#23454;&#29616;&#65292;&#21508;&#31867;&#26041;&#27861;&#37117;&#36820;&#22238;&#19968;&#20010;&#21861;&#20063;&#19981;&#24178;&#30340;&#25351;&#26631;&#23545;&#35937;&#10;&#9474;   &#9500;&#9472;&#9472; NoopMeterProvider.ts  # &#40664;&#35748;&#30340; MeterProvider &#23454;&#29616;&#65292;getMeter &#36820;&#22238; NoopMeter &#23545;&#35937;&#10;&#9474;   &#9492;&#9472;&#9472; ObservableResult.ts   # ObservableResult&#12289;BatchObservableResult &#25509;&#21475;&#23450;&#20041;&#10;&#9500;&#9472;&#9472; platform # &#26681;&#25454;&#19981;&#21516;&#24179;&#21488;&#33719;&#21462; global &#23545;&#35937;&#10;&#9474;   &#9500;&#9472;&#9472; browser # &#27983;&#35272;&#22120;&#29615;&#22659;&#33719;&#21462; global&#10;&#9474;   &#9474;   &#9500;&#9472;&#9472; globalThis.ts&#10;&#9474;   &#9474;   &#9492;&#9472;&#9472; index.ts&#10;&#9474;   &#9500;&#9472;&#9472; index.ts # &#40664;&#35748;&#36820;&#22238; node &#29615;&#22659; global&#10;&#9474;   &#9492;&#9472;&#9472; node # node &#29615;&#22659;&#33719;&#21462; global&#10;&#9474;       &#9500;&#9472;&#9472; globalThis.ts&#10;&#9474;       &#9492;&#9472;&#9472; index.ts&#10;&#9500;&#9472;&#9472; propagation                    # propagation &#30456;&#20851;&#25509;&#21475;&#23450;&#20041;&#21644;&#40664;&#35748;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; NoopTextMapPropagator.ts   # &#40664;&#35748;&#30340; TextMapPropagator &#23454;&#29616;&#65292;&#21861;&#20063;&#19981;&#24178;&#10;&#9474;   &#9492;&#9472;&#9472; TextMapPropagator.ts       # TextMapPropagator &#25509;&#21475;&#23450;&#20041;&#65292;&#25552;&#20379; inject /extract &#26041;&#27861;&#10;&#9500;&#9472;&#9472; trace                            # trace &#30456;&#20851;&#25509;&#21475;&#21644;&#40664;&#35748;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; internal&#10;&#9474;   &#9474;   &#9500;&#9472;&#9472; tracestate-impl.ts       # &#19968;&#20010;&#23454;&#29616; TraceState &#25509;&#21475;&#30340; TraceStateImpl&#65292;&#25552;&#20379;&#35774;&#32622;/&#35835;&#21462;&#10;&#9;&#9;&#9;&#9;     # &#24207;&#21015;&#21270;&#12289;&#21453;&#24207;&#21015;&#21270;&#31561;&#26041;&#27861; &#10;&#9474;   &#9474;   &#9500;&#9472;&#9472; tracestate-validators.ts # &#25552;&#20379; TraceState &#30340;key/value&#21512;&#27861;&#24615;&#26657;&#39564;&#26041;&#27861;&#65288;&#27491;&#21017;&#65289;&#10;&#9474;   &#9474;   &#9492;&#9472;&#9472; utils.ts                 # &#22522;&#20110; TraceStateImp &#21253;&#35013; createTraceState &#20989;&#25968;&#24182;&#23548;&#20986;&#10;&#9474;   &#9500;&#9472;&#9472; NonRecordingSpan.ts          # &#40664;&#35748;&#30340; Span &#23454;&#29616;&#65292;&#19968;&#20010;&#21861;&#20063;&#19981;&#24178;&#30340; Span &#31867;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; NoopTracer.ts                # &#40664;&#35748;&#30340; Tracer &#23454;&#29616;&#65292;&#36820;&#22238; NonRecordingSpan&#10;&#9474;   &#9500;&#9472;&#9472; NoopTracerProvider.ts        # &#40664;&#35748;&#30340; TracerProvider &#23454;&#29616;&#65292;&#36820;&#22238; NoopTracer&#10;&#9474;   &#9500;&#9472;&#9472; ProxyTracer.ts               # &#20195;&#29702; Tracer&#65292;&#21487;&#20197;&#20256;&#20837; TracerDelegator &#23545;&#35937;&#20195;&#29702;&#20854;&#20182; Tracer&#10;&#9474;   &#9500;&#9472;&#9472; ProxyTracerProvider.ts       # &#20195;&#29702; TracerProvider&#65292;&#21487;&#20197;&#20195;&#29702;&#21478;&#19968;&#20010; TracerProvider&#10;&#9474;   &#9500;&#9472;&#9472; Sampler.ts                   # Sampler &#37319;&#26679;&#22120;&#25509;&#21475;&#23450;&#20041;&#65292;&#25552;&#20379; shouldSample &#26041;&#27861;&#10;&#9474;   &#9500;&#9472;&#9472; SamplingResult.ts            # SamplingResult &#37319;&#26679;&#32467;&#26524;&#25509;&#21475;&#23450;&#20041;&#65292;&#21547;&#26159;&#21542;&#35760;&#24405;&#32467;&#26524;&#10;&#9474;   &#9500;&#9472;&#9472; SpanOptions.ts               # startSpan &#20256;&#20837;&#30340;&#36873;&#39033;&#10;&#9474;   &#9500;&#9472;&#9472; attributes.ts              # &#23545; ../common/Attributes &#20013;&#30340;&#23646;&#24615;&#23548;&#20986;&#10;&#9474;   &#9500;&#9472;&#9472; context-utils.ts           # &#23553;&#35013;&#20174; content &#20013;&#22686;&#21024;&#25913;&#26597; span &#30340;&#26041;&#27861;&#10;&#9474;   &#9500;&#9472;&#9472; invalid-span-constants.ts  # &#23454;&#29616;&#40664;&#35748;&#30340;&#38750;&#27861; SpanContext&#10;&#9474;   &#9500;&#9472;&#9472; link.ts                    # Link &#25509;&#21475;&#23450;&#20041;&#10;&#9474;   &#9500;&#9472;&#9472; span.ts                    # Span &#25509;&#21475;&#23450;&#20041;&#10;&#9474;   &#9500;&#9472;&#9472; span_context.ts            # SpanContext &#25509;&#21475;&#23450;&#20041;&#65292;&#26159; Span &#30340;&#19968;&#20010;&#23646;&#24615;&#65292;&#21547; spanid&#12289;traceid&#31561;&#10;&#9474;   &#9500;&#9472;&#9472; span_kind.ts               # SpanKind &#26522;&#20030; &#10;&#9474;   &#9500;&#9472;&#9472; spancontext-utils.ts       # &#25552;&#20379; SpanContext &#21512;&#27861;&#24615;&#21028;&#26029;&#30456;&#20851;&#26041;&#27861;&#10;                                   # &#25552;&#20379; wrapSpanContext &#22522;&#20110; SpanContext &#21019;&#24314;&#19968;&#20010; Span &#23545;&#35937;&#10;&#9474;   &#9500;&#9472;&#9472; status.ts                  # SpanStatus &#25509;&#21475;&#65292;&#21547; code&#12289;message &#23646;&#24615;&#10;&#9474;   &#9500;&#9472;&#9472; trace_flags.ts             # TraceFlags &#26522;&#20030;&#10;&#9474;   &#9500;&#9472;&#9472; trace_state.ts             # TraceState &#25509;&#21475;&#23450;&#20041;&#65292;&#21547;&#35774;&#32622;&#12289;&#33719;&#21462;&#12289;&#24207;&#21015;&#21270;&#31561;&#26041;&#27861;&#10;&#9474;   &#9500;&#9472;&#9472; tracer.ts                  # Tracer &#25509;&#21475;&#23450;&#20041;&#65292;&#21547; startSpan&#12289;startActiveSpan &#26041;&#27861;&#10;&#9474;   &#9500;&#9472;&#9472; tracer_options.ts          # TracerOptions &#25509;&#21475;&#23450;&#20041;&#10;&#9474;   &#9492;&#9472;&#9472; tracer_provider.ts         # TracerProvider &#25509;&#21475;&#23450;&#20041;&#65292;&#21547; getTracer &#26041;&#27861;&#10;&#9500;&#9472;&#9472; context-api.ts            # &#23548;&#20986; ContextAPI &#23454;&#20363;&#65288;/api/context.ts&#65289;&#10;&#9500;&#9472;&#9472; diag-api.ts               # &#23548;&#20986; DiagAPI &#23454;&#20363;&#65288;/api/diag.ts&#65289;&#10;&#9500;&#9472;&#9472; index.ts                  # &#20840;&#23616;&#20986;&#21475;&#10;&#9500;&#9472;&#9472; metrics-api.ts            # &#23548;&#20986; MetricsAPI &#23454;&#20363;&#65288;/api/metrics&#65289;&#10;&#9500;&#9472;&#9472; propagation-api.ts        # &#23548;&#20986; PropagationAPI &#23454;&#20363;&#65288;/api/propagation&#65289;&#10;&#9492;&#9472;&#9472; trace-api.ts              # &#23548;&#20986; TraceAPI &#23454;&#20363;&#65288;/api/trace&#65289;</span><br></pre></td></tr></table></figure>
<p>其中内容比较多的 trace 模块结构：  </p>
<p><img src="https://raw.githubusercontent.com/zouchengzhuo/opentelemetry-js/read-doc/api/trace.svg" alt="trace uml"></p>
<h1 id="2-__u7A33_u5B9A_u9636_u6BB5_u6A21_u5757"><a href="#2-__u7A33_u5B9A_u9636_u6BB5_u6A21_u5757" class="headerlink" title="2. 稳定阶段模块"></a>2. 稳定阶段模块</h1><h2 id="2-1__u7A33_u5B9A_u9636_u6BB5_u6A21_u5757_u7ED3_u6784_u68B3_u7406"><a href="#2-1__u7A33_u5B9A_u9636_u6BB5_u6A21_u5757_u7ED3_u6784_u68B3_u7406" class="headerlink" title="2.1 稳定阶段模块结构梳理"></a>2.1 稳定阶段模块结构梳理</h2><p><img src="https://zoucz.com/blogimgs/a833c170-9c8d-11ee-9fa0-5dbc93f9d3ee.md/c9a64c19ca02750a06bc6c9d31caeee3.jpg" alt="image.png">  </p>
<p>模块列表：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">packages&#10;&#9500;&#9472;&#9472; opentelemetry-context-async-hooks      # &#22522;&#20110; async_hooks &#23454;&#29616;&#24322;&#27493;&#19978;&#19979;&#25991;&#24211;&#10;                                           # &#24182;&#23454;&#29616; ContextManager API&#25509;&#21475;&#10;&#9500;&#9472;&#9472; opentelemetry-context-zone             # &#22522;&#20110; zone.js &#23454;&#29616;&#30340; web &#29615;&#22659;&#24322;&#27493;&#19978;&#19979;&#25991;&#24211;&#10;                                           # &#23454;&#38469;&#19978;&#21482;&#26159;&#23548;&#20986; @opentelemetry/context-zone-peer-dep&#10;&#9500;&#9472;&#9472; opentelemetry-context-zone-peer-dep    # &#22522;&#20110; zone.js &#23454;&#29616; ContextManager API&#25509;&#21475;&#10;&#9500;&#9472;&#9472; opentelemetry-core                     # API &#20013; trace &#30340;&#30456;&#20851;&#23454;&#29616;&#65292;&#21518;&#38754;&#32454;&#35828;&#10;&#9500;&#9472;&#9472; opentelemetry-exporter-jaeger          # jaeger &#30340; SpanExporter &#23454;&#29616;&#10;&#9500;&#9472;&#9472; opentelemetry-exporter-zipkin          # zipkin &#30340; SpanExporter &#23454;&#29616;&#10;&#9500;&#9472;&#9472; opentelemetry-propagator-b3            # B3 &#26684;&#24335;&#30340; TextMapPropagator &#23454;&#29616;&#10;&#9500;&#9472;&#9472; opentelemetry-propagator-jaeger        # jaeger &#30340; TextMapPropagator &#23454;&#29616;&#10;&#9500;&#9472;&#9472; opentelemetry-resources                # Resource &#30340;&#25509;&#21475;&#23450;&#20041;&#12289;&#20855;&#20307;&#23454;&#29616;&#10;                                           # &#40664;&#35748;&#30340;&#27983;&#35272;&#22120;&#12289;&#29615;&#22659;&#21464;&#37327; detector &#23454;&#29616;&#10;&#9500;&#9472;&#9472; opentelemetry-sdk-trace-base           # trace &#30340;&#22522;&#30784;&#23454;&#29616;&#65292;&#25552;&#20379;&#25163;&#21160; instrumentation &#26041;&#27861;&#10;                                           # &#23454;&#29616;&#20102; API &#20013;&#30340; TracerProvider &#21644; Sampler&#10;&#9500;&#9472;&#9472; opentelemetry-sdk-trace-node           # &#23454;&#29616;&#20102; node &#29615;&#22659;&#30340; auto instrumentation&#10;                                           # &#37197;&#21512; opentelemetry-instrumentation &#23454;&#29616;&#10;&#9500;&#9472;&#9472; opentelemetry-sdk-trace-web            # &#23454;&#29616;&#20102; web &#29615;&#22659;&#30340; auto instrumentation&#10;&#9500;&#9472;&#9472; opentelemetry-semantic-conventions     # Trace &#21644; Resource &#30340;&#26415;&#35821;&#34920;&#10;&#9500;&#9472;&#9472; opentelemetry-shim-opentracing         # &#20174; opentracing &#21319;&#32423;&#21040; otel &#32452;&#20214;&#10;&#9500;&#9472;&#9472; sdk-metrics                            # metrics &#30340;&#30456;&#20851;&#23454;&#29616;&#65292;&#25552;&#20379;&#25163;&#21160; instrumentation &#26041;&#27861;&#10;&#9492;&#9472;&#9472; template</span><br></pre></td></tr></table></figure>
<h2 id="2-2_@opentelemetry/core__u6A21_u5757_u5177_u4F53_u5B9E_u73B0"><a href="#2-2_@opentelemetry/core__u6A21_u5757_u5177_u4F53_u5B9E_u73B0" class="headerlink" title="2.2 @opentelemetry/core 模块具体实现"></a>2.2 @opentelemetry/core 模块具体实现</h2><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">packages/opentelemetry-core/src&#10;&#9500;&#9472;&#9472; baggage&#10;&#9474;   &#9500;&#9472;&#9472; propagation&#10;&#9474;   &#9474;   &#9492;&#9472;&#9472; W3CBaggagePropagator.ts  # &#25353; W3C trace &#35268;&#33539;&#23454;&#29616;&#30340; Baggage TextMapPropagator&#10;&#9474;   &#9500;&#9472;&#9472; constants.ts                 # Baggage &#30456;&#20851;&#32422;&#26463;&#24120;&#37327;&#10;&#9474;   &#9492;&#9472;&#9472; utils.ts                     # Baggage &#30456;&#20851;&#23454;&#29616;&#10;&#9500;&#9472;&#9472; common                           # API &#20013;&#24037;&#20855;&#20989;&#25968;&#30340;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; ......&#10;&#9500;&#9472;&#9472; internal                         &#10;&#9474;   &#9500;&#9472;&#9472; exporter.ts                  # Exporter &#25509;&#21475;&#23450;&#20041;&#10;&#9474;   &#9492;&#9472;&#9472; validators.ts                # &#21629;&#21517;&#26657;&#39564;&#24037;&#20855;&#20989;&#25968;&#10;&#9500;&#9472;&#9472; platform                         # &#24179;&#21488;&#30456;&#20851;&#24037;&#20855;&#20989;&#25968;&#10;&#9474;   &#9500;&#9472;&#9472; browser                      # &#27983;&#35272;&#22120;&#36816;&#34892;&#29615;&#22659;&#30456;&#20851;&#24037;&#20855;&#20989;&#25968;&#10;&#9474;   &#9474;   &#9500;&#9472;&#9472; ......&#10;&#9474;   &#9500;&#9472;&#9472; node                         # node &#36816;&#34892;&#29615;&#22659;&#30456;&#20851;&#24037;&#20855;&#20989;&#25968;&#10;&#9474;   &#9474;   &#9500;&#9472;&#9472; ......&#10;&#9474;   &#9492;&#9472;&#9472; index.ts&#10;&#9500;&#9472;&#9472; propagation&#10;&#9474;   &#9492;&#9472;&#9472; composite.ts                    # &#22810;&#20256;&#25773;&#28192;&#36947; TextMapPropagator &#23454;&#29616;&#10;&#9500;&#9472;&#9472; trace                               # trace&#30456;&#20851;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; sampler                         # &#21508;&#31867;&#37319;&#26679;&#22120;&#23454;&#29616;&#10;&#9474;   &#9474;   &#9500;&#9472;&#9472; AlwaysOffSampler.ts         # &#19981;&#19978;&#25253;&#30340;&#37319;&#26679;&#22120;&#10;&#9474;   &#9474;   &#9500;&#9472;&#9472; AlwaysOnSampler.ts          # &#24635;&#26159;&#19978;&#25253;&#30340;&#37319;&#26679;&#22120;&#10;&#9474;   &#9474;   &#9500;&#9472;&#9472; ParentBasedSampler.ts       # &#22522;&#20110;&#29238;&#37319;&#26679;&#32467;&#26524;&#30340;&#37319;&#26679;&#22120;&#10;&#9474;   &#9474;   &#9492;&#9472;&#9472; TraceIdRatioBasedSampler.ts # &#22522;&#20110; trace_id &#30340;&#27604;&#20363;&#37319;&#26679;&#22120;&#10;&#9474;   &#9500;&#9472;&#9472; IdGenerator.ts                  # IdGenerator &#25509;&#21475;&#23450;&#20041;&#10;&#9474;   &#9500;&#9472;&#9472; TraceState.ts                   # TraceState API &#25509;&#21475;&#30340;&#20855;&#20307;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; W3CTraceContextPropagator.ts    # &#25353; W3C trace &#35268;&#33539;&#23454;&#29616;&#30340; TraceContext TextMapPropagator&#10;&#9474;   &#9500;&#9472;&#9472; rpc-metadata.ts                 # rpc context &#20803;&#25968;&#25454;&#35774;&#32622;&#20989;&#25968;&#10;&#9474;   &#9492;&#9472;&#9472; suppress-tracing.ts             # &#35774;&#32622;/&#33719;&#21462; &#21462;&#28040; trace &#26631;&#35760;&#30340;&#20989;&#25968;&#10;&#9500;&#9472;&#9472; utils                               # &#24037;&#20855;&#20989;&#25968;&#10;&#9474;   &#9500;&#9472;&#9472; ......&#10;&#9500;&#9472;&#9472; ExportResult.ts                     # export &#32467;&#26524;&#25509;&#21475;&#23450;&#20041;&#10;&#9492;&#9472;&#9472; index.ts</span><br></pre></td></tr></table></figure>
<h2 id="2-3_@opentelemetry/sdk-metrics__u5177_u4F53_u5B9E_u73B0"><a href="#2-3_@opentelemetry/sdk-metrics__u5177_u4F53_u5B9E_u73B0" class="headerlink" title="2.3 @opentelemetry/sdk-metrics 具体实现"></a>2.3 @opentelemetry/sdk-metrics 具体实现</h2><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#9500;&#9472;&#9472; aggregator                         # &#25968;&#25454;&#32858;&#21512;&#30456;&#20851;&#23454;&#29616; DROP/SUM/LAST_VALUE/HISTOGRAM/...&#10;&#9474;   &#9500;&#9472;&#9472; ......&#10;&#9500;&#9472;&#9472; exemplar                           # &#25351;&#26631;&#25968;&#25454;&#37319;&#38598;&#30456;&#20851;&#23454;&#29616;&#65288;measurement&#65289;                                         &#10;&#9474;   &#9500;&#9472;&#9472; ......&#10;&#9500;&#9472;&#9472; export                                  # exporter &#30456;&#20851;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; AggregationSelector.ts              # &#26102;&#24207;&#32858;&#21512;&#26041;&#26696;&#36873;&#25321;&#20989;&#25968;&#10;&#9474;   &#9500;&#9472;&#9472; AggregationTemporality.ts           # &#26102;&#24207;&#32858;&#21512;&#26041;&#26696;&#26522;&#20030;&#65292;&#22686;&#37327;/&#24635;&#37327;&#10;&#9474;   &#9500;&#9472;&#9472; ConsoleMetricExporter.ts            # PushMetricExporter &#22522;&#20110;&#25511;&#21046;&#21488;&#30340;&#23454;&#29616;&#31867;&#10;&#9474;   &#9500;&#9472;&#9472; InMemoryMetricExporter.ts           # PushMetricExporter &#20445;&#23384;&#21040;&#20869;&#23384;&#30340;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; MetricData.ts                       # &#23450;&#20041;&#25351;&#26631;&#25968;&#25454;&#31867;&#22411;&#25509;&#21475;&#65292;&#21253;&#35013; aggregator/types &#30340;&#31867;&#22411;&#10;&#9474;   &#9500;&#9472;&#9472; MetricExporter.ts                   # &#25351;&#26631;&#25968;&#25454;&#23548;&#20986;&#25509;&#21475; PushMetricExporter&#10;&#9474;   &#9500;&#9472;&#9472; MetricProducer.ts                   # &#25351;&#26631;&#25968;&#25454;&#29983;&#20135;&#22120;&#25509;&#21475;&#65292;&#25552;&#20379; collect &#26041;&#27861;&#10;&#9474;   &#9500;&#9472;&#9472; MetricReader.ts                     # &#25351;&#26631;&#35835;&#21462;&#22120;&#25277;&#35937;&#31867;&#65292;&#32500;&#25252; MetricProducer&#10;&#9474;   &#9492;&#9472;&#9472; PeriodicExportingMetricReader.ts    # &#23450;&#26102;&#23548;&#20986;&#30340; MetricReader &#27966;&#29983;&#31867;&#10;&#9500;&#9472;&#9472; state                              # &#25351;&#26631;&#25968;&#25454;&#23384;&#20648;&#30456;&#20851;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; ......&#10;&#9500;&#9472;&#9472; view                               # &#25968;&#25454;&#35270;&#22270;&#30456;&#20851;&#23454;&#29616;&#10;&#9474;   &#9500;&#9472;&#9472; ......&#10;&#9500;&#9472;&#9472; InstrumentDescriptor.ts    # &#25351;&#26631;&#25551;&#36848;&#20449;&#24687;&#25509;&#21475;&#23450;&#20041;&#10;&#9500;&#9472;&#9472; Instruments.ts             # &#23545; API &#20013; Metrics &#20013;&#21508;&#31867;&#25351;&#26631;&#31867;&#22411;&#25509;&#21475;&#30340;&#20855;&#20307;&#23454;&#29616;&#65292;&#22914; Histogram &#31561;&#10;&#9500;&#9472;&#9472; Meter.ts                   # &#23545; API &#20013; Meter &#25509;&#21475;&#30340;&#20855;&#20307;&#23454;&#29616;&#10;&#9500;&#9472;&#9472; MeterProvider.ts           # &#23545; API &#20013; MeterProvider &#25509;&#21475;&#30340;&#20855;&#20307;&#23454;&#29616;&#10;&#9500;&#9472;&#9472; ObservableResult.ts        # &#23545; API &#20013; ObservableResult &#25509;&#21475;&#30340;&#20855;&#20307;&#23454;&#29616;&#10;&#9500;&#9472;&#9472; index.ts                   # &#27169;&#22359;&#20986;&#21475;</span><br></pre></td></tr></table></figure>
<p>sdk-mertics 模块UML：  </p>
<p><img src="https://raw.githubusercontent.com/zouchengzhuo/opentelemetry-js/read-doc/sdk-metrics.svg" alt="trace uml"></p>
<p>指标采集内部组件之间的关系:  </p>
<p><img src="https://zoucz.com/blogimgs/a833c170-9c8d-11ee-9fa0-5dbc93f9d3ee.md/36a83b30fe4fdd90b12ac742ee28fb43.jpg" alt="image.png">  </p>
<p>一个指标 创建 → 收集指标值 → 数据聚合视图 → 指标读取 → 指标上报 的流程:  </p>
<p><img src="https://zoucz.com/blogimgs/a833c170-9c8d-11ee-9fa0-5dbc93f9d3ee.md/e9cc4c750010da80fcaf109831e759f6.jpg" alt="image.png">  </p>
<h1 id="3-__u5B9E_u9A8C_u9636_u6BB5_u6A21_u5757"><a href="#3-__u5B9E_u9A8C_u9636_u6BB5_u6A21_u5757" class="headerlink" title="3. 实验阶段模块"></a>3. 实验阶段模块</h1><p>实验阶段的模块 api 处于不稳定阶段，目前 logs 的 api 在不稳定阶段。还有一些官方的 exporter、instrumentation 模块也处于不稳定阶段。     </p>
<h2 id="3-1__u5B9E_u9A8C_u9636_u6BB5_u6A21_u5757_u7ED3_u6784_u68B3_u7406"><a href="#3-1__u5B9E_u9A8C_u9636_u6BB5_u6A21_u5757_u7ED3_u6784_u68B3_u7406" class="headerlink" title="3.1 实验阶段模块结构梳理"></a>3.1 实验阶段模块结构梳理</h2><p>实验阶段模块代码结构：  </p>
<p><img src="https://zoucz.com/blogimgs/a833c170-9c8d-11ee-9fa0-5dbc93f9d3ee.md/a15f710a5b960dc14dd70e1b247c3659.jpg" alt="image.png">  </p>
<p>实验阶段模块具体的列表和作用如下：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./experimental/packages&#10;&#9500;&#9472;&#9472; api-events  # EventsAPI&#65292;&#21547;&#20840;&#23616; EventEmitterProvider &#27880;&#20876;&#12289;&#20107;&#20214;&#24211;&#38;&#20107;&#20214;&#25509;&#21475;&#23450;&#20041;&#31561;&#10;&#9500;&#9472;&#9472; api-logs    # LogsAPI&#65292;&#21547;&#20840;&#23616; LoggerProvider &#27880;&#20876;&#12289;&#26085;&#24535;&#24211;&#38;&#26085;&#24535;&#25509;&#21475;&#23450;&#20041;&#31561;&#10;&#9500;&#9472;&#9472; exporter-logs-otlp-grpc  # &#23454;&#29616; otlp pb &#25968;&#25454;&#32534;&#30721;&#21327;&#35758; &#38; grpc &#20256;&#36755;&#21327;&#35758;&#30340; logs &#23548;&#20986;&#27169;&#22359;&#10;&#9500;&#9472;&#9472; exporter-logs-otlp-http  # &#23454;&#29616; otlp json &#25968;&#25454;&#32534;&#30721;&#21327;&#35758; &#38; http &#20256;&#36755;&#21327;&#35758;&#30340; logs &#23548;&#20986;&#27169;&#22359;&#10;&#9500;&#9472;&#9472; exporter-logs-otlp-proto # &#23454;&#29616; otlp pb &#25968;&#25454;&#32534;&#30721;&#21327;&#35758; &#38; http &#20256;&#36755;&#21327;&#35758;&#30340; logs &#23548;&#20986;&#27169;&#22359;&#10;&#9500;&#9472;&#9472; exporter-trace-otlp-grpc # &#23454;&#29616; otlp pb &#25968;&#25454;&#32534;&#30721;&#21327;&#35758; &#38; grpc &#20256;&#36755;&#21327;&#35758;&#30340; trace &#23548;&#20986;&#27169;&#22359;&#10;&#9500;&#9472;&#9472; exporter-trace-otlp-http # &#23454;&#29616; otlp json &#25968;&#25454;&#32534;&#30721;&#21327;&#35758; &#38; http &#20256;&#36755;&#21327;&#35758;&#30340; trace &#23548;&#20986;&#27169;&#22359;&#10;&#9500;&#9472;&#9472; exporter-trace-otlp-proto # &#23454;&#29616; otlp pb &#25968;&#25454;&#32534;&#30721;&#21327;&#35758; &#38; http &#20256;&#36755;&#21327;&#35758;&#30340; trace &#23548;&#20986;&#27169;&#22359;&#10;&#9500;&#9472;&#9472; opentelemetry-browser-detector # &#27983;&#35272;&#22120;&#29615;&#22659;&#30340; Resource detector &#10;&#9500;&#9472;&#9472; opentelemetry-exporter-metrics-otlp-grpc # otlp pb &#32534;&#30721; &#38; grpc &#20256;&#36755;&#21327;&#35758;&#30340; metrics &#23548;&#20986;&#10;&#9500;&#9472;&#9472; opentelemetry-exporter-metrics-otlp-http # otlp json &#32534;&#30721; &#38; http &#20256;&#36755;&#21327;&#35758;&#30340; metrics &#23548;&#20986;&#10;&#9500;&#9472;&#9472; opentelemetry-exporter-metrics-otlp-proto # otlp pb &#32534;&#30721; &#38; http &#20256;&#36755;&#21327;&#35758;&#30340; metrics &#23548;&#20986;&#10;&#9500;&#9472;&#9472; opentelemetry-exporter-prometheus    # &#23454;&#29616; prometheus &#21327;&#35758;&#30340; metrics &#23548;&#20986;&#65288;pull&#27169;&#24335;&#65289;&#10;&#9500;&#9472;&#9472; opentelemetry-instrumentation        # &#25552;&#20379; instrumentation &#25509;&#21475;&#23450;&#20041;&#12289;&#22522;&#30784;&#31867;&#23454;&#29616;&#10;                                         # &#20197;&#21450;&#23454;&#29616;&#33258;&#21160;&#26816;&#27979;&#30456;&#20851;&#30340;&#26041;&#27861;&#10;&#9500;&#9472;&#9472; opentelemetry-instrumentation-fetch  # &#22522;&#20110; instrumentation &#23454;&#29616;&#30340; fetch trace&#30417;&#25511;&#27169;&#22359;&#10;&#9500;&#9472;&#9472; opentelemetry-instrumentation-grpc   # &#22522;&#20110; instrumentation &#23454;&#29616;&#30340; grpc trace&#12289;metrics &#30417;&#25511;&#27169;&#22359;&#10;&#9500;&#9472;&#9472; opentelemetry-instrumentation-http   # &#22522;&#20110; instrumentation &#23454;&#29616;&#30340; http trace&#12289;metrics &#30417;&#25511;&#27169;&#22359;&#10;&#9500;&#9472;&#9472; opentelemetry-instrumentation-xml-http-request # &#22522;&#20110; instrumentation &#23454;&#29616;&#30340; xhr trace&#30417;&#25511;&#27169;&#22359;&#10;&#9500;&#9472;&#9472; opentelemetry-sdk-node    # &#25552;&#20379;&#23454;&#29616;&#20102;&#20840;&#37096; Trace &#21644; Metrics API &#30340; SDK (&#27492;&#27169;&#22359;&#21482;&#26159;&#23548;&#20986;&#21253;&#35013;)&#10;&#9500;&#9472;&#9472; otlp-exporter-base        # &#25552;&#20379;&#19968;&#20010; otlp &#23548;&#20986;&#30340;&#22522;&#31867;&#65292;&#20379;&#19978;&#25253;&#21040; collector &#20351;&#29992;&#10;&#9500;&#9472;&#9472; otlp-grpc-exporter-base   # &#22522;&#20110; otlp-exporter-base &#23454;&#29616;&#30340; grpc &#29256;&#65292;&#21482;&#25903;&#25345; node &#10;&#9500;&#9472;&#9472; otlp-proto-exporter-base  # &#22522;&#20110; otlp-exporter-base &#23454;&#29616;&#30340; http + pb &#29256;&#10;&#9500;&#9472;&#9472; otlp-transformer          # otlp &#24207;&#21015;&#21270;&#26041;&#27861;&#10;&#9500;&#9472;&#9472; sdk-logs                  # logs &#30340;&#30456;&#20851;&#23454;&#29616;&#10;&#9492;&#9472;&#9472; shim-opencensus           # opencensus &#21319;&#32423;&#21040; opentelemetry &#24037;&#20855;&#21253;</span><br></pre></td></tr></table></figure>
<h2 id="3-2__u5B9E_u73B0_u81EA_u5B9A_u4E49_u7684_instrumentation__u6A21_u5757"><a href="#3-2__u5B9E_u73B0_u81EA_u5B9A_u4E49_u7684_instrumentation__u6A21_u5757" class="headerlink" title="3.2 实现自定义的 instrumentation 模块"></a>3.2 实现自定义的 instrumentation 模块</h2><ul>
<li><a href="https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation" target="_blank" rel="external">自定义 instrumentation 实现指引文档</a></li>
<li><a href="https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/metapackages/auto-instrumentations-node" target="_blank" rel="external">三方 instrumentation 模块列表</a>  </li>
</ul>
<p>参考文档：  </p>
<ul>
<li><a href="https://github.com/open-telemetry/opentelemetry-js" target="_blank" rel="external">https://github.com/open-telemetry/opentelemetry-js</a></li>
<li><a href="https://www.timescale.com/blog/a-deep-dive-into-open-telemetry-metrics/" target="_blank" rel="external">https://www.timescale.com/blog/a-deep-dive-into-open-telemetry-metrics/</a> </li>
</ul>
]]></content>
    <summary type="html">
    <![CDATA[<p>本文基于 Opentelemetry JS 语言的实现，梳理了其对 OTEL 的具体实现，方便深入理解 OTEL 的工作原理，便于日常项目中的可观测性数据上报和自定义的三方 instrumentation 组件开发。  </p>
<h1 id="1-_API"><a hre]]>
    </summary>
    
      <category term="Opentelemetry" scheme="https://www.zoucz.com/blog/tags/Opentelemetry/"/>
    
      <category term="可观测性" scheme="https://www.zoucz.com/blog/categories/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[Opentelemetry 可观测性规范（二）—— OLTP 协议]]></title>
    <link href="https://www.zoucz.com/blog/2023/12/14/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/12/14/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee/</id>
    <published>2023-12-14T15:26:24.000Z</published>
    <updated>2023-12-14T16:38:43.000Z</updated>
    <content type="html"><![CDATA[<p>官方文档：<a href="https://opentelemetry.io/docs/specs/otlp/" target="_blank" rel="external">https://opentelemetry.io/docs/specs/otlp/</a>  </p>
<p> 本文仓库：<a href="https://github.com/zouchengzhuo/opentelemetry-proto.git" target="_blank" rel="external">https://github.com/zouchengzhuo/opentelemetry-proto.git</a>  </p>
<p>本文UML使用 <a href="https://github.com/GoogleCloudPlatform/proto-gen-md-diagrams.git" target="_blank" rel="external">https://github.com/GoogleCloudPlatform/proto-gen-md-diagrams.git</a> 工具生成   </p>
<h1 id="1-_u57FA_u672C_u6570_u636E_u7ED3_u6784"><a href="#1-_u57FA_u672C_u6570_u636E_u7ED3_u6784" class="headerlink" title="1.基本数据结构"></a>1.基本数据结构</h1><p>opentelemetry/proto 目录下有 log / trace / metrics 的具体数据结构定义  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#9500;&#9472;&#9472; common&#10;&#9500;&#9472;&#9472; logs&#10;&#9500;&#9472;&#9472; metrics&#10;&#9500;&#9472;&#9472; resource&#10;&#9492;&#9472;&#9472; trace</span><br></pre></td></tr></table></figure>
<h2 id="1-1_common__u901A_u7528_u6570_u636E"><a href="#1-1_common__u901A_u7528_u6570_u636E" class="headerlink" title="1.1 common 通用数据"></a>1.1 common 通用数据</h2><p>AnyValue 是 oneof 这些属性  </p>
<p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/9f040c163454db23e16d1c3bbc958c86.jpg" alt="image.png"></p>
<h2 id="1-2_resource__u8D44_u6E90_u63CF_u8FF0"><a href="#1-2_resource__u8D44_u6E90_u63CF_u8FF0" class="headerlink" title="1.2 resource 资源描述"></a>1.2 resource 资源描述</h2><p>在 OpenTelemetry 中，resource.proto 文件定义了 Resource 数据结构。Resource 是一个表示可观察对象（如服务、应用程序、主机等）的实体。它包含一组键值对，用于描述这些实体的属性。这些属性可以用于过滤和聚合观察到的数据，以便更好地理解和分析系统性能。  </p>
<p>Resource 数据结构的主要目的是：  </p>
<ul>
<li>为可观察对象提供上下文信息，以便更好地理解和分析度量、追踪和日志数据。</li>
<li>通过提供有关实体的元数据，帮助将观察到的数据与实际系统组件关联起来。</li>
<li>允许对观察到的数据进行过滤、分组和聚合，以便根据实体属性对性能进行深入分析。  </li>
</ul>
<p>总之，resource.proto 文件中定义的 Resource 数据结构在 OpenTelemetry 中起到了关键作用，它为可观察对象提供了描述性信息，以便在分析和监控系统性能时获得更丰富的上下文。  </p>
<p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/e03a01933af94ddfa78cfce7f91899c5.jpg" alt="image.png">  </p>
<p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/3a776d11cbdc42430c15ff599af2711a.jpg" alt="image.png">  </p>
<h2 id="1-3_logs__u6570_u636E_u7ED3_u6784_u63CF_u8FF0"><a href="#1-3_logs__u6570_u636E_u7ED3_u6784_u63CF_u8FF0" class="headerlink" title="1.3 logs 数据结构描述"></a>1.3 logs 数据结构描述</h2><p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/d76c3b0b9010dde3bbf5a2b2b96d7102.jpg" alt="image.png">  </p>
<p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/c2deb51b5c4b9f70d083f54238823eed.jpg" alt="image.png">  </p>
<p>数据构成方式： <code>Data → List&lt;Resource&gt; → List&lt;Scope&gt; → List&lt;Record&gt;</code>  </p>
<p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/b0f257d516487d69e8128a999c959cb8.jpg" alt="image.png">  </p>
<p>metrics 和 trace 也是一样，经过了几层相同的包装，才到最终的具体字段数据结构。  </p>
<h2 id="1-4_metrics__u6570_u636E_u7ED3_u6784_u63CF_u8FF0"><a href="#1-4_metrics__u6570_u636E_u7ED3_u6784_u63CF_u8FF0" class="headerlink" title="1.4 metrics 数据结构描述"></a>1.4 metrics 数据结构描述</h2><p><a href="https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/opentelemetry/proto/metrics/v1/metrics.proto.md" target="_blank" rel="external">https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/opentelemetry/proto/metrics/v1/metrics.proto.md</a>  </p>
<p>数据构成方式： <code>Data → List&lt;Resource&gt; → List&lt;Scope&gt; → List&lt;Metric&gt;</code>   </p>
<p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/c5965ef391f7bc01697ee2a1069e0470.jpg" alt="image.png"></p>
<p>省略细节数据结构  </p>
<h2 id="1-5_trace_u6570_u636E_u7ED3_u6784_u63CF_u8FF0"><a href="#1-5_trace_u6570_u636E_u7ED3_u6784_u63CF_u8FF0" class="headerlink" title="1.5 trace数据结构描述"></a>1.5 trace数据结构描述</h2><p><a href="https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.proto.md" target="_blank" rel="external">https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.proto.md</a>  </p>
<p>数据构成方式： <code>Data → List&lt;Resource&gt; → List&lt;Scope&gt; → List&lt;Span&gt;</code>  </p>
<p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/8f9f628efc487c313ccf8f524911c6fa.jpg" alt="image.png">  </p>
<h1 id="2-_Collector_Service__u63A5_u53E3_u5B9A_u4E49"><a href="#2-_Collector_Service__u63A5_u53E3_u5B9A_u4E49" class="headerlink" title="2. Collector Service 接口定义"></a>2. Collector Service 接口定义</h1><p>opentelemetry/proto/collector 目录下有服务端的 service 定义  </p>
<h2 id="2-1_logs__u670D_u52A1_u63A5_u53E3"><a href="#2-1_logs__u670D_u52A1_u63A5_u53E3" class="headerlink" title="2.1 logs 服务接口"></a>2.1 logs 服务接口</h2><p><a href="https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/opentelemetry/proto/collector/logs/v1/logs_service.proto.md" target="_blank" rel="external">https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/opentelemetry/proto/collector/logs/v1/logs_service.proto.md</a>  </p>
<p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/66d47596ecbafe025dea2d02a026f0c0.jpg" alt="image.png">  </p>
<h2 id="2-2_metrics__u670D_u52A1_u63A5_u53E3"><a href="#2-2_metrics__u670D_u52A1_u63A5_u53E3" class="headerlink" title="2.2 metrics 服务接口"></a>2.2 metrics 服务接口</h2><p><a href="https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/opentelemetry/proto/collector/metrics/v1/metrics_service.proto.md" target="_blank" rel="external">https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/opentelemetry/proto/collector/metrics/v1/metrics_service.proto.md</a>   </p>
<p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/0a37101d46b10f14a4ea46ccf70136ba.jpg" alt="image.png">  </p>
<h2 id="2-3_trace_u670D_u52A1_u63A5_u53E3"><a href="#2-3_trace_u670D_u52A1_u63A5_u53E3" class="headerlink" title="2.3 trace服务接口"></a>2.3 trace服务接口</h2><p><a href="https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/opentelemetry/proto/collector/trace/v1/trace_service.proto.md" target="_blank" rel="external">https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/opentelemetry/proto/collector/trace/v1/trace_service.proto.md</a>  </p>
<p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/ee03b8587431a5b949b562c0fdcac042.jpg" alt="image.png">  </p>
<h1 id="3-_Collector_Service__u5B9E_u73B0_u89C4_u8303"><a href="#3-_Collector_Service__u5B9E_u73B0_u89C4_u8303" class="headerlink" title="3. Collector Service 实现规范"></a>3. Collector Service 实现规范</h1><p>docs 目录下有完整的设计理念和设计思路的描述， 包括支持的数据编码格式、支持的通信协议、请求&amp;响应的实现规范等。  </p>
<p>此处不再赘述，详见文档：<a href="https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/docs/specification.cn.md" target="_blank" rel="external">https://github.com/zouchengzhuo/opentelemetry-proto/blob/main/docs/specification.cn.md</a>。  </p>
<p><img src="https://zoucz.com/blogimgs/242da5c0-9a95-11ee-9fa0-5dbc93f9d3ee.md/2ce62b50437ffcb2899f853b475fa0b4.jpg" alt="image.png">  </p>
<h1 id="4-__u534F_u8BAE_u7F16_u8BD1"><a href="#4-__u534F_u8BAE_u7F16_u8BD1" class="headerlink" title="4. 协议编译"></a>4. 协议编译</h1><p>根目录提供了 makefile 文件以从协议文件编译得到各个语言的实现。<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">make gen-cpp / gen-csharp / gen-go / gen-java / gen-kotlin / gen-js / gen-objc / gen-openapi / gen-php / gen-python / gen-ruby</span><br></pre></td></tr></table></figure></p>
]]></content>
    <summary type="html">
    <![CDATA[<p>官方文档：<a href="https://opentelemetry.io/docs/specs/otlp/" target="_blank" rel="external">https://opentelemetry.io/docs/specs/otlp/</a>  </]]>
    </summary>
    
      <category term="Opentelemetry" scheme="https://www.zoucz.com/blog/tags/Opentelemetry/"/>
    
      <category term="可观测性" scheme="https://www.zoucz.com/blog/categories/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[Opentelemetry 可观测性规范（一）—— 文档概念梳理]]></title>
    <link href="https://www.zoucz.com/blog/2023/12/14/8499aee0-9a8b-11ee-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/12/14/8499aee0-9a8b-11ee-9fa0-5dbc93f9d3ee/</id>
    <published>2023-12-14T14:17:31.000Z</published>
    <updated>2023-12-14T15:25:38.000Z</updated>
    <content type="html"><![CDATA[<h1 id="1-_Opentelemetry__u662F_u4EC0_u4E48"><a href="#1-_Opentelemetry__u662F_u4EC0_u4E48" class="headerlink" title="1. Opentelemetry 是什么"></a>1. Opentelemetry 是什么</h1><p>Opentelemetry 是一个云原生可观测性的行业标准，它作为一个可观测性框架，提供了 trace、metrics、logs 从协议到 API 到可观测性数据从收集到处理到导出的 SDK 实现。  </p>
<p>它本身并不实现可观测性数据的存储，而是提出一套标准协议（OTLP），和一套对 API 的实现 SDK（OTEL），由具体的各类数据存储后端来适配 OTLP 协议，或者在 OTEL 的具体 SDK 中对三方数据存储后端的协议进行适配。  </p>
<p><img src="https://zoucz.com/blogimgs/8499aee0-9a8b-11ee-9fa0-5dbc93f9d3ee.md/0baa402ad10d867648b0ddbf3416d52e.jpg" alt="image.png">  </p>
<p>支持的数据存储后端：<a href="https://opentelemetry.io/ecosystem/vendors/" target="_blank" rel="external">https://opentelemetry.io/ecosystem/vendors/</a>  </p>
<h1 id="2-__u91CD_u8981_u6982_u5FF5"><a href="#2-__u91CD_u8981_u6982_u5FF5" class="headerlink" title="2. 重要概念"></a>2. 重要概念</h1><p><img src="https://zoucz.com/blogimgs/8499aee0-9a8b-11ee-9fa0-5dbc93f9d3ee.md/a62b13a947098f5452327f67a6b9358c.jpg" alt="image.png">  </p>
<h1 id="3-_Instrumentation__u5B9E_u73B0"><a href="#3-_Instrumentation__u5B9E_u73B0" class="headerlink" title="3. Instrumentation 实现"></a>3. Instrumentation 实现</h1><p>以 JavaScript 语言的实现版本为例进行梳理。   </p>
<p><img src="https://zoucz.com/blogimgs/8499aee0-9a8b-11ee-9fa0-5dbc93f9d3ee.md/bdffea679afee3f5a4c87e3ed99ceb63.jpg" alt="image.png"></p>
<h1 id="4-__u5B9E_u73B0_u89C4_u8303"><a href="#4-__u5B9E_u73B0_u89C4_u8303" class="headerlink" title="4. 实现规范"></a>4. 实现规范</h1><ul>
<li><a href="https://opentelemetry.io/docs/specs/otel/" target="_blank" rel="external">OTEL</a> API、SDK 实现规范</li>
<li><a href="https://opentelemetry.io/docs/specs/otlp/" target="_blank" rel="external">OTLP</a> OTLP 协议数据结构、接口定义规范</li>
<li><a href="https://opentelemetry.io/docs/specs/opamp/" target="_blank" rel="external">OpAMP</a>  Open Agent Management Protocol 开放代理管理协议</li>
<li><a href="https://opentelemetry.io/docs/specs/semconv/" target="_blank" rel="external">Semantic Conventions</a> 语义约定全集  </li>
</ul>
<h1 id="5-_Collector"><a href="#5-_Collector" class="headerlink" title="5. Collector"></a>5. Collector</h1><h2 id="5-1__u7B80_u4ECB"><a href="#5-1__u7B80_u4ECB" class="headerlink" title="5.1 简介"></a>5.1 简介</h2><p><img src="https://zoucz.com/blogimgs/8499aee0-9a8b-11ee-9fa0-5dbc93f9d3ee.md/887ea78a7f992aeb5b5c338262bf502b.jpg" alt="image.png">  </p>
<p><a href="https://github.com/open-telemetry/opentelemetry-collector" target="_blank" rel="external">https://github.com/open-telemetry/opentelemetry-collector</a>  </p>
<p>一个接收(pull or push) otlp 协议数据 → 进行处理 → 上报(pull or push)到多个协议后端  的服务程序。 </p>
<p>用 go 实现。</p>
<p>支持 docker 安装、源码编译安装。   </p>
<h2 id="5-2__u4E09_u79CD_u6570_u636E_u4E0A_u62A5_u6A21_u5F0F"><a href="#5-2__u4E09_u79CD_u6570_u636E_u4E0A_u62A5_u6A21_u5F0F" class="headerlink" title="5.2 三种数据上报模式"></a>5.2 三种数据上报模式</h2><h3 id="No_Collector"><a href="#No_Collector" class="headerlink" title="No Collector"></a>No Collector</h3><p><img src="https://zoucz.com/blogimgs/8499aee0-9a8b-11ee-9fa0-5dbc93f9d3ee.md/ac9a1655214f3af36fa4c463e60acc92.jpg" alt="image.png">  </p>
<p>遥测数据直通后端，适合较简单的开发场景或者小项目，且应用开发语言有对应 backend 协议的 exporter。</p>
<h3 id="Collector_As_Agent"><a href="#Collector_As_Agent" class="headerlink" title="Collector As Agent"></a>Collector As Agent</h3><p><img src="https://zoucz.com/blogimgs/8499aee0-9a8b-11ee-9fa0-5dbc93f9d3ee.md/a9fe1eb835a41bdeb600ebc9d10d9b5a.jpg" alt="image.png">  </p>
<ul>
<li>将 collector 作为一个本地代理程序或者 sidecar </li>
<li>应用程序通过 otlp 协议将数据上报到 collector，collector 提供了主流的 backend export 协议</li>
<li>collector 可以将处理后的结果上报到多个 backend</li>
<li>application 可以是另一个 opentelemetry collector</li>
</ul>
<h3 id="Collector_As_Gateway"><a href="#Collector_As_Gateway" class="headerlink" title="Collector As Gateway"></a>Collector As Gateway</h3><p><img src="https://zoucz.com/blogimgs/8499aee0-9a8b-11ee-9fa0-5dbc93f9d3ee.md/36c6a753441ae0a65ada5437456f3f81.jpg" alt="image.png"></p>
<ul>
<li>和代理模式类似，application 和 collector 不再是一对一，而是多个 application 对同一个 collector 集群</li>
<li>适合大规模集群部署</li>
<li>会引入额外的运维成本</li>
</ul>
<h2 id="5-3__u9AD8_u7EA7_u529F_u80FD"><a href="#5-3__u9AD8_u7EA7_u529F_u80FD" class="headerlink" title="5.3 高级功能"></a>5.3 高级功能</h2><h3 id="Collector__u914D_u7F6E"><a href="#Collector__u914D_u7F6E" class="headerlink" title="Collector 配置"></a>Collector 配置</h3><ul>
<li><a href="https://opentelemetry.io/docs/collector/configuration/#receivers" target="_blank" rel="external">Receivers</a>  接收 pipeline 配置（可以是push模式如grpc/http推送，可以是pull模式如 prometheus export数据拉取）</li>
<li><a href="https://opentelemetry.io/docs/collector/configuration/#processors" target="_blank" rel="external">Processors</a>  处理 pipeline 配置（filtering, dropping, renaming, or recalculating telemetry）</li>
<li><a href="https://opentelemetry.io/docs/collector/configuration/#exporters" target="_blank" rel="external">Exporters</a>  导出 pipeline 配置（可以是push模式如grpc/http推送，可以是pull模式如 prometheus export数据拉取）</li>
<li><a href="https://opentelemetry.io/docs/collector/configuration/#connectors" target="_blank" rel="external">Connectors</a> pipelines连接配置，从上面的一个 pipeline 接受数据，传给下一个 pipeline</li>
<li>扩展插件</li>
</ul>
<p>connectors 示例：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">receivers:&#10;  foo:&#10;&#10;exporters:&#10;  bar:&#10;&#10;connectors:&#10;  count:&#10;    spanevents:&#10;      my.prod.event.count:&#10;        description: The number of span events from my prod environment.&#10;        conditions:&#10;          - &#39;attributes[&#34;env&#34;] == &#34;prod&#34;&#39;&#10;          - &#39;name == &#34;prodevent&#34;&#39;&#10;service:&#10;  pipelines:&#10;    traces:&#10;      receivers: [foo]&#10;      exporters: [count]&#10;    metrics:&#10;      receivers: [count]&#10;      exporters: [bar]</span><br></pre></td></tr></table></figure>
<h3 id="Collector__u6269_u5BB9"><a href="#Collector__u6269_u5BB9" class="headerlink" title="Collector 扩容"></a>Collector 扩容</h3><p><a href="https://opentelemetry.io/docs/collector/scaling/" target="_blank" rel="external">https://opentelemetry.io/docs/collector/scaling/</a>  </p>
<h3 id="u9065_u6D4B_u6570_u636E_u8F6C_u6362"><a href="#u9065_u6D4B_u6570_u636E_u8F6C_u6362" class="headerlink" title="遥测数据转换"></a>遥测数据转换</h3><p><a href="https://opentelemetry.io/docs/collector/transforming-telemetry/" target="_blank" rel="external">https://opentelemetry.io/docs/collector/transforming-telemetry/</a>  </p>
<ul>
<li><a href="https://opentelemetry.io/docs/collector/transforming-telemetry/#adding-or-deleting-attributes" target="_blank" rel="external">添加或删除 Attributes</a></li>
<li><a href="https://opentelemetry.io/docs/collector/transforming-telemetry/#renaming-metrics-or-metric-labels" target="_blank" rel="external">重命名 Metrics 或 Metrics Label</a></li>
<li><a href="https://opentelemetry.io/docs/collector/transforming-telemetry/#enriching-telemetry-with-resource-attributes" target="_blank" rel="external">修改 Resource</a></li>
<li><a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/transformprocessor" target="_blank" rel="external">高级转换</a>  </li>
</ul>
<h3 id="u81EA_u5B9A_u4E49_collector"><a href="#u81EA_u5B9A_u4E49_collector" class="headerlink" title="自定义 collector"></a>自定义 collector</h3><ul>
<li><a href="https://opentelemetry.io/docs/collector/custom-collector/" target="_blank" rel="external">Building a custom collector</a></li>
<li><a href="https://opentelemetry.io/docs/collector/custom-auth/" target="_blank" rel="external">Building a custom authenticator</a></li>
<li><a href="https://opentelemetry.io/docs/collector/trace-receiver/" target="_blank" rel="external">Building a Trace Receiver</a></li>
<li><a href="https://opentelemetry.io/docs/collector/build-connector/" target="_blank" rel="external">Building a Connector</a>  </li>
</ul>
<p>参考文档：<a href="https://opentelemetry.io/docs" target="_blank" rel="external">https://opentelemetry.io/docs</a></p>
]]></content>
    <summary type="html">
    <![CDATA[<h1 id="1-_Opentelemetry__u662F_u4EC0_u4E48"><a href="#1-_Opentelemetry__u662F_u4EC0_u4E48" class="headerlink" title="1. Opentelemetry 是什么">]]>
    </summary>
    
      <category term="Opentelemetry" scheme="https://www.zoucz.com/blog/tags/Opentelemetry/"/>
    
      <category term="可观测性" scheme="https://www.zoucz.com/blog/categories/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[Prometheus部署及使用]]></title>
    <link href="https://www.zoucz.com/blog/2023/07/19/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/07/19/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee/</id>
    <published>2023-07-19T12:14:11.000Z</published>
    <updated>2023-12-14T14:56:49.000Z</updated>
    <content type="html"><![CDATA[<h1 id="1-__u542F_u52A8_u5BB9_u5668_u73AF_u5883"><a href="#1-__u542F_u52A8_u5BB9_u5668_u73AF_u5883" class="headerlink" title="1. 启动容器环境"></a>1. 启动容器环境</h1><p>启动带ssh容器.<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -it --name prometheus --privileged -p 36021:22 -p 36022-36030:36022-36030 centos:7 bash</span><br></pre></td></tr></table></figure></p>
<p>安装相关软件.<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install -y wget openssh-server net-tools</span><br></pre></td></tr></table></figure></p>
<p>生成sshkey，修改ssh密码，启动ssh.<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key&#10;passwd&#10;/usr/sbin/sshd</span><br></pre></td></tr></table></figure></p>
<p>配置ssh免密<br>/etc/ssh/sshd_config 文件中解开下面的注释  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">RSAAuthentication yes&#10;PubkeyAuthentication yes&#10;AuthorizedKeysFile  .ssh/authorized_keys</span><br></pre></td></tr></table></figure>
<p>向 <code>~/.ssh/authorized_keys</code> 中贴入本地的 id_rsa.pub 内容，重启sshd</p>
<h1 id="2-__u5B89_u88C5_u3001_u542F_u52A8_prometheus"><a href="#2-__u5B89_u88C5_u3001_u542F_u52A8_prometheus" class="headerlink" title="2. 安装、启动 prometheus"></a>2. 安装、启动 prometheus</h1><p>下载最新的 lts 版本<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget https://github.com/prometheus/prometheus/releases/download/v2.43.0/prometheus-2.43.0.linux-amd64.tar.gz</span><br></pre></td></tr></table></figure></p>
<p>解压<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tar zxvf prometheus-2.43.0.linux-amd64.tar.gz</span><br></pre></td></tr></table></figure></p>
<p>进入解压目录<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cd prometheus-2.43.0.linux-amd64</span><br></pre></td></tr></table></figure></p>
<p>创建数据目录（也可以使用三方存储，见文档 Storage | Prometheus）<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mkdir -p datamkdir -p data</span><br></pre></td></tr></table></figure></p>
<p>由于容器的端口暴露范围是 36022~36030，将 prometheus.yml 中监控自身的 9090 端口改为 36022<br>在 36022 端口启动 prometheus  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./prometheus --web.listen-address=:36022</span><br></pre></td></tr></table></figure>
<p>此时即可通过 <a href="http://xxx.xxx.xxx.xxx:36022/" target="_blank" rel="external">http://xxx.xxx.xxx.xxx:36022/</a> 访问 prometheus<br><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/e1833177c8c9b0098fe160a9e4562f4e.jpg" alt="image.png"></p>
<h1 id="3-__u6570_u636E_u91C7_u96C6"><a href="#3-__u6570_u636E_u91C7_u96C6" class="headerlink" title="3. 数据采集"></a>3. 数据采集</h1><p><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/5369f9e6602962b0ff54379593a75da8.jpg" alt="image.png">  </p>
<p>prometheus 是 pull 系统  </p>
<h2 id="3-1__u5B89_u88C5exporter"><a href="#3-1__u5B89_u88C5exporter" class="headerlink" title="3.1 安装exporter"></a>3.1 安装exporter</h2><p>以采集机器节点指标（CPU, 内存，磁盘等信息CPU, 内存，磁盘等信息）上报为例<br>下载最新的 lts 版本<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget https://github.com/prometheus/node_exporter/releases/download/v1.5.0/node_exporter-1.5.0.linux-amd64.tar.gz</span><br></pre></td></tr></table></figure></p>
<p>解压<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tar zxvf node_exporter-1.5.0.linux-amd64.tar.gz</span><br></pre></td></tr></table></figure></p>
<p>启动<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./node_exporter --web.listen-address=:36023</span><br></pre></td></tr></table></figure></p>
<p>此时即可通过 <a href="http://xxx.xxx.xxx.xxx:36023/" target="_blank" rel="external">http://xxx.xxx.xxx.xxx:36023/</a> 访问 node_exporter  </p>
<p><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/d160aa35393414bdd349de9f6ddf04b8.jpg" alt="image.png">  </p>
<p><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/ff0cac9c640d16bf8cf19aca99ba4239.jpg" alt="image.png">  </p>
<ul>
<li>HELP用于解释当前指标的含义</li>
<li>TYPE 则说明当前指标的数据类型</li>
<li>go_gc_duration_seconds{quantile=”0”} 2.6617e-05 是一条指标在此时此刻的一个样本，其构成含义是<metric name>{<label name>=<label value>, …} <sample value></sample></label></label></metric></li>
</ul>
<h2 id="3-2__u6DFB_u52A0_u672C_u5730_u91C7_u96C6_u914D_u7F6E"><a href="#3-2__u6DFB_u52A0_u672C_u5730_u91C7_u96C6_u914D_u7F6E" class="headerlink" title="3.2 添加本地采集配置"></a>3.2 添加本地采集配置</h2><p>修改 prometheus 目录下的 prometheus.yml 文件，添加上一步添加的 node_exporter，然后重启 prometheus 或者 reload。<code>curl -X POST http://localhost:36022/-/reload</code>  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">scrape_configs:&#10;  - job_name: &#39;prometheus&#39;&#10;    static_configs:&#10;      - targets: [&#39;localhost:36022&#39;]&#10;  # &#37319;&#38598;node exporter&#30417;&#25511;&#25968;&#25454;&#10;  - job_name: &#39;node&#39;&#10;    static_configs:&#10;      - targets: [&#39;localhost:36023&#39;]</span><br></pre></td></tr></table></figure>
<p>访问 prometheus 的网页，输入 up 查询，即可看到采集 job 。  </p>
<p><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/78254e86616cdd8dba1b8c4d714b9e62.jpg" alt="image.png">  </p>
<p>除了通过使用 ‘up’ 表达式查询当前所有Instance的状态以外，还可以通过Prometheus UI中的Targets页面查看当前所有的监控采集任务，以及各个任务下所有实例的状态：  </p>
<p><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/c5a0b98c67fc0ba0b4552dff19448a84.jpg" alt="image.png"></p>
<h2 id="3-3__u6DFB_u52A0_u52A8_u6001_u91C7_u96C6_u914D_u7F6E"><a href="#3-3__u6DFB_u52A0_u52A8_u6001_u91C7_u96C6_u914D_u7F6E" class="headerlink" title="3.3 添加动态采集配置"></a>3.3 添加动态采集配置</h2><p>上面的示例中，使用的是静态配置 (static_configs) 的方式定义监控目标。  </p>
<p>prometheus还支持与DNS、Consul、E2C、Kubernetes等方式自动发现监控目标。  </p>
<p>详见配置文档：<a href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config" target="_blank" rel="external">Configuration | Prometheus</a> 中的 …_sd_config.   </p>
<h1 id="4-__u6982_u5FF5_u7406_u89E3"><a href="#4-__u6982_u5FF5_u7406_u89E3" class="headerlink" title="4. 概念理解"></a>4. 概念理解</h1><h2 id="4-1__u6307_u6807_28metric_29"><a href="#4-1__u6307_u6807_28metric_29" class="headerlink" title="4.1 指标(metric)"></a>4.1 指标(metric)</h2><p>这就是一个指标，包含指标名，指标label等信息。  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#60;metric name&#62;&#123;&#60;label name&#62;=&#60;label value&#62;, ...&#125;</span><br></pre></td></tr></table></figure>
<p>指标的名称(metric name)代表被监控样本的含义，指标名称只能由ASCII字符、数字、下划线以及冒号组成并必须符合正则表达式[a-zA-Z_:][a-zA-Z0-9_:]*。  </p>
<p>标签(label)反映了当前样本的特征维度，通过这些维度 prometheus 可以对样本数据进行过滤，聚合等。标签的名称只能由ASCII字符、数字以及下划线组成并满足正则表达式[a-zA-Z_][a-zA-Z0-9_]*。  </p>
<p>其中以 <code>__</code> 作为前缀的标签，是系统保留的关键字，只能在系统内部使用。标签的值则可以包含任何Unicode编码的字符。在Prometheus的底层实现中指标名称实际上是以 <code>__name__=&lt;metric name&gt;</code>的形式保存在数据库中的，因此以下两种方式等价：  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">api_http_requests_total&#123;method=&#34;POST&#34;, handler=&#34;/messages&#34;&#125;&#10;&#10;&#123;__name__=&#34;api_http_requests_total&#34;&#65292;method=&#34;POST&#34;, handler=&#34;/messages&#34;&#125;</span><br></pre></td></tr></table></figure>
<p>在 prometheus 源码中也可以找到指标(Metric)对应的数据结构，如下所示：  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">type Metric LabelSet&#10;type LabelSet map[LabelName]LabelValue&#10;type LabelName string&#10;type LabelValue string</span><br></pre></td></tr></table></figure>
<h2 id="4-2__u6837_u672C_uFF08sample_uFF09"><a href="#4-2__u6837_u672C_uFF08sample_uFF09" class="headerlink" title="4.2 样本（sample）"></a>4.2 样本（sample）</h2><p>prometheus 针对一个指标，在一个时间点的采样，称为一个样本（sample）。  </p>
<p>样本由以下三部分组成：  </p>
<ul>
<li>指标(metric)：metric name 和 label set</li>
<li>时间戳(timestamp)：一个精确到毫秒的时间戳</li>
<li>样本值(value)： 一个float64的浮点型数据表示当前样本的值（从 prometheus v2.4.0 开始还支持了直方图类型）。</li>
</ul>
<h2 id="4-3__u6837_u672C_u6570_u636E_u7C7B_u578B"><a href="#4-3__u6837_u672C_u6570_u636E_u7C7B_u578B" class="headerlink" title="4.3 样本数据类型"></a>4.3 样本数据类型</h2><h3 id="Counter__u8BA1_u6570_u5668"><a href="#Counter__u8BA1_u6570_u5668" class="headerlink" title="Counter 计数器"></a>Counter 计数器</h3><p>只增不减的计数器Counter：只增不减的计数器。  </p>
<p>如 node_cpu_guest_seconds_total CPU 使用总时间、http_requests_total http 请求次数。</p>
<h3 id="Gauge__u4EEA_u8868_u76D8"><a href="#Gauge__u4EEA_u8868_u76D8" class="headerlink" title="Gauge 仪表盘"></a>Gauge 仪表盘</h3><p>可增可减的仪表盘Gauge：可增可减的仪表盘。  </p>
<p>如 node_load1 CPU 的实时负载、node_memory_Active_bytes 内存用量。</p>
<h3 id="Histogram__u76F4_u65B9_u56FE"><a href="#Histogram__u76F4_u65B9_u56FE" class="headerlink" title="Histogram 直方图"></a>Histogram 直方图</h3><p>在客户端计算好各分段的值  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># HELP prometheus_tsdb_compaction_chunk_range Final time range of chunks on their first compaction&#10;# TYPE prometheus_tsdb_compaction_chunk_range histogram&#10;prometheus_tsdb_compaction_chunk_range_bucket&#123;le=&#34;100&#34;&#125; 0&#10;prometheus_tsdb_compaction_chunk_range_bucket&#123;le=&#34;400&#34;&#125; 0&#10;prometheus_tsdb_compaction_chunk_range_bucket&#123;le=&#34;1600&#34;&#125; 0&#10;prometheus_tsdb_compaction_chunk_range_bucket&#123;le=&#34;6400&#34;&#125; 0&#10;prometheus_tsdb_compaction_chunk_range_bucket&#123;le=&#34;25600&#34;&#125; 0&#10;prometheus_tsdb_compaction_chunk_range_bucket&#123;le=&#34;102400&#34;&#125; 0&#10;prometheus_tsdb_compaction_chunk_range_bucket&#123;le=&#34;409600&#34;&#125; 0&#10;prometheus_tsdb_compaction_chunk_range_bucket&#123;le=&#34;1.6384e+06&#34;&#125; 260&#10;prometheus_tsdb_compaction_chunk_range_bucket&#123;le=&#34;6.5536e+06&#34;&#125; 780&#10;prometheus_tsdb_compaction_chunk_range_bucket&#123;le=&#34;2.62144e+07&#34;&#125; 780&#10;prometheus_tsdb_compaction_chunk_range_bucket&#123;le=&#34;+Inf&#34;&#125; 780&#10;prometheus_tsdb_compaction_chunk_range_sum 1.1540798e+09&#10;prometheus_tsdb_compaction_chunk_range_count 780</span><br></pre></td></tr></table></figure>
<h3 id="sample__u6458_u8981"><a href="#sample__u6458_u8981" class="headerlink" title="sample 摘要"></a>sample 摘要</h3><p>在客户端计算好各分位的分位数、总数、计数  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># HELP prometheus_tsdb_wal_fsync_duration_seconds Duration of WAL fsync.&#10;# TYPE prometheus_tsdb_wal_fsync_duration_seconds summary&#10;prometheus_tsdb_wal_fsync_duration_seconds&#123;quantile=&#34;0.5&#34;&#125; 0.012352463&#10;prometheus_tsdb_wal_fsync_duration_seconds&#123;quantile=&#34;0.9&#34;&#125; 0.014458005&#10;prometheus_tsdb_wal_fsync_duration_seconds&#123;quantile=&#34;0.99&#34;&#125; 0.017316173&#10;prometheus_tsdb_wal_fsync_duration_seconds_sum 2.888716127000002&#10;prometheus_tsdb_wal_fsync_duration_seconds_count 216</span><br></pre></td></tr></table></figure>
<h2 id="4-4__u65F6_u95F4_u5E8F_u5217"><a href="#4-4__u65F6_u95F4_u5E8F_u5217" class="headerlink" title="4.4 时间序列"></a>4.4 时间序列</h2><p>从上面 node_exporter 示例中，某一时刻拿到的 一条指标数据格式为：  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#60;metric name&#62;&#123;&#60;label name&#62;=&#60;label value&#62;, ...&#125; &#60;sample value&#62;</span><br></pre></td></tr></table></figure>
<p>prometheus 在存储数据时，会按照时间顺序将每个 “<metric name>{<label name>=<label value>, …}” 对应的 “<sample value>“ 存储起来，称之为时间序列。  </sample></label></label></metric></p>
<p><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/d693359ab5a5ec7f00f86d82f89132c9.jpg" alt="image.png"></p>
<h1 id="5-__u6570_u636E_u67E5_u8BE2"><a href="#5-__u6570_u636E_u67E5_u8BE2" class="headerlink" title="5. 数据查询"></a>5. 数据查询</h1><p>prometheus 支持的查询方法：  </p>
<ul>
<li><a href="https://prometheus.io/docs/prometheus/latest/querying/basics/" target="_blank" rel="external">基本查询</a></li>
<li><a href="https://prometheus.io/docs/prometheus/latest/querying/operators/" target="_blank" rel="external">运算符查询</a></li>
<li><a href="https://prometheus.io/docs/prometheus/latest/querying/functions/" target="_blank" rel="external">函数查询</a></li>
</ul>
<p>下面是各种查询使用的实例。  </p>
<h2 id="5-1__u6839_u636E_u6307_u6807_u540D_u67E5_u8BE2"><a href="#5-1__u6839_u636E_u6307_u6807_u540D_u67E5_u8BE2" class="headerlink" title="5.1 根据指标名查询"></a>5.1 根据指标名查询</h2><p>可以直接输入属性名  </p>
<p><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/4121890789169586c96d6d5fa997cf10.jpg" alt="image.png"></p>
<h2 id="5-2__u51FD_u6570_u67E5_u8BE2"><a href="#5-2__u51FD_u6570_u67E5_u8BE2" class="headerlink" title="5.2 函数查询"></a>5.2 函数查询</h2><p>例如使用rate()函数，可以计算在单位时间内样本数据的变化情况即增长率，因此通过该函数我们可以近似的通过CPU使用时间计算CPU的利用率  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rate(node_cpu_seconds_total[2m])</span><br></pre></td></tr></table></figure>
<p><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/70c5f9b667f9658bd9a7ad865c456064.jpg" alt="image.png">  </p>
<p><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/cdb5c25403336f6916418f506fbb37c4.jpg" alt="image.png">  </p>
<p>这个查询会把所有CPU、所有模式下 的数据分别统计。<br>支持的函数列表详见：<a href="https://prometheus.io/docs/prometheus/latest/querying/functions/" target="_blank" rel="external">Query functions | Prometheus</a></p>
<h2 id="5-3__u8FD0_u7B97_u7B26"><a href="#5-3__u8FD0_u7B97_u7B26" class="headerlink" title="5.3 运算符"></a>5.3 运算符</h2><h3 id="u805A_u5408_u8FD0_u7B97_u7B26"><a href="#u805A_u5408_u8FD0_u7B97_u7B26" class="headerlink" title="聚合运算符"></a>聚合运算符</h3><p>忽略某个 label 的维度，上面的图中，可以看到有 cpu、mode 两个 label，下面的语句可以去掉 cpu 的维度，聚合方法使用 avg  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">avg without(cpu) (rate(node_cpu_seconds_total[2m]))</span><br></pre></td></tr></table></figure>
<p><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/b26e1040ead0922710c404dbef5ef6b0.jpg" alt="image.png"></p>
<p>如果需要计算系统CPU的总体使用率，通过排除系统闲置的CPU使用率即可获得如果需要计算系统CPU的总体使用率，通过排除系统闲置的CPU使用率即可获得  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">avg without(cpu) (rate(node_cpu&#123;mode=&#34;idle&#34;&#125;[2m]))</span><br></pre></td></tr></table></figure>
<h3 id="u7B97_u672F_u8FD0_u7B97_u7B26"><a href="#u7B97_u672F_u8FD0_u7B97_u7B26" class="headerlink" title="算术运算符"></a>算术运算符</h3><p>如果需要计算系统CPU的总体使用率，通过排除系统闲置的CPU使用率即可获得:如果需要计算系统CPU的总体使用率，通过排除系统闲置的CPU使用率即可获得:  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">1 - avg without(cpu) (rate(node_cpu_seconds_total&#123;mode=&#34;idle&#34;&#125;[2m]))</span><br></pre></td></tr></table></figure>
<p>写一段脚本占用一个 CPU 核心，观察监控指标变化  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#!/bin/sh&#10;# set task to cpu0&#10;nice taskset -c 0 yes &#62;/dev/null &#38;&#10;# Wait for 20 seconds&#10;sleep 20&#10;&#10;# Kill the yes commands&#10;pkill yes</span><br></pre></td></tr></table></figure>
<p><img src="https://zoucz.com/blogimgs/c4c25d00-262d-11ee-9fa0-5dbc93f9d3ee.md/2ad109fe876d1ff25b96827fbd478424.jpg" alt="image.png"></p>
<p>支持的所有运算符详见：<a href="https://prometheus.io/docs/prometheus/latest/querying/operators/" target="_blank" rel="external">Operators | Prometheus</a></p>
<p>参考文档：  </p>
<ul>
<li><a href="https://yunlzheng.gitbook.io/prometheus-book/parti-prometheus-ji-chu/promql/what-is-prometheus-metrics-and-labels" target="_blank" rel="external">理解时间序列 - prometheus-book</a></li>
<li><a href="https://prometheus.io/docs/introduction/overview/" target="_blank" rel="external">Overview | Prometheus</a></li>
</ul>
]]></content>
    <summary type="html">
    <![CDATA[<h1 id="1-__u542F_u52A8_u5BB9_u5668_u73AF_u5883"><a href="#1-__u542F_u52A8_u5BB9_u5668_u73AF_u5883" class="headerlink" title="1. 启动容器环境"></a]]>
    </summary>
    
      <category term="Prometheus" scheme="https://www.zoucz.com/blog/tags/Prometheus/"/>
    
      <category term="可观测性" scheme="https://www.zoucz.com/blog/categories/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/"/>
    
      <category term="后台开发" scheme="https://www.zoucz.com/blog/categories/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/%E5%90%8E%E5%8F%B0%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[协程实现原理]]></title>
    <link href="https://www.zoucz.com/blog/2023/04/09/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/04/09/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee/</id>
    <published>2023-04-09T07:43:07.000Z</published>
    <updated>2024-01-06T18:48:05.000Z</updated>
    <content type="html"><![CDATA[<h1 id="1-__u7A0B_u5E8F_u8FDB_u7A0B_u5185_u5B58_u5E03_u5C40"><a href="#1-__u7A0B_u5E8F_u8FDB_u7A0B_u5185_u5B58_u5E03_u5C40" class="headerlink" title="1. 程序进程内存布局"></a>1. 程序进程内存布局</h1><p>本文参考《程序员的自我修养》部分章节的内容，描述了进程虚拟地址空间布局、并调试了函数调用栈帧调度过程。然后基于对函数调用过程的理解，尝试了最简单的协程及协程库的实现方式。最后描述和无栈协程和有栈协程的区别，并列举了常见的无栈协程和它们的实现方式。  </p>
<h2 id="1-1__u5178_u578B_u865A_u62DF_u5185_u5B58_u5E03_u5C40"><a href="#1-1__u5178_u578B_u865A_u62DF_u5185_u5B58_u5E03_u5C40" class="headerlink" title="1.1 典型虚拟内存布局"></a>1.1 典型虚拟内存布局</h2><p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/56d0653013a1c95d544432393cff7018.jpg" alt="image.png"></p>
<p>上图描述了程序加载执行时，其虚拟内存空间的典型布局，从上到下依次是  </p>
<ul>
<li>内核空间</li>
<li>自高位地址向低位地址增长的栈空间</li>
<li>动态库映射区</li>
<li>自低位地址向高位地址生长的堆空间</li>
<li>可读写的数据段</li>
<li>只读数据段</li>
<li>预留区</li>
</ul>
<h2 id="1-2__u591A_u7EBF_u7A0B_u7684_u5185_u5B58_u7A7A_u95F4"><a href="#1-2__u591A_u7EBF_u7A0B_u7684_u5185_u5B58_u7A7A_u95F4" class="headerlink" title="1.2 多线程的内存空间"></a>1.2 多线程的内存空间</h2><p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/313b46075aabdcd448158e380f531ed0.jpg" alt="image.png"></p>
<p>进程内的线程，共享进程的代码、数据、进程空间、打开的fd，但是有各自的寄存器值、栈、线程局部数据。  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/cccd41512c5dfc1471478858b49a3e2e.jpg" alt="image.png"></p>
<p>拥有多线程的进程虚拟内存空间中，每个线程都拥有自己的栈。<br>假设有2个线程运行在一个处理器上，从运行一个线程(T1)切换到另一个线程(T2)时，一定会发生上下文切换。对于进程，我们需要将状态保存到进程控制块(PCB)中，现在我们需要一个或多个线程控制块(TCB)来保存每个线程的状态，但是和进程上下文切换相比，线程在进行上下文切换的时候地址空间保持不变(即不需要切换当前使用的页表)。  </p>
<h2 id="1-3__u51FD_u6570_u6267_u884C_u7684_u6808_u5E27"><a href="#1-3__u51FD_u6570_u6267_u884C_u7684_u6808_u5E27" class="headerlink" title="1.3 函数执行的栈帧"></a>1.3 函数执行的栈帧</h2><p>栈内存中保存了一个函数调用所需要的维护信息，这常常被称为堆栈帧（Stack Frame）或活动记录（Activate Record）。   </p>
<p>堆栈帧一般包括如下几方面内容：</p>
<ul>
<li>函数的返回地址和参数</li>
<li>临时变量：包括函数的非静态局部变量以及编译器自动生成的其他临时变量</li>
<li>保存的上下文：包括在函数调用前后需要保持不变的寄存器</li>
</ul>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/6493f7fb9f3e6d92624469acf4f5d34b.jpg" alt="image.png"></p>
<p>该函数对应的堆栈帧的内存空间如下所示，一个函数的堆栈帧增长过程是这样的：</p>
<ul>
<li>把所有或一部分参数加入栈中，如果有其他参数没有入栈，那么使用某些寄存器传递</li>
<li>把当前指令的下一条指令地址压入栈中</li>
<li>跳转到函数体执行:</li>
<li>把[e|r]bp压入栈中，指向上一个函数堆栈帧中的帧指针的位置（32位的是ebp，64位的是rbp）</li>
<li>保存调用前后需要保存不变的寄存器的值</li>
<li>将局部变量压入栈中</li>
<li>…</li>
</ul>
<h2 id="1-4__u51FD_u6570_u6267_u884C_u8FC7_u7A0B_u8C03_u8BD5"><a href="#1-4__u51FD_u6570_u6267_u884C_u8FC7_u7A0B_u8C03_u8BD5" class="headerlink" title="1.4 函数执行过程调试"></a>1.4 函数执行过程调试</h2><h3 id="u5BC4_u5B58_u5668"><a href="#u5BC4_u5B58_u5668" class="headerlink" title="寄存器"></a>寄存器</h3><p>在x86-64架构的Linux中，常用的寄存器有以下几种：</p>
<ul>
<li>通用寄存器：包括 RAX, RBX, RCX, RDX, RDI, RSI, RBP, RSP, R8-R15。它们主要用于存储临时数据，参与各种算术和逻辑运算。</li>
<li>段寄存器：包括 CS, DS, ES, FS, GS, SS。它们主要用于存储内存中各段的基地址。</li>
<li>指令指针寄存器：RIP，它存储下一条将要执行的指令的地址。</li>
<li>标志寄存器：RFLAGS，它存储了各种状态标志，如零标志（ZF）、进位标志（CF）、奇偶标志（PF）等。</li>
<li>浮点寄存器：包括 ST0-ST7，它们用于浮点运算。</li>
<li>MMX寄存器：包括 MM0-MM7，它们用于多媒体指令集（MMX）的运算。</li>
<li>XMM寄存器：包括 XMM0-XMM15，它们用于流式 SIMD 扩展指令集（SSE）的运算。<br>具体的寄存器列表和含义可以参考相关的x86-64架构的相关文档。如Intel官方的《Intel 64 and IA-32 Architectures Software Developer Manuals》或者  <a href="https://wiki.osdev.org/CPU_Registers_x86-64" target="_blank" rel="external">https://wiki.osdev.org/CPU_Registers_x86-64</a> 。  </li>
</ul>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/6f9d9d21f439854af656d0cf7d1dfd9c.jpg" alt="image.png"></p>
<p>寄存器从易失性（volatility）角度可以分为易失性寄存器和非易失性寄存器。易失性是指在中断、异常、函数调用或上下文切换等情况下，寄存器的值可能会被改变，而且系统不会自动保存和恢复这些值。因此，如果程序需要在这些情况下保留寄存器的值，就需要显式地保存（push）和恢复（pop）这些值。  </p>
<p>如上图（System V ABI 规范），下面这些寄存器需要在函数调用过程后，返回原函数时，保证其值与原来一致，即被调函数需要保存其原值，并负责恢复。</p>
<ul>
<li>%rbp, %rbx, %r12, %r13, %r14, %r15</li>
<li>%rsp</li>
<li>mxcsr</li>
<li>x87 CW</li>
</ul>
<h3 id="u51FD_u6570_u6267_u884C_u8FC7_u7A0B"><a href="#u51FD_u6570_u6267_u884C_u8FC7_u7A0B" class="headerlink" title="函数执行过程"></a>函数执行过程</h3><p>下面写一段  c 代码，来逐步执行调试，查看其寄存器操作和栈帧变化情况  </p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">sum</span><span class="params">(<span class="keyword">int</span> l, <span class="keyword">int</span> r)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> l+r;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">    <span class="keyword">int</span> a = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">int</span> b = <span class="number">2</span>;</span><br><span class="line">    <span class="keyword">int</span> c = sum(a, b);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>编译出调试版本的可执行文件<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gcc -g ./main.c -o main</span><br></pre></td></tr></table></figure></p>
<p>基于此可执行文件，我们按下面的计划执行并观察：</p>
<ul>
<li>Step1. 观察 main 函数的汇编代码，查看 main 函数自身执行以及调用 sum 函数过程中的操作</li>
<li>Step2. 观察 sum 函数的汇编代码，查看 sum 函数执行过程中的操作</li>
<li>Step3. 在 main 函数入口打上断点，查看 main 函数栈帧区间（即 %rbp - %rsp）的值和局部变量值</li>
<li>Step4. 在调用 sum 函数的地方打上断点，查看 main 函数栈帧区间的值</li>
<li>Step5. 在 sum 函数入口打上断点，查看 sum 函数的栈帧区间的值、返回地址、%rbp的值指向地址的值</li>
<li>Step6. 在 main 函数的结尾打上断点，查看 main 函数栈帧区间的值</li>
</ul>
<p>Step1. 观察 main 函数的汇编代码<br>然后使用 gdb 进入函数调试，并使用 disassemble 指令反汇编 main 函数  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/329ec77c2dbd50c9f833d549a6000999.jpg" alt="image.png"></p>
<p>可以看到，main 函数的实际执行过程是<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">5       int main()&#123;&#10;   0x0000000000400501 &#60;+0&#62;:     push   %rbp             # &#23558; %rbp &#23492;&#23384;&#22120;&#20837;&#26632;&#65292;&#20197;&#20445;&#23384;&#20043;&#21069;&#30340;&#22522;&#22320;&#22336;&#10;   0x0000000000400502 &#60;+1&#62;:     mov    %rsp,%rbp        # &#23558;&#26632;&#39030;&#23492;&#23384;&#22120;&#65288;%rsp&#65289;&#30340;&#20540;&#22797;&#21046;&#21040;&#22522;&#22336;&#23492;&#23384;&#22120;&#65288;%rbp&#65289;&#65292;&#21019;&#24314;&#26032;&#30340;&#26632;&#24103;&#10;   0x0000000000400505 &#60;+4&#62;:     sub    $0x10,%rsp       # &#23558;&#26632;&#39030;&#23492;&#23384;&#22120;&#65288;%rsp&#65289;&#20943; 16&#65292;&#20026;&#23616;&#37096;&#21464;&#37327;&#39044;&#30041;&#31354;&#38388;&#10;&#10;6           int a = 1;&#10;   0x0000000000400509 &#60;+8&#62;:     movl   $0x1,-0x4(%rbp)  # &#23558;&#20540; 1 &#23384;&#20837;&#22522;&#22336;&#23492;&#23384;&#22120;&#65288;%rbp&#65289;&#20943; 4 &#30340;&#20301;&#32622;&#65292;&#21363;&#21464;&#37327; a &#30340;&#20869;&#23384;&#20301;&#32622;&#10;&#10;7           int b = 2;&#10;   0x0000000000400510 &#60;+15&#62;:    movl   $0x2,-0x8(%rbp)  # &#23558;&#20540; 2 &#23384;&#20837;&#22522;&#22336;&#23492;&#23384;&#22120;&#65288;%rbp&#65289;&#20943; 8 &#30340;&#20301;&#32622;&#65292;&#21363;&#21464;&#37327; b &#30340;&#20869;&#23384;&#20301;&#32622;&#10;&#10;8           int c = sum(a, b);                          # &#19979;&#38754;&#30340;&#27719;&#32534;&#21363;&#20026;&#19968;&#20010;&#20989;&#25968;&#30340;&#35843;&#29992;&#36807;&#31243;&#10;   0x0000000000400517 &#60;+22&#62;:    mov    -0x8(%rbp),%edx  # &#23558;&#21464;&#37327; b &#30340;&#20540;&#22797;&#21046;&#21040;&#25968;&#25454;&#23492;&#23384;&#22120;&#65288;%edx&#65289;&#10;   0x000000000040051a &#60;+25&#62;:    mov    -0x4(%rbp),%eax  # &#23558;&#21464;&#37327; a &#30340;&#20540;&#22797;&#21046;&#21040;&#32047;&#21152;&#23492;&#23384;&#22120;&#65288;%eax&#65289;&#10;   0x000000000040051d &#60;+28&#62;:    mov    %edx,%esi        # &#23558;&#25968;&#25454;&#23492;&#23384;&#22120;&#65288;%edx&#65289;&#30340;&#20540;&#22797;&#21046;&#21040;&#28304;&#32034;&#24341;&#23492;&#23384;&#22120;&#65288;%esi&#65289;&#65292;&#20316;&#20026; sum &#20989;&#25968;&#30340;&#31532;&#20108;&#20010;&#21442;&#25968;&#10;   0x000000000040051f &#60;+30&#62;:    mov    %eax,%edi        # &#23558;&#32047;&#21152;&#23492;&#23384;&#22120;&#65288;%eax&#65289;&#30340;&#20540;&#22797;&#21046;&#21040;&#30446;&#30340;&#32034;&#24341;&#23492;&#23384;&#22120;&#65288;%edi&#65289;&#65292;&#20316;&#20026; sum &#20989;&#25968;&#30340;&#31532;&#19968;&#20010;&#21442;&#25968;&#10;   0x0000000000400521 &#60;+32&#62;:    callq  0x4004ed &#60;sum&#62;   # &#35843;&#29992; sum &#20989;&#25968;&#10;   0x0000000000400526 &#60;+37&#62;:    mov    %eax,-0xc(%rbp)  # &#23558;&#32047;&#21152;&#23492;&#23384;&#22120;&#65288;%eax&#65289;&#30340;&#20540;&#65292;&#21363; sum &#20989;&#25968;&#30340;&#36820;&#22238;&#20540;&#65292;&#23384;&#20837;&#22522;&#22336;&#23492;&#23384;&#22120;&#65288;%rbp&#65289;&#20943; 12 &#30340;&#20301;&#32622;&#65292;&#21363;&#21464;&#37327; c &#30340;&#20869;&#23384;&#20301;&#32622;&#10;&#10;9           return 0;&#10;   0x0000000000400529 &#60;+40&#62;:    mov    $0x0,%eax        # &#23558;&#20540; 0 &#23384;&#20837;&#32047;&#21152;&#23492;&#23384;&#22120;&#65288;%eax&#65289;&#65292;&#20316;&#20026; main &#20989;&#25968;&#30340;&#36820;&#22238;&#20540;&#10;&#10;10      &#125;   0x000000000040052e &#60;+45&#62;:   leaveq          # &#24674;&#22797;&#26087;&#30340;&#26632;&#24103;&#65292;&#31561;&#25928;&#20110;&#21516;&#26102;&#25191;&#34892; &#34;mov %rbp, %rsp&#34; &#21644; &#34;pop %rbp&#34;&#10;   0x000000000040052f &#60;+46&#62;:    retq                    # &#36820;&#22238;&#21040;&#35843;&#29992; main &#20989;&#25968;&#30340;&#20195;&#30721;&#20301;&#32622;</span><br></pre></td></tr></table></figure></p>
<p>Step2. 观察 sum 函数的汇编代码  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/b08658342adec3db9085cd843234fbe3.jpg" alt="image.png"></p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">1       int sum(int l, int r) &#123;&#10;   0x00000000004004ed &#60;+0&#62;:     push   %rbp             # &#23558; %rbp &#23492;&#23384;&#22120;&#20837;&#26632;&#65292;&#20197;&#20445;&#23384;&#20043;&#21069;&#30340;&#22522;&#22320;&#22336;&#10;   0x00000000004004ee &#60;+1&#62;:     mov    %rsp,%rbp        # &#23558;&#26632;&#39030;&#23492;&#23384;&#22120;&#65288;%rsp&#65289;&#30340;&#20540;&#22797;&#21046;&#21040;&#22522;&#22336;&#23492;&#23384;&#22120;&#65288;%rbp&#65289;&#65292;&#21019;&#24314;&#26032;&#30340;&#26632;&#24103;&#10;   0x00000000004004f1 &#60;+4&#62;:     mov    %edi,-0x4(%rbp)  # &#23558;&#31532;&#19968;&#20010;&#21442;&#25968;&#65288;%edi&#65289;&#30340;&#20540;&#23384;&#20837;&#22522;&#22336;&#23492;&#23384;&#22120;&#65288;%rbp&#65289;&#20943; 4 &#30340;&#20301;&#32622;&#65292;&#21363;&#21464;&#37327; l &#30340;&#20869;&#23384;&#20301;&#32622;&#10;   0x00000000004004f4 &#60;+7&#62;:     mov    %esi,-0x8(%rbp)  # &#23558;&#31532;&#20108;&#20010;&#21442;&#25968;&#65288;%esi&#65289;&#30340;&#20540;&#23384;&#20837;&#22522;&#22336;&#23492;&#23384;&#22120;&#65288;%rbp&#65289;&#20943; 8 &#30340;&#20301;&#32622;&#65292;&#21363;&#21464;&#37327; r &#30340;&#20869;&#23384;&#20301;&#32622;&#10;&#10;2           return l+r;&#10;   0x00000000004004f7 &#60;+10&#62;:    mov    -0x8(%rbp),%eax  # &#23558;&#21464;&#37327; r &#30340;&#20540;&#22797;&#21046;&#21040;&#32047;&#21152;&#23492;&#23384;&#22120;&#65288;%eax&#65289;&#10;   0x00000000004004fa &#60;+13&#62;:    mov    -0x4(%rbp),%edx  # &#23558;&#21464;&#37327; l &#30340;&#20540;&#22797;&#21046;&#21040;&#25968;&#25454;&#23492;&#23384;&#22120;&#65288;%edx&#65289;&#10;   0x00000000004004fd &#60;+16&#62;:    add    %edx,%eax        # &#23558;&#25968;&#25454;&#23492;&#23384;&#22120;&#65288;%edx&#65289;&#30340;&#20540;&#21152;&#21040;&#32047;&#21152;&#23492;&#23384;&#22120;&#65288;%eax&#65289;&#65292;&#32467;&#26524;&#23384;&#20837; %eax&#65292;&#21363;&#20989;&#25968;&#36820;&#22238;&#20540;&#10;&#10;3       &#125;&#10;   0x00000000004004ff &#60;+18&#62;:    pop    %rbp             # &#20174;&#22534;&#26632;&#20013;&#24377;&#20986;&#20540;&#21040;&#22522;&#22336;&#23492;&#23384;&#22120;&#65288;%rbp&#65289;&#65292;&#24674;&#22797;&#20043;&#21069;&#30340;&#26632;&#24103;&#10;   0x0000000000400500 &#60;+19&#62;:    retq                    # &#36820;&#22238;&#21040;&#35843;&#29992; sum &#20989;&#25968;&#30340;&#20195;&#30721;&#20301;&#32622;</span><br></pre></td></tr></table></figure>
<p>Step3.  在 main 函数入口打上断点，并开始运行观察栈帧区间  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/cfe8a15f04934e79b485a9fe887a0899.jpg" alt="image.png"></p>
<p>可以看到，%rbp 的地址是 0x7fffffffe0b0，%rsp 的地址是 0x7fffffffe0a0，%rsp 比 %rbp 低 16 位，符合在 step1 中看到的执行步骤  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/f9faebae8de2016dc083a28772d967fb.jpg" alt="image.png"></p>
<p>查看 main 函数执行时 %rbp 地址中的值，发现是 0，说明没有上一次调用函数的 %rbp 地址    </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/d71d3ebb3544a87e6ee61789dabd2c1f.jpg" alt="image.png"></p>
<p>查看局部变量值，都是0  </p>
<p>Step4. 在调用 sum 函数的地方打上断点，查看 main 函数栈帧区间的值和局部变量值  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/8d5611c9aa826c3af1c3065da519d3af.jpg" alt="image.png"></p>
<p>在第 8 行打上断点，继续执行，然后根据 %rbp 偏移定位得到局部变量 a、b 的地址，打印，发现已经有值了  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/85da1ac1b8a996fdda4aa58d6651a89b.jpg" alt="image.png">  </p>
<p>栈帧区间不变  </p>
<p>Step5. 在 sum 函数入口打上断点，查看 sum 函数的栈帧区间的值、返回地址、%rbp的值指向地址的值  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/58c58b7104f225455ffaebf1995d201f.jpg" alt="image.png">  </p>
<p>可以看到，%rbp 和 %rsp 的值发生了变化，且都是 0x7fffffffe090，因为 sum 函数没有局部变量，也不需要预留空间，符合在 Step2 中观察到的 sum 函数执行步骤。  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/34bd4994b0cf685ce0608318dd5d5fee.jpg" alt="image.png">  </p>
<p>查看 %rbp 指向的值，和 Step3 中 main 函数的栈帧 %rbp 指针地址一致，符合预期  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/6f75cba55e00202286129b8c7fdd2451.jpg" alt="image.png">  </p>
<p>根据 1.3 小节中的函数栈帧结构，我们知道 %rbp 地址向上偏移8个字节，就是函数的返回地址，打印出它的值，发现和 Step1 中 main 函数的返回地址能对上  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">0x0000000000400526 &#60;+37&#62;:    mov    %eax,-0xc(%rbp)</span><br></pre></td></tr></table></figure>
<p>Step6. 在 main 函数的结尾打上断点，查看 main 函数栈帧区间的值  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/b657ed6806e537d372a56039c6a8c998.jpg" alt="image.png">  </p>
<p>发现栈帧区间恢复到 main 函数的区间了，符合预期。  </p>
<h1 id="2-__u6709_u6808_u534F_u7A0B"><a href="#2-__u6709_u6808_u534F_u7A0B" class="headerlink" title="2. 有栈协程"></a>2. 有栈协程</h1><p>当CPU执行任务时，切换线程需要执行保存当前线程的上下文（主要是寄存器）、加载新线程的上下文、更新线程控制块（Thread Control Block）、刷新内存管理单元（Memory Management Unit）等操作，而切换函数调用只需要执行一些栈帧的上下文切换工作。  </p>
<p>函数调用的代价主要取决于参数的数量和大小以及栈操作的开销。相比之下，线程切换需要保存和恢复更多的寄存器，更新线程控制块和刷新内存管理单元等，这些操作会导致更高的延迟和资源消耗，通常会比函数执行的切换高出一个数量级。  </p>
<p>有栈协程可以理解为一个用户态下的线程，在用户态下进行函数的上下文切换。和线程不同的是：线程是抢占式执行，当发生系统调用或者中断的时候，交由内核调度执行；而协程是通过 yield 在用户态主动让出 cpu 所有权，切换到其他协程执行。  </p>
<h2 id="2-1__u6781_u7B80_u6709_u6808_u534F_u7A0B_u5B9E_u73B0"><a href="#2-1__u6781_u7B80_u6709_u6808_u534F_u7A0B_u5B9E_u73B0" class="headerlink" title="2.1 极简有栈协程实现"></a>2.1 极简有栈协程实现</h2><p>c 标准库提供了一套方法用于操作用户态的上下文栈<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">#include &lt;ucontext.h&gt;</span><br><span class="line">int getcontext(ucontext_t *ucp);  // 获取</span><br><span class="line">int setcontext(const ucontext_t *ucp); // 设置</span><br><span class="line">void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); // 修改指向</span><br><span class="line">int swapcontext(ucontext_t *restrict oucp,</span><br><span class="line">                       const ucontext_t *restrict ucp);  // 切换上下文</span><br></pre></td></tr></table></figure></p>
<p>基于这些方法，我们可以实现一个极简版的协程示例<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;ucontext.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建协程上下文</span></span><br><span class="line"><span class="keyword">ucontext_t</span> main_ctx, co1_ctx, co2_ctx;</span><br><span class="line"><span class="comment">// 创建协程栈</span></span><br><span class="line"><span class="keyword">char</span> co1_stack[<span class="number">1024</span>*<span class="number">128</span>], co2_stack[<span class="number">1024</span>*<span class="number">128</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 协程1：打印偶数</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">co1</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; <span class="number">10</span>; i+=<span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"co1: %d\n"</span>, i);</span><br><span class="line">        <span class="comment">// 切换到协程2</span></span><br><span class="line">        swapcontext(&amp;co1_ctx, &amp;co2_ctx);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 打印完偶数后切换回主协程</span></span><br><span class="line">    swapcontext(&amp;co1_ctx, &amp;main_ctx);</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="function"><span class="keyword">void</span> <span class="title">co2</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i &lt; <span class="number">10</span>; i+=<span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"co2: %d\n"</span>, i);</span><br><span class="line">        <span class="comment">// 切换到协程1</span></span><br><span class="line">        swapcontext(&amp;co2_ctx, &amp;co1_ctx);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 打印完奇数后切换回主协程</span></span><br><span class="line">    swapcontext(&amp;co2_ctx, &amp;main_ctx);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 初始化协程1上下文并切换到协程1</span></span><br><span class="line">    getcontext(&amp;co1_ctx);</span><br><span class="line">    co1_ctx.uc_stack.ss_sp = co1_stack;</span><br><span class="line">    co1_ctx.uc_stack.ss_size = <span class="keyword">sizeof</span>(co1_stack);</span><br><span class="line">    co1_ctx.uc_link = &amp;main_ctx;</span><br><span class="line">    makecontext(&amp;co1_ctx, co1, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 初始化协程2上下文并切换到协程2</span></span><br><span class="line">    getcontext(&amp;co2_ctx);</span><br><span class="line">    co2_ctx.uc_stack.ss_sp = co2_stack;</span><br><span class="line">    co2_ctx.uc_stack.ss_size = <span class="keyword">sizeof</span>(co2_stack);</span><br><span class="line">    co2_ctx.uc_link = &amp;main_ctx;</span><br><span class="line">    makecontext(&amp;co2_ctx, co2, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"start coroutine\n"</span>);</span><br><span class="line">    swapcontext(&amp;main_ctx, &amp;co1_ctx);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"stop coroutine\n"</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>执行结果：  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/f0b2231aaa1fe09c60ba7060fd5b16c4.jpg" alt="image.png">  </p>
<p>这样就实现了最简单的协程功能。</p>
<h2 id="2-2__u57FA_u4E8E_u6C47_u7F16_u6709_u6808_u534F_u7A0B_u5B9E_u73B0"><a href="#2-2__u57FA_u4E8E_u6C47_u7F16_u6709_u6808_u534F_u7A0B_u5B9E_u73B0" class="headerlink" title="2.2 基于汇编有栈协程实现"></a>2.2 基于汇编有栈协程实现</h2><p>这里给出一个最简单的基于汇编替代 ucontext 来实现协程功能的示例，无法真正用在项目中，但是可以演示出有栈协程的基本原理了。实际业界的 libco 等库也会自行基于汇编实现替代 ucontext 的部分功能，已获取更好的性能表现。<br>首先定义三个方法，用来保存、恢复、切换协程上下文 coctx_op.h<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">ifndef</span> _COCTX_OP_H </span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> _COCTX_OP_H</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义各种字节大小的类型</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">unsigned</span> <span class="keyword">char</span>      <span class="keyword">byte1_t</span>; </span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">unsigned</span> <span class="keyword">short</span>     <span class="keyword">byte2_t</span>;</span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">unsigned</span> <span class="keyword">int</span>       <span class="keyword">byte4_t</span>;</span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">unsigned</span> <span class="keyword">long</span> <span class="keyword">long</span> <span class="keyword">byte8_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义协程上下文结构体</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">struct</span> &#123;</span><br><span class="line">    <span class="keyword">byte8_t</span> retaddr;       <span class="comment">// 返回地址</span></span><br><span class="line">    <span class="keyword">byte8_t</span> registers[<span class="number">7</span>];  <span class="comment">// 寄存器</span></span><br><span class="line">    <span class="keyword">byte4_t</span> mxcsr;         <span class="comment">// 浮点状态寄存器</span></span><br><span class="line">    <span class="keyword">byte2_t</span> x87cw;         <span class="comment">// 控制字</span></span><br><span class="line">    <span class="keyword">byte2_t</span> padding;       <span class="comment">// 不使用，仅用于填充大小到8字节倍数</span></span><br><span class="line">&#125; <span class="keyword">coctx_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 声明协程保存、恢复和切换函数</span></span><br><span class="line"><span class="function"><span class="keyword">extern</span> <span class="keyword">int</span> <span class="title">coctx_save</span><span class="params">(coctx_t *old_ctx)</span></span>;     <span class="comment">// 保存协程上下文</span></span><br><span class="line"><span class="function"><span class="keyword">extern</span> <span class="keyword">int</span> <span class="title">coctx_resume</span><span class="params">(coctx_t *new_ctx)</span></span>;   <span class="comment">// 恢复协程上下文</span></span><br><span class="line"><span class="function"><span class="keyword">extern</span> <span class="keyword">int</span> <span class="title">coctx_swap</span><span class="params">(coctx_t *old_ctx, coctx_t *new_ctx)</span></span>;  <span class="comment">// 切换协程上下文</span></span><br><span class="line"></span><br><span class="line"><span class="preprocessor">#<span class="keyword">endif</span> <span class="comment">/* coctx_op.h  */</span></span></span><br></pre></td></tr></table></figure></p>
<p>基于汇编实现上面的三个方法 coctx_op.S<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">define</span> _retaddr <span class="number">0</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> _rsp <span class="number">8</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> _rbp <span class="number">16</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> _rbx <span class="number">24</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> _r12 <span class="number">32</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> _r13 <span class="number">40</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> _r14 <span class="number">48</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> _r15 <span class="number">56</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> _mxcsr <span class="number">64</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">define</span> _x87 <span class="number">68</span></span></span><br><span class="line"></span><br><span class="line">    .text  <span class="comment">// 指定代码段</span></span><br><span class="line">    .globl coctx_save  <span class="comment">// 定义全局符号 coctx_save</span></span><br><span class="line">    .type  coctx_save, @function  <span class="comment">// 指定 coctx_save 类型为函数</span></span><br><span class="line">coctx_save:  <span class="comment">// coctx_save 函数实现</span></span><br><span class="line">    <span class="comment">// 保存返回地址</span></span><br><span class="line">    movq (%rsp), %r11</span><br><span class="line">    movq %r11, _retaddr(%rdi)</span><br><span class="line">    <span class="comment">// 保存栈指针</span></span><br><span class="line">    leaq <span class="number">8</span>(%rsp), %r11</span><br><span class="line">    movq %r11, _rsp(%rdi)</span><br><span class="line">    <span class="comment">// 保存寄存器</span></span><br><span class="line">    movq %rbp, _rbp(%rdi)</span><br><span class="line">    movq %rbx, _rbx(%rdi)</span><br><span class="line">    movq %r12, _r12(%rdi)</span><br><span class="line">    movq %r13, _r13(%rdi)</span><br><span class="line">    movq %r14, _r14(%rdi)</span><br><span class="line">    movq %r15, _r15(%rdi)</span><br><span class="line">    <span class="comment">// 保存浮点状态</span></span><br><span class="line">    stmxcsr _mxcsr(%rdi)</span><br><span class="line">    fnstcw  _x87(%rdi)</span><br><span class="line">    <span class="comment">// 返回 0</span></span><br><span class="line">    movl $<span class="number">0</span>, %eax</span><br><span class="line">    <span class="comment">// 返回</span></span><br><span class="line">    retq</span><br><span class="line">    .size   coctx_save, .-coctx_save</span><br><span class="line"></span><br><span class="line">    .globl coctx_resume  <span class="comment">// 定义全局符号 coctx_resume</span></span><br><span class="line">    .type  coctx_resume, @function  <span class="comment">// 指定 coctx_resume 类型为函数</span></span><br><span class="line">coctx_resume:  <span class="comment">// coctx_resume 函数实现</span></span><br><span class="line">    <span class="comment">// 恢复栈指针和返回地址</span></span><br><span class="line">    movq _rsp(%rdi), %rsp</span><br><span class="line">    movq _retaddr(%rdi), %r11</span><br><span class="line">    pushq %r11</span><br><span class="line">    <span class="comment">// 恢复寄存器</span></span><br><span class="line">    movq _rbp(%rdi), %rbp</span><br><span class="line">    movq _rbx(%rdi), %rbx</span><br><span class="line">    movq _r12(%rdi), %r12</span><br><span class="line">    movq _r13(%rdi), %r13</span><br><span class="line">    movq _r14(%rdi), %r14</span><br><span class="line">    movq _r15(%rdi), %r15</span><br><span class="line">    <span class="comment">// 恢复浮点状态</span></span><br><span class="line">    ldmxcsr _mxcsr(%rdi)</span><br><span class="line">    fldcw  _x87(%rdi)</span><br><span class="line">    <span class="comment">// 返回 0</span></span><br><span class="line">    movl $<span class="number">0</span>, %eax</span><br><span class="line">    <span class="comment">// 返回</span></span><br><span class="line">    retq</span><br><span class="line">    .size   coctx_resume, .-coctx_resume</span><br><span class="line"></span><br><span class="line">    .globl coctx_swap  <span class="comment">// 定义全局符号 coctx_swap</span></span><br><span class="line">    .type  coctx_swap, @function  <span class="comment">// 指定 coctx_swap 类型为函数</span></span><br><span class="line">coctx_swap:  <span class="comment">// coctx_swap 函数实现</span></span><br><span class="line">    <span class="comment">// 保存返回地址、栈指针和寄存器</span></span><br><span class="line">    movq (%rsp), %r11</span><br><span class="line">    movq %r11, _retaddr(%rdi)</span><br><span class="line">    leaq <span class="number">8</span>(%rsp), %r11</span><br><span class="line">    movq %r11, _rsp(%rdi)</span><br><span class="line">    movq %rbp, _rbp(%rdi)</span><br><span class="line">    movq %rbx, _rbx(%rdi)</span><br><span class="line">    movq %r12, _r12(%rdi)</span><br><span class="line">    movq %r13, _r13(%rdi)</span><br><span class="line">    movq %r14, _r14(%rdi)</span><br><span class="line">    movq %r15, _r15(%rdi)</span><br><span class="line">    <span class="comment">// 保存浮点状态</span></span><br><span class="line">    stmxcsr _mxcsr(%rdi)</span><br><span class="line">    fnstcw  _x87(%rdi)</span><br><span class="line">    <span class="comment">// 恢复栈指针、返回地址和寄存器</span></span><br><span class="line">    movq _rsp(%rsi), %rsp</span><br><span class="line">    movq _retaddr(%rsi), %r11</span><br><span class="line">    pushq %r11</span><br><span class="line">    movq _rbp(%rsi), %rbp</span><br><span class="line">    movq _rbx(%rsi), %rbx</span><br><span class="line">    movq _r12(%rsi), %r12</span><br></pre></td></tr></table></figure></p>
<p>使用的示例代码<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">"coctx_op.h"</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建协程上下文</span></span><br><span class="line"><span class="keyword">coctx_t</span> main_ctx, co1_ctx, co2_ctx;</span><br><span class="line"><span class="comment">// 创建协程栈</span></span><br><span class="line"><span class="keyword">char</span> co1_stack[<span class="number">1024</span>*<span class="number">128</span>], co2_stack[<span class="number">1024</span>*<span class="number">128</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 协程1：打印偶数</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">co1</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; <span class="number">10</span>; i+=<span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"co1: %d\n"</span>, i);</span><br><span class="line">        <span class="comment">// 切换到协程2</span></span><br><span class="line">        coctx_swap(&amp;co1_ctx, &amp;co2_ctx);</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="function"><span class="keyword">void</span> <span class="title">co2</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i &lt; <span class="number">10</span>; i+=<span class="number">2</span>) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"co2: %d\n"</span>, i);</span><br><span class="line">        <span class="comment">// 切换到协程1</span></span><br><span class="line">        coctx_swap(&amp;co2_ctx, &amp;co1_ctx);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 初始化协程1上下文并切换到协程1</span></span><br><span class="line">    co1_ctx.registers[<span class="number">0</span>] = (<span class="keyword">byte8_t</span>)(co1_stack + <span class="keyword">sizeof</span>(co1_stack));</span><br><span class="line">    co1_ctx.retaddr = (<span class="keyword">byte8_t</span>)co1;</span><br><span class="line">    <span class="comment">// 初始化协程2上下文并切换到协程2</span></span><br><span class="line">    co2_ctx.registers[<span class="number">0</span>] = (<span class="keyword">byte8_t</span>)(co2_stack + <span class="keyword">sizeof</span>(co2_stack));</span><br><span class="line">    co2_ctx.retaddr = (<span class="keyword">byte8_t</span>)co2;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"start coroutine\n"</span>);</span><br><span class="line">    <span class="comment">// 保存主协程上下文</span></span><br><span class="line">    coctx_save(&amp;main_ctx);</span><br><span class="line">    <span class="comment">// 启动协程</span></span><br><span class="line">    coctx_resume(&amp;co1_ctx);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"stop coroutine\n"</span>);</span><br><span class="line">    <span class="comment">// 切回主协程上下文</span></span><br><span class="line">    coctx_swap(&amp;co1_ctx, &amp;main_ctx);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">cmake_minimum_required</span>(VERSION <span class="number">3.10</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">project</span>(coctx_example C)</span><br><span class="line"></span><br><span class="line"><span class="keyword">set</span>(CMAKE_C_STANDARD <span class="number">11</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">add_executable</span>(coctx_example main.c)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 编译 coctx_op.S 文件</span></span><br><span class="line"><span class="keyword">enable_language</span>(ASM)</span><br><span class="line"><span class="keyword">set_source_files_properties</span>(coctx_op.S PROPERTIES LANGUAGE ASM)</span><br><span class="line"><span class="keyword">add_library</span>(coctx_op STATIC coctx_op.S)</span><br><span class="line"></span><br><span class="line"><span class="keyword">target_link_libraries</span>(coctx_example coctx_op)</span><br></pre></td></tr></table></figure>
<p>编译执行结果  </p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/a056ddf02b72d922890bb0e1e8fd3eaf.jpg" alt="image.png"></p>
<h2 id="2-3__u534F_u7A0B_u5E93_u5B9E_u73B0"><a href="#2-3__u534F_u7A0B_u5E93_u5B9E_u73B0" class="headerlink" title="2.3 协程库实现"></a>2.3 协程库实现</h2><p>基于前面极简版的实现，我们可以稍微做一点封装，实现协程的调度器，以及创建、切换、恢复、销毁等方法。<br>coroutine.h<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;ucontext.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 前向声明</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">struct</span> coroutine <span class="keyword">coroutine_t</span>;</span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">struct</span> scheduler <span class="keyword">scheduler_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 协程结构体</span></span><br><span class="line"><span class="keyword">struct</span> coroutine &#123;</span><br><span class="line">    <span class="keyword">ucontext_t</span> ctx;          <span class="comment">// 协程上下文</span></span><br><span class="line">    <span class="keyword">void</span> (*func)(<span class="keyword">void</span> *);    <span class="comment">// 协程函数</span></span><br><span class="line">    <span class="keyword">void</span> *arg;               <span class="comment">// 协程函数参数</span></span><br><span class="line">    <span class="keyword">int</span> is_done;             <span class="comment">// 协程是否已完成</span></span><br><span class="line">    <span class="keyword">scheduler_t</span> *sched;      <span class="comment">// 指向调度器的指针</span></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">struct</span> scheduler &#123;</span><br><span class="line">    <span class="keyword">coroutine_t</span> *current;    <span class="comment">// 当前运行的协程</span></span><br><span class="line">    <span class="keyword">ucontext_t</span> main_ctx;     <span class="comment">// 主函数上下文，用于在协程间切换</span></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">coroutine_t</span> *coroutine_create(<span class="keyword">scheduler_t</span> *sched, <span class="keyword">void</span> (*func)(<span class="keyword">void</span> *), <span class="keyword">void</span> *arg) &#123;</span><br><span class="line">    <span class="keyword">coroutine_t</span> *co = (<span class="keyword">coroutine_t</span> *)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="keyword">coroutine_t</span>));</span><br><span class="line">    getcontext(&amp;co-&gt;ctx);</span><br><span class="line">    co-&gt;ctx.uc_stack.ss_sp = <span class="built_in">malloc</span>(<span class="number">1024</span> * <span class="number">1024</span>); <span class="comment">// 分配栈空间</span></span><br><span class="line">    co-&gt;ctx.uc_stack.ss_size = <span class="number">1024</span> * <span class="number">1024</span>;</span><br><span class="line">    co-&gt;ctx.uc_link = &amp;sched-&gt;main_ctx;</span><br><span class="line">    co-&gt;func = func;</span><br><span class="line">    co-&gt;arg = arg;</span><br><span class="line">    co-&gt;is_done = <span class="number">0</span>;</span><br><span class="line">    co-&gt;sched = sched;</span><br><span class="line">    makecontext(&amp;co-&gt;ctx, (<span class="keyword">void</span> (*)(<span class="keyword">void</span>))func, <span class="number">1</span>, co);</span><br><span class="line">    <span class="keyword">return</span> co;</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="function"><span class="keyword">void</span> <span class="title">coroutine_destroy</span><span class="params">(coroutine_t *co)</span> </span>&#123;</span><br><span class="line">    <span class="built_in">free</span>(co-&gt;ctx.uc_stack.ss_sp);</span><br><span class="line">    <span class="built_in">free</span>(co);</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="function"><span class="keyword">void</span> <span class="title">coroutine_yield</span><span class="params">(coroutine_t *co)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (co-&gt;sched-&gt;current != co) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Error: not the current coroutine.\n"</span>);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    swapcontext(&amp;co-&gt;sched-&gt;current-&gt;ctx, &amp;co-&gt;sched-&gt;main_ctx);</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="function"><span class="keyword">void</span> <span class="title">coroutine_resume</span><span class="params">(coroutine_t *co)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (co-&gt;is_done) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Coroutine is already done.\n"</span>);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    co-&gt;sched-&gt;current = co;</span><br><span class="line">    swapcontext(&amp;co-&gt;sched-&gt;main_ctx, &amp;co-&gt;ctx);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>调用示例 main.c<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">"coroutine.h"</span></span></span><br><span class="line"><span class="comment">// 协程函数示例</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">my_coroutine</span><span class="params">(<span class="keyword">void</span> *arg)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">coroutine_t</span> *co = (<span class="keyword">coroutine_t</span> *)arg;</span><br><span class="line">    <span class="keyword">int</span> *n = (<span class="keyword">int</span> *)co-&gt;arg;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; *n; i++) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">"Coroutine: %d\n"</span>, i);</span><br><span class="line">        coroutine_yield(co);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">scheduler_t</span> sched = &#123;<span class="number">0</span>&#125;;</span><br><span class="line">    <span class="keyword">int</span> n = <span class="number">5</span>;</span><br><span class="line">    <span class="keyword">coroutine_t</span> *co1 = coroutine_create(&amp;sched, my_coroutine, &amp;n);</span><br><span class="line">    <span class="keyword">coroutine_t</span> *co2 = coroutine_create(&amp;sched, my_coroutine, &amp;n);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; n; i++) &#123;</span><br><span class="line">        coroutine_resume(co1);</span><br><span class="line">        coroutine_resume(co2);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    coroutine_destroy(co1);</span><br><span class="line">    coroutine_destroy(co2);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p><img src="https://zoucz.com/blogimgs/2b247f70-d6aa-11ed-9fa0-5dbc93f9d3ee.md/04b057a6978d7851622ad5f72f42dbc1.jpg" alt="image.png"></p>
<h2 id="2-4__u914D_u5408_u4E8B_u4EF6_u5E93/_u591A_u8DEF_u590D_u7528_u5E93_u5B9E_u73B0_u540C_u6B65_u8BED_u6CD5"><a href="#2-4__u914D_u5408_u4E8B_u4EF6_u5E93/_u591A_u8DEF_u590D_u7528_u5E93_u5B9E_u73B0_u540C_u6B65_u8BED_u6CD5" class="headerlink" title="2.4 配合事件库/多路复用库实现同步语法"></a>2.4 配合事件库/多路复用库实现同步语法</h2><p>基于事件库或者 epoll 等多路复用库操作 IO 时，对于异步执行的结果，一般是通过回调函数来执行获取结果和后续操作</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;event2/event.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">on_timeout</span><span class="params">(evutil_socket_t fd, <span class="keyword">short</span> event, <span class="keyword">void</span> *arg)</span> </span>&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"do someting after io success\n"</span>);</span><br><span class="line">    <span class="keyword">struct</span> event_base *base = (<span class="keyword">struct</span> event_base *)arg;</span><br><span class="line">    event_base_loopexit(base, <span class="literal">NULL</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">wait_for_io</span><span class="params">(<span class="keyword">struct</span> event_base *base)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">struct</span> timeval one_sec = &#123;<span class="number">1</span>, <span class="number">0</span>&#125;;</span><br><span class="line">    <span class="comment">// 通过回调实现，将后续工作放到 on_timeout 回调函数中执行</span></span><br><span class="line">    <span class="keyword">struct</span> event *ev = evtimer_new(base, on_timeout, base);</span><br><span class="line">    evtimer_add(ev, &amp;one_sec);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">struct</span> event_base *base = event_base_new();</span><br><span class="line">    wait_for_io(base);</span><br><span class="line">    event_base_dispatch(base);</span><br><span class="line">    event_base_free(base);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>回调函数的写法会使功能逻辑分散，且回调多了之后代码比较难维护。我们可以借助前面封装的协程库，配合事件库 / epoll 一起，实现一个协程  IO 调度功能，以同步的语法写 IO 结果获取和后续操作。  </p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;event2/event.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"><span class="preprocessor">#<span class="keyword">include</span> <span class="string">"coroutine.h"</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">on_timeout</span><span class="params">(evutil_socket_t fd, <span class="keyword">short</span> event, <span class="keyword">void</span> *arg)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">coroutine_t</span> *co = (<span class="keyword">coroutine_t</span> *)arg;</span><br><span class="line">    coroutine_resume(co);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">wait_for_io</span><span class="params">(<span class="keyword">void</span> *arg)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">coroutine_t</span> *co = (<span class="keyword">coroutine_t</span> *)arg;</span><br><span class="line">    <span class="keyword">struct</span> event_base *base = (event_base*)co-&gt;arg;</span><br><span class="line">    <span class="keyword">struct</span> timeval one_sec = &#123;<span class="number">1</span>, <span class="number">0</span>&#125;;</span><br><span class="line">    <span class="keyword">struct</span> event *ev = evtimer_new(base, on_timeout, co);</span><br><span class="line">    evtimer_add(ev, &amp;one_sec);</span><br><span class="line">    coroutine_yield(co);</span><br><span class="line">    <span class="comment">// 后续操作直接在原函数中执行，回调中仅做协程上下文切回操作</span></span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"do someting after io success\n"</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">struct</span> event_base *base = event_base_new();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">scheduler_t</span> sched = &#123;<span class="number">0</span>&#125;;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">coroutine_t</span> *co1 = coroutine_create(&amp;sched, wait_for_io, base);</span><br><span class="line">    coroutine_resume(co1);</span><br><span class="line"></span><br><span class="line">    event_base_dispatch(base);</span><br><span class="line">    event_base_free(base);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>实际上 libco 内部也实现了类似的机制，不过 libco 中是基于 epoll 自行封装的事件模块。  </p>
<h2 id="2-5__u5E38_u89C1_u7684_c++__u534F_u7A0B_u5E93"><a href="#2-5__u5E38_u89C1_u7684_c++__u534F_u7A0B_u5E93" class="headerlink" title="2.5 常见的 c++ 协程库"></a>2.5 常见的 c++ 协程库</h2><p>C++ 协程库有以下几个常见的库，它们的实现方案也有所不同：  </p>
<ul>
<li>Boost.Coroutine2：Boost.Coroutine2 是 Boost C++ 库中的一个协程库。它基于底层的上下文切换库 Boost.Context 实现，提供了协程的创建、切换和销毁等功能。Boost.Coroutine2 使用栈式协程，可以在函数调用过程中进行协程切换。它的接口设计和实现都遵循了 C++ 标准中的协程规范。</li>
<li>Libaco：Libaco 是一个轻量级的 C++ 协程库，它基于汇编实现了协程的上下文切换。Libaco 支持多种平台，并提供了简洁的 API。它使用共享栈式协程，可以在多个协程之间共享栈空间，从而减少内存开销。</li>
<li>Libtask：Libtask 是一个基于 C 语言实现的协程库，但可以在 C++ 项目中使用。它基于 setjmp/longjmp 实现了协程的上下文切换，并提供了协程调度器和同步原语等功能。Libtask 使用栈式协程，支持协程之间的同步和通信。</li>
<li>Gorilla：Gorilla 是一个基于 C++11 实现的协程库，它使用 ucontext API 实现了协程的上下文切换。Gorilla 提供了基于事件的异步 I/O 模型，支持协程之间的同步和通信。它使用栈式协程，支持多个协程并发执行。</li>
<li>Protothreads：Protothreads 是一个非常轻量级的协程库，它基于 C 语言宏实现了协程的创建和切换。Protothreads 使用线程式协程，通过编译器技巧将协程切换嵌入到函数调用过程中。Protothreads 适用于资源受限的嵌入式系统。</li>
</ul>
<p>这些协程库的实现方案主要有以下几种：  </p>
<ul>
<li>基于底层上下文切换库（如 Boost.Context）实现。</li>
<li>基于汇编实现协程的上下文切换。</li>
<li>基于 setjmp/longjmp 实现协程的上下文切换。</li>
<li>基于 ucontext API 实现协程的上下文切换。</li>
<li>基于编译器技巧（如 C 语言宏）实现协程的创建和切换。</li>
</ul>
<h1 id="3-__u65E0_u6808_u534F_u7A0B"><a href="#3-__u65E0_u6808_u534F_u7A0B" class="headerlink" title="3. 无栈协程"></a>3. 无栈协程</h1><h2 id="3-1__u548C_u6709_u6808_u534F_u7A0B_u7684_u533A_u522B"><a href="#3-1__u548C_u6709_u6808_u534F_u7A0B_u7684_u533A_u522B" class="headerlink" title="3.1 和有栈协程的区别"></a>3.1 和有栈协程的区别</h2><p>无栈协程和有栈协程的主要区别在于它们在协程切换时是否需要保存和恢复栈上下文。  </p>
<p>无栈协程：</p>
<ul>
<li>定义：无栈协程是一种不依赖于栈进行上下文切换的协程实现方式。在无栈协程中，协程之间的切换不会涉及到栈的保存和恢复，而是通过其他方式（如闭包、编译器技巧、内部数据结构等）来保留协程的状态和局部变量。</li>
<li>内存开销：由于无栈协程不使用栈进行切换，因此每个协程的内存开销较小。这使得无栈协程在资源受限的环境中，如嵌入式系统，具有较好的适应性。</li>
<li>切换开销：无栈协程的上下文切换不涉及栈的保存和恢复，因此切换开销相对较低。这使得无栈协程在需要频繁切换的场景下具有较好的性能。</li>
<li>编程模型：由于无栈协程的实现通常需要使用编译器技巧，如 C 语言宏或特殊的控制结构，因此无栈协程的使用和编程模型相对复杂。这可能导致无栈协程在实际开发中的应用受到限制。</li>
</ul>
<p>有栈协程：</p>
<ul>
<li>定义：有栈协程是一种依赖于栈进行上下文切换的协程实现方式。在有栈协程中，协程之间的切换涉及到栈的保存和恢复。每个协程都有自己独立的栈空间，用于保存局部变量和执行状态。</li>
<li>内存开销：由于有栈协程使用栈进行切换，因此每个协程的内存开销较大。这使得有栈协程在资源受限的环境中可能不太适用。</li>
<li>切换开销：有栈协程的上下文切换涉及栈的保存和恢复，因此切换开销相对较高。这使得有栈协程在需要频繁切换的场景下可能性能较差。</li>
<li>编程模型：有栈协程的编程模型相对简单，它允许在协程中直接使用函数调用、循环和其他控制结构，而无需使用复杂的回调函数或特殊语法。这使得有栈协程在实际开发中的应用更加广泛。</li>
</ul>
<h2 id="3-2__u5E38_u89C1_u7684_u65E0_u6808_u534F_u7A0B"><a href="#3-2__u5E38_u89C1_u7684_u65E0_u6808_u534F_u7A0B" class="headerlink" title="3.2 常见的无栈协程"></a>3.2 常见的无栈协程</h2><p>以下是一些常见的无栈协程及其实现方案：  </p>
<ul>
<li>Protothreads：Protothreads 是一个非常轻量级的协程库，主要用于资源受限的嵌入式系统。Protothreads 使用 C 语言宏实现协程的创建和切换，它通过将协程的状态保存在局部变量中，而不是在栈中，从而实现无栈协程。</li>
<li>Lua 协程：Lua 语言中的协程是无栈协程，它通过将协程的状态保存在 Lua 虚拟机的内部数据结构中，而不是在栈中，从而实现无栈协程。Lua 协程提供了 yield 和 resume 函数，用于在协程之间进行切换。</li>
<li>JavaScript 的 async/await：JavaScript 的 async/await 是一种特殊的无栈协程。它通过 Promise 对象和事件循环机制，实现了在异步操作中使用同步编程风格的能力。在 async/await 中，协程的状态被保存在 Promise 对象和闭包中，而不是在栈中。</li>
<li>C# 的 async/await：C# 的 async/await 也是一种特殊的无栈协程。它通过 Task 对象和编译器转换，实现了在异步操作中使用同步编程风格的能力。在 C# 的 async/await 中，协程的状态被保存在 Task 对象和编译器生成的状态机中，而不是在栈中。</li>
<li>Python 的 asyncio：Python 的 asyncio 库提供了一种基于事件循环的无栈协程。在 asyncio 中，协程是通过生成器函数实现的，协程的状态被保存在生成器对象中，而不是在栈中。asyncio 提供了 yield from 和 await 语法，用于在协程之间进行切换。</li>
<li>C++20 的协程：在 C++20 的协程中，协程的状态和局部变量可以被存储在堆上分配的内存中，从而减少栈的使用。这种优化取决于编译器和协程库的实现。C++20 提供了 co_await，co_yield 和 co_return 等关键字，用于创建和控制协程。</li>
</ul>
<p>这些无栈协程的实现方案主要有以下几种：  </p>
<ul>
<li>使用编译器技巧，如 C 语言宏或特殊的控制结构，实现协程的创建和切换。</li>
<li>将协程的状态保存在内部数据结构或对象中，如 Lua 虚拟机的内部数据结构，Promise 对象，Task 对象，生成器对象等。</li>
<li>使用事件循环和回调机制，实现协程的调度和切换。</li>
</ul>
<p>参考文档：</p>
<ul>
<li>《程序员的自我修养》</li>
<li><a href="https://yangsoon.github.io/有栈协程实现原理" target="_blank" rel="external">https://yangsoon.github.io/有栈协程实现原理</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/347445164" target="_blank" rel="external">https://zhuanlan.zhihu.com/p/347445164</a> 有栈协程和无栈协程</li>
<li><a href="https://cloud.tencent.com/developer/article/1888257" target="_blank" rel="external">https://cloud.tencent.com/developer/article/1888257</a></li>
</ul>
]]></content>
    <summary type="html">
    <![CDATA[<h1 id="1-__u7A0B_u5E8F_u8FDB_u7A0B_u5185_u5B58_u5E03_u5C40"><a href="#1-__u7A0B_u5E8F_u8FDB_u7A0B_u5185_u5B58_u5E03_u5C40" class="headerlin]]>
    </summary>
    
      <category term="协程" scheme="https://www.zoucz.com/blog/tags/%E5%8D%8F%E7%A8%8B/"/>
    
      <category term="性能" scheme="https://www.zoucz.com/blog/tags/%E6%80%A7%E8%83%BD/"/>
    
      <category term="后台开发" scheme="https://www.zoucz.com/blog/categories/%E5%90%8E%E5%8F%B0%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[unity android 子线程中调用JNI导致的 crash]]></title>
    <link href="https://www.zoucz.com/blog/2023/03/23/93292fb0-c950-11ed-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/03/23/93292fb0-c950-11ed-9fa0-5dbc93f9d3ee/</id>
    <published>2023-03-23T07:59:02.000Z</published>
    <updated>2023-03-23T08:01:23.000Z</updated>
    <content type="html"><![CDATA[<p>这是一个偶现的 crash。<br>某次版本更新后，本地平板、手机测试均正常，车机上运行也正常，但是后续实车偶现 crash 的问题上报，后来测试的稳定性测试中复现了 crash 问题，并提供了日志。  </p>
<h2 id="u5206_u6790_u65E5_u5FD7"><a href="#u5206_u6790_u65E5_u5FD7" class="headerlink" title="分析日志"></a>分析日志</h2><p><img src="https://zoucz.com/blogimgs/93292fb0-c950-11ed-9fa0-5dbc93f9d3ee.md/1658858fb54632341d39047110437404.jpg" alt="image.png">  </p>
<p>拿到日志，查看 crash 堆栈，发现调用路径是从 libil2cpp.so 到 libunity.so 的。<br>看来这次的问题比较好找 —— 大部分调用路径都是 libil2cpp.so 中的业务逻辑。  </p>
<h2 id="u8FD8_u539F_libunity-so__u4E2D_u7684_u7B26_u53F7"><a href="#u8FD8_u539F_libunity-so__u4E2D_u7684_u7B26_u53F7" class="headerlink" title="还原 libunity.so 中的符号"></a>还原 libunity.so 中的符号</h2><p>虽然业务库中的信息更多，但是最终 crash 点位还是在 libunity.so 中，所以先看这里的问题。<br> libunity.so 的符号可以在 unity 导出 Android Library 项目时勾选导出符号信息生成，然后用 ndk 的 addr2line 工具还原堆栈地址的符号：  </p>
<p><img src="https://zoucz.com/blogimgs/93292fb0-c950-11ed-9fa0-5dbc93f9d3ee.md/d4e333a9cee1678e797fcf1ed3232bac.jpg" alt="image.png"></p>
<p>可以看到，是  JNI 在创建一个新的全局引用时 crash 的。 这个是 unity 引擎内部的逻辑了，作为业务开发层自然是一脸懵逼。  </p>
<p>拿 unity  NewGlobalRef google 搜索一下，发现了这个官方论坛的帖子：  <a href="https://forum.unity.com/threads/fatal-exception-at-unityengine-androidjni-newglobalref.1178776/" target="_blank" rel="external">https://forum.unity.com/threads/fatal-exception-at-unityengine-androidjni-newglobalref.1178776/</a>。  </p>
<p>里边有人提到：“I found what was causing the crash, you cannot create a AndroidJavaObject outside the Unity MainThread.”。  </p>
<h2 id="u8FD8_u539F_libil2cpp__u7684_u7B26_u53F7"><a href="#u8FD8_u539F_libil2cpp__u7684_u7B26_u53F7" class="headerlink" title="还原 libil2cpp 的符号"></a>还原 libil2cpp 的符号</h2><p>接下来分析是什么调用路径导致的 crash。<br>libil2cpp.so 的符号可以在 build aar 库的项目根目录下的 symbols 目录下找到。  </p>
<p><img src="https://zoucz.com/blogimgs/93292fb0-c950-11ed-9fa0-5dbc93f9d3ee.md/dc030a5443d0cda1abf59d47075f8450.jpg" alt="image.png"></p>
<p>看到这里，问题就比较清晰了， 是在线程中加载缓存数据时，触发了动画开启回调通知，此时会通过 JNI 通知给 Android，和官方论坛中的说法一致。  </p>
<p>修改此逻辑，问题修复。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>这是一个偶现的 crash。<br>某次版本更新后，本地平板、手机测试均正常，车机上运行也正常，但是后续实车偶现 crash 的问题上报，后来测试的稳定性测试中复现了 crash 问题，并提供了日志。  </p>
<h2 id="u5206_u6790_u65E5_u5FD]]>
    </summary>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/tags/unity3d/"/>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/categories/unity3d/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[unity 项目导出到 android 平台以 library 的方式运行]]></title>
    <link href="https://www.zoucz.com/blog/2023/03/02/d2516850-b90c-11ed-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/03/02/d2516850-b90c-11ed-9fa0-5dbc93f9d3ee/</id>
    <published>2023-03-02T15:13:43.000Z</published>
    <updated>2023-03-03T03:10:33.000Z</updated>
    <content type="html"><![CDATA[<p>在有的应用场景下，需要把 unity 项目导出作为 lib 运行在终端平台上，之前也零碎记了一些开发过程中遇到的问题，此文总结一下经验，并长期更新此场景下的问题。<br>官方示例项目：<a href="https://github.com/Unity-Technologies/uaal-example" target="_blank" rel="external">https://github.com/Unity-Technologies/uaal-example</a>。  </p>
<h1 id="android__u5E73_u53F0_u5DE5_u7A0B_u5BFC_u51FA"><a href="#android__u5E73_u53F0_u5DE5_u7A0B_u5BFC_u51FA" class="headerlink" title="android 平台工程导出"></a>android 平台工程导出</h1><h2 id="u5BFC_u51FA_u65B9_u6CD5"><a href="#u5BFC_u51FA_u65B9_u6CD5" class="headerlink" title="导出方法"></a>导出方法</h2><p><img src="https://zoucz.com/blogimgs/d2516850-b90c-11ed-9fa0-5dbc93f9d3ee.md/fbfd38ad62eb9d7e939a566ba6061304.jpg" alt="image.png"></p>
<p>file —— build settings，将平台切换为 android 平台，导出即可。  </p>
<p><img src="https://zoucz.com/blogimgs/d2516850-b90c-11ed-9fa0-5dbc93f9d3ee.md/7b4ae2e24177ddfed6e9c291a3983f1b.jpg" alt="image.png">  </p>
<p>导出的工程目录结构如上，unityLibrary 目录下的工程可以 build 出一个 aar 的包。  </p>
<p>launcher 目录下的工程依赖 unityLibrary 工程，可以用来 build 出来一个 apk 应用。  </p>
<h2 id="mono__u6A21_u5F0F_u548C_il2cpp__u6A21_u5F0F"><a href="#mono__u6A21_u5F0F_u548C_il2cpp__u6A21_u5F0F" class="headerlink" title="mono 模式和 il2cpp 模式"></a>mono 模式和 il2cpp 模式</h2><p>导出时，在 player settings —— other settings —— scripting backend 中，可以选择 mono 模式或者 il2cpp 模式。   </p>
<p>Mono模式：  </p>
<ul>
<li>运行时需要Mono虚拟机（Mono Runtime），占用一定的内存和CPU资源。</li>
<li>在编译时，Unity会将C#代码编译为字节码，然后在Android设备上运行时使用Mono虚拟机将字节码转换为机器码。这种方式在一定程度上会影响游戏的性能。</li>
<li>Mono模式下编译出的apk文件比较小，但运行时需要额外的Mono虚拟机支持。</li>
<li>Mono只支持打包为32位指令集（使用32位指令集的计算机内存最大只有4G）。</li>
<li>适用于简单的游戏或应用程序，不需要特别高的性能要求。</li>
</ul>
<p>IL2CPP模式：  </p>
<ul>
<li>运行时不需要Mono虚拟机，减少了内存和CPU资源的占用。</li>
<li>在编译时，Unity会将C#代码编译为C++代码，然后使用C++编译器将其转换为机器码。这种方式可以提高游戏的性能。</li>
<li>IL2CPP模式下编译出的apk文件比较大，但运行时不需要额外的Mono虚拟机支持。</li>
<li>IL2CPP支持64位（兼容32位）</li>
<li>适用于要求高性能的游戏或应用程序，可以提高游戏的帧率和运行速度。</li>
</ul>
<p>如果不需要太高的性能，可以选择Mono模式，它可以使得游戏包更小，占用更少的存储空间。但如果需要高性能，可以选择IL2CPP模式，以提高游戏的性能和运行速度。</p>
<h1 id="u901A_u4FE1"><a href="#u901A_u4FE1" class="headerlink" title="通信"></a>通信</h1><h2 id="unity__u4E2D_u8C03_u7528_android__u65B9_u6CD5"><a href="#unity__u4E2D_u8C03_u7528_android__u65B9_u6CD5" class="headerlink" title="unity 中调用 android 方法"></a>unity 中调用 android 方法</h2><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">...&#10;// &#21021;&#22987;&#21270;&#19968;&#20010; java &#31867;&#10;AndroidJavaClass jc = new AndroidJavaClass(&#34;com.tencent.ivh.IvhSDKCallback&#34;);&#10;// java&#31867;&#20013;&#23454;&#29616;&#19968;&#20010;&#21333;&#20363;&#26041;&#27861;&#65292;&#35843;&#29992;&#36825;&#20010;&#26041;&#27861;&#33719;&#21462;&#31867;&#30340;&#23454;&#20363;&#10;this.callbackObj = jc.CallStatic&#60;AndroidJavaObject&#62;(&#34;getInstance&#34;);&#10;// &#35843;&#29992;&#31867;&#30340;&#26041;&#27861;&#65292;&#24182;&#20256;&#20837;&#21442;&#25968;&#10;callbackObj.Call(&#34;OnChatResponse&#34;, JsonMapper.ToJson(chatRsp));&#10;// &#35843;&#29992;&#31867;&#30340;&#26041;&#27861;&#65292;&#24182;&#33719;&#21462;&#36820;&#22238;&#20540;&#10;string str = callbackObj.Call&#60;string&#62;(&#34;OnCredential&#34;);&#10;...</span><br></pre></td></tr></table></figure>
<h2 id="android__u4E2D_u8C03_u7528_unity__u65B9_u6CD5"><a href="#android__u4E2D_u8C03_u7528_unity__u65B9_u6CD5" class="headerlink" title="android 中调用 unity 方法"></a>android 中调用 unity 方法</h2><p>要在 android 中调用 unity 的方法，需要在场景中创建一个 gameobject，然后在 gameobject 上挂载一个 MonoBehaviour 组件，MonoBehaviour 上的 public 方法可以被 android 调用。  </p>
<p>但是有一个限制，只能接收一个字符串类型的参数，更复杂的数据结构需要自行序列化和反序列化。  </p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.unity3d.player;</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 调用 unity</span></span><br><span class="line">UnityPlayer.UnitySendMessage(<span class="string">"sdk"</span>, <span class="string">"Init"</span>, JSONObject.toJSONString(options));</span><br><span class="line">...</span><br></pre></td></tr></table></figure>
<p>以上代码中， sdk 是场景中挂载的 gameobject 名，Init 是 gameobject 上挂载的 MonoBehaviour 暴露出来的 public 的方法名。  </p>
<h1 id="u5728_android__u4E2D_u6E32_u67D3_unity__u89C6_u56FE"><a href="#u5728_android__u4E2D_u6E32_u67D3_unity__u89C6_u56FE" class="headerlink" title="在 android 中渲染 unity 视图"></a>在 android 中渲染 unity 视图</h1><h2 id="u83B7_u53D6_u6E32_u67D3_view"><a href="#u83B7_u53D6_u6E32_u67D3_view" class="headerlink" title="获取渲染 view"></a>获取渲染 view</h2><p>可以直接使用 com.unity3d.player.UnityPlayer 类，也可以继承后实现自己的 UnityPlayer 类<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.unity3d.player;</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyUnityPlayer</span> <span class="keyword">extends</span> <span class="title">UnityPlayer</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> Context mContext;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">MyUnityPlayer</span><span class="params">(Context context)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">super</span>(context);</span><br><span class="line">        <span class="keyword">this</span>.mContext = context;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="annotation">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">draw</span><span class="params">(Canvas canvas)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">super</span>.draw(canvas);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="annotation">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addView</span><span class="params">(View child)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(child <span class="keyword">instanceof</span> SurfaceView) &#123;</span><br><span class="line">            ((SurfaceView) child).setZOrderOnTop(<span class="keyword">true</span>);</span><br><span class="line">            ((SurfaceView) child).setZOrderMediaOverlay(<span class="keyword">true</span>);</span><br><span class="line">            ((SurfaceView) child).getHolder().setFormat(PixelFormat.TRANSLUCENT);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">super</span>.addView(child);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>传入一个 activity 对象，实例化 unity 视图类，并从中获取 view 对象，加入到应用视图中</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 需要传入一个存活状态的 activity 对象</span></span><br><span class="line">UnityPlayer ivhSDK = <span class="keyword">new</span> IvhSDK(<span class="keyword">this</span>, <span class="keyword">new</span> MyUnityPlayer(<span class="keyword">this</span>));</span><br><span class="line">RelativeLayout layout = (RelativeLayout) findViewById((R.id.vhView));</span><br><span class="line">View vhView = <span class="keyword">this</span>.ivhSDK.mUnityPlayer.getView();</span><br><span class="line">layout.addView(vhView);</span><br></pre></td></tr></table></figure>
<h2 id="u751F_u547D_u5468_u671F_u7ED1_u5B9A"><a href="#u751F_u547D_u5468_u671F_u7ED1_u5B9A" class="headerlink" title="生命周期绑定"></a>生命周期绑定</h2><p>UnityPlayer 类提供了一些和 android 生命周期对应的生命周期函数。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">resume</span><span class="params">()</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">pause</span><span class="params">()</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">destroy</span><span class="params">()</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">windowFocusChanged</span><span class="params">(<span class="keyword">boolean</span> hasFocus)</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">lowMemory</span><span class="params">()</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">configurationChanged</span><span class="params">(Configuration newConfig)</span></span>;</span><br><span class="line">...</span><br></pre></td></tr></table></figure></p>
<p>需要将这些生命周期函数和实例化 Player 时传入的 activity 的对应的生命周期函数绑定起来，在 activity 的生命周期函数中调用 Player 对应的生命周期函数。  </p>
<p>这个绑定操作非常重要，如果生命周期函数绑定不当，会导致一些很难排查的问题。  </p>
<h3 id="windowFocusChanged__u7ED1_u5B9A_u4E0D_u5F53_u9020_u6210_u7684_u89C6_u56FE_u5361_u6B7B"><a href="#windowFocusChanged__u7ED1_u5B9A_u4E0D_u5F53_u9020_u6210_u7684_u89C6_u56FE_u5361_u6B7B" class="headerlink" title="windowFocusChanged 绑定不当造成的视图卡死"></a>windowFocusChanged 绑定不当造成的视图卡死</h3><p>例如没有绑定 windowFocusChanged，可能会导致渲染视图在 pause 然后 resume 后无法恢复，一直处于暂停状态，如 <a href="https://unoniceday.github.io/timeout-while-trying-to-pause-the-Unity-Engine/" target="_blank" rel="external">这个帖子</a> 就提到了这种问题。。  </p>
<h3 id="destroy__u7ED1_u5B9A_u4E0D_u5F53_u9020_u6210_u6A2A_u7AD6_u5C4F_u5207_u6362_u65F6_u5E94_u7528_crash"><a href="#destroy__u7ED1_u5B9A_u4E0D_u5F53_u9020_u6210_u6A2A_u7AD6_u5C4F_u5207_u6362_u65F6_u5E94_u7528_crash" class="headerlink" title="destroy 绑定不当造成横竖屏切换时应用 crash"></a>destroy 绑定不当造成横竖屏切换时应用 crash</h3><p>见 <a href="https://zoucz.com/blog/2022/11/30/66788390-70bc-11ed-9fa0-5dbc93f9d3ee/" target="_blank" rel="external">横竖屏切换导致的crash</a>  </p>
<p><img src="https://zoucz.com/blogimgs/d2516850-b90c-11ed-9fa0-5dbc93f9d3ee.md/49d21c116ed877fd34a361c5009e27b8.jpg" alt="image.png">  </p>
<p>漏掉 destroy 绑定会导致这种很难排查的，匪夷所思的 crash。  </p>
<h1 id="u751F_u6210_u5E26_u7B26_u53F7_u7684_libunity-so__u4EE5_u6392_u67E5_u95EE_u9898"><a href="#u751F_u6210_u5E26_u7B26_u53F7_u7684_libunity-so__u4EE5_u6392_u67E5_u95EE_u9898" class="headerlink" title="生成带符号的 libunity.so 以排查问题"></a>生成带符号的 libunity.so 以排查问题</h1><p>使用 il2cpp 模式导出时，会将一些 unity 自身和渲染的逻辑打包在动态库中，端上运行 crash 后比较难排查问题。  </p>
<p>复现问题后，可以导出带符号的 debug 版本，结合 addr2line 工具来分析错误堆栈，定位到具体的逻辑。  </p>
<p>见 <a href="https://zoucz.com/blog/2022/11/30/011a7e40-70ad-11ed-9fa0-5dbc93f9d3ee/" target="_blank" rel="external">unity il2cpp导出时生成符号表排查crash问题</a>  </p>
<p>生成带符号的动态库：  </p>
<p><img src="https://zoucz.com/blogimgs/d2516850-b90c-11ed-9fa0-5dbc93f9d3ee.md/45426a0d7a5221c0a0cee8a6a7b165a1.jpg" alt="image.png">  </p>
<p>使用 addr2line 定位问题  </p>
<p><img src="https://zoucz.com/blogimgs/d2516850-b90c-11ed-9fa0-5dbc93f9d3ee.md/5d9c84865034426a1d17041ceeb7f245.jpg" alt="image.png"></p>
<h1 id="u5176_u5B83_u95EE_u9898_u548C_u6CE8_u610F_u4E8B_u9879"><a href="#u5176_u5B83_u95EE_u9898_u548C_u6CE8_u610F_u4E8B_u9879" class="headerlink" title="其它问题和注意事项"></a>其它问题和注意事项</h1><h2 id="unity_bug_3A_game_view_content_description__u914D_u7F6E_u7F3A_u5931_u4F1A_u5BFC_u81F4_u65E0_u6CD5_u542F_u52A8"><a href="#unity_bug_3A_game_view_content_description__u914D_u7F6E_u7F3A_u5931_u4F1A_u5BFC_u81F4_u65E0_u6CD5_u542F_u52A8" class="headerlink" title="unity bug: game_view_content_description 配置缺失会导致无法启动"></a>unity bug: game_view_content_description 配置缺失会导致无法启动</h2><p>使用此模块的 Android 应用中，需要在 src/main/res/values/strings.xml 中加上 game_view_content_description 字段配置，：  </p>
<resources><br>    &lt;string name=p_name”&gt;VpaDemo<br>    <string name="game_view_content_description">Game view</string><br></resources>

<p>此配置缺失会导致应用无法启动，目前来看是 unity 一些版本的 bug，我在 unity pro 2021.3.6f1c1 中导出的项目存在此问题。</p>
<h2 id="unity_bug_3A__u5E27_u540C_u6B65_u8BBE_u7F6E_u5BFC_u81F4_u90E8_u5206_u8BBE_u5907_u5076_u73B0_u957F_u65F6_u95F4_u8FD0_u884C_u540E_u6E32_u67D3_u5361_u6B7B"><a href="#unity_bug_3A__u5E27_u540C_u6B65_u8BBE_u7F6E_u5BFC_u81F4_u90E8_u5206_u8BBE_u5907_u5076_u73B0_u957F_u65F6_u95F4_u8FD0_u884C_u540E_u6E32_u67D3_u5361_u6B7B" class="headerlink" title="unity bug: 帧同步设置导致部分设备偶现长时间运行后渲染卡死"></a>unity bug: 帧同步设置导致部分设备偶现长时间运行后渲染卡死</h2><p>也是一个很难排查的问题，见 <a href="https://zoucz.com/blog/2023/01/17/a7767ab0-963b-11ed-9fa0-5dbc93f9d3ee/" target="_blank" rel="external">unity android 长时间运行时 Filter0 线程造成渲染卡死的问题</a>。  </p>
<p><img src="https://zoucz.com/blogimgs/d2516850-b90c-11ed-9fa0-5dbc93f9d3ee.md/e3247a3f9a78ff3f508a169288a17a41.jpg" alt="image.png">  </p>
<p>在 android 平台上有一个默认打开的  “Optimized Frame Pacing” 选项，大部分设备下运行都不会有问题，但是少量设备偶现长时间运行后，渲染帧率越来越低，直到完全卡死。  </p>
<p>有一个名为 Filter0 的线程会吃满1核的CPU，排查得让人头秃，最后发现是 unity 的一个 <a href="https://forum.unity.com/threads/unity-freeze-on-long-run-application-after-migrate-to-unity-2019-3.907628/" target="_blank" rel="external">bug</a>。</p>
<h2 id="il2cpp__u4EE3_u7801_u4F18_u5316_u5BFC_u81F4_u8D44_u6E90_u5305_u4E2D_u7684_u52A8_u753B_u5931_u6548"><a href="#il2cpp__u4EE3_u7801_u4F18_u5316_u5BFC_u81F4_u8D44_u6E90_u5305_u4E2D_u7684_u52A8_u753B_u5931_u6548" class="headerlink" title="il2cpp 代码优化导致资源包中的动画失效"></a>il2cpp 代码优化导致资源包中的动画失效</h2><p>playerSettings —— stripEngineCode 代码优化开启后可能导致资源包中的 classID 找不到等问题，参考 <a href="https://www.jianshu.com/p/e7120f025281" target="_blank" rel="external">https://www.jianshu.com/p/e7120f025281</a> 。  </p>
<p>比如我遇到的是 Could not produce class with ID 74，去 unity 的 ClassIDReference文档 里边一查，确实就是 AnimationClip 类找不到了。在Assets目录下创建 link.xml，内容写  </p>
<pre><code class="xml"><span class="tag">&lt;<span class="name">linker</span>&gt;</span>
  <span class="comment">&lt;!--Preserve types and members in an assembly--&gt;</span>
  <span class="tag">&lt;<span class="name">assembly</span> <span class="attr">fullname</span>=<span class="string">"UnityEngine.AnimationModule"</span>&gt;</span>
    <span class="tag">&lt;<span class="name">type</span> <span class="attr">fullname</span>=<span class="string">"UnityEngine.AnimationClip"</span> <span class="attr">preserve</span>=<span class="string">"all"</span>/&gt;</span>
  <span class="tag">&lt;/<span class="name">assembly</span>&gt;</span>
<span class="tag">&lt;/<span class="name">linker</span>&gt;</span>
</code></pre>
<p>重新导出打包，问题解决。 注意，assembly 名称，可以在 IDE 中点类名进去看头部声明查找到。</p>
<h2 id="u6297_u952F_u9F7F_u8BBE_u7F6E"><a href="#u6297_u952F_u9F7F_u8BBE_u7F6E" class="headerlink" title="抗锯齿设置"></a>抗锯齿设置</h2><p>如果目标设备可能涉及低 dpi 的设备，导出项目时可以打开抗锯齿设置（不同渲染管线打开方法不同），抗锯齿默认是关闭的。  </p>
<p><img src="https://zoucz.com/blogimgs/d2516850-b90c-11ed-9fa0-5dbc93f9d3ee.md/cda575ceb9060936f45414293bb016d5.jpg" alt="image.png"></p>
]]></content>
    <summary type="html">
    <![CDATA[<p>在有的应用场景下，需要把 unity 项目导出作为 lib 运行在终端平台上，之前也零碎记了一些开发过程中遇到的问题，此文总结一下经验，并长期更新此场景下的问题。<br>官方示例项目：<a href="https://github.com/Unity-Technologie]]>
    </summary>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/tags/unity3d/"/>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/categories/unity3d/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[iOS App UI 开发笔记]]></title>
    <link href="https://www.zoucz.com/blog/2023/02/26/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/02/26/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee/</id>
    <published>2023-02-26T08:50:29.000Z</published>
    <updated>2023-03-01T11:29:36.000Z</updated>
    <content type="html"><![CDATA[<h1 id="frame__u5E03_u5C40"><a href="#frame__u5E03_u5C40" class="headerlink" title="frame 布局"></a>frame 布局</h1><p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/4270ee41e67b505835732284dd0a5778.jpg" alt="image.png">  </p>
<p>简单理解 frame 布局，就是制定一个 UI 元素的左上角 x坐标、y坐标、宽度、长度。  </p>
<p>这样就能绝对定位一个元素了。  </p>
<p>这种布局有个问题是，在不同大小的屏幕上，无法自动调整大小和位置。  </p>
<p>例如在 iPhone14 设备上的一块区域，使用 frame 布局，此时方块占了屏幕的大部分宽度，大致位于中间位置。  </p>
<p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/8842a7233e9e8e04af2e91f63c41c8ea.jpg" alt="image.png">  </p>
<p>显示到 iPad 大屏幕设备上时，大小和位置会发生变化  </p>
<p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/6ae7780d906bd5b15faae89cafbf6f4f.jpg" alt="image.png"></p>
<h1 id="autoresize"><a href="#autoresize" class="headerlink" title="autoresize"></a>autoresize</h1><p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/e1882b6aedb16def3eaf7dece5c43098.jpg" alt="image.png"></p>
<p>UIView有一个 autoresizing 功能，如上图。  </p>
<p>四周的短线分别代表 UI 距离父容器的边界距离是否自动调整。点亮时，代表对应方向上的边距固定；不点亮时，代表边距随父容器尺寸变化自动调整。  </p>
<p>中间的箭头代表元素自身尺寸是否随父容器变化自动调整，点亮时，代表对应方向上的尺寸进行自动调整。  </p>
<h1 id="autolayout__u5E03_u5C40"><a href="#autolayout__u5E03_u5C40" class="headerlink" title="autolayout 布局"></a>autolayout 布局</h1><p>autoresizing 一定程度上可以自动适配UI尺寸，但是仍然有一定局限性，比如只能设置父子容器之间的布局，不能设置兄弟容器之间的布局，而且布局属性也不够丰富。  </p>
<h2 id="autolayout__u7406_u89E3"><a href="#autolayout__u7406_u89E3" class="headerlink" title="autolayout 理解"></a>autolayout 理解</h2><p>autolayout简单的理解，就一个核心：约束。 可以参考官方文档 <a href="https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/AnatomyofaConstraint.html#//apple_ref/doc/uid/TP40010853-CH9-SW1" target="_blank" rel="external">Anatomy of a Constraint
</a>  </p>
<p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/04ccacd827014fd632a75dfc2c62037a.jpg" alt="image.png">  </p>
<p>这张图很清晰的描述了一个约束的所有元素组成，含义是 red view 的横向起始是相对于 blue view 的横向结束的 1.0 倍，加上 8 个像素。  </p>
<p>其中，leading 是 autolayout 支持的一个属性，下面这张图比较清晰的说明了各个属性的含义：  </p>
<p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/9f564121208fb2de773b503e2fa39c8a.jpg" alt="image.png"></p>
<h2 id="u9519_u8BEF_u7684_autolayout__u5E03_u5C40"><a href="#u9519_u8BEF_u7684_autolayout__u5E03_u5C40" class="headerlink" title="错误的 autolayout 布局"></a>错误的 autolayout 布局</h2><p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/88e97c33112fed00b415d9f492a3aac5.jpg" alt="image.png"><br>如果只给元素添加一个约束，可以看到会有报错。  </p>
<p>约束需要让应用知道如何定位一个UI元素，能够直接或间接的计算出UI元素的左上角 xy坐标、宽度、高度，约束元素 <strong>无缺失</strong>、<strong>无冗余歧义</strong>。  </p>
<p>错误的类型见官方文档 <a href="https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/TypesofErrors.html#//apple_ref/doc/uid/TP40010853-CH17-SW1" target="_blank" rel="external">布局的错误类型</a>。  </p>
<p>例如只指定了 leading，没有指定 tail 或者 width，那么应用就无法计算出宽度；如果只指定了 width 和 height，应用无法计算出位置；如果同时指定了 leading、tail、width，则可能产生宽度上的歧义。  </p>
<p>有的 UI 元素实现了 intrinsicContentSize 方法，可以根据内容 <strong>自动计算视图尺寸</strong>，所以自动布局约束不足以计算出准确尺寸时，也不会报错。常见的UI元素有：  </p>
<ul>
<li>UILabel：可以根据文本内容自动调整宽度和高度。</li>
<li>UITextView：可以根据文本内容自动调整高度。</li>
<li>UIButton：可以根据标题和图片自动调整大小。</li>
<li>UIImageView：可以根据图像大小自动调整大小。</li>
<li>UISlider：可以根据滑块的值自动调整大小。</li>
<li>UISwitch：大小是固定的，但可以根据内容自动调整位置。</li>
<li>UIStackView：可以根据其子视图的大小和位置自动调整大小。</li>
<li>UICollectionViewFlowLayout：可以根据其布局属性自动调整其子视图的大小和位置。</li>
<li>UITableView：可以根据其单元格的大小和数量自动调整大小。</li>
</ul>
<h2 id="u6297_u538B_u7F29_u548C_u6297_u62C9_u4F38"><a href="#u6297_u538B_u7F29_u548C_u6297_u62C9_u4F38" class="headerlink" title="抗压缩和抗拉伸"></a>抗压缩和抗拉伸</h2><p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/47a80df2c2d7c043d9bf4e6cf3634351.jpg" alt="image.png">  </p>
<p>这个特性比较好理解，当多个元素同时根据内容计算尺寸时，会考虑到抗拉伸和抗压缩，优先级高的越不容易被拉伸和压缩。  </p>
<h1 id="xib_u6587_u4EF6_u4F7F_u7528"><a href="#xib_u6587_u4EF6_u4F7F_u7528" class="headerlink" title="xib文件使用"></a>xib文件使用</h1><h2 id="u521B_u5EFA_UIViewController__u65F6_u81EA_u52A8_u521B_u5EFA_xib__u6587_u4EF6"><a href="#u521B_u5EFA_UIViewController__u65F6_u81EA_u52A8_u521B_u5EFA_xib__u6587_u4EF6" class="headerlink" title="创建 UIViewController 时自动创建 xib 文件"></a>创建 UIViewController 时自动创建 xib 文件</h2><p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/accd74ba5ddb00fe2f966e20c5e09689.jpg" alt="image.png"><br>在创建 UIViewController 时，可勾选自动创建 xib 文件，通过这种方式，将自动创建和 interface 同名的 xib 文件，并将其关联到类文件。  </p>
<h2 id="u81EA_u884C_u521B_u5EFA_xib__u6587_u4EF6_u5E76_u5173_u8054_u5230_u7C7B"><a href="#u81EA_u884C_u521B_u5EFA_xib__u6587_u4EF6_u5E76_u5173_u8054_u5230_u7C7B" class="headerlink" title="自行创建 xib 文件并关联到类"></a>自行创建 xib 文件并关联到类</h2><p>先创建类文件，再创建 xib 文件，选中 xib 文件的 file’s owner，将 class 设置为类名。<br><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/622ecee52e9afb88d973ab3cec9b4c0d.jpg" alt="image.png">  </p>
<p>在 interface 的 init 函数中，从 xib 文件 load view。  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-(id) init &#123;&#10;    self = [super init];&#10;    if (self) &#123;&#10;        UIView *view = [[[NSBundle mainBundle] loadNibNamed:@&#34;XXXView&#34; owner:self options:nil] firstObject];&#10;        [self addSubview:view];&#10;        view.frame = self.bounds;&#10;    &#125;&#10;    return self;&#10;&#125;</span><br></pre></td></tr></table></figure>
<h2 id="xib_u6587_u4EF6_u4E2D_u7684_u7EC4_u4EF6_u81EA_u52A8_u521B_u5EFA_u4EE3_u7801_u4E2D_u7684_u5C5E_u6027"><a href="#xib_u6587_u4EF6_u4E2D_u7684_u7EC4_u4EF6_u81EA_u52A8_u521B_u5EFA_u4EE3_u7801_u4E2D_u7684_u5C5E_u6027" class="headerlink" title="xib文件中的组件自动创建代码中的属性"></a>xib文件中的组件自动创建代码中的属性</h2><p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/32537cbe1369e7411a51c30a00dc6b8a.jpg" alt="image.png"><br>开两个窗口，分别打开 UIViewController 和 xib 文件，选中UI组件，按住 control 键拖动到 controller 中，即可自动创建关联属性，代码中可以直接使用。  </p>
<p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/e25d5cecde48cbd1c6d4888710434af1.jpg" alt="image.png"></p>
<h2 id="u5728_u4EE3_u7801_u4E2D_u4FEE_u6539_u5143_u7D20_u7684_u7EA6_u675F_u5C5E_u6027"><a href="#u5728_u4EE3_u7801_u4E2D_u4FEE_u6539_u5143_u7D20_u7684_u7EA6_u675F_u5C5E_u6027" class="headerlink" title="在代码中修改元素的约束属性"></a>在代码中修改元素的约束属性</h2><p>简单的视图可以直接在 xib 文件中可视化创建和配置，但还是有比较复杂的场景需要通过代码创建。<br>通过 oc 设置元素约束属性的示例：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">NSLayoutConstraint *imageViewHeightConstraint = [imageView.heightAnchor constraintEqualToConstant:imageViewHeight];&#10;imageViewHeightConstraint.active = YES;&#10;imageViewHeightConstraint.constant = imageViewHeight;&#10;&#10;NSLayoutConstraint *imageViewWidthConstraint = [imageView.widthAnchor constraintEqualToConstant:imageViewWidth];&#10;imageViewWidthConstraint.active = YES;&#10;imageViewWidthConstraint.constant = imageViewWidth;</span><br></pre></td></tr></table></figure>
<p>也可以通过三方库 <a href="https://github.com/SnapKit/Masonry" target="_blank" rel="external">Masonry</a> 来管理约束。  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[view1 mas_makeConstraints:^(MASConstraintMaker *make) &#123;&#10;    make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler&#10;    make.left.equalTo(superview.mas_left).with.offset(padding.left);&#10;    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);&#10;    make.right.equalTo(superview.mas_right).with.offset(-padding.right);&#10;&#125;];</span><br></pre></td></tr></table></figure>
<h1 id="u90E8_u5206UI_u7EC4_u4EF6_u7684_u5E03_u5C40_u7279_u6027"><a href="#u90E8_u5206UI_u7EC4_u4EF6_u7684_u5E03_u5C40_u7279_u6027" class="headerlink" title="部分UI组件的布局特性"></a>部分UI组件的布局特性</h1><h2 id="ScrollView"><a href="#ScrollView" class="headerlink" title="ScrollView"></a>ScrollView</h2><p><img src="https://zoucz.com/blogimgs/9f479c70-b5b2-11ed-9fa0-5dbc93f9d3ee.md/11b3bca457a4ac08f6726c526b53c911.jpg" alt="image.png">  </p>
<p>ScrollView 在 AutoLayout 中比较特殊，要创建一个滚动区域，我们必须知道 ScrollView 自身宽/高，以及被滚动的内容区域宽/高，才能正确的创建滚动组件。  </p>
<p>所以 ScrollView 组件附带了 Content Layout Guide 和 Frame Layout Guide 两个子组件。其中 Content Layout Guide 代表内容区域的布局组件，Frame Layout Guide 代表 ScrollView 的布局属性。  </p>
<p>如果只设置 ScrollView 自身的约束，即使设置了完整的约束，还是会报错，原因是无法确定内容区域的约束。将 Content Layout Guide 的宽高约束设置为和 ScrollView 中内容视图一致就可以了。  </p>
<h2 id="StackView"><a href="#StackView" class="headerlink" title="StackView"></a>StackView</h2><p>向 StackView 中添加元素，可以让元素自动按轴向方向排列，并支持设置轴向方向元素之间的填充或间隔属性；支持设置垂直于轴向方向的填充和对齐属性。<br>因此 StackView 配合 ScrollView 使用可以创建动态变化尺寸的滚动区域。  </p>
<h1 id="u76F8_u5173_u8D44_u6599"><a href="#u76F8_u5173_u8D44_u6599" class="headerlink" title="相关资料"></a>相关资料</h1><p><a href="https://zsisme.gitbooks.io/ios-/content/index.html" target="_blank" rel="external">ios核心动画高级技巧</a>  </p>
]]></content>
    <summary type="html">
    <![CDATA[<h1 id="frame__u5E03_u5C40"><a href="#frame__u5E03_u5C40" class="headerlink" title="frame 布局"></a>frame 布局</h1><p><img src="https://zoucz.co]]>
    </summary>
    
      <category term="iOS开发" scheme="https://www.zoucz.com/blog/tags/iOS%E5%BC%80%E5%8F%91/"/>
    
      <category term="iOS开发" scheme="https://www.zoucz.com/blog/categories/iOS%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[iOS Libraries 开发笔记]]></title>
    <link href="https://www.zoucz.com/blog/2023/02/26/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/02/26/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee/</id>
    <published>2023-02-26T08:49:55.000Z</published>
    <updated>2023-03-01T07:31:35.000Z</updated>
    <content type="html"><![CDATA[<p>前段时间做了一些 iOS framework 开发的工作，总结一些作为上手资料的经验吧。</p>
<h1 id="Libraries_u7B80_u4ECB"><a href="#Libraries_u7B80_u4ECB" class="headerlink" title="Libraries简介"></a>Libraries简介</h1><h2 id="u662F_u4EC0_u4E48"><a href="#u662F_u4EC0_u4E48" class="headerlink" title="是什么"></a>是什么</h2><p>官方frameworks、官方系统库、 三方frameworks、三方.a库、三方 workspace。<br>framework就是库文件 + 头文件 + 资源文件。  </p>
<h2 id="u5982_u4F55_u4F7F_u7528"><a href="#u5982_u4F55_u4F7F_u7528" class="headerlink" title="如何使用"></a>如何使用</h2><p>引入方式、embed 的含义。<br><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/04e12860757f4fd5adf8183d206e0af0.jpg" alt="image.png">  </p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/e9dbddaabc139e0e46205b6af77e2849.jpg" alt="image.png">  </p>
<p>最后一列 embed 的含义是，是否将库嵌入到目标 bundle 中。</p>
<h3 id="framework__u9879_u76EE__u4E0E_embed"><a href="#framework__u9879_u76EE__u4E0E_embed" class="headerlink" title="framework 项目 与 embed"></a>framework 项目 与 embed</h3><p>对于一个 framework 项目，比如一个 framework 依赖其它的 framework。如果将静态库 framework 设置为 embed，xcode 会将 framework 当做动态库处理，将被依赖的静态库放到 bundle 的 frameworks 目录下。<br><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/6bb1e6da523b465cf1ca41799feb8746.jpg" alt="image.png"><br>如果设置为 not embed，则不会将三方依赖库放到输出包的 Framework 中。  </p>
<p>无论用哪种方式，framework 在被其它项目引入时，仍然需要引入这个 framework 依赖的三方库。</p>
<h3 id="app__u9879_u76EE__u4E0E_embed"><a href="#app__u9879_u76EE__u4E0E_embed" class="headerlink" title="app 项目 与 embed"></a>app 项目 与 embed</h3><p>对于一个 app，由于静态库在编译的过程中打包到应用可执行程序文件中了，而动态库是运行时链接。所以如果是引入动态库 framework，则需要设置为 embed，静态库则选择 not embed，否则会报错。  </p>
<p>将动态库设置成 not embed 时，编译会成功，但是动态库不会被打包到应用 bundle 中，导致 app 运行时动态链接失败而报错：<br><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/8b1725beab8575705a9e213a7904288d.jpg" alt="image.png">  </p>
<p>将静态库设置成动态库，则应用编译可以成功，但是 iOS 在运行时 app 和动态库是都需要签名的，编译成功的应用在往设备上安装时会因库签名失败而报错，因为实际上库是一个静态库。  </p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/b531b658053b55bdf94cf55c7ba268ee.jpg" alt="image.png">  </p>
<h1 id="framework__u5F00_u53D1"><a href="#framework__u5F00_u53D1" class="headerlink" title="framework 开发"></a>framework 开发</h1><h2 id="u521B_u5EFA_u9879_u76EE"><a href="#u521B_u5EFA_u9879_u76EE" class="headerlink" title="创建项目"></a>创建项目</h2><p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/08ceda59f84f185c083678508f081302.jpg" alt="image.png">  </p>
<p>可以创建 framework 项目或者 static library 项目，前者构建目标是 framework 库，而后者是 .a 库。</p>
<h2 id="u67E5_u627E_u8DEF_u5F84_u8BBE_u7F6E"><a href="#u67E5_u627E_u8DEF_u5F84_u8BBE_u7F6E" class="headerlink" title="查找路径设置"></a>查找路径设置</h2><p>编译的过程中，xcode 可能需要查找项目依赖的 framework、library、头文件，可以在 build settings 中搜索 search paths 来配置  </p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/fb41b04fe8a2d346f118d8616ded2a5c.jpg" alt="image.png"></p>
<p>配置路径时可以用一些环境变量，如项目根目录 <code>$(PROJECT_DIR)</code>。  </p>
<h2 id="u5BF9_u5916_u66B4_u9732_-h__u6587_u4EF6"><a href="#u5BF9_u5916_u66B4_u9732_-h__u6587_u4EF6" class="headerlink" title="对外暴露 .h 文件"></a>对外暴露 .h 文件</h2><p>这是一个 framework 的编译结果：<br><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/6bb1e6da523b465cf1ca41799feb8746.jpg" alt="image.png">  </p>
<p>可以看到库文件 <code>FrameworkDev</code>，还有一个头文件目录 Headers。<br>默认情况下，代码中的头文件都不会包含到 Headers 目录中，如果外部项目依赖库中的头文件，则会因找不到头文件而无法编译。  </p>
<h3 id="u66B4_u9732_u6A21_u5757_u81EA_u8EAB_u7684_u5934_u6587_u4EF6"><a href="#u66B4_u9732_u6A21_u5757_u81EA_u8EAB_u7684_u5934_u6587_u4EF6" class="headerlink" title="暴露模块自身的头文件"></a>暴露模块自身的头文件</h3><p> <img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/3e17beaa3b2b7c55adf3ec40ff4c4199.jpg" alt="image.png">  </p>
<p>选择头文件，将 target membership 选择为构建目标，并设置为 public，这样构建完毕后，这些头文件就会被包含到 Headers 目录中</p>
<h3 id="u66B4_u9732_u4F9D_u8D56_u6A21_u5757_u7684_u5934_u6587_u4EF6"><a href="#u66B4_u9732_u4F9D_u8D56_u6A21_u5757_u7684_u5934_u6587_u4EF6" class="headerlink" title="暴露依赖模块的头文件"></a>暴露依赖模块的头文件</h3><p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/0d176138c9e3149ac0f63f1f83c40fd7.jpg" alt="image.png">  </p>
<p>点加号，add other，选择文件，将依赖模块的头文件加到 build phases 中的 headers 的 public 区域即可。<br>有一些特殊场景比如希望做库合并时，可能需要暴露依赖库的头文件。  </p>
<h2 id="u4E0D_u5BF9_u5916_u66B4_u9732_u7684_-h__u6587_u4EF6"><a href="#u4E0D_u5BF9_u5916_u66B4_u9732_u7684_-h__u6587_u4EF6" class="headerlink" title="不对外暴露的 .h 文件"></a>不对外暴露的 .h 文件</h2><p>如果有两个头文件， A.h 和 B.h，A.h 希望对外暴露，而 B.h 不希望对外暴露，但是 A.h 中 import 了 B.h。<br>此时如果 B.h 没有被暴露出去，那么三方应用在编译时就会报错，找不到 B.h 而编译失败。  </p>
<p>这种情况可以将头文件的依赖关系写在 .m 文件中，而不是.h 文件中。 在 .h 文件中，通过<br>import 语句写在 A.m 文件中，而不是 A.h 文件中。 同时在 A.h 中通过预声明 B.h 中被依赖的 interface 或 protocol，来保证依赖关系。示例：  </p>
<p>B.h  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">// interface &#22768;&#26126;&#10;@interface SRWebSocket&#10;@end&#10;...&#10;// delegate &#22768;&#26126;&#10;@protocol SRWebSocketDelegate&#10;@end</span><br></pre></td></tr></table></figure>
<p>A.h  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">// &#39044;&#22768;&#26126; B.h &#20013;&#30340;&#20381;&#36182;&#39033;&#10;@protocol SRWebSocketDelegate;&#10;@class SRWebSocket;&#10;// &#22312; A.h &#33258;&#36523;&#30340;&#22768;&#26126;&#20013;&#20351;&#29992;&#39044;&#22768;&#26126;&#30340; B.h &#25104;&#21592;&#10;...&#10;@interface IvhSDK : NSObject &#60;SRWebSocketDelegate&#62; &#123;&#10;    @private&#10;    SRWebSocket* _ws;&#10;&#125;&#10;...</span><br></pre></td></tr></table></figure>
<p>A.m  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">// &#22312; A.m &#20195;&#30721;&#25991;&#20214;&#20013;&#30495;&#23454;&#24341;&#20837; B.h &#30340;&#20869;&#23481;&#10;#import &#34;B.h&#34;&#10;@implementation IvhSDK&#10;...&#10;@end</span><br></pre></td></tr></table></figure>
<p>不过这里有一个特殊情况，就是当 B.h 中的类需要作为父类时，通过 class 声明的类是没办法使用的。 我曾经折腾了一阵子，仍然没找到啥好的办法，只好把头文件暴露。  </p>
<h2 id="u5982_u4F55_u67E5_u770B_u7F16_u8BD1_u65E5_u5FD7"><a href="#u5982_u4F55_u67E5_u770B_u7F16_u8BD1_u65E5_u5FD7" class="headerlink" title="如何查看编译日志"></a>如何查看编译日志</h2><p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/0eddb80647865f2bca23dab3f3bae326.jpg" alt="image.png">  </p>
<p>xcode 中可以很方便的查看编译日志，如图所示，xcode将近期的每次编译记录都保存下来了，可以根据时间或者构建目标分组来查看。  </p>
<p>右侧的记录，将编译过程中 xcode 做的每一件事情都按顺序记录下来了，排查问题时非常方便。</p>
<h2 id="u9759_u6001_u5E93_framework__u548C__u52A8_u6001_u5E93_framework"><a href="#u9759_u6001_u5E93_framework__u548C__u52A8_u6001_u5E93_framework" class="headerlink" title="静态库 framework 和 动态库 framework"></a>静态库 framework 和 动态库 framework</h2><h3 id="xcode__u8BBE_u7F6E_u7F16_u8BD1_u76EE_u6807_u7C7B_u578B"><a href="#xcode__u8BBE_u7F6E_u7F16_u8BD1_u76EE_u6807_u7C7B_u578B" class="headerlink" title="xcode 设置编译目标类型"></a>xcode 设置编译目标类型</h3><p>在 xcode 的 build settings 的 linking 设置中，修改  Mach-O Type 类型，可以设置编译类型是动态库还是静态库。</p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/76770dc7ad6d7284d5fbaf9955321448.jpg" alt="image.png">  </p>
<p>通过 file 查看 framework 中的库二进制文件信息可以判断类型。  </p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/bf3b02c8bc22d0c0ddde33032987a608.jpg" alt="image.png">  </p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/b78293e6dbe8cb2ed5dce14e76f9df58.jpg" alt="image.png">  </p>
<p>静态库 framework 和动态库 framework 在编译和链接的过程中有一些区别，链接的区别网上资料也比较多，参考 <a href="https://www.cnblogs.com/king-lps/p/7757919.html" target="_blank" rel="external">https://www.cnblogs.com/king-lps/p/7757919.html</a>。  </p>
<p>下面主要描述一下编译过程的区别在 xcode 中的表现。  </p>
<h3 id="u9759_u6001_u5E93_u7F16_u8BD1_u8FC7_u7A0B"><a href="#u9759_u6001_u5E93_u7F16_u8BD1_u8FC7_u7A0B" class="headerlink" title="静态库编译过程"></a>静态库编译过程</h3><ul>
<li>预处理（Preprocessing）：预处理器会处理源代码文件，包括宏替换和头文件展开等操作。</li>
<li>编译（Compilation）：编译器会将预处理后的源代码编译成汇编代码。</li>
<li>汇编（Assembly）：汇编器将汇编代码转换成目标代码，即一个个对象文件（Object File）。</li>
<li>归档（Archiving）：Xcode 会使用 ar 工具将多个目标文件打包成一个静态库文件（.a文件）。</li>
</ul>
<p>这里值的注意的是，没有链接的过程。也就是说，如果静态库 framework 依赖其它的静态 framework，并不会把其它静态库的内容包含到最终生成的 .a 文件中。三方开发者需要将静态库和静态库依赖的库全部加入项目依赖。  </p>
<p>查看 xcode 的编译日志，发现一个地方很容易让人误解    </p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/773e2f8800f7af0f90a4bfd48ea1e215.jpg" alt="image.png">  </p>
<p>可以发现创建静态库的过程是用的 libtool 工具，里边有个 LinkFileList 的参数指定，实际上这个参数指定是没有任何作用的，因为压根不会有链接过程。把日志里的命令复制出来，LinkFileList 参数修改掉随便指定个任意值，仍然可以成功执行。  </p>
<p>用 nm 查看生成的静态库文件中的符号表，前面的 ‘U’ 代表找不到相关符号，也能说明并没有把依赖的三方静态库打包进去。  </p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/fb6669975b579833b0bfaf1ecbaee84c.jpg" alt="image.png">  </p>
<p>在应用中使用，如果不引入 framework 依赖的其它库，则会报错找不到符号。  </p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/8fb5890d8d40078a9a34c77e559efb1b.jpg" alt="image.png"></p>
<h3 id="u52A8_u6001_u5E93_uFF08_u6216_u53EF_u6267_u884C_u7A0B_u5E8F_uFF09_u7F16_u8BD1_u8FC7_u7A0B"><a href="#u52A8_u6001_u5E93_uFF08_u6216_u53EF_u6267_u884C_u7A0B_u5E8F_uFF09_u7F16_u8BD1_u8FC7_u7A0B" class="headerlink" title="动态库（或可执行程序）编译过程"></a>动态库（或可执行程序）编译过程</h3><ul>
<li>预处理（Preprocessing）：将源代码文件中的预处理指令（如 #include、#define 等）替换为实际的代码。这个步骤可以使用预处理器（Preprocessor）完成。</li>
<li>编译（Compiling）：将预处理后的源代码文件编译成目标文件（Object file），目标文件包含了编译后的机器码（Machine code）和一些元数据（Metadata）。这个步骤可以使用编译器（Compiler）完成。</li>
<li>汇编（Assembling）：将编译后的目标文件转换成机器码指令的二进制表示形式。这个步骤可以使用汇编器（Assembler）完成。</li>
<li>链接（Linking）：将编译和汇编后的目标文件链接成最终的动态库（Framework）。在链接过程中，会解决符号依赖关系、地址重定向等问题。这个步骤可以使用链接器（Linker）完成。</li>
<li>代码签名（Code signing）：如果启用了代码签名功能，编译后的动态库将会被签名，以保证代码的完整性和安全性。这个步骤可以使用代码签名工具完成  </li>
</ul>
<p>查看 xcode 的编译日志  </p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/381f942314795d7f4dcefe19f8072942.jpg" alt="image.png"></p>
<p>查看符号表，编译动态库时，链接是会生效的。  </p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/e47c9fcd66bc3c78abe5881e953923b9.jpg" alt="image.png"></p>
<h3 id="u9759_u6001_u5E93_u3001_u52A8_u6001_u5E93_u4EA4_u53C9_u4F9D_u8D56_u7684_u573A_u666F"><a href="#u9759_u6001_u5E93_u3001_u52A8_u6001_u5E93_u4EA4_u53C9_u4F9D_u8D56_u7684_u573A_u666F" class="headerlink" title="静态库、动态库交叉依赖的场景"></a>静态库、动态库交叉依赖的场景</h3><p><a href="https://juejin.cn/post/6964993273244942343#heading-10" target="_blank" rel="external">https://juejin.cn/post/6964993273244942343#heading-10</a> 这篇文章做了一下动静态库交叉依赖的总结，看着比较清晰。   </p>
<p>搬运一下结论：  </p>
<ul>
<li>静态库A依赖静态库B，使用时需要在Link Binary With Libraries引入静态库A、B；</li>
<li>静态库A依赖动态库B，使用时需要在Link Binary With Libraries引入静态库A和动态库B，并且在Embeded Binaries引入动态库B；</li>
<li>动态库A依赖静态库B，使用时需要在Link Binary With Libraries引入动态库A，并且在Embeded Binaries引入动态库A；</li>
<li>动态库A依赖动态库B，使用时需要在Link Binary With Libraries引入动态库A和B，并且在Embeded Binaries引入动态库A和B；</li>
</ul>
<p>测试代码：<a href="https://github.com/loyinglin/LearnBuild/tree/master/%E5%8A%A8%E6%80%81%E5%BA%93%E5%92%8C%E9%9D%99%E6%80%81%E5%BA%93" target="_blank" rel="external">点击访问</a></p>
<h2 id="u8BBE_u7F6E_u7F16_u8BD1_u540E_u7F6E_u811A_u672C"><a href="#u8BBE_u7F6E_u7F16_u8BD1_u540E_u7F6E_u811A_u672C" class="headerlink" title="设置编译后置脚本"></a>设置编译后置脚本</h2><p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/b7cfe5d07abd888a89f4780b9cc8cebc.jpg" alt="image.png">  </p>
<p>可以用编译后置脚本做一些事情，比如：  </p>
<h3 id="u9759_u6001_u5E93_u5408_u5E76"><a href="#u9759_u6001_u5E93_u5408_u5E76" class="headerlink" title="静态库合并"></a>静态库合并</h3><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">TARGET_BUILD_DIR=$&#123;TARGET_BUILD_DIR&#125;&#10;PROJECT_DIR=$&#123;PROJECT_DIR&#125;&#10;libtool -static -o &#34;$&#123;TARGET_BUILD_DIR&#125;/FrameworkDev.framework/FrameworkDev&#34; &#34;$&#123;TARGET_BUILD_DIR&#125;/FrameworkDev.framework/FrameworkDev&#34; &#34;$&#123;PROJECT_DIR&#125;/ThirdParty/JSONModel.framework/JSONModel&#34; &#34;$&#123;PROJECT_DIR&#125;/ThirdParty/SocketRocket.framework/SocketRocket&#34;</span><br></pre></td></tr></table></figure>
<h3 id="u7F16_u8BD1_u7ED3_u679C_u6253_u5305"><a href="#u7F16_u8BD1_u7ED3_u679C_u6253_u5305" class="headerlink" title="编译结果打包"></a>编译结果打包</h3><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ARGET_BUILD_DIR=$&#123;TARGET_BUILD_DIR&#125;&#10;PROJECT_DIR=$&#123;PROJECT_DIR&#125;&#10;# &#22240;&#20026; JSONModel &#38656;&#35201;&#22312; header &#20013;&#20381;&#36182;&#65292;&#38656;&#35201;&#23558;&#20854;&#32534;&#35793;&#22909;&#30340;&#38745;&#24577;&#24211;&#19968;&#36215;&#25171;&#21253;&#21040; pod &#20013;&#10;# &#22797;&#21046; JSONModel.framework &#21040; buid &#30446;&#24405;&#10;cp -r &#34;$&#123;PROJECT_DIR&#125;/ThirdParty/JSONModel.framework&#34; &#34;$&#123;TARGET_BUILD_DIR&#125;/JSONModel.framework&#34; \&#10;&#38;&#38; cp -r &#34;$&#123;PROJECT_DIR&#125;/ThirdParty/SocketRocket.framework&#34; &#34;$&#123;TARGET_BUILD_DIR&#125;/SocketRocket.framework&#34; \&#10;&#38;&#38; cd &#34;$&#123;TARGET_BUILD_DIR&#125;&#34; \&#10;&#38;&#38; zip -r &#34;FrameworkDev.zip&#34; &#34;FrameworkDev.framework&#34; &#34;JSONModel.framework&#34; &#34;SocketRocket.framework&#34;</span><br></pre></td></tr></table></figure>
<h1 id="cocoapods__u5305_u7BA1_u7406"><a href="#cocoapods__u5305_u7BA1_u7406" class="headerlink" title="cocoapods 包管理"></a>cocoapods 包管理</h1><p>cocoapods 是一个类似 npm、maven 等包管理工具，用它可以管理本地项目的依赖，避免在本地管理一大堆依赖模块；也可以将自己的模块通过 cocoapods 模块的形式发布，方便第三方引入。  </p>
<h2 id="u5C06_u81EA_u5DF1_u7684_u5E93_u901A_u8FC7_cocoapods__u53D1_u5E03"><a href="#u5C06_u81EA_u5DF1_u7684_u5E93_u901A_u8FC7_cocoapods__u53D1_u5E03" class="headerlink" title="将自己的库通过 cocoapods 发布"></a>将自己的库通过 cocoapods 发布</h2><p>参考官方的 <a href="https://guides.cocoapods.org/syntax/podspec.html" target="_blank" rel="external">podspec 语法说明</a>。  </p>
<p>可以用 podspec 文件管理模块的依赖，并制定模块代码/库文件的地址。 然后将 podspec 发布到 cocoapods 官方平台，或者存放到自己的服务器，给三方开发者提供链接。  </p>
<p>下面是一个示例：  </p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="constant">Pod::Spec</span>.new <span class="keyword">do</span> |s|</span><br><span class="line">  s.name             = <span class="string">'SGPlayer'</span></span><br><span class="line">  s.version          = <span class="string">'2.0.1'</span></span><br><span class="line">  s.summary          = <span class="string">'SGPlayer'</span></span><br><span class="line">  s.ios.framework = [<span class="string">'AVFoundation'</span>, <span class="string">'AudioToolbox'</span>, <span class="string">'VideoToolbox'</span>]</span><br><span class="line">  s.library = <span class="string">'iconv'</span>, <span class="string">'bz2'</span>, <span class="string">'z'</span></span><br><span class="line">  s.homepage         = <span class="string">'https://github.com/libobjc/SGPlayer'</span></span><br><span class="line">  <span class="comment"># s.screenshots     = ''</span></span><br><span class="line">  s.license          = &#123; <span class="symbol">:type</span> =&gt; <span class="string">'MIT'</span>, <span class="symbol">:file</span> =&gt; <span class="string">'LICENSE'</span> &#125;</span><br><span class="line">  s.author           = <span class="string">'czzou@tencent.com'</span></span><br><span class="line">  s.source = &#123; <span class="symbol">:http</span> =&gt; <span class="string">'https://xxxxx/SGPlayer/2.0.1/SGPlayer.framework.zip'</span> &#125;</span><br><span class="line">  <span class="comment"># s.social_media_url = ''</span></span><br><span class="line"></span><br><span class="line">  s.ios.deployment_target = <span class="string">'11.0'</span></span><br><span class="line">  s.static_framework = <span class="keyword">true</span></span><br><span class="line">  s.vendored_frameworks = <span class="string">'SGPlayer.framework'</span></span><br><span class="line">  </span><br><span class="line">  s.public_header_files = <span class="string">'SGPlayer.framework/Headers/*.h'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>
<p>这个 podspec 文件中，指定了模块的版本、依赖的系统Framework、系统lib、文件地址 source、头文件路径等信息。  </p>
<p>source 属性可以指定一个 git 源码仓库地址、一个本地源码路径，或者一个可下载的代码或模块 zip 文件的url，三方开发者在使用时，cocoapods可以处理这几个类型的模块数据。  </p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">s.source = &#123;</span><br><span class="line">  <span class="symbol">:git</span> =&gt; <span class="string">'https://github.com/username/repo.git'</span>,</span><br><span class="line">  <span class="symbol">:tag</span> =&gt; <span class="string">'1.0.0'</span></span><br><span class="line">&#125;</span><br><span class="line">s.source = &#123;</span><br><span class="line">  <span class="symbol">:path</span> =&gt; <span class="string">'~/Documents/MyProject'</span></span><br><span class="line">&#125;</span><br><span class="line">s.source = &#123;</span><br><span class="line">  <span class="symbol">:http</span> =&gt; <span class="string">'https://example.com/myproject.tar.gz'</span>,</span><br><span class="line">  <span class="comment"># 可选，用于完整性校验</span></span><br><span class="line">  <span class="symbol">:sha256</span> =&gt; <span class="string">'4bfe565adceec28a74e9c71a9a0f0a8e2cbbe4c14a4f4d45c96ad54a98e9d7bb'</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>使用 git 指定地址时，可以将模块发布到 cocoapods 仓库中，完整的步骤为：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># &#21019;&#24314; podspec&#10;pod spec create MyLibrary&#10;# &#20462;&#25913;&#20869;&#23481;&#21518;&#65292;&#36827;&#34892;&#26657;&#39564;&#10;pod lib lint MyLibrary.podspec&#10;# &#27880;&#20876;&#10;pod trunk register youremail@example.com&#10;# &#21457;&#24067;&#10;pod trunk push MyLibrary.podspec</span><br></pre></td></tr></table></figure>
<h2 id="u9879_u76EE_u4E2D_u4F7F_u7528_cocoapods__u7BA1_u7406_u4F9D_u8D56_u5E93"><a href="#u9879_u76EE_u4E2D_u4F7F_u7528_cocoapods__u7BA1_u7406_u4F9D_u8D56_u5E93" class="headerlink" title="项目中使用 cocoapods 管理依赖库"></a>项目中使用 cocoapods 管理依赖库</h2><p>首先在操作系统中安装 cocoapods<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo gem install cocoapods</span><br></pre></td></tr></table></figure></p>
<p>然后通过 <code>pod init</code> 命令，或者手动创建 Podfile 文件，可以参考官方的 <a href="https://guides.cocoapods.org/syntax/podfile.html" target="_blank" rel="external">podfile语法</a>  </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># &#25351;&#23450;&#24179;&#21488;&#21450;&#20854;&#29256;&#26412;&#21495;&#10;platform :ios, &#39;12.0&#39;&#10;&#10;# &#26159;&#21542;&#20351;&#29992;&#21160;&#24577;&#24211;&#10;use_frameworks!&#10;&#10;# &#25351;&#23450;&#28304;&#10;source &#39;https://github.com/CocoaPods/Specs.git&#39;&#10;&#10;# &#25351;&#23450;&#20840;&#23616;&#37197;&#32622;&#10;configurations = &#123;&#10;  &#39;Debug&#39; =&#62; :debug,&#10;  &#39;Release&#39; =&#62; :release&#10;&#125;&#10;&#10;# &#25351;&#23450;&#22810;&#20010;&#30446;&#26631;&#10;target &#39;MyApp&#39; do&#10;  # &#25351;&#23450;&#20381;&#36182;&#24211;&#21450;&#20854;&#29256;&#26412;&#21495;&#10;  pod &#39;Alamofire&#39;, &#39;~&#62; 5.0&#39;&#10;  pod &#39;SwiftyJSON&#39;, &#39;~&#62; 4.0&#39;&#10;  pod &#39;SDWebImage&#39;, &#39;~&#62; 5.0&#39;&#10;&#10;  # &#25351;&#23450;&#20381;&#36182;&#24211;&#21450;&#20854;&#29305;&#23450;&#30340;&#37197;&#32622;&#10;  pod &#39;Analytics&#39;, &#39;~&#62; 3.0&#39;, :configurations =&#62; [&#39;Debug&#39;]&#10;&#10;  # &#25351;&#23450;&#20381;&#36182;&#24211;&#21450;&#20854;&#29305;&#23450;&#30340; subspec&#10;  pod &#39;Firebase/Core&#39;, &#39;~&#62; 8.0&#39;&#10;&#10;  # &#25351;&#23450;&#20381;&#36182;&#24211;&#21450;&#20854;&#29305;&#23450;&#30340;&#29256;&#26412;&#21495;&#21644; subspec&#10;  pod &#39;GoogleMaps&#39;, &#39;~&#62; 4.0&#39;, :subspecs =&#62; [&#39;Maps&#39;, &#39;Places&#39;], :git =&#62; &#39;https://github.com/googlemaps/google-maps-ios.git&#39;&#10;&#10;  # &#25351;&#23450;&#20381;&#36182;&#24211;&#21450;&#20854;&#29305;&#23450;&#30340;&#26469;&#28304;&#10;  pod &#39;MyLibrary&#39;, :git =&#62; &#39;https://github.com/MyOrganization/MyLibrary.git&#39;, :branch =&#62; &#39;develop&#39;&#10;end&#10;&#10;# &#25351;&#23450;&#21333;&#20010;&#30446;&#26631;&#10;target &#39;MyAppTests&#39; do&#10;  # &#27979;&#35797;&#30456;&#20851;&#20381;&#36182;&#10;  pod &#39;Quick&#39;&#10;  pod &#39;Nimble&#39;&#10;end</span><br></pre></td></tr></table></figure>
<p>除了官方 podspec 仓库和 git 仓库，还可以从三方地址获取 spec 文件  </p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod <span class="string">'JSONKit'</span>, <span class="symbol">:podspec</span> =&gt; <span class="string">'https://example.com/JSONKit.podspec'</span></span><br></pre></td></tr></table></figure>
<p>然后运行 <code>pod install</code>，或者已经安装过了运行 <code>pod update</code>，清理缓存 <code>pod cache clean --all</code> 来更新依赖包。</p>
<p><img src="https://zoucz.com/blogimgs/8adbf330-b5b2-11ed-9fa0-5dbc93f9d3ee.md/8e8eaf82c3a6dcd85cd53b6795a6b476.jpg" alt="image.png"></p>
<p>cocoapods 会在本地创建一个 pods 项目，和一个 workspace 文件。  </p>
<p>项目的依赖都用 Pods 项目来管理，如果 podfile 中指定了 <code>use_frameworks!</code>，则编译时会将依赖的源码格式的 pod 编译成动态库放到结果 bundle 中，否则编译成静态库。  </p>
<p>这里实操的时候，有两个要注意的点：  </p>
<ol>
<li>使用 cocoapods 管理项目后，需要打开 workspace 工程，而不能直接打开原项目的工程文件了， 因为 workspace 中配置好了依赖关系。  </li>
<li>打开 workspace 之前需要关掉原 proj 的 xcode 窗口，不然 workspace 中无法访问原 proj。  </li>
</ol>
<h2 id="u4ECE_u9879_u76EE_u4E2D_u5378_u8F7D_cocoapods"><a href="#u4ECE_u9879_u76EE_u4E2D_u5378_u8F7D_cocoapods" class="headerlink" title="从项目中卸载 cocoapods"></a>从项目中卸载 cocoapods</h2><p>在终端中进入项目目录：  </p>
<ul>
<li>运行 pod deintegrate 命令以将 CocoaPods 从项目中卸载。</li>
<li>删除项目目录中的 Podfile 和 Podfile.lock 文件。</li>
<li>如果在 xcode 中打开了 MyApp.xcworkspace，则关闭它。</li>
<li>删除 MyApp.xcworkspace 文件。</li>
</ul>
<h2 id="u5176_u5B83_u7279_u6B8A_u7279_u6027"><a href="#u5176_u5B83_u7279_u6B8A_u7279_u6027" class="headerlink" title="其它特殊特性"></a>其它特殊特性</h2><h3 id="framework_u9879_u76EE_u4F9D_u8D56_u7684_u5934_u6587_u4EF6_u5FC5_u987B_u662F_u5728_u5E93_u4E2D"><a href="#framework_u9879_u76EE_u4F9D_u8D56_u7684_u5934_u6587_u4EF6_u5FC5_u987B_u662F_u5728_u5E93_u4E2D" class="headerlink" title="framework项目依赖的头文件必须是在库中"></a>framework项目依赖的头文件必须是在库中</h3><p>一个很不好理解，但是确实存在的一个问题。  </p>
<p>A.framework 依赖 B.framework，A.framework 通过静态库的形式发布，B.framework 通过源码的形式发布。  </p>
<p>此时编译时会报错：<code>Include of non-modular header inside framework module</code>   </p>
<p>参考这个讨论：<a href="https://stackoverflow.com/questions/27776497/include-of-non-modular-header-inside-framework-module" target="_blank" rel="external">https://stackoverflow.com/questions/27776497/include-of-non-modular-header-inside-framework-module</a>。  </p>
<p>解决的办法有三种：  </p>
<ul>
<li>通过修改 xcode 的设置来解决，不推荐</li>
<li>将对 .h 文件的 import 放到 .m 文件中，可以解决部分场景（如前面所说有时候无法做到）</li>
<li>将 B.framework 也打包成模块发布，可以解决</li>
</ul>
<h1 id="future"><a href="#future" class="headerlink" title="future"></a>future</h1><h2 id="framework__u5355_u6D4B_u65B9_u6CD5"><a href="#framework__u5355_u6D4B_u65B9_u6CD5" class="headerlink" title="framework 单测方法"></a>framework 单测方法</h2><p>通过单测保证模块质量。</p>
<h2 id="u5982_u4F55_u901A_u8FC7_cli__u7F16_u8BD1_u9879_u76EE_uFF0C_u642D_u5EFA_u6D41_u6C34_u7EBF"><a href="#u5982_u4F55_u901A_u8FC7_cli__u7F16_u8BD1_u9879_u76EE_uFF0C_u642D_u5EFA_u6D41_u6C34_u7EBF" class="headerlink" title="如何通过 cli 编译项目，搭建流水线"></a>如何通过 cli 编译项目，搭建流水线</h2><p>高效构建发布版本。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>前段时间做了一些 iOS framework 开发的工作，总结一些作为上手资料的经验吧。</p>
<h1 id="Libraries_u7B80_u4ECB"><a href="#Libraries_u7B80_u4ECB" class="headerlink" title]]>
    </summary>
    
      <category term="iOS开发" scheme="https://www.zoucz.com/blog/tags/iOS%E5%BC%80%E5%8F%91/"/>
    
      <category term="iOS开发" scheme="https://www.zoucz.com/blog/categories/iOS%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[unity 项目导出到 iOS 平台以 library 的方式运行]]></title>
    <link href="https://www.zoucz.com/blog/2023/02/16/5b4a8090-adf7-11ed-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/02/16/5b4a8090-adf7-11ed-9fa0-5dbc93f9d3ee/</id>
    <published>2023-02-16T12:42:21.000Z</published>
    <updated>2023-03-03T08:11:40.000Z</updated>
    <content type="html"><![CDATA[<p>在有的应用场景下，需要把 unity 项目导出作为 lib 运行在终端平台上，此文记录把 unity 项目导出到 iOS 平台，打包成 framework 提供给应用使用的过程，以及实践中遇到的问题。 </p>
<h1 id="u5BFC_u51FA_u65B9_u6CD5"><a href="#u5BFC_u51FA_u65B9_u6CD5" class="headerlink" title="导出方法"></a>导出方法</h1><h2 id="u9879_u76EE_u5BFC_u51FA"><a href="#u9879_u76EE_u5BFC_u51FA" class="headerlink" title="项目导出"></a>项目导出</h2><p>导出的步骤可以参考官方论坛的一个帖子 <a href="https://forum.unity.com/threads/integration-unity-as-a-library-in-native-ios-app.685219/" target="_blank" rel="external">https://forum.unity.com/threads/integration-unity-as-a-library-in-native-ios-app.685219/</a>，里边也有一个 demo 工程。  </p>
<p><img src="https://zoucz.com/blogimgs/5b4a8090-adf7-11ed-9fa0-5dbc93f9d3ee.md/283a5fc6335b13a8be9ded9e8870fa49.jpg" alt="image.png">  </p>
<p>将平台切换到 iOS ，Build 即可。  </p>
<p><img src="https://zoucz.com/blogimgs/5b4a8090-adf7-11ed-9fa0-5dbc93f9d3ee.md/3db55945bc7e39ccf06bb468e8943131.jpg" alt="image.png">  </p>
<p>打开项目，可以看到两个 Build Targets，一个用于启动应用，一个用于打包 framework。可以把 framework 名字修改为自己的包名。  </p>
<h2 id="u9879_u76EE_u7F16_u8BD1_u8BBE_u7F6E"><a href="#u9879_u76EE_u7F16_u8BD1_u8BBE_u7F6E" class="headerlink" title="项目编译设置"></a>项目编译设置</h2><p>data 目录包含应用程序的序列化资源以及 .NET 程序集（.dll 或 .dat 文件），其形式为完整代码或元数据。将 data 目录的 target membership 由 app 目标设置为 framework 目标。   </p>
<p><img src="https://zoucz.com/blogimgs/5b4a8090-adf7-11ed-9fa0-5dbc93f9d3ee.md/4076c4a0e656410e2515538ac41a224a.jpg" alt="image.png">  </p>
<p>若有 iOS Plugin 代码，需要将其头文件暴露为 public，供三方应用使用。  </p>
<p><img src="https://zoucz.com/blogimgs/5b4a8090-adf7-11ed-9fa0-5dbc93f9d3ee.md/57abc2e22b2d0b841d2d8a6cd44d10a4.jpg" alt="image.png">  </p>
<h1 id="u5728_iOS__u5E94_u7528_u4E2D_u663E_u793A_unity__u6E32_u67D3_u89C6_u56FE"><a href="#u5728_iOS__u5E94_u7528_u4E2D_u663E_u793A_unity__u6E32_u67D3_u89C6_u56FE" class="headerlink" title="在 iOS 应用中显示 unity 渲染视图"></a>在 iOS 应用中显示 unity 渲染视图</h1><h2 id="u542F_u52A8_u6E32_u67D3"><a href="#u542F_u52A8_u6E32_u67D3" class="headerlink" title="启动渲染"></a>启动渲染</h2><p>在初始化 unity 打包成的 framework 时，需要指定框架路径，并通过 setDataBundleId 指定从哪里读取 data 目录。</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">NSString *message = [options toJSONString];&#10;NSString* bundlePath = nil;&#10;bundlePath = [[NSBundle mainBundle] bundlePath];&#10;bundlePath = [bundlePath stringByAppendingString: @&#34;/Frameworks/ivhsdk.framework&#34;];&#10;&#10;NSBundle* bundle = [NSBundle bundleWithPath: bundlePath];&#10;if ([bundle isLoaded] == false) [bundle load];&#10;UnityFramework* ufw = [bundle.principalClass getInstance];&#10;[ufw setDataBundleId: &#34;com.tencent.ivhsdk&#34;];&#10;[IvhLibAPI setUfw:ufw];</span><br></pre></td></tr></table></figure>
<p>然后启动渲染  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[ufw runEmbeddedWithArgc: 1 argv:    (char *[])&#123;&#34;&#34;&#125; appLaunchOpts: @&#123;&#125;];</span><br></pre></td></tr></table></figure>
<h2 id="u751F_u547D_u5468_u671F_u7ED1_u5B9A"><a href="#u751F_u547D_u5468_u671F_u7ED1_u5B9A" class="headerlink" title="生命周期绑定"></a>生命周期绑定</h2><p>在 UIApplicationDelegate 的实现中绑定 unity framework 的生命周期相关的函数。  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">- (void)applicationWillResignActive:(UIApplication *)application &#123; [[[self ufw] appController] applicationWillResignActive: application]; &#125;&#10;- (void)applicationDidEnterBackground:(UIApplication *)application &#123; [[[self ufw] appController] applicationDidEnterBackground: application]; &#125;&#10;- (void)applicationWillEnterForeground:(UIApplication *)application &#123; [[[self ufw] appController] applicationWillEnterForeground: application]; &#125;&#10;- (void)applicationDidBecomeActive:(UIApplication *)application &#123; [[[self ufw] appController] applicationDidBecomeActive: application]; &#125;&#10;- (void)applicationWillTerminate:(UIApplication *)application &#123; [[[self ufw] appController] applicationWillTerminate: application]; &#125;</span><br></pre></td></tr></table></figure>
<h1 id="u901A_u4FE1"><a href="#u901A_u4FE1" class="headerlink" title="通信"></a>通信</h1><h2 id="unity__u4E2D_u8C03_u7528_iOS__u65B9_u6CD5"><a href="#unity__u4E2D_u8C03_u7528_iOS__u65B9_u6CD5" class="headerlink" title="unity 中调用 iOS 方法"></a>unity 中调用 iOS 方法</h2><p>在 unity 项目的 Plugins/iOS 目录下，创建 plugin 需要的 oc文件 xxx.mm，在其中定义 c 函数  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">extern &#34;C&#34; &#123;&#10;    void unityCallIOS(string params)&#123;&#10;    &#125;&#10;&#125;</span><br></pre></td></tr></table></figure>
<p>在 unity 的 c# 代码中，通过 <code>[DllImport(&quot;__Internal&quot;)]</code> 引入方法并调用。  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[DllImport(&#34;__Internal&#34;)]&#10;public static extern void unityCallIOS(string params);</span><br></pre></td></tr></table></figure>
<p>在 c# 中调用 unityCallIOS 即可。  </p>
<h2 id="iOS__u4E2D_u8C03_u7528_unity__u65B9_u6CD5"><a href="#iOS__u4E2D_u8C03_u7528_unity__u65B9_u6CD5" class="headerlink" title="iOS 中调用 unity 方法"></a>iOS 中调用 unity 方法</h2><p>参考 “启动渲染” 的步骤，初始化并得到  UnityFramework 对象后，通过对象的 sendMessageToGOWithName 方法调用 unity 场景节点上挂载的 MonoBehaviour 组件暴露的方法。  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[ufw sendMessageToGOWithName: [@&#34;gameobject name&#34; UTF8String] functionName: @&#34;method name&#34; message: [@&#34;params&#34; UTF8String]];</span><br></pre></td></tr></table></figure>
<h1 id="u5176_u5B83_u95EE_u9898_u548C_u6CE8_u610F_u4E8B_u9879"><a href="#u5176_u5B83_u95EE_u9898_u548C_u6CE8_u610F_u4E8B_u9879" class="headerlink" title="其它问题和注意事项"></a>其它问题和注意事项</h1><h2 id="u5305_u540D_u95EE_u9898_u5F15_u8D77_u7684_unity_ios_Thread_1_3A_EXC_BAD_ACCESS"><a href="#u5305_u540D_u95EE_u9898_u5F15_u8D77_u7684_unity_ios_Thread_1_3A_EXC_BAD_ACCESS" class="headerlink" title="包名问题引起的 unity ios Thread 1: EXC_BAD_ACCESS"></a>包名问题引起的 unity ios Thread 1: EXC_BAD_ACCESS</h2><p>如 “启动渲染” 步骤中的描述，如果没有正确的设置 data path，会报这个错误而 crash 掉。  </p>
<p>很奇怪的报错，从堆栈中看不出任何问题，各种试，最后发现 unityframework 设置的包名，必须和 framework打包的包名一致。  </p>
<p><img src="https://zoucz.com/blogimgs/5b4a8090-adf7-11ed-9fa0-5dbc93f9d3ee.md/b8c5d29b46b2a4a43032a4f21dd885b9.jpg" alt="image.png"></p>
<h2 id="u5E94_u7528_u64AD_u653E_u7684_u58F0_u97F3_u975E_u5E38_u5C0F"><a href="#u5E94_u7528_u64AD_u653E_u7684_u58F0_u97F3_u975E_u5E38_u5C0F" class="headerlink" title="应用播放的声音非常小"></a>应用播放的声音非常小</h2><p>导出后发现应用的声音非常小，原因是声音是从听筒播放的而不是扬声器播放的。  </p>
<p><img src="https://zoucz.com/blogimgs/5b4a8090-adf7-11ed-9fa0-5dbc93f9d3ee.md/027511a65b20600ae838d4588caffce3.jpg" alt="image.png">  </p>
<p>导出时取消勾选 Prepare iOS for Recording 可修复。  </p>
<h2 id="u65E0_u6CD5_u4FEE_u6539_u6E32_u67D3_u533A_u57DF_u7684_u5927_u5C0F_u548C_u4F4D_u7F6E"><a href="#u65E0_u6CD5_u4FEE_u6539_u6E32_u67D3_u533A_u57DF_u7684_u5927_u5C0F_u548C_u4F4D_u7F6E" class="headerlink" title="无法修改渲染区域的大小和位置"></a>无法修改渲染区域的大小和位置</h2><p><a href="https://docs.unity3d.com/Manual/UnityasaLibrary-iOS.html" target="_blank" rel="external">https://docs.unity3d.com/Manual/UnityasaLibrary-iOS.html</a>  </p>
<p>官方文档描述，只能全屏。  </p>
<p>虽然可以通过 unityFramework.appController.unityView，并修改它的 frame，但是实际上并不会生效，渲染仍然是全屏的。  </p>
<p>这里只能考虑暴露一些 unity 内部 camera 的 transform 调整的方法，通过 <code>sendMessageToGOWithName</code> 封装成一些位置大小调整 API 给用户调用。</p>
<h2 id="u56E0_u4E3A_u53EA_u80FD_u5168_u5C4F_uFF0Cunity_view__u53EF_u80FD_u906E_u6321_u5176_u5B83view"><a href="#u56E0_u4E3A_u53EA_u80FD_u5168_u5C4F_uFF0Cunity_view__u53EF_u80FD_u906E_u6321_u5176_u5B83view" class="headerlink" title="因为只能全屏，unity view 可能遮挡其它view"></a>因为只能全屏，unity view 可能遮挡其它view</h2><p>需要注意view的加载顺序，如果没有其它需求，可以考虑将 view 放到最下层。<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">// &#33267;&#23569;&#24471;&#20256;&#19968;&#20010;&#21442;&#25968;&#65292;&#21542;&#21017;core&#10;//  [[self ufw] runEmbeddedWithArgc: 1 argv:    (char *[])&#123;&#34;ailing&#34;&#125; appLaunchOpts: @&#123;&#125;];&#10;// add other view</span><br></pre></td></tr></table></figure></p>
<h2 id="u900F_u660E_u80CC_u666F_u5728_iOS__u7684_view__u4E0A_u4F1A_u5931_u6548"><a href="#u900F_u660E_u80CC_u666F_u5728_iOS__u7684_view__u4E0A_u4F1A_u5931_u6548" class="headerlink" title="透明背景在 iOS 的 view 上会失效"></a>透明背景在 iOS 的 view 上会失效</h2><p>和作为 android library 运行不一样，unity 自己创建的 unityView 会丢失透明的特性，但是通过 window 添加 subview 到 unity 生成的 view 中，这样可以修改渲染的背景，如：  </p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UIView* backview = [[UIView alloc] initWithFrame:[UIScreen.mainScreen bounds]];&#10;backview.backgroundColor = [UIColor redColor];&#10;[self.ufw.appController.window addSubview:backview];&#10;[self.ufw.appController.window sendSubviewToBack:backview];</span><br></pre></td></tr></table></figure>
]]></content>
    <summary type="html">
    <![CDATA[<p>在有的应用场景下，需要把 unity 项目导出作为 lib 运行在终端平台上，此文记录把 unity 项目导出到 iOS 平台，打包成 framework 提供给应用使用的过程，以及实践中遇到的问题。 </p>
<h1 id="u5BFC_u51FA_u65B9_u6CD5]]>
    </summary>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/tags/unity3d/"/>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/categories/unity3d/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[unity 资源包版本兼容性问题及il2cpp模式下代码优化问题]]></title>
    <link href="https://www.zoucz.com/blog/2023/02/14/31132b00-ac59-11ed-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/02/14/31132b00-ac59-11ed-9fa0-5dbc93f9d3ee/</id>
    <published>2023-02-14T11:17:39.000Z</published>
    <updated>2023-02-14T11:51:43.000Z</updated>
    <content type="html"><![CDATA[<h1 id="assetsbundle_u5305_u5728_u4E0D_u540C_u7279_u6027_u7248_u672C_u4E4B_u95F4_u7684_u517C_u5BB9_u6027_u95EE_u9898"><a href="#assetsbundle_u5305_u5728_u4E0D_u540C_u7279_u6027_u7248_u672C_u4E4B_u95F4_u7684_u517C_u5BB9_u6027_u95EE_u9898" class="headerlink" title="assetsbundle包在不同特性版本之间的兼容性问题"></a>assetsbundle包在不同特性版本之间的兼容性问题</h1><p>我们基于 unity2021.3.6f1c1 打包了模型的 assetsbundle 包，交付给客户使用。  </p>
<p>然而客户使用 unity2021.3.13f1c1 版本导入资源，反馈模型无法正常渲染。  </p>
<p>unity editor 中运行时，发现控制台有类似下面这种告警：  </p>
<p><img src="https://zoucz.com/blogimgs/31132b00-ac59-11ed-9fa0-5dbc93f9d3ee.md/157d776a11f4d1a62176dd9045e0bf1d.jpg" alt="image.png">  </p>
<p><code>Shader &#39;xxx&#39; uses 153 texture parameters, more than the 64 supported by the current graphics device.</code>  </p>
<p>搜到了这篇帖子：<a href="https://forum.unity.com/threads/new-warning-about-texture-parameter-support-in-2021-3-10.1338212/" target="_blank" rel="external">https://forum.unity.com/threads/new-warning-about-texture-parameter-support-in-2021-3-10.1338212/</a>  </p>
<p>帖子里提到，资源在不同的子版本之间打包和导入，可能存在渲染异常。  </p>
<p>经过一段时间的排查和调试，也踩了一些坑，得出下面的经验:  </p>
<h2 id="u4E0D_u540C_u7279_u6027_u7248_u672C_u95F4_u6253_u5305_u548C_u52A0_u8F7D"><a href="#u4E0D_u540C_u7279_u6027_u7248_u672C_u95F4_u6253_u5305_u548C_u52A0_u8F7D" class="headerlink" title="不同特性版本间打包和加载"></a>不同特性版本间打包和加载</h2><p>特性版本1打 assetsbundle 包，特性版本2下加载，模型可能渲染正常，也可能shader无法工作导致渲染完全失效：<br><img src="https://zoucz.com/blogimgs/31132b00-ac59-11ed-9fa0-5dbc93f9d3ee.md/cffc2493e18cf48260666e73825e98f0.jpg" alt="image.png"></p>
<h2 id="u76F8_u540C_u7279_u6027_u7248_u672C_u6253_u5305_u548C_u52A0_u8F7D_uFF0C_u4F46_u662F_u6D89_u53CA_u5230_u9879_u76EE_u7248_u672C_u5347_u7EA7"><a href="#u76F8_u540C_u7279_u6027_u7248_u672C_u6253_u5305_u548C_u52A0_u8F7D_uFF0C_u4F46_u662F_u6D89_u53CA_u5230_u9879_u76EE_u7248_u672C_u5347_u7EA7" class="headerlink" title="相同特性版本打包和加载，但是涉及到项目版本升级"></a>相同特性版本打包和加载，但是涉及到项目版本升级</h2><p>使用 unity2021.3.13f1c1 打开以前基于 unity2021.3.6f1c1 创建的项目，打开的过程中 unity 提示涉及版本转换，点击 continue。  </p>
<h3 id="u5F53_u524D_u9879_u76EE_u4F7F_u7528"><a href="#u5F53_u524D_u9879_u76EE_u4F7F_u7528" class="headerlink" title="当前项目使用"></a>当前项目使用</h3><p>打包模型的 assetsbundle，在当前项目中使用，渲染正常。  </p>
<h3 id="u57FA_u4E8E_u76EE_u6807_u7279_u6027_u7248_u672C_u521B_u5EFA_u65B0_u9879_u76EE_u4F7F_u7528"><a href="#u57FA_u4E8E_u76EE_u6807_u7279_u6027_u7248_u672C_u521B_u5EFA_u65B0_u9879_u76EE_u4F7F_u7528" class="headerlink" title="基于目标特性版本创建新项目使用"></a>基于目标特性版本创建新项目使用</h3><p>基于 unity2021.3.13f1c1 创建一个新的项目，加载上面打包的 assetsbundle，渲染看似正常，但是出现色差。<br><img src="https://zoucz.com/blogimgs/31132b00-ac59-11ed-9fa0-5dbc93f9d3ee.md/1c7446a4f2ebef3205b3b143f616f0c4.jpg" alt="image.png"><br><img src="https://zoucz.com/blogimgs/31132b00-ac59-11ed-9fa0-5dbc93f9d3ee.md/4368b8090224f97bdbb0a27e951f7122.jpg" alt="image.png"></p>
<h3 id="u57FA_u4E8E_u76EE_u6807_u7279_u6027_u7248_u672C_u521B_u5EFA_u65B0_u9879_u76EE_u5E76_u5207_u6362_target_platform__u4F7F_u7528"><a href="#u57FA_u4E8E_u76EE_u6807_u7279_u6027_u7248_u672C_u521B_u5EFA_u65B0_u9879_u76EE_u5E76_u5207_u6362_target_platform__u4F7F_u7528" class="headerlink" title="基于目标特性版本创建新项目并切换 target platform 使用"></a>基于目标特性版本创建新项目并切换 target platform 使用</h3><p>上面一个步骤， build target 是 win / mac / linux，将 build target 切换为 ios 或者 android，渲染出现异常，出现白色蒙层。  </p>
<p><img src="https://zoucz.com/blogimgs/31132b00-ac59-11ed-9fa0-5dbc93f9d3ee.md/8350e643014edf4e3280c3b036e84a67.jpg" alt="image.png">  </p>
<h2 id="u76F8_u540C_u7279_u6027_u7248_u672C_u521B_u5EFA_u3001_u6253_u5305_u548C_u52A0_u8F7D_uFF0C_u4E0D_u6D89_u53CA_u5230_u5347_u7EA7"><a href="#u76F8_u540C_u7279_u6027_u7248_u672C_u521B_u5EFA_u3001_u6253_u5305_u548C_u52A0_u8F7D_uFF0C_u4E0D_u6D89_u53CA_u5230_u5347_u7EA7" class="headerlink" title="相同特性版本创建、打包和加载，不涉及到升级"></a>相同特性版本创建、打包和加载，不涉及到升级</h2><p>加载后渲染完全正常，切换目标平台后渲染也正常。  </p>
<h1 id="u57FA_u4E8E_il2cpp__u6A21_u5F0F_u4E0B_u4EE3_u7801_u4F18_u5316_u95EE_u9898"><a href="#u57FA_u4E8E_il2cpp__u6A21_u5F0F_u4E0B_u4EE3_u7801_u4F18_u5316_u95EE_u9898" class="headerlink" title="基于 il2cpp 模式下代码优化问题"></a>基于 il2cpp 模式下代码优化问题</h1><p>prefab上附带了 animator，结果使用 il2cpp 模式打包并在终端运行时，动画无法正常执行，无明显报错，只有一些告警信息，<code>Could not produce class with ID 74</code>。  </p>
<p>这个问题其实有在 <a href="https://zoucz.com/blog/2022/11/30/c1ffbdd0-70c3-11ed-9fa0-5dbc93f9d3ee/" target="_blank" rel="external">unity 动画&amp;音频控制、平台打包过程中遇到的坑</a> 这里提到过，从今天的case来看，可能是由于 il2cpp 转换的过程中，没有处理到 assetsbundle 资源包，导致资源包加载后找不到相关类。</p>
]]></content>
    <summary type="html">
    <![CDATA[<h1 id="assetsbundle_u5305_u5728_u4E0D_u540C_u7279_u6027_u7248_u672C_u4E4B_u95F4_u7684_u517C_u5BB9_u6027_u95EE_u9898"><a href="#assetsbundle]]>
    </summary>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/tags/unity3d/"/>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/categories/unity3d/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[unity android 长时间运行时 Filter0 线程造成渲染卡死的问题]]></title>
    <link href="https://www.zoucz.com/blog/2023/01/17/a7767ab0-963b-11ed-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2023/01/17/a7767ab0-963b-11ed-9fa0-5dbc93f9d3ee/</id>
    <published>2023-01-17T07:50:47.000Z</published>
    <updated>2023-01-17T12:27:25.000Z</updated>
    <content type="html"><![CDATA[<p>前段时间的项目中，分别实践了把 unity 项目导出到 android / ios 平台，并分别封装 aar 库和 framework 库，提供给各平台的 native 项目使用。期间遇到了各种问题，不过经过一些调试或者资料查找都解决了。  </p>
<p>把这个单独记录下来，是因为问题解决的比较偶然，套不上通用的问题解决路径，作为一个解决问题的路子记录下来吧。  </p>
<h1 id="BUG_u73B0_u8C61"><a href="#BUG_u73B0_u8C61" class="headerlink" title="BUG现象"></a>BUG现象</h1><p>使用 unity 渲染了一个人物，会执行各种骨骼动画。在 android 平台上，activity在前台，或者浮窗使用，都没有任何问题，即使运行一天一夜，依然状态稳定。  </p>
<p>产品实测过程中，发现偶尔使用近一个小时后，会出现人物卡住不动的情况。后续不断测试尝试复现，发现是以浮窗形式使用的时候，同时频繁切换导航软件并开启音乐软件听歌，大约10~20分钟后，人物会卡住不动。</p>
<h1 id="u6000_u7591_u65B9_u5411_u4E00_uFF1Aunity__26amp_3B_android__u751F_u547D_u5468_u671F_u914D_u5408_u5B58_u5728_u95EE_u9898"><a href="#u6000_u7591_u65B9_u5411_u4E00_uFF1Aunity__26amp_3B_android__u751F_u547D_u5468_u671F_u914D_u5408_u5B58_u5728_u95EE_u9898" class="headerlink" title="怀疑方向一：unity &amp; android 生命周期配合存在问题"></a>怀疑方向一：unity &amp; android 生命周期配合存在问题</h1><p><img src="https://zoucz.com/blogimgs/a7767ab0-963b-11ed-9fa0-5dbc93f9d3ee.md/b8badf63a1652b28b245c1b2bb4d6bef.jpg" alt="image.png"><br>unity导出到 android 平台后，需要通过 UnityPlayer 类初始化，初始化时需要传入一个 activity 的 context。  </p>
<p>并且需要在 activity 的 onResume、onPause、onWindowFocusChanged 生命周期函数中分别调用 UnityPlayer 的 resume、pause、windowFocusChanged 函数。  </p>
<p>亲测，这几个生命周期函数是必须全部绑定的。如果暂停时调用了 pause，然后恢复时调用 resume，但是没有调用 windowFocusChanged，此时渲染仍然不会恢复，画面卡住，必须调用 onWindowFocusChanged 后才会恢复渲染。  </p>
<p>本地比较好复现这种情况，但是经过分析log，sdk的使用方对生命周期函数的使用并没有问题。  </p>
<p>进一步添加 log 分析，发现甚至 unity 的主线程都没有暂停，monobehaviour 的 LateUpdate 函数都还在正常执行，更加说明了生命周期绑定这一块没有问题。</p>
<h1 id="u6000_u7591_u65B9_u5411_u4E8C_uFF1Aunity__u52A8_u753B_u72B6_u6001_u673A_u5B58_u5728_u95EE_u9898"><a href="#u6000_u7591_u65B9_u5411_u4E8C_uFF1Aunity__u52A8_u753B_u72B6_u6001_u673A_u5B58_u5728_u95EE_u9898" class="headerlink" title="怀疑方向二：unity 动画状态机存在问题"></a>怀疑方向二：unity 动画状态机存在问题</h1><p>另一种思路，既然 unity 本身的渲染没有卡住，那么有没有可能是人物在正常渲染，只是没有动了？  </p>
<p>sdk 内部一个动画切换逻辑，有根据动画播放进度判断是否需要切换的逻辑。之前在另外一个项目有踩过一个坑：  </p>
<p><img src="https://zoucz.com/blogimgs/a7767ab0-963b-11ed-9fa0-5dbc93f9d3ee.md/e86c985f197c1a505283ac048856d701.jpg" alt="image.png">  </p>
<p>通过 CrossFade 切换动画，然后通过获取当前播放进度的方式进行切换：  </p>
<p><img src="https://zoucz.com/blogimgs/a7767ab0-963b-11ed-9fa0-5dbc93f9d3ee.md/4172a85f60ed1567f0ec567be3366532.jpg" alt="image.png"><br><img src="https://zoucz.com/blogimgs/a7767ab0-963b-11ed-9fa0-5dbc93f9d3ee.md/994294a9dcb2bbb5e1e6c8a4c0c6b009.jpg" alt="image.png">  </p>
<p>此时会偶现一个问题，获取的进度信息可能出现延迟。动画切换后，预期在下一帧获取的动画播放进度是 0，然而实际情况中，有时候会获取到上一个动画的播放进度，即 1.0（100%），这样就会导致一个后果：动画被不断地设置为从头开始，表现为人物卡住不动。  </p>
<p>检查代码逻辑，该项目使用的动画切换并不是 CrossFade，而是在动画状态机中设置过渡选项，然后直接设置动画状态机的 Params 来切换动画。但是通过进度判断是否应该切换动画的逻辑仍然存在。给代码加上判断切换中状态的保护逻辑，然后加上关键日志，验证。  </p>
<p>结论是并没有出现动画状态机卡住的情况，人物会根据用户调用或者闲时动作正常切换，输出正确的log。</p>
<h1 id="u6000_u7591_u65B9_u5411_u4E09_uFF1A_u9891_u7E41_u53D1_u9001_u7684_u52A8_u4F5C_u6307_u4EE4_u5B58_u5728_u95EE_u9898"><a href="#u6000_u7591_u65B9_u5411_u4E09_uFF1A_u9891_u7E41_u53D1_u9001_u7684_u52A8_u4F5C_u6307_u4EE4_u5B58_u5728_u95EE_u9898" class="headerlink" title="怀疑方向三：频繁发送的动作指令存在问题"></a>怀疑方向三：频繁发送的动作指令存在问题</h1><p>此时思路有一点停滞了，没有办法，继续分析log。  </p>
<p><img src="https://zoucz.com/blogimgs/a7767ab0-963b-11ed-9fa0-5dbc93f9d3ee.md/b2ec8307e46d1816f39b43d22f8973c6.jpg" alt="image.png"></p>
<p>发现了一个比较有价值的信息，执行动作指令在一秒钟内被发送了多次。业务表示没有在短时间内调用这么多次指令，那么这里就出现了没对齐的地方了，会不会是某未知原因频繁的发送指令导致了频繁的动作切换，最终造成卡顿呢？</p>
<p>通过对代码的第一直觉，不会是这个原因，因为动作切换的第一步，就是清空之前的动画序列，然后才开始执行新的动作，即使高频调用切换动作方法，也不会造成计算的性能瓶颈。  </p>
<p>那么为什么会有如此高频的调用呢？  </p>
<p>我在本地模拟用户的使用场景 —— 频繁的暂停，恢复，各种操作，又暂停，如此循环。最终发现是 unity 的一个特点，引擎被 pause 后，渲染被暂停，通过 UnityPlayer.UnitySendMessage 调用引擎暴露的方法，也无法响应。但是这些调用并没有被丢弃，而是做了一个类似队列缓存的操作，一旦 unity 被 resmue 恢复之后，之前的所有调用会一起被触发，也就是上面的日志看到的情形。  </p>
<p>通过在本地调试，可以很容易复现这种场景，但是应用也没有出现任何卡顿，CPU利用率也无任何波动。。。思路再次停滞。  </p>
<h1 id="u7834_u6848_uFF1A_u771F_u5B9E_u539F_u56E0_u548C_u89E3_u51B3"><a href="#u7834_u6848_uFF1A_u771F_u5B9E_u539F_u56E0_u548C_u89E3_u51B3" class="headerlink" title="破案：真实原因和解决"></a>破案：真实原因和解决</h1><p>业务同学帮忙观察复现，并录制视频，人物并不是一下卡住不动的，而是慢慢的降低帧率，越来越卡顿，最终完全不动。  </p>
<p>看起来还是比较符合第三种猜测，奈何本地怎么也无法复现。  </p>
<p>不过这个现象最终比较明确的把问题指向了 CPU，而不是其它问题。所以再一次复现之后，让业务同学帮忙看一下进程的各线程的 CPU 占用。最终发现了这么一个不认识名字的线程，吃满了单核的计算性能：  </p>
<p><img src="https://zoucz.com/blogimgs/a7767ab0-963b-11ed-9fa0-5dbc93f9d3ee.md/ab20990488c919f096edff515b1fee1a.jpg" alt="image.png">  </p>
<p>业务在一般场景下，不会有这么一个线程刚好吃满单核性能，于是抱着试一试的心态，Google了一下：“unity filter0 thread”。第一条就蹦出来这个官方论坛的链接 <a href="https://forum.unity.com/threads/unity-freeze-on-long-run-application-after-migrate-to-unity-2019-3.907628/" target="_blank" rel="external">Unity freeze on long run application after migrate to unity 2019.3</a>。先不看内容，看标题就感觉症状非常像。  </p>
<p>里边描述了 unity 在 android 平台上的一个 bug，在长时间运行后，应用会卡住，并且有一个叫 Filter0 的线程吃满一个核心的性能。  </p>
<p>里边又有一个链接 <a href="https://forum.unity.com/threads/android-build-project-freezes-after-5-minutes-with-playerloop-in-profiler-at-60-000-ms.863143/#post-6947501" target="_blank" rel="external">Android build project freezes after 5 minutes with playerloop in profiler at 60,000 ms</a>。  </p>
<p>有好几个用户遇到这个问题，版本分布在 2018.3、2019.3、2020.3，而我的版本是 2021.3。  </p>
<p><img src="https://zoucz.com/blogimgs/a7767ab0-963b-11ed-9fa0-5dbc93f9d3ee.md/3a52c2ed6a75c5024f19b1034dded3e2.jpg" alt="image.png">  </p>
<p>通过官方论坛里边用户的说法，关闭 “Optimized Frame Pacing” 选项，再次尝试复现，发现关闭这个默认开启的 “帧同步优化” 选项后，确实有效，渲染不再卡住！确定是一个 unity 本身的 bug。  </p>
<p>很扯蛋的是，贴子里官方开发人员说这个问题很难复现，先关闭 issue …   </p>
<p>确实很难复现，我在非常多的设备和场景下调试，都不会有问题。就这一个业务的设备上同时使用导航和音乐时出现了问题。但是这个选项默认是打开的啊！可以预想到会有不少开发者被这个 bug 坑到，留此文，也许能帮到人。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>前段时间的项目中，分别实践了把 unity 项目导出到 android / ios 平台，并分别封装 aar 库和 framework 库，提供给各平台的 native 项目使用。期间遇到了各种问题，不过经过一些调试或者资料查找都解决了。  </p>
<p>把这个单独记录下]]>
    </summary>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/tags/unity3d/"/>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/categories/unity3d/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[unity 动画&音频控制、平台打包过程中遇到的坑]]></title>
    <link href="https://www.zoucz.com/blog/2022/11/30/c1ffbdd0-70c3-11ed-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2022/11/30/c1ffbdd0-70c3-11ed-9fa0-5dbc93f9d3ee/</id>
    <published>2022-11-30T15:29:19.000Z</published>
    <updated>2022-12-02T16:04:45.000Z</updated>
    <content type="html"><![CDATA[<p>记录音画控制、平台打包过程中遇到的一些坑点，有一部分是 unity 的文档描述不够完善造成的使用错误；一部分是属于神坑级BUG，比较难分析出来，只有网上查经验或者肝unity源码能解决的。   </p>
<p>在此记录，不断更新吧，先简单记一下能回忆起来的部分。</p>
<h1 id="u52A8_u753B"><a href="#u52A8_u753B" class="headerlink" title="动画"></a>动画</h1><h2 id="Animator-CrossFade__u5207_u6362_u65F6_u8FDB_u5EA6_u53D8_u5316_u7684_u5751_u70B9"><a href="#Animator-CrossFade__u5207_u6362_u65F6_u8FDB_u5EA6_u53D8_u5316_u7684_u5751_u70B9" class="headerlink" title="Animator.CrossFade 切换时进度变化的坑点"></a>Animator.CrossFade 切换时进度变化的坑点</h2><p>做一个逻辑，在上一个动画快播放完毕时，淡入下一个动画。<br><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.animator.CrossFade(name, <span class="number">0.1</span>f, <span class="number">0</span>, <span class="number">0</span>f);</span><br></pre></td></tr></table></figure></p>
<p>切换动画，紧接着立即或者在接下来几帧<br><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">AnimatorStateInfo animatorStateInfo = animator.GetCurrentAnimatorStateInfo(<span class="number">0</span>);</span><br><span class="line">animatorStateInfo.normalizedTime; <span class="comment">// do somethine with this progress</span></span><br></pre></td></tr></table></figure></p>
<p>获取动画播放进度并执行相关逻辑，发现播放进度并没有切换到新的动画的进度，而是会在旧动画的进度停留一段时间。</p>
<h1 id="u97F3_u9891"><a href="#u97F3_u9891" class="headerlink" title="音频"></a>音频</h1><h2 id="u64AD_u653E_u5EF6_u65F6_u95EE_u9898"><a href="#u64AD_u653E_u5EF6_u65F6_u95EE_u9898" class="headerlink" title="播放延时问题"></a>播放延时问题</h2><p>Unity Editor中打开Edit菜单<br>选择Project Settings<br>选择Audio，您会在Inspector中看到DSP Buffer Size，其中有数个选项可供选择：Default, Best latency, Good latency, Best Performance.<br>选择 Best latency，检查延迟问题是否得到了解决  </p>
<h2 id="u64AD_u653E_u6742_u97F3_u95EE_u9898"><a href="#u64AD_u653E_u6742_u97F3_u95EE_u9898" class="headerlink" title="播放杂音问题"></a>播放杂音问题</h2><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>._currentAudioClip = AudioClip.Create(<span class="string">"tts"</span>, lengthSamples, channels, sampleRate, <span class="keyword">true</span>, _PcmReaderCallback);</span><br></pre></td></tr></table></figure>
<p>神坑！！！ 流式音频数据读取回调，在每次读取数据时会默认分配一段 unsafe 的内存，如果读取数据时，tts的数据尚未返回，就会导致播放杂音或者之前内存块中残留的音频。<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">private void _PcmReaderCallback(float[] data)&#10;&#123;&#10;    int copyCount = Math.Min(this._pcmArray.Count, data.Length);&#10;    if (copyCount == 0)&#123;&#10;        // &#65281;&#65281;&#65281; &#37325;&#35201;&#65306;PcmReaderCallback &#20026; data &#20998;&#37197;&#20102; unSafe &#30340;&#20869;&#23384;&#31354;&#38388;&#65292;&#36825;&#37324;&#22914;&#26524;&#19981;&#32473; fill 0&#65292;&#23601;&#20250;&#23548;&#33268;&#25773;&#25918;&#26434;&#38899;&#25110;&#32773;&#27531;&#30041;&#20043;&#21069;&#20869;&#23384;&#22359;&#20869;&#30340;&#38899;&#39057;&#65281;&#10;        Array.Fill(data, 0);&#10;        return;&#10;    &#125;;&#10;    // ...&#10;&#125;</span><br></pre></td></tr></table></figure></p>
<p>需要判断，若没有数据时，手动填0。</p>
<h2 id="u90E8_u5206_u97F3_u8282_u88AB_u62C9_u957F"><a href="#u90E8_u5206_u97F3_u8282_u88AB_u62C9_u957F" class="headerlink" title="部分音节被拉长"></a>部分音节被拉长</h2><p>其实和播放杂音问题比较类似，其实上面的修复方案并不完整，还有一点要注意的：<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">try &#123;&#10;    //&#25335;&#36125;&#25968;&#25454;&#21040; AudioClip &#30340; buffer &#20013;&#10;    this._pcmArray.CopyTo(0, data, 0, copyCount);&#10;    //&#33509;&#26377;&#19981;&#22815;&#30340;&#37096;&#20998;&#65292;&#22635;&#20805;0&#65292;&#36991;&#20813;&#25773;&#25918;&#26410;&#30693;&#25968;&#25454;&#10;    if(copyCount &#60; data.Length) &#123;&#10;        Array.Fill(data, 0, copyCount, data.Length - copyCount);&#10;    &#125;&#10;&#125; catch(Exception e)&#123;&#10;    Debug.Log(&#34;PcmReaderCallback error:&#34; + e.Message);&#10;&#125;</span><br></pre></td></tr></table></figure></p>
<p>有数据，但是数据不够时，也需要填0。</p>
<h2 id="u5FEB_u901F_u5207_u6362_u5BF9_u8BDD_u97F3_u9891_u6D41_u65F6_u5076_u73B0_u64AD_u653E_u524D_u6B21"><a href="#u5FEB_u901F_u5207_u6362_u5BF9_u8BDD_u97F3_u9891_u6D41_u65F6_u5076_u73B0_u64AD_u653E_u524D_u6B21" class="headerlink" title="快速切换对话音频流时偶现播放前次"></a>快速切换对话音频流时偶现播放前次</h2><p>之前的处理时，开始新的对话时，情况音频缓存 buffer<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">this._pcmArray.Clear();</span><br></pre></td></tr></table></figure></p>
<p>但是光清空可能会有隐患，因为下一次对话开启时，可能正好有一个 <code>PcmReaderCallback</code> 正在执行中。<br>所以最好的方法是在开始新会话时重建buffer。<br><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">this._pcmArray = new ArrayList();</span><br></pre></td></tr></table></figure></p>
<h2 id="u5076_u5C14_u51FA_u73B0_u5361_u987F_u3001_u97F3_u8282_u91CD_u590D"><a href="#u5076_u5C14_u51FA_u73B0_u5361_u987F_u3001_u97F3_u8282_u91CD_u590D" class="headerlink" title="偶尔出现卡顿、音节重复"></a>偶尔出现卡顿、音节重复</h2><p>最恶心的问题之一，很难排查，听个一两分钟的音频也不一定能复现，可能很多次才复现一次，但是肯定是能复现的。unity的文档写的跟开玩笑一样，最终还是得肉身趟坑攒经验。排查历程：  </p>
<ul>
<li>是不是服务端数据错误？<br>将音频保存到本地播放多次，没有出现</li>
<li>是不是audio read 时，因数据不足，中间填0，打印，没有填0  </li>
</ul>
<p><img src="https://zoucz.com/blogimgs/c1ffbdd0-70c3-11ed-9fa0-5dbc93f9d3ee.md/f7bc932588605cc4534cf459bcf02b0c.jpg" alt="image.png"></p>
<p>这里发现有的卡顿是有填0的。  </p>
<p>但是后来发现，并不是所有的卡顿都会填0日志。</p>
<ul>
<li>数据片段处理错误，是否有非法数据？<br>后来又看到写入音频流式数据过程中的 cast 报错 <code>At least one element in the source array could not be cast down to the destination array type.</code>。  </li>
</ul>
<figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>._pcmArray.CopyTo(<span class="number">0</span>, data, <span class="number">0</span>, copyCount);</span><br></pre></td></tr></table></figure>
<p>cast 报错时有个逻辑，会丢掉当前报错的片段，也会造成卡顿。_pcmArray 是一个 ArrayList。   </p>
<p>这里提示数据源中有部分数据不是正常的 float。</p>
<p>audioclip是要求数据范围在 -1~1 之间的，尝试写死数据为 -1.1，发现并未报错。</p>
<ul>
<li><p>是不是偶尔有数据范围是奇数位，导致 pasre to int16 失败？<br>打印流式片包中字节长度，发现都是偶数。</p>
</li>
<li><p>是不是线程安全问题？<br>audioclip的数据读取回调是在音频线程中调用的，数据源是在unity的主线程中，打印调用时间戳发现经常会同一个时间戳调用多次，是否存在线程安全问题？会导致 data race，造成 cast 报错，或者多次读取同一个片段，导致音频重复（确实是出现过）。    </p>
</li>
</ul>
<p>加锁，仍然出现卡顿的问题。考虑到加锁对性能的影响，去掉锁。  </p>
<ul>
<li>尝试修改数据源类型，避免cast工作<br>思考，都是 float，为啥还要 cast 呢？ 数据源类型从 ArrayList 修改为泛型数组 List<float>。  </float></li>
</ul>
<p>又听了十几分钟，好似没有复现卡顿了。   </p>
<p>真机调试 —— 你妹！ 听了几分钟出现了重复音节！   </p>
<p>真机倒是没有卡顿的问题了，也没有出现 cast 报错了，说明这个改动多少可能是有效果的。</p>
<ul>
<li>继续加锁<br>性能没有正确性重要，加回锁试试。  </li>
</ul>
<p>听了10分钟，重复读没有出现了， 没有任何异常log打出来，但是仍然出现了极轻微的一次卡顿。  </p>
<p>暂时放弃，等以后有新的思路再继续排查，或者封装平台原生的流式播放器解决。</p>
<h1 id="u5E73_u53F0_u6253_u5305"><a href="#u5E73_u53F0_u6253_u5305" class="headerlink" title="平台打包"></a>平台打包</h1><h2 id="android__u4E0A_u5076_u73B0_u52A8_u4EBA_u7269_u5361_u6B7B"><a href="#android__u4E0A_u5076_u73B0_u52A8_u4EBA_u7269_u5361_u6B7B" class="headerlink" title="android 上偶现动人物卡死"></a>android 上偶现动人物卡死</h2><p>伴随 Timeout while trying to pause the Unity Engine 报错。  </p>
<p>原因是 android 端代码在某些情况下调用了 unityPlayer 的resume，但是没有调用 onWindowFocusChange，导致 unity 的 view 一直未获取焦点，处于暂停状态。</p>
<h2 id="u6A2A_u7AD6_u5C4F_u5207_u6362_u65F6_crash"><a href="#u6A2A_u7AD6_u5C4F_u5207_u6362_u65F6_crash" class="headerlink" title="横竖屏切换时 crash"></a>横竖屏切换时 crash</h2><p>原因是 activity 重建，再加上 activity 的生命周期未绑定 unityplayer 的 destroy，导致crash。见上篇文章。  </p>
<p>抓到的引擎crash堆栈有好几种：<br><img src="https://zoucz.com/blogimgs/c1ffbdd0-70c3-11ed-9fa0-5dbc93f9d3ee.md/af17fafbc2f3ddcd792bcd740ffcd348.jpg" alt="image.png"><br><img src="https://zoucz.com/blogimgs/66788390-70bc-11ed-9fa0-5dbc93f9d3ee.md/fc600e107f1eb85ed6d43c2e793ca64d.jpg" alt="image.png"><br><img src="https://zoucz.com/blogimgs/c1ffbdd0-70c3-11ed-9fa0-5dbc93f9d3ee.md/0908d9543ba8fcae1f6cfe3251271184.jpg" alt="image.png"></p>
<h2 id="u5076_u73B0_u542F_u52A8_u65F6_crash"><a href="#u5076_u73B0_u542F_u52A8_u65F6_crash" class="headerlink" title="偶现启动时 crash"></a>偶现启动时 crash</h2><p>引擎侧 crash<br><img src="https://zoucz.com/blogimgs/c1ffbdd0-70c3-11ed-9fa0-5dbc93f9d3ee.md/b33e19b7856e93094210cc086d08a60b.jpg" alt="image.png"><br>debug中，估计还是和 destory 未绑定有关</p>
<h2 id="u542F_u52A8_u6210_u529F_u4F46_u662F_u52A8_u753B_u5931_u6548"><a href="#u542F_u52A8_u6210_u529F_u4F46_u662F_u52A8_u753B_u5931_u6548" class="headerlink" title="启动成功但是动画失效"></a>启动成功但是动画失效</h2><p>playerSettings —— stripEngineCode 代码优化开启后会导致classID找不到等问题，参考 <a href="https://www.jianshu.com/p/e7120f025281" target="_blank" rel="external">https://www.jianshu.com/p/e7120f025281</a> 解决，或者关闭。  </p>
<p>比如我遇到的是 <code>Could not produce class with ID 74</code>，去 unity 的 <a href="https://docs.unity3d.com/Manual/ClassIDReference.html" target="_blank" rel="external">ClassIDReference文档</a> 里边一查，确实就是 AnimationClip 类找不到了。<br>在Assets目录下创建 link.xml，内容写<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="title">linker</span>&gt;</span></span><br><span class="line">  <span class="comment">&lt;!--Preserve types and members in an assembly--&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="title">assembly</span> <span class="attribute">fullname</span>=<span class="value">"UnityEngine.AnimationModule"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="title">type</span> <span class="attribute">fullname</span>=<span class="value">"UnityEngine.AnimationClip"</span> <span class="attribute">preserve</span>=<span class="value">"all"</span>/&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="title">assembly</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="title">linker</span>&gt;</span></span><br></pre></td></tr></table></figure></p>
<p>重新导出打包，问题解决。 注意，assembly 名称，可以在 IDE 中点类名进去看头部声明查找到。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>记录音画控制、平台打包过程中遇到的一些坑点，有一部分是 unity 的文档描述不够完善造成的使用错误；一部分是属于神坑级BUG，比较难分析出来，只有网上查经验或者肝unity源码能解决的。   </p>
<p>在此记录，不断更新吧，先简单记一下能回忆起来的部分。</p>
<]]>
    </summary>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/tags/unity3d/"/>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/categories/unity3d/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[unity native crash —— 横竖屏切换导致的crash]]></title>
    <link href="https://www.zoucz.com/blog/2022/11/30/66788390-70bc-11ed-9fa0-5dbc93f9d3ee/"/>
    <id>https://www.zoucz.com/blog/2022/11/30/66788390-70bc-11ed-9fa0-5dbc93f9d3ee/</id>
    <published>2022-11-30T14:36:39.000Z</published>
    <updated>2022-11-30T14:44:23.000Z</updated>
    <content type="html"><![CDATA[<p>写了一个 demo 应用，横竖屏切换时应用必然 crash 在 libunity.so 中。<br>分析原因，看不出任何端倪。<br><img src="https://zoucz.com/blogimgs/66788390-70bc-11ed-9fa0-5dbc93f9d3ee.md/fc600e107f1eb85ed6d43c2e793ca64d.jpg" alt="image.png">  </p>
<p>一通google，受 <a href="https://blog.csdn.net/ximsfei/article/details/40518727" target="_blank" rel="external">https://blog.csdn.net/ximsfei/article/details/40518727</a> 这篇文章启发，有可能是 activity 重建，引起的依赖 activity 存在的 unityplayer 内部的错误。<br>在 AndroidManifest.xml 中加上下面的配置以避免 activity 重建。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">android:configChanges=&#34;mcc|mnc|orientation|screenSize&#34;</span><br></pre></td></tr></table></figure></p>
<p>再次试验，果然不再 crash 了。  </p>
<p>思考，为什么 activity 重建会引起 crash 呢？ 应该跟随生命周期才对啊。  </p>
<p>一通排查，发现 unity 的 destory 没有绑定到 activity 的 destory 生命周期上。<br>给加上：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="annotation">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onDestroy</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>.onDestroy();</span><br><span class="line">    <span class="keyword">if</span>(<span class="keyword">this</span>.vhSDK!=<span class="keyword">null</span>)&#123;</span><br><span class="line">        <span class="keyword">this</span>.vhSDK.destroy();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p>
<p>重试，没有再 crash，即使去掉 <code>android:configChanges</code> 配置也不会再 crash。  </p>
<p>原因很简单，但是从排查问题的过程来看，属于怎么开脑洞也想不出的问题 —— 要么疯狂 google ，要么研究 unity 源码。故此记录一下，以供后人 google。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>写了一个 demo 应用，横竖屏切换时应用必然 crash 在 libunity.so 中。<br>分析原因，看不出任何端倪。<br><img src="https://zoucz.com/blogimgs/66788390-70bc-11ed-9fa0-5dbc93f9d]]>
    </summary>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/tags/unity3d/"/>
    
      <category term="unity3d" scheme="https://www.zoucz.com/blog/categories/unity3d/"/>
    
  </entry>
  
</feed>
