<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Bodhisatan&#39;s blog</title>
  
  
  <link href="https://bodhisatan.github.io/atom.xml" rel="self"/>
  
  <link href="https://bodhisatan.github.io/"/>
  <updated>2021-02-16T07:58:20.206Z</updated>
  <id>https://bodhisatan.github.io/</id>
  
  <author>
    <name>Yao Xianjie</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>图像拼接系统设计与实现（2）：Web 拼接系统设计与实现</title>
    <link href="https://bodhisatan.github.io/2021/02/16/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0%EF%BC%882%EF%BC%89%EF%BC%9AWeb-%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/"/>
    <id>https://bodhisatan.github.io/2021/02/16/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0%EF%BC%882%EF%BC%89%EF%BC%9AWeb-%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/</id>
    <published>2021-02-16T02:22:16.000Z</published>
    <updated>2021-02-16T07:58:20.206Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>之前的博客中，已经介绍了图像拼接主要可以分为4个阶段：<code>特征提取</code>、<code>特征匹配</code>、<code>图像配准</code>、<code>图像融合</code>，其中特征提取阶段有<code>SURF</code>、<code>SIFT</code>、<code>ORB</code>、<code>Harris</code>等算法。于是我写了一个Web系统，用于使用不同的特征提取算法进行图像拼接，并且比较之间的效率和质量。</p><p>项目为前后端分离项目，前端代码仓库地址：<a href="https://github.com/bodhisatan/stitch-frontend" target="_blank" rel="noopener">https://github.com/bodhisatan/stitch-frontend</a> ；后端代码仓库地址：<a href="https://github.com/bodhisatan/stitch-backend" target="_blank" rel="noopener">https://github.com/bodhisatan/stitch-backend</a></p><h2 id="系统设计"><a href="#系统设计" class="headerlink" title="系统设计"></a>系统设计</h2><h3 id="算法选取"><a href="#算法选取" class="headerlink" title="算法选取"></a>算法选取</h3><p>在图像拼接领域中，主要流行的有三种特征提取算法：SIFT、SURF、ORB，这三种都在OpenCV里有实现（SURF因为专利原因已经从OpenCV里下掉），SURF是SIFT的加强版，运算效率比SIFT高一个数量级，ORB基于FAST，运算效率比SIFT高两个数量级，但是鲁棒性不及SIFT和SURF。因此在算法的选取上，选择了OpenCV已经实现的SIFT和ORB，在运算效率和鲁棒性方面都有明显的比较。除此之外，我自己也实现了一个Harris方案，Harris是一种角点匹配方法，对尺度很敏感，不具有尺度不变性，需要先将图片转为灰度图再进行运算。</p><h3 id="比较参数选取"><a href="#比较参数选取" class="headerlink" title="比较参数选取"></a>比较参数选取</h3><p>对于算法的比较，主要从三方面考虑：</p><ul><li>待拼接图像的相似度（前置条件角度）</li><li>输出图像的质量（输出质量角度）</li><li>运行时间（运行效率角度）<h4 id="输入图像相似度"><a href="#输入图像相似度" class="headerlink" title="输入图像相似度"></a>输入图像相似度</h4>有些特征提取、匹配算法要求待匹配图像相似度较高，于是我记录了相似度参数。相似度采用两方面指标：</li><li>三通道直方图相似度: 比较常见的衡量相似度的指标，先得到图像像素值的直方图，再计算直方图的相似度，这种计算方案有个缺陷，它仅反映图像像素值的数量，不能反映图像纹理结构，很明显该方法存在很多误判，比如纹理结构相同，但明暗不同的图像，应该相似度很高，但实际结果是相似度很低，而纹理结构不同，但明暗相近的图像，相似度却很高。</li><li>SSIM: 为了弥补直方图相似度的缺陷，我又采取了SSIM指标。SSIM是结构化相似度，是一种全参考的图像质量评价指标，它分别从亮度、对比度、结构三方面度量图像相似性，能够弥补直方图相似度误判的缺陷。（在之前的博文中也有详细介绍）</li></ul><h4 id="输出图像质量"><a href="#输出图像质量" class="headerlink" title="输出图像质量"></a>输出图像质量</h4><p>拼接的输出图像的品质，一般从有没有裂缝、有没有鬼影、有没有正确拼接等角度，但没有一个具体客观的指标。我看了相关的论文之后，发现有一篇论文通过PSNR指标作为输出图像质量的参考。</p><p>PSNR(Peak Signal to Noise Ratio): 峰值信噪比，主要是为了衡量经过处理后的影像品质，PSNR指标越高，代表输出图像的失真越少，质量越高</p><script type="math/tex; mode=display">PSNR=10*log_{10}(\frac{MAX_I^2}{MSE})</script><script type="math/tex; mode=display">MSE = \frac{1}{mn}\sum_{i=0}^{m-1}\sum_{j=0}^{n-1}[I(i, j)-K(i,j)]^2</script><p>其中，MAXI是表示图像点颜色的最大数值，如果每个采样点用 8 位表示，那么就是 255。</p><h4 id="运行时间"><a href="#运行时间" class="headerlink" title="运行时间"></a>运行时间</h4><p>运行时间记录两个时间：</p><ul><li>特征提取阶段运算耗时（主要比较的算法耗时）</li><li>图像拼接运算总耗时</li></ul><h2 id="系统实现"><a href="#系统实现" class="headerlink" title="系统实现"></a>系统实现</h2><h3 id="技术选型"><a href="#技术选型" class="headerlink" title="技术选型"></a>技术选型</h3><p>前端采用<code>Vue.js</code>和<code>webpack</code>技术实现前后端分离，组件采用<code>elementUI</code>和<code>ECharts</code>，与后端通信采用<code>axios</code>，后端框架从易用性角度考虑采用<code>Flask</code>，数据库从方便扩展的角度考虑采用<code>MongoDB</code></p><h3 id="实现结果"><a href="#实现结果" class="headerlink" title="实现结果"></a>实现结果</h3><p><img src="/2021/02/16/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0%EF%BC%882%EF%BC%89%EF%BC%9AWeb-%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/1.png" alt></p><p><img src="/2021/02/16/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0%EF%BC%882%EF%BC%89%EF%BC%9AWeb-%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/2.png" alt></p><p><img src="/2021/02/16/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0%EF%BC%882%EF%BC%89%EF%BC%9AWeb-%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/3.png" alt></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;之前的博客中，已经介绍了图像拼接主要可以分为4个阶段：&lt;code&gt;特征提取&lt;/code&gt;、&lt;code&gt;特征匹配&lt;/code&gt;、&lt;code&gt;图</summary>
      
    
    
    
    <category term="图像算法" scheme="https://bodhisatan.github.io/categories/%E5%9B%BE%E5%83%8F%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="图像算法" scheme="https://bodhisatan.github.io/tags/%E5%9B%BE%E5%83%8F%E7%AE%97%E6%B3%95/"/>
    
    <category term="图像拼接" scheme="https://bodhisatan.github.io/tags/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5/"/>
    
  </entry>
  
  <entry>
    <title>两种不同的包体积把控思路</title>
    <link href="https://bodhisatan.github.io/2021/02/02/%E4%B8%A4%E7%A7%8D%E4%B8%8D%E5%90%8C%E7%9A%84%E5%8C%85%E4%BD%93%E7%A7%AF%E6%8A%8A%E6%8E%A7%E6%80%9D%E8%B7%AF/"/>
    <id>https://bodhisatan.github.io/2021/02/02/%E4%B8%A4%E7%A7%8D%E4%B8%8D%E5%90%8C%E7%9A%84%E5%8C%85%E4%BD%93%E7%A7%AF%E6%8A%8A%E6%8E%A7%E6%80%9D%E8%B7%AF/</id>
    <published>2021-02-02T07:59:37.000Z</published>
    <updated>2021-02-04T07:42:53.647Z</updated>
    
    <content type="html"><![CDATA[<p>这两天重启了之前搁置的包体积监控治理，看了一些别的组同学监控包体积的方案和思路，有很多收获，在此总结记录一下。这次主要总结一下两种不同的包体积监控思路，一种是基于成品app，一种是基于代码提交，相当于一个是<strong>测试右移</strong>，一个是<strong>测试左移</strong>。</p><h2 id="基于成品app的包体积监控"><a href="#基于成品app的包体积监控" class="headerlink" title="基于成品app的包体积监控"></a>基于成品app的包体积监控</h2><p>所谓基于成品app，也就是app提测甚至是发版之后，对于app的包体积进行监控。这种比较适用于竞品包体积比对这方面。包体积又分为两种，一种是下载大小，一种是安装大小。</p><h3 id="下载大小"><a href="#下载大小" class="headerlink" title="下载大小"></a>下载大小</h3><p>下载大小，也就是用户在应用商店看到的app大小。这个会影响用户的下载意愿，但是，我们在页面上看到的下载大小到底是什么？为什么不同手机不同系统看到的下载大小不一样？</p><p>对于iOS来说，app的下载大小是：</p><ol><li><code>.app</code> 文件（<code>.app</code>也就是<code>.ipa</code>解压之后的产物）</li><li>的二进制部分被<strong>加壳</strong>后</li><li>再经过 <code>app slicing</code>的结果</li></ol><p>加壳对app包体积有影响，但影响不大，关键在于瘦身这一步。我们在给iOS app打包的时候可以通过指定瘦身的版本来模拟<code>app slicing</code>的效果得到数据。</p><p>对于Android来说，如果经过了app bundle，那就类似于iOS的<code>app slicing</code>，对于这种的话，可以通过UI自动化，解析应用商店的下载页面获取下载大小。</p><h3 id="安装大小"><a href="#安装大小" class="headerlink" title="安装大小"></a>安装大小</h3><p>安装大小，就是app解压安装到手机上后占据磁盘的空间。对于iOS来说，可以粗略的认为，ipa解压后的.app文件，大小近似于安装大小。对于Android来说，可以通过在手机里捞取安装包的方式获取安装大小。apk的安装流程如下：</p><ol><li>复制APK到/data/app目录下，解压并扫描安装包。</li><li>资源管理器解析APK里的资源文件。</li><li>解析AndroidManifest文件，并在/data/data/目录下创建对应的应用数据目录。</li><li>然后对dex文件进行优化，并保存在dalvik-cache目录下。</li><li>将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。</li><li>安装完成后，发送广播</li></ol><p>那么，就可以通过root手机，访问/data/app目录下的对应应用文件夹来获取安装大小。</p><h2 id="基于代码提交的包体积监控"><a href="#基于代码提交的包体积监控" class="headerlink" title="基于代码提交的包体积监控"></a>基于代码提交的包体积监控</h2><p>包大小前置检查，是包大小监控中一种重要方式，其做法是对合入代码所产生的包大小增量进行监控。如果采用定时对主分支打包做包大小检查或者提测的时候再做包检查，会产生一些bad case：    </p><ul><li>case 1：在一次检查中我们发现了抖音包体积涨了1M多，能看到是有多个so文件大小变大了，但是不知道具体是由哪个业务线、哪个需求、哪个RD引入的，无法对症下药。       </li><li>case 2：某次提交中C同学引入了一些无用资源会造成包大小不必要的增加，但是代码合进去了才发现问题，后期要进行跟踪和优化都比较困难，C同学对包大小优化的意识也会弱化。       </li><li>case 3：在某个版本开发中，A同学做了直播模块的包大小压缩，优化了900KB，而B同学写了个需求使包大小增加了600KB，如果仅对主分支的包做检查，此类问题将不能暴露出来。</li></ul><p>由此可见，在代码合入之后做检查，带来的问题是等到发现包大小增加超过预期时，回退代码或进行修复往往比较困难，后续的优化工作也没能支持。比较好的做法是，我们在每次代码合入前检查包体积的大小变化。选择在每个MR的粒度上对包大小变化进行监控，目的是更早、更快地发现和定位包大小增加/减少的引入源，同时能够关联到对应的业务线及代码提交者。</p><p>那么就引入了一个很重要的问题：如何选取<strong>基准包</strong>？</p><p>之前的博客里也提到过，我们组采取的方案是，rd合入代码进某分支之后，对这条分支进行打包，打包结果和这条分支的上一次打包结果做比较。这样简单粗暴的选取基准包测试结果不准，偶尔还会引起误报警。学习了抖音的监控方案之后，发现他们基准包节点是选取的<strong>当前提交分支与目标分支最近的一个公共节点</strong>，当监听到代码merge操作之后，会回溯找到这个基准包节点，如果该节点没打过包，则先打基准包再进行比较。</p><p>关于最近的一个公共节点，考虑两种case：一是从develop拉出分支feature/author_name/xxx进行开发，那么拉出来的那个commit节点就是它们最近的公共节点；二是如果在之后的开发中又merge了develop分支，则这个最近的公共节点变为merge时候develop分支上的commit节点。</p><h2 id="附录：iOS对安装包大小的限制"><a href="#附录：iOS对安装包大小的限制" class="headerlink" title="附录：iOS对安装包大小的限制"></a>附录：iOS对安装包大小的限制</h2><p>苹果对于开发者提供的安装包存在2个方面的限制:</p><h3 id="安装包大小限制："><a href="#安装包大小限制：" class="headerlink" title="安装包大小限制："></a>安装包大小限制：</h3><ul><li>安装包总大小限制：完整的.app文件未压缩情况下大小不得超过4GB</li><li>下载大小限制（OTA）：用户在 App Store 可以使用流量下载的大小为200MB</li></ul><p>这个限制简称 OTA 限制 （Cellular Over-the-Air App Download Limit）主要是为了防止用户在下载应用程序时无意间使用大量数据 变更历史:</p><ul><li>2019年6月1日， OTA 限制从150M提高到200M</li><li>2017年9月19日，OTA 限制从100M提高到150M</li><li>2013年9月18日，OTA 限制从50M提高到100M<h3 id="TEXT段大小限制："><a href="#TEXT段大小限制：" class="headerlink" title="_TEXT段大小限制："></a>_TEXT段大小限制：</h3></li></ul><ul><li>低于iOS 7 ：可执行文件所有架构_TEXT段总大小不得超过80M</li><li>iOS 7.x 和iOS 8.X：可执行文件单架构_TEXT段大小不得超过60M</li><li>iOS 9.x 以及更高版本：可执行文件所有架构_TEXT段总大小不得超过500M</li></ul><p>（上边的限制均以 <strong>1000</strong> 为进位单位而不是 1024：<strong>1MB=1000KB</strong>）</p><h3 id="iOS系统的两个口径：Download-Size和Install-Size"><a href="#iOS系统的两个口径：Download-Size和Install-Size" class="headerlink" title="iOS系统的两个口径：Download Size和Install Size"></a>iOS系统的两个口径：Download Size和Install Size</h3><p>在前文说<strong>下载大小</strong>这一概念的时候，关于iOS的部分说的不是很精确，在这里补充说明一下。</p><p>在 itunes connect 后台，开发者可以看到当前版本针对不同机型的大小。<br><img src="/2021/02/02/%E4%B8%A4%E7%A7%8D%E4%B8%8D%E5%90%8C%E7%9A%84%E5%8C%85%E4%BD%93%E7%A7%AF%E6%8A%8A%E6%8E%A7%E6%80%9D%E8%B7%AF/iTunes.png" alt></p><p>这里的大小分为两个口径： <strong>Download Size</strong> 和 <strong>Install Size</strong>。</p><p>根据网页上的说明： Install Size 是这个 app 安装后，会占用的磁盘大小； Download Size 是 app 经过压缩后的大小。</p><p>根据经验，用户在 app store 上看到的大小，就是 itunes connect 后台中显示的 <strong>Install Size</strong>。（注意：<strong>不是</strong>Download Size！！！）</p><p>而令开发者在意的，“超过 200 MB 的 app 必须连接至无线局域网才能下载”的规则中的 200 MB，指的其实是 <strong>Download Size</strong>。</p><p>所以与上文结合可以知道，.ipa文件解压之后的.app文件，大小和install size更为接近。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;这两天重启了之前搁置的包体积监控治理，看了一些别的组同学监控包体积的方案和思路，有很多收获，在此总结记录一下。这次主要总结一下两种不同的包体积监控思路，一种是基于成品app，一种是基于代码提交，相当于一个是&lt;strong&gt;测试右移&lt;/strong&gt;，一个是&lt;strong&gt;测</summary>
      
    
    
    
    <category term="客户端质量" scheme="https://bodhisatan.github.io/categories/%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%B4%A8%E9%87%8F/"/>
    
    
    <category term="Android包体积" scheme="https://bodhisatan.github.io/tags/Android%E5%8C%85%E4%BD%93%E7%A7%AF/"/>
    
  </entry>
  
  <entry>
    <title>JS Arrow Function</title>
    <link href="https://bodhisatan.github.io/2021/01/28/JS-Arrow-Function/"/>
    <id>https://bodhisatan.github.io/2021/01/28/JS-Arrow-Function/</id>
    <published>2021-01-28T12:09:24.000Z</published>
    <updated>2021-01-28T13:40:56.964Z</updated>
    
    <content type="html"><![CDATA[<p>不会前端实在是太不方便了！！！</p><p>不会前端，即使你有想法有逻辑，还是不能实现出一个完整的产品出来，这次借着看组里自动化平台前端源码的机会，入门了一下<code>vue.js</code>，在写demo的时候，碰到了一个<code>=&gt;</code>函数的写法，了解了一下之后发现它类似Java里的lambda表达式，但在js里也有一些细节需要注意，在此记录一下。</p><h2 id="箭头函数的概念"><a href="#箭头函数的概念" class="headerlink" title="箭头函数的概念"></a>箭头函数的概念</h2><p>JS的箭头函数（lambda表达式），其主要意图是定义轻量级的内联回调函数</p><ul><li>回调函数：这个在之前的<a href="https://bodhisatan.github.io/2020/05/02/%E5%85%B3%E4%BA%8E%E9%97%AD%E5%8C%85%E7%9A%84%E7%90%86%E8%A7%A3/">博文</a>里写过</li><li>内联函数：函数调用的过程，是调用栈入栈出栈的过程，这是比较耗时的。内联（<code>inline</code>）也就是内嵌，也就意味着就像C语言<code>#define</code>一样，当编译器发现某段代码在调用一个内联函数时，它不是去调用该函数，而是将该函数的代码，整段<strong>内嵌</strong>到当前位置。这样做的好处是省去了调用的过程，加快程序运行速度，缺点就是会消耗空间。</li></ul><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x =&gt; x * x</span><br></pre></td></tr></table></figure><p>上面的箭头函数相当于：<br><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> (<span class="params">x</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> x * x;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="不同场景箭头函数的用法"><a href="#不同场景箭头函数的用法" class="headerlink" title="不同场景箭头函数的用法"></a>不同场景箭头函数的用法</h2><p>箭头函数简化了函数定义，相当于只需指定一个<strong>参数到结果的映射</strong>。箭头函数有两种格式，一种像上面的，只包含一个表达式，连{ … }和return都省略掉了。还有一种可以包含多条语句，这时候就不能省略{ … }和return：<br><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></pre></td><td class="code"><pre><span class="line">x =&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (x &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> x * x;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> - x * x;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>如果参数不是一个，就需要用括号()括起来：<br><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 两个参数:</span></span><br><span class="line">(x, y) =&gt; x * x + y * y</span><br><span class="line"></span><br><span class="line"><span class="comment">// 无参数:</span></span><br><span class="line">() =&gt; <span class="number">3.14</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 可变参数:</span></span><br><span class="line">(x, y, ...rest) =&gt; &#123;</span><br><span class="line">    <span class="keyword">var</span> i, sum = x + y;</span><br><span class="line">    <span class="keyword">for</span> (i=<span class="number">0</span>; i&lt;rest.length; i++) &#123;</span><br><span class="line">        sum += rest[i];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> sum;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>如果要返回一个对象，因为和函数体的{ … }有语法冲突，所以要改为：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x =&gt; (&#123; <span class="attr">foo</span>: x &#125;)</span><br></pre></td></tr></table></figure></p><h2 id="JS中箭头函数与匿名函数的区别（this）"><a href="#JS中箭头函数与匿名函数的区别（this）" class="headerlink" title="JS中箭头函数与匿名函数的区别（this）"></a>JS中箭头函数与匿名函数的区别（this）</h2><p>在之前讲闭包的博文中，也提到了匿名函数的概念，JS中箭头函数与匿名函数最大的区别是箭头函数内部的this是词法作用域（<code>lexically scoped</code>），由上下文确定。个人感觉这里的<strong>词法作用域</strong>，和闭包里<strong>引用环境</strong>的概念非常像。</p><p>在匿名函数里，由于JavaScript函数对this绑定的错误处理，下面的例子无法得到预期结果，需要用点hack的方法，比如<code>let that = this</code>：<br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> obj = &#123;</span><br><span class="line">    birth: <span class="number">1990</span>,</span><br><span class="line">    getAge: <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> b = <span class="keyword">this</span>.birth; <span class="comment">// 1990</span></span><br><span class="line">        <span class="keyword">var</span> fn = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Date</span>().getFullYear() - <span class="keyword">this</span>.birth; <span class="comment">// this指向window或undefined</span></span><br><span class="line">        &#125;;</span><br><span class="line">        <span class="keyword">return</span> fn();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></p><p>现在，箭头函数完全修复了this的指向，this总是指向词法作用域，也就是外层调用者obj：<br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> obj = &#123;</span><br><span class="line">    birth: <span class="number">1990</span>,</span><br><span class="line">    getAge: <span class="function"><span class="keyword">function</span> (<span class="params">year</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> b = <span class="keyword">this</span>.birth; <span class="comment">// 1990</span></span><br><span class="line">        <span class="keyword">var</span> fn = <span class="function">(<span class="params">y</span>) =&gt;</span> y - <span class="keyword">this</span>.birth; <span class="comment">// this.birth仍是1990</span></span><br><span class="line">        <span class="keyword">return</span> fn.call(&#123;<span class="attr">birth</span>:<span class="number">2000</span>&#125;, year);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line">obj.getAge(<span class="number">2015</span>); <span class="comment">// 25</span></span><br></pre></td></tr></table></figure></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;不会前端实在是太不方便了！！！&lt;/p&gt;
&lt;p&gt;不会前端，即使你有想法有逻辑，还是不能实现出一个完整的产品出来，这次借着看组里自动化平台前端源码的机会，入门了一下&lt;code&gt;vue.js&lt;/code&gt;，在写demo的时候，碰到了一个&lt;code&gt;=&amp;gt;&lt;/code&gt;函数的写</summary>
      
    
    
    
    <category term="编程语言" scheme="https://bodhisatan.github.io/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
    
    <category term="javascript" scheme="https://bodhisatan.github.io/tags/javascript/"/>
    
    <category term="闭包" scheme="https://bodhisatan.github.io/tags/%E9%97%AD%E5%8C%85/"/>
    
  </entry>
  
  <entry>
    <title>关于客户端自动化测试的思考和经验总结</title>
    <link href="https://bodhisatan.github.io/2021/01/22/%E5%85%B3%E4%BA%8E%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95%E7%9A%84%E6%80%9D%E8%80%83%E5%92%8C%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
    <id>https://bodhisatan.github.io/2021/01/22/%E5%85%B3%E4%BA%8E%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95%E7%9A%84%E6%80%9D%E8%80%83%E5%92%8C%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/</id>
    <published>2021-01-22T09:01:44.000Z</published>
    <updated>2021-02-07T15:19:26.347Z</updated>
    
    <content type="html"><![CDATA[<p>2020年的1月15号，我入职了百度，成了一名测试开发实习生，到现在为止，也已经有整整一年的时间了。</p><p>这一年发生了很多事，因为疫情原因在出租屋里吃了一个月的泡面，因为不返校而得以一直实习，先是在百度，然后是在字节，还认识了一个我坚定地觉得可以相伴一生的人。一年前的我只有后端开发的经验，对测试领域一无所知，误打误撞进了客户端测试的领域，并且一直做了下来，经历了很多，也算是有所思考和沉淀吧。下面就谈一谈我经过一年的工作，对于客户端自动化测试手段的思考。</p><h2 id="UI自动化"><a href="#UI自动化" class="headerlink" title="UI自动化"></a>UI自动化</h2><p><strong>UI自动化</strong>，这几乎是每一个客户端方向的测试工程师在学习或者团队内发展自动化测试的时候的<u>第一个方向</u>。大家提到自动化测试，一般而言，如果是客户端领域，指的就是UI自动化。在我刚入职百度的时候，我的方向就是双端的UI自动化。</p><h3 id="什么是UI自动化"><a href="#什么是UI自动化" class="headerlink" title="什么是UI自动化"></a>什么是UI自动化</h3><p>UI自动化，就是将用户的行为通过“自动化测试框架”进行模拟，例如模拟划动、模拟点击，将一些测试场景抽象出来进行自动化测试，以此达到节约人力的作用。目前市面上的自动化测试框架非常多，例如<code>appium</code>、<code>totoro</code>、<code>QTA</code>、<code>uia</code>、<code>uia2</code>等。一些大厂基于不同的侧重点也会进行自己的自动化框架的研发，比如<a href="https://mp.weixin.qq.com/s?__biz=MjM5MDI3MjA5MQ==&amp;mid=2697270030&amp;idx=1&amp;sn=6be0a2c1a1671e66db89a725ad6af4ce&amp;chksm=8376ec3ab401652cb8c3e21fdb0a3c70825af2820233be19b7e218be09c84394db5ac385ea6e&amp;scene=27#wechat_redirect" target="_blank" rel="noopener">携程基于行为驱动（BDD）的自动化测试</a>，网易提出的用图片就能编写case的<code>Airtest</code>，腾讯从稳定性和多端支持角度提出的<code>QTA</code>等。不同的框架出发点不一样，有的是为了稳定性，有的是为了编写时上手的简单性，有的是为了可扩展性，但是框架的原理大同小异，可以认为一个完整的UI自动化框架一般由两部分组成：<strong>UI驱动</strong>和<strong>设备驱动</strong>。</p><p>UI驱动用来执行对元素的操作，比如查找元素、点击元素，设备驱动用来执行对设备的操作，比如冷启app、关机等。设备驱动的话安卓端的开源方案有<code>adb</code>，iOS端的开源方案有facebook的<code>wda</code>和<code>idb</code>，一些公司也会进行自己的设备驱动的开发，比如字节自研的<code>BDC</code>。UI驱动的话，主要思路有两种，一种是注入式，一种是非注入式。像上手最简单的<code>Appium</code>采用的就是非注入式方案，查找和操作UI是从App进程外对App进行操作的。非注入式的UI驱动有：安卓端的<code>UIA</code>，iOS端的<code>Instrumentation</code>、<code>XCUITest</code>等。腾讯的<code>QTA</code>和字节的<code>Shoots</code>采用的就是注入式方案，对app进行重打包，往包里注入一个server，在app启动的时候，启动一个对应的网络服务，通过这个网络服务提供测试接口。</p><p>因此，从UI驱动这个角度出发，可以将自动化框架分成两大类：注入式、非注入式，那么这两种各有什么优劣势呢？最大的不同点在于<strong>稳定性</strong>和<strong>性能</strong>：</p><ul><li>稳定性：记得刚用Appium的时候，测试机是一台vivo低端机，case编写好之后运行的时候，稳定性非常差，首先是定位元素很慢（这和设备性能也有关系），然后是经常跑着跑着，从一个case开始，后面的全部case都失败了，然后查了日志才发现，vivo魔改OS，把耗电高的进程——也就是测试服务的进程给杀了。但是如果UI驱动是注入式的，那么测试服务和App本身就会“同生共死”，只要App进程不死，测试进程也不会死，同时注入式的UI驱动也会极大增大元素定位的速度</li><li>性能：因为测试服务是注入进App进程里的，所以或多或少都会影响App本身运行的速度，所以做性能评测时，要么保证框架对性能的影响有限，要么采用非注入式框架</li></ul><h3 id="UI自动化的用处"><a href="#UI自动化的用处" class="headerlink" title="UI自动化的用处"></a>UI自动化的用处</h3><p>UI自动化不光光是UI自动化，可以以UI自动化为入手点，将客户端测试的很多方面通过自动化落地，比如埋点自动化测试、性能自动化测试等。</p><ul><li><p>功能回归测试：这是UI自动化最基础的用途，因为一般新功能的话手工回归更稳妥，所以重复性高的回归case采用自动化测试更为合适，可以将功能回归测试放在持续集成流程里，每出一个新包/每监听到一个新push就执行一遍，保证新增代码不影响旧功能或者核心功能</p></li><li><p>埋点测试：埋点上报的本质是发送http请求，埋点测试也是进入到某个App场景后进行埋点数据校验，那么可以利用一些技术手段，劫持App发送的埋点数据，然后通过json schema进行自动化校验。json schema可以自己编写，也可以在录制case的时候进行生成</p></li><li><p>性能测试：有时候需要把自己产品的一些核心场景与竞品横向对比，做性能评测。性能主要有以下几个指标：耗时、cpu、fps等。如果不用自动化手段，需要手动录屏，然后手动拆帧获得耗时数据，安卓端通过adb命令获得cpu、fps等数据，iOS则更为复杂。用自动化可以实现自动录屏，自动分帧，自动选取开始帧结束帧计算耗时，还可以集成一些工具，自动获取cpu、fps等信息。</p></li><li><p>服务端接口防劣化：假如有这样一个场景——一个服务端接口因为种种原因挂了，导致线上功能异常或者页面白屏，此时一般只能通过用户反馈或者服务端报警才能知道接口出了问题，这个通过UI自动化也可以无人值守的监控：每隔一定时间跑一遍核心接口相关的UI自动化，同时用一些cv工具对页面进行白屏检测，那么如果出现问题，就能及时报警</p></li></ul><h3 id="UI自动化的挑战"><a href="#UI自动化的挑战" class="headerlink" title="UI自动化的挑战"></a>UI自动化的挑战</h3><p>UI自动化是否合理，在每一个测试团队推行UI自动化的时候都会遇到这样的争议。团队leader会觉得，自动化收益不明显、反而更耗费人力，业务测试peer会觉得，这是给自己工作增负，而且落地的好也只不过是给他人做嫁衣。这些矛盾的关键点在于：UI自动化能否真正给团队带来效益？</p><p>UI自动化其实难度很大，有一张很经典的三角形示意图，最底层面积最大的是<code>Unit Test</code>，cost最小，收益最大，最顶端面积最小的就是<code>UI Test</code>，cost最大，收益最小。做UI自动化，不光要求对框架的原理、app的代码都有所了解，还要求对产品和业务非常了解。不清楚自动化框架的原理，case出了问题无法排查。UI自动化本质上也算是一种白盒测试，不了解客户端的知识也很难下手，比如不清楚iOS的证书体系，就无法为app注入代码执行case，不了解安卓UI，就会不理解为什么QTA对UI层的抽象设计。假设这些都了解了，也需要很熟悉产品业务。例如哪些场景有ab test，ab test如何解决，如何尽量减少对线上数据的影响等。同时，如何让编写的case稳定性更好、维护性更好也很有学问，避免使用坐标点击、考虑不同分辨率设备的UI界面、将case提取出公共场景解耦等等…</p><p>这些是对开发者的要求，从对框架的要求来看，什么算是“优秀的自动化框架”呢？我认为主要有以下几点：</p><ul><li>稳定性：尽量不出现因为框架不稳定导致case失败的情况</li><li>易维护性：将case提取出一步一步的路径节点进行解耦复用</li><li>易扩展性：例如通过sdk或者注入得到一个统一格式的ui tree，框架层进行元素查找，再调用对应driver进行操作，不管以后接入什么终端，只要提供sdk和driver，框架层不需要作出任何修改</li><li>易编写性：我个人看法是“去IDE化”，有些框架会提供本地IDE给开发者编写case，但是环境的包袱太重，可以采用云IDE，在云端利用云真机编写/录制生成case，也不需要开发者在本地配置各种冗杂的环境，同时利用mapping文件云端解混淆，也可以做到对混淆无感知</li><li>可分布式执行、并发执行：可以将case打到设备集群上并发执行，减少测试时长，也能增大机型/系统覆盖率</li></ul><h2 id="UI自动化2-0"><a href="#UI自动化2-0" class="headerlink" title="UI自动化2.0"></a>UI自动化2.0</h2><p>从百度一路走来，在UI自动化方面踩了很多坑，刚写case的时候，没有注意case的解耦、复用，导致维护起来非常消耗人力，于是后来进字节之后，将case提取出公共场景进行复用，比如评论和买车都需要登录，就把登录场景单独拎出来给评论case、买车case复用，这样如果以后登录case页面做出改动，也只要修改一个case就行。字节自研了一个自动化框架，叫“Shoots”，采用<code>PageObject</code>设计模式，框架设计和稳定性都很不错。但是编写这个case的上手门槛非常高，要求QA很熟悉python和客户端环境，虽然它提供了一个本地IDE，但是case还是需要自己新建类自己写，非常不适合代码能力没那么高的业务同学用，于是我和师兄开发了一个测试平台，设计了一套类似uia2的脚本语法，将生成的脚本用jinjia模版翻译成Shoots的代码，这样就曲线救国实现了case的自动生成。云真机的前端渲染是开发app的SDK提供截图+ui树（Native控件使用原生代码获取ui树，webview界面通过向webview注入js获取ui树），发给服务端生成预览。</p><p>UI自动化的2.0，意味着要放宽思路，很多技术都可以应用于UI自动化中。例如通过hook，对一些没有id的控件自动生成id（<a href="https://github.com/yulingtianxia/TBUIAutoTest" target="_blank" rel="noopener">https://github.com/yulingtianxia/TBUIAutoTest</a> ），降低元素定位难度。例如将一些cv算法用于UI自动化测试过程中判断界面是否存在花屏/白屏，图像对比、模版匹配算法对比两幅图像是否一致，可以用于性能测试时获取开始帧、结束帧，也可以判断ui自动化测试的结果是否符合预期。还可以结合<code>fuzz test</code>，利用UI自动化访问特定接口进行接口健壮性测试。还可以从很多角度优化自动化脚本编写的体验和效率，比如<code>元素id变了之后自动修复脚本</code>、<code>提供历史截图和控件树实现不需要连手机就能编写case</code>。</p><h2 id="客户端自动化的未来：AI-Test"><a href="#客户端自动化的未来：AI-Test" class="headerlink" title="客户端自动化的未来：AI Test"></a>客户端自动化的未来：AI Test</h2><p>现在很多公司都在AI Test方面进行探索，客户端的话主要是自动测试生成（<code>Automated Testing Generation</code>）技术，编写case不再需要工程师，而是可以自动生成和维护case，目前在稳定性测试方面落地得比较好（<a href="https://github.com/bytedance/Fastbot_Android" target="_blank" rel="noopener">https://github.com/bytedance/Fastbot_Android</a> ）。case生成主要有两种思路，一种是提取出界面的控件，对Activity/ViewController进行bfs/dfs/A*搜索，还有一种是利用线上用户的行为提取行为链进行case生成。我在我们组负责过一个AI Test工具的落地，crash召回率有40%左右，收益还是挺不错的。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;2020年的1月15号，我入职了百度，成了一名测试开发实习生，到现在为止，也已经有整整一年的时间了。&lt;/p&gt;
&lt;p&gt;这一年发生了很多事，因为疫情原因在出租屋里吃了一个月的泡面，因为不返校而得以一直实习，先是在百度，然后是在字节，还认识了一个我坚定地觉得可以相伴一生的人。一年</summary>
      
    
    
    
    <category term="客户端质量" scheme="https://bodhisatan.github.io/categories/%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%B4%A8%E9%87%8F/"/>
    
    
    <category term="自动化测试" scheme="https://bodhisatan.github.io/tags/%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95/"/>
    
    <category term="UI自动化" scheme="https://bodhisatan.github.io/tags/UI%E8%87%AA%E5%8A%A8%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>搞点新花样——飞书签名自动更新</title>
    <link href="https://bodhisatan.github.io/2021/01/21/%E6%90%9E%E7%82%B9%E6%96%B0%E8%8A%B1%E6%A0%B7%E2%80%94%E2%80%94%E9%A3%9E%E4%B9%A6%E7%AD%BE%E5%90%8D%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0/"/>
    <id>https://bodhisatan.github.io/2021/01/21/%E6%90%9E%E7%82%B9%E6%96%B0%E8%8A%B1%E6%A0%B7%E2%80%94%E2%80%94%E9%A3%9E%E4%B9%A6%E7%AD%BE%E5%90%8D%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0/</id>
    <published>2021-01-21T04:05:48.000Z</published>
    <updated>2021-01-22T08:19:30.347Z</updated>
    
    <content type="html"><![CDATA[<p>前两天，看见公司有一些同学利用一些技术手段动态更新飞书签名，有人实时更新微博热搜，有人春节放假倒计时，还是挺有趣的，正巧北京这几天疫情比较紧张，我就也写了一个服务在飞书签名上定时更新北京的风险地区。</p><p>梳理了一下步骤流程，首先获取到飞书的cookie，然后在字节的“轻服务”平台上线一个服务，用来利用cookie更新飞书签名，接着在开发机部署一个定时任务，定时在丁香园爬取北京的风险地区，然后将数据post到刚刚上线的轻服务上，数据打过去之后，轻服务就能更新签名了。至于为什么用轻服务而不在开发机走完整个流程，因为cookie里的信息比较重要，放在开发机不够安全。</p><h2 id="获取cookie"><a href="#获取cookie" class="headerlink" title="获取cookie"></a>获取cookie</h2><p>登录飞书网页版，对任意飞书api请求均可，复制cookie</p><p><img src="/2021/01/21/%E6%90%9E%E7%82%B9%E6%96%B0%E8%8A%B1%E6%A0%B7%E2%80%94%E2%80%94%E9%A3%9E%E4%B9%A6%E7%AD%BE%E5%90%8D%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0/cookie.png" alt="cookie"></p><h2 id="在轻服务平台创建轻函数并上线"><a href="#在轻服务平台创建轻函数并上线" class="headerlink" title="在轻服务平台创建轻函数并上线"></a>在轻服务平台创建轻函数并上线</h2><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></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="keyword">async</span> <span class="function"><span class="keyword">function</span>(<span class="params">params, context</span>) </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> request = axios.create(&#123;</span><br><span class="line">    baseURL: <span class="string">'https://internal-api-lark-api.feishu.cn'</span>,</span><br><span class="line">    headers: &#123;</span><br><span class="line">      <span class="string">'content-type'</span>: <span class="string">'text/plain;charset=UTF-8'</span>,</span><br><span class="line">      <span class="string">'cookie'</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">await</span> request.put(<span class="string">'/passport/users/details/'</span>,</span><br><span class="line">    &#123;<span class="string">"description"</span>: params.data, <span class="string">"descriptionType"</span>: <span class="number">0</span>&#125; )</span><br><span class="line">  .then(<span class="function"><span class="params">res</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(res.data)</span><br><span class="line">  &#125;)</span><br><span class="line">  .catch(<span class="function"><span class="params">error</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(error)</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="在开发机部署定时爬虫任务"><a href="#在开发机部署定时爬虫任务" class="headerlink" title="在开发机部署定时爬虫任务"></a>在开发机部署定时爬虫任务</h2><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><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="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> re</span><br><span class="line"><span class="keyword">import</span> traceback</span><br><span class="line"><span class="keyword">import</span> requests</span><br><span class="line"><span class="keyword">import</span> datetime</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">crawl_dxy_data</span><span class="params">()</span>:</span></span><br><span class="line">    <span class="string">"""爬取丁香园实时统计数据，发送到字节轻服务，更新lark签名</span></span><br><span class="line"><span class="string">    """</span></span><br><span class="line">    response = requests.get(<span class="string">'https://ncov.dxy.cn/ncovh5/view/pneumonia'</span>)  <span class="comment"># 发送get请求</span></span><br><span class="line">    print(response.status_code)  <span class="comment"># 打印状态码</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        today = datetime.datetime.now().strftime(<span class="string">'%Y-%m-%d %H:%M'</span>)</span><br><span class="line">        url_text = response.content.decode()  <span class="comment"># 获取响应的html页面</span></span><br><span class="line">        <span class="comment"># re.search()用于扫描字符串以查找正则表达式模式产生匹配项的第一个位置，然后返回相应的match对象</span></span><br><span class="line">        <span class="comment"># 在字符串a中，包含换行符\n，这种情况下：如果不适用re.S参数，则只在每一行内进行匹配，如果一行没有，就换下一行重新开始匹配</span></span><br><span class="line">        url_content = re.search(<span class="string">r'window.getAreaStat = (.*?)&#125;]&#125;catch'</span>,</span><br><span class="line">                                url_text, re.S)</span><br><span class="line">        texts = url_content.group()  <span class="comment"># 获取匹配正则表达式的整体结果</span></span><br><span class="line">        content = texts.replace(<span class="string">'window.getAreaStat = '</span>, <span class="string">''</span>).replace(<span class="string">'&#125;catch'</span>, <span class="string">''</span>)  <span class="comment"># 去除多余字符</span></span><br><span class="line">        json_data = json.loads(content)</span><br><span class="line">        string_danger_areas = <span class="string">"【"</span> + today + <span class="string">"自动更新北京疫情风险地区】"</span></span><br><span class="line">        num_of_high = <span class="number">0</span></span><br><span class="line">        num_of_mid = <span class="number">0</span></span><br><span class="line">        danger_areas = []</span><br><span class="line">        <span class="keyword">for</span> _json <span class="keyword">in</span> json_data:</span><br><span class="line">            <span class="keyword">if</span> _json[<span class="string">"provinceShortName"</span>] == <span class="string">"北京"</span>:</span><br><span class="line">                danger_areas = _json[<span class="string">"dangerAreas"</span>]</span><br><span class="line">                <span class="keyword">break</span></span><br><span class="line">        <span class="keyword">for</span> danger_area <span class="keyword">in</span> danger_areas:</span><br><span class="line">            <span class="keyword">if</span> danger_area[<span class="string">'dangerLevel'</span>] == <span class="number">1</span>:</span><br><span class="line">                num_of_high += <span class="number">1</span></span><br><span class="line">            <span class="keyword">elif</span> danger_area[<span class="string">'dangerLevel'</span>] == <span class="number">2</span>:</span><br><span class="line">                num_of_mid += <span class="number">1</span></span><br><span class="line">        <span class="keyword">if</span> num_of_high &gt; <span class="number">0</span>:</span><br><span class="line">            string_danger_areas += <span class="string">"高风险："</span></span><br><span class="line">            <span class="keyword">for</span> danger_area <span class="keyword">in</span> danger_areas:</span><br><span class="line">                <span class="keyword">if</span> danger_area[<span class="string">'dangerLevel'</span>] == <span class="number">1</span>:</span><br><span class="line">                    string_danger_areas += danger_area[<span class="string">'cityName'</span>] + danger_area[<span class="string">'areaName'</span>] + <span class="string">";"</span></span><br><span class="line">        <span class="keyword">if</span> num_of_mid &gt; <span class="number">0</span>:</span><br><span class="line">            string_danger_areas += <span class="string">"中风险："</span></span><br><span class="line">            <span class="keyword">for</span> danger_area <span class="keyword">in</span> danger_areas:</span><br><span class="line">                <span class="keyword">if</span> danger_area[<span class="string">'dangerLevel'</span>] == <span class="number">2</span>:</span><br><span class="line">                    string_danger_areas += danger_area[<span class="string">'cityName'</span>] + danger_area[<span class="string">'areaName'</span>] + <span class="string">";"</span></span><br><span class="line"></span><br><span class="line">        url = <span class="string">"轻服务url"</span></span><br><span class="line">        post_data = &#123;<span class="string">"data"</span>: string_danger_areas&#125;</span><br><span class="line">        <span class="comment"># 字符串格式</span></span><br><span class="line">        res = requests.post(url=url, json=post_data)</span><br><span class="line">        print(res.status_code)</span><br><span class="line">        print(string_danger_areas)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">except</span>:</span><br><span class="line">        print(traceback.format_exc())</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line">    <span class="keyword">while</span> (<span class="literal">True</span>):</span><br><span class="line">        crawl_dxy_data()</span><br><span class="line">        time.sleep(<span class="number">300</span>)</span><br></pre></td></tr></table></figure><p>在后台不挂起执行<br><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash">nohup python3 crawl.py &amp;</span></span><br></pre></td></tr></table></figure></p><h2 id="最终效果"><a href="#最终效果" class="headerlink" title="最终效果"></a>最终效果</h2><p><img src="/2021/01/21/%E6%90%9E%E7%82%B9%E6%96%B0%E8%8A%B1%E6%A0%B7%E2%80%94%E2%80%94%E9%A3%9E%E4%B9%A6%E7%AD%BE%E5%90%8D%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0/jieguo.png" alt></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;前两天，看见公司有一些同学利用一些技术手段动态更新飞书签名，有人实时更新微博热搜，有人春节放假倒计时，还是挺有趣的，正巧北京这几天疫情比较紧张，我就也写了一个服务在飞书签名上定时更新北京的风险地区。&lt;/p&gt;
&lt;p&gt;梳理了一下步骤流程，首先获取到飞书的cookie，然后在字节</summary>
      
    
    
    
    
    <category term="杂项" scheme="https://bodhisatan.github.io/tags/%E6%9D%82%E9%A1%B9/"/>
    
  </entry>
  
  <entry>
    <title>图像拼接系统设计与实现（1）：流程与传统算法</title>
    <link href="https://bodhisatan.github.io/2021/01/11/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0%EF%BC%881%EF%BC%89%EF%BC%9A%E6%B5%81%E7%A8%8B%E4%B8%8E%E4%BC%A0%E7%BB%9F%E7%AE%97%E6%B3%95/"/>
    <id>https://bodhisatan.github.io/2021/01/11/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0%EF%BC%881%EF%BC%89%EF%BC%9A%E6%B5%81%E7%A8%8B%E4%B8%8E%E4%BC%A0%E7%BB%9F%E7%AE%97%E6%B3%95/</id>
    <published>2021-01-11T06:22:15.000Z</published>
    <updated>2021-01-11T09:07:07.815Z</updated>
    
    <content type="html"><![CDATA[<h2 id="图像拼接"><a href="#图像拼接" class="headerlink" title="图像拼接"></a>图像拼接</h2><p>图像拼接是指将拍摄到的具有重叠区域的的若干图像拼接成一张无缝全景图, 以获得更大的视角和分辨率的图像。例如：</p><p>输入下面一组有重叠区域的图片：</p><p><img src="/2021/01/11/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0%EF%BC%881%EF%BC%89%EF%BC%9A%E6%B5%81%E7%A8%8B%E4%B8%8E%E4%BC%A0%E7%BB%9F%E7%AE%97%E6%B3%95/input.jpg" alt="输入"></p><p>得到下面这张拼接大图：</p><p><img src="/2021/01/11/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0%EF%BC%881%EF%BC%89%EF%BC%9A%E6%B5%81%E7%A8%8B%E4%B8%8E%E4%BC%A0%E7%BB%9F%E7%AE%97%E6%B3%95/output.jpg" alt="输出"></p><h2 id="图像拼接流程和涉及的算法"><a href="#图像拼接流程和涉及的算法" class="headerlink" title="图像拼接流程和涉及的算法"></a>图像拼接流程和涉及的算法</h2><p><img src="/2021/01/11/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0%EF%BC%881%EF%BC%89%EF%BC%9A%E6%B5%81%E7%A8%8B%E4%B8%8E%E4%BC%A0%E7%BB%9F%E7%AE%97%E6%B3%95/liucheng.png" alt="图像拼接"></p><p>图像拼接的完整流程如上所示，首先对输入图像提取鲁棒的特征点，并根据特征描述子完成特征点的匹配，然后根据已经匹配的特征点对得到相邻图像的位置关系从而进行图像配准，由于直接进行图像配准会破坏视场的一致性，因而一般先将图像投影在球面或者柱面上，最后计算相邻图像的拼缝并完成重叠区域的融合，得到最终的全景图像。</p><h3 id="第一步：图像特征点提取"><a href="#第一步：图像特征点提取" class="headerlink" title="第一步：图像特征点提取"></a>第一步：图像特征点提取</h3><p>特征点指的是图像灰度值发生剧烈变化的点或者在图像边缘上曲率较大的点，用直白的话来说就是指，从不同的角度对同一个场景进行拍照，在每一幅照片中都能鲁棒的提取的图像的点。一个好的特征点提取算法需要具有以下的特征：数量多，在不同场景下都能提取得到足够数量的特征点；独特性好，从而便于对特征点进行匹配；抗旋转，抗亮度变化，抗尺度缩放等。特征是要匹配的两个输入图像中的元素，它们是在图像块的内部，这些图像块是图像中的像素组，对输入图像进行Patch匹配。目前比较流行的特征检索算法有：</p><ul><li><strong>Harris</strong>：检测角点</li><li><strong>SIFT</strong>（Scale Invariant Feature Transform ）：检测斑点</li><li><strong>SURF</strong>（Speeded Up Robust Features）：检测斑点</li><li><strong>FAST</strong>（Features from Accelerated Segments Test）：检测角点</li><li><strong>BRIEF</strong>（Binary Robust Independent Elementary Features）：检测斑点</li><li><strong>ORB</strong>（Oriented Fast and Rotated Brief）：带方向的FAST算法和具有旋转不变性的BRIEF算法<h3 id="第二步：图像特征点匹配"><a href="#第二步：图像特征点匹配" class="headerlink" title="第二步：图像特征点匹配"></a>第二步：图像特征点匹配</h3>在特征点被检索出来之后，提取出其特征描述子，然后用<strong>SSD</strong>、<strong>NCC</strong>、<strong>BBF</strong>、<strong>KNN</strong>或<strong>brute-force</strong>等算法对不同图片中提取到的相同的特征点进行特征匹配。<h3 id="第三步：图像配准"><a href="#第三步：图像配准" class="headerlink" title="第三步：图像配准"></a>第三步：图像配准</h3>在得到了匹配对之后，需要根据这些匹配对得到图像的相对位置，从而把多幅图像融合成为一幅图像，该步骤的计算思路是计算两幅图像的单应性矩阵（<strong>homography matrix</strong>），从而得到一幅图像相对于另一幅图像的位置。而匹配点对是有噪声的，需要对匹配点对进行筛选，在这一步，用到的筛选算法有<strong>Lowe’s</strong>算法或<strong>RANSAC</strong>算法。得到初始单应矩阵之后，也可以运用<strong>L-M</strong>算法对初始单应矩阵加以改进,获得准确性较高的单应矩阵,从而实现图像的准确配准。<h3 id="第四步：图像融合"><a href="#第四步：图像融合" class="headerlink" title="第四步：图像融合"></a>第四步：图像融合</h3>在最后图像融合阶段，也可以进行寻找最佳拼接缝（常见方法：逐点法，动态规划法和图割法）和融合处理（常见算法：羽化融合和拉普拉斯融合），消除拼接图像中的“裂缝”和“鬼影”。</li></ul><h2 id="拼接系统拟实现方案"><a href="#拼接系统拟实现方案" class="headerlink" title="拼接系统拟实现方案"></a>拼接系统拟实现方案</h2><ol><li><p>特征提取阶段改进</p><p>在图像特征点提取配准阶段，传统算法存在<em>运算时间长</em>、<em>对输入图像相似度要求高</em>的问题，打算采用深度学习提取ROI的方式提取图像特征，这样降低了图像配准的时间，且使得图像的变换更符合所关注的区域。</p></li><li><p>在拼接系统实现算法间的对比</p><ul><li>采用B/S架构，前端使用vue.js，后端使用Python为主语言</li><li>图片上传后，先使用SSIM算法计算图像相似度</li><li>将自己实现的特征提取方案，与SIFT、SURF、ORB、Harris等传统算法进行运行效率对比、拼接质量对比，拼接质量采用PSNR指标</li><li>对比数据使用MongoDB数据库进行落库</li></ul></li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;图像拼接&quot;&gt;&lt;a href=&quot;#图像拼接&quot; class=&quot;headerlink&quot; title=&quot;图像拼接&quot;&gt;&lt;/a&gt;图像拼接&lt;/h2&gt;&lt;p&gt;图像拼接是指将拍摄到的具有重叠区域的的若干图像拼接成一张无缝全景图, 以获得更大的视角和分辨率的图像。例如：&lt;/p&gt;
&lt;p&gt;</summary>
      
    
    
    
    <category term="图像算法" scheme="https://bodhisatan.github.io/categories/%E5%9B%BE%E5%83%8F%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="图像算法" scheme="https://bodhisatan.github.io/tags/%E5%9B%BE%E5%83%8F%E7%AE%97%E6%B3%95/"/>
    
    <category term="图像拼接" scheme="https://bodhisatan.github.io/tags/%E5%9B%BE%E5%83%8F%E6%8B%BC%E6%8E%A5/"/>
    
  </entry>
  
  <entry>
    <title>Matrix-ApkChecker：微信团队的安卓包体积监控方案学习</title>
    <link href="https://bodhisatan.github.io/2021/01/07/Matrix-ApkChecker%EF%BC%9A%E5%BE%AE%E4%BF%A1%E5%9B%A2%E9%98%9F%E7%9A%84%E5%AE%89%E5%8D%93%E5%8C%85%E4%BD%93%E7%A7%AF%E7%9B%91%E6%8E%A7%E6%96%B9%E6%A1%88%E5%AD%A6%E4%B9%A0/"/>
    <id>https://bodhisatan.github.io/2021/01/07/Matrix-ApkChecker%EF%BC%9A%E5%BE%AE%E4%BF%A1%E5%9B%A2%E9%98%9F%E7%9A%84%E5%AE%89%E5%8D%93%E5%8C%85%E4%BD%93%E7%A7%AF%E7%9B%91%E6%8E%A7%E6%96%B9%E6%A1%88%E5%AD%A6%E4%B9%A0/</id>
    <published>2021-01-07T06:21:49.000Z</published>
    <updated>2021-01-07T15:50:49.537Z</updated>
    
    <content type="html"><![CDATA[<p><strong>Matrix</strong> 是微信终端自研和正在使用的一套 APM（应用性能管理）系统。git地址：<a href="https://github.com/Tencent/matrix" target="_blank" rel="noopener">https://github.com/Tencent/matrix</a></p><p><strong>Matrix-ApkChecker</strong> 作为 Matrix 系统的一部分，是针对 android 安装包的分析检测工具，根据一系列设定好的规则检测 apk 是否存在特定的问题。但微信没有对其开源，它是以一个jar包的形式提供使用，可以用在持续集成系统里面用于分析排查问题并输出较为详细的检测结果报告。下面记录一下这个工具是从哪些角度分析/哪些技术手段分析问题的。</p><h2 id="分析apk包的角度"><a href="#分析apk包的角度" class="headerlink" title="分析apk包的角度"></a>分析apk包的角度</h2><ol><li>读取 manifest 的信息<ul><li>从 AndroidManifest.xml 文件中读取 apk 的全局信息，如 packageName、versionCode 等 </li></ul></li><li>按文件大小排序列出 apk 中包含的文件<ul><li>列出超过一定大小的文件，可按文件后缀过滤，并且按文件大小排序，这样作展示会更直观 </li></ul></li><li>统计方法数<ul><li>统计 dex 包含的方法数， 并将输出结果按照类名 (class) 或者包名 (package) 来分组 </li></ul></li><li>检查是否经过了资源混淆<ul><li>检查 apk 是否经过了资源混淆，微信团队认为，混淆可以缩减包体积（其实混淆本身对包体积影响微乎其微，但混淆工具一般会自带压缩功能，可以了解一下微信团队的混淆工具<a href="https://github.com/shwenzhang/AndResGuard" target="_blank" rel="noopener">AndResGuard</a>） </li></ul></li><li>搜索不含 alpha 通道的 png 文件<ul><li>alpha通道是用于图像透明/半透明显示的，如果png图片不含alpha通道，则不需要使用png，用jpg更节约空间 </li></ul></li><li>检查是否包含多个ABI版本的动态库<ul><li>so 文件的大小可能会在 apk 文件大小中占很大的比例，可以考虑在 apk 中只包含一个 ABI 版本的动态库</li></ul></li><li>搜索未经压缩的文件类型<ul><li>没有压缩的文件当然要考虑压缩～</li></ul></li><li>统计apk中包含的R类以及R类中的 field count<ul><li>编译之后，代码中对资源的引用都会优化成 int 常量，除了 R.styleable 之外，其他的 R 类其实都可以删除</li></ul></li><li>搜索冗余的文件<ul><li>对于两个内容完全相同的文件，应该去冗余</li></ul></li><li>检查是否有多个动态库静态链接了 STL<ul><li>如果有多个动态库都依赖了 STL ，应该采用动态链接的方式而非多个动态库都去静态链接 STL</li></ul></li><li>搜索 apk 中包含的无用资源<ul><li>apk 中未经使用到的资源，应该予以删除</li></ul></li><li>搜索apk中包含的无用 assets 文件<ul><li>apk 中未经使用的 assets 文件，应该予以删除（注：assets也是资源文件夹，相对于res来说，它不会生成R文件索引）</li></ul></li><li>搜索 apk 中未经裁剪的动态库文件<ul><li>动态库经过裁剪之后，文件大小通常会减小很多，一般来讲可以从几个方面考虑，不需要的字体/语言，或者可以裁剪的图片资源等，比如说字节有些sdk/so团队会为了适配海外app而加入多语言支持，但是如果自己的业务线不需要支持海外业务完全可以把语言部分裁掉</li></ul></li></ol><h2 id="对应技术选型与实现"><a href="#对应技术选型与实现" class="headerlink" title="对应技术选型与实现"></a>对应技术选型与实现</h2><p><img src="/2021/01/07/Matrix-ApkChecker%EF%BC%9A%E5%BE%AE%E4%BF%A1%E5%9B%A2%E9%98%9F%E7%9A%84%E5%AE%89%E5%8D%93%E5%8C%85%E4%BD%93%E7%A7%AF%E7%9B%91%E6%8E%A7%E6%96%B9%E6%A1%88%E5%AD%A6%E4%B9%A0/tasks.png" alt="tasks"></p><ol><li><p>ManifestAnalyzeTask 用于读取 AndroidManifest.xml 中的信息，如：packageName、verisonCode、clientVersion 等。</p><ul><li>实现方法：利用 ApkTool 中的 AXmlResourceParser 来解析二进制的 AndroidManifest.xml 文件，并且可以反混淆出 AndroidManifest.xml 中引用的资源名称。</li></ul></li><li><p>ShowFileSizeTask 根据文件大小以及文件后缀名来过滤出超过指定大小的文件，并按照升序或降序排列结果。</p><ul><li>实现方法：直接利用 UnzipTask 中统计的文件大小来过滤输出结果。</li></ul></li><li><p>MethodCountTask 可以统计出各个 Dex 中的方法数，并按照类名或者包名来分组输出结果。</p><ul><li>实现方法：利用 google 开源的 com.android.dexdeps 类库来读取 dex 文件，统计方法数。</li></ul></li><li><p>ResProguardCheckTask 可以判断 apk 是否经过了资源混淆</p><ul><li>实现方法：资源混淆之后的 res 文件夹会重命名成 r ，直接判断是否存在文件夹 r 即可判断是否经过了资源混淆。</li></ul></li><li><p>FindNonAlphaPngTask 可以检测出 apk 中非透明的 png 文件</p><ul><li>实现方法：通过 java.awt.BufferedImage 类读取png文件并判断是否有 alpha 通道。</li></ul></li><li><p>MultiLibCheckTask 可以判断 apk 中是否有针对多个 ABI 的 so</p><ul><li>实现方法：直接判断 lib 文件夹下是否包含多个目录。</li></ul></li><li><p>CheckMultiSTLTask 可以检测 apk 中的 so 是否静态链接 STL</p><ul><li>实现方法：通过 nm 工具来读取 so 的符号表，如果出现 std:: 即表示 so 静态链接了 STL 。</li></ul></li><li><p>CountRTask 可以统计 R 类以及 R 类的中的 field 数目</p><ul><li>实现方法：同样是利用 com.android.dexdeps 类库来读取 dex 文件，找出 R 类以及 field 数目。</li></ul></li><li><p>UncompressedFileTask 可以检测出未经压缩的文件类型</p><ul><li>实现方法：直接利用 UnzipTask 中统计的各个文件的压缩前和压缩后的大小，判断压缩前和压缩后大小是否相等。</li></ul></li><li><p>DuplicatedFileTask 可以检测出冗余的文件</p><ul><li>实现方法：通过比较文件的 MD5 是否相等来判断文件内容是否相同。</li></ul></li><li><p>UnusedResourceTask 可以检测出 apk 中未使用的资源，对于 getIdentifier 获取的资源可以加入白名单</p><ul><li>实现方法：</li></ul><ol><li>过读取 R.txt 获取 apk 中声明的所有资源得到 declareResourceSet ；</li><li>通过读取 smali 文件中引用资源的指令（包括通过 reference 和直接通过资源 id 引用资源）得出 class 中引用的资源 classRefResourceSet ；</li><li>通过 ApkTool 解析 res 目录下的 xml 文件、AndroidManifest.xml 以及 resource.arsc 得出资源之间的引用关系；</li><li>根据上述几步得到的中间数据即可确定出 apk 中未使用到的资源。</li></ol></li></ol><h2 id="我在包体积监控实践中的一些思考"><a href="#我在包体积监控实践中的一些思考" class="headerlink" title="我在包体积监控实践中的一些思考"></a>我在包体积监控实践中的一些思考</h2><p>之前在百度实习的时候，曾经用过手百团队的一个包体积分析脚本，python代码部署在流水线上，对打包完的产物进行分析并输出一个html文件，百度的流水线平台可以展示指定的html文件，所以查看分析结果还算方便。但是这个代码分析包的耗时太久了，如果想做成一个分析平台会很不方便（毕竟在前端上传一个安装包之后需要等待好久才能拿到生成的html文件并渲染出来），所以一直在找更好的解决方案，于是找到了这个微信团队的。微信的这套解决方案虽然并行的处理各个分析任务，但也还是有一定耗时，可能分析的粒度和效率很难两全吧。但是<a href="https://github.com/pengchenglin/ApkChecker_new" target="_blank" rel="noopener">https://github.com/pengchenglin/ApkChecker_new</a> 这个方案从前端的角度，也算是避免了上传apk后用户干等的尴尬场景。包体积分析其实算是一种静态代码分析，可以不光局限于包体积这个角度，debug开关检查等静态检查也可以放进这个流程。如果流程较多的话，也完全可以当成一个“小流水线”串行执行，每处理完一个子任务就传输数据到前端进行动态渲染，也是比单纯生成一个html页面更灵活一些。</p><p>来字节之后，也做过一个基于代码提交的包体积监控报警工具，将jenkins与gitlab绑定，每次rd git push都会触发jenkins打包，如果当前包大小与该分支上一次打包结果相比高于一个阈值，那么就触发报警。同时在前端也有基于不同分支的包体积上升折线图，方便定位是哪个分支的哪次push导致了发版包的增量。看似很天衣无缝，实则由于git的复杂性，该系统做好之后也遇到很多问题最后也不了了之了，从此也能看到一个平台开发前事先调研工作的重要性。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;strong&gt;Matrix&lt;/strong&gt; 是微信终端自研和正在使用的一套 APM（应用性能管理）系统。git地址：&lt;a href=&quot;https://github.com/Tencent/matrix&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ht</summary>
      
    
    
    
    <category term="客户端质量" scheme="https://bodhisatan.github.io/categories/%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%B4%A8%E9%87%8F/"/>
    
    
    <category term="Android包体积" scheme="https://bodhisatan.github.io/tags/Android%E5%8C%85%E4%BD%93%E7%A7%AF/"/>
    
  </entry>
  
  <entry>
    <title>Android Native内存泄漏管理（2）：Android内存分配与OOM解决方案</title>
    <link href="https://bodhisatan.github.io/2020/12/30/Android%20Native%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E7%AE%A1%E7%90%86%EF%BC%882%EF%BC%89%EF%BC%9AAndroid%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E4%B8%8EOOM%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    <id>https://bodhisatan.github.io/2020/12/30/Android%20Native%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E7%AE%A1%E7%90%86%EF%BC%882%EF%BC%89%EF%BC%9AAndroid%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E4%B8%8EOOM%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/</id>
    <published>2020-12-30T09:39:20.000Z</published>
    <updated>2020-12-31T09:07:30.244Z</updated>
    
    <content type="html"><![CDATA[<h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><h3 id="OOM"><a href="#OOM" class="headerlink" title="OOM"></a>OOM</h3><p>OutOfMemory，Android平台上主要有三类：Java OOM，虚拟内存OOM，物理内存OOM，Java OOM指Java堆内存耗尽。</p><p>物理内存即RAM，虚拟内存主要是为了满足操作系统和应用程序对物理内存的需求。虚拟内存有几个重要特点，第一个是<strong>按需分配</strong>，当分配虚拟内存时，内核先分配一个空闲地址区间，当cpu访问这个地址时，才分配实际物理页（真正使用的内存远小于实际分配的内存）。当cpu访问一个地址时，若发现当前地址未分配物理页，会触发<code>page fault</code>异常，在page fault异常处理中会给当前地址分配物理页，同时加载对应数据。第二个特点是<strong>按页分配</strong>，内核会以每次<code>4kb</code>为单位分配物理内存（内存页），为了减少内存分配的浪费。第三个特点是页表转换，在内存的申请/释放过程后，物理内存中会存在内存碎片，页表转换可以将不连续的物理内存映射到连续的虚拟内存地址。</p><p>虚拟内存不涉及实际物理内存分配，所以理论上虚拟内存应该是无限的，但是地址空间又受限于cpu的寻址能力，32位cpu寻址范围最大4G，所以32位设备虚拟内存上限也是4GB左右。虚拟内存不足会引发<strong>App OOM</strong>，进而引发App崩溃。而物理内存是内核管理的，当物理内存无法满足内存申请时，内核会进行内存回收动作，具体有释放缓存、压缩内存、后台杀进程等。内核在回收内存时会持有内存的大锁，所以如page fault等操作会卡住，导致整个用户空间的执行会非常缓慢，具体表现就是<strong>app卡顿/anr/闪退</strong>，但一般不会导致OOM，因为应用程序的内存需求是按页分配的，这算是一种最低的要求，内存总能满足，如果内存连一个物理页也提供不了，就表明系统其它重要的流程也无法执行了，就会触发<strong>内核的OOM</strong>，具体的表现就是手机的重启。</p><p><img src="/2020/12/30/Android%20Native%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E7%AE%A1%E7%90%86%EF%BC%882%EF%BC%89%EF%BC%9AAndroid%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E4%B8%8EOOM%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/VSS.png" alt="VSS"></p><h3 id="内存表示"><a href="#内存表示" class="headerlink" title="内存表示"></a>内存表示</h3><div class="table-container"><table><thead><tr><th>VSS</th><th>虚拟内存大小</th></tr></thead><tbody><tr><td>RSS</td><td>实际使用（独占+共享）物理内存大小</td></tr><tr><td>PSS</td><td>Σ(独占物理页) + Σ(共享物理页/共享进程数)</td></tr></tbody></table></div><p><strong>PSS越高 越容易在内核回收时被选中杀掉</strong></p><h3 id="内存的申请"><a href="#内存的申请" 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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span>* <span class="title">mmap</span><span class="params">(<span class="keyword">void</span>* start, <span class="comment">//起始地址，可以是空，也可以指定地址</span></span></span></span><br><span class="line"><span class="function"><span class="params">           <span class="keyword">size_t</span> length, <span class="comment">//内存长度</span></span></span></span><br><span class="line"><span class="function"><span class="params">           <span class="keyword">int</span> prot, <span class="comment">//访问属性 可读（r）可写（w）可执行（x）</span></span></span></span><br><span class="line"><span class="function"><span class="params">           <span class="keyword">int</span> falgs, <span class="comment">//MAP_ANONYMOUS/MAP_FIXED/MAP_PRIVATE/MAP_SHARED</span></span></span></span><br><span class="line"><span class="function"><span class="params">           <span class="keyword">int</span> fd, <span class="comment">//被映射文件的句柄，flags设置MAP_ANONYMOUS时忽略</span></span></span></span><br><span class="line"><span class="function"><span class="params">           <span class="keyword">off_t</span> offset)</span></span>; <span class="comment">//被映射文件的偏移，必须是page size整数倍</span></span><br></pre></td></tr></table></figure><ul><li>文件映射<ul><li>flags未设置MAP_ANONYMOUS</li><li>fd指向一个已经打开的文件<ul><li>设备文件：dev</li><li>普通文件：system/data/vendor/sdcard</li></ul></li><li>内存段名是文件路径<ul><li>设备文件可以通过ioctl(ctl: control)修改内存段名</li></ul></li></ul></li><li>匿名映射<ul><li>flags设置MAP_ANONYMOUS</li><li>fd一般为-1</li><li>内存段名默认为空<ul><li>可通过prctl设置成<code>anon:xxx</code>(Android10以后webview就是这样)</li></ul></li></ul></li><li>其他映射函数：mremap/munmap/mprotect/ioctl/prctl</li></ul><h2 id="内存分类分布与对应优化"><a href="#内存分类分布与对应优化" class="headerlink" title="内存分类分布与对应优化"></a>内存分类分布与对应优化</h2><h3 id="内存分类与分布"><a href="#内存分类与分布" class="headerlink" title="内存分类与分布"></a>内存分类与分布</h3><p>案例分析：32位设备运行32位app，总虚拟内存3G左右，从zygote到OOM这个过程中各阶段虚拟内存增量情况</p><p>注：</p><ul><li>zygoat：所有应用进程的父进程，app进程都由这个进程孵化而来）</li><li>dalvik：dalvik虚拟机所占据内存</li></ul><p><img src="/2020/12/30/Android%20Native%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E7%AE%A1%E7%90%86%EF%BC%882%EF%BC%89%EF%BC%9AAndroid%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E4%B8%8EOOM%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/lifecycle.png" alt="lifecycle"></p><h3 id="具体内存段的对应优化方案"><a href="#具体内存段的对应优化方案" class="headerlink" title="具体内存段的对应优化方案"></a>具体内存段的对应优化方案</h3><h4 id="虚拟机内存段"><a href="#虚拟机内存段" class="headerlink" title="虚拟机内存段"></a>虚拟机内存段</h4><p><img src="/2020/12/30/Android%20Native%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E7%AE%A1%E7%90%86%EF%BC%882%EF%BC%89%EF%BC%9AAndroid%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E4%B8%8EOOM%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/jvm.png" alt="jvm"></p><p>可以看出，内存消耗大头是<code>mainspace</code>、<code>large object space</code>和<code>Bitmap</code>，mainspace为什么有两个呢？这是虚拟机在内存碎片整理的时候所用（之前的博客里提到过），所以对应优化可以从这两块内存下手：</p><ul><li>Java堆裁剪：屏蔽内存碎片整理这个过程，将一块mainspace空间释放</li><li>LargeObjectSpace</li><li>Bitmap：Android8以上Bitmap在Native堆里分配内存，而不再在Java堆里分配内存，所以这也导致了Java OOM减少，而Native OOM增多</li></ul><h4 id="unnamed内存段"><a href="#unnamed内存段" class="headerlink" title="unnamed内存段"></a>unnamed内存段</h4><p><img src="/2020/12/30/Android%20Native%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E7%AE%A1%E7%90%86%EF%BC%882%EF%BC%89%EF%BC%9AAndroid%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E4%B8%8EOOM%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/unnamed.png" alt="unnamed"></p><ul><li>webview：为了保证webview启动效率，Google在zygote阶段就给webview预留了一部分空间，但这部分空间不一定会被用到，而且现在很多公司自研浏览器内核，那这部分空间就是可以释放的（Android10 webview空间已经被命名了）</li><li>线程：一个线程占据1M，但通常用不到1M，可以hook Java线程创建函数然后裁剪Java线程栈，同时线程栈也会存在泄漏问题可以治理。<h4 id="anon（已命名匿名内存段）"><a href="#anon（已命名匿名内存段）" class="headerlink" title="anon（已命名匿名内存段）"></a>anon（已命名匿名内存段）</h4></li><li>libc_malloc：malloc/new申请的<strong>Native堆</strong>（占内存最多）<ul><li>系统问题：jemalloc优化</li><li>APP问题：<strong>堆内存泄漏监控</strong>（下文第三部分）</li></ul></li><li>.bss：so和dex文件的bss段</li><li>thread*：栈保护页，信号栈<h4 id="data内存段（文件映射）"><a href="#data内存段（文件映射）" class="headerlink" title="data内存段（文件映射）"></a>data内存段（文件映射）</h4>和system区分，data是app自己的而system是系统的。</li></ul><p>data目录下分<code>data/app/</code>和<code>data/data</code>，app目录下是so（动态库）、odex（字节码转换文件）等，优化空间较小，data目录下有plugins（插件）、webview等，如果下发插件比较多，plugins也占据很大。</p><h4 id="system内存段（目录文件映射）"><a href="#system内存段（目录文件映射）" class="headerlink" title="system内存段（目录文件映射）"></a>system内存段（目录文件映射）</h4><p>system目录下由四部分，so（动态库），ttf（字库），dat和other，其中字库占据内存但有可能利用率极低，也可以释放一部分。</p><h4 id="ashmen内存段"><a href="#ashmen内存段" class="headerlink" title="ashmen内存段"></a>ashmen内存段</h4><p>Google为了解决多进程的内存共享而做的Linux里的驱动，路径信息基本无效，调用链都是系统库函数，很难对应具体业务，解决方案：hook（命名or记录），收集更多业务相关信息进行治理</p><h4 id="other"><a href="#other" class="headerlink" title="other"></a>other</h4><p>一般是设备文件</p><h2 id="堆内存泄漏检测原理"><a href="#堆内存泄漏检测原理" class="headerlink" title="堆内存泄漏检测原理"></a>堆内存泄漏检测原理</h2><p>上一篇博客已经讲了内存泄漏检测的简单原理，这篇从代码角度加以阐述<br>系统malloc函数<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></pre></td><td class="code"><pre><span class="line"><span class="comment">// @binoc/libc/binoc/malloc_debug_common.cpp</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> Malloc(function) je_ ## function</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">const</span> MallocDebug __libc_malloc_default_dispatch =  &#123;</span><br><span class="line">    Malloc(<span class="built_in">calloc</span>),</span><br><span class="line">    Malloc(<span class="built_in">free</span>),</span><br><span class="line">    Malloc(mallinfo),</span><br><span class="line">    Malloc(<span class="built_in">malloc</span>),</span><br><span class="line">    Malloc(malloc_usable_size),</span><br><span class="line">    Malloc(memalign),</span><br><span class="line">    ...</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">static</span> <span class="keyword">const</span> MallocDebug* __libc_malloc_dispatch = &amp;__libc_malloc_default_dispatch;</span><br><span class="line"><span class="keyword">extern</span> <span class="string">"C"</span> <span class="function"><span class="keyword">void</span>* <span class="title">malloc</span><span class="params">(<span class="keyword">size_t</span> bytes)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> __libc_malloc_dispatch-&gt;<span class="built_in">malloc</span>(bytes);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>hook原理<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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> Proxy(function) proxy_ ## function</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">const</span> MallocDebug s_proxy_dispatch = &#123;</span><br><span class="line">    Proxy(<span class="built_in">calloc</span>),</span><br><span class="line">    Proxy(<span class="built_in">free</span>),</span><br><span class="line">    Proxy(mallinfo),</span><br><span class="line">    Proxy(<span class="built_in">malloc</span>),</span><br><span class="line">    Proxy(malloc_usable_size),</span><br><span class="line">    Proxy(memalign),</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">static</span> <span class="keyword">const</span> MallocDebug* sDefaultDispatch = <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">do_hook_malloc</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">void</span>* handle = npth_dlopen(<span class="string">"libc.so"</span>);</span><br><span class="line">    <span class="keyword">void</span>* libc_malloc_dispatch = npth_dlsym_symtab(handle, <span class="string">"__libc_malloc_dispatch"</span>);</span><br><span class="line">    npth_dlclose(handle);</span><br><span class="line"></span><br><span class="line">    sDefaultDispatch = *(<span class="keyword">static</span> <span class="keyword">const</span> MallocDebug **)lib_malloc_dispatch;</span><br><span class="line">    *(<span class="keyword">static</span> <span class="keyword">const</span> MallocDebug **)libc_malloc_dispatch = &amp;s_proxy_dispatch;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>代理函数<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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span>* <span class="title">proxy_malloc</span><span class="params">(<span class="keyword">size_t</span> size)</span> </span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">void</span>* raddr = __builtin_return_address(<span class="number">0</span>);</span><br><span class="line">    <span class="keyword">void</span>* faddr = __builtin_frame_address(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">void</span>* ptr = sDefaultDispatch-&gt;<span class="built_in">malloc</span>(size); <span class="comment">// 执行原malloc</span></span><br><span class="line"></span><br><span class="line">    push_mem(ptr, size, raddr, faddr); <span class="comment">// 保存信息</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> ptr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span>* <span class="title">proxy_free</span><span class="params">(<span class="keyword">void</span>* ptr)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    pop_mem(ptr); <span class="comment">//删除信息</span></span><br><span class="line"></span><br><span class="line">    sDefaultDispatch-&gt;<span class="built_in">free</span>(ptr); <span class="comment">// 执行原free</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>PS. <code>一般在分配的时候会获取当前的调用栈，但是调用栈获取可能很影响效率，所以这里获取函数返回地址和线程栈帧地址（都是从寄存器读，速度很快），返回地址可以定位函数，进而定位动态库，线程栈帧地址可以定位线程栈，进而定位线程。</code></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;基本概念&quot;&gt;&lt;a href=&quot;#基本概念&quot; class=&quot;headerlink&quot; title=&quot;基本概念&quot;&gt;&lt;/a&gt;基本概念&lt;/h2&gt;&lt;h3 id=&quot;OOM&quot;&gt;&lt;a href=&quot;#OOM&quot; class=&quot;headerlink&quot; title=&quot;OOM&quot;&gt;&lt;/a&gt;OO</summary>
      
    
    
    
    <category term="客户端质量" scheme="https://bodhisatan.github.io/categories/%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%B4%A8%E9%87%8F/"/>
    
    
    <category term="Android性能稳定性" scheme="https://bodhisatan.github.io/tags/Android%E6%80%A7%E8%83%BD%E7%A8%B3%E5%AE%9A%E6%80%A7/"/>
    
    <category term="内存管理" scheme="https://bodhisatan.github.io/tags/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/"/>
    
    <category term="Android Native" scheme="https://bodhisatan.github.io/tags/Android-Native/"/>
    
  </entry>
  
  <entry>
    <title>Android Native内存泄漏管理（1）：基本概念/原理</title>
    <link href="https://bodhisatan.github.io/2020/12/28/Android-Native%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E7%AE%A1%E7%90%86%EF%BC%881%EF%BC%89%EF%BC%9A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5-%E5%8E%9F%E7%90%86/"/>
    <id>https://bodhisatan.github.io/2020/12/28/Android-Native%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E7%AE%A1%E7%90%86%EF%BC%881%EF%BC%89%EF%BC%9A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5-%E5%8E%9F%E7%90%86/</id>
    <published>2020-12-28T06:22:35.000Z</published>
    <updated>2020-12-31T09:07:46.547Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Native内存基础"><a href="#Native内存基础" class="headerlink" title="Native内存基础"></a>Native内存基础</h2><h3 id="Native内存的基本概念"><a href="#Native内存的基本概念" class="headerlink" title="Native内存的基本概念"></a>Native内存的基本概念</h3><ul><li>物理内存（PSS）<strong>硬件概念</strong><ul><li>RAM硬件</li></ul></li><li>虚拟内存（VSS）<strong>操作系统概念，为了解决物理内存不足的问题</strong><ul><li>内存分页</li><li>缺页中断</li><li>页面置换</li></ul></li><li>安卓应用内存使用（均在用户空间内  <code>user space：相对于kernel space的概念</code>）<ul><li>Java使用内存<ul><li>Java heap size * 2</li><li>Android oat/art文件</li></ul></li><li>Native可用内存<ul><li>除Java之外的用户空间</li></ul></li></ul></li><li>App &amp; Device运行时 用户空间虚拟内存上限（Native OOM的本质：用户空间虚拟内存耗尽）<ul><li>32位App &amp; 32位Device：最大3G左右</li><li>32位App &amp; 64位Device：最大4G左右</li><li>64位App &amp; 64位Device：最大512G左右（是不是超大？所以64位app几乎不会有native OOM问题，支持64位app也是治理OOM途径之一）</li></ul></li></ul><p><img src="/2020/12/28/Android-Native%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E7%AE%A1%E7%90%86%EF%BC%881%EF%BC%89%EF%BC%9A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5-%E5%8E%9F%E7%90%86/虚拟内存原理.png" alt="虚拟内存基本原理"></p><h3 id="Native内存的分配方式"><a href="#Native内存的分配方式" class="headerlink" title="Native内存的分配方式"></a>Native内存的分配方式</h3><p>无论是*alloc还是mmap都不能直接分配物理内存，只能分配虚拟内存，但是*alloc系函数分配小块内存可能直接<strong>映射到</strong>物理内存。<strong>Thread、Webview、Flutter、显存内存分配基本都是走mmap虚拟内存</strong></p><h4 id="PSS"><a href="#PSS" class="headerlink" title="PSS"></a>PSS</h4><ul><li>不能直接被分配 只能被映射</li><li>malloc/calloc/realloc/memalign</li><li>free</li><li>posix_memalign</li><li>aligned_alloc</li><li>malloc_usable_size</li><li>pvalloc/valloc</li></ul><h4 id="VSS"><a href="#VSS" class="headerlink" title="VSS"></a>VSS</h4><ul><li>当*alloc函数分配内存 <strong>&gt;=128k</strong> 时，底层调用mmap</li><li>mmap/mmap64</li><li>munmap</li><li>mremap</li></ul><h3 id="Native内存不足的表现"><a href="#Native内存不足的表现" class="headerlink" title="Native内存不足的表现"></a>Native内存不足的表现</h3><ul><li>Java Crash（pthread_create OOM）</li><li>Native Crash</li><li>黑屏（底层做了异常处理，可能以黑屏方式体现）</li></ul><h3 id="栈回溯"><a href="#栈回溯" class="headerlink" title="栈回溯"></a>栈回溯</h3><ul><li>由调用栈栈顶向栈底推到调用链的过程</li><li>通常用在打印crash堆栈（例如Java层的<code>printStackTrace()</code>）</li></ul><h2 id="Native内存泄漏工具"><a href="#Native内存泄漏工具" class="headerlink" title="Native内存泄漏工具"></a>Native内存泄漏工具</h2><p>目的：找出用户虚拟内存空间被哪些业务逻辑耗尽（不光聚焦虚拟内存，还需聚焦物理内存，提高虚拟内存使用率，即找出虚拟内存中申请但未在物理内存中分配的内存，另外随着64位app的普及，在虚拟内存耗尽之前，物理内存会先耗尽并产生ANR/重启等后果）</p><h3 id="Native内存泄漏原理"><a href="#Native内存泄漏原理" class="headerlink" title="Native内存泄漏原理"></a>Native内存泄漏原理</h3><p>Why：与Java层不同，Java层相当于在Native层上又做了一次抽象，可以比较容易理清内存块的属性和依赖关系，所以Native内存不能像Java那样进行<strong>静态分析</strong>，只能渗透到内存分配的过程监控内存分配/释放过程<br>How：类似于<strong>筛子模型</strong>，过滤出分配了但没释放的内存</p><ul><li>Native内存泄漏监控原理<ul><li>通过代理拦截内存分配的地址和大小</li><li>通过<strong>回溯调用栈</strong>获取内存分配的调用链（相比Java是通过依赖关系获取引用链，然后通过引用链获得调用链）</li><li>通过缓存crud过滤出未释放的内存</li></ul></li><li>Native内存泄漏监控组成<ul><li>代理</li><li>栈回溯</li><li>缓存管理</li></ul></li></ul><p><img src="/2020/12/28/Android-Native%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E7%AE%A1%E7%90%86%EF%BC%881%EF%BC%89%EF%BC%9A%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5-%E5%8E%9F%E7%90%86/监控原理.png" alt="Native内存监控原理"></p><h3 id="已有工具-解决方案"><a href="#已有工具-解决方案" class="headerlink" title="已有工具/解决方案"></a>已有工具/解决方案</h3><ul><li><a href="https://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.md" target="_blank" rel="noopener">Malloc Debug</a>：AOSP原生支持，难堪大用<ul><li>稳定性问题：存在栈回溯crash</li><li>性能问题：性能损失十倍以上</li></ul></li><li>LeakTracer：依赖LD_PRELOAD机制和系统栈回溯实现</li><li>MTrace：仅支持malloc/realloc等 不支持new/new[]等</li><li>MemWatch</li><li>Valgrind-memcheck</li><li>TCMalloc</li><li>LeakSanitizer</li><li><a href="https://mp.weixin.qq.com/s/1Vb3qk6H-2CekgPQzCni-g" target="_blank" rel="noopener">高德系统化解决方案</a></li></ul><h3 id="代理方案-栈回溯方案-缓存管理方案对比"><a href="#代理方案-栈回溯方案-缓存管理方案对比" class="headerlink" title="代理方案/栈回溯方案/缓存管理方案对比"></a>代理方案/栈回溯方案/缓存管理方案对比</h3><h4 id="代理方案"><a href="#代理方案" class="headerlink" title="代理方案"></a>代理方案</h4><ul><li><a href="https://android.googlesource.com/platform/bionic/+/master/libc/malloc_hooks/README.md" target="_blank" rel="noopener">Malloc hook</a>：Android7以上，debug包，wrap.sh</li><li>LD_PRELOAD：依赖Android版本 debug包 wrap.sh</li><li>PLT/GOT hook：<a href="https://github.com/iqiyi/xHook" target="_blank" rel="noopener">xHook</a></li><li>Inline hook：<a href="https://github.com/ele7enxxh/Android-Inline-Hook" target="_blank" rel="noopener">Android inline hook</a></li></ul><div class="table-container"><table><thead><tr><th>模式</th><th>hook原理</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>Malloc hook</td><td>AOSP原生代理实现</td><td>没有性能/稳定性问题</td><td>接口不暴露，不支持mmap</td></tr><tr><td>LD_PRELOAD</td><td>so加载先后顺序</td><td>没有性能/稳定性问题</td><td>Android上难以实现</td></tr><tr><td>PLT/GOT hook</td><td>跳转表</td><td>单点hook 成熟可靠</td><td>hook效率低</td></tr><tr><td>Inline hook</td><td>目标代码</td><td>全局hook 效率高</td><td>兼容性问题多 风险大</td></tr></tbody></table></div><h4 id="栈回溯方案"><a href="#栈回溯方案" class="headerlink" title="栈回溯方案"></a>栈回溯方案</h4><ul><li>libunwind_llvm：LLVM内置unwind库，<a href="https://zhuanlan.zhihu.com/p/33937283" target="_blank" rel="noopener">libunwind llvm编年史</a></li><li>libunwind_nongnu：第三方unwind实现</li><li>libgcc_s：GCC内置unwind库</li><li>libbacktrace：AOSP内置</li><li>libunwindstack：Android9.0新方案</li><li>libudf：MTK实现</li></ul><div class="table-container"><table><thead><tr><th>Unwind</th><th>性能</th><th>兼容性</th><th>支持</th></tr></thead><tbody><tr><td>libudf</td><td>性能最好</td><td>兼容性好</td><td>无更新</td></tr><tr><td>libunwind_llvm</td><td>性能较好</td><td>兼容性差</td><td>会被取代</td></tr><tr><td>libunwind_nongnu</td><td>性能最差</td><td>兼容性好</td><td>官方持续更新</td></tr></tbody></table></div><h4 id="缓存管理方案"><a href="#缓存管理方案" class="headerlink" title="缓存管理方案"></a>缓存管理方案</h4><ul><li>Malloc Debug：全局锁，hash散列，栈聚合，动态申请缓存空间</li><li>LeakTracer：全局锁，大缓存，hash散列，缓存满之后动态申请缓存空间</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;Native内存基础&quot;&gt;&lt;a href=&quot;#Native内存基础&quot; class=&quot;headerlink&quot; title=&quot;Native内存基础&quot;&gt;&lt;/a&gt;Native内存基础&lt;/h2&gt;&lt;h3 id=&quot;Native内存的基本概念&quot;&gt;&lt;a href=&quot;#Native内存</summary>
      
    
    
    
    <category term="客户端质量" scheme="https://bodhisatan.github.io/categories/%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%B4%A8%E9%87%8F/"/>
    
    
    <category term="Android性能稳定性" scheme="https://bodhisatan.github.io/tags/Android%E6%80%A7%E8%83%BD%E7%A8%B3%E5%AE%9A%E6%80%A7/"/>
    
    <category term="内存管理" scheme="https://bodhisatan.github.io/tags/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/"/>
    
    <category term="Android Native" scheme="https://bodhisatan.github.io/tags/Android-Native/"/>
    
  </entry>
  
  <entry>
    <title>命令式 声明式 响应式 函数式</title>
    <link href="https://bodhisatan.github.io/2020/12/27/%E5%91%BD%E4%BB%A4%E5%BC%8F-%E5%A3%B0%E6%98%8E%E5%BC%8F-%E5%93%8D%E5%BA%94%E5%BC%8F-%E5%87%BD%E6%95%B0%E5%BC%8F/"/>
    <id>https://bodhisatan.github.io/2020/12/27/%E5%91%BD%E4%BB%A4%E5%BC%8F-%E5%A3%B0%E6%98%8E%E5%BC%8F-%E5%93%8D%E5%BA%94%E5%BC%8F-%E5%87%BD%E6%95%B0%E5%BC%8F/</id>
    <published>2020-12-27T06:31:15.000Z</published>
    <updated>2020-12-27T07:01:10.529Z</updated>
    
    <content type="html"><![CDATA[<h2 id="命令式编程（Imperative-programming）"><a href="#命令式编程（Imperative-programming）" class="headerlink" title="命令式编程（Imperative programming）"></a>命令式编程（Imperative programming）</h2><p>详细的命令机器怎么（How）去处理一件事情以达到你想要的结果（What）</p><p>例子：我晚上打车回家，司机不认识路，我需要一步一步告诉司机每个路口怎么走</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> user</span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">var</span> i=<span class="number">0</span>; i &lt; user.length; i++) &#123;</span><br><span class="line">    <span class="keyword">if</span>(user.user_name == <span class="string">"Ben"</span>) &#123;</span><br><span class="line">         print(<span class="string">"find"</span>);</span><br><span class="line">         <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="声明式编程（-Declarative-programming）"><a href="#声明式编程（-Declarative-programming）" class="headerlink" title="声明式编程（ Declarative programming）"></a>声明式编程（ Declarative programming）</h2><p>只告诉你想要的结果（What），机器自己摸索过程（How）</p><p>例子：我晚上打车回家，只需要告诉司机我家在哪里即可</p><figure class="highlight sql"><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">SELECT</span> * <span class="keyword">from</span> <span class="keyword">user</span></span><br><span class="line"><span class="keyword">WHERE</span> user_name = LiXiang</span><br></pre></td></tr></table></figure><h2 id="响应式编程（Reactive-programming）"><a href="#响应式编程（Reactive-programming）" class="headerlink" title="响应式编程（Reactive programming）"></a>响应式编程（Reactive programming）</h2><p>是使用异步数据流进行编程。本质上是对数据流或某种变化所作出的反应，但是这个变化什么时候发生是未知的，所以是基于异步、回调的方式在处理问题。<br>例子：我晚上打车回家，在滴滴上面下了订单发出信号，我可以在等待的时候随便做什么，不会干等着。（异步调用不阻塞）<br>司机接单，给我一个信号，那我就可以等司机到上车。（信号回调）<br>整个过程是异步回调的方式来进行的。这样我和司机都不用一直干等着，效率比较高。<br>代码例子可以参考<code>RxJava/RxJS</code> <a href="https://zhuanlan.zhihu.com/p/27678951" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/27678951</a> 这篇文章里面有很详细的介绍</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><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">Disposable subscribe = Observable.just(url)</span><br><span class="line">                    .subscribeOn(Schedulers.io())</span><br><span class="line">                    .map(s -&gt; &#123;</span><br><span class="line">                        ArrayList&lt;BasicNameValuePair&gt; params = <span class="keyword">new</span> ArrayList&lt;BasicNameValuePair&gt;();</span><br><span class="line">                        params.add(<span class="keyword">new</span> BasicNameValuePair(<span class="string">"email"</span>, getEmail()));</span><br><span class="line">                        params.add(<span class="keyword">new</span> BasicNameValuePair(<span class="string">"host_ip"</span>, getHostIp()));</span><br><span class="line">                        params.add(<span class="keyword">new</span> BasicNameValuePair(<span class="string">"host_port"</span>, <span class="string">"10086"</span>));</span><br><span class="line">                        params.add(<span class="keyword">new</span> BasicNameValuePair(<span class="string">"package"</span>, getCurrentActivity().getPackageName()));</span><br><span class="line">                        String executeGet = NetworkUtils.executePost(<span class="number">200</span> * <span class="number">1024</span>, s, params);</span><br><span class="line">                        JSONObject object = <span class="keyword">new</span> JSONObject(executeGet);</span><br><span class="line">                        <span class="keyword">return</span> object;</span><br><span class="line">                    &#125;)</span><br><span class="line">                    .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line">                    .subscribe(object -&gt; &#123;</span><br><span class="line">                        ToastUtils.showLongToast(getCurrentActivity(), <span class="string">"配置成功"</span>);</span><br><span class="line">                    &#125;);</span><br></pre></td></tr></table></figure><h2 id="函数式编程（Functional-programming）"><a href="#函数式编程（Functional-programming）" class="headerlink" title="函数式编程（Functional programming）"></a>函数式编程（Functional programming）</h2><p>函数式的代码是对映射的描述,在函数式语言中，函数作为一等公民，可以在任何地方定义，在函数内或函数外，可以作为函数的参数和返回值，可以对函数进行组合。</p><p>好处：首先，最直观的角度来说，函数式风格的代码可以写得很精简，其次，函数式的代码是“对映射的描述”，它不仅可以描述二叉树这样的数据结构之间的对应关系，任何能在计算机中体现的东西之间的对应关系都可以描述——比如函数和函数之间的映射；比如外部操作到 GUI 之间的映射（就是现在前端热炒的所谓 FRP）。它的抽象程度可以很高，这就意味着函数式的代码可以更方便的复用。</p><p>最后，可以很好的写出并行代码<br>代码例子(翻转二叉树):<br><figure class="highlight haskell"><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="class"><span class="keyword">data</span> <span class="type">Tree</span> a = <span class="type">Nil</span> | <span class="type">Node</span> a (<span class="type">Tree</span> <span class="title">a</span>) (<span class="type">Tree</span> <span class="title">a</span>)</span></span><br><span class="line">    <span class="keyword">deriving</span> (<span class="type">Show</span>, <span class="type">Eq</span>)</span><br><span class="line">    </span><br><span class="line"><span class="title">invert</span> :: <span class="type">Tree</span> a -&gt; <span class="type">Tree</span> a</span><br><span class="line"><span class="title">invert</span> <span class="type">Nil</span> = <span class="type">Nil</span></span><br><span class="line"><span class="title">invert</span> (<span class="type">Node</span> v l r) = <span class="type">Node</span> v (invert r) (invert l)</span><br></pre></td></tr></table></figure></p><p>函数式编程和声明式编程有所关联，因为他们思想是一致的：即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;命令式编程（Imperative-programming）&quot;&gt;&lt;a href=&quot;#命令式编程（Imperative-programming）&quot; class=&quot;headerlink&quot; title=&quot;命令式编程（Imperative programming）&quot;&gt;&lt;/a</summary>
      
    
    
    
    <category term="编程语言" scheme="https://bodhisatan.github.io/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
    
    <category term="杂项" scheme="https://bodhisatan.github.io/tags/%E6%9D%82%E9%A1%B9/"/>
    
  </entry>
  
  <entry>
    <title>Java虚拟机学习笔记二：GC算法初探</title>
    <link href="https://bodhisatan.github.io/2020/05/31/Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E4%BA%8C%EF%BC%9AGC%E7%AE%97%E6%B3%95%E5%88%9D%E6%8E%A2/"/>
    <id>https://bodhisatan.github.io/2020/05/31/Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E4%BA%8C%EF%BC%9AGC%E7%AE%97%E6%B3%95%E5%88%9D%E6%8E%A2/</id>
    <published>2020-05-31T08:07:20.000Z</published>
    <updated>2020-05-31T15:55:42.363Z</updated>
    
    <content type="html"><![CDATA[<h2 id="常见对象存活判定算法"><a href="#常见对象存活判定算法" class="headerlink" title="常见对象存活判定算法"></a>常见对象存活判定算法</h2><h3 id="引用计数法"><a href="#引用计数法" class="headerlink" title="引用计数法"></a>引用计数法</h3><p>具体方法是，在对象中添加一个引用计数器，每当有一个地方引用它时，计数器+1，当引用失效时，计数器-1，如果计数器为零，就说明没有地方在引用它。这个方法看似原理很简单，效率也高，但其实需要很多额外的考虑才能保证这个判定过程正确执行，比如单纯的引用计数很难解决对象间的循环引用的问题，举个例子：<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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ReferenceCountingGC</span> </span>&#123;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> Object instance=<span class="keyword">null</span>;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> _1MB=<span class="number">1024</span>*<span class="number">1024</span>;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">byte</span>[] bigSize=<span class="keyword">new</span> <span class="keyword">byte</span>[<span class="number">1024</span>*<span class="number">1024</span>];<span class="comment">//1MB的堆空间</span></span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">ReferenceCountingGC objA=<span class="keyword">new</span> ReferenceCountingGC(); <span class="comment">// step1</span></span><br><span class="line">ReferenceCountingGC objB=<span class="keyword">new</span> ReferenceCountingGC(); <span class="comment">// step2</span></span><br><span class="line">objA.instance=objB; <span class="comment">// step3</span></span><br><span class="line">objB.instance=objA; <span class="comment">// step4</span></span><br><span class="line">objA=<span class="keyword">null</span>; <span class="comment">// step5</span></span><br><span class="line">objB=<span class="keyword">null</span>; <span class="comment">// step6</span></span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>分析一下这段代码，首先经过了step1和step2之后，虚拟机栈的局部变量表里有两个reference类型变量，objA和objB，他们分别指向堆上的两个ReferenceCountingGC对象实例，我们用实例A和实例B表示，实例A因为objA引用它，所以计数器加1，cntA = 1，同理cntB = 1；step3阶段，实例B被实例A的instance成员引用，cntB = 2，同理step4阶段cntA = 2。step5阶段，栈帧中的objA不再指向实例A，cntA = 1，step6阶段，栈帧中的objB不再指向实例B，cntB = 1。到此，实例A和实例B的引用计数器均不为0，如果只是单纯的引用计数法，这便产生了<strong>内存泄漏</strong></p><h3 id="可达性分析法"><a href="#可达性分析法" class="headerlink" title="可达性分析法"></a>可达性分析法</h3><p>可达性分析法就是通过一系列的<strong>GC Root</strong>根对象作为起始节点，用类似二叉树遍历的方法向下搜索，如果GC Root到某个对象<strong>不可达</strong>，这时的对象就是不可能再被引用的，会被回收掉。</p><p>可作为GC Root的对象包含下面几种：</p><ul><li><strong>虚拟机栈本地变量表中引用的对象</strong></li><li><strong>方法区中常量引用的对象、静态属性引用的变量</strong></li><li>本地方法区中JNI引用的变量</li><li>Java虚拟机内部的引用，比如异常对象<code>NullPointerException</code></li><li>所有被同步锁（synchronized关键字）持有的对象</li><li>反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码的缓存等…</li></ul><p>除了这些固定的对象之外，根据垃圾收集的区域不同，还可以有其他对象临时加入，因为根据虚拟机自己的实现细节，某个区域里的对象完全有可能被其他区域对象所引用</p><h2 id="对象的引用分类（强度依次递减）"><a href="#对象的引用分类（强度依次递减）" class="headerlink" title="对象的引用分类（强度依次递减）"></a>对象的引用分类（强度依次递减）</h2><ul><li>强引用：例如<code>Object a = new Object()</code>只要强引用关系存在，对象就不会被回收</li><li>软引用：还有用，但非必须的对象，这部分对象会在系统将要内存溢出前进行回收</li><li>弱引用：一旦发起垃圾回收，就会回收弱引用的对象</li><li>虚引用：“形同虚设”，一个对象是否有虚引用，完全不影响它的生命周期。虚引用与软引用和弱引用的一个区别在于：虚引用必须和引用队列（ReferenceQueue）联合使用。当垃圾回收器准备回收一个对象时，如果发现它还有虚引用，就会在回收对象的内存之前，把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用，来了解被引用的对象是否将要被垃圾回收。</li></ul><h2 id="finalize-方法"><a href="#finalize-方法" class="headerlink" title="finalize()方法"></a>finalize()方法</h2><p>要真正宣告一个对象的死亡，至少要经过两次标记过程：第一次是可达性分析后发现不可达，被第一次标记；第二次是一次筛选，筛选条件是此对象是否有必要执行<code>finalize()</code>方法，如果对象未覆盖<code>finalize()</code>方法，或者<code>finalize()</code>方法已经被对象调用过，那就没必要执行，假如被判定为有必要执行，那么该对象会被放在一个名为F-Queue的队列中，稍后由一条虚拟机自动建立的、<strong>低调度优先级</strong>的Finalize()线程执行队列中对象的finalize()方法。执行是指仅仅触发，并不会等待一个对象执行完，因为假如一个对象的finalize()方法运行缓慢，如果等待的话会影响其他对象的回收。</p><p>finalize()有点类似C/C++的析构函数，这个语法当初就是为了迎合C/C++程序员而做的一项妥协，但这个语法不合适当成析构函数用，因为它运行代价高昂，不确定性大，无法保证各个对象的调用顺序，已经被官方声明为不推荐的语法。</p><p>finalize()方法是对象最后的抢救机会，假如对象在此方法中又与GC Root建立了联系，那么这个对象实例就会逃过这次GC，但一个实例的finalize方法只会被执行一次，所以在下一次GC发起时，这个对象不会再因为finalize方法而再逃一次。</p><h2 id="方法区的回收"><a href="#方法区的回收" class="headerlink" title="方法区的回收"></a>方法区的回收</h2><p>方法区的垃圾回收收益远不及堆区，所以一般不要求虚拟机在方法区中实现垃圾收集，但有些情况下，方法区也会有一些较大的内存压力，那么方法区如何垃圾回收呢？</p><p>从上一篇博客我们知道，方法区主要有两部分数据，<strong>类型信息</strong>和<strong>常量池</strong>，常量池中常量的回收比较简单，类似于对象，但判断一个类型是否属于<strong>不再被使用的类</strong>条件就比较苛刻了，需要同时满足下面三个条件：</p><ul><li>该类所有实例已经被回收，堆中不存在该类或者任何派生类的实例</li><li>加载该类的类加载器已经被回收</li><li>该类对应的<code>java.lang.Class</code>对象没有在任何地方被引用，无法在任何地方通过反射获取这个类的方法</li></ul><p>如果同时满足了上述三个条件，那么该类型便<strong>允许被回收</strong>，而并不是像对象那样，<strong>必然被回收</strong></p><h2 id="垃圾收集算法"><a href="#垃圾收集算法" class="headerlink" title="垃圾收集算法"></a>垃圾收集算法</h2><p>上文说，对象判活有两种算法，引用计数和可达性分析，从这两个算法出发，垃圾收集算法可以分为<strong>引用计数式垃圾收集</strong>和<strong>追踪式垃圾收集</strong>。主流的是追踪式垃圾收集。</p><h3 id="分代收集理论"><a href="#分代收集理论" class="headerlink" title="分代收集理论"></a>分代收集理论</h3><p>在垃圾回收时，虚拟机要关注的对象体量是非常庞大的，假如每次都要遍历一边，无疑会造成很多性能上的损失，于是提出了分代收集理论，将Java堆划分出不同区域，然后将对象按照年龄（年龄即熬过垃圾收集过程的次数）分配到不同区域中，这个理论是建立在两个分代假说之上：</p><ol><li>弱分代假说：绝大多数对象都是朝生夕灭</li><li>强分代假说：熬过越多次垃圾收集的对象越难消亡</li></ol><p>根据这两个假说，大部分虚拟机将Java堆划分成<strong>新生代</strong>和<strong>老年代</strong>，新生代中存放朝生夕灭的对象，每次垃圾回收时都有大批量对象死去，而每次回收后存活的少量对象，将晋升到老年代中存放，老年代对象因为很难消亡，所以只需很低的频次对这块区域进行回收即可，这样就比原先每次遍历整个堆效率提升了很多。</p><p>但仔细思考一下，分代收集真的只是划分一下区域这么简单吗？这里其实存在一个问题，对象不是孤立的，而是存在跨代引用。假如现在要进行一次只针对新生代的收集（Minor GC），但新生代中的对象完全有可能被老年代中对象引用，这样的话，GC Root就得再加上老年代对象，来进行可达性分析，这种方案理论可行，但也会为内存增大很大负担，所以垃圾分代理论有了第三条经验法则：</p><ol><li>跨代引用假说：跨代引用相对于同代引用来说仅占极少数</li></ol><p>有了这条假说，我们就不必为了少量的跨代引用去扫描整个老年代，只需要在新生代上建立一个<strong>记忆集</strong>（Remember Set），这个结构作用是将老年代划分成若干小块，标示出哪一块会存在跨代引用，此后每次进行Minor GC时，只需将跨代引用的那一小块加入GC Root中，这个做法会增加一些运行时常数复杂度的开销，但相比于扫描整个老年代仍是划算的。</p><h3 id="三种垃圾收集算法"><a href="#三种垃圾收集算法" class="headerlink" title="三种垃圾收集算法"></a>三种垃圾收集算法</h3><h4 id="标记-清除算法"><a href="#标记-清除算法" class="headerlink" title="标记-清除算法"></a>标记-清除算法</h4><p>扫描一遍对象之后，标记出哪些可回收哪些不可回收，接着保留不可回收的，清除可回收的，这个算法最为简单，也是后面两个算法的基础，它有几个显而易见的缺点：</p><ul><li>执行效率不稳定，假如Java堆中对象数量很大，并且大部分都是需要回收的，那么要进行大量的标记清除工作，执行效率就会降低</li><li>直接原地清除的话会产生大量不连续的内存碎片，如果内存碎片太多，程序运行也需要分配较大对象的话，可能会出现无法找到足够的连续内存而提前出发GC</li></ul><h4 id="标记-复制算法"><a href="#标记-复制算法" class="headerlink" title="标记-复制算法"></a>标记-复制算法</h4><p>为了解决标记-清除算法面对大量可回收对象时执行效率低的问题，有人提出了<strong>半区复制</strong>算法，将可用内存划为大小相等的两块，当一块内存用完时，将存活的对象复制到另一块空间上，并将自身已经使用的内存空间一次性清理掉。如果内存中大量的对象都是存活的，那么这个算法将产生大量的复制开销，但如果大多数内存都是可回收的，那么算法每次复制的就是少量的存活对象，并且这种算法也不会出现空间碎片的情况。但这个算法还有点不足，内存缩小为原先的二分之一，空间浪费太大。</p><p>因为这种算法适用大多数内存可回收的情况，所以很多商用JVM用这个算法来回收新生代，根据新生代朝生夕灭的特点，有人对这个算法做了改进，弥补了空间浪费这个缺点，被称为<strong>Appel式回收</strong>。</p><p>Appel式回收的具体做法是将新生代划为一块较大的Eden区，两块较小的survivor区，内存大小比例是8:1:1，每次分配内存<strong>只在Eden区和一块survivor区上进行</strong>，发生垃圾收集时，将Eden区和survivor区上的存活对象复制到另一块survivor区上，然后对Eden区和已用的survivor区清理内存，这样的话，每次新生代的空间利用率可以达到90%，假如存活对象太多，一块survivor区不足以承载怎么办呢？Appel式回收提供了一个<strong>逃生门</strong>的设计，如果survivor区空间不够，就会依赖别的内存区域（多数是老年代）进行内存担保，借一块内存。</p><h4 id="标记-整理算法"><a href="#标记-整理算法" class="headerlink" title="标记-整理算法"></a>标记-整理算法</h4><p>这种算法是针对老年代的特点提出的，其标记过程仍与“标记-清除”算法一样，但后续步骤不是对对象进行清理，而是将所有存活对象都向内存空间一端移动，然后直接清除掉边界外的内存。因为老年代中存活的对象很多，所以移动对象并更新对象引用是一个极为负重的操作，并且对象的移动操作必须要<strong>全程暂停用户应用线程</strong>才能进行，这无疑是增加延迟的。但如果和标记-清除算法一样不考虑移动整理存活对象的话，空间碎片化问题只能依赖更为复杂的内存分配器/内存访问器解决（比如像计算机硬盘存储大文件那样），两者均有利弊，那到底是整理内存还是不整理呢？从整个程序的吞吐量来看，因为内存分配和访问相比于垃圾收集频率要大得多，所以权衡利弊，应该用垃圾收集时的短暂延迟换取内存分配访问所耗费的时间复杂度。</p><p>另外，也可以两者兼顾，让虚拟机平时多数时间采用标记-清除算法，当内存碎片化程度高于一定阈值时，再采用标记-整理算法收集一次，换取规整的内存空间。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;常见对象存活判定算法&quot;&gt;&lt;a href=&quot;#常见对象存活判定算法&quot; class=&quot;headerlink&quot; title=&quot;常见对象存活判定算法&quot;&gt;&lt;/a&gt;常见对象存活判定算法&lt;/h2&gt;&lt;h3 id=&quot;引用计数法&quot;&gt;&lt;a href=&quot;#引用计数法&quot; class=&quot;he</summary>
      
    
    
    
    <category term="Java" scheme="https://bodhisatan.github.io/categories/Java/"/>
    
    
    <category term="Java" scheme="https://bodhisatan.github.io/tags/Java/"/>
    
    <category term="JVM" scheme="https://bodhisatan.github.io/tags/JVM/"/>
    
  </entry>
  
  <entry>
    <title>Java虚拟机学习笔记一：对象与JVM</title>
    <link href="https://bodhisatan.github.io/2020/05/29/Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E4%B8%80%EF%BC%9A%E5%AF%B9%E8%B1%A1%E4%B8%8EJVM/"/>
    <id>https://bodhisatan.github.io/2020/05/29/Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E4%B8%80%EF%BC%9A%E5%AF%B9%E8%B1%A1%E4%B8%8EJVM/</id>
    <published>2020-05-28T23:59:08.000Z</published>
    <updated>2020-05-31T08:09:50.787Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Java虚拟机运行时数据区"><a href="#Java虚拟机运行时数据区" class="headerlink" title="Java虚拟机运行时数据区"></a>Java虚拟机运行时数据区</h2><h3 id="程序计数器（线程私有）"><a href="#程序计数器（线程私有）" class="headerlink" title="程序计数器（线程私有）"></a>程序计数器（线程私有）</h3><p>执行字节码的行号指示器</p><h3 id="虚拟机栈（线程私有）"><a href="#虚拟机栈（线程私有）" class="headerlink" title="虚拟机栈（线程私有）"></a>虚拟机栈（线程私有）</h3><p>Java方法执行的本地线程模型：每个方法被执行的时候，Java虚拟机同步创建一个栈帧，里面存放局部变量表，操作数栈，动态链接，方法出口等信息。每个方法从被调用到执行完毕，对应着一个栈桢在虚拟机栈中出栈到入栈的过程。很多人将JVM数据区笼统划分为堆内存和栈内存，栈内存通常就是指虚拟机栈，或者虚拟机栈中局部变量表部分。</p><p>局部变量表里存放两种类型数据，一种是基本类型，一种是引用类型。基本类型有三种，数值类型，布尔类型和returnAddress类型，数值类型和引用类型构成了8大基本数据类型，returnAddress类型指向一条字节码指令的地址，这是干什么用的呢？对于JVM来说，程序是存储在方法区的字节码指令，Java中每个线程私有一个程序计数器，程序计数器的值就是当前指令的地址，该值的类型就是returnAddress</p><h3 id="本地方法栈（线程私有）"><a href="#本地方法栈（线程私有）" class="headerlink" title="本地方法栈（线程私有）"></a>本地方法栈（线程私有）</h3><p>和虚拟机栈类似，差别是本地方法栈执行本地方法，虚拟机栈执行Java方法</p><h3 id="堆（线程共有）"><a href="#堆（线程共有）" class="headerlink" title="堆（线程共有）"></a>堆（线程共有）</h3><p>数据区中最大的一部分，JVM启动的时候被创建，用于存放<strong>几乎所有</strong>对象实例。这也是GC的区域，所以也叫GC堆</p><h3 id="方法区（线程共有）"><a href="#方法区（线程共有）" class="headerlink" title="方法区（线程共有）"></a>方法区（线程共有）</h3><p>用于存储已被虚拟机加载的<strong>类型信息</strong>（类的完整名称、类的直接父类的完整名称、类的直接实现接口的有序列表、类型标志（类类型还是接口类型）类的修饰符）、<strong>常量池</strong>、<strong>静态变量</strong>、即时编译器编译后的<strong>代码</strong>缓存等数据，在JDK8之前，很多人将方法区称为<strong>永久代</strong>，这是因为hotspot虚拟机用永久代实现了方法区，因为永久代经常会溢出，到了jdk8之后，取消了永久代的概念，将方法区移到了<strong>元空间</strong>中，将字符串常量移到了堆内存中</p><h4 id="运行时常量池"><a href="#运行时常量池" class="headerlink" title="运行时常量池"></a>运行时常量池</h4><p>class文件中除了有类的版本、字段、方法、接口等信息之外，还有一个常量池表，用于存放编译器生成的<strong>字面量</strong>和<strong>符号引用</strong>，这个常量池表在类加载后存放到方法区的运行时常量池中。</p><ul><li>字面量：</li></ul><figure class="highlight java"><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">int</span> a = <span class="number">1</span>; <span class="comment">// 1是字面量</span></span><br><span class="line">String s = <span class="string">"a"</span>; <span class="comment">// a是字面量</span></span><br></pre></td></tr></table></figure><ul><li>符号引用：和直接引用相对，在代码编译阶段，编译器并不知道所引用的类的地址，就会用一个符号来代替，在JVM解析class阶段，会将符号引用解析为直接引用（指针/偏移量/句柄）</li></ul><h2 id="hotspot虚拟机与对象"><a href="#hotspot虚拟机与对象" class="headerlink" title="hotspot虚拟机与对象"></a>hotspot虚拟机与对象</h2><h3 id="虚拟机中对象的创建"><a href="#虚拟机中对象的创建" class="headerlink" title="虚拟机中对象的创建"></a>虚拟机中对象的创建</h3><ol><li>Java虚拟机遇到一条字节码new的指令时，先检查这个指令的参数能否在<strong>运行时常量池</strong>中找到对应的符号引用，并检查这个符号引用有没有被解析加载，如果没有，必须要先进行解析加载操作</li><li>虚拟机在堆内存中为该对象实例划分内存空间，这里有两种，一种是连续内存，连续内存里一段是已经使用的内存，一段是空闲内存，之间由一个指针指向分界点，这种划分内存就是让这个指针向空闲内存偏移一段地址，称为<strong>指针碰撞</strong>，另一种是非连续的内存，这种就需要维护一个list，记录哪段内存被占用了哪段没有，这种分配方法称为<strong>空闲列表</strong>。在这一个阶段中，假如是并发状态，分配空间的操作不一定是线程安全的，这时也有两种方案，一种是CAS加锁，另一种是让每个线程在虚拟机中预先分配一块内存，称为本地线程分配缓冲（Thread Local Allocation Buffer, TLAB）</li><li>内存分配完成之后，将这块内存空间都初始化为零值，这就保证了对象实例在Java代码中不赋初始值就能直接使用</li><li>对对象进行必要的设置，这些设置信息存放在<strong>对象头</strong>中</li></ol><h3 id="虚拟机中对象的布局（hotspot为例）"><a href="#虚拟机中对象的布局（hotspot为例）" class="headerlink" title="虚拟机中对象的布局（hotspot为例）"></a>虚拟机中对象的布局（hotspot为例）</h3><p>在JVM中，一个对象在堆内存中的存储布局由三部分组成，<strong>对象头</strong>，<strong>实例数据</strong>，<strong>对齐填充</strong></p><ol><li>对象头：对象头中存储两个方面的数据，<strong>运行时数据</strong>和<strong>类型指针</strong>。运行时数据主要有，GC分代年龄，锁信息，hashcode等，在32位系统中，运行时数据占32个比特，在64位系统中，运行时数据占64个比特，类型指针是一个指向对象的元类型数据的指针（也就是指向存在方法区的类型数据），虚拟机通过这个判断该对象是哪个类的实例。</li><li>实例数据：就是对象中存储的有效信息，也就是从父类继承的字段和自己定义的字段信息，这些字段会被按序分配内存</li><li>对齐填充：为了提高垃圾回收时指针扫描的效率，hotspot自动内存管理系统要求对象起始指针必须是8字节的整数倍，对象头占内存要么是8字节的一倍（32比特），要么是8字节两倍（64比特），如果实例数据不满足内存长度的要求，就会有对齐填充，起到对齐数据的作用</li></ol><h3 id="虚拟机中对象的访问定位"><a href="#虚拟机中对象的访问定位" class="headerlink" title="虚拟机中对象的访问定位"></a>虚拟机中对象的访问定位</h3><p>主要有两种方式：句柄定位，直接指针定位</p><ol><li>句柄定位：上文我们提到，在虚拟机虚拟机栈的局部变量表中，有一个“reference”引用类型，如果是句柄定位的话，reference类型就指向<strong>堆内存中句柄池</strong>里的一个句柄，这个句柄包含两方面信息（指针），对象实例信息，对象类型信息，一个指针指向<strong>堆内存中实例池</strong>里的对象实例，一个指针指向<strong>方法区</strong>的对象类型数据。这个的优点是，如果对象实例被移动（比如垃圾回收时），只会改变句柄中的对应地址，而reference地址是稳定的</li><li>直接指针定位（hotspot主要使用这个方法）：reference存储的就是<strong>堆内存</strong>里对象的地址，这样只需要一步就能直接访问到对象的实例数据，由上文对象的布局我们也可以知道，对象头里有一个类型指针，这个类型指针也会和句柄定位一样，指向方法区的类型数据</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;Java虚拟机运行时数据区&quot;&gt;&lt;a href=&quot;#Java虚拟机运行时数据区&quot; class=&quot;headerlink&quot; title=&quot;Java虚拟机运行时数据区&quot;&gt;&lt;/a&gt;Java虚拟机运行时数据区&lt;/h2&gt;&lt;h3 id=&quot;程序计数器（线程私有）&quot;&gt;&lt;a href=</summary>
      
    
    
    
    <category term="Java" scheme="https://bodhisatan.github.io/categories/Java/"/>
    
    
    <category term="Java" scheme="https://bodhisatan.github.io/tags/Java/"/>
    
    <category term="JVM" scheme="https://bodhisatan.github.io/tags/JVM/"/>
    
  </entry>
  
  <entry>
    <title>关于Go的异常处理</title>
    <link href="https://bodhisatan.github.io/2020/05/25/%E5%85%B3%E4%BA%8EGo%E7%9A%84%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/"/>
    <id>https://bodhisatan.github.io/2020/05/25/%E5%85%B3%E4%BA%8EGo%E7%9A%84%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/</id>
    <published>2020-05-25T00:10:17.000Z</published>
    <updated>2020-05-25T14:05:17.401Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引子"><a href="#引子" class="headerlink" title="引子"></a>引子</h2><p>在Java中，我们可以通过<code>throw</code>、<code>try{}catch{}finally{}</code>进行方便的异常处理，在C++中稍微复杂一些，没有<code>finally</code>语法，因此在遇到程序发生异常但需要关闭资源的时候，在C++中通常两种做法，第一种也是最常用的一种是使用RAII，即<code>Resource Aquisition Is Initialization</code>就是将资源封装成一个类，将资源的初始化封装在构造函数里，释放封装在析构函数里。要在局部使用资源的时候，就实例化一个local object。在抛出异常的时候，由于local object脱离了作用域，自动调用析构函数，这样就保证资源被释放。例如：<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">try</span> &#123;</span><br><span class="line">   <span class="function"><span class="built_in">File</span> <span class="title">f</span><span class="params">(<span class="string">"xxx.ttt"</span>)</span></span>;</span><br><span class="line">    <span class="comment">//other file operation</span></span><br><span class="line">&#125;<span class="comment">//File pointer is released here</span></span><br><span class="line"><span class="keyword">catch</span> &#123;</span><br><span class="line">    <span class="comment">//exception process</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>另一种是沿用的C语言对于异常的处理方法：使用goto语句，将需要释放的资源变量都声明在函数开头部分，并在函数末尾统一释放资源，当函数需要退出时，使用goto语句跳转到指定位置完成资源清理工作，而不调用return直接返回：<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></pre></td><td class="code"><pre><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">0</span>;</span><br><span class="line">    <span class="keyword">int</span> b = <span class="number">0</span>;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"请输入两个值:\n"</span>);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"a = "</span>);</span><br><span class="line">    <span class="built_in">scanf</span>(<span class="string">"%d"</span>,&amp;a);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"b = "</span>);</span><br><span class="line">    <span class="built_in">scanf</span>(<span class="string">"%d"</span>,&amp;b);</span><br><span class="line">    <span class="keyword">if</span>(b==<span class="number">0</span>)&#123;</span><br><span class="line">        <span class="keyword">goto</span> Error;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"a/b = %d\n"</span>,a/b);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">Error:</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"除数不能为0,程序异常退出!\n"</span>);</span><br><span class="line">    <span class="built_in">exit</span>(<span class="number">-1</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>Java处理异常方便，但是它将异常与控制结构混在一起会很容易使得代码变得混乱，开发者也容易滥用异常，对性能会造成影响，代码也不符合Go“简洁优雅”的设计理念；C/C++处理异常需要开发者遵守一套编码规范，如果不遵守的话，维护就会成为很大的问题，Go站在前人肩膀上看问题，提出了一套新的解决方案。</p><h2 id="defer"><a href="#defer" class="headerlink" title="defer"></a>defer</h2><p>defer就相当于finally，defer的特性是，不管会不会发生异常，在函数返回之前，先调用defer函数，如果有多个defer语句，按照先进后出的方式进行执行，需要注意的是，defer语句中的变量，在defer声明时就决定了。</p><p>例如使用defer时的一个坑：</p><figure class="highlight go"><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="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">a := <span class="number">1</span></span><br><span class="line"><span class="keyword">defer</span> fmt.Print(a) <span class="comment">// 因为在defer声明的时候，变量已经确定，所以还是输出1</span></span><br><span class="line">a++</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面我们说到，defer会在函数返回之前被调用，但这并不意味着defer是在return之前被执行，是这样吗？看下面的例子：<br><figure class="highlight go"><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">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">test</span><span class="params">()</span> <span class="params">(res <span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line">res = <span class="number">1</span></span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">res++</span><br><span class="line">&#125;()</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">fmt.Print(test()) <span class="comment">// output: 1</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>这样的原因是，<strong>return不是原子性的</strong>，它包含的过程如下：</p><ol><li>给返回值赋值；</li><li>调用RET返回指令并传入返回值，而RET则会检查defer是否存在，若存在就先逆序插播defer语句，最后RET携带返回值退出函数。</li></ol><p>在这个例子中的话，<code>res = 1 -&gt; res = 0 -&gt; res++</code>，所以res返回值最终是1</p><h2 id="panic和recover"><a href="#panic和recover" class="headerlink" title="panic和recover"></a>panic和recover</h2><p>panic，恐慌，也就是抛出异常；</p><p>recover，恢复，也就是从异常中恢复状态；</p><p>在go里，panic和recover是抛异常和捕获异常的解决方案，panic相当于throw Exception，recover和defer结合起来相当于try…catch…</p><p>先看一个手动引起异常的例子：<br><figure class="highlight go"><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">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">a := []<span class="keyword">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;</span><br><span class="line"><span class="built_in">panic</span>(<span class="string">"触发异常"</span>)</span><br><span class="line">fmt.Print(a[<span class="number">2</span>])</span><br><span class="line">&#125;</span><br><span class="line">-----output-----</span><br><span class="line"><span class="built_in">panic</span>: 触发异常</span><br></pre></td></tr></table></figure><br>panic是golang的内建函数，panic会中断函数F的正常执行流程, 从F函数中跳出来, 跳回到F函数的调用者. 调用者会继续向上跳出, 直到当前goroutine返回，在这个控制器传播过程中，panic详情会积累和完善，并在程序终止之前打印出来。在跳出的过程中, 进程会保持这个函数栈. 当goroutine退出时, 程序会crash。</p><p>要注意的是, F函数中的defered函数会正常执行, 按照上面defer的规则。</p><p>同时引起panic除了我们主动调用panic之外, 其他的任何运行时错误, 例如数组越界都会造成panic，比如：<br><figure class="highlight go"><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="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">a := []<span class="keyword">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;</span><br><span class="line">fmt.Print(a[<span class="number">3</span>])</span><br><span class="line">&#125;</span><br><span class="line">-----output-----</span><br><span class="line"><span class="built_in">panic</span>: runtime error: index out of <span class="keyword">range</span> [<span class="number">3</span>] with length <span class="number">3</span></span><br></pre></td></tr></table></figure><br>recover也是golang的一个内建函数， 其实就是try catch。</p><p>不过需要注意的是：</p><ul><li>recover如果想起作用的话， 必须在defered函数中使用。</li><li>在正常函数执行过程中，调用recover没有任何作用, 他会返回nil。如这样：fmt.Println(recover()) 。（当然了，没捕获到异常肯定是nil了==）</li><li>如果当前的goroutine panic了，那么recover将会捕获这个panic的值，并且<strong>让程序正常执行下去。不会让程序crash。</strong><br>看一个正常的捕获异常的例子：<figure class="highlight go"><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="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> r := <span class="built_in">recover</span>(); r != <span class="literal">nil</span> &#123;</span><br><span class="line">        log.Printf(<span class="string">"Runtime error caught: %v"</span>, r)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;()</span><br><span class="line">foo()</span><br></pre></td></tr></table></figure>无论foo()是否触发了错误处理流程，defer函数都会在函数退出时得到执行，如果没抛出异常，那么r就是nil，如果抛出了异常，recover就能捕获它、处理它。</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;引子&quot;&gt;&lt;a href=&quot;#引子&quot; class=&quot;headerlink&quot; title=&quot;引子&quot;&gt;&lt;/a&gt;引子&lt;/h2&gt;&lt;p&gt;在Java中，我们可以通过&lt;code&gt;throw&lt;/code&gt;、&lt;code&gt;try{}catch{}finally{}&lt;/code&gt;进行方便</summary>
      
    
    
    
    <category term="Go语言" scheme="https://bodhisatan.github.io/categories/Go%E8%AF%AD%E8%A8%80/"/>
    
    
    <category term="golang" scheme="https://bodhisatan.github.io/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>从多种语言看传值传引用</title>
    <link href="https://bodhisatan.github.io/2020/05/04/%E4%BB%8E%E5%A4%9A%E7%A7%8D%E8%AF%AD%E8%A8%80%E7%9C%8B%E4%BC%A0%E5%80%BC%E4%BC%A0%E5%BC%95%E7%94%A8/"/>
    <id>https://bodhisatan.github.io/2020/05/04/%E4%BB%8E%E5%A4%9A%E7%A7%8D%E8%AF%AD%E8%A8%80%E7%9C%8B%E4%BC%A0%E5%80%BC%E4%BC%A0%E5%BC%95%E7%94%A8/</id>
    <published>2020-05-04T06:59:40.000Z</published>
    <updated>2020-05-04T14:16:06.234Z</updated>
    
    <content type="html"><![CDATA[<p>最近学习golang，顺带复习一下传值和传引用的概念。</p><p>传值：函数传递的是参数的一个副本，将传入的变量在内存中复制一份进行操作，本质上是存储在不同内存地址的不同变量</p><p>传引用：传引用是指函数通过内存地址将函数取出进行操作，本质上是存储在相同内存地址的相同变量</p><h2 id="C"><a href="#C" class="headerlink" title="C"></a>C</h2><p>C语言的传参没有传引用，只有传值。那C语言是如何做到通过调用函数修改函数体外的实参的呢？答案是<strong>传址调用</strong>。经常有人把C语言的传值调用和传址调用并列比较，在我看来，传址调用只不过是传值调用的子集。当<strong>传的值是地址</strong>的时候，传值调用就是传址调用了。</p><p>传值调用的时候，函数会给形参a单独分配一个内存，实参a会把值复制到形参a的内存中，对形参的操作是在形参对应的内存里操作，这是完全独立于实参的。如果参数是一个地址的话，这里举一个例子：<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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">modify</span><span class="params">(<span class="keyword">int</span>* a)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"address of pointer is:%p\n"</span>, &amp;a);</span><br><span class="line">    *a = <span class="number">2</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></span><br><span class="line"><span class="function"></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>* p = &amp;a;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"address of pointer is:%p\n"</span>, &amp;p);</span><br><span class="line">    modify(p);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"%d"</span>, a);</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><br>指针是指向变量的地址，假如传入的参数是指针，函数则会为这个指针分配一个空间来存放指针的值（说得绕口一点，就是另分配一个地址来存放变量的地址，是<strong>变量地址的地址</strong>）。所以上述代码的运行结果是<br><figure class="highlight plain"><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">address of pointer is:0x7ffee9cdb490</span><br><span class="line">address of pointer is:0x7ffee9cdb468</span><br><span class="line">2</span><br></pre></td></tr></table></figure><br>也就是说，在两处地址中均存着指向a的指针，地址不同，但存放的指针的值是相同的。通过这个指针，就能读取并修改实参的值。</p><h2 id="Go"><a href="#Go" class="headerlink" title="Go"></a>Go</h2><p>go的函数传参和C类似（毕竟是增强版C语言嘛），是绝对的<strong>传值</strong>，当指针作为函数参数时，调用过程和C也一样，也是临时申请一个空间存放指针的值，然后通过这个指针修改实参。但Go有三种自带的数据类型<strong>map</strong>，<strong>chan</strong>，和<strong>slice</strong>，他们可以认为是一种<strong>引用类型</strong>，虽然调用的时候不需要加&amp;符号，但实际上也是一种<strong>传址</strong>。</p><h3 id="map"><a href="#map" class="headerlink" title="map"></a>map</h3><figure class="highlight go"><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="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">testMap</span><span class="params">(m <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">"inner: %v, %p\n"</span>, m, m)</span><br><span class="line">m[<span class="string">"a"</span>] = <span class="number">11</span></span><br><span class="line">fmt.Printf(<span class="string">"inner: %v, %p\n"</span>, m, m)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">m := <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span>&#123;</span><br><span class="line"><span class="string">"a"</span>: <span class="number">1</span>,</span><br><span class="line"><span class="string">"b"</span>: <span class="number">2</span>,</span><br><span class="line"><span class="string">"c"</span>: <span class="number">3</span>,</span><br><span class="line">&#125;</span><br><span class="line">fmt.Printf(<span class="string">"Outer: %v %p\n"</span>, m, m)</span><br><span class="line">testMap(m)</span><br><span class="line">fmt.Printf(<span class="string">"Outer: %v %p\n"</span>, m, m)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：<br><figure class="highlight plain"><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">Outer: map[a:1 b:2 c:3] 0xc000064180</span><br><span class="line">inner: map[a:1 b:2 c:3], 0xc000064180</span><br><span class="line">inner: map[a:11 b:2 c:3], 0xc000064180</span><br><span class="line">Outer: map[a:11 b:2 c:3] 0xc000064180</span><br></pre></td></tr></table></figure><br>没错，指针是一样的，说明形参m和实参m的地址是一样的，而map并不是以地址形式传入的呀，那为什么这里发生了类似“传引用”的情况呢？看一下makemap的源码就知道：<br><figure class="highlight go"><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="function"><span class="keyword">func</span> <span class="title">makemap</span><span class="params">(t *maptype, hint <span class="keyword">int</span>, h *hmap)</span> *<span class="title">hmap</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> hint &lt; <span class="number">0</span> || hint &gt; <span class="keyword">int</span>(maxSliceCap(t.bucket.size)) &#123;</span><br><span class="line">        hint = <span class="number">0</span></span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br></pre></td></tr></table></figure><br>可以看到，makemap返回的类型是一个指针类型<code>*hmap</code>，也就是说：testMap(map)实际上等同于testMap(*hmap)。因此，在golang中，当map作为形参时，虽然是值传递，但是由于make()返回的是一个指针类型，也就和传指针是一样的效果了。</p><h3 id="chan"><a href="#chan" class="headerlink" title="chan"></a>chan</h3><p>chan类型和map类型类似，看一下makechan源码：<br><figure class="highlight go"><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="function"><span class="keyword">func</span> <span class="title">makechan</span><span class="params">(t *chantype, size <span class="keyword">int</span>)</span> *<span class="title">hchan</span></span> &#123;</span><br><span class="line">    elem := t.elem</span><br><span class="line">  ...</span><br></pre></td></tr></table></figure><br>也就是make() chan的返回值为一个hchan类型的指针，因此当我们的业务代码在函数内对channel操作的同时，也会影响到函数外的数值。</p><h3 id="slice"><a href="#slice" class="headerlink" title="slice"></a>slice</h3><p>slice和map/chan略有区别，先看一个例子：<br><figure class="highlight go"><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="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"></span><br><span class="line">sl := []<span class="keyword">string</span>&#123;</span><br><span class="line"><span class="string">"a"</span>,</span><br><span class="line"><span class="string">"b"</span>,</span><br><span class="line"><span class="string">"c"</span>,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">"%v, %p\n"</span>, sl, sl)</span><br><span class="line">test_slice(sl)</span><br><span class="line">fmt.Printf(<span class="string">"%v, %p\n"</span>, sl, sl)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">test_slice</span><span class="params">(sl []<span class="keyword">string</span>)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">"%v, %p\n"</span>, sl, sl)</span><br><span class="line">    sl[<span class="number">0</span>] = <span class="string">"aa"</span></span><br><span class="line">    fmt.Printf(<span class="string">"%v, %p\n"</span>, sl, sl)</span><br><span class="line">sl = <span class="built_in">append</span>(sl, <span class="string">"d"</span>)</span><br><span class="line">fmt.Printf(<span class="string">"%v, %p\n"</span>, sl, sl)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>运行结果：<br><figure class="highlight plain"><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">[a b c], 0xc000090180</span><br><span class="line">[a b c], 0xc000090180</span><br><span class="line">[aa b c], 0xc000090180</span><br><span class="line">[aa b c d], 0xc0000b0120</span><br><span class="line">[aa b c], 0xc000090180</span><br></pre></td></tr></table></figure><br>可以看出，修改的部分生效了，但append追加的部分却没有生效，这是因为，slice在Go的实现中，是一个结构体，对应源码如下：<br><figure class="highlight go"><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">type</span> slice <span class="keyword">struct</span> &#123;</span><br><span class="line">    array unsafe.Pointer</span><br><span class="line">    <span class="built_in">len</span>   <span class="keyword">int</span></span><br><span class="line">    <span class="built_in">cap</span>   <span class="keyword">int</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>array是具体数据，len和cap分别是长度和承载量。数据array部分由于是指针类型，所以在函数内部对slice数据的修改是可以生效的。而同一时刻，表示长度的len和容量的cap均为int类型，那么在传递到函数内部的就仅仅只是一个副本，因此在函数内部通过append修改了len的数值，影响不到函数外部slice的len变量。</p><p>也就是说，slice还是<strong>以值的形式</strong>传入函数中的，函数对slice实参进行拷贝得到了一个slice形参，在对slice形参修改的过程中，因为array部分是指针，所以能修改到实参里的array，而len和cap是int类型，所以只能修改副本。那既然形参是实参的拷贝，为什么打印出来的地址值是一样的呢？这就是printf的实现细节了，继续看源码：<br><figure class="highlight go"><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">func</span> <span class="params">(v Value)</span> <span class="title">Pointer</span><span class="params">()</span> <span class="title">uintptr</span></span> &#123;</span><br><span class="line">    <span class="comment">// <span class="doctag">TODO:</span> deprecate</span></span><br><span class="line">    k := v.kind()</span><br><span class="line">    <span class="keyword">switch</span> k &#123;</span><br><span class="line">    <span class="keyword">case</span> Chan, Map, Ptr, UnsafePointer:</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">uintptr</span>(v.pointer())</span><br><span class="line">    <span class="keyword">case</span> Func:</span><br><span class="line">        ...</span><br><span class="line">    <span class="keyword">case</span> Slice:</span><br><span class="line">        <span class="keyword">return</span> (*SliceHeader)(v.ptr).Data</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>我们可以看到，在打印的时候，对于slice类型的数据，打印的是Data的地址值，也就是前文array的地址值。所以，<strong>虽然slice形参和slice实参实际的地址并不一样，但是由于array地址一样，所以输出的地址就是一样的</strong>。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>Go只能传值，指针的传值类似C语言，map和chan因为底层就是指针，所以<strong>看上去像传引用，实际上只不过传的是经过封装的指针</strong>，而slice，底层是结构体（类），在传参过程中，妥妥的传值、拷贝，但是由于成员变量array是地址，所以能影响到实参，又因为fmt打印地址的时候，输出的是array的地址，所以<strong>看似形参实参地址相同</strong></p><h3 id="是不是Go里没有引用呢？"><a href="#是不是Go里没有引用呢？" class="headerlink" title="是不是Go里没有引用呢？"></a>是不是Go里没有引用呢？</h3><p>在之前讲闭包的时候，提到过一个<strong>引用环境</strong>的概念，引用环境就是对自由变量的引用，例如：<br><figure class="highlight go"><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="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">swap_</span><span class="params">(fn <span class="keyword">func</span>()</span>)</span> &#123;</span><br><span class="line">fn()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">a := <span class="number">0</span></span><br><span class="line">b := <span class="number">1</span></span><br><span class="line">fmt.Println(<span class="string">"a: "</span>, a)</span><br><span class="line">swap := <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">a, b = b, a</span><br><span class="line">&#125;</span><br><span class="line">swap_(swap)</span><br><span class="line">fmt.Println(<span class="string">"a: "</span>, a)</span><br><span class="line">swap_(swap)</span><br><span class="line">fmt.Println(<span class="string">"a: "</span>, a)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>输出：<br><figure class="highlight plain"><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">a:  0</span><br><span class="line">a:  1</span><br><span class="line">a:  0</span><br></pre></td></tr></table></figure><br>由此可见，a和b被捕获到了闭包中，闭包中的逻辑之所以可以具有<strong>记忆性</strong>，就是因为通过<strong>引用</strong>修改了变量，变量随着闭包的生命周期存在</p><h2 id="C-1"><a href="#C-1" class="headerlink" title="C++"></a>C++</h2><p>C++在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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">modify</span><span class="params">(<span class="keyword">int</span>&amp; a)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    a = <span class="number">2</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></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">int</span> a = <span class="number">1</span>;</span><br><span class="line">    modify(a);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"%d"</span>, a); <span class="comment">// 输出2</span></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><br>引用传递的形参加一个&amp;符号，这个形参相当于实参的一个别名，对形参的操作都相当于对实参的操作。</p><h2 id="Java"><a href="#Java" class="headerlink" title="Java"></a>Java</h2><p>先得解释一下Java的一些术语</p><ol><li>基本数据类型、引用类型定义<ul><li>基本数据类：Java 中有八种基本数据类型“byte、short、int、long、float、double、char、boolean”</li><li>引用类型：new 创建的实体类、对象、及数组</li></ul></li><li>基本数据类型、引用类型在内存中的存储方式<ul><li>基本数据类型：存放在栈内存中。用完就消失。</li><li>引用类型：在栈内存中存放引用堆内存的地址，在堆内存中存储类、对象、数组等。当没用引用指向堆内存中的类、对象、数组时，由 GC回收机制不定期自动清理。</li></ul></li></ol><p>Java这里也很类似Go，Java也是<strong>只有值传递</strong>。Java没有指针，和<strong>Go里传入参数是指针（引用类型）的情况相似</strong>，当Java传入引用类型时，会先给形参一个与实参相同的地址(此处<strong>与 C++ 的不同之处是</strong>，C++ 是别名，没有在内存中给形参开辟空间，而 Java 给形参开辟了一个栈内存空间，存放与实参相同的引用地址)。Java在进行方法调用修改了引用类型形参后，会影响到实参的值，这和Go的结果也是一样的。</p><p>但是Java有一点<strong>要注意的是</strong>，一些<strong>特殊的类</strong>，比如String，包装类，虽然是<strong>引用类型</strong>，但是它们每次赋值的时候会重新创建对象，也就是说，它们身为<strong>引用类型</strong>的特性已经被破坏了，修改形参是不能对实参构成影响的。</p><h2 id="Python"><a href="#Python" class="headerlink" title="Python"></a>Python</h2><p>python目前还不熟悉，待填坑～</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近学习golang，顺带复习一下传值和传引用的概念。&lt;/p&gt;
&lt;p&gt;传值：函数传递的是参数的一个副本，将传入的变量在内存中复制一份进行操作，本质上是存储在不同内存地址的不同变量&lt;/p&gt;
&lt;p&gt;传引用：传引用是指函数通过内存地址将函数取出进行操作，本质上是存储在相同内存地址</summary>
      
    
    
    
    <category term="Go语言" scheme="https://bodhisatan.github.io/categories/Go%E8%AF%AD%E8%A8%80/"/>
    
    
    <category term="指针" scheme="https://bodhisatan.github.io/tags/%E6%8C%87%E9%92%88/"/>
    
    <category term="编程语言" scheme="https://bodhisatan.github.io/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>函数、指针、闭包再探讨（待填坑）</title>
    <link href="https://bodhisatan.github.io/2020/05/03/%E5%87%BD%E6%95%B0%E3%80%81%E6%8C%87%E9%92%88%E3%80%81%E9%97%AD%E5%8C%85%E5%86%8D%E6%8E%A2%E8%AE%A8%EF%BC%88%E5%BE%85%E5%A1%AB%E5%9D%91%EF%BC%89/"/>
    <id>https://bodhisatan.github.io/2020/05/03/%E5%87%BD%E6%95%B0%E3%80%81%E6%8C%87%E9%92%88%E3%80%81%E9%97%AD%E5%8C%85%E5%86%8D%E6%8E%A2%E8%AE%A8%EF%BC%88%E5%BE%85%E5%A1%AB%E5%9D%91%EF%BC%89/</id>
    <published>2020-05-03T14:11:47.000Z</published>
    <updated>2020-05-04T14:13:21.769Z</updated>
    
    <content type="html"><![CDATA[<p>在之前的博客里分别探究了一下闭包和指针的概念，今天将函数、闭包和指针贯通起来，从C，Python，Java和Go这些不同编程语言的角度，深究一下这三者间的联系。</p><h2 id="函数是什么"><a href="#函数是什么" class="headerlink" title="函数是什么"></a>函数是什么</h2><p>函数，是内存里一段可执行代码。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在之前的博客里分别探究了一下闭包和指针的概念，今天将函数、闭包和指针贯通起来，从C，Python，Java和Go这些不同编程语言的角度，深究一下这三者间的联系。&lt;/p&gt;
&lt;h2 id=&quot;函数是什么&quot;&gt;&lt;a href=&quot;#函数是什么&quot; class=&quot;headerlink&quot; t</summary>
      
    
    
    
    <category term="编程语言" scheme="https://bodhisatan.github.io/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
    
    <category term="闭包" scheme="https://bodhisatan.github.io/tags/%E9%97%AD%E5%8C%85/"/>
    
    <category term="指针" scheme="https://bodhisatan.github.io/tags/%E6%8C%87%E9%92%88/"/>
    
    <category term="函数" scheme="https://bodhisatan.github.io/tags/%E5%87%BD%E6%95%B0/"/>
    
  </entry>
  
  <entry>
    <title>FFmpeg对SSIM的实现</title>
    <link href="https://bodhisatan.github.io/2020/05/03/FFmpeg%E5%AF%B9SSIM%E7%9A%84%E5%AE%9E%E7%8E%B0/"/>
    <id>https://bodhisatan.github.io/2020/05/03/FFmpeg%E5%AF%B9SSIM%E7%9A%84%E5%AE%9E%E7%8E%B0/</id>
    <published>2020-05-03T04:45:16.000Z</published>
    <updated>2020-05-03T04:52:52.662Z</updated>
    
    <content type="html"><![CDATA[<p>源码：tiny_ssim.c</p><p>源码链接：<a href="https://github.com/bodhisatan/LearnSSIM/blob/master/test_ssim.cpp" target="_blank" rel="noopener">https://github.com/bodhisatan/LearnSSIM/blob/master/test_ssim.cpp</a></p><p>友链：<a href="https://wangwei1237.github.io/2020/02/15/how-to-calculate-the-SSIM-in-FFMpeg/" target="_blank" rel="noopener">https://wangwei1237.github.io/2020/02/15/how-to-calculate-the-SSIM-in-FFMpeg/</a></p><p>代码前面叭叭叭说了一大堆，有用的只有两句，一是告诉了我们输入格式：两个YV12（YUV420P）格式视频文件，二是告诉我们为了提升速度，代码没有用论文里的高斯卷积核作加权平均，而是用了8x8有重叠的像素块求和的方法。</p><p>读源码，先从main函数读起</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></pre></td><td class="code"><pre><span class="line">FILE *f[<span class="number">2</span>];</span><br><span class="line"><span class="keyword">uint8_t</span> *buf[<span class="number">2</span>], *plane[<span class="number">2</span>][<span class="number">3</span>];</span><br><span class="line">   <span class="keyword">int</span> *temp;</span><br><span class="line">   <span class="keyword">uint64_t</span> ssd[<span class="number">3</span>] = &#123;<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>&#125;;</span><br><span class="line">   <span class="keyword">double</span> ssim[<span class="number">3</span>] = &#123;<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>&#125;;</span><br><span class="line">   <span class="keyword">int</span> frame_size, w, h;</span><br><span class="line">   <span class="keyword">int</span> frames, <span class="built_in">seek</span>;</span><br><span class="line">   <span class="keyword">int</span> i;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">if</span> (argc &lt; <span class="number">4</span> || <span class="number">2</span> != <span class="built_in">sscanf</span>(argv[<span class="number">3</span>], <span class="string">"%dx%d"</span>, &amp;w, &amp;h))</span><br><span class="line">   &#123;</span><br><span class="line">       <span class="built_in">printf</span>(<span class="string">"tiny_ssim &lt;file1.yuv&gt; &lt;file2.yuv&gt; &lt;width&gt;x&lt;height&gt; [&lt;seek&gt;]\n"</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">   f[<span class="number">0</span>] = fopen(argv[<span class="number">1</span>], <span class="string">"rb"</span>);</span><br><span class="line">   f[<span class="number">1</span>] = fopen(argv[<span class="number">2</span>], <span class="string">"rb"</span>);</span><br><span class="line">   <span class="built_in">sscanf</span>(argv[<span class="number">3</span>], <span class="string">"%dx%d"</span>, &amp;w, &amp;h);</span><br><span class="line"></span><br><span class="line">   <span class="keyword">if</span> (w &lt;= <span class="number">0</span> || h &lt;= <span class="number">0</span> || w * (<span class="keyword">int64_t</span>)h &gt;= INT_MAX / <span class="number">3</span> || <span class="number">2L</span>L * w + <span class="number">12</span> &gt;= INT_MAX / <span class="keyword">sizeof</span>(*temp))</span><br><span class="line">   &#123;</span><br><span class="line">       <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">"Dimensions are too large, or invalid\n"</span>);</span><br><span class="line">       <span class="keyword">return</span> <span class="number">-2</span>;</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><p>先进行了变量定义和初始化，规定了程序运行格式，读入了两个文件f[0]和f[1]，以及视频长宽w，h。</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></pre></td><td class="code"><pre><span class="line">frame_size = w * h * <span class="number">3L</span>L / <span class="number">2</span>;</span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; <span class="number">2</span>; i++)</span><br><span class="line">&#123;</span><br><span class="line">        buf[i] = (<span class="keyword">uint8_t</span> *)<span class="built_in">malloc</span>(frame_size);</span><br><span class="line">        plane[i][<span class="number">0</span>] = buf[i];</span><br><span class="line">        plane[i][<span class="number">1</span>] = plane[i][<span class="number">0</span>] + w * h;</span><br><span class="line">        plane[i][<span class="number">2</span>] = plane[i][<span class="number">1</span>] + w * h / <span class="number">4</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这句话就要联系我上篇wiki中关于YUV420P的知识，视频文件在计算机中被读取/计算的时候，数据是以字节流的形式存储的，frame_size就是一帧视频占内存的大小。Y占w*h字节，U和V都是占1/4*w*h个字节，合起来就是1.5*w*h个字节。</p><p>一帧数据读进内存之后，分离出Y，U，V三个分量。因为YUV420P是planar格式存储的，先Y然后U最后V，所以只需要读取的时候加一个内存偏移量就能分别读到这三个分量。plane[i][0]存储Y分量信息，plane[i][1]存储U分量信息，plane[i][2]存储V分量信息。i取0和1，这是用来区分两个输入文件的。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (frames = <span class="number">0</span>;; frames++)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">uint64_t</span> ssd_one[<span class="number">3</span>]; </span><br><span class="line">    <span class="keyword">double</span> ssim_one[<span class="number">3</span>]; </span><br><span class="line">    <span class="keyword">if</span> (fread(buf[<span class="number">0</span>], frame_size, <span class="number">1</span>, f[<span class="number">0</span>]) != <span class="number">1</span>)</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">if</span> (fread(buf[<span class="number">1</span>], frame_size, <span class="number">1</span>, f[<span class="number">1</span>]) != <span class="number">1</span>)</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; <span class="number">3</span>; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        ssd_one[i] = ssd_plane(plane[<span class="number">0</span>][i], plane[<span class="number">1</span>][i], w * h &gt;&gt; <span class="number">2</span> * !!i);</span><br><span class="line">        ssim_one[i] = ssim_plane(plane[<span class="number">0</span>][i], w &gt;&gt; !!i,</span><br><span class="line">                                 plane[<span class="number">1</span>][i], w &gt;&gt; !!i,</span><br><span class="line">                                 w &gt;&gt; !!i, h &gt;&gt; !!i, temp, <span class="literal">NULL</span>);</span><br><span class="line">        ssd[i] += ssd_one[i];</span><br><span class="line">        ssim[i] += ssim_one[i];</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">"Frame %d | "</span>, frames);</span><br><span class="line">    print_results(ssd_one, ssim_one, <span class="number">1</span>, w, h);</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">"                \r"</span>);</span><br><span class="line">    fflush(<span class="built_in">stdout</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>ssd_one和ssime_one代表一帧数据的ssd与ssim结果，ssd的含义在前一篇wiki中也解释过了。他们都是大小为3的数组，因为Y，U，V三个数据分量需要分别计算存储。接下来两行fread，读取数据送入buf中，但因为前文的代码里Y分量的起始存储位置就是从buf开始的，所以plane数组就自动的获得了数据。不得不感叹代码的精妙！</p><p>接下来是一个i从0到3的循环，i=0时比较Y，i=1时比较U，i=2时比较V，ssd_plane是按plane计算ssd，ssim_plane是按照plane计算ssim，ssd_plane代码如下：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">uint64_t</span> <span class="title">ssd_plane</span><span class="params">(<span class="keyword">const</span> <span class="keyword">uint8_t</span> *pix1, <span class="keyword">const</span> <span class="keyword">uint8_t</span> *pix2, <span class="keyword">int</span> <span class="built_in">size</span>)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">uint64_t</span> ssd = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; <span class="built_in">size</span>; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">int</span> d = pix1[i] - pix2[i];</span><br><span class="line">        ssd += d * d;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> ssd;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>先分析上文调用的语句<code>ssd_one[i] = ssd_plane(plane[0][i], plane[1][i], w * h &gt;&gt; 2 * !!i);</code> <code>plane[0][i]</code>表示原视频的像素信息起始地址，<code>plane[1][i]</code>表示重建后视频的像素信息起始地址，<code>w*h &gt;&gt; 2*!!i</code>表示这段信息的大小，这里的<code>!!i</code>就有点让人费解，仔细一想，再次不得不叹服这段代码的巧妙。<code>!!i</code>在i=0时值是0，大于0时值是1，所以这就巧妙地得到了Y向量信息占内存的大小（w<em>h），U/V向量占内存的大小（w \</em>h &gt;&gt; 2）。</p><p>ssd的计算分析完了，现在开始分析ssim的计算，先看一下所调函数ssim_plane的代码</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">float</span> <span class="title">ssim_plane</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params">    pixel *pix1, <span class="keyword">intptr_t</span> stride1,</span></span></span><br><span class="line"><span class="function"><span class="params">    pixel *pix2, <span class="keyword">intptr_t</span> stride2,</span></span></span><br><span class="line"><span class="function"><span class="params">    <span class="keyword">int</span> <span class="built_in">width</span>, <span class="keyword">int</span> <span class="built_in">height</span>, <span class="keyword">void</span> *buf, <span class="keyword">int</span> *cnt)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">int</span> z = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">int</span> x, y;</span><br><span class="line">    <span class="keyword">float</span> ssim = <span class="number">0.0</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">int</span>(*sum0)[<span class="number">4</span>] = (<span class="keyword">int</span>(*)[<span class="number">4</span>])buf; </span><br><span class="line">    <span class="keyword">int</span>(*sum1)[<span class="number">4</span>] = sum0 + (<span class="built_in">width</span> &gt;&gt; <span class="number">2</span>) + <span class="number">3</span>;</span><br><span class="line">    <span class="built_in">width</span> &gt;&gt;= <span class="number">2</span>;</span><br><span class="line">    <span class="built_in">height</span> &gt;&gt;= <span class="number">2</span>; </span><br><span class="line">    <span class="keyword">for</span> (y = <span class="number">1</span>; y &lt; <span class="built_in">height</span>; y++)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">for</span> (; z &lt;= y; z++)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="comment">// FFSWAP( (int (*)[4]), sum0, sum1 );</span></span><br><span class="line">            <span class="keyword">int</span>(*tmp)[<span class="number">4</span>] = sum0;</span><br><span class="line">            sum0 = sum1;</span><br><span class="line">            sum1 = tmp;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">for</span> (x = <span class="number">0</span>; x &lt; <span class="built_in">width</span>; x += <span class="number">2</span>)</span><br><span class="line">                ssim_4x4x2_core(&amp;pix1[<span class="number">4</span> * (x + z * stride1)], stride1, &amp;pix2[<span class="number">4</span> * (x + z * stride2)], stride2, &amp;sum0[x]);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">for</span> (x = <span class="number">0</span>; x &lt; <span class="built_in">width</span> - <span class="number">1</span>; x += <span class="number">4</span>)</span><br><span class="line">            ssim += ssim_end4(sum0 + x, sum1 + x, FFMIN(<span class="number">4</span>, <span class="built_in">width</span> - x - <span class="number">1</span>));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//     *cnt = (height-1) * (width-1);</span></span><br><span class="line">    <span class="keyword">return</span> ssim / ((<span class="built_in">height</span> - <span class="number">1</span>) * (<span class="built_in">width</span> - <span class="number">1</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上文调用它的语句是<code>ssim_one[i] = ssim_plane(plane[0][i], w &gt;&gt; !!i, plane[1][i], w &gt;&gt; !!i, w &gt;&gt; !!i, h &gt;&gt; !!i, temp, NULL);</code>对于U和V向量来说，他们的内存占比是四分之一w*h，那么在填写width和height参数的时候就要分别取w和h的二分之一。</p><p>然后看ssim_plane函数体，这个函数是按照4x4的块对像素进行处理的，使用sum1保存上一行块的“信息”，sum0保存当前一行块的“信息”。sum0、sum1是一个数组指针，其中存储了一个4元素数组的地址，换句话说，sum0、sum1中每一个元素对应一个4x4块的信息（该信息包含4个元素）。<strong>4个元素中，[0]代表原始像素之和，[1]代表重建像素之和，[2]代表原始像素平方之和+重建像素平方之和，[3]代表原始像素*重建像素的值的和</strong>。然后width和height分别右移两位（÷4），因为这一步的计算是以4<em>4的像素块为基本单位的。然后进入循环，看到这句话，<code>for (; z &lt;= y; z++)</code>在这个函数开头，定义了z=0，也就意味着这个循环体里的语句在第一次执行时会执行两次，其他时候就会执行一次（妙啊），为什么要执行两次呢？因为sum0存储的是一行里4x4块的信息，sum1里存着上一行里的4x4块的信息，在下文的ssim_4x4x2_core运算中，是要将这两行合起来，计算<em>*有重叠8x8像素块的信息</em></em>。在这一步循环中<code>for (x = 0; x &lt; width; x += 2)</code>，x每次加2，也就是每次前进两个像素，然后在ssim_4x4x2_core中计算两个4x4的像素块，也就是说，在一行中，这些4x4的像素块都是两两重叠的。</p><p>接下来进入这个循环：<code>for (x = 0; x &lt; width - 1; x += 4)</code>，x每次+4，步长为4，每次跳过4*4个int，进入ssim_end4:</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">float</span> <span class="title">ssim_end4</span><span class="params">(<span class="keyword">int</span> sum0[<span class="number">5</span>][<span class="number">4</span>], <span class="keyword">int</span> sum1[<span class="number">5</span>][<span class="number">4</span>], <span class="keyword">int</span> <span class="built_in">width</span>)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">float</span> ssim = <span class="number">0.0</span>;</span><br><span class="line">    <span class="keyword">int</span> i;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; <span class="built_in">width</span>; i++)</span><br><span class="line">        ssim += ssim_end1(sum0[i][<span class="number">0</span>] + sum0[i + <span class="number">1</span>][<span class="number">0</span>] + sum1[i][<span class="number">0</span>] + sum1[i + <span class="number">1</span>][<span class="number">0</span>],</span><br><span class="line">                          sum0[i][<span class="number">1</span>] + sum0[i + <span class="number">1</span>][<span class="number">1</span>] + sum1[i][<span class="number">1</span>] + sum1[i + <span class="number">1</span>][<span class="number">1</span>],</span><br><span class="line">                          sum0[i][<span class="number">2</span>] + sum0[i + <span class="number">1</span>][<span class="number">2</span>] + sum1[i][<span class="number">2</span>] + sum1[i + <span class="number">1</span>][<span class="number">2</span>],</span><br><span class="line">                          sum0[i][<span class="number">3</span>] + sum0[i + <span class="number">1</span>][<span class="number">3</span>] + sum1[i][<span class="number">3</span>] + sum1[i + <span class="number">1</span>][<span class="number">3</span>]);</span><br><span class="line">    <span class="keyword">return</span> ssim;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>一开始的时候，我因为指针不熟悉，有这样一个疑问，在ssim_4x4x2_core中，每次计算的是<code>sums[0][0~3]</code>和<code>sums[1][0~3]</code>，每次都计算一样的值吗，循环变量x的变化又体现在哪里呢？以及，在ssim_end4中，参与运算的是<code>sum0[0~width][0~3]</code>啊，在ssim_4x4x2_core里，只计算了<code>sum[0~1][0~3]</code>啊，很多在这步需要参与运算的值，在之前都没计算过啊。一番查资料，才知道：</p><p>ssim_4x4x2_core里的sums和ssim_end4里的sum意义并不完全相同，和ssim_plane里的sum意义也不相同。sum0和sum1是一个<code>int[*][4]</code>，也就是指向int[4]数组的指针，C/C++里，二位数组在传参的时候，第一个维度是可以不写的（写是为了起提示作用），也就是说，ssim_4x4x2_core里的<code>int sums[2][4]</code>和ssim_end4里的<code>int sum0[5][4], int sum1[5][4]</code>在数据类型上并没有什么不同，都是<code>int[*][4]</code>，在for语句中，循环变量x第一次+2，第二次+4，因为x变了，指针的首地址也就变了，所以每次在ssim_4x4x2_core里计算的sums虽然看似每次都是计算<code>sums[0~1][0~3]</code>，实际上每次计算的都是不同的4x4块的信息。理解了这个，对应的ssim_end4里的变量sum0/sum1也就可以理解了。</p><p>然后，就到了最终汇总结果，通过sums求ssim的部分：ssim_end1:</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">float</span> <span class="title">ssim_end1</span><span class="params">(<span class="keyword">int</span> s1, <span class="keyword">int</span> s2, <span class="keyword">int</span> ss, <span class="keyword">int</span> s12)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line"><span class="comment">/* Maximum value for 10-bit is: ss*64 = (2^10-1)^2*16*4*64 = 4286582784, which will overflow in some cases.</span></span><br><span class="line"><span class="comment"> * s1*s1, s2*s2, and s1*s2 also obtain this value for edge cases: ((2^10-1)*16*4)^2 = 4286582784.</span></span><br><span class="line"><span class="comment"> * Maximum value for 9-bit is: ss*64 = (2^9-1)^2*16*4*64 = 1069551616, which will not overflow. */</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">if</span> BIT_DEPTH &gt; 9</span></span><br><span class="line">    <span class="keyword">typedef</span> <span class="keyword">float</span> type;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">const</span> <span class="keyword">float</span> ssim_c1 = <span class="number">.01</span> * <span class="number">.01</span> * PIXEL_MAX * PIXEL_MAX * <span class="number">64</span>;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">const</span> <span class="keyword">float</span> ssim_c2 = <span class="number">.03</span> * <span class="number">.03</span> * PIXEL_MAX * PIXEL_MAX * <span class="number">64</span> * <span class="number">63</span>;</span><br><span class="line"><span class="meta">#<span class="meta-keyword">else</span></span></span><br><span class="line">    <span class="keyword">typedef</span> <span class="keyword">int</span> type;</span><br><span class="line">    <span class="comment">// k1=0.01, k2=0.03</span></span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">const</span> <span class="keyword">int</span> ssim_c1 = (<span class="keyword">int</span>)(<span class="number">.01</span> * <span class="number">.01</span> * PIXEL_MAX * PIXEL_MAX * <span class="number">64</span> + <span class="number">.5</span>);</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">const</span> <span class="keyword">int</span> ssim_c2 = (<span class="keyword">int</span>)(<span class="number">.03</span> * <span class="number">.03</span> * PIXEL_MAX * PIXEL_MAX * <span class="number">64</span> * <span class="number">63</span> + <span class="number">.5</span>);</span><br><span class="line"><span class="meta">#<span class="meta-keyword">endif</span></span></span><br><span class="line">    type fs1 = s1;</span><br><span class="line">    type fs2 = s2;</span><br><span class="line">    type fss = ss;</span><br><span class="line">    type fs12 = s12;</span><br><span class="line">    type vars = fss * <span class="number">64</span> - fs1 * fs1 - fs2 * fs2;</span><br><span class="line">    type covar = fs12 * <span class="number">64</span> - fs1 * fs2;</span><br><span class="line">    <span class="keyword">return</span> (<span class="keyword">float</span>)(<span class="number">2</span> * fs1 * fs2 + ssim_c1) * (<span class="keyword">float</span>)(<span class="number">2</span> * covar + ssim_c2) / ((<span class="keyword">float</span>)(fs1 * fs1 + fs2 * fs2 + ssim_c1) * (<span class="keyword">float</span>)(vars + ssim_c2));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>由上文分析我们可以得知：</p><script type="math/tex; mode=display">s1=\sum\sum a(i,j)=fs1\\s2=\sum\sum b(i,j)=fs2\\ss=\sum\sum [b(i,j) ^ 2 + a(i,j) ^ 2]=fss\\s12=\sum\sum a(i,j)*b(i,j)=fs12\\</script><p>而我们从上篇Wiki可以知道SSIM的化简公式：</p><script type="math/tex; mode=display">SSIM(a,b)=\frac{(2\mu_a\mu_b+C_1)(2\sigma_{ab}+C_2)}{(\mu_a^2+\mu_b^2+C_1)(\sigma_a^2+\sigma_b^2+C_2)}</script><p>需要将这个表达式用fs1,fs2,fss,fs12变量表示。先计算均值，方差，协方差：</p><script type="math/tex; mode=display">\mu_a=\frac{1}{64}fs1\\\mu_b=\frac{1}{64}fs2\\\sigma_a^2+\sigma_b^2=\frac{1}{63}(\sum_{i,j}(a(i,j)-\mu_a)^2 + \sum_{i,j}(b(i,j)-\mu_b)^2)\\=\frac{1}{63}\sum_{i,j}(a(i,j)^2+\mu_a^2-2*a(i,j)*\mu_a+b(i,j)^2+\mu_b^2-2*b(i,j)*\mu_b)\\=\frac{1}{63}(\sum_{i,j}(a(i,j)^2+b(i,j)^2)-2(\mu_a\sum_{i,j}a(i,j)+\mu_b\sum_{i,j}b(i,j))+\sum_{i,j}(\mu_a^2+\mu_b^2))\\=\frac{1}{63}(fss-2*\frac{1}{64}(fs1^2+fs2^2)+\frac{1}{64}(fs1^2+fs2^2)\\=\frac{1}{63}(fss-\frac{1}{64}(fs1^2+fs2^2))\\=\frac{1}{63}\frac{1}{64}(64fss-fs1^2-fs2^2)\\\sigma_{ab}=\frac{1}{63}\sum_{i,j}((a(i,j)-\mu_a)(b(i,j)-\mu_b))\\=\frac{1}{63}\sum_{i,j}(a(i,j)*b(i,j)-a(i,j)\mu_b-b(i,j)\mu_a+\mu_a*\mu_b)\\=\frac{1}{63}(fs12-fs1*\mu_b-fs2*\mu_a+fs1*\mu_b)\\=\frac{1}{63}(fs12-\frac{1}{64}fs1*fs2)\\=\frac{1}{63}\frac{1}{64}(64fs12-fs1*fs2)</script><p>带入SSIM公式中，分子分母左边的项约去1/(64*64)，右边的项约去1/(63*64)：</p><script type="math/tex; mode=display">SSIM(a,b)=\frac{(2fs1*fs2+64^2C_1)(2*64fs12-2fs1fs2+63*64C_2)}{(fs1^2+fs2^2+64^2C_1)(64fss-s1^2-s2^2+63*64C_2)}</script><p>这就是代码里的：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(<span class="keyword">float</span>)(<span class="number">2</span> * fs1 * fs2 + ssim_c1) * (<span class="keyword">float</span>)(<span class="number">2</span> * covar + ssim_c2) / ((<span class="keyword">float</span>)(fs1 * fs1 + fs2 * fs2 + ssim_c1) * (<span class="keyword">float</span>)(vars + ssim_c2));</span><br></pre></td></tr></table></figure><p>不过，在这段代码中，ssim_c1理应等于<code>0.01 * 0.01 * 255 * 255 * 64 * 64 + 0.5</code><strong>作者似乎少乘一个64</strong>。</p><p>以上，就是FFmpeg对SSIM的实现，读前人的代码，一开始读不懂，但越读越有深意，越读越能感受到作者的智慧与推敲的匠心。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;源码：tiny_ssim.c&lt;/p&gt;
&lt;p&gt;源码链接：&lt;a href=&quot;https://github.com/bodhisatan/LearnSSIM/blob/master/test_ssim.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;htt</summary>
      
    
    
    
    <category term="图像算法" scheme="https://bodhisatan.github.io/categories/%E5%9B%BE%E5%83%8F%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="图像算法" scheme="https://bodhisatan.github.io/tags/%E5%9B%BE%E5%83%8F%E7%AE%97%E6%B3%95/"/>
    
    <category term="SSIM" scheme="https://bodhisatan.github.io/tags/SSIM/"/>
    
    <category term="FFmpeg" scheme="https://bodhisatan.github.io/tags/FFmpeg/"/>
    
    <category term="C语言" scheme="https://bodhisatan.github.io/tags/C%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>SSIM算法与FFmpeg的实现</title>
    <link href="https://bodhisatan.github.io/2020/05/03/SSIM%E7%AE%97%E6%B3%95%E4%B8%8EFFmpeg%E7%9A%84%E5%AE%9E%E7%8E%B0/"/>
    <id>https://bodhisatan.github.io/2020/05/03/SSIM%E7%AE%97%E6%B3%95%E4%B8%8EFFmpeg%E7%9A%84%E5%AE%9E%E7%8E%B0/</id>
    <published>2020-05-03T04:32:43.000Z</published>
    <updated>2020-12-29T05:00:19.280Z</updated>
    
    <content type="html"><![CDATA[<h2 id="SSIM算法"><a href="#SSIM算法" class="headerlink" title="SSIM算法"></a>SSIM算法</h2><p>在图像重建，压缩领域，有很多算法可以计算输出图像与原图的差距，在SSIM算法出现之前，最长用的是MSE(Mean Square Error loss)算法，他的公式很简单：</p><script type="math/tex; mode=display">MSE = \frac{1}{mn}\sum_{i=0}^{m-1}\sum_{j=0}^{n-1}[I(i, j)-K(i,j)]^2</script><p>也就是计算重建后图像与输入图像的像素值差的平方和，然后在全图上求平均</p><p>但这个计算方法明显是有问题的，例如两张图片假如只是亮度不同，MSE可能很大，而一张模糊处理后的图与原图，可能MSE就很小。于是有人在<em>Image Quality Assessment: From Error Visibility to Structural Similarity</em>这篇论文里提出了SSIM算法。</p><p>人类在衡量两幅图的差距时，更偏重于两幅图的结构相似性，而不是像MSE那样，逐像素计算差距，SSIM就是一种基于结构相似度的度量。有两幅图x，y，他们的相似度按三个维度进行比较：亮度（luminance）l(x,y)，对比度（contrast）c(x,y)，和结构（structure）s(x,y)，最终x与y的相似度为这三者的函数：</p><script type="math/tex; mode=display">S(x,y) = f(l(x,y),c(x,y),s(x,y))</script><p>作者设计了三个公式定量计算这三者的相似性，公式的设计遵循三个原则：</p><p>对称性：s(x,y)=s(y,x)</p><p>有界性：s(x,y)≤1</p><p>极限值唯一：s(x,y)=1 当且仅当 x = y</p><p><strong>首先研究亮度</strong>。如果一幅图有 N 个像素点，每个像素点的像素值为<em>xi</em>，那么该图像的平均亮度为：</p><script type="math/tex; mode=display">μ_x = \frac{1}{N}\sum_{i=1}^{N}x_i</script><p>亮度相似度为：</p><script type="math/tex; mode=display">l(x,y) = \frac{2μ_xμ_y+C_1}{μ_x^2+μ_y^2+C1}</script><p><em>C1</em>是为了防止分母为零的情况，且</p><script type="math/tex; mode=display">C_1 = (K_1L)^2</script><p><em>K1</em>默认0.01，L是灰度动态范围，根据像素位深决定，8位的话，L=2^8-1=255</p><p><strong>然后研究对比度</strong>，所谓对比度，就是像素值的标准差：</p><script type="math/tex; mode=display">\sigma_x=\sqrt{\frac{1}{N-1}\sum_{i=1}^{N}(x_i-\mu_x)^2}</script><p>对比度的相似度计算公式：</p><script type="math/tex; mode=display">c(x,y)=\frac{2\sigma_x\sigma_y+C_2}{\sigma_x^2+\sigma_y^2+C_2}</script><p>其中，</p><script type="math/tex; mode=display">C_2=(K_2L)^2, K2\text{ 默认0.03}</script><p><strong>第三步，通过归一化向量的关系，研究结构相似度</strong></p><p>两个归一化向量: (x-<em>μx</em>)/<em>σx</em>和(y-<em>μy</em>)/<em>σy</em> </p><p>他们的余弦相似度：</p><script type="math/tex; mode=display">s(x,y) = (\frac{1}{N-1}\frac{x-\mu_x}{\sigma_x})*(\frac{1}{N-1}\frac{y-\mu_y}{\sigma_y}) = \frac{1}{\sigma_x\sigma_y}(\frac{1}{N-1}\sum_{i=1}^{N}(x_i-\mu_x)(y_i-\mu_y))</script><p>结构相似度计算公式：</p><script type="math/tex; mode=display">s(x,y)=\frac{\sigma_{xy}+C_3}{\sigma_x\sigma_y+C_3}</script><p>C3一般取C2/2</p><p><strong>最后</strong>，SSIM公式：</p><script type="math/tex; mode=display">SSIM(x,y)=l(x,y)*c(x,y)*s(x,y)</script><h2 id="yuv格式"><a href="#yuv格式" class="headerlink" title="yuv格式"></a>yuv格式</h2><p>在FFmpeg实现好的tiny_ssim里，视频文件是以yuv格式读入的</p><p>yuv类似于rgb，Y代表明亮度，U代表色度，V代表浓度，通常UV一起描述影像色彩和饱和度，用于指定像素的颜色。如果只有Y分量而没有UV分量，则图像就是黑白电视机里的那样。</p><p>人眼对色彩的敏感度低于对亮度的敏感度，即使适当降低色彩的采样人眼并不会有明显的感觉。所以并不是每个像素点都需要包含了 Y、U、V 三个分量，根据不同的采样格式，可以每个 Y 分量都对应自己的 UV 分量，也可以几个 Y 分量共用 UV 分量。<strong>相比 RGB，能够节约不少存储空间。</strong></p><p>YUV图像的主流采样方式主要有三种：</p><ul><li>YUV 4:4:4采样</li><li>YUV 4:2:2 采样</li><li>YUV 4:2:0 采样</li></ul><p>一个分量信息占8bit，也就是一个字节，假如采用444采样，则一个像素信息占3个字节。为什么叫4:4:4 , 意思就是每4个像素里的数据有4个Y， 4个U, 4个V。</p><p>在tiny_ssim中输入的yuv格式视频文件，是YUV420格式，YUV 4:2:0 并不意味着不采样 V 分量。它指的是对每条扫描线来说，只有一种色度分量以 2:1 的采样率存储，相邻的扫描行存储不同的色度分量。也就是说，如果第一行是 4:2:0，下一行就是 4:0:2，在下一行就是 4:2:0，以此类推。</p><p>YUV 数据有两种<strong>存储格式</strong>：平面格式（planar format）和打包格式（packed format）。</p><ul><li>planar format：先连续存储所有像素点的 Y，紧接着存储所有像素点的 U，随后是所有像素点的 V。</li><li>packed format：每个像素点的 Y、U、V 是连续交错存储的。</li></ul><p>YUV420有两种小类，YUV420P与YUV420SP：</p><ul><li><p>YUV420P 是基于 <strong>planar</strong> 平面模式进行存储，先存储所有的 Y 分量，然后存储所有的 U 分量或者 V 分量。</p></li><li><p>YUV420SP 也是基于<strong>planar</strong> 平面模式存储，与 YUV420P 的区别在于它的 U、V 分量是按照 UV 或者 VU 交替顺序进行存储。</p></li></ul><p>在tiny_ssim中负责输入的YUV格式，就是<strong>YUV420P的格式（也叫YV12）</strong></p><h2 id="FFmpeg代码里的术语"><a href="#FFmpeg代码里的术语" class="headerlink" title="FFmpeg代码里的术语"></a>FFmpeg代码里的术语</h2><ul><li><p>ssd</p><p>Sum of Squared Differences 估算值与估算对象差的平方和</p></li><li><p>PSNR </p><p>Peak Signal-to-Noise Ratio 峰值信噪比</p><script type="math/tex; mode=display">PSNR=10*log_{10}(\frac{MAX_I^2}{MSE})\\MAX_I=2^{bit-width}-1=255</script></li></ul><ul><li><p>plane</p><p>平面/分量，指Y，U，V</p></li><li><p>stride （间距，跨距）</p><ul><li><p>stride为什么会出现</p><p>这个参数看起来似乎没什么用，因为它的值和图像的宽度一样。但是那是大多数情况下，一旦遇到它和宽度不一样的时候，如果你不了解它的含义，那么程序肯定要出问题。可是为什么有时候它等于宽度，有时候又不等于呢？这就和它的含义有关了。<br>我们都知道现在计算机的cpu都是32位或者64位的cpu，他们一次最少读取4、8个字节，如果少于这些，反而要做一些额外的工作，会花更长的时间。所有会有一个概念叫做内存对齐，将结构体的长度设为4、8的倍数。<br>间距也是因为同样的理由出现的。因为图像的操作通常按行操作的，如果图像的所有数据都紧密排列，那么会发生非常多次的读取非对齐内存。会影响效率。而图像的处理本就是一个分秒必争的操作，所以为了性能的提高就引入了间距这个概念。</p></li><li><p>stride的含义</p><p>间距就是指图像中的一行图像数据所占的存储空间的长度，它是一个大于等于图像宽度的内存对齐的长度。这样每次以行为基准读取数据的时候就能内存对齐，虽然可能会有一点内存浪费，但是在内存充裕的今天已经无所谓了。</p></li><li><p>stride的值</p><p>所以如果图像的宽度如果是内存对齐长度的整数倍，那么间距就会等于宽度，而现在的cpu通常一次读取都是4个字节，而我们通常见到的分辨率都是4的整数倍，所以我们通常发现间距和图像的宽度一样（这里通常指rgb32格式或者以通道表示的yuv420p格式的y通道）。但是如果遇到一些少见的分辨率时间距和图像的宽度就不一样。</p></li></ul></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;SSIM算法&quot;&gt;&lt;a href=&quot;#SSIM算法&quot; class=&quot;headerlink&quot; title=&quot;SSIM算法&quot;&gt;&lt;/a&gt;SSIM算法&lt;/h2&gt;&lt;p&gt;在图像重建，压缩领域，有很多算法可以计算输出图像与原图的差距，在SSIM算法出现之前，最长用的是MSE(Me</summary>
      
    
    
    
    <category term="图像算法" scheme="https://bodhisatan.github.io/categories/%E5%9B%BE%E5%83%8F%E7%AE%97%E6%B3%95/"/>
    
    
    <category term="图像算法" scheme="https://bodhisatan.github.io/tags/%E5%9B%BE%E5%83%8F%E7%AE%97%E6%B3%95/"/>
    
    <category term="SSIM" scheme="https://bodhisatan.github.io/tags/SSIM/"/>
    
    <category term="FFmpeg" scheme="https://bodhisatan.github.io/tags/FFmpeg/"/>
    
  </entry>
  
  <entry>
    <title>关于C和Go的指针问题</title>
    <link href="https://bodhisatan.github.io/2020/05/03/%E5%85%B3%E4%BA%8EC%E5%92%8CGo%E7%9A%84%E6%8C%87%E9%92%88%E9%97%AE%E9%A2%98/"/>
    <id>https://bodhisatan.github.io/2020/05/03/%E5%85%B3%E4%BA%8EC%E5%92%8CGo%E7%9A%84%E6%8C%87%E9%92%88%E9%97%AE%E9%A2%98/</id>
    <published>2020-05-03T04:26:22.000Z</published>
    <updated>2020-05-25T00:38:53.934Z</updated>
    
    <content type="html"><![CDATA[<h2 id="指针的含义和相关运算符"><a href="#指针的含义和相关运算符" class="headerlink" title="指针的含义和相关运算符"></a>指针的含义和相关运算符</h2><p><strong>变量</strong>是存储值的地方，而<strong>指针</strong>的值就是<strong>变量的地址</strong>。不是所有值都有地址，但是所有变量都有地址。通过使用指针，就可以在无需知道变量名字的情况下，<strong>间接</strong>读取/更新变量的值。</p><p>在C语言和Go中，有两个特别重要又非常容易搞混的相关运算符，一个是&amp;，一个是*</p><p>取址运算符&amp;</p><ul><li>格式：&amp;变量名</li><li>含义：取出存放变量的地址</li></ul><p>间接运算符*</p><ul><li>格式：*指针名</li><li>含义：取出存放在此地址中的值</li></ul><p>举个例子：<br><figure class="highlight go"><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">x := <span class="number">1</span></span><br><span class="line">p := &amp;x <span class="comment">// p是整形指针，指向x</span></span><br><span class="line">fmt.Println(*p) <span class="comment">// "1"</span></span><br><span class="line">*p = <span class="number">2</span> <span class="comment">// 等价于 x = 2</span></span><br><span class="line">fmt.Println(x) <span class="comment">// 结果 "2"</span></span><br></pre></td></tr></table></figure><br>在这里，C/C++和Go有一点差异，C/C++是可以对指针变量进行运算的，而Go是不支持这种操作的。例如：<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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">int</span> a = <span class="number">10</span>, *pa = &amp;a;</span><br><span class="line">    <span class="keyword">char</span> c = <span class="string">'@'</span>, *pc = &amp;c;</span><br><span class="line">    pa++; <span class="comment">// 地址值+4，因为int占4个字节</span></span><br><span class="line">    pc++; <span class="comment">// 地址值+1，因为char占一个字节</span></span><br><span class="line">&#125;</span><br><span class="line">其实，也正是因为C++的指针计算功能过于强大，导致在C++中支持GC变成一个很困难的工作，假如C++支持垃圾收集，下面的代码在运行时将会变成一个严峻的考验：</span><br><span class="line">```c++</span><br><span class="line"><span class="keyword">int</span>* p = <span class="keyword">new</span> <span class="keyword">int</span>;</span><br><span class="line">p += <span class="number">10</span>; <span class="comment">// 指针发生偏移，因此那块内存不再被引用</span></span><br><span class="line"><span class="comment">// 这里可能发生针对那块内存的垃圾收集</span></span><br><span class="line">p -= <span class="number">10</span>; <span class="comment">// 又偏移回原来位置</span></span><br><span class="line">*p = <span class="number">2</span>; <span class="comment">// 如果有垃圾收集，这里就无法保证正常运行</span></span><br></pre></td></tr></table></figure><br><figure class="highlight plain"><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">在Go语言里，指针类型的零值是nil，相当于C语言里的NULL和C++11里的nullptr，说到这里，不妨顺带谈一谈C&#x2F;C++里的空指针。</span><br><span class="line">在C语言里，我们使用NULL来表示空指针，如下：</span><br><span class="line">&#96;&#96;&#96;c</span><br><span class="line">int *i &#x3D; NULL;</span><br><span class="line">foo_t *f &#x3D; NULL;</span><br></pre></td></tr></table></figure><br>其实在C语言中，NULL通常被定义为如下：<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> NULL ((void *)0)</span></span><br></pre></td></tr></table></figure><br>也就是说，C语言里的NULL是一个void*类型的指针，然后将void    *赋值给int*和foo_t*类型指针时，隐式转换成了相应类型（注意：GO无隐式转换）。而如果换一个C++编译器来编译这是要出错的，因为在C++里，void*是不能隐式转换成其他类型指针的，所以通常情况下，编译器头文件会这样定义NULL：<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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">ifdef</span> __cplusplus</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> NULL 0</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">else</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> NULL ((void *)0)</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">endif</span></span></span><br></pre></td></tr></table></figure><br>也就是说，假如是C++编译环境，就将NULL定义为0，这就带来了一个问题，“<strong>二义性</strong>”，代码如下：<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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">test</span><span class="params">(<span class="keyword">void</span>* p)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="built_in">cout</span> &lt;&lt; <span class="string">"pointer"</span> &lt;&lt; <span class="built_in">endl</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">test</span><span class="params">(<span class="keyword">int</span> num)</span> </span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="built_in">cout</span> &lt;&lt; <span class="string">"number"</span> &lt;&lt; <span class="built_in">endl</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    test(<span class="literal">NULL</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>编译会报错，为什么呢，因为NULL=0，test(NULL)可以匹配上面两个函数，所以有二义性。解决的方法，一是尽量用0代替NULL，这样写的时候自己就会发现问题，二就是C++11带来的解决方案<code>nullptr</code>，例如上面的代码，改成<code>test(nullptr)</code>则不会有问题。</p><h2 id="Go中指针与函数-方法结合"><a href="#Go中指针与函数-方法结合" class="headerlink" title="Go中指针与函数/方法结合"></a>Go中指针与函数/方法结合</h2><p>Go语言中没有类这一概念，但是可以给结构体定义方法。</p><p>在Go中，方法是一种带有<strong>接收者</strong>参数的特殊的函数。方法接收者在它的参数列表内，位于func关键字和方法名之间，例如下面这个代码，Abs方法拥有一个类型为Vertex的接收者：<br><figure class="highlight go"><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="function"><span class="keyword">func</span> <span class="params">(v Vertex)</span> <span class="title">Abs</span><span class="params">()</span> <span class="title">float64</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> math.Sqrt(v.X*v.X + v.Y*v.Y)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>注意：接收者的类型定义和方法声明必须在同一包内，不能以其他包里定义的类型为接收者声明方法，比如下面这个就是非法的：<br><figure class="highlight go"><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="function"><span class="keyword">func</span> <span class="params">(f <span class="keyword">int</span>)</span> <span class="title">Abs</span><span class="params">()</span> <span class="title">float64</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> f &lt; <span class="number">0</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">float64</span>(-f)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">float64</span>(f)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>方法接收者可以是<strong>值</strong>，也可以是<strong>指针</strong>。当指针作为接收者的时候，该方法就可以修改指针指向的值。当值作为接收者的时候，方法会对<strong>原始值的副本</strong>进行操作而不修改原始值，取副本是需要每次调用方法的时候进行复制的，如果值的类型是大型结构体，那么这样做的效率比较低。由是观之，使用指针作为接收者有<strong>两点好处</strong>：</p><ul><li>方法能够修改其接收者指向的值</li><li>避免在每次调用方法时复制该值，较为高效</li></ul><p><strong>最后</strong>，关于指针和方法在使用上还有一点要注意的。</p><ul><li>参数是指针的函数必须接受一个指针<figure class="highlight go"><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="function"><span class="keyword">func</span> <span class="title">ScaleFunc</span><span class="params">(v *Vertex, f <span class="keyword">float64</span>)</span></span> &#123;</span><br><span class="line">v.X = v.X * f</span><br><span class="line">v.Y = v.Y * f</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> v Vertex</span><br><span class="line">ScaleFunc(v, <span class="number">5</span>)  <span class="comment">// 编译错误！</span></span><br><span class="line">ScaleFunc(&amp;v, <span class="number">5</span>) <span class="comment">// OK</span></span><br></pre></td></tr></table></figure></li><li>以指针为接收者的方法被调用时，接收者<strong>既能为值也能为指针</strong>，此时用值用指针<strong>效果一样</strong><figure class="highlight go"><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="function"><span class="keyword">func</span> <span class="params">(v *Vertex)</span> <span class="title">Scale</span><span class="params">(f <span class="keyword">float64</span>)</span></span> &#123;</span><br><span class="line">v.X = v.X * f</span><br><span class="line">v.Y = v.Y * f</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> v Vertex</span><br><span class="line">v.Scale(<span class="number">5</span>)  <span class="comment">// OK，v改变，因为Go会将语句 v.Scale(5) 解释为 (&amp;v).Scale(5)</span></span><br><span class="line">p := &amp;v</span><br><span class="line">p.Scale(<span class="number">10</span>) <span class="comment">// OK，v改变</span></span><br></pre></td></tr></table></figure></li><li>参数是值的函数必须接受一个值<figure class="highlight go"><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">func</span> <span class="title">AbsFunc</span><span class="params">(v Vertex)</span> <span class="title">float64</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> math.Sqrt(v.X*v.X + v.Y*v.Y)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> v Vertex</span><br><span class="line">fmt.Println(AbsFunc(v))  <span class="comment">// OK</span></span><br><span class="line">fmt.Println(AbsFunc(&amp;v)) <span class="comment">// 编译错误</span></span><br></pre></td></tr></table></figure></li><li>以值为接收者的方法被调用时，接收者既能为值又能为指针，此时用值用指针<strong>效果一样</strong><figure class="highlight go"><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="function"><span class="keyword">func</span> <span class="params">(v Vertex)</span> <span class="title">Abs</span><span class="params">()</span> <span class="title">float64</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> math.Sqrt(v.X*v.X + v.Y*v.Y)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> v Vertex</span><br><span class="line">fmt.Println(v.Abs()) <span class="comment">// OK</span></span><br><span class="line">p := &amp;v</span><br><span class="line">fmt.Println(p.Abs()) <span class="comment">// OK，这种情况下，方法调用 p.Abs() 会被解释为 (*p).Abs()</span></span><br></pre></td></tr></table></figure></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;指针的含义和相关运算符&quot;&gt;&lt;a href=&quot;#指针的含义和相关运算符&quot; class=&quot;headerlink&quot; title=&quot;指针的含义和相关运算符&quot;&gt;&lt;/a&gt;指针的含义和相关运算符&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;变量&lt;/strong&gt;是存储值的地方，而&lt;stron</summary>
      
    
    
    
    <category term="Go语言" scheme="https://bodhisatan.github.io/categories/Go%E8%AF%AD%E8%A8%80/"/>
    
    
    <category term="C语言" scheme="https://bodhisatan.github.io/tags/C%E8%AF%AD%E8%A8%80/"/>
    
    <category term="指针" scheme="https://bodhisatan.github.io/tags/%E6%8C%87%E9%92%88/"/>
    
    <category term="golang" scheme="https://bodhisatan.github.io/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>关于闭包的理解</title>
    <link href="https://bodhisatan.github.io/2020/05/02/%E5%85%B3%E4%BA%8E%E9%97%AD%E5%8C%85%E7%9A%84%E7%90%86%E8%A7%A3/"/>
    <id>https://bodhisatan.github.io/2020/05/02/%E5%85%B3%E4%BA%8E%E9%97%AD%E5%8C%85%E7%9A%84%E7%90%86%E8%A7%A3/</id>
    <published>2020-05-02T06:59:50.000Z</published>
    <updated>2020-12-31T10:13:45.652Z</updated>
    
    <content type="html"><![CDATA[<h2 id="什么是闭包"><a href="#什么是闭包" class="headerlink" title="什么是闭包"></a>什么是闭包</h2><p>在很多高级语言中，都有“闭包”这一概念，闭包（Closure）是词法闭包（Lexical Closure）的简称。官方一点的定义是，闭包是由<strong>函数</strong>和与其相关的<strong>引用环境</strong>组合而成的实体。</p><p>闭包，严格意义上来说，只是形式上像函数，但其实不是函数。函数是一些可执行代码，这些代码在函数被定义后就已经被确定了，不会在执行时发生变化，所以一个函数只有一个实例。而闭包不同，不同的引用环境和相同的函数组合可以产生不同的实例，所以闭包在运行时可以有多个实例。所谓的<strong>引用环境</strong>，是指在程序执行中的某个点，所有处于活跃状态的约束所组成的集合。听起来复杂，通俗一点的话说，约束就是一个变量的名字和其所代表的对象时间的联系。通过闭包，函数就可以访问函数体之外的<strong>自由变量</strong>。</p><p>举个例子：<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><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="function"><span class="keyword">def</span> <span class="title">outer_func</span><span class="params">()</span>:</span></span><br><span class="line">    string = []</span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">inner_func</span><span class="params">(s)</span>:</span></span><br><span class="line">        string.append(s)</span><br><span class="line">        <span class="keyword">print</span> string</span><br><span class="line">    <span class="keyword">return</span> inner_func</span><br><span class="line"></span><br><span class="line">f1 = outer_func()</span><br><span class="line">f1(<span class="string">"a"</span>)</span><br><span class="line">f1(<span class="string">"b"</span>)</span><br><span class="line">f2 = outer_func()</span><br><span class="line">f2(<span class="string">"a"</span>)</span><br></pre></td></tr></table></figure><br>在这里，<em>string</em>变量就是闭包函数<em>inner_func</em>的<strong>自由变量</strong>（如果一个变量在代码块中被使用但不是在此代码块里定义，那么它就是自由变量）。假设没有闭包和自由变量的概念，这段代码存在一个问题：当调用outer_func()时，在其执行上下文中生成了局部变量string的实例，所以函数inner_func()中的string引用的就是这个实例。但inner_func()并没有在此时执行，而是作为返回值返回。当outer_func()返回后，其执行上下文失效，string实例的生命周期也随之结束了，在后面对f1,f2的调用其实是对inner_func()的调用，而此处并不在string的作用域里，这看起来是无法正确执行的。</p><p>也就是说，假如没有闭包，如果按照作用域规则在执行时确定一个函数的引用环境，那么这个引用环境可能和函数定义时不同，想要让这个函数正常运行，一个简单的方法就是在函数定义时捕获当时的引用环境，并与函数本体代码组成一个整体，而这个“整体”，就是闭包。<strong>可以用C语言的全局变量，Java的static变量帮助理解</strong>。</p><p>借用一个非常好的说法来做个总结：<strong>对象是附有行为的数据，而闭包是附有数据的行为</strong>。</p><h2 id="关于自由变量和匿名函数"><a href="#关于自由变量和匿名函数" class="headerlink" title="关于自由变量和匿名函数"></a>关于自由变量和匿名函数</h2><p>上文的python代码，执行结果是：<br><figure class="highlight plain"><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">[&#39;a&#39;]</span><br><span class="line">[&#39;a&#39;, &#39;b&#39;]</span><br><span class="line">[&#39;a&#39;]</span><br></pre></td></tr></table></figure><br>既然函数和引用变量被打包成了一个整体，那么为什么结果不是<code>[&#39;a&#39;][&#39;a&#39;, &#39;b&#39;][&#39;a&#39;, &#39;b&#39;, &#39;c&#39;]</code>呢？从这个例子就可以知道，闭包的自由变量，只和具体闭包实例相关联，f1和f2是不同的闭包实例，每个闭包实例引用的自由变量互不干扰，同时毋庸置疑的，一个闭包实例对其自由变量的修改会被传递到下一次该闭包实例的调用。<br>在golang中，是不允许在函数（function）里定义方法（method）的，也就是说，我们没有办法像python一样，在一个叫outer_func的函数里面再定义一个叫inner_func的函数，但有替代方法，那就是匿名函数。下面是一个用go的匿名函数特性写的求斐波那契数列：<br><figure class="highlight go"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 返回一个“返回int的函数”</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">fibonacci</span><span class="params">()</span> <span class="title">func</span><span class="params">()</span> <span class="title">int</span></span> &#123;</span><br><span class="line">a := <span class="number">0</span></span><br><span class="line">b := <span class="number">1</span></span><br><span class="line"><span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">int</span></span> &#123;</span><br><span class="line">tmp := a</span><br><span class="line">a, b = b, a + b</span><br><span class="line"><span class="keyword">return</span> tmp</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">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">f := fibonacci()</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">10</span>; i++ &#123;</span><br><span class="line">fmt.Println(f())</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="闭包对于编程语言特性的要求"><a href="#闭包对于编程语言特性的要求" class="headerlink" title="闭包对于编程语言特性的要求"></a>闭包对于编程语言特性的要求</h2><ul><li>函数是一阶值/一等公民（First-class value），即<strong>函数可以作为另一个函数的返回值或参数</strong>，还可以作为一个变量的值；</li><li>函数可以嵌套定义，即在一个函数内部可以定义另一个函数；</li><li>可以捕获引用环境，并把引用环境和函数代码组成一个可调用的实体；</li><li>允许定义匿名函数；</li></ul><p>这些条件并不是必要的，但具备这些条件能说明一个编程语言对闭包的支持较为完善。</p><h2 id="闭包和回调函数的区别"><a href="#闭包和回调函数的区别" class="headerlink" title="闭包和回调函数的区别"></a>闭包和回调函数的区别</h2><p>回调函数就是一个通过函数指针调用的函数。如果你把函数的指针（地址）作为参数传递给另一个函数，当这个指针被用来调用其所指向的函数时，我们就说这是回调函数。回调函数不是由该函数的实现方直接调用，而是在特定的事件或条件发生时由另外的一方调用的，用于对该事件或条件进行响应。简单来说，就是将一个方法对象 a 传递给另一个方法对象 b，让后者在适当的时候执行 a。</p><p>其实，回调也是闭包的一种实现形式，前面两段代码是以函数作为返回值的闭包，还可以将函数作为参数传入另一个函数实现闭包，例如下面的代码，同样是求斐波那契，但换了一种闭包形式：<br><figure class="highlight go"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 返回一个“返回int的函数”</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">fibonacci</span><span class="params">(fn <span class="keyword">func</span>()</span>)</span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">10</span>; i++ &#123;</span><br><span class="line">fn()</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">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">a := <span class="number">0</span></span><br><span class="line">b := <span class="number">1</span></span><br><span class="line">swap := <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">tmp := a</span><br><span class="line">a, b = b, b + tmp</span><br><span class="line">fmt.Println(tmp)</span><br><span class="line">&#125;</span><br><span class="line">fibonacci(swap)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>最后，顺带说一下Java的写法，Java里的内部类就是一种闭包，因为它持有一个指向外围类的引用<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><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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Fib</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> a = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> b = <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="class"><span class="keyword">class</span> <span class="title">Inner</span> </span>&#123;</span><br><span class="line">        <span class="function"><span class="keyword">void</span> <span class="title">print</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">int</span> tmp = a;</span><br><span class="line">            a = b;</span><br><span class="line">            b = tmp + a;</span><br><span class="line">            System.out.println(tmp);</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">Inner <span class="title">getInnerInstance</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> Inner();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        Fib t = <span class="keyword">new</span> Fib();</span><br><span class="line">        Inner inner = t.getInnerInstance();</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++) &#123;</span><br><span class="line">            inner.print();</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><h2 id="闭包的作用和缺点"><a href="#闭包的作用和缺点" class="headerlink" title="闭包的作用和缺点"></a>闭包的作用和缺点</h2><ul><li>对于不同的闭包实例，可以进行数据的隔离，同时减少对全局变量的污染</li><li>闭包可以认为是附带数据的行为，这使得闭包具有较好抽象能力，可以用闭包模拟面向对象编程</li><li>程序语言会识别出自由变量，并将其从函数的栈内存调整到堆内存，这会给内存和GC带来压力</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;什么是闭包&quot;&gt;&lt;a href=&quot;#什么是闭包&quot; class=&quot;headerlink&quot; title=&quot;什么是闭包&quot;&gt;&lt;/a&gt;什么是闭包&lt;/h2&gt;&lt;p&gt;在很多高级语言中，都有“闭包”这一概念，闭包（Closure）是词法闭包（Lexical Closure）的简称。官</summary>
      
    
    
    
    <category term="Go语言" scheme="https://bodhisatan.github.io/categories/Go%E8%AF%AD%E8%A8%80/"/>
    
    
    <category term="闭包" scheme="https://bodhisatan.github.io/tags/%E9%97%AD%E5%8C%85/"/>
    
    <category term="golang" scheme="https://bodhisatan.github.io/tags/golang/"/>
    
  </entry>
  
</feed>
