<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Enki&#39;s Notes</title>
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="http://enkichen.com/"/>
  <updated>2019-07-27T00:06:25.886Z</updated>
  <id>http://enkichen.com/</id>
  
  <author>
    <name>Enki</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>macOS 开启 Web 服务器</title>
    <link href="http://enkichen.com/2019/07/11/macos-web-server/"/>
    <id>http://enkichen.com/2019/07/11/macos-web-server/</id>
    <published>2019-07-11T07:46:25.000Z</published>
    <updated>2019-07-27T00:06:25.886Z</updated>
    
    <content type="html"><![CDATA[<p>macOS 系统下自带了 Apache 和 PHP 环境，如果有需要的话，以下命令来控制服务器的启动、重启与停止：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line"><span class="comment"># 启动 Apache 服务器</span></div><div class="line"><span class="variable">$sudo</span> apachectl start</div><div class="line"></div><div class="line"><span class="comment"># 重启 Apache 服务器</span></div><div class="line"><span class="variable">$sudo</span> apachectl restart</div><div class="line"></div><div class="line"><span class="comment"># 停止 Apache 服务器</span></div><div class="line"><span class="variable">$sudo</span> apachectl stop</div></pre></td></tr></table></figure>
<a id="more"></a>
<p>Apache 服务一些信息：</p>
<ul>
<li>Web 跟目录在： <code>/Library/WebServer/Documents</code></li>
<li>配置文件路径：<code>/etc/apache2</code></li>
</ul>
<p>将文件放至 Web 根目录下用户即可方法，服务的运行端口的配置都可以在 <code>httpd.conf</code> 找到配置项。</p>
<p>开启 PHP 模块：</p>
<ul>
<li>找到并打开配置文件 <code>/etc/apache2/httpd.conf</code> </li>
<li>搜索 <code>libphp</code> 字样找到类似 <code>#LoadModule php7_module libexec/apache2/libphp7.so</code> 的配置项</li>
<li>去除前面的 <code>#</code> 字样来开启 PHP 模块</li>
<li>执行 <code>sudo apachectl restart</code> 命令来重启 Apache 服务器</li>
</ul>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;macOS 系统下自带了 Apache 和 PHP 环境，如果有需要的话，以下命令来控制服务器的启动、重启与停止：&lt;/p&gt;
&lt;figure class=&quot;highlight sh&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;1&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;2&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;3&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;4&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;5&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;6&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;7&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;8&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# 启动 Apache 服务器&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$sudo&lt;/span&gt; apachectl start&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# 重启 Apache 服务器&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$sudo&lt;/span&gt; apachectl restart&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# 停止 Apache 服务器&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$sudo&lt;/span&gt; apachectl stop&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
    
    </summary>
    
      <category term="知识整理/总结" scheme="http://enkichen.com/categories/%E7%9F%A5%E8%AF%86%E6%95%B4%E7%90%86-%E6%80%BB%E7%BB%93/"/>
    
    
  </entry>
  
  <entry>
    <title>mac 平台 git 命令自动补全</title>
    <link href="http://enkichen.com/2019/06/05/git-automatic-completion/"/>
    <id>http://enkichen.com/2019/06/05/git-automatic-completion/</id>
    <published>2019-06-05T01:19:37.000Z</published>
    <updated>2019-06-05T02:30:13.536Z</updated>
    
    <content type="html"><![CDATA[<h4 id="安装-brew"><a href="#安装-brew" class="headerlink" title="安装 brew"></a>安装 brew</h4><p>如果有安装 brew 则跳过该步骤，执行如下命令进行安装：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ ruby -e &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)&quot;</div></pre></td></tr></table></figure>
<h4 id="安装-bash-completion"><a href="#安装-bash-completion" class="headerlink" title="安装 bash-completion"></a>安装 bash-completion</h4><p>执行 <code>brew list</code> 查看是否安装了 <code>bash-completion</code> 如果没有则执行以下命令进行安装：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ brew install bash-completion</div></pre></td></tr></table></figure>
<a id="more"></a> 
<h4 id="下载-git-源码"><a href="#下载-git-源码" class="headerlink" title="下载 git 源码"></a>下载 git 源码</h4><p>这里需要用到 git 仓库中一个文件，需要将 git clone 到本地，使用以下命令下载 git 仓库源码：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ git clone https://github.com/git/git.git</div></pre></td></tr></table></figure>
<p>下载完成后，需要将 <em><code>contrib/completion/</code></em> 目录下的 <code>git-completion.bash</code> 文件复制到当前用户根目录下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ cp git-completion.bash ~/.git-completion.bash</div></pre></td></tr></table></figure>
<h4 id="配置环境"><a href="#配置环境" class="headerlink" title="配置环境"></a>配置环境</h4><p>在 <em><code>~/.bash_profile</code></em> 文件（如果没有则需要手动创建）中添加以下代码：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">if [ -f ~/.git-completion.bash ]; then</div><div class="line">  . ~/.git-completion.bash</div><div class="line">fi</div></pre></td></tr></table></figure>
<p>在 <em><code>~/.bashrc</code></em> 文件（如果没有则需要手动创建）中添加以下内容：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">source ~/.git-completion.bash</div></pre></td></tr></table></figure>
<blockquote>
<p>~/ 表示当前用户的根目录</p>
</blockquote>
<p>重启终端后使用 tab 键来补全 git 命令。</p>
]]></content>
    
    <summary type="html">
    
      &lt;h4 id=&quot;安装-brew&quot;&gt;&lt;a href=&quot;#安装-brew&quot; class=&quot;headerlink&quot; title=&quot;安装 brew&quot;&gt;&lt;/a&gt;安装 brew&lt;/h4&gt;&lt;p&gt;如果有安装 brew 则跳过该步骤，执行如下命令进行安装：&lt;/p&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;1&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;$ ruby -e &amp;quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)&amp;quot;&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;h4 id=&quot;安装-bash-completion&quot;&gt;&lt;a href=&quot;#安装-bash-completion&quot; class=&quot;headerlink&quot; title=&quot;安装 bash-completion&quot;&gt;&lt;/a&gt;安装 bash-completion&lt;/h4&gt;&lt;p&gt;执行 &lt;code&gt;brew list&lt;/code&gt; 查看是否安装了 &lt;code&gt;bash-completion&lt;/code&gt; 如果没有则执行以下命令进行安装：&lt;/p&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;1&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;$ brew install bash-completion&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
    
    </summary>
    
      <category term="知识整理/总结" scheme="http://enkichen.com/categories/%E7%9F%A5%E8%AF%86%E6%95%B4%E7%90%86-%E6%80%BB%E7%BB%93/"/>
    
    
      <category term="Git" scheme="http://enkichen.com/tags/Git/"/>
    
  </entry>
  
  <entry>
    <title>方差、标准差、均方差、均方误差</title>
    <link href="http://enkichen.com/2018/12/14/standard-deviation/"/>
    <id>http://enkichen.com/2018/12/14/standard-deviation/</id>
    <published>2018-12-14T03:00:22.000Z</published>
    <updated>2018-12-14T04:16:41.795Z</updated>
    
    <content type="html"><![CDATA[<p>方差是在概率论和统计学中衡量随机变量或一组数据时离散程度的度量。概率论中方差用来度量随机变量和其数学期望（即均值）之间的偏离程度。<strong><code>统计中的方差（样本方差）是每个样本值与全体样本值的平均数之差的平方值的平均数</code></strong>。方差可以用来描述变量的波动程度。</p>
<p>方差在统计学和概率分布中各有不同的定义，并有不同的公式。在统计学中，方差用来计算每一个变量（观察值）与总体均数之间的差异。为避免出现离均差总和为零，离均差平方和受样本含量的影响，统计学采用平均离均差平方和来描述变量的变异程度。总体方差计算公式：<br><a id="more"></a><br><img src="/uploads/variance.png" alt="Alt text"></p>
<p>σ 的平方表示总体方差，X 表示变量，μ 表示总体的均值，N 表示总体样本数量。在实际项目中，总体均值难以得到时，应用样本统计量替代总体参数，经校正后，样本方差的计算公式：</p>
<p><img src="/uploads/variance_.png" alt="Alt text"></p>
<p>σ 的平方表示样本方差，X 表示变量，{X_i … X_n} 表示样本均值，N 表示样本数量。 之所以除以 N-1 而不是 N，是因为这样能使我们以较小的样本集更好地逼近总体的标准差，即统计上所谓的 「无偏估计」。<strong><code>由于方差是数据的平方，与检测值本身相差太大，难以直观的衡量，所以常用方差开根号换算回来，就成了标准差（Standard Deviation）用 σ 表示</code></strong>，公式如下：</p>
<p><img src="/uploads/standard.png" alt="Alt text"></p>
<p>例如存在以下 python 代码:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">import numpy as np </div><div class="line">data1 = [10, 30, 40, 50, 10]</div><div class="line">data2 = [5, 20, 25, 80, 10]</div><div class="line">print(np.mean(data1), np.var(data1), np.std(data1))</div><div class="line">print(np.mean(data2), np.var(data2), np.std(data2))</div></pre></td></tr></table></figure>
<p>输出结果：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">28.0 256.0 16.0</div><div class="line">28.0 726.0 26.94438717061496</div></pre></td></tr></table></figure>
<p>可以看出两组数据的均值都是 28.0 但是方差以及标准差都是不一样，方差或标准差的值越大反应了数据波动就越大，反之则越稳定。</p>
<p><strong><code>标准差在中文坏境中也被称为均方差，但不同于均方误差（mean squared error），均方误差是样本数据值偏离真实样本数据值的平方和的平均数，也即误差平方和的平均数</code></strong>，计算公式形式上接近方差，它的开方叫均方根误差，均方根误差才和标准差形式上接近。例如用 X 表示样本值，x 表示真实值，那么均方误差可用以下公式表示：</p>
<p><img src="/uploads/mse.png" alt="MSE"></p>
<p>那么均分根误差可用以下公式表示：</p>
<p><img src="/uploads/rmse.png" alt="RMSE"></p>
<p>在机器学习中均方误差可以用来作为模型的损失函数，用来预测和回归，均方误差越小，说明模型预测的越准确，反之则越不准确。总的来说，均方差是数据样本与均值的关系，而均方误差是数据样本与真实值之间的关系，在实际工作中根据需要来选择使用均方差还是均方误差。</p>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;方差是在概率论和统计学中衡量随机变量或一组数据时离散程度的度量。概率论中方差用来度量随机变量和其数学期望（即均值）之间的偏离程度。&lt;strong&gt;&lt;code&gt;统计中的方差（样本方差）是每个样本值与全体样本值的平均数之差的平方值的平均数&lt;/code&gt;&lt;/strong&gt;。方差可以用来描述变量的波动程度。&lt;/p&gt;
&lt;p&gt;方差在统计学和概率分布中各有不同的定义，并有不同的公式。在统计学中，方差用来计算每一个变量（观察值）与总体均数之间的差异。为避免出现离均差总和为零，离均差平方和受样本含量的影响，统计学采用平均离均差平方和来描述变量的变异程度。总体方差计算公式：&lt;br&gt;
    
    </summary>
    
      <category term="机器学习" scheme="http://enkichen.com/categories/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    
    
      <category term="机器学习" scheme="http://enkichen.com/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    
      <category term="损失函数" scheme="http://enkichen.com/tags/%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0/"/>
    
  </entry>
  
  <entry>
    <title>同步 WebRTC 主仓库</title>
    <link href="http://enkichen.com/2018/11/14/clone-webrtc/"/>
    <id>http://enkichen.com/2018/11/14/clone-webrtc/</id>
    <published>2018-11-14T01:29:01.000Z</published>
    <updated>2019-08-22T13:47:08.419Z</updated>
    
    <content type="html"><![CDATA[<p>之前在 <a href="http://www.enkichen.com/2017/05/12/webrtc-ios-build/" target="_blank" rel="external">WebRTC iOS&amp;OSX 库的编译</a> 介绍了如何下载和编译 WebRTC 的源码，但是很多时候可能只需要查看最新 WebRTC 的源码，并不需要下载庞大的第三方库以及编译依赖。</p>
<p>WebRTC 的仓库是使用 Git 来管理的，地址为 <a href="https://chromium.googlesource.com/external/webrtc/" target="_blank" rel="external">https://chromium.googlesource.com/external/webrtc/</a> 所以我们只需要 clone 到本地就好了（前提得备好梯子），如下命令：<br><a id="more"></a></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">git clone https://chromium.googlesource.com/external/webrtc</div></pre></td></tr></table></figure>
<p>同步到本地后，你会发现只有 <code>master</code>、<code>infra/config</code> 以及 <code>lkgr</code> 几个分支，其中 <code>master</code> 是官方的主开发分支了，每天都有大量的代码提交，但并没有类似的 <code>M70</code> 的分支或者 <code>tag</code>。</p>
<p>我们可以修改仓库的 <code>.git/config</code> 文件，在 <code>[remote &quot;origin&quot;]</code> 节中添加以下内容：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">fetch = +refs/branch-heads/*:refs/remotes/origin/*</div></pre></td></tr></table></figure>
<p>修改后内容如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div></pre></td><td class="code"><pre><div class="line">[core]</div><div class="line">        repositoryformatversion = 0</div><div class="line">        filemode = true</div><div class="line">        bare = false</div><div class="line">        logallrefupdates = true</div><div class="line">        ignorecase = true</div><div class="line">        precomposeunicode = true</div><div class="line">[remote &quot;origin&quot;]</div><div class="line">        url = https://chromium.googlesource.com/external/webrtc</div><div class="line">        fetch = +refs/heads/*:refs/remotes/origin/*</div><div class="line">        fetch = +refs/branch-heads/*:refs/remotes/origin/*</div><div class="line">[branch &quot;master&quot;]</div><div class="line">        remote = origin</div><div class="line">        merge = refs/heads/master</div></pre></td></tr></table></figure>
<p>之后就可以用 <code>git branch -av</code> 来查看远程分支了，当然也可以 <code>checkout</code> 到其他分支。</p>
<h3 id="使用脚本自动同步至-GitHub"><a href="#使用脚本自动同步至-GitHub" class="headerlink" title="使用脚本自动同步至 GitHub"></a>使用脚本自动同步至 GitHub</h3><p>WebRTC 的更新还是非常频繁的，可以使用 python 脚本将自动同步至 GitHub 上，首先在自己的 GitHub 空间中创建用于存储 webrtc 的仓库，然后在 webrtc 源码目录下执行以下命令，用于关联 GitHub 仓库：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ git remote add github git@github.com:xxxx/webrtc.git</div></pre></td></tr></table></figure>
<p>这样 webrtc 就存在两个远程仓库，使用以下脚本来同步两个仓库，脚本如下：</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div><div class="line">50</div><div class="line">51</div><div class="line">52</div><div class="line">53</div></pre></td><td class="code"><pre><div class="line"><span class="comment">#!/usr/bin/env python3</span></div><div class="line"></div><div class="line"><span class="keyword">import</span> os</div><div class="line"><span class="keyword">import</span> subprocess</div><div class="line"><span class="keyword">import</span> re</div><div class="line"></div><div class="line">remote_repo = <span class="string">'github'</span></div><div class="line">webrtc_path = <span class="string">"xxxx"</span></div><div class="line"></div><div class="line"><span class="function"><span class="keyword">def</span> <span class="title">execute_shell</span><span class="params">(cmd)</span>:</span></div><div class="line">    print(<span class="string">'$ '</span> + cmd)</div><div class="line">    (status, output) = subprocess.getstatusoutput(cmd)</div><div class="line">    <span class="keyword">if</span> (len(output) &gt; <span class="number">0</span>):</div><div class="line">        print(output)</div><div class="line">    <span class="keyword">return</span> (status, output)</div><div class="line"></div><div class="line"></div><div class="line"><span class="function"><span class="keyword">def</span> <span class="title">sync_tag</span><span class="params">(name, commit)</span>:</span></div><div class="line">    execute_shell(<span class="string">'git tag -a m%s %s -m m%s'</span> % (name, commit, name))</div><div class="line">    execute_shell(<span class="string">'git push %s --tags'</span> % (remote_repo))</div><div class="line"></div><div class="line"></div><div class="line"><span class="function"><span class="keyword">def</span> <span class="title">sync_branch</span><span class="params">(name, commit)</span>:</span></div><div class="line">    execute_shell(<span class="string">'git checkout -b '</span> + name + <span class="string">' '</span> + commit)</div><div class="line">    execute_shell(<span class="string">'git checkout '</span> + name)</div><div class="line">    execute_shell(<span class="string">'git pull origin '</span> + name)</div><div class="line">    execute_shell(<span class="string">'git push '</span> + remote_repo + <span class="string">' '</span> + name)</div><div class="line"></div><div class="line"></div><div class="line"><span class="function"><span class="keyword">def</span> <span class="title">sync_repo</span><span class="params">(path)</span>:</span></div><div class="line">    os.chdir(path)</div><div class="line">    execute_shell(<span class="string">'git checkout master'</span>)</div><div class="line">    execute_shell(<span class="string">'git fetch'</span>)</div><div class="line">    execute_shell(<span class="string">'git pull origin'</span>)</div><div class="line">    (status, output) = subprocess.getstatusoutput(<span class="string">'git br -av'</span>)</div><div class="line">    <span class="keyword">if</span> status != <span class="number">0</span>:</div><div class="line">        print(<span class="string">'webrtc path error with code:'</span>, status)</div><div class="line">        <span class="keyword">return</span> <span class="keyword">None</span></div><div class="line"></div><div class="line">    pattern = re.compile(<span class="string">r'remotes/origin/(\S+)\s+([a-zA-Z0-9]&#123;10&#125;)\s+'</span>, re.M)</div><div class="line">    results = pattern.findall(output)</div><div class="line">    <span class="keyword">for</span> (branch, commit) <span class="keyword">in</span> results:</div><div class="line">        sync_branch(branch, commit)</div><div class="line"></div><div class="line">    execute_shell(<span class="string">'git checkout master'</span>)</div><div class="line"></div><div class="line"></div><div class="line"><span class="function"><span class="keyword">def</span> <span class="title">main</span><span class="params">()</span>:</span></div><div class="line">    sync_repo(webrtc_path)</div><div class="line"></div><div class="line"></div><div class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</div><div class="line">    main()</div></pre></td></tr></table></figure>
<p>脚本中的 <code>remote_repo</code> 就是指 GitHub 的远程仓库的名字，<code>webrtc_path</code> 就是 webrtc 源码目录，完成配置后，执行脚本就可以同步两个仓库了，这可能需要点时间。我已经在我的 <a href="https://github.com/EnkiChen/webrtc" target="_blank" rel="external">GitHub</a> 空间中进行了同步，有需要的自取。</p>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;之前在 &lt;a href=&quot;http://www.enkichen.com/2017/05/12/webrtc-ios-build/&quot;&gt;WebRTC iOS&amp;amp;OSX 库的编译&lt;/a&gt; 介绍了如何下载和编译 WebRTC 的源码，但是很多时候可能只需要查看最新 WebRTC 的源码，并不需要下载庞大的第三方库以及编译依赖。&lt;/p&gt;
&lt;p&gt;WebRTC 的仓库是使用 Git 来管理的，地址为 &lt;a href=&quot;https://chromium.googlesource.com/external/webrtc/&quot;&gt;https://chromium.googlesource.com/external/webrtc/&lt;/a&gt; 所以我们只需要 clone 到本地就好了（前提得备好梯子），如下命令：&lt;br&gt;
    
    </summary>
    
      <category term="音视频/WebRTC" scheme="http://enkichen.com/categories/%E9%9F%B3%E8%A7%86%E9%A2%91-WebRTC/"/>
    
    
      <category term="WebRTC" scheme="http://enkichen.com/tags/WebRTC/"/>
    
  </entry>
  
  <entry>
    <title>Mac OSX 鼠标键盘事件的监听和模拟</title>
    <link href="http://enkichen.com/2018/09/12/osx-mouse-keyboard-event/"/>
    <id>http://enkichen.com/2018/09/12/osx-mouse-keyboard-event/</id>
    <published>2018-09-12T11:15:45.000Z</published>
    <updated>2018-11-14T01:40:21.397Z</updated>
    
    <content type="html"><![CDATA[<p>最近完成了 Mac OSX 平台下的远程控制功能，期间找了不少资料，这里做个总结，主要涉及到一下知识点：</p>
<ol>
<li>OSX 的事件机制</li>
<li>OSX/iOS 响应链者链</li>
<li>鼠标事件的监听及模拟（鼠标单击、双击、拖动、滚动等事件）</li>
<li>键盘事件的监听及模拟（包括组合键的模拟）</li>
<li>Keycode 键盘编码（统一 Windows、OSX、浏览器端键盘按键的编码值）</li>
</ol>
<h3 id="事件分发机制"><a href="#事件分发机制" class="headerlink" title="事件分发机制"></a>事件分发机制</h3><p>在 OSX 系统中鼠标和键盘的活动事件都会产生底层的系统事件，首先传递到 IOKit 框架处理后存储到队列中，通知 Window Server 服务层处理。Window Server 存储到 FIFO 优先队列中，然后逐一转发到当前活动窗口或者能响应这个事件的应用程序去处理。</p>
<a id="more"></a>
<p>在 OSX 或者 iOS 程序中，都会有一个 Main Run Loop 的线程，RunLoop 循环中会遍历 event 消息队列，逐一分发这些事件到应用中合适的对象去处理。具体来说就是调用 <code>NSApp</code> 的 <em><code>sendEvent:</code></em> 方法发送消息到<code>NSWindow</code>，<code>NSWindow</code> 再分发到 <code>NSView</code> 视图对象，由其鼠标或键盘事件响应方法去处理。</p>
<p><img src="/uploads/EventDispatch.png" alt="Alt text"></p>
<h3 id="事件响应链"><a href="#事件响应链" class="headerlink" title="事件响应链"></a>事件响应链</h3><p>在 OSX 和 iOS 程序中响应者链是 Application Kit 事件处理架构的中心机制，它由一系列链接在一起的响应者对象组成，事件或者动作消息可以沿着这些对象进行传递。如果一个响应者对象不能处理某个事件或动作，也就是说，它不响应那个消息，或者不认识那个事件，则将该消息重新发送给链中的下一个响应者。消息沿着响应者链向上、向更高级别的对象传递，直到最终被处理（如果最终还是没有被处理，就会被抛弃）。</p>
<p>事件响应者 <code>Responders</code> 类为核心应用程序架构的三个主要模式或机制定义了一个接口：</p>
<ul>
<li>它声明了一些处理事件消息（也就是源自用户事件的消息，比如象鼠标点击或按键按下这样的事件）的方法。</li>
<li>它声明了数十个处理动作消息的方法，它们和标准的键绑定（比如那些在文本内部移动插入点的绑定）密切相关。动作消息会被派发到目标对象；如果目标没有被指定，应用程序会负责检索合适的响应者。</li>
<li>它定义了一套在应用程序中指派和管理响应者的方法。这些响应者组成了我们所知道的响应者链，即一系列响应者，事件或动作消息在它们之间传递，直到找到能够对它们进行处理的对象。</li>
</ul>
<p>当 Application Kit 在应用程序中构造对象时，会为每个窗口建立响应者链。响应者链中的基本对象是<code>NSWindow</code> 对象及其视图层次。在视图层次中级别较低的视图将比级别更高的视图优先获得处理事件或动作消息的机会。<strong><code>NSWindow</code> 中保有一个第一响应者的引用，它通常是当前窗口中处于选择状态的视图，窗口通常把响应消息的机会首先给它</strong>。对于事件消息，响应者链通常以发生事件的窗口对应的 <code>NSWindow</code> 对象作为结束，虽然其它对象也可以作为下一个响应者被加入到 <code>NSWindow</code> 对象的后面。</p>
<p>从层级上看离观察者最近的视图优先响应事件，通过 <code>view</code> 的 <em><code>hitTest</code></em> 方法检测，满足 <em><code>hitTest</code></em> 方法的的子视图优先响应事件。</p>
<p><code>NSApplication</code>, <code>NSWindow</code>, <code>NSDrawer</code>, <code>NSWindowController</code>, <code>NSView</code> 以及继承于 <code>NSView</code> 的所有控件对象都直接或间接继承了 <code>Responders</code> 类，所以这些类都能处理鼠标和键盘事件。</p>
<p>iOS 程序相比于 OSX 程序会有点不一样：</p>
<ol>
<li>OSX 程序可能存在多个窗口，会有多个响应者链，iPhone 的应用程序就一个窗口，所以只会有一个响应者链。</li>
<li>在 iOS 程序中与加速计、陀螺仪和磁力计相关的运动事件不遵循响应者链，Core Motion 会将这些事件直接传递给我们指定的对象。有关更多信息，可以参看 <a href="https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/iPhoneOSTechOverview/CoreServicesLayer/CoreServicesLayer.html#//apple_ref/doc/uid/TP40007898-CH10-SW27" target="_blank" rel="external">Core Motion Framework</a>。</li>
</ol>
<h3 id="相关类的解析说明"><a href="#相关类的解析说明" class="headerlink" title="相关类的解析说明"></a>相关类的解析说明</h3><h4 id="NSResponder"><a href="#NSResponder" class="headerlink" title="NSResponder"></a>NSResponder</h4><p><code>NSResponder</code> 在这里是非常重要的一个类，其中定义了鼠标键盘触控板等多种事件，这里列举一些鼠标跟键盘的主要方法：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div></pre></td><td class="code"><pre><div class="line">// 鼠标按下事件</div><div class="line">- (void)mouseDown:(NSEvent *)event;</div><div class="line"></div><div class="line">// 鼠标右键按下事件</div><div class="line">- (void)rightMouseDown:(NSEvent *)event;</div><div class="line"></div><div class="line">// 鼠标抬起事件</div><div class="line">- (void)mouseUp:(NSEvent *)event;</div><div class="line"></div><div class="line">// 鼠标右键抬起事件</div><div class="line">- (void)rightMouseUp:(NSEvent *)event;</div><div class="line"></div><div class="line">// 鼠标移动事件</div><div class="line">- (void)mouseMoved:(NSEvent *)event;</div><div class="line"></div><div class="line">// 鼠标拖拽事件</div><div class="line">- (void)mouseDragged:(NSEvent *)event;</div><div class="line"></div><div class="line">// 鼠标滚动事件</div><div class="line">- (void)scrollWheel:(NSEvent *)event;</div><div class="line"></div><div class="line">// 鼠标右键拖拽事件</div><div class="line">- (void)rightMouseDragged:(NSEvent *)event;</div><div class="line"></div><div class="line">// 鼠标进入监控区域事件</div><div class="line">- (void)mouseEntered:(NSEvent *)event;</div><div class="line"></div><div class="line">// 鼠标离开监控区域事件</div><div class="line">- (void)mouseExited:(NSEvent *)event;</div><div class="line"></div><div class="line">// 键盘按下事件</div><div class="line">- (void)keyDown:(NSEvent *)event;</div><div class="line"></div><div class="line">// 键盘按下事件</div><div class="line">- (void)keyUp:(NSEvent *)event;</div><div class="line"></div><div class="line">// 键盘控制键的按下标记状态发送改变，后面用该方法来获取控制按下事件，参考 NSEventModifierFlags 定义</div><div class="line">- (void)flagsChanged:(NSEvent *)event;</div></pre></td></tr></table></figure>
<p><code>NSResponder</code> 除了定义基本的响应事件外，还定义了很多其他事件方法。具体请参考 <em><code>NSResponder.h</code></em> 的头文件定义。</p>
<h4 id="NSEvent"><a href="#NSEvent" class="headerlink" title="NSEvent"></a>NSEvent</h4><p><code>NSEvent</code> 类描述了事件的具体信息，这里列举跟鼠标和键盘相关的一些字段的介绍：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div></pre></td><td class="code"><pre><div class="line">// 事件类型</div><div class="line">@property (readonly) NSEventType type;</div><div class="line"></div><div class="line">// 键盘控制键的按下状态的标记</div><div class="line">@property (readonly) NSEventModifierFlags modifierFlags;</div><div class="line"></div><div class="line">// 事件的时间戳</div><div class="line">@property (readonly) NSTimeInterval timestamp;</div><div class="line"></div><div class="line">// 鼠标点击的次数（只有鼠标事件，才可使用）</div><div class="line">@property (readonly) NSInteger clickCount;</div><div class="line">@property (readonly) NSInteger buttonNumber; </div><div class="line">@property (readonly) NSInteger eventNumber;</div><div class="line"></div><div class="line">// 压力值</div><div class="line">@property (readonly) float pressure;</div><div class="line"></div><div class="line">// 鼠标在窗口的位置</div><div class="line">@property (readonly) NSPoint locationInWindow;</div><div class="line"></div><div class="line">// 鼠标滚动时。分别在 X 和 Y 轴上的偏移 </div><div class="line">@property (readonly) CGFloat scrollingDeltaX NS_AVAILABLE_MAC(10_7);</div><div class="line">@property (readonly) CGFloat scrollingDeltaY NS_AVAILABLE_MAC(10_7);</div><div class="line"></div><div class="line">// 键盘事件的字符编码和 key code 值</div><div class="line">@property (nullable, readonly, copy) NSString *characters;</div><div class="line">@property (readonly) unsigned short keyCode;</div></pre></td></tr></table></figure>
<h4 id="NSEventType"><a href="#NSEventType" class="headerlink" title="NSEventType"></a>NSEventType</h4><p><code>NSEventType</code> 类型定义了事件的具体类型，如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div></pre></td><td class="code"><pre><div class="line">typedef NS_ENUM(NSUInteger, NSEventType) &#123;        /* various types of events */</div><div class="line">    NSEventTypeLeftMouseDown             = 1,</div><div class="line">    NSEventTypeLeftMouseUp               = 2,</div><div class="line">    NSEventTypeRightMouseDown            = 3,</div><div class="line">    NSEventTypeRightMouseUp              = 4,</div><div class="line">    NSEventTypeMouseMoved                = 5,</div><div class="line">    NSEventTypeLeftMouseDragged          = 6,</div><div class="line">    NSEventTypeRightMouseDragged         = 7,</div><div class="line">    NSEventTypeMouseEntered              = 8,</div><div class="line">    NSEventTypeMouseExited               = 9,</div><div class="line">    NSEventTypeKeyDown                   = 10,</div><div class="line">    NSEventTypeKeyUp                     = 11,</div><div class="line">    NSEventTypeFlagsChanged              = 12,</div><div class="line">    NSEventTypeAppKitDefined             = 13,</div><div class="line">    NSEventTypeSystemDefined             = 14,</div><div class="line">    NSEventTypeApplicationDefined        = 15,</div><div class="line">    NSEventTypePeriodic                  = 16,</div><div class="line">    NSEventTypeCursorUpdate              = 17,</div><div class="line">    NSEventTypeScrollWheel               = 22,</div><div class="line">    NSEventTypeTabletPoint               = 23,</div><div class="line">    NSEventTypeTabletProximity           = 24,</div><div class="line">    NSEventTypeOtherMouseDown            = 25,</div><div class="line">    NSEventTypeOtherMouseUp              = 26,</div><div class="line">    NSEventTypeOtherMouseDragged         = 27,</div><div class="line">    /* The following event types are available on some hardware on 10.5.2 and later */</div><div class="line">    NSEventTypeGesture NS_ENUM_AVAILABLE_MAC(10_5)       = 29,</div><div class="line">    NSEventTypeMagnify NS_ENUM_AVAILABLE_MAC(10_5)       = 30,</div><div class="line">    NSEventTypeSwipe   NS_ENUM_AVAILABLE_MAC(10_5)       = 31,</div><div class="line">    NSEventTypeRotate  NS_ENUM_AVAILABLE_MAC(10_5)       = 18,</div><div class="line">    NSEventTypeBeginGesture NS_ENUM_AVAILABLE_MAC(10_5)  = 19,</div><div class="line">    NSEventTypeEndGesture NS_ENUM_AVAILABLE_MAC(10_5)    = 20,</div><div class="line">    </div><div class="line">#if __LP64__</div><div class="line">    NSEventTypeSmartMagnify NS_ENUM_AVAILABLE_MAC(10_8) = 32,</div><div class="line">#endif</div><div class="line">    NSEventTypeQuickLook NS_ENUM_AVAILABLE_MAC(10_8) = 33,</div><div class="line">    </div><div class="line">#if __LP64__</div><div class="line">    NSEventTypePressure NS_ENUM_AVAILABLE_MAC(10_10_3) = 34,</div><div class="line">    NSEventTypeDirectTouch NS_ENUM_AVAILABLE_MAC(10_10) = 37,</div><div class="line">#endif</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<h4 id="NSEventModifierFlags"><a href="#NSEventModifierFlags" class="headerlink" title="NSEventModifierFlags"></a>NSEventModifierFlags</h4><p><code>NSEventModifierFlags</code> 类型描述了一些控制键，是否处于按下状态，定义如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div></pre></td><td class="code"><pre><div class="line">/* Device-independent bits found in event modifier flags */</div><div class="line">typedef NS_OPTIONS(NSUInteger, NSEventModifierFlags) &#123;</div><div class="line">    NSEventModifierFlagCapsLock           = 1 &lt;&lt; 16, // Set if Caps Lock key is pressed.</div><div class="line">    NSEventModifierFlagShift              = 1 &lt;&lt; 17, // Set if Shift key is pressed.</div><div class="line">    NSEventModifierFlagControl            = 1 &lt;&lt; 18, // Set if Control key is pressed.</div><div class="line">    NSEventModifierFlagOption             = 1 &lt;&lt; 19, // Set if Option or Alternate key is pressed.</div><div class="line">    NSEventModifierFlagCommand            = 1 &lt;&lt; 20, // Set if Command key is pressed.</div><div class="line">    NSEventModifierFlagNumericPad         = 1 &lt;&lt; 21, // Set if any key in the numeric keypad is pressed.</div><div class="line">    NSEventModifierFlagHelp               = 1 &lt;&lt; 22, // Set if the Help key is pressed.</div><div class="line">    NSEventModifierFlagFunction           = 1 &lt;&lt; 23, // Set if any function key is pressed.</div><div class="line">    </div><div class="line">    // Used to retrieve only the device-independent modifier flags, allowing applications to mask off the device-dependent modifier flags, including event coalescing information.</div><div class="line">    NSEventModifierFlagDeviceIndependentFlagsMask    = 0xffff0000UL</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<h3 id="事件的监听方法"><a href="#事件的监听方法" class="headerlink" title="事件的监听方法"></a>事件的监听方法</h3><p>鼠标键盘事件的监听有多种方法，第一种方法是重写事件响应者 <code>Responders</code> 对应的方法来获取对应的事件；第二是通过重写 <code>NSWindow</code> 的 <em><code>sendEvent:</code></em> 方法； 第三是通过的 <code>NSEvent</code> 提供静态方法来监听对应的事件：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">+ (nullable id)addGlobalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(void (^)(NSEvent*))block`</div><div class="line"></div><div class="line">+ (nullable id)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(NSEvent* __nullable (^)(NSEvent*))block</div><div class="line"></div><div class="line">+ (void)removeMonitor:(id)eventMonitor</div></pre></td></tr></table></figure>
<p><code>NSEvent</code> 提供的静态方法可以用监听整个系统的事件或者当前应用程序内的事件。全局事件是异步过程因此无法修改事件，应用程序内的消息可以在捕获到消息后，修改事件然后继续交由响应者链中下一个响应者处理。</p>
<h3 id="鼠标事件监听"><a href="#鼠标事件监听" class="headerlink" title="鼠标事件监听"></a>鼠标事件监听</h3><p>这里介绍鼠标的一下事件处理方法和注意事项：</p>
<ol>
<li>左/右键的按下抬起事件</li>
<li>左键的双击（或者多击事件）</li>
<li>左键或者右键的拖拽事件</li>
<li>鼠标移动事件</li>
<li>鼠标的滚动事件</li>
</ol>
<p>前面介绍了三种监听事件的方法，这里使用重写 <code>Responders</code> 的方法来监听鼠标事件：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line">- (void)mouseDown:(NSEvent *)event;</div><div class="line">- (void)rightMouseDown:(NSEvent *)event;</div><div class="line">- (void)mouseUp:(NSEvent *)event;</div><div class="line">- (void)rightMouseUp:(NSEvent *)event;</div><div class="line">- (void)mouseMoved:(NSEvent *)event;</div><div class="line">- (void)mouseDragged:(NSEvent *)event;</div><div class="line">- (void)rightMouseDragged:(NSEvent *)event;</div><div class="line">- (void)scrollWheel:(NSEvent *)event;</div></pre></td></tr></table></figure>
<p>鼠标按键的按下抬起事件，只要判断一下 <code>NSEvent</code> 的 <em><code>type</code></em> 属性即可知道。</p>
<p>当前鼠标的位置信息可通过 <em><code>locationInWindow</code></em> 属性来获取，该坐标是当前 Window 窗口的坐标，其中包含了 Window 窗口标题栏的高度，所以如果要想获取当前鼠标在当前 <code>NSView</code> 中的位置，需要做一次坐标转换，可以调用 <code>NSView</code> 的 <em><code>convertPoint:</code></em> 方法来转换坐标。</p>
<p>鼠标左键的 <code>按下 - 抬起</code> 两个连续的动作被定义为单击事件，<em><code>clickCount</code></em> 属于用于描述当前点击的次数。在模拟鼠标双击时，需要用到该字段值，而不能用连续两次点击事件来模拟双击。</p>
<p>监听鼠标的移动事件时需要设置一个跟踪区域，只有在跟踪区域内的鼠标移动事件才会触发。可以通过 <code>NSView</code> 的 <code>- (void)addTrackingArea:(NSTrackingArea *)trackingArea</code> 方法来设置跟踪区域。同时需要重写 <code>- (void)updateTrackingAreas</code> 方法，当跟踪区域发送改变时，需要手动将之前的跟踪区域移除，再添加新的跟踪区域。</p>
<p>鼠标的拖拽事件是指用户按下鼠标左键或右键移动鼠标，当拖拽事件发生时 <em><code>mouseMoved:</code></em> 事件将不会触发。</p>
<p>鼠标的滚动可以通过 <em><code>deltaX</code></em> 和 <em><code>deltaY</code></em> 两个属性来获取分别在水平方向和垂直方向的滚动偏移。</p>
<blockquote>
<p>OSX 的坐标系统以左下角为 (0,0) 右上角为 (x_max, y_max)</p>
</blockquote>
<h3 id="键盘事件的监听"><a href="#键盘事件的监听" class="headerlink" title="键盘事件的监听"></a>键盘事件的监听</h3><p>键盘事件的监听也使用重写事件响应者 <code>Responders</code> 对应的方法来实现，需要重写的方法如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">- (void)keyDown:(NSEvent *)event;</div><div class="line">- (void)keyUp:(NSEvent *)event;</div><div class="line">- (void)flagsChanged:(NSEvent *)event;</div></pre></td></tr></table></figure>
<p>键盘事件重写上述方法外还需要重写以下方法：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">- (BOOL)acceptsFirstResponder;</div></pre></td></tr></table></figure>
<p>该方法用来说明是否成为响应者链的第一个响应者，这里需要返回 <code>YES</code> 表示成为第一响应者，否则无法监听键盘消息。</p>
<p><code>NSEvent</code> 的 <em><code>characters</code></em> 描述了当前键盘按键的字符，<em><code>keyCode</code></em> 描述了按键的值，每个按键的 <code>keyCode</code> 值定义可以在 <em><code>Carbon/HIToolbox/Events.h</code></em> 文件中找到对应的按键的宏定义。</p>
<p>在 <em><code>keyDown:</code></em> 和 <em><code>keyUp:</code></em> 方法中可以监听到大部分的按键的消息，但一些控制键需要通过 <em><code>flagsChanged:</code></em> 方法来处理，当 <code>NSEventModifierFlags</code> 定义的按键状态发送改变时，该方法就会被触发。这里需要注意的是大小写锁定键 <code>NSEventModifierFlagCapsLock</code> 只有当大写锁定或者取消锁定时，该方法才会被调用，并不会因为 <code>CapsLock</code> 按键按下或者抬起时触发。</p>
<blockquote>
<p>keyCode 值在 Windows 和浏览器上都有对应的键盘按键的值的定义，当需要与其他平台进行通信时，例如远程控制时，可以将 Mac 下的 keyCode 值转换成浏览器 JS 上的对应值定义，因为浏览器和 Windows 平台的定义是一致的。</p>
<p> <em><code>CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, kVK_CapsLock)</code></em> 方法可以用来获取按键是否处于按下状态。</p>
</blockquote>
<h3 id="鼠标键盘事件的模拟"><a href="#鼠标键盘事件的模拟" class="headerlink" title="鼠标键盘事件的模拟"></a>鼠标键盘事件的模拟</h3><p>OSX 下的鼠标和键盘事件模拟需要用到 <code>CoreGraphics</code> 及 <code>Carbon</code> 框架，在 <code>CoreGraphics</code> 框架中定义了一些用于创建底层事件的方法，<code>Carbon</code> 框架定义了一些跟键盘相关的宏和方法。</p>
<p>在模拟鼠标或者键盘事件时，都需要使用 <em><code>CGEventSourceCreate(CGEventSourceStateID stateID)</code></em> 方法来创建事件源，事件源类型定义了 3 个类型，如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">typedef CF_ENUM(int32_t, CGEventSourceStateID) &#123;</div><div class="line">  kCGEventSourceStatePrivate = -1,</div><div class="line">  kCGEventSourceStateCombinedSessionState = 0,</div><div class="line">  kCGEventSourceStateHIDSystemState = 1</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<ul>
<li><code>kCGEventSourceStatePrivate</code>：代表专门的应用，如远程控制程序可以生成和跟踪事件源状态独立于其他进程。</li>
<li><code>kCGEventSourceStateCombinedSessionState</code>：该状态表反映了所有事件源的组合状态发布到当前用户的登录会话。如果您的程序发布的事件在一个登录会话，您应该使用这个源状态当你创建一个事件源。</li>
<li><code>kCGEventSourceStateHIDSystemState</code>：该状态表反映了组合硬件输入源从 HID 系统硬件层面发送的事件源。生成的事件。 就是外接键盘或者 macbook 本机键盘以及一些系统定义的按键点击事件。</li>
</ul>
<p>这里自己封装了鼠标事件、鼠标滚动事件及键盘事件的方法，需要引入 <code>&lt;Carbon/Carbon.h&gt;</code> 及 <code>&lt;AppKit/AppKit.h&gt;</code> 头文件</p>
<h4 id="1-模拟鼠标事件："><a href="#1-模拟鼠标事件：" class="headerlink" title="1. 模拟鼠标事件："></a>1. 模拟鼠标事件：</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div></pre></td><td class="code"><pre><div class="line">void PostMouseEvent(CGMouseButton button, CGEventType type, const CGPoint &amp;point, int64_t clickCount)</div><div class="line">&#123;</div><div class="line">    CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStatePrivate);</div><div class="line">    CGEventRef theEvent = CGEventCreateMouseEvent(source, type, point, button);</div><div class="line">    CGEventSetIntegerValueField(theEvent, kCGMouseEventClickState, clickCount);</div><div class="line">    CGEventSetType(theEvent, type);</div><div class="line">    CGEventPost(kCGHIDEventTap, theEvent);</div><div class="line">    CFRelease(theEvent);</div><div class="line">    CFRelease(source);</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p><strong>左键单击模拟：</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 1);</div><div class="line">PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 1);</div></pre></td></tr></table></figure>
<p><strong>左键双击模拟：</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 1);</div><div class="line">PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 1);</div><div class="line">PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 2);</div><div class="line">PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 2);</div></pre></td></tr></table></figure>
<p><strong>拖拽事件：</strong> 如果是拖拽事件，例如左键拖拽事件，则需要先发送左键的 <code>kCGEventLeftMouseDown</code> 事件，然后连续发送 <code>kCGEventLeftMouseDragged</code> 事件，再发送 <code>kCGEventLeftMouseUp</code> 事件，代码如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 1);</div><div class="line">PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDragged, CGPointZero, 1);</div><div class="line">...</div><div class="line">PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDragged, CGPointZero, 1);</div><div class="line">PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 1);</div></pre></td></tr></table></figure>
<p>模拟其他鼠标事件，将枚举值修改一下即可。</p>
<h4 id="2-模拟鼠标滚动事件"><a href="#2-模拟鼠标滚动事件" class="headerlink" title="2. 模拟鼠标滚动事件"></a>2. 模拟鼠标滚动事件</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line">void PostScrollWheelEvent(int32_t scrollingDeltaX, int32_t scrollingDeltaY)</div><div class="line">&#123;</div><div class="line">    CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStatePrivate);</div><div class="line">    CGEventRef theEvent = CGEventCreateScrollWheelEvent(source, kCGScrollEventUnitPixel, 2, scrollingDeltaY, scrollingDeltaX);</div><div class="line">    CGEventPost(kCGHIDEventTap, theEvent);</div><div class="line">    CFRelease(theEvent);</div><div class="line">    CFRelease(source);</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>鼠标滚轮事件只要传入水平和垂直方向的偏移即可实现。</p>
<h4 id="3-模拟键盘事件"><a href="#3-模拟键盘事件" class="headerlink" title="3. 模拟键盘事件"></a>3. 模拟键盘事件</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line">void PostKeyboardEvent(CGKeyCode virtualKey, bool keyDown, CGEventFlags flags)</div><div class="line">&#123;</div><div class="line">    CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStatePrivate);</div><div class="line">    CGEventRef push = CGEventCreateKeyboardEvent(source, virtualKey, keyDown);</div><div class="line">    CGEventSetFlags(push, flags);</div><div class="line">    CGEventPost(kCGHIDEventTap, push);</div><div class="line">    CFRelease(push);</div><div class="line">    CFRelease(source);</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>键盘事件的模拟需要注意的就是 <em><code>CGEventFlags flags</code></em> 参数，该参数用来模拟组合键的实现，类型定义如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div></pre></td><td class="code"><pre><div class="line">typedef CF_OPTIONS(uint64_t, CGEventFlags) &#123; /* Flags for events */</div><div class="line">  /* Device-independent modifier key bits. */</div><div class="line">  kCGEventFlagMaskAlphaShift =          NX_ALPHASHIFTMASK,</div><div class="line">  kCGEventFlagMaskShift =               NX_SHIFTMASK,</div><div class="line">  kCGEventFlagMaskControl =             NX_CONTROLMASK,</div><div class="line">  kCGEventFlagMaskAlternate =           NX_ALTERNATEMASK,</div><div class="line">  kCGEventFlagMaskCommand =             NX_COMMANDMASK,</div><div class="line"></div><div class="line">  /* Special key identifiers. */</div><div class="line">  kCGEventFlagMaskHelp =                NX_HELPMASK,</div><div class="line">  kCGEventFlagMaskSecondaryFn =         NX_SECONDARYFNMASK,</div><div class="line"></div><div class="line">  /* Identifies key events from numeric keypad area on extended keyboards. */</div><div class="line">  kCGEventFlagMaskNumericPad =          NX_NUMERICPADMASK,</div><div class="line"></div><div class="line">  /* Indicates if mouse/pen movement events are not being coalesced */</div><div class="line">  kCGEventFlagMaskNonCoalesced =        NX_NONCOALSESCEDMASK</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>解析如下：</p>
<ul>
<li>kCGEventFlagMaskAlphaShift：大小写锁定键是否处于开启状态</li>
<li>kCGEventFlagMaskShift：Shift 键是否按下</li>
<li>kCGEventFlagMaskControl：Control 键是否按下</li>
<li>kCGEventFlagMaskAlternate：Alt 键是否按下，对应 Mac 键盘的 option 键</li>
<li>kCGEventFlagMaskCommand：Command 键是否按下，对应 Windows 的 WIN 键</li>
<li>kCGEventFlagMaskHelp：Help 键</li>
<li>kCGEventFlagMaskSecondaryFn：Fn 键</li>
<li>kCGEventFlagMaskNumericPad：数字键盘</li>
<li>kCGEventFlagMaskNonCoalesced：没有任何键按下</li>
</ul>
<p>如果有多个控制键同时按下，则使用位运算的或 <code>|</code> 加上对应的键值即可。例如模拟 <code>Command + Control + S</code>:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">PostKeyboardEvent(kVK_ANSI_S, true, kCGEventFlagMaskCommand | kCGEventFlagMaskControl)</div><div class="line">PostKeyboardEvent(kVK_ANSI_S, false, kCGEventFlagMaskNonCoalesced)</div></pre></td></tr></table></figure>
<blockquote>
<p>大小写锁定键，无法通过 kVK_CapsLock 按键的按下和抬起事件来模拟大小键的锁定，同时按键上的 LED 灯也是不会有变化的。</p>
</blockquote>
<h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul>
<li><a href="http://www.macdev.io/ebook/event.html" target="_blank" rel="external">鼠标和键盘事件</a></li>
<li><a href="https://www.sunyazhou.com/2017/02/22/macOS-simulate-keyborad-NSEvent/" target="_blank" rel="external">macOS上模拟发送键盘事件</a></li>
<li><a href="http://zhihaozhang.github.io/2017/09/23/%E8%AE%A9iMac%E4%B8%8EMacBook%E9%AB%98%E6%95%88%E5%8D%8F%E5%90%8C%E5%B7%A5%E4%BD%9C%E2%80%94%E2%80%94mouseSync%E5%BC%80%E5%8F%91%E5%BF%83%E5%BE%97/" target="_blank" rel="external">开发mouseSync的初衷</a></li>
<li><a href="https://blog.csdn.net/ch_soft/article/details/7371136" target="_blank" rel="external">监听Mac OS X的全局鼠标事件</a></li>
</ul>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近完成了 Mac OSX 平台下的远程控制功能，期间找了不少资料，这里做个总结，主要涉及到一下知识点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;OSX 的事件机制&lt;/li&gt;
&lt;li&gt;OSX/iOS 响应链者链&lt;/li&gt;
&lt;li&gt;鼠标事件的监听及模拟（鼠标单击、双击、拖动、滚动等事件）&lt;/li&gt;
&lt;li&gt;键盘事件的监听及模拟（包括组合键的模拟）&lt;/li&gt;
&lt;li&gt;Keycode 键盘编码（统一 Windows、OSX、浏览器端键盘按键的编码值）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;事件分发机制&quot;&gt;&lt;a href=&quot;#事件分发机制&quot; class=&quot;headerlink&quot; title=&quot;事件分发机制&quot;&gt;&lt;/a&gt;事件分发机制&lt;/h3&gt;&lt;p&gt;在 OSX 系统中鼠标和键盘的活动事件都会产生底层的系统事件，首先传递到 IOKit 框架处理后存储到队列中，通知 Window Server 服务层处理。Window Server 存储到 FIFO 优先队列中，然后逐一转发到当前活动窗口或者能响应这个事件的应用程序去处理。&lt;/p&gt;
    
    </summary>
    
      <category term="知识整理/总结" scheme="http://enkichen.com/categories/%E7%9F%A5%E8%AF%86%E6%95%B4%E7%90%86-%E6%80%BB%E7%BB%93/"/>
    
    
      <category term="OSX" scheme="http://enkichen.com/tags/OSX/"/>
    
  </entry>
  
  <entry>
    <title>H264/SVC Temporal Scalability</title>
    <link href="http://enkichen.com/2018/05/23/H264-SVC-Temporal-Scalability/"/>
    <id>http://enkichen.com/2018/05/23/H264-SVC-Temporal-Scalability/</id>
    <published>2018-05-23T09:46:10.000Z</published>
    <updated>2018-12-14T00:57:02.325Z</updated>
    
    <content type="html"><![CDATA[<p>在多人远程会议或直播系统中，参与的用户可能处于不同的网络环境（有线、Wifi、3G、4G）中，网络质量各不一致，为了所有用户可进行远程会议或者直播的观看，简单的做法就是降低发送端的视频码流，这样不管网络质量好坏，参与的用户都将观看低码率的视频流。这种方案缺点在于大部分网络较好的用户会被少数的网络较差的用户给拖累。这里介绍 H264 编码器中的 <code>Temporal Scalability</code> 机制来优化该方案。</p>
<p><code>Temporal Scalability</code> 是 H264/SVC 编码器中的概念，意思为时间上可伸缩的，在实际编码中编码器进行了分层编码，可简单的理解为 <strong>编码器对同一组输入的数据进行编码，可以输出不同帧率的码流</strong>，例如当前编码器输入帧率为 30fps 的流，编码器可同时输出多个码流，例如同时输出 3 层码流，从而可以得到不同帧率的码流。<br><a id="more"></a></p>
<blockquote>
<p>这里的 3 层码流是有依赖关系，比如输出有 A、B、C 3 层码流，单独的发送 A 层则得到低帧率的码流例如 5fps，如果同时发送 A 和 B 两层码流，则能得到相对较高的码流例如 15fps，如果同时发送 ABC ，则能得到最高的码流例如 30fps。</p>
</blockquote>
<p>在具体应用中便可以根据用户的网络质量来分配不同码率的视频流，这样网络质量较好的用户能看到高帧率的视频流，而网络较差的用户则看到低帧率的视频流。如下图所示：</p>
<p><img src="/uploads/SVC_Temporal_Scalability.png" alt="Alt text"></p>
<p>在 H264/SVC 编码器中进行了分层编码，例如上图描述编码端同时将所有的码流层发送至服务器，如果用户网络质量非常好，服务器将所有层的码流数据转发至该用户例如 Receiver2；如果用户网络非常差，则只需要将低层级码流数据转发给该用户例如 Receiver1。实际项目中可根据接收端带宽情况，转发对应码率的流即可。</p>
<h3 id="编码器的支持"><a href="#编码器的支持" class="headerlink" title="编码器的支持"></a>编码器的支持</h3><p>Temporal Scalability 机制并不是所有的编码都支持的，Apple 平台下（包括iOS和MacOS）的硬件编解码框架 <code>VideoToolbox</code> 是不支持的该特性的，因为没有看到任何的可设置的参数或者 API 接口。Android 平台下硬件编解码框架 <code>MediaCodec</code> 在 API 21 一下是不支持的，在 API 21 时添加了对 <code>KEY_TEMPORAL_LAYERING</code> 编码属性的配置，但只支持 VP8 编码格式 H264 编码格式不支持（毕竟 Android 和 VPx 都是属于 Google 的产品）。Android 下有需要了解详情的可以查看 <a href="https://developer.android.google.cn/reference/android/media/MediaFormat.html#KEY_TEMPORAL_LAYERING" target="_blank" rel="external">MediaFormat</a> 类的说明。</p>
<p>在两大主流的移动平台上，硬件编码器都是不支持 H264/SVC Temporal Scalability 特性，如果要在移动平台上使用该特性只能使用软编码器，可以使用开源的 openh264 编解码库，代码托管在 GitHub 上对应的地址为 <a href="https://github.com/cisco/openh264" target="_blank" rel="external">openh264</a> ，其中有该项目的介绍和使用方法。</p>
<blockquote>
<p>x264 编码器当前还不支持 Temporal Scalability 特性。</p>
</blockquote>
<p>在 openh264 中使用 <code>SEncParamExt</code> 结构体来对编码进行参数的配置，Temporal Scalability 的特性使用 <code>iTemporalLayerNum</code> 字段来配置，用来指定输出多少层，这里最大只支持 4 层。</p>
<p>使用 <em><code>EncodeFrame</code></em> 方法进行 H264 的编码后可以得到 <code>SFrameBSInfo</code> 的输出结果，该结构体以及关联的结构体的定义如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div></pre></td><td class="code"><pre><div class="line">typedef struct &#123;</div><div class="line">  int           iLayerNum;</div><div class="line">  SLayerBSInfo  sLayerInfo[MAX_LAYER_NUM_OF_FRAME];</div><div class="line">  EVideoFrameType eFrameType;</div><div class="line">  int iFrameSizeInBytes;</div><div class="line">  long long uiTimeStamp;</div><div class="line">&#125; SFrameBSInfo, *PFrameBSInfo;</div><div class="line"></div><div class="line">typedef struct &#123;</div><div class="line">  unsigned char uiTemporalId;</div><div class="line">  unsigned char uiSpatialId;</div><div class="line">  unsigned char uiQualityId;</div><div class="line">  EVideoFrameType eFrameType;</div><div class="line">  unsigned char uiLayerType;</div><div class="line">  int   iSubSeqId;</div><div class="line">  int   iNalCount;</div><div class="line">  int*  pNalLengthInByte;</div><div class="line">  unsigned char*  pBsBuf;</div><div class="line">&#125; SLayerBSInfo, *PLayerBSInfo;</div></pre></td></tr></table></figure>
<p>在 <code>SLayerBSInfo</code> 结构体的 <code>uiTemporalId</code> 字段用来描述当前编码后的数据流属于哪一层。openh264 的使用可查看其它文档。</p>
<blockquote>
<p>openh264 对 H264/SVC 的支持，不仅在时间维度上，并且也在空间和质量维度上都有支持。</p>
</blockquote>
<h4 id="openh264-中-temporal-layer-的输出顺序与帧率"><a href="#openh264-中-temporal-layer-的输出顺序与帧率" class="headerlink" title="openh264 中 temporal layer 的输出顺序与帧率"></a>openh264 中 temporal layer 的输出顺序与帧率</h4><p>在 openh264 的内部，存在一张表，用来记录每一层的输出顺序，可以在 <a href="https://github.com/cisco/openh264/blob/master/codec/encoder/core/src/encoder_data_tables.cpp" target="_blank" rel="external">encoder_data_tables.cpp</a> 中找到内容如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div></pre></td><td class="code"><pre><div class="line">const uint8_t   g_kuiTemporalIdListTable[MAX_TEMPORAL_LEVEL][MAX_GOP_SIZE + 1] = &#123;</div><div class="line">  &#123;</div><div class="line">    0, 0, 0, 0, 0, 0, 0, 0,</div><div class="line">    0</div><div class="line">  &#125;,  // uiGopSize = 1</div><div class="line">  &#123;</div><div class="line">    0, 1, 0, 0, 0, 0, 0, 0,</div><div class="line">    0</div><div class="line">  &#125;,  // uiGopSize = 2</div><div class="line">  &#123;</div><div class="line">    0, 2, 1, 2, 0, 0, 0, 0,</div><div class="line">    0</div><div class="line">  &#125;,  // uiGopSize = 4</div><div class="line">  &#123;</div><div class="line">    0, 3, 2, 3, 1, 3, 2, 3,</div><div class="line">    0</div><div class="line">  &#125;  //uiGopSize = 8</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>解析如下：</p>
<ul>
<li><code>iTemporalLayerNum</code> 的值为 1 时，使用 <code>uiGopSize = 1</code> 的配置，即每一帧为一组，每一组的 <code>uiTemporalId</code> 值为 0</li>
<li><code>iTemporalLayerNum</code> 的值为 2 时，使用 <code>uiGopSize = 2</code> 的配置，即每两帧为一组，每一组中对应的<br><code>uiTemporalId</code> 为 <code>[0, 1]</code></li>
<li><code>iTemporalLayerNum</code> 的值为 3 时，使用 <code>uiGopSize = 4</code> 的配置，即每四帧为一组，每一组中对应的<br><code>uiTemporalId</code> 为 <code>[0, 2, 1, 2]</code></li>
<li><code>iTemporalLayerNum</code> 的值为 4 时，使用 <code>uiGopSize = 8</code> 的配置，即每 8 帧为一组，每一组中对应的<br><code>uiTemporalId</code> 为 <code>[0, 3, 2, 3, 1, 3, 2, 3]</code></li>
</ul>
<p>根据上述描述以及输入的帧率可计算每一层的帧率是多少，例如在 30fps 下分两层输出，则 T0 帧率为 15fps；分 3 层时，每 4 帧组则有完整 7 组，则 T0 的帧率有 7 + 1 = 8fps，T1 的帧率有  8 + 7 = 15fps，T2 则有 30fps；分 4 层的情况可按相同的方法计算每一层的帧率。</p>
<h3 id="RTP-数据包的封装"><a href="#RTP-数据包的封装" class="headerlink" title="RTP 数据包的封装"></a>RTP 数据包的封装</h3><p>Temporal Scalability 属于 H264/SVC 的范畴，在封装成 RTP 包时也有对应的标准做法，但流程相对复杂，有兴趣的同学可以了解以下资料：</p>
<ul>
<li><a href="https://wenku.baidu.com/view/dfc855d5195f312b3169a5dd.html" target="_blank" rel="external">SVC的RTP封装算法及其应用</a></li>
<li><a href="https://blog.csdn.net/vblittleboy/article/details/11872469" target="_blank" rel="external">可伸缩视频编解码SVC技术介绍应用分析</a></li>
</ul>
<p>大致的思想是将基本层（低帧率）和增强层（高帧率）的层通过不同的 payload 分开传输，同时两个通道分别使用不同的 QoS 机制来保证传输的质量。</p>
<p>除了上述的较为复杂的做法外也可以使用 RTP 的扩展头来实现，在 RTP 头后面添加一个扩展头用来描述当前数据包的 temporal layer id，这样服务器就能区分 RTP 包分别属于哪个层级，从而根据客户端的网络质量来转发不同的帧率的码流。具体的做法可参考其他资料。</p>
<h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>H264/SVC 是以 H264/AVC 为基础，利用了AVC 编解码器的各种高效算法工具，在语法和工具集上进行了扩展，所以 H264/SVC 时间可扩展性完全向后兼容 H.264/AVC，即当使用 SVC 编码出多层的码流时，AVC 的编码器也是可以正常解码的。当 Temporal Scalability 帧率降低一半时，比特率通常不会降低一半，因为低层级的帧可能是其他高层级帧的参考帧，编码器会为其分配更多的码流。</p>
<p>由于编码端到服务器以及服务器到接收端的码流有了更灵活的传输方式，结合网络上其他的优化方案，可以衍生出更多的优化方案，例如针对低层级的丢包重传、缓存低层级的帧等。</p>
<p>可惜的是在 iOS 和 Android 两大主流移动平台上，对应的硬件编码器都不支持该特性，不过在分辨率不高的场景下还是可以尝试使用软编码器来实现的。一些时候用户更偏向于接受低分辨率的图像而需要更流畅的画面，这时候 <code>Temporal Scalability</code> 机制并不能达到要求，后续将介绍 H264/SVC 中的空间分层来解决该问题。</p>
<h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul>
<li><a href="https://blog.csdn.net/worldpharos/article/details/3369933" target="_blank" rel="external">The Scalable Video Coding Amendment of the H.264/AVC Standard</a></li>
<li><a href="https://www.researchgate.net/publication/224227711_Rate_Control_Optimization_for_Temporal-Layer_Scalable_Video_Coding" target="_blank" rel="external">Rate Control Optimization for Temporal-LayerScalable Video Coding</a></li>
<li><a href="https://webrtchacks.com/chrome-vp9-svc/" target="_blank" rel="external">Chrome’s WebRTC VP9 SVC Layer Cake: Sergio Garcia Murillo &amp; Gustavo Garcia</a></li>
</ul>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在多人远程会议或直播系统中，参与的用户可能处于不同的网络环境（有线、Wifi、3G、4G）中，网络质量各不一致，为了所有用户可进行远程会议或者直播的观看，简单的做法就是降低发送端的视频码流，这样不管网络质量好坏，参与的用户都将观看低码率的视频流。这种方案缺点在于大部分网络较好的用户会被少数的网络较差的用户给拖累。这里介绍 H264 编码器中的 &lt;code&gt;Temporal Scalability&lt;/code&gt; 机制来优化该方案。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Temporal Scalability&lt;/code&gt; 是 H264/SVC 编码器中的概念，意思为时间上可伸缩的，在实际编码中编码器进行了分层编码，可简单的理解为 &lt;strong&gt;编码器对同一组输入的数据进行编码，可以输出不同帧率的码流&lt;/strong&gt;，例如当前编码器输入帧率为 30fps 的流，编码器可同时输出多个码流，例如同时输出 3 层码流，从而可以得到不同帧率的码流。&lt;br&gt;
    
    </summary>
    
      <category term="音视频/WebRTC" scheme="http://enkichen.com/categories/%E9%9F%B3%E8%A7%86%E9%A2%91-WebRTC/"/>
    
    
      <category term="h264" scheme="http://enkichen.com/tags/h264/"/>
    
  </entry>
  
  <entry>
    <title>Apple 平台下的 VideoToolBox 框架</title>
    <link href="http://enkichen.com/2018/03/24/videotoolbox/"/>
    <id>http://enkichen.com/2018/03/24/videotoolbox/</id>
    <published>2018-03-24T05:17:16.000Z</published>
    <updated>2018-11-29T01:01:56.722Z</updated>
    
    <content type="html"><![CDATA[<p><code>VideoToolBox</code> 是一个低级的框架，可直接访问硬件的编解码器。它能够为视频提供压缩和解压缩的服务，同时也提供存储在 <code>CoreVideo</code> 像素缓冲区的图像进行格式的转换。该框架在 <code>iOS(8.0)/macOS(10.8)/tvOS(10.2)</code> 以后才被开放出来，在这之前都是不能使用的。</p>
<p><code>VideoToolBox</code> 框架为我们提供的接口都是以 C 语言函数形式提供的，并且以会话 <code>Session</code> 方式来进行管理，这些接口分为以下几个类别：</p>
<ol>
<li>VTCompressionSession：提供数据压缩的会话</li>
<li>VTDecompressionSession：提供数据解压缩的会话</li>
<li>VTPixelTransferSession：提供数据格式转换的会话</li>
<li>VTMultiPassStorage&amp;VTFrameSilo：提供多通道压缩的会话</li>
<li>VTSession&amp;Utilities：针对会话的管理以及辅助的函数</li>
</ol>
<p>这里主要针对 VTCompressionSession 以及 VTDecompressionSession 两个方面的介绍。由于 <code>VideoToolBox</code> 可进行多种格式的压缩以及解压缩，下文都是以 H264 编码格式进行介绍和举例。<br><a id="more"></a></p>
<h3 id="VTCompressionSession-编码器"><a href="#VTCompressionSession-编码器" class="headerlink" title="VTCompressionSession 编码器"></a>VTCompressionSession 编码器</h3><p>编码器的使用主要有以下四个流程：</p>
<ol>
<li>使用 <em><code>VTCompressionSessionCreate</code></em> 函数来创建编码器的 session。</li>
<li>使用 <em><code>VTSessionSetProperty</code></em> 方法来对编码器进行配置。</li>
<li>使用 <em><code>VTCompressionSessionEncodeFrame</code></em> 方法来进行数据的编码，并在回调函数中进行编码后的数据处理。</li>
<li>使用 <em><code>VTCompressionSessionCompleteFrames</code></em> 来完成数据的编码。</li>
<li>最后使用 <em><code>VTCompressionSessionInvalidate</code></em> 以及 <em><code>CFRelease</code></em> 来释放资源和内存。</li>
</ol>
<h4 id="编码器的创建"><a href="#编码器的创建" class="headerlink" title="编码器的创建"></a>编码器的创建</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div></pre></td><td class="code"><pre><div class="line">OSStatus VTCompressionSessionCreate(</div><div class="line">	CFAllocatorRef allocator, </div><div class="line">	int32_t width, int32_t height, </div><div class="line">	CMVideoCodecType codecType, </div><div class="line">	CFDictionaryRef encoderSpecification, </div><div class="line">	CFDictionaryRef sourceImageBufferAttributes, </div><div class="line">	CFAllocatorRef compressedDataAllocator, </div><div class="line">	VTCompressionOutputCallback outputCallback, </div><div class="line">	void *outputCallbackRefCon, </div><div class="line">	VTCompressionSessionRef  _Nullable *compressionSessionOut);</div></pre></td></tr></table></figure>
<p>参数介绍：</p>
<ul>
<li>CFAllocatorRef allocator：内存的构造器，默认传递 NULL 即可</li>
<li>int32_t width, int32_t height：指定编码器的宽高，传入源数据的宽高即可</li>
<li>CMVideoCodecType codecType： 编码器的类型，H264 使用 <code>kCMVideoCodecType_H264</code> 或者 <code>avc1</code> 字符</li>
<li>CFDictionaryRef encoderSpecification：编码规范，传递 NULL 让 <code>VideoToolbox</code> 选择</li>
<li>CFDictionaryRef sourceImageBufferAttributes：设置源图像缓冲区属性</li>
<li>CFAllocatorRef compressedDataAllocator：指定一个编码后数据的构造器，传递 NULL 即可</li>
<li>VTCompressionOutputCallback outputCallback：数据编码后的回调接口</li>
<li>void *outputCallbackRefCon：回调函数的自定义参数</li>
<li>VTCompressionSessionRef  _Nullable *compressionSessionOut：输出参数，构造好的编码器 Session</li>
</ul>
<h4 id="编码器的参数配置"><a href="#编码器的参数配置" class="headerlink" title="编码器的参数配置"></a>编码器的参数配置</h4><p>编码器的参数是多维度的，都是可以单独的配置，使用下面的函数进行编码器的参数配置：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">OSStatus VTSessionSetProperty(</div><div class="line">	VTSessionRef session, </div><div class="line">	CFStringRef propertyKey, </div><div class="line">	CFTypeRef propertyValue);</div></pre></td></tr></table></figure>
<p>参数介绍：</p>
<ul>
<li>VTSessionRef session：编码器</li>
<li>CFStringRef propertyKey：属性字段的 Key</li>
<li>CFTypeRef propertyValue：属性的值</li>
</ul>
<p>可配置的属性有多种类型如下：</p>
<h5 id="流配置"><a href="#流配置" class="headerlink" title="流配置"></a>流配置</h5><ol>
<li>kVTCompressionPropertyKey_Depth：编码器颜色的像素深度，例如 16 位 RGB 或者 24 位 RGB</li>
<li>kVTCompressionPropertyKey_ProfileLevel：编码比特流的配置文件和级别</li>
<li>kVTCompressionPropertyKey_H264EntropyMode：H.264 压缩的熵编码模式，可以设置为 CAVLC 或者 CABAC</li>
</ol>
<blockquote>
<p>更改默认熵模式可能导致与请求的配置文件和级别不兼容的配置。在这种情况下的结果是未定义的，可能包括编码错误或不合规的输出流。</p>
</blockquote>
<h5 id="缓冲区配置"><a href="#缓冲区配置" class="headerlink" title="缓冲区配置"></a>缓冲区配置</h5><ol>
<li>kVTCompressionPropertyKey_NumberOfPendingFrames：压缩会话中待处理帧的数量</li>
<li>kVTCompressionPropertyKey_PixelBufferPoolIsShared：指示公共像素缓冲池是否在视频编码器和会话客户端之间共享</li>
<li>kVTCompressionPropertyKey_VideoEncoderPixelBufferAttributes</li>
</ol>
<h5 id="预期值配置"><a href="#预期值配置" class="headerlink" title="预期值配置"></a>预期值配置</h5><ol>
<li>kVTCompressionPropertyKey_ExpectedDuration：压缩会话的预期总持续时间（如果已知）</li>
<li>kVTCompressionPropertyKey_ExpectedFrameRate：预期的帧速率，如果知道的话</li>
<li>kVTCompressionPropertyKey_SourceFrameCount：源 frame 的数量，如果已知</li>
</ol>
<h5 id="帧相关"><a href="#帧相关" class="headerlink" title="帧相关"></a>帧相关</h5><ol>
<li>kVTCompressionPropertyKey_AllowFrameReordering：指示是否启用了帧重新排序</li>
<li>kVTCompressionPropertyKey_AllowTemporalCompression：指示是否启用时间压缩</li>
<li>kVTCompressionPropertyKey_MaxKeyFrameInterval：关键帧之间的最大间隔，也称为关键帧速率。</li>
<li>kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration：从几个关键帧到下一个关键帧的最长持续时间</li>
<li>kVTEncodeFrameOptionKey_ForceKeyFrame：指示当前帧是否被强制为关键帧</li>
</ol>
<h5 id="硬件加速"><a href="#硬件加速" class="headerlink" title="硬件加速"></a>硬件加速</h5><ol>
<li>kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder：指示是否使用硬件加速视频编码器</li>
<li>kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder：指示是否需要硬件加速编码</li>
<li>kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder：指示是否允许硬件加速视频编码</li>
</ol>
<h5 id="码率控制"><a href="#码率控制" class="headerlink" title="码率控制"></a>码率控制</h5><ol>
<li>kVTCompressionPropertyKey_AverageBitRate：期望的平均比特率，以比特/秒为单位</li>
<li>kVTCompressionPropertyKey_DataRateLimits：</li>
<li>kVTCompressionPropertyKey_MoreFramesAfterEnd：指示压缩会话的帧是否以及如何与其他压缩帧连接以形成更长的序列</li>
<li>kVTCompressionPropertyKey_MoreFramesBeforeStart：指示压缩会话的帧是否以及如何与其他压缩帧连接以形成更长的序列</li>
<li>kVTCompressionPropertyKey_Quality：所需的压缩质量，该值应该被指定为 0.0 到 1.0 的范围，其中 low = 0.25，normal = 0.50，high = 0.7 5和 1.0 表示支持编码器的无损压缩</li>
</ol>
<h5 id="运行时"><a href="#运行时" class="headerlink" title="运行时"></a>运行时</h5><ol>
<li>kVTCompressionPropertyKey_RealTime：表示是否建议视频编码器实时执行压缩</li>
<li>kVTCompressionPropertyKey_MaxH264SliceBytes：H.264编码的最大片大小</li>
<li>kVTCompressionPropertyKey_MaxFrameDelayCount：在压缩器必须输出压缩帧之前允许压缩器保持的最大帧数</li>
</ol>
<p>以上参数并不是都要设置，保持默认值即可。</p>
<h4 id="数据的编码"><a href="#数据的编码" class="headerlink" title="数据的编码"></a>数据的编码</h4><p>在进行数据的编码之前，可手动调用下面的方法来申请必要的资源，如果不手动调用，则会在第一次进行数据编码时自动调用：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">OSStatus VTCompressionSessionPrepareToEncodeFrames(VTCompressionSessionRef session);</div></pre></td></tr></table></figure>
<p>该函数调用一次之后，后续的调用将是无效的调用。使用一下方法来进行数据的编码：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line">OSStatus VTCompressionSessionEncodeFrame(</div><div class="line">	VTCompressionSessionRef session, </div><div class="line">	CVImageBufferRef imageBuffer, </div><div class="line">	CMTime presentationTimeStamp, </div><div class="line">	CMTime duration, </div><div class="line">	CFDictionaryRef frameProperties, </div><div class="line">	void *sourceFrameRefCon, </div><div class="line">	VTEncodeInfoFlags *infoFlagsOut);</div></pre></td></tr></table></figure>
<p>参数解析：</p>
<ul>
<li>VTCompressionSessionRef session：编码器的 session</li>
<li>CVImageBufferRef imageBuffer：待编码的数据</li>
<li>CMTime presentationTimeStamp：待编码数据时间戳。当前时间戳必须大于前一个</li>
<li>CMTime duration：待编码数据的持续时间（画面的持续时间），可传入 <code>kCMTimeInvalid</code></li>
<li>CFDictionaryRef frameProperties：指定编码当前帧属性的键/值对。一些属性将会影响后续的编码帧</li>
<li>void *sourceFrameRefCon：传递给回调函数的自定义数据</li>
<li>VTEncodeInfoFlags *infoFlagsOut：</li>
</ul>
<p>当需要主动停止编码时，可调用下面方法来强制停止编码器：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">OSStatus VTCompressionSessionCompleteFrames(</div><div class="line">	VTCompressionSessionRef session, </div><div class="line">	CMTime completeUntilPresentationTimeStamp);</div></pre></td></tr></table></figure>
<h4 id="编码器资源释放与销毁"><a href="#编码器资源释放与销毁" class="headerlink" title="编码器资源释放与销毁"></a>编码器资源释放与销毁</h4><p>编码器的销毁需要调用一下两个方法来进行</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">void VTCompressionSessionInvalidate(VTCompressionSessionRef session);</div></pre></td></tr></table></figure>
<p>该方法用来释放编码器占用的内存资源，在调用一下方法来释放 session 本身的内存资源：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">void CFRelease(CFTypeRef cf);</div></pre></td></tr></table></figure>
<h3 id="VTDecompressionSession-解码器"><a href="#VTDecompressionSession-解码器" class="headerlink" title="VTDecompressionSession 解码器"></a>VTDecompressionSession 解码器</h3><p>解码器的使用与编码器类似，大致流程如下：</p>
<ol>
<li>使用 <em><code>VTDecompressionSessionCreate</code></em> 方法来创一个解码器。</li>
<li>使用 <em><code>VTSessionSetProperty</code></em> 或者 <em><code>VTSessionSetProperties</code></em> 来对解码器进行配置。</li>
<li>使用 <em><code>VTDecompressionSessionDecodeFrame</code></em> 来进行数据的解码。</li>
<li>使用 <em><code>VTDecompressionSessionInvalidate</code></em> 以及 <em><code>CFRelease</code></em> 来对资源的释放。</li>
</ol>
<h4 id="解码器的创建"><a href="#解码器的创建" class="headerlink" title="解码器的创建"></a>解码器的创建</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line">OSStatus VTDecompressionSessionCreate(</div><div class="line">	CFAllocatorRef allocator, </div><div class="line">	CMVideoFormatDescriptionRef videoFormatDescription, </div><div class="line">	CFDictionaryRef videoDecoderSpecification, </div><div class="line">	CFDictionaryRef destinationImageBufferAttributes, </div><div class="line">	const VTDecompressionOutputCallbackRecord *outputCallback, </div><div class="line">	VTDecompressionSessionRef  _Nullable *decompressionSessionOut);</div></pre></td></tr></table></figure>
<p>参数介绍：</p>
<ul>
<li>CFAllocatorRef allocator：构造器，传 NULL 即可</li>
<li>CMVideoFormatDescriptionRef videoFormatDescription：源数据的格式设置</li>
<li>CFDictionaryRef videoDecoderSpecification：解码器的规范，可使用 NULL 让 <code>VideoToolbox</code> 自动选择</li>
<li>CFDictionaryRef destinationImageBufferAttributes：缓冲区配置，NULL 保存默认即可</li>
<li>const VTDecompressionOutputCallbackRecord *outputCallback：用来配置解码后的回调函数以及自定义数据</li>
<li>VTDecompressionSessionRef  _Nullable *decompressionSessionOut：输出参数用来存储解码器的 session</li>
</ul>
<h4 id="解码器的参数配置"><a href="#解码器的参数配置" class="headerlink" title="解码器的参数配置"></a>解码器的参数配置</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">OSStatus VTSessionSetProperty(</div><div class="line">	VTSessionRef session, </div><div class="line">	CFStringRef propertyKey, </div><div class="line">	CFTypeRef propertyValue);</div></pre></td></tr></table></figure>
<p>参数介绍：</p>
<ul>
<li>VTSessionRef session：编码器</li>
<li>CFStringRef propertyKey：属性字段的 Key</li>
<li>CFTypeRef propertyValue：属性的值</li>
</ul>
<p>可配置的选项：</p>
<h5 id="解码器行为"><a href="#解码器行为" class="headerlink" title="解码器行为"></a>解码器行为</h5><ol>
<li>kVTDecompressionPropertyKey_RealTime：设置解码器是否实时解码数据</li>
</ol>
<h4 id="数据的解码"><a href="#数据的解码" class="headerlink" title="数据的解码"></a>数据的解码</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line">OSStatus VTDecompressionSessionDecodeFrame(</div><div class="line">	VTDecompressionSessionRef session, </div><div class="line">	CMSampleBufferRef sampleBuffer, </div><div class="line">	VTDecodeFrameFlags decodeFlags, </div><div class="line">	void *sourceFrameRefCon, </div><div class="line">	VTDecodeInfoFlags *infoFlagsOut);</div></pre></td></tr></table></figure>
<p>参数介绍：</p>
<ul>
<li>VTDecompressionSessionRef session：解码器的 session</li>
<li>CMSampleBufferRef sampleBuffer：包含一个或者多个待解码帧的 buffer</li>
<li>VTDecodeFrameFlags decodeFlags：设置标记为，来影响解码器的解码行为，可传 0</li>
<li>void *sourceFrameRefCon：传递给回调函数的自定义数据</li>
<li>VTDecodeInfoFlags *infoFlagsOut：输出参数，解码的一些标志位，可传 NULL</li>
</ul>
<p><code>VTDecodeFrameFlags</code> 与 <code>VTDecodeInfoFlags</code> 的定义如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div></pre></td><td class="code"><pre><div class="line">typedef CF_OPTIONS(uint32_t, VTDecodeFrameFlags) &#123;</div><div class="line">	kVTDecodeFrame_EnableAsynchronousDecompression = 1&lt;&lt;0,</div><div class="line">	kVTDecodeFrame_DoNotOutputFrame = 1&lt;&lt;1,</div><div class="line">	kVTDecodeFrame_1xRealTimePlayback = 1&lt;&lt;2, </div><div class="line">	kVTDecodeFrame_EnableTemporalProcessing = 1&lt;&lt;3,</div><div class="line">&#125;;</div><div class="line"></div><div class="line">typedef CF_OPTIONS(UInt32, VTDecodeInfoFlags) &#123;</div><div class="line">	kVTDecodeInfo_Asynchronous = 1UL &lt;&lt; 0,</div><div class="line">	kVTDecodeInfo_FrameDropped = 1UL &lt;&lt; 1,</div><div class="line">	kVTDecodeInfo_ImageBufferModifiable = 1UL &lt;&lt; 2,</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<h4 id="解码器的资源释放以及销毁"><a href="#解码器的资源释放以及销毁" class="headerlink" title="解码器的资源释放以及销毁"></a>解码器的资源释放以及销毁</h4><p>解码器的资源释放与销毁也编码器类似，依次调用下面两个方法：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">void VTDecompressionSessionInvalidate(VTDecompressionSessionRef session);</div><div class="line">void CFRelease(CFTypeRef cf);</div></pre></td></tr></table></figure>
<h3 id="基本数据类型"><a href="#基本数据类型" class="headerlink" title="基本数据类型"></a>基本数据类型</h3><ul>
<li>CVPixelBuffer：编码前和解码后的图像数据结构。</li>
<li>CMBlockBuffer：编码后 NALU 数据结构。</li>
<li>pixelBufferAttributes：字典设置.可能包括Width/height、pixel format type、• Compatibility (e.g., OpenGL ES, Core Animation)</li>
<li>CMVideoFormatDescription：图像存储方式，编解码器等格式描述。</li>
<li>CMSampleBuffer：存放编解码前后的视频图像的容器数据结构。</li>
<li>CMTime：时间戳相关。时间以 64-bit/32-bit 的形式出现。</li>
</ul>
<p><img src="/uploads/CMSampleBuffer.png" alt="Alt text"></p>
<h4 id="CVPixelBuffer"><a href="#CVPixelBuffer" class="headerlink" title="CVPixelBuffer"></a>CVPixelBuffer</h4><p><code>CVPixelBufferRef</code> 是 <code>Core Video</code> 框架中定义的数据类型，用来描述一张图片的内存空间，它的定义可以从 <a href="https://developer.apple.com/documentation/corevideo/cvpixelbuffer?language=objc" target="_blank" rel="external">API 文档</a> 或者头文件中可以查找到定义如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">typedef CVImageBufferRef CVPixelBufferRef;</div><div class="line">typedef CVBufferRef CVImageBufferRef;</div></pre></td></tr></table></figure>
<p>而 <code>CVBufferRef</code> 本质上就是一个 <code>void *</code> 的指针，至于 <code>void *</code> 具体是什么样的数据类型，则只有系统才知道，我们则不需要关心。<code>CVPixelBufferRef</code> 描述了一张图片的内存空间，对于该图片的属性可以通过对应的 C 语言函数来获得，例如图片的宽、高、图片的格式通过以下函数来获取：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">size_t CVPixelBufferGetWidth(CVPixelBufferRef pixelBuffer);</div><div class="line">size_t CVPixelBufferGetHeight(CVPixelBufferRef pixelBuffer);</div><div class="line">OSType CVPixelBufferGetPixelFormatType(CVPixelBufferRef pixelBuffer);</div></pre></td></tr></table></figure>
<p><code>CVPixelBufferRef</code>中的图片格式可以有 RGB 或者 YUV 的格式，这两种图片格式都可以有多种模式，对于 RGB 以及 YUV 可以参考之前的文章。</p>
<p>对于 YUV 格式的处理，可以先通过 <em><code>Boolean CVPixelBufferIsPlanar(CVPixelBufferRef pixelBuffer);</code></em> 函数来判断是否为 <code>Planar</code> 模式。若是 <code>Planar</code>，则通过 <em><code>size_t CVPixelBufferGetPlaneCount(CVPixelBufferRef pixelBuffer);</code></em> 获取 <code>YUV Plane</code> 数量，然后由 <em>`void </em> CVPixelBufferGetBaseAddressOfPlane(CVPixelBufferRef pixelBuffer, size_t planeIndex);<code>* 得到相应的通道的存储地址。在进行编码时从摄像头采集的数据就已经是</code>CVPixelBufferRef<code>格式的数据了，只需要将该数据传入编码器即可。对于解码器输出的就是</code>YUV<code>格式的</code>CVPixelBufferRef<code>的数据。对于后续的渲染可以将</code>YUV<code>转成</code>RGB<code>模式进行渲染，也可以将</code>CVPixelBufferRef<code>转成</code>OpenGL<code>所需要的</code>texture<code>然后进行渲染。也可以转成</code>CIImage<code>或者</code>UIImage` 对象进行处理。</p>
<h5 id="CMBlockBuffer"><a href="#CMBlockBuffer" class="headerlink" title="CMBlockBuffer"></a>CMBlockBuffer</h5><p>与 <code>CVPixelBufferRef</code> 类似 <code>CMBlockBufferRef</code> 也是对内存块的封装描述，在 H264 编码器中用来存储编码后的图像数据，通常是一系列的 nalu 数据。<code>CMBlockBufferRef</code> 管理的内存可以是不连续的内存块，也可能是对其他内存的引用，而 <code>CMBlockBufferRef</code> 隐藏了这些细节，通过对应的函数来对内存进行操作，比如追加内存，获取内存数据等。相关的操作函数可查看 <a href="https://developer.apple.com/documentation/coremedia/cmblockbuffer?language=objc" target="_blank" rel="external">API 文档</a>。</p>
<h5 id="CMSampleBuffer"><a href="#CMSampleBuffer" class="headerlink" title="CMSampleBuffer"></a>CMSampleBuffer</h5><p>编解码前后的视频图像均封装在 <code>CMSampleBuffer</code> 中，如果是编码后的图像，以 <code>CMBlockBuffe</code> 方式存储；解码后的图像，以 <code>CVPixelBuffer</code> 存储。<code>CMSampleBuffer</code> 里面还有另外的时间信息 <code>CMTime</code> 和视频描述信息 <code>CMVideoFormatDesc</code></p>
<p><img src="/uploads/CMSampleBuffer.png" alt="Alt text"></p>
<h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul>
<li><a href="https://developer.apple.com/documentation/videotoolbox" target="_blank" rel="external">Apple Developer</a></li>
<li><a href="http://www.jianshu.com/u/c68741efc396" target="_blank" rel="external">iOS音视频底层开发与FFmpeg</a></li>
<li><a href="http://www.jianshu.com/p/6dfe49b5dab8" target="_blank" rel="external">VideoToolbox解析</a></li>
<li><a href="https://developer.apple.com/videos/play/wwdc2014/513/" target="_blank" rel="external">Direct Access to Video Encoding and Decoding</a></li>
<li><a href="http://blog.csdn.net/kasupermen/article/details/52297138" target="_blank" rel="external">CVImageBufferRef中YUV图像</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/24762605?utm_medium=social&amp;utm_source=weibo" target="_blank" rel="external">深入理解 CVPixelBufferRef</a></li>
<li><a href="http://blog.csdn.net/fernandowei/article/details/52179631" target="_blank" rel="external">在iOS端使用AVSampleBufferDisplayLayer进行视频渲染</a></li>
<li><a href="http://www.cnblogs.com/isItOk/p/5964647.html" target="_blank" rel="external">iOS VideoToolbox硬编H.265（HEVC）H.264（AVC）：1 概述</a></li>
</ul>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;&lt;code&gt;VideoToolBox&lt;/code&gt; 是一个低级的框架，可直接访问硬件的编解码器。它能够为视频提供压缩和解压缩的服务，同时也提供存储在 &lt;code&gt;CoreVideo&lt;/code&gt; 像素缓冲区的图像进行格式的转换。该框架在 &lt;code&gt;iOS(8.0)/macOS(10.8)/tvOS(10.2)&lt;/code&gt; 以后才被开放出来，在这之前都是不能使用的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;VideoToolBox&lt;/code&gt; 框架为我们提供的接口都是以 C 语言函数形式提供的，并且以会话 &lt;code&gt;Session&lt;/code&gt; 方式来进行管理，这些接口分为以下几个类别：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;VTCompressionSession：提供数据压缩的会话&lt;/li&gt;
&lt;li&gt;VTDecompressionSession：提供数据解压缩的会话&lt;/li&gt;
&lt;li&gt;VTPixelTransferSession：提供数据格式转换的会话&lt;/li&gt;
&lt;li&gt;VTMultiPassStorage&amp;amp;VTFrameSilo：提供多通道压缩的会话&lt;/li&gt;
&lt;li&gt;VTSession&amp;amp;Utilities：针对会话的管理以及辅助的函数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里主要针对 VTCompressionSession 以及 VTDecompressionSession 两个方面的介绍。由于 &lt;code&gt;VideoToolBox&lt;/code&gt; 可进行多种格式的压缩以及解压缩，下文都是以 H264 编码格式进行介绍和举例。&lt;br&gt;
    
    </summary>
    
      <category term="音视频/WebRTC" scheme="http://enkichen.com/categories/%E9%9F%B3%E8%A7%86%E9%A2%91-WebRTC/"/>
    
    
      <category term="h264" scheme="http://enkichen.com/tags/h264/"/>
    
      <category term="VideoToolBox" scheme="http://enkichen.com/tags/VideoToolBox/"/>
    
  </entry>
  
  <entry>
    <title>C++ 实现的 Dispatch Queue</title>
    <link href="http://enkichen.com/2018/03/10/dispatch-queue/"/>
    <id>http://enkichen.com/2018/03/10/dispatch-queue/</id>
    <published>2018-03-10T11:35:45.000Z</published>
    <updated>2018-03-24T05:24:09.528Z</updated>
    
    <content type="html"><![CDATA[<p>iOS/Mac 平台下 apple 提供了非常好用的 <code>dispatch_queue</code> 能够很方便的进行线程的管理以及各个线程之间的切换（当然还有很多其他特性）。虽说 C++ 的标准库中提供了很多线程管理的方法，但相比于 <code>dispatch_queue</code> 还是弱爆了。由于项目中会经常用到，<code>GitHub</code> 上找了一些类似的实现都不太理想，于是自己实现了一版简单的主要支持一下特性：</p>
<ol>
<li>支持并发调用，并支持并发的任务处理</li>
<li>支持任务的同步执行和异步执行</li>
<li>可创建指定数量的任务线程</li>
<li>执行同步任务时，在任务线程中可继续执行同步任务而不卡死</li>
<li>同一任务可重复执行</li>
<li>支持 lambda 表达式</li>
<li>支持任务线程的安全退出</li>
</ol>
<a id="more"></a>
<h3 id="结构设计"><a href="#结构设计" class="headerlink" title="结构设计"></a>结构设计</h3><p><img src="/uploads/dispatch_queue_class.png" alt="dispatch_queue"></p>
<p>上图是 <code>DispatchQueue</code> 类结构图，结构比较简单，<code>DispatchQueue</code> 是核心抽象类；<code>QueueTask</code> 就是任务的抽象基类了，<code>ClosureTask</code> 类是一个模板类，主要用来实现 lambda 表达式；<code>DisruptorImp</code> 与  <code>MutexQueueImp</code> 则是两个具体的 <code>DispatchQueue</code> 的实现，<code>disruptor</code> 包是一个第三方库，后面会有详细介绍，。</p>
<h4 id="DispatchQueue-抽象类的定义"><a href="#DispatchQueue-抽象类的定义" class="headerlink" title="DispatchQueue 抽象类的定义"></a>DispatchQueue 抽象类的定义</h4><p>以下是 <code>DispatchQueue</code> 接口的定义和实现：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div></pre></td><td class="code"><pre><div class="line">class DispatchQueue &#123;</div><div class="line">public:</div><div class="line">    DispatchQueue(int thread_count) &#123;&#125;</div><div class="line">    </div><div class="line">    virtual ~DispatchQueue() &#123;&#125;</div><div class="line">    </div><div class="line">    template &lt;class T, typename std::enable_if&lt;std::is_copy_constructible&lt;T&gt;::value&gt;::type* = nullptr&gt;</div><div class="line">    void sync(const T &amp;task) &#123;</div><div class="line">        sync(std::shared_ptr&lt;QueueTask&gt;(new ClosureTask&lt;T&gt;(task)));</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    void sync(std::shared_ptr&lt;QueueTask&gt; task) &#123;</div><div class="line">        if( nullptr != task ) &#123;</div><div class="line">            sync_imp(task);</div><div class="line">        &#125;</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    template &lt;class T, typename std::enable_if&lt;std::is_copy_constructible&lt;T&gt;::value&gt;::type* = nullptr&gt;</div><div class="line">    int64_t async(const T &amp;task) &#123;</div><div class="line">        return async(std::shared_ptr&lt;QueueTask&gt;(new ClosureTask&lt;T&gt;(task)));</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    int64_t async(std::shared_ptr&lt;QueueTask&gt; task) &#123;</div><div class="line">        if ( nullptr != task ) &#123;</div><div class="line">            return async_imp(task);</div><div class="line">        &#125;</div><div class="line">        return -1;</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">protected:</div><div class="line">    virtual void sync_imp(std::shared_ptr&lt;QueueTask&gt; task) = 0;</div><div class="line">    </div><div class="line">    virtual int64_t async_imp(std::shared_ptr&lt;QueueTask&gt; task) = 0;</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p><code>DispatchQueue</code> 是基于建造者模式进行设计，将同步方法、异步方法对 lambda 表达式的支持，放在父类中实现，因为这部分代码相对固定，而具体的同步与异步的实现都推迟到子类中实现，因为这部分可以有不同的实现方式。抽象的父类相对简单，只实现了对 lambda 的支持，在来看看 <code>QueueTask</code> 的类设计如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div></pre></td><td class="code"><pre><div class="line">class QueueTask &#123;</div><div class="line">public:</div><div class="line">    QueueTask() : _signal(false) &#123;&#125;</div><div class="line"></div><div class="line">    virtual ~QueueTask() &#123;&#125;</div><div class="line">    </div><div class="line">    virtual void run() = 0;</div><div class="line"></div><div class="line">    virtual void signal() &#123;</div><div class="line">        _signal = true;</div><div class="line">        _condition.notify_all();</div><div class="line">    &#125;</div><div class="line"></div><div class="line">    virtual void wait() &#123;</div><div class="line">        std::unique_lock &lt;std::mutex&gt; lock(_mutex);</div><div class="line">        _condition.wait(lock, [this]()&#123; return _signal; &#125;);</div><div class="line">        _signal = false;</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    virtual void reset() &#123;</div><div class="line">        _signal = false;</div><div class="line">    &#125;</div><div class="line"></div><div class="line">private:</div><div class="line">    bool _signal;</div><div class="line">    std::mutex _mutex;</div><div class="line">    std::condition_variable _condition;</div><div class="line">&#125;;</div><div class="line"></div><div class="line">template &lt;class T&gt;</div><div class="line">class ClosureTask : public QueueTask &#123;</div><div class="line">public:</div><div class="line">    explicit ClosureTask(const T&amp; closure) : _closure(closure) &#123;&#125;</div><div class="line">private:</div><div class="line">    void run() override &#123;</div><div class="line">        _closure();</div><div class="line">    &#125;</div><div class="line">    T _closure;</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p><em><code>virtual void run() = 0</code></em> 是纯虚函数用来给子类重载，<em><code>signal()/wait()</code></em> 是两个具体方法用来支持任务的同步执行，在生产线程中执行 <code>wait()</code> 方法用来等待当前任务执行完成，而在任务线程中 <code>QueueTask</code> 执行完成后会调用 <code>signal</code> 来通知生产线程完成等待。<code>reset</code> 方法可以让该任务执行完成后重置内部状态，以便可继续将当前任务添加都队列中。<code>ClosureTask</code> 模板类则用来包装 lambda 表达式。</p>
<h4 id="基于-std-mutex-std-queue-的实现"><a href="#基于-std-mutex-std-queue-的实现" class="headerlink" title="基于 std::mutex/std::queue 的实现"></a>基于 std::mutex/std::queue 的实现</h4><p>基于标准库实现的思路很简单，使用标准库中提供的 <code>std::mutex</code>  和 <code>std::queue</code> ，在进行插入任务和执行任务时，对任务队列进行加锁操作，这里使用递归锁 <code>std::recursive_mutex</code> 而非 <code>std::mutex</code>。具体实现代码如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div><div class="line">50</div><div class="line">51</div><div class="line">52</div><div class="line">53</div><div class="line">54</div><div class="line">55</div><div class="line">56</div><div class="line">57</div><div class="line">58</div><div class="line">59</div><div class="line">60</div><div class="line">61</div><div class="line">62</div><div class="line">63</div><div class="line">64</div><div class="line">65</div><div class="line">66</div><div class="line">67</div><div class="line">68</div><div class="line">69</div><div class="line">70</div><div class="line">71</div><div class="line">72</div><div class="line">73</div><div class="line">74</div><div class="line">75</div><div class="line">76</div><div class="line">77</div><div class="line">78</div><div class="line">79</div><div class="line">80</div><div class="line">81</div><div class="line">82</div><div class="line">83</div></pre></td><td class="code"><pre><div class="line">class MutexQueueImp : public DispatchQueue &#123;</div><div class="line">public:</div><div class="line">    MutexQueueImp(int thread_count)</div><div class="line">        : DispatchQueue(thread_count),</div><div class="line">        _cancel(false),</div><div class="line">        _thread_count(thread_count) &#123;</div><div class="line">        for ( int i = 0; i &lt; thread_count; i++ ) &#123;</div><div class="line">            create_thread();</div><div class="line">        &#125;</div><div class="line">    &#125;</div><div class="line"></div><div class="line">    ~MutexQueueImp() &#123;</div><div class="line">        _cancel = true;</div><div class="line">        _condition.notify_all();</div><div class="line">        for ( auto&amp; future : _futures ) &#123;</div><div class="line">            future.wait();</div><div class="line">        &#125;</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    void sync_imp(std::shared_ptr&lt;QueueTask&gt; task) override &#123;</div><div class="line">        if ( _thread_count == 1 &amp;&amp; _thread_id == std::this_thread::get_id() ) &#123;</div><div class="line">            task-&gt;reset();</div><div class="line">            task-&gt;run();</div><div class="line">            task-&gt;signal();</div><div class="line">        &#125; else &#123;</div><div class="line">            async_imp(task);</div><div class="line">            task-&gt;wait();</div><div class="line">        &#125;</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    int64_t async_imp(std::shared_ptr&lt;QueueTask&gt; task) override &#123;</div><div class="line">        _mutex.lock();</div><div class="line">        task-&gt;reset();</div><div class="line">        _dispatch_tasks.push(task);</div><div class="line">        _mutex.unlock();</div><div class="line">        _condition.notify_one();</div><div class="line">        return 0;</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">private:</div><div class="line">    </div><div class="line">    MutexQueueImp(const MutexQueueImp&amp;);</div><div class="line">    </div><div class="line">    void create_thread() &#123;</div><div class="line">        _futures.emplace_back(std::async(std::launch::async, [&amp;]() &#123;</div><div class="line">            _thread_id = std::this_thread::get_id();</div><div class="line">            while (!_cancel) &#123;</div><div class="line">                </div><div class="line">                &#123;</div><div class="line">                    std::unique_lock &lt;std::mutex&gt; work_signal(_signal_mutex);</div><div class="line">                    _condition.wait(work_signal, [this]()&#123;</div><div class="line">                        return _cancel || !_dispatch_tasks.empty();</div><div class="line">                    &#125;);</div><div class="line">                &#125;</div><div class="line">                </div><div class="line">                while (!_dispatch_tasks.empty() &amp;&amp; !_cancel) &#123;</div><div class="line">                    _mutex.lock();</div><div class="line">                    if ( _dispatch_tasks.empty() ) &#123;</div><div class="line">                        _mutex.unlock();</div><div class="line">                        break;</div><div class="line">                    &#125;</div><div class="line">                    std::shared_ptr&lt;QueueTask&gt; task(_dispatch_tasks.front());</div><div class="line">                    _dispatch_tasks.pop();</div><div class="line">                    _mutex.unlock();</div><div class="line">                    if ( nullptr != task ) &#123;</div><div class="line">                        task-&gt;run();</div><div class="line">                        task-&gt;signal();</div><div class="line">                    &#125;</div><div class="line">                &#125;</div><div class="line">            &#125;</div><div class="line">        &#125;));</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">private:</div><div class="line">    std::vector&lt;std::future&lt;void&gt;&gt; _futures;</div><div class="line">    std::queue&lt;std::shared_ptr&lt;QueueTask&gt;&gt; _dispatch_tasks;</div><div class="line">    std::recursive_mutex _mutex;</div><div class="line">    bool _cancel;</div><div class="line">    std::mutex _signal_mutex;</div><div class="line">    std::condition_variable _condition;</div><div class="line">    int _thread_count;</div><div class="line">    std::thread::id _thread_id;</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>该类除了实现父类的<code>sync_imp/async_imp</code> 方法外，还有用来创建线程的  <code>create_thread</code> 方法，该方法每调用一次可以产生一个新的任务线程；在类的析构方法中会停止当前所有线程，并等待线程的安全退出。</p>
<p>在 <code>MutexQueueImp</code> 实现中，是典型的 <code>生产者-消费者</code> 线程模型，在 <code>async_imp</code> 方法中将任务插入到队列中，并通知任务线程；任务线程接收到任务加入队列的信号后，循环的从任务队列中取出任务，当所有任务处理完成，进入到休眠模式，直到下一个任务加入队列。</p>
<p>在 <code>sync_imp</code> 方法中会判断当前线程是否为任务线程，如果是任务线程，并且只有一个任务线程时，则直接执行任务，以免造成当前线程等待自己的情况，以免造成死锁。</p>
<h4 id="基于-disruptor-的实现"><a href="#基于-disruptor-的实现" class="headerlink" title="基于 disruptor 的实现"></a>基于 disruptor 的实现</h4><p><code>Disruptor</code> 最初是在 Java 上被发明的，这里使用它的 C++ 实现版本原理和 Java 版本是一致的。但由于 <code>Disruptor</code> 是基于 <code>发布者-订阅者</code> 的分发模型，所以当一个任务来到时，所以等待的线程，都将被唤醒，该任务可能被多个线程同时执行，在当前我们的 <code>Dispatch Queue</code> 中是不被允许的，所以只有在单线程时，才会使用 <code>Disruptor</code> 来作为 <code>Dispatch Queue</code> 的实现，以确保高效和正确性。</p>
<blockquote>
<p>C++ 版本的阻塞等待工具类 BlockingStrategy 的实现有错误，无法唤醒，所以不要使用 BlockingStrategy 作为等待策略。</p>
</blockquote>
<p>以下是 <code>disruptor</code> 实现的主要代码：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div><div class="line">50</div><div class="line">51</div><div class="line">52</div><div class="line">53</div><div class="line">54</div><div class="line">55</div><div class="line">56</div><div class="line">57</div><div class="line">58</div><div class="line">59</div><div class="line">60</div><div class="line">61</div><div class="line">62</div><div class="line">63</div><div class="line">64</div><div class="line">65</div><div class="line">66</div><div class="line">67</div><div class="line">68</div><div class="line">69</div><div class="line">70</div><div class="line">71</div><div class="line">72</div><div class="line">73</div><div class="line">74</div><div class="line">75</div><div class="line">76</div><div class="line">77</div><div class="line">78</div><div class="line">79</div><div class="line">80</div><div class="line">81</div><div class="line">82</div><div class="line">83</div><div class="line">84</div></pre></td><td class="code"><pre><div class="line">class DisruptorImp : public DispatchQueue &#123;</div><div class="line">    </div><div class="line">public:</div><div class="line">    DisruptorImp()</div><div class="line">        : DispatchQueue(1),</div><div class="line">        _cancel(false) &#123;</div><div class="line">        _sequencer = new DisruptorSequencer(_calls);</div><div class="line">        create_thread();</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    ~DisruptorImp() &#123;</div><div class="line">        _cancel = true;</div><div class="line">        int64_t seq = _sequencer-&gt;Claim();</div><div class="line">        (*_sequencer)[seq] = nullptr;</div><div class="line">        _sequencer-&gt;Publish(seq);</div><div class="line">        _future.wait();</div><div class="line">        delete _sequencer;</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">private:</div><div class="line">    void sync_imp(std::shared_ptr&lt;QueueTask&gt; task) override &#123;</div><div class="line">        if ( _thread_id == std::this_thread::get_id() ) &#123;</div><div class="line">            task-&gt;reset();</div><div class="line">            task-&gt;run();</div><div class="line">            task-&gt;signal();</div><div class="line">        &#125; else &#123;</div><div class="line">            async_imp(task);</div><div class="line">            task-&gt;wait();</div><div class="line">        &#125;</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    int64_t async_imp(std::shared_ptr&lt;QueueTask&gt; task) override &#123;</div><div class="line">        task-&gt;reset();</div><div class="line">        int64_t seq = _sequencer-&gt;Claim();</div><div class="line">        (*_sequencer)[seq] = task;</div><div class="line">        _sequencer-&gt;Publish(seq);</div><div class="line">        return 0;</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    void create_thread() &#123;</div><div class="line">        _future = std::async(std::launch::async, [&amp;]() &#123;</div><div class="line">            _thread_id = std::this_thread::get_id();</div><div class="line">            this-&gt;run();</div><div class="line">        &#125;);</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    void run() &#123;</div><div class="line">        int64_t seqWant(disruptor::kFirstSequenceValue);</div><div class="line">        int64_t seqGeted, i;</div><div class="line">        std::vector&lt;disruptor::Sequence*&gt; dependents;</div><div class="line">        disruptor::SequenceBarrier&lt;kWaitStrategy&gt;* barrier;</div><div class="line">        </div><div class="line">        disruptor::Sequence handled(disruptor::kInitialCursorValue);</div><div class="line">        dependents.push_back(&amp;handled);</div><div class="line">        _sequencer-&gt;set_gating_sequences(dependents);</div><div class="line">        dependents.clear();</div><div class="line">        barrier = _sequencer-&gt;NewBarrier(dependents);</div><div class="line">        </div><div class="line">        while (!_cancel)</div><div class="line">        &#123;</div><div class="line">            seqGeted = barrier-&gt;WaitFor(seqWant);</div><div class="line">            for (i = seqWant; i &lt;= seqGeted; i++)</div><div class="line">            &#123;</div><div class="line">                std::shared_ptr&lt;QueueTask&gt; task((*_sequencer)[i]);</div><div class="line">                (*_sequencer)[i] = nullptr;</div><div class="line">                handled.set_sequence(i);</div><div class="line">                if ( nullptr != task ) &#123;</div><div class="line">                    task-&gt;run();</div><div class="line">                    task-&gt;signal();</div><div class="line">                &#125;</div><div class="line">            &#125;</div><div class="line">            seqWant = seqGeted + 1;</div><div class="line">        &#125;</div><div class="line">        </div><div class="line">        delete barrier;</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">private:</div><div class="line">    DisruptorSequencer *_sequencer;</div><div class="line">    std::array&lt;std::shared_ptr&lt;QueueTask&gt;, disruptor::kDefaultRingBufferSize&gt; _calls;</div><div class="line">    bool _cancel;</div><div class="line">    std::future&lt;void&gt; _future;</div><div class="line">    std::thread::id _thread_id;</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p><code>DisruptorImp</code> 类的结构与 <code>MutexQueueImp</code> 基本一致，只有换成了 <code>Disruptor</code> 实现，至于对 <code>Disruptor</code> 的使用可参看文章后面的链接即可。最后 <code>DispatchQueue</code> 接口对象的创建使用一下方法来创建：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line">std::shared_ptr&lt;DispatchQueue&gt; create(int thread_count) &#123;</div><div class="line">    if ( 1 == thread_count ) &#123;</div><div class="line">        return std::shared_ptr&lt;DispatchQueue&gt;(new DisruptorImp());</div><div class="line">    &#125; else &#123;</div><div class="line">        return std::shared_ptr&lt;DispatchQueue&gt;(new MutexQueueImp(thread_count));</div><div class="line">    &#125;</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p><code>DispatchQueue</code> 可以说满足了基本的线程管理的需要，配合 lambda 表达式，使用起来也非常方便了。当然你也把它当做线程池来使用。后续如果有需要可以方便的扩展其他特性例如：延迟执行、任务之间的依赖关系等，完整的代码可在我的 <a href="https://github.com/EnkiChen/DispatchQueue" target="_blank" rel="external">GitHub</a> 上找到。</p>
<h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul>
<li><a href="http://ifeve.com/disruptor/" target="_blank" rel="external">并发框架 Disruptor</a></li>
<li><a href="http://blog.csdn.net/bjrxyz/article/details/53084539" target="_blank" rel="external">Disruptor c++ 使用指南</a></li>
</ul>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;iOS/Mac 平台下 apple 提供了非常好用的 &lt;code&gt;dispatch_queue&lt;/code&gt; 能够很方便的进行线程的管理以及各个线程之间的切换（当然还有很多其他特性）。虽说 C++ 的标准库中提供了很多线程管理的方法，但相比于 &lt;code&gt;dispatch_queue&lt;/code&gt; 还是弱爆了。由于项目中会经常用到，&lt;code&gt;GitHub&lt;/code&gt; 上找了一些类似的实现都不太理想，于是自己实现了一版简单的主要支持一下特性：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;支持并发调用，并支持并发的任务处理&lt;/li&gt;
&lt;li&gt;支持任务的同步执行和异步执行&lt;/li&gt;
&lt;li&gt;可创建指定数量的任务线程&lt;/li&gt;
&lt;li&gt;执行同步任务时，在任务线程中可继续执行同步任务而不卡死&lt;/li&gt;
&lt;li&gt;同一任务可重复执行&lt;/li&gt;
&lt;li&gt;支持 lambda 表达式&lt;/li&gt;
&lt;li&gt;支持任务线程的安全退出&lt;/li&gt;
&lt;/ol&gt;
    
    </summary>
    
      <category term="开发笔记" scheme="http://enkichen.com/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="线程" scheme="http://enkichen.com/tags/%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>Android 对 C++ Runtime 的支持</title>
    <link href="http://enkichen.com/2017/12/20/android-c-runtime-version/"/>
    <id>http://enkichen.com/2017/12/20/android-c-runtime-version/</id>
    <published>2017-12-20T12:50:24.000Z</published>
    <updated>2017-12-20T12:51:56.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote>
<p>近期将项目中的 webrtc 代码与官方最新的 m63 分支进行了同步，由于之前的代码比较老，这次升级后，webrtc 的代码结构相对来说有了很多的调整和变化，再加上对 webrtc 代码的修改和代码的添加，索性依照 m63 分支的 gn 脚本使用 cmake 重写了 webrtc 的编译脚本。在编译 android 平台时遇到一些 c++ 标准库中的函数无法进行链接的问题，于是了解了一下 android 对 c++ 标准库的支持。</p>
</blockquote>
<p>Android 对 C++ 基础库的实现有多个版本，具体的版本可以在 NDK 目录下的 <code>sources/cxx-stl</code> 子目录中查看，我当前使用的版本是 <code>android-ndk-r12b</code> 子目录内容如下：</p>
<a id="more"></a>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line">drwxr-xr-x  10 Enki  staff   340 12  7 18:05 gabi++</div><div class="line">drwxr-xr-x   5 Enki  staff   170 12  7 17:29 gnu-libstdc++</div><div class="line">drwxr-xr-x  16 Enki  staff   544 12  7 17:37 llvm-libc++</div><div class="line">drwxr-xr-x   8 Enki  staff   272 12  7 17:12 llvm-libc++abi</div><div class="line">drwxr-xr-x  16 Enki  staff   544 12  7 18:37 stlport</div><div class="line">drwxr-xr-x   9 Enki  staff   306 12  7 20:47 system</div></pre></td></tr></table></figure>
<ul>
<li>gabi++：该库不支持 C++ 标准库，但加入了对异常和 RTTI 的支持。</li>
<li>gnu-libstdc++：GNU 标准 C++ 运行时库（libstdc++-v3），同样支持异常和 RTTI。</li>
<li>llvm-libc++：该版本是 <a href="http://libcxx.llvm.org/" target="_blank" rel="external">LLVM libc++</a> 的一个移植版本，支持 C++ 的所有特性。</li>
<li>stlport：该库也提供 C++ 所有的特性支持，它是开源项目 <a href="http://www.stlport.org" target="_blank" rel="external">STLPort</a> 的一个 android 移植版本</li>
<li>system：默认最小的 C++ 运行库，这样生成的应用体积小，内存占用小，但部分功能将无法支持</li>
</ul>
<p>在 NDK 目录中都有预编译好的静态库和动态库，在实际项目中，只要链接对应的库即可。在使用 <code>cmake</code> 脚本的项目中可以使用 <code>ANDROID_STL</code> 宏来指定需要链接的库，在使用 <code>MK</code> 项目中可以使用 <code>APP_STL</code> 宏来指定链接的库例如：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line"># cmake</div><div class="line">-DANDROID_STL=c++_static</div><div class="line"></div><div class="line"># Application.mk</div><div class="line">APP_STL:= c++_static</div></pre></td></tr></table></figure>
<p>链接库可选值有如下几种：</p>
<ul>
<li>stlport_static：stlport 作为静态库</li>
<li>stlport_shared：stlport 作为动态库，这个可能产生兼容性和部分低版本的 Android 固件</li>
<li>c++_static：stdlib 作为静态库链接</li>
<li>c++_shared：stdlib 作为动态库链接</li>
<li>gnustl_static：使用 GNU libstdc++ 作为静态库</li>
<li>system：使用默认的最小的 C++ 运行库</li>
</ul>
<p>在 android 平台下编译 m63 分支时，原来使用的是 <code>gnustl_static</code> 版本的标准库，在新的 m63 分支中使用了一些诸如 <code>std::strtoll()</code> 的方法（还使用了一些其他方法也 <code>gnustl_static</code> 中没有实现的），而在 <code>gnustl_static</code> 版本的标准库中，并没有提供该函数的实现，从而导致 android 平台下无法链接 webrtc 库。这里使用了 <code>c++_static</code> 替换了 <code>gnustl_static</code> 来解决链接的问题。</p>
<p>NDK 中提供了不同版本，不同的版本有不同的特性，有些体积大提供更丰富的特性和标准函数的支持，一些体积小占用空间少，可根据具体的项目需求来选择，随着不 NDK 版本的升级，特性也会有改动。</p>
]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;近期将项目中的 webrtc 代码与官方最新的 m63 分支进行了同步，由于之前的代码比较老，这次升级后，webrtc 的代码结构相对来说有了很多的调整和变化，再加上对 webrtc 代码的修改和代码的添加，索性依照 m63 分支的 gn 脚本使用 cmake 重写了 webrtc 的编译脚本。在编译 android 平台时遇到一些 c++ 标准库中的函数无法进行链接的问题，于是了解了一下 android 对 c++ 标准库的支持。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Android 对 C++ 基础库的实现有多个版本，具体的版本可以在 NDK 目录下的 &lt;code&gt;sources/cxx-stl&lt;/code&gt; 子目录中查看，我当前使用的版本是 &lt;code&gt;android-ndk-r12b&lt;/code&gt; 子目录内容如下：&lt;/p&gt;
    
    </summary>
    
      <category term="开发笔记" scheme="http://enkichen.com/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    
    
  </entry>
  
  <entry>
    <title>图像编码与 H264 基础知识</title>
    <link href="http://enkichen.com/2017/11/26/image-h264-encode/"/>
    <id>http://enkichen.com/2017/11/26/image-h264-encode/</id>
    <published>2017-11-26T06:23:58.000Z</published>
    <updated>2018-10-23T09:06:01.510Z</updated>
    
    <content type="html"><![CDATA[<h3 id="RGB-颜色模型"><a href="#RGB-颜色模型" class="headerlink" title="RGB 颜色模型"></a>RGB 颜色模型</h3><p>图像的采集可以通过摄像头或者截取屏幕来获取的图像数据。一幅图像可以看作为一个二维的矩阵，其中矩阵中的每一个点被称为像素。像素的颜色可以通过红、绿、蓝来表示，也就是常说的 3 基色。如下图所示：</p>
<p><img src="/uploads/image_3d_matrix_rgb.png" alt="Alt text"></p>
<p>每个像素可以用不同的数据位数来表示，常用的量化位数有 16 位、24 位、32 位等，24 位最好理解，就是 RGB 的各个分量各占 8 位，取值范围为 0 ~ 255；32 位则是 24 位的基础上增加了透明度的量化位数，也是 8 位，用来表示当前像素的透明度，根据透明的位置可以分为 RGBA 和 ARGB；16 位可以分为 565 和 555 两种模式，565 则表示绿色分量占 6 位，红色和蓝色各占 5 位，555 模式则丢弃一位不用，RGB 各个分量占 5 位。量化位数越多，所能表示颜色的层次也越多，颜色则越丰富。<br><a id="more"></a><br>在表示图像的二维的矩阵中，宽和高两个维度中像素的数量称为分辨率，通常用 <code>宽 x 高</code> 来表示，例如下面分辨率为 4 x 4 的图像。</p>
<p><img src="/uploads/resolution.png" alt="Alt text"></p>
<p>图像还有另一个属性就是宽高比，例如常见的 16:9、4:3、21:9 等，这里通常指显示宽高比（DAR），同样像素也有不同的宽高比，称之为像素长宽比（PAR）。</p>
<p><img src="/uploads/DAR.png" alt="Alt text"><br><img src="/uploads/PAR.png" alt="Alt text"></p>
<h3 id="YUV-颜色模型与冗余删除"><a href="#YUV-颜色模型与冗余删除" class="headerlink" title="YUV 颜色模型与冗余删除"></a>YUV 颜色模型与冗余删除</h3><p>一张 1280x720 分辨率的图片，如果使用 RGB 颜色模型存储话，24 位量化位数的情况下需要 1280 <em> 720 </em> 3(24bit) = 2.6M 空间来存储一张图片，如果是 32 位则需要 1280 <em> 720 </em> 4(24bit) = 3.5M 存储空间，而我们的眼睛对于亮度相比于颜色更敏感，例如一下图片：</p>
<p><img src="/uploads/luminance_vs_color.png" alt="Alt text"></p>
<p>区域 A 与区域 B，是相同的颜色，第一张很难区分出来，这跟我们人眼的构造相关，人眼更加注意光线明亮度。所以针对这一特点存在另一种颜色模型 <code>YUV 颜色模型</code>，YUV 颜色模型中，其中的 Y 分量表示的是明亮度(Luminance、Luma)， U、V 表示的是色度 (Chrominance/Chroma)，如下图：</p>
<p><img src="/uploads/ycbcr.png" alt="Alt text"></p>
<p>该模型下可以采用不同的量化位数来表示亮度以及色度，所以存在不同到模式：4:4:4、4:2:2、4:1:1、4:2:0、4:1:0 和 3:1:1 六种不同的模式。在使用 4:2:0 模式时，同样的图片相比 RGB 模式，所用的内存减少一半。RGB 可以和 YUV（YCbCr）进行转换，公式如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line"># 第一步计算亮度</div><div class="line">Y = 0.299R + 0.587G + 0.114B</div><div class="line"></div><div class="line"># 一旦有了亮度，可以分割颜色（色度蓝色和红色）：</div><div class="line">Cb = 0.564(B - Y)</div><div class="line">Cr = 0.713(R - Y)</div><div class="line"></div><div class="line"># 也可以通过使用 YCbCr 进行转换，甚至获得 RGB</div><div class="line">R = Y + 1.402Cr</div><div class="line">B = Y + 1.772Cb</div><div class="line">G = Y - 0.344Cb - 0.714Cr</div></pre></td></tr></table></figure>
<h3 id="采样率、码率、帧以及场的概念"><a href="#采样率、码率、帧以及场的概念" class="headerlink" title="采样率、码率、帧以及场的概念"></a>采样率、码率、帧以及场的概念</h3><p>图像则是对模拟信号进行采样量化后获得，而视频则是由一系列的图像组成，采集时图像的分辨率及量化位数越高，所能表达的信息越多，画面则越清晰。视频存在一个采样频率的属性，即单位时间内采样的次数。视频的采样频率也受人眼的限制，通常在每秒 20 ~ 30 帧之间。当采样频率在每秒 10~20 帧时，对于快速运动的图像，人眼可以感觉到不流畅，而采样频率提高到 20~30 帧时，人眼看起来比较流畅了。如果将采样频率在提高，人眼是很难感觉这种差异的，这也是目前电影拍摄时使用 24 帧或者 30 帧采样频率的原因。</p>
<p>显示视频所需要的每秒位数称作为比特率，也叫码率。计算公式为 <em>`比特率=宽 </em> 高 <em> 位深度 </em> 每秒帧数`* 例如，如果我们不采用任何类型的压缩，每秒 30 帧，每像素 24 位，480x240 分辨率的视频将需要 82,944,000位/秒 或 82.944 Mbps（30x480x240x24）。</p>
<p>当比特率几乎恒定时，称为恒定比特率（CBR），但它也可以变化，然后称为可变比特率（VBR）。下图显示了一个受限的VBR，在帧为黑色时不会花太多位。</p>
<p><img src="/uploads/vbr.png" alt="Alt text"></p>
<p>视频采样中通过逐行扫描得到一幅完整的图像称之为一帧，通常帧频率为 25 帧（PAL制）、30 帧每秒（NTSC制），而通过隔行扫描（奇、偶数行），那么一帧图像就被分成两场，通常场频为 50Hz（PAL制）、60Hz（NTSC制）。这是在早起，工程师们提出的一种技术，能够在不消耗额外的带宽的情况下，使得显示器的感知帧率倍增。这种技术称为隔行视频；它基本上在 1 帧中发送一半的屏幕，而在下一帧中发送另一半。</p>
<p><img src="/uploads/interlaced_vs_progressive.png" alt="Alt text"></p>
<p>今天视频的显示主要使用逐行扫描技术。逐行是显示、存储或发送运动图像的方法，其中每帧的所有行被依次绘制。</p>
<h3 id="H264-编码"><a href="#H264-编码" class="headerlink" title="H264 编码"></a>H264 编码</h3><p>当需要存储视频或者网络传输时，无论是使用 RGB 还是 YUV 格式，所需要的码率和存储空间都是非常大的，例如一个分辨率为 1280 x 720 ，24 位量化位数，采样频率为每秒 30 帧，一个小时的视频则需要 <em><code>1280 x 720 x 3 x 30 x 60 x 60 = 278G</code></em> 的空间来存储该视频。而在显示或网络传输该视频时所需要的码率为 <em><code>1280 x 720 x 24 x 30 = 632Mbps</code></em>，这么大数据量很难实际应用，因为网络带宽和硬盘存储空间都是非常有限的。</p>
<p>针对以上问题则需要对视频进行编码压缩，而 H264 是其中一种编解码标准；H264 由视频编解码器 H.261 发展而来的，它诞生于 1990 年（技术上为 1988 年），其设计工作的数据速率为 64 kbit / s。它已经使用了色度子采样，宏块等方面的想法。在 1995 年，H.263 视频编解码器标准被公布，并持续延续至 2001 年。</p>
<p>2003 年，第一版 H.264/AVC 完成。同一年，一家名为 TrueMotion 的公司将其视频编解码器作为免版税的有损视频压缩称为 VP3。2008 年，Google 收购了该公司，同年发布了 VP8。2012 年 12 月，Google 发布了 VP9， 大约有 3/4 的浏览器市场（包括手机）支持。</p>
<p>AV1 是一种新的视频编解码器，免版税并且开放源代码，由 AOMedia 联盟设计，该组织包括：谷歌，Mozilla，微软，亚马逊，Netflix，AMD，ARM，NVIDIA，英特尔，思科等等，编解码器的第一个版本 0.1.0 于 2016 年 4 月 7 日发布。</p>
<p><img src="/uploads/codec_history_timeline.png" alt="Alt text"></p>
<h4 id="H264-编解码方法"><a href="#H264-编解码方法" class="headerlink" title="H264 编解码方法"></a>H264 编解码方法</h4><p> <strong>去除数据统计冗余</strong>：H264采用两种熵编码方式：CAVLC （ 基于上下文的可变字长编码）和 CABAC（基于上下文的二进制算数编码），CAVLC 实现相对简单，编码效率高，但压缩率要比 CABAC 低 15% 左右，CABAC 复杂度高，可以分场景采用不同的熵编码，让视频压缩后的平均码长接近信源熵值。</p>
<p><strong>去除空间冗余</strong>：去除空间冗余则仅考虑本帧的数据而不考虑相邻帧之间的冗余信息，这实际上与静态图像压缩类似。帧内一般采用有损压缩算法，由于帧内压缩是编码一个完整的图像，所以可以独立的解码、显示。帧内压缩一般达不到很高的压缩，跟编码 jpeg 差不多。除此以外，H264采用变换编码的方式，将残差从空间域利用 DCT(离散余弦变换)变换到频率域，结合差异量化编码方式，更进一步的去除空间冗余。</p>
<p><strong>去除时间冗余</strong>：相邻几帧的数据有很大的相关性，或者说前后两帧信息变化很小的特点。也即连续的视频其相邻帧之间具有冗余信息，根据这一特性，压缩相邻帧之间的冗余量就可以进一步提高压缩量，减小压缩比。帧间压缩也称为时间压缩（Temporal compression），它通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩一般是无损的。帧差值（Frame differencing）算法是一种典型的时间压缩法，它通过比较本帧与相邻帧之间的差异，仅记录本帧与其相邻帧的差值，这样可以大大减少数据量。</p>
<p><strong>去除人眼视觉冗余</strong>：H264编码器的输入图像或视频的色彩空间采样格式一般为 YUV420，不同于 RGB 采样，YUV420 利用人眼视觉对像素亮度分量更敏感，而色度分量没那么敏感，进一步将图像或视频的色度分量做 2:1 的采样，4 个亮度分量，2 个色度分量，另外，H264 采用量化编码的有损编码方式，也正是利用了人眼视觉对高频细节部分不敏感的理论基础，将残差系数低频部分采用更细的量化参数，而高频部分则粗化量化，一般的视频压缩失真也正是这个阶段产生。</p>
<p>除了以上是 H264 所使用的技术，还使用了以下方法：</p>
<p><strong>帧分组（GOP）</strong>：在 H264 中图像以序列为单位进行组织，一个序列是一段图像编码后的数据流，以 I 帧开始，到下一个 I 帧结束。当运动变化比较少时，一个序列可以很长，因为运动变化少就代表图像画面的内容变动很小，所以就可以编一个 I 帧，然后一直 P 帧、B 帧了，可以有效的降低码率。当运动变化多时，一个序列就需要设置的比较小，比如就包含一个 I 帧 和 3、4 个 P 帧，这样可以控制 P 帧和 B 帧的大小。</p>
<p><strong>帧类型的定义</strong>：将每组内各帧图像定义为三种类型，即 I 帧（IDR）、B 帧和 P 帧；<code>I 帧是帧内编码帧</code>，I 帧表示关键帧，可以理解为这一帧画面的完整保留， I 帧特点：</p>
<ol>
<li>它是一个全帧压缩编码帧。它将全帧图像信息进行 JPEG 压缩编码及传输； </li>
<li>解码时仅用 I 帧的数据就可重构完整图像；</li>
<li>I 帧描述了图像背景和运动主体的详情；</li>
<li>I 帧不需要参考其他画面而生成；</li>
<li>I 帧是 P 帧和 B 帧的参考帧，其质量直接影响到同组中以后各帧的质量；</li>
<li>I 帧是帧组 GOP 的基础帧（第一帧），在一组中只有一个 I 帧； </li>
<li>I 帧不需要考虑运动矢量；</li>
<li>I 帧所占数据的信息量比较大。</li>
</ol>
<p><code>P 帧是前向预测编码帧</code>。P 帧表示的是这一帧跟之前的一个关键帧（或 P 帧）的差别，解码时需要用之前缓存的画面叠加上本帧定义的差别，生成最终画面。P帧是以 I 帧或 P 帧为参考帧，在参考帧中找出 P 帧的差异 P 帧特点: </p>
<ol>
<li>P 帧采用运动补偿的方法传送它与前面的 I 或 P 帧的差值及运动矢量（预测误差）；</li>
<li>解码时必须将 I 帧中的预测值与预测误差求和后才能重构完整的 P 帧图像；</li>
<li>P 帧属于前向预测的帧间编码，它只参考前面最靠近它的  I 帧 或 P 帧；</li>
<li>P 帧可以是其后面 P 帧的参考帧，也可以是其前后的 <code>B帧</code> 的参考帧；</li>
<li>由于 P 帧是参考帧，它可能造成解码错误的扩散；</li>
<li>由于是差值传送，P 帧的压缩比较高。 </li>
</ol>
<p><code>B 帧是双向预测内插编码帧</code>。B 帧是双向差别帧，也就是 B 帧记录的是本帧与前后帧的差别，换言之，要解码 B 帧不仅要取得之前的缓存画面，还要解码之后的画面，通过前后画面的与本帧数据的叠加取得最终的画面。 B 帧以前面的 I 或 P 帧 和后面的 P 帧为参考帧，B 帧特点：</p>
<ol>
<li>B 帧是由前面的 I 或 P 帧和后面的 P 帧来进行预测的；</li>
<li>B 帧传送的是它与前面的 I 或 P 帧和后面的 P 帧之间的预测误差及运动矢量；</li>
<li>B 帧是双向预测编码帧；</li>
<li>B 帧压缩比最高，因为它只反映丙参考帧间运动主体的变化情况，预测比较准确；</li>
<li>B 帧不是参考帧，不会造成解码错误的扩散。 </li>
</ol>
<blockquote>
<ol>
<li>I、P、B 各帧是根据压缩算法的需要，是人为定义的，它们都是实实在在的物理帧。</li>
<li>一般来说，I 帧的压缩率是 7（跟 JPG 差不多），P 帧是 20，B 帧可以达到 50。</li>
<li>B 帧的使用会导致解码的延时，因为需要参考前后帧，同时也会增加 CPU 负担。</li>
<li>P 帧和 B 帧不会越过 I 帧（IDR图像）去参考其他帧。</li>
</ol>
</blockquote>
<h4 id="H264-的编码参数"><a href="#H264-的编码参数" class="headerlink" title="H264 的编码参数"></a>H264 的编码参数</h4><p>H264 的编码的实现存在很多的控制参数，不同的编码器实现可能有一些特殊的参数可供设置，不同的控制参数得到不同的输出结果，以便适应不同的应用场景，这里介绍一些通用的参数，后续在介绍一些具体编码器以及应用时，在对一些参数进行详细的说明。</p>
<p><strong>H.264 Profiles</strong>：Profile 用于确定编码过程中帧间压缩使用的算法，常见的 H264 编码 Profile 有如下 3 种：</p>
<p><code>1、Baseline Profile (BP)</code>：只支持 I/P 帧，无交错（Progressive）和 CAVLC，主要用于计算能力有限的应用环境，一般用于低阶或需要额外容错的应用，比如视频通话、手机视频等；<br>优势：编解码开销较小，速度快，适用于视频会议等延时敏感的场景或者解码能力不够的环境。<br>不足：同等码率下，Baseline 的画质最差。</p>
<p><code>2、Main Profile (MP)</code>：支持 I/P/B 帧，无交错（Progressive）和交错（Interlaced），同时提供对于 CAVLC 和 CABAC 的支持，用于主流消费类电子产品规格如低解码（相对而言）的 MP4、便携的视频播放器；<br>优势：CABAC 的支持和 B 帧的引入，相同码率下画质相对 Baseline 有 15% 的提升。<br>不足：运算量上和画质相对于 HiP 没有太大优势，绝大部份情况下已经被 HiP 取代。</p>
<p><code>3、High Profile (HiP)</code>：在 Main 的基础上增加了 8x8 内部预测、自定义量化、无损视频编码和更多的 YUV 格式（如4：4：4）用于广播，高清电视及视频碟片存储（HiP被采纳为 HD DVD 和蓝光的编码标准）。<br>优势：8x8 内部预测的引入使得 HiP 能够较好的编码快速移动的场景、支持码流之间有效的切换（SP 和 SI 片）、改进误码性能。</p>
<blockquote>
<p>CAVLC 和 CABAC 是两种不同的熵编码（一种无损，变长编码数据压缩方案）形式，CAVLC 效率高压缩比低，CABAC 反之。</p>
</blockquote>
<p>除此之外还存一下类别：</p>
<p><code>High 10 Profile (Hi10P)</code>：超越目前主流的消费型产品能力，此 profile 建立在 High Profile 之上，多了支援 10 bit 的精度，色彩更精准。以影片压制而言，将 High Profile (8 bit) 影片重新编码输出为 H.264 High 10 Profile ，虽然色彩精度不会变，但至少压缩率比输出 High Profile (8 bit) 还要高。</p>
<p><code>High 4:2:2 Profile (Hi422P)</code>：针对专业领域应用，此 profile 建立在 High 10 Profile 之上，多了支援 4:2:2 色彩取样，位元深度达 10 bit。</p>
<p><code>High 4:4:4 Predictive Profile (Hi444PP)</code>：此 profile 建立在 High 4:2:2 Profile 之上，多了支援 4:4:4 色彩取样，位元深度达 14 bit，并且支援高效率无损重新编码，且每张画面编码为三个独立的色彩平面。</p>
<p><strong>H.264 Level</strong>： Level 是对视频本身特性的一些描述(码率，分辨率，fps等)，Level 越高，视频的码率、分辨率、fps 越高。Level 也表示了对播放设备的解码性能要求，值越高，代表播放设备解码性能要求越高，相对的输出影片的压缩率也越越高。</p>
<blockquote>
<p>iOS 设备对 Profile 和 Level 的支持<br>H.264 Baseline 3.0: All devices.<br>H.264 Baseline 3.1: iPhone 3G and later, and iPod touch 2nd generation and later.<br>H.264 Main profile 3.1: iPad (all versions), Apple TV 2 and later, and iPhone 4 and later.<br>H.264 Main Profile 4.0: Apple TV 3 and later, iPad 2 and later, and iPhone 4S and later.<br>H.264 High Profile 4.0: Apple TV 3 and later, iPad 2 and later, and iPhone 4S and later.<br>H.264 High Profile 4.1: iPad 2 and later and iPhone 4S and later.</p>
</blockquote>
<p>关于 Profile 和 Level 更详细资料可参考 WiKi 百科的 <a href="https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC" target="_blank" rel="external">H.264/MPEG-4 AVC</a> 资料。</p>
<p><strong>GOP 长度</strong>：一个 <code>key frame (IDR)</code> 到下一个 <code>key frame</code> 的范围。表示关键帧的间隔，一个序列的第一个图像叫做 IDR 图像（立即刷新图像），IDR 图像都是 I帧 图像。当解码器解码到 IDR 图像时，立即将参考帧队列清空，将已解码的数据全部输出或抛弃，重新查找参数集，开始一个新的序列。这样，如果前一个序列出现重大错误，在这里可以获得重新同步的机会。IDR 图像之后的图像永远不会使用 IDR 之前的图像的数据来解码。</p>
<p><strong>Constant Quantizer (QP)</strong>：恒定量化值（Constant Quantizer）可以用来控制图像编码的品质。编码器中一般可以这是最大值和最小值。比较低的数值会得到比较高的品质。</p>
<p><strong>Maximun B-frame</strong>：当设定 B 帧时，重复部分比较多/变化较少的 Frames 会被编码为 B 帧此值限制 B 帧的最大连续数量。</p>
<p><strong>Reference frame (ref)</strong>： 设定一个 P 帧所能参考的帧的数量，ref 会影响播放相容性。</p>
<p><strong>码率控制(Bitrate Control)</strong>：CBR（恒定码率）、VBR（动态码率）；码率控制实际上是一种编码的优化算法，它用于实现对视频流码流大小的控制。目的在于同样的视频编码格式，码流大，它包含的信息也就越多，那么对应的图像也就越清晰，反之亦然。VBR 码流控制方式可以降低图像动态画面少时候的带宽占用，CBR 控制方式码流稳定，图像状态较稳定。他们为了解决的是不同需求下的不同应用。</p>
<h4 id="AVC-与-SVC"><a href="#AVC-与-SVC" class="headerlink" title="AVC 与 SVC"></a>AVC 与 SVC</h4><p>AVC 实际上是 H.264 协议的别名。SVC 是 scalable video coding，有的翻译成分层，有的翻译成分级；SVC 是以 H264/AVC 为基础，利用了AVC编解码器的各种高效算法工具，在语法和工具集上进行了扩展，支持分级特性的码流，包括空域（分辨率）可分级、时域（帧率）可分级和质量可分级；从而可产生不同帧速率、分辨率或质量等级的解码视频。</p>
<p>自从 H.264 协议中增加了 SVC 的部分之后，人们习惯将不包含 SVC 的 H.264 协议那一部分称为 AVC，而将 SVC 这一部分单独称为 SVC。所以提到 AVC 的时候，需要根据具体情况判断到底是指 H.264 协议还是指协议中不包含 SVC 的那一部分。</p>
<p>为此，具备 H.264 SVC 编码的视频会议系统，在保证高效的视频压缩性能的基础上，视频广播端可以通过一次编码产生具有不同帧率、分辨率的视频压缩码流，以适应不同网络带宽、不同的显示屏幕和终端解码能力的应用需求，从而有效地避免了视频会议系统中 MCU 上复杂而昂贵的转码。</p>
<p>H.264 SVC 与以往传统视频协议 H.264AVC 的区别表</p>
<table>
<thead>
<tr>
<th style="text-align:center">项目</th>
<th style="text-align:center">H264/AVC</th>
<th style="text-align:center">H264/SVC</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">视频会议可以容忍的网络丢包率</td>
<td style="text-align:center">&lt; 2~3 %</td>
<td style="text-align:center">&lt; 20%</td>
</tr>
<tr>
<td style="text-align:center">对网络的要求</td>
<td style="text-align:center">专线</td>
<td style="text-align:center">共享线路</td>
</tr>
<tr>
<td style="text-align:center">视频会议延迟</td>
<td style="text-align:center">400ms</td>
<td style="text-align:center">&lt; 200ms</td>
</tr>
<tr>
<td style="text-align:center">视频会议实时性</td>
<td style="text-align:center">非实时性交互</td>
<td style="text-align:center">实时性交互</td>
</tr>
<tr>
<td style="text-align:center">HD 会议室终端开销</td>
<td style="text-align:center">昂贵</td>
<td style="text-align:center">普及型</td>
</tr>
</tbody>
</table>
<h5 id="分级编码的应用"><a href="#分级编码的应用" class="headerlink" title="分级编码的应用"></a>分级编码的应用</h5><p><strong>1、监控领域</strong>：监控视频流一般产生 2 路，1 路质量好的用于存储，1 路用于预览。用 SVC 编码器可以产生 2 层的分级码流，1 个基本层用于预览，1 个增强层保证存储的图像质量是较高的。使用手机远程监控预览的情况下，可以产生一个低码率的基本层。</p>
<p><strong>2、视频会议</strong>：视频会议终端利用 SVC 编出多分辨率，分层质量，会议的中心点替代传统 MCU 二次编解码方法改为视频路由分解转发。也可在网络丢包环境下利用时域可分级，抛弃部分时域级实现网络适应性。在云视讯领域 SVC 也有想像空间。</p>
<p><strong>3、流媒体IPTV应用</strong>：服务器可以根据不同的网络情况丢弃质量层，保证视频的流畅。</p>
<p><strong>4、兼容不同网络环境和终端的应用</strong></p>
<h5 id="分级的优缺点"><a href="#分级的优缺点" class="headerlink" title="分级的优缺点"></a>分级的优缺点</h5><p><strong>优点</strong>： 分级码流优点是应用非常灵活，可以根据需要产生不同的码流或者提取出不同的码流。使用 SVC 实现一次分层编码比用 AVC 编多次更高效。分层编码有技术优势，新的编码器 H.265 也使用了分层思想，可以实现灵活的应用，也可提高网络适应性。</p>
<p><strong>缺点</strong>： 分级码流的解码复杂度增加。基本层是 AVC 兼容码流，编码效率没有影响。在同样的条件下，分级码流比单层码流的压缩效率要低 10% 左右，分级层数越多，效率下降越多，现在的 JSVM 编码器最多支持 3 个空域分级层。在同样的条件下，分级码流比单层码流的解码计算复杂度高。SVC 是 2007 年 10 月才做为正式标准，兼容性和对通性远没有 AVC 好，所以 SVC 实际应用不是广泛。</p>
<h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul>
<li><a href="https://github.com/leandromoreira/digital_video_introduction" target="_blank" rel="external">视频技术介绍</a></li>
<li><a href="https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC" target="_blank" rel="external">H.264/MPEG-4 AVC</a></li>
<li><a href="https://www.quora.com/H-264-Any-good-explanations-out-there-for-the-actual-pros-cons-of-using-Baseline-vs-Main-vs-High-profiles" target="_blank" rel="external">对于使用Baseline vs. Main与High Profile的实际利弊有什么好的解释？</a></li>
<li><a href="http://www.jianshu.com/p/f03e9ac9c9ef" target="_blank" rel="external">深入理解color model(颜色模型)</a></li>
<li><a href="http://www.cnblogs.com/yjg2014/p/6144977.html" target="_blank" rel="external">H264(NAL简介与I帧判断)</a></li>
<li><a href="http://www.cnblogs.com/yjg2014/p/6127454.html" target="_blank" rel="external">移动直播技术秒开优化经验</a></li>
</ul>
]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;RGB-颜色模型&quot;&gt;&lt;a href=&quot;#RGB-颜色模型&quot; class=&quot;headerlink&quot; title=&quot;RGB 颜色模型&quot;&gt;&lt;/a&gt;RGB 颜色模型&lt;/h3&gt;&lt;p&gt;图像的采集可以通过摄像头或者截取屏幕来获取的图像数据。一幅图像可以看作为一个二维的矩阵，其中矩阵中的每一个点被称为像素。像素的颜色可以通过红、绿、蓝来表示，也就是常说的 3 基色。如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/image_3d_matrix_rgb.png&quot; alt=&quot;Alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;每个像素可以用不同的数据位数来表示，常用的量化位数有 16 位、24 位、32 位等，24 位最好理解，就是 RGB 的各个分量各占 8 位，取值范围为 0 ~ 255；32 位则是 24 位的基础上增加了透明度的量化位数，也是 8 位，用来表示当前像素的透明度，根据透明的位置可以分为 RGBA 和 ARGB；16 位可以分为 565 和 555 两种模式，565 则表示绿色分量占 6 位，红色和蓝色各占 5 位，555 模式则丢弃一位不用，RGB 各个分量占 5 位。量化位数越多，所能表示颜色的层次也越多，颜色则越丰富。&lt;br&gt;
    
    </summary>
    
      <category term="音视频/WebRTC" scheme="http://enkichen.com/categories/%E9%9F%B3%E8%A7%86%E9%A2%91-WebRTC/"/>
    
    
      <category term="h264" scheme="http://enkichen.com/tags/h264/"/>
    
  </entry>
  
  <entry>
    <title>iOS 静态库中的 Category 运行时错误</title>
    <link href="http://enkichen.com/2017/09/13/ios-static-library-categories/"/>
    <id>http://enkichen.com/2017/09/13/ios-static-library-categories/</id>
    <published>2017-09-13T03:49:24.000Z</published>
    <updated>2017-09-13T03:52:05.000Z</updated>
    
    <content type="html"><![CDATA[<p>最近在弄 iOS 下的音视频 SDK 的移植和适配，该 SDK 是基于 WebRTC 项目但并未使用官方的 <code>ninja</code> 编译脚本，而是使用的 <code>cmake</code> 作为编译工具。在 WebRTC 的音频模块中引用了一个 <code>UIDevice</code> 的 <code>Category</code> 来做设备类型的判断，编译和链接都没有出现问题，但在运行时出现了 <code>selector not recognized</code> 的异常。该异常可以从我之前的  <a href="http://www.enkichen.com/2017/04/21/ios-message-forwarding/" target="_blank" rel="external">《iOS 运行时之消息转发机制》</a> 文章中了解到，由于对象接收到了一个无法处理该 <code>selector</code> ，经过消息转发后还未得到处理，会在 <code>doesNotRecognizeSelector</code> 方法中抛出的异常。</p>
<p><code>Category</code> 以及一些其他的工具类被编译在一个基础的静态库中，在音频模块中引用该静态库，除了 <code>Category</code> 代码其他所有的代码都能正常编译、链接以及运行，但唯独 <code>Category</code> 在运行时出现了错误，由于 <code>Category</code> 的 Objective-C 语言的特性，最开始我以为需要为编译器添加针对 <code>Category</code> 的参数，找了很久也没找到针对 <code>Category</code> 特性的编译参数，无奈之下只好求助 <code>stackoverflow</code> ，最终找到了根本原因和解决方法。<br><a id="more"></a> </p>
<h3 id="编译-链接过程"><a href="#编译-链接过程" class="headerlink" title="编译/链接过程"></a>编译/链接过程</h3><p>编译器在编译的过程中首先会将 <code>.c/.cc/.cpp/.m/.mm</code> 的源文件编译成后缀为 <code>.o</code> 的对象文件 ，源文件与对象文件是一一对应的，对象文件中包含了符号、代码以及数据，对象文件是不能被系统加载并使用的。当在编译生成静态库时，这些所有的对象文件，都会被封装到 <code>.a(archive)</code> 文件，可以理解为一个归档文件，也就是我们平常使用的静态库。</p>
<p>当需要生产二进制文件或是动态库时，编译器会对静态库 <code>.a(archive)</code> 文件进行处理；编译器将获取静态库中所以的符号表，并检查哪些符号被引用，只有被引用的对象文件才会被链接器真正的加载并处理。例如在静态库中有 10 个对象文件，但被引用到只有 2 个，则链接器只加载被引用的 2 个对象文件，未被使用到的 8 个对象文件则会被忽略。</p>
<p>在 C/C++ 语言中，这种机制可以很好的工作，因为 C/C++ 语言会尽可能的在编译期去做这些事。在 Objective-C 语言中非常依赖运行时特性，<code>Category</code> 就是基于运行时实现，但它并不会像类或者函数一样被创建链接符号，编译器在检查符号表时便不能检查到 <code>Category</code> 对应的符号表，从而 <code>Category</code> 的对象文件就不能被正常的加载，在运行时便无法找到对应的 <code>selector</code> 。</p>
<h3 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h3><p>基于上述的原因，网上存在 5 种解决方法：</p>
<ol>
<li>在编译选项 <code>Other Linker Flags</code> 中添加 <code>-all_load</code>，用于会告诉编译器 <strong>对于所有静态库中的所有对象文件，不管里面的符号有没有被用到，全部都载入</strong>，这种方法可以解决问题，但是会产生比较大的二进制文件。</li>
<li>添加编译选项 <code>-force_load</code> 并指定路径，<code>-force_load $(BUILT_PRODUCTS_DIR)/&lt;library_name.a&gt;</code> 这种方法和 <code>-all_load</code> 类似，不同的是它只载入指定的静态库。</li>
<li>在编译选项 <code>Other Linker Flags</code> 中添加 <code>-ObjC</code>，这个标识告诉编译器 <strong>如果在静态库的对象文件中发现了 Objective-C 代码，就把它载入</strong>，<code>Category</code>  中肯定会存在 Objective-C 代码。该方法与前两张类似，只是将加载的范围减少了。</li>
<li>另一种解决方法是新版本 Xcode 里 <code>build setting</code> 中的 <code>Perform Single-Object PreLink</code>，如果启用这个选项，所有的对象文件都会被合并成一个单文件（这不是真正的链接，所以叫做预链接），这个对象文件（有时被称做主对象文件 <code>master object file</code>）被添加到静态库中。现在如果主对象文件中的任何符号被认为是在使用，整个主对象文件都会被认为在使用，这样它里面的 Objective-C  部分就会被载入了，当然也包括 <code>Category</code> 对应的对象文件。</li>
<li>最后一种解决方法是在 <code>Category</code> 的源文件里添加 <code>Fake symbol</code>，并确保以某种方法在编译时引用了该 <code>Fake symbol</code>，这会使得 <code>Fake symbol</code> 对象文件被加载时它里面 <code>Category</code>  代码也会被载入。该方法可以控制哪些 <code>Category</code> 可以被正常加载，同时也不需要添加编译参数做特殊处理。</li>
</ol>
<blockquote>
<p>在 64 位的 Mac 系统或者 iOS 系统下，链接器有一个 bug，会导致只包含有 Category 的静态库无法使用 -ObjC 标志来加载  Objective-C 对象文件。</p>
</blockquote>
<h3 id="问题的处理"><a href="#问题的处理" class="headerlink" title="问题的处理"></a>问题的处理</h3><p>我这里用的是最后一种方法来解决该问题，原因在于前 4 种都会增加二进制文件的体积，在第三方使用当前 SDK 时需要手动设置编译参数，会给第三方带来不好的使用体验。为了使用方便可定义一下宏：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line">#define FIX_CATEGORY_BUG_H(name) \</div><div class="line">@interface FIX_CATEGORY_BUG_##name : NSObject \</div><div class="line">+(void)print; \</div><div class="line">@end</div><div class="line"></div><div class="line">#define FIX_CATEGORY_BUG_M(name) \</div><div class="line">@implementation FIX_CATEGORY_BUG_##name \</div><div class="line">+ (void)print &#123;&#125; \</div><div class="line">@end</div><div class="line"></div><div class="line">#define ENABLE_CATEGORY(name) [FIX_CATEGORY_BUG_##name print]</div></pre></td></tr></table></figure>
<p>在 <code>Category</code> 的头文件中使用 <code>FIX_CATEGORY_BUG_H()</code> 宏来声明一个 <code>Fake symbol</code> ，在 <code>Category</code> 的实现文件中使用 <code>FIX_CATEGORY_BUG_M()</code> 宏来实现该 <code>Fake symbol</code>。最后在找一处运行 <code>ENABLE_CATEGORY()</code> 宏，可以是初始化方法中，也可以是其他任何地方，只要确保它能被正常调用，目的在于该 <code>Fake symbol</code> 确保编译器能正常加载它。</p>
<h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>在 iOS/Mac 平台下，包含 <code>Category</code> 的静态库无法被正常加载，原因在于 <code>Category</code> 是 Objective-C 语言的特性，编译器并不会为它生成链接符号，在链接过程中便无法找到该对象文件的引用关系，链接器将会直接忽略掉 <code>Category</code>  对应的对象文件，从而在运行时无法找到相应的 <code>selector</code>。解决该问题的目标就是让链接器加载 <code>Category</code> 对应的对象文件，一种方法是添加编译参数让编译器加载所有的对象文件或是加载指定的对象文件；另一种方法是在 <code>Category</code>  的对象文件中添加 <code>Fake symbol</code> ，当 <code>Fake symbol</code> 被加载时 <code>Category</code> 的对象文件便一同被加载。</p>
<h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul>
<li><a href="https://stackoverflow.com/questions/2567498/objective-c-categories-in-static-library" target="_blank" rel="external">Objective-C categories in static library</a></li>
<li><a href="http://www.dreamingwish.com/frontui/article/default/the-create-the-static-the-library-containing-the-category.html" target="_blank" rel="external">创建含有category的静态库,selector not recognized的解决方案</a></li>
</ul>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近在弄 iOS 下的音视频 SDK 的移植和适配，该 SDK 是基于 WebRTC 项目但并未使用官方的 &lt;code&gt;ninja&lt;/code&gt; 编译脚本，而是使用的 &lt;code&gt;cmake&lt;/code&gt; 作为编译工具。在 WebRTC 的音频模块中引用了一个 &lt;code&gt;UIDevice&lt;/code&gt; 的 &lt;code&gt;Category&lt;/code&gt; 来做设备类型的判断，编译和链接都没有出现问题，但在运行时出现了 &lt;code&gt;selector not recognized&lt;/code&gt; 的异常。该异常可以从我之前的  &lt;a href=&quot;http://www.enkichen.com/2017/04/21/ios-message-forwarding/&quot;&gt;《iOS 运行时之消息转发机制》&lt;/a&gt; 文章中了解到，由于对象接收到了一个无法处理该 &lt;code&gt;selector&lt;/code&gt; ，经过消息转发后还未得到处理，会在 &lt;code&gt;doesNotRecognizeSelector&lt;/code&gt; 方法中抛出的异常。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Category&lt;/code&gt; 以及一些其他的工具类被编译在一个基础的静态库中，在音频模块中引用该静态库，除了 &lt;code&gt;Category&lt;/code&gt; 代码其他所有的代码都能正常编译、链接以及运行，但唯独 &lt;code&gt;Category&lt;/code&gt; 在运行时出现了错误，由于 &lt;code&gt;Category&lt;/code&gt; 的 Objective-C 语言的特性，最开始我以为需要为编译器添加针对 &lt;code&gt;Category&lt;/code&gt; 的参数，找了很久也没找到针对 &lt;code&gt;Category&lt;/code&gt; 特性的编译参数，无奈之下只好求助 &lt;code&gt;stackoverflow&lt;/code&gt; ，最终找到了根本原因和解决方法。&lt;br&gt;
    
    </summary>
    
      <category term="开发笔记" scheme="http://enkichen.com/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="Category" scheme="http://enkichen.com/tags/Category/"/>
    
  </entry>
  
  <entry>
    <title>WebRTC 的丢帧策略</title>
    <link href="http://enkichen.com/2017/07/29/webrtc-drop-frame/"/>
    <id>http://enkichen.com/2017/07/29/webrtc-drop-frame/</id>
    <published>2017-07-29T09:35:15.000Z</published>
    <updated>2018-11-14T01:41:09.989Z</updated>
    
    <content type="html"><![CDATA[<blockquote>
<p>最近在弄一个 Android 平台下远程桌面项目，是基于 WebRTC 框架实现的，由于平台限制，芯片是用的特定厂商的芯片，桌面采集以及 H264 硬编码都是芯片厂商提供方案，能够有很好性能表现。在将桌面的采集以及 H264 的编码库整合到 WebRTC 框架中时，发现的存在画面延时的问题，于是对 WebRTC 的编解码框架的源码进行了分析，了解到 WebRTC 的两个特性。</p>
</blockquote>
<p>在 WebRTC 中有很多可以控制视频帧率和码率的行为，这里介绍一下当视频采集过快以及编码器输出码率过高时，WebRTC 主动丢帧的策略。这里采集过快是指视频采集的速度大于编码器处理的速度。例如视频采集线程每秒采集 30 帧，而编码线程每秒只能处理 15 帧，这时候就出现了采集过快或者说是编码器太慢。编码器输出码率过高，是指编码器的输出码率大于设定的最高输出码率值，例如设置编码器码率为 1024kbps，而实际产生的码率是 1500kbps 这时候就出现了输出码率过高的情况。本文主要介绍上述两种情况下 WebRTC 是如何实现丢帧行为的。<br><a id="more"></a><br>下图是这次涉及到一些类的结构图，如果要在 WebRTC 源码中找对对应的文件，直接搜索类名称即可。WebRTC 的源码由于在墙外，我这里为了后续方便拷贝了一份到我的 GitHub 空间中，<a href="https://github.com/EnkiChen/webrtc" target="_blank" rel="external">点击这里</a> 可以找到源码。</p>
<p><img src="/uploads/webrtc_class_drop.png" alt="Example"></p>
<h3 id="采集过快时的丢帧策略"><a href="#采集过快时的丢帧策略" class="headerlink" title="采集过快时的丢帧策略"></a>采集过快时的丢帧策略</h3><p>在 WebRTC 中视频或者桌面图像的采集和图像的编码线程各自是独立线程进行工作的，采集线程会将采集好的图像打包成 <code>VideoFrame</code> 对象，这里的 <code>VideoFrame</code> 封装了经过转换的 <code>YUV</code> 数据而非 <code>RGB</code> 数据， <code>VideoFrame</code> 在经过一系列的周转后，最终会在 <em><code>ViEEncoder</code></em> 类中得到处理，在 <em><code>ViEEncoder</code></em> 类一部分工作是将数据从采集线程 <code>post</code> 到编码线程中，这里就会涉及到多线程间协作的问题。</p>
<p>这里用的是 <code>rtc::TaskQueue</code> 队列类，在 <em><code>ViEEncoder</code></em> 类内部，会将所有跟编码器相关的操作都将打包成一个 <code>Task</code> 扔到队列中，确保所有的操作都是顺序执行的。在 <em><code>onFrame()</code></em> 方法中接收到采集线程传进来的 <code>VideoFrame</code> 对象，并将其打包成一个 <code>EncodeTask</code> 对象放入队列中。代码片段如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line">void ViEEncoder::OnFrame(const VideoFrame&amp; video_frame) &#123;</div><div class="line"></div><div class="line">    ...</div><div class="line"></div><div class="line">    last_captured_timestamp_ = incoming_frame.ntp_time_ms();</div><div class="line">    encoder_queue_.PostTask(std::unique_ptr&lt;rtc::QueuedTask&gt;(new EncodeTask(</div><div class="line">      incoming_frame, this, rtc::TimeMicros(), log_stats)));</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p><code>EncodeTask</code> 是一个内部类，代码非常少，这里就直接贴上来了：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div></pre></td><td class="code"><pre><div class="line">class ViEEncoder::EncodeTask : public rtc::QueuedTask &#123;</div><div class="line"> public:</div><div class="line">  EncodeTask(const VideoFrame&amp; frame,</div><div class="line">             ViEEncoder* vie_encoder,</div><div class="line">             int64_t time_when_posted_us,</div><div class="line">             bool log_stats)</div><div class="line">      : frame_(frame),</div><div class="line">        vie_encoder_(vie_encoder),</div><div class="line">        time_when_posted_us_(time_when_posted_us),</div><div class="line">        log_stats_(log_stats) &#123;</div><div class="line">    ++vie_encoder_-&gt;posted_frames_waiting_for_encode_;</div><div class="line">  &#125;</div><div class="line"></div><div class="line"> private:</div><div class="line">  bool Run() override &#123;</div><div class="line">    RTC_DCHECK_RUN_ON(&amp;vie_encoder_-&gt;encoder_queue_);</div><div class="line">    RTC_DCHECK_GT(vie_encoder_-&gt;posted_frames_waiting_for_encode_.Value(), 0);</div><div class="line">    vie_encoder_-&gt;stats_proxy_-&gt;OnIncomingFrame(frame_.width(),</div><div class="line">                                                frame_.height());</div><div class="line">    ++vie_encoder_-&gt;captured_frame_count_;</div><div class="line">    if (--vie_encoder_-&gt;posted_frames_waiting_for_encode_ == 0) &#123;</div><div class="line">      vie_encoder_-&gt;EncodeVideoFrame(frame_, time_when_posted_us_);</div><div class="line">    &#125; else &#123;</div><div class="line">      // There is a newer frame in flight. Do not encode this frame.</div><div class="line">      LOG(LS_VERBOSE)</div><div class="line">          &lt;&lt; &quot;Incoming frame dropped due to that the encoder is blocked.&quot;;</div><div class="line">      ++vie_encoder_-&gt;dropped_frame_count_;</div><div class="line">    &#125;</div><div class="line">    if (log_stats_) &#123;</div><div class="line">      LOG(LS_INFO) &lt;&lt; &quot;Number of frames: captured &quot;</div><div class="line">                   &lt;&lt; vie_encoder_-&gt;captured_frame_count_</div><div class="line">                   &lt;&lt; &quot;, dropped (due to encoder blocked) &quot;</div><div class="line">                   &lt;&lt; vie_encoder_-&gt;dropped_frame_count_ &lt;&lt; &quot;, interval_ms &quot;</div><div class="line">                   &lt;&lt; kFrameLogIntervalMs;</div><div class="line">      vie_encoder_-&gt;captured_frame_count_ = 0;</div><div class="line">      vie_encoder_-&gt;dropped_frame_count_ = 0;</div><div class="line">    &#125;</div><div class="line">    return true;</div><div class="line">  &#125;</div><div class="line">  VideoFrame frame_;</div><div class="line">  ViEEncoder* const vie_encoder_;</div><div class="line">  const int64_t time_when_posted_us_;</div><div class="line">  const bool log_stats_;</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>第一段代码生成 <code>EncodeTask</code> 后直接调用 <code>PostTask</code> 方法将其放入队列中，该代码是在采集线程中执行的。在 <code>EncodeTask</code> 的构造方法中会对 <code>posted_frames_waiting_for_encode_</code> 属性执行一次自增操作，用来记录待编码的帧数。</p>
<p><code>EncodeTask</code>  的 <em><code>Run</code></em> 方法 是在 <code>rtc::TaskQueue</code> 的队列线程中执行的，方法中的条件判断可以理解为，当队列中只有当前一个待编码的帧时，才进行编码，否则丢弃当前帧并记录被丢弃帧的数量。由于是一个队列，采集的帧会被依次存入队列中，如果编码线程处理不过来时，编码线程将队列中的待编码的帧依次丢弃，直到最后一帧，然后调用 <em><code>EncodeVideoFrame</code></em> 方法进入下一步的编码处理。</p>
<h3 id="编码器码率过高时的丢帧策略"><a href="#编码器码率过高时的丢帧策略" class="headerlink" title="编码器码率过高时的丢帧策略"></a>编码器码率过高时的丢帧策略</h3><p>采集过快时，会在 <code>ViEEncoder</code> 类中处理，而输出码率过高时则是在 <code>VideoSender</code> 类中处理。数据会沿着 <em><code>EncodeVideoFrame</code></em> 方法，数据会经过 <code>VideoSender</code> 类的 <em><code>AddVideoFrame</code></em> 方法，在该方法中存在以下逻辑：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div></pre></td><td class="code"><pre><div class="line">// Add one raw video frame to the encoder, blocking.</div><div class="line">int32_t VideoSender::AddVideoFrame(const VideoFrame&amp; videoFrame,</div><div class="line">                                   const CodecSpecificInfo* codecSpecificInfo) &#123;</div><div class="line">  ...</div><div class="line">  </div><div class="line">  if (_mediaOpt.DropFrame()) &#123;</div><div class="line">     LOG(LS_VERBOSE) &lt;&lt; &quot;Drop Frame &quot;</div><div class="line">                     &lt;&lt; &quot;target bitrate &quot;</div><div class="line">                     &lt;&lt; encoder_params.target_bitrate.get_sum_bps()</div><div class="line">                     &lt;&lt; &quot; loss rate &quot; &lt;&lt; encoder_params.loss_rate &lt;&lt; &quot; rtt &quot;</div><div class="line">                     &lt;&lt; encoder_params.rtt &lt;&lt; &quot; input frame rate &quot;</div><div class="line">                     &lt;&lt; encoder_params.input_frame_rate;</div><div class="line">     post_encode_callback_-&gt;OnDroppedFrame();</div><div class="line">     return VCM_OK;</div><div class="line">  &#125;</div><div class="line">  </div><div class="line">  ...</div><div class="line">  </div><div class="line">  int32_t ret =</div><div class="line">      _encoder-&gt;Encode(converted_frame, codecSpecificInfo, next_frame_types);</div><div class="line">  if (ret &lt; 0) &#123;</div><div class="line">    LOG(LS_ERROR) &lt;&lt; &quot;Failed to encode frame. Error code: &quot; &lt;&lt; ret;</div><div class="line">    return ret;</div><div class="line">  &#125;</div></pre></td></tr></table></figure>
<p><em><code>DropFrame</code></em> 方法是 <code>MediaOptimization</code> 类的一个方法，用来判断是否丢弃当前帧。 <code>MediaOptimization</code> 类实际上并不直接处理该逻辑，而是交给关联的 <code>FrameDropper</code> 类对象进行处理。<code>MediaOptimization</code> 类主要任务是依赖于编码后的数据对象来计算和记录当前输出码率、帧率以及一些其他信息，这里介绍该类其他两个主要方法：</p>
<p> <em><code>void SetEncodingData(int32_t max_bit_rate, uint32_t bit_rate, uint16_t width, uint16_t height, uint32_t frame_rate, int num_temporal_layers, int32_t mtu)</code></em> </p>
<p>该方法用来初始一些配置参数，例如最大码率、帧率等，另一个方法就是：</p>
<p><em><code>int32_t UpdateWithEncodedData(const EncodedImage&amp; encoded_image);</code></em> </p>
<p>该方法用来接收编码后的 <code>EncodedImage</code> 对象，该对象包含了编码的数据，以及长度时间等信息。至于该方法什么时候会被调用，以及在哪里被调用，这里就不介绍了，因为涉及到编码类的一些结构，比较复杂，有时间单独写一篇专门介绍 WebRTC 的编解码模块的框架结构。</p>
<p><code>FrameDropper</code> 类的声明如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div></pre></td><td class="code"><pre><div class="line">// The Frame Dropper implements a variant of the leaky bucket algorithm</div><div class="line">// for keeping track of when to drop frames to avoid bit rate</div><div class="line">// over use when the encoder can&apos;t keep its bit rate.</div><div class="line">class FrameDropper &#123;</div><div class="line"> public:</div><div class="line">  FrameDropper();</div><div class="line">  explicit FrameDropper(float max_time_drops);</div><div class="line">  virtual ~FrameDropper() &#123;&#125;</div><div class="line"></div><div class="line">  // Resets the FrameDropper to its initial state.</div><div class="line">  // This means that the frameRateWeight is set to its</div><div class="line">  // default value as well.</div><div class="line">  virtual void Reset();</div><div class="line"></div><div class="line">  virtual void Enable(bool enable);</div><div class="line">  // Answers the question if it&apos;s time to drop a frame</div><div class="line">  // if we want to reach a given frame rate. Must be</div><div class="line">  // called for every frame.</div><div class="line">  //</div><div class="line">  // Return value     : True if we should drop the current frame</div><div class="line">  virtual bool DropFrame();</div><div class="line">  // Updates the FrameDropper with the size of the latest encoded</div><div class="line">  // frame. The FrameDropper calculates a new drop ratio (can be</div><div class="line">  // seen as the probability to drop a frame) and updates its</div><div class="line">  // internal statistics.</div><div class="line">  //</div><div class="line">  // Input:</div><div class="line">  //          - frameSizeBytes    : The size of the latest frame</div><div class="line">  //                                returned from the encoder.</div><div class="line">  //          - deltaFrame        : True if the encoder returned</div><div class="line">  //                                a key frame.</div><div class="line">  virtual void Fill(size_t frameSizeBytes, bool deltaFrame);</div><div class="line"></div><div class="line">  virtual void Leak(uint32_t inputFrameRate);</div><div class="line"></div><div class="line">  // Sets the target bit rate and the frame rate produced by</div><div class="line">  // the camera.</div><div class="line">  //</div><div class="line">  // Input:</div><div class="line">  //          - bitRate       : The target bit rate</div><div class="line">  virtual void SetRates(float bitRate, float incoming_frame_rate);</div><div class="line">  </div><div class="line">  ...</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>可以了解到  <code>FrameDropper</code> 类是一个漏斗算法的一种变体实现，当编码器无法保证比特率时， 用来跟踪何时应该丢弃帧。具体的算法实现代码我没有细看，有兴趣的同学可以看看，从方法的输入参数可以推测，计算模型跟时间、帧率、数据量以及是否为关键帧几个参数相关。可以理解为通过计算之前编码后的实际码率，来判断当前是否丢弃帧的。</p>
<p>从上面的分析可以了解到在 <em><code>VideoSender::AddVideoFrame</code></em> 方法中，间接的依赖了 <code>FrameDropper</code> 类的漏斗算法模型，来处理是否丢弃当前帧。从而确保输出码率不大于设定的值。</p>
<h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>当采集过快或者编码过慢都可能导致编码队列中存在很多待处理的帧，从而导致码率过高或者画面延时很大（可能还取决于编码器）。主动丢弃一些帧行，可以有效的控制码率和画面延时，代价就是损失帧率。在一些情况下编码器的输出码率并不能达到预先设定的值，从而导致码率过高增加了网络负担，第二种丢帧行为可以根据实际的码率来控制最终的码率，而非通过参数来调整。</p>
<p>可能有些人会有疑问，为什么采集就会过快呢？编码器可以通过参数设定码率，为何又不能按要求输的给定的码率呢？这里从两方面来表达我个人的看法。第一个问题可以从软件设计原则角度来解析，采集模块与编码模块可以说是两个相对独立的模块，两个模块分别独立工作，各自都不需要依赖对方的具体实现，知道的细节越少越好。简单的说就是依赖接口编程，而非具体实现。第二个问题也比较好解析，H264 的编码参数有很多例如帧率、码率、Qp、Profiles 级别等一些参数，并且一些参数之间相互影响，一些特定的编码器可能还存在一些特定的编码参数。在配置参数时可能存在一些不合理的情况。编解码器（暂且为H264）可以有两种实现方式，一种是硬编码，依赖于一些特点硬件设备；另一种是纯软件算法实现；软编码相对来说稳定，硬编解码在不同平台下存在差异，尤其是 Android 设备下，差异更大。</p>
]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;最近在弄一个 Android 平台下远程桌面项目，是基于 WebRTC 框架实现的，由于平台限制，芯片是用的特定厂商的芯片，桌面采集以及 H264 硬编码都是芯片厂商提供方案，能够有很好性能表现。在将桌面的采集以及 H264 的编码库整合到 WebRTC 框架中时，发现的存在画面延时的问题，于是对 WebRTC 的编解码框架的源码进行了分析，了解到 WebRTC 的两个特性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 WebRTC 中有很多可以控制视频帧率和码率的行为，这里介绍一下当视频采集过快以及编码器输出码率过高时，WebRTC 主动丢帧的策略。这里采集过快是指视频采集的速度大于编码器处理的速度。例如视频采集线程每秒采集 30 帧，而编码线程每秒只能处理 15 帧，这时候就出现了采集过快或者说是编码器太慢。编码器输出码率过高，是指编码器的输出码率大于设定的最高输出码率值，例如设置编码器码率为 1024kbps，而实际产生的码率是 1500kbps 这时候就出现了输出码率过高的情况。本文主要介绍上述两种情况下 WebRTC 是如何实现丢帧行为的。&lt;br&gt;
    
    </summary>
    
      <category term="音视频/WebRTC" scheme="http://enkichen.com/categories/%E9%9F%B3%E8%A7%86%E9%A2%91-WebRTC/"/>
    
    
      <category term="WebRTC" scheme="http://enkichen.com/tags/WebRTC/"/>
    
  </entry>
  
  <entry>
    <title>iperf 的介绍和使用</title>
    <link href="http://enkichen.com/2017/06/06/iperf-introduce/"/>
    <id>http://enkichen.com/2017/06/06/iperf-introduce/</id>
    <published>2017-06-06T12:31:58.000Z</published>
    <updated>2017-06-06T12:34:31.000Z</updated>
    
    <content type="html"><![CDATA[<p><code>iperf</code> 是一个网络性能测试工具，做服务开发或者测试的同学，接触的可能比较多。因为最近有用到这个工具，并且这个工具做的非常不错，这里记录一下工具的使用方法。<code>iperf</code> 是个开源并且跨平台的软件，代码托管在 <a href="https://github.com/esnet/iperf" target="_blank" rel="external">GitHub</a> 上，可以从 <a href="https://github.com/esnet/iperf/releases" target="_blank" rel="external">Releases</a> 找到各个发行版本，也可以去 <a href="https://iperf.fr/iperf-download.php" target="_blank" rel="external">官网</a> 下载各个平台的版本。 使用 <code>iperf</code> 时，需要分别运行服务端和客户端，在测试是最好保证两个端的软件版本一致，这样会免去一些没必要的麻烦。</p>
<p>下载好后，可以先在本机做一个简单的回环测试，结果如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div></pre></td><td class="code"><pre><div class="line"># 运行服务端</div><div class="line">$ ./iperf -s</div><div class="line">------------------------------------------------------------</div><div class="line">Server listening on TCP port 5001</div><div class="line">TCP window size:  128 KByte (default)</div><div class="line">------------------------------------------------------------</div><div class="line">[  4] local 127.0.0.1 port 5001 connected with 127.0.0.1 port 54817</div><div class="line">[ ID] Interval       Transfer     Bandwidth</div><div class="line">[  4]  0.0-10.0 sec  35.1 GBytes  30.1 Gbits/sec</div><div class="line"></div><div class="line"># 运行客户端</div><div class="line">$ ./iperf -c 127.0.0.1</div><div class="line">------------------------------------------------------------</div><div class="line">Client connecting to 127.0.0.1, TCP port 5001</div><div class="line">TCP window size:  144 KByte (default)</div><div class="line">------------------------------------------------------------</div><div class="line">[  4] local 127.0.0.1 port 54817 connected with 127.0.0.1 port 5001</div><div class="line">[ ID] Interval       Transfer     Bandwidth</div><div class="line">[  4]  0.0-10.0 sec  35.1 GBytes  30.1 Gbits/sec</div></pre></td></tr></table></figure>
<a id="more"></a>
<p>默认情况下，会使用 TCP 连接，绑定在 5001 端口上，可以从上述结果看到，当前本机的带宽为 <code>30.1 Gbits/sec</code> 。</p>
<h3 id="主要参数信息"><a href="#主要参数信息" class="headerlink" title="主要参数信息"></a>主要参数信息</h3><p><strong>适用于 服务端/客户端</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line">-f 指定数据显示格式 [k|m|K|M] 分别表示 Kbits、Mbits、KBytes、MBytes，默认是 Mbits</div><div class="line">-l 读写缓冲区的大小，默认是 8K</div><div class="line">-u 使用 udp 协议</div><div class="line">-i 以秒为单位统计带宽值</div><div class="line">-m 显示最大的 TCP 数据段大小</div><div class="line">-p 指定服务端或者客户端的端口号</div><div class="line">-w 指定 TCP 窗口大小</div><div class="line">-B 绑定道指定的主机地址或接口</div><div class="line">-C 兼容旧版本</div><div class="line">-M 设置 TCP 数据包的最大 MTU 值</div><div class="line">-V 传输 IPV6 包</div></pre></td></tr></table></figure>
<p><strong>适用于 服务端</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">-s 以服务器模式启动</div><div class="line">-U 单线程 UDP 模式</div><div class="line">-D 以守护进程模式运行</div></pre></td></tr></table></figure>
<p><strong>适用于 客服端</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div></pre></td><td class="code"><pre><div class="line">-c 以客户端模式运行，并指定服务端的地址</div><div class="line">-b 指定客户端通过 UDP 协议发送信息的带宽，默认为 1Mbit/s</div><div class="line">-d 同时进行双向传输测试</div><div class="line">-n 指定传输的字节数</div><div class="line">-r 单独进行双向传输测试</div><div class="line">-t 指定 iperf 测试的时间，默认 10s</div><div class="line">-F 指定要传输的文件</div><div class="line">-L 指定一个端口，服务利用这端口与客户端连接</div><div class="line">-P 指定客户端到服务器的连接数，默认是 1</div><div class="line">-T 指定 ttl 值</div></pre></td></tr></table></figure>
<blockquote>
<ul>
<li>用 -u 参数来指定使用 UDP 协议，需要在 -p 参数之前指定</li>
<li>测试之前确保防火墙为关闭状态</li>
</ul>
</blockquote>
<h3 id="网络性能测试"><a href="#网络性能测试" class="headerlink" title="网络性能测试"></a>网络性能测试</h3><p><strong>TCP 协议测试带宽</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div></pre></td><td class="code"><pre><div class="line"># 运行服务端</div><div class="line">$ iperf -s</div><div class="line"></div><div class="line"># 运行客户端</div><div class="line">$ iperf -c 172.18.142.62 -i 1 -t 10</div><div class="line">------------------------------------------------------------</div><div class="line">Client connecting to 172.18.142.62, TCP port 5001</div><div class="line">TCP window size:  129 KByte (default)</div><div class="line">------------------------------------------------------------</div><div class="line">[  4] local 172.18.98.209 port 57809 connected with 172.18.142.62 port 28756</div><div class="line">[ ID] Interval       Transfer     Bandwidth</div><div class="line">[  4]  0.0- 1.0 sec   384 KBytes  3.15 Mbits/sec</div><div class="line">[  4]  1.0- 2.0 sec   256 KBytes  2.10 Mbits/sec</div><div class="line">[  4]  2.0- 3.0 sec   256 KBytes  2.10 Mbits/sec</div><div class="line">[  4]  3.0- 4.0 sec   256 KBytes  2.10 Mbits/sec</div><div class="line">[  4]  4.0- 5.0 sec   512 KBytes  4.19 Mbits/sec</div><div class="line">[  4]  5.0- 6.0 sec  1.12 MBytes  9.44 Mbits/sec</div><div class="line">[  4]  6.0- 7.0 sec  1.12 MBytes  9.44 Mbits/sec</div><div class="line">[  4]  7.0- 8.0 sec  1.12 MBytes  9.44 Mbits/sec</div><div class="line">[  4]  8.0- 9.0 sec  1.25 MBytes  10.5 Mbits/sec</div><div class="line">[  4]  9.0-10.0 sec  1.12 MBytes  9.44 Mbits/sec</div><div class="line">[  4]  0.0-10.1 sec  7.50 MBytes  6.25 Mbits/sec</div></pre></td></tr></table></figure>
<p>使用 TCP 协议进行测试时，需要注意的就是 TCP 窗口大小，可以使用 <code>-w</code> 参数指定，网络通道的容量 <code>capacity = bandwidth * round-trip time</code>，而理论 TCP 窗口大小就是网络通道的容量。例如，网络带宽为 <code>40Mbit/s</code>，回环路径消耗时间是 2ms，那么 TCP 的窗口大小不小于 <code>40Mbit/s×2ms = 80kbit = 10Kbytes</code> 。</p>
<p><strong>UDP 协议测试带宽</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div></pre></td><td class="code"><pre><div class="line"># 运行服务端</div><div class="line">$ iperf -u -s</div><div class="line"></div><div class="line"># 运行客户端</div><div class="line">$ iperf -c 172.18.142.62 -u -i 1 -t 10 -b 30M</div><div class="line">------------------------------------------------------------</div><div class="line">Client connecting to 172.18.142.62, UDP port 5001</div><div class="line">Sending 1470 byte datagrams</div><div class="line">UDP buffer size: 9.00 KByte (default)</div><div class="line">------------------------------------------------------------</div><div class="line">[  4] local 172.18.98.209 port 53220 connected with 172.18.142.62 port 28756</div><div class="line">[ ID] Interval       Transfer     Bandwidth</div><div class="line">[  4]  0.0- 1.0 sec  3.58 MBytes  30.0 Mbits/sec</div><div class="line">[  4]  1.0- 2.0 sec  3.58 MBytes  30.0 Mbits/sec</div><div class="line">[  4]  2.0- 3.0 sec  3.58 MBytes  30.0 Mbits/sec</div><div class="line">[  4]  3.0- 4.0 sec  3.58 MBytes  30.0 Mbits/sec</div><div class="line">[  4]  4.0- 5.0 sec  3.58 MBytes  30.0 Mbits/sec</div><div class="line">[  4]  5.0- 6.0 sec  3.57 MBytes  30.0 Mbits/sec</div><div class="line">[  4]  6.0- 7.0 sec  3.58 MBytes  30.0 Mbits/sec</div><div class="line">[  4]  7.0- 8.0 sec  3.58 MBytes  30.0 Mbits/sec</div><div class="line">[  4]  8.0- 9.0 sec  3.58 MBytes  30.0 Mbits/sec</div><div class="line">[  4]  9.0-10.0 sec  3.58 MBytes  30.0 Mbits/sec</div><div class="line">[  4]  0.0-10.0 sec  35.8 MBytes  30.0 Mbits/sec</div><div class="line">[  4] Sent 25511 datagrams</div><div class="line">[  4] Server Report:</div><div class="line">[  4]  0.0-11.6 sec  13.6 MBytes  9.83 Mbits/sec   1.971 ms 15786/25497 (62%)</div><div class="line">[  4]  0.0-11.6 sec  140 datagrams received out-of-order</div></pre></td></tr></table></figure>
<p>上述命令指定了客户端以 <code>30Mbit/s</code> 速度发送数据，由于 UDP 协议是无连接不可靠的，并且只管发包，不确保包在服务端是否接收到，所以需要查看服务报告才能确定当前网络性能数据。如果在不知道当前网络带宽的情况下，需要不断的调整参数值，并且查看丢包率，来确定当前网络性能情况。如果你当前是远程登录到服务器上进行测试的，可以从小到大的方式进行测试，否则很容易导致服务当前带宽被占满。</p>
<h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul>
<li><a href="http://blog.csdn.net/evenness/article/details/7371845" target="_blank" rel="external">使用 iperf 测试网络性能</a></li>
<li><a href="http://www.52os.net/articles/iperf-check-bandwidth.html" target="_blank" rel="external">iperf 测试带宽</a></li>
</ul>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;&lt;code&gt;iperf&lt;/code&gt; 是一个网络性能测试工具，做服务开发或者测试的同学，接触的可能比较多。因为最近有用到这个工具，并且这个工具做的非常不错，这里记录一下工具的使用方法。&lt;code&gt;iperf&lt;/code&gt; 是个开源并且跨平台的软件，代码托管在 &lt;a href=&quot;https://github.com/esnet/iperf&quot;&gt;GitHub&lt;/a&gt; 上，可以从 &lt;a href=&quot;https://github.com/esnet/iperf/releases&quot;&gt;Releases&lt;/a&gt; 找到各个发行版本，也可以去 &lt;a href=&quot;https://iperf.fr/iperf-download.php&quot;&gt;官网&lt;/a&gt; 下载各个平台的版本。 使用 &lt;code&gt;iperf&lt;/code&gt; 时，需要分别运行服务端和客户端，在测试是最好保证两个端的软件版本一致，这样会免去一些没必要的麻烦。&lt;/p&gt;
&lt;p&gt;下载好后，可以先在本机做一个简单的回环测试，结果如下：&lt;/p&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;1&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;2&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;3&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;4&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;5&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;6&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;7&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;8&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;9&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;10&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;11&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;12&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;13&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;14&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;15&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;16&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;17&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;18&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;19&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;# 运行服务端&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;$ ./iperf -s&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;------------------------------------------------------------&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;Server listening on TCP port 5001&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;TCP window size:  128 KByte (default)&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;------------------------------------------------------------&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;[  4] local 127.0.0.1 port 5001 connected with 127.0.0.1 port 54817&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;[ ID] Interval       Transfer     Bandwidth&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;[  4]  0.0-10.0 sec  35.1 GBytes  30.1 Gbits/sec&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;# 运行客户端&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;$ ./iperf -c 127.0.0.1&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;------------------------------------------------------------&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;Client connecting to 127.0.0.1, TCP port 5001&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;TCP window size:  144 KByte (default)&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;------------------------------------------------------------&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;[  4] local 127.0.0.1 port 54817 connected with 127.0.0.1 port 5001&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;[ ID] Interval       Transfer     Bandwidth&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;[  4]  0.0-10.0 sec  35.1 GBytes  30.1 Gbits/sec&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
    
    </summary>
    
      <category term="知识整理/总结" scheme="http://enkichen.com/categories/%E7%9F%A5%E8%AF%86%E6%95%B4%E7%90%86-%E6%80%BB%E7%BB%93/"/>
    
    
      <category term="iperf" scheme="http://enkichen.com/tags/iperf/"/>
    
  </entry>
  
  <entry>
    <title>Dnsmasq 介绍与使用</title>
    <link href="http://enkichen.com/2017/05/23/dnsmasq-introduce/"/>
    <id>http://enkichen.com/2017/05/23/dnsmasq-introduce/</id>
    <published>2017-05-23T06:06:00.000Z</published>
    <updated>2017-05-24T06:21:38.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote>
<p>上一个星期在查找一个设备无法解析公司内部域名的问题，最终查出来问题是在网关设备上运行的 <code>Dnsmasq</code> 服务没有正确的配置上游 DNS 服务器，导致内网域名无法被正常的解析。在这期间对 <code>DNS协议</code> 又重新学习了一番，并且对 <code>Dnsmasq</code> 服务有了一些了解，结合网上一些资料，对 <code>Dnsmasq</code> 提供的 DNS 和 DHCP 服务的配置做一些总结和备忘。</p>
</blockquote>
<p><code>Dnsmasq</code>  是一个开源的项目，可以在 <a href="http://www.thekelleys.org.uk/dnsmasq/doc.html" target="_blank" rel="external">thekelleys</a> 上找到最新版本和源码，它能提供 DNS 、DHCP、TFTP、PXE 等功能。<code>Dnsmasq</code> 的 DNS 服务工作原理是，当接收到一个 DNS 请求是， <code>Dnsmasq</code> 首先会查找 <code>/etc/hosts</code> 文件，如果没有查找到，会查询本地 DNS 缓存记录，如果还是未找到对应的记录，则会将请求装发到 <code>/etc/resolv.conf</code> 文件中定义的上游 DNS 服务器中，从而实现对域名的解析。</p>
<p>基于上述原理，我们可以在 <code>/etc/hosts</code> 文件中添加本地内网的域名解析，从而实现本地内网的域名解析。同时我们还可以使用 <code>Dnsmasq</code> 来为一些特定的域名指定 DNS 服务器，或者阻止某些域名的访问。由于 <code>Dnsmasq</code> 会缓存上游 DNS 服务的查询记录，从而可以提高访问过的网址的连接速度。</p>
<p>默认情况下，<code>Dnsmasq</code> 会从 <code>/etc/dnsmasq.conf</code> 读取配置项，我们也可以使用 <code>-C</code> 的启动参数来指定配置文件。下面介绍一下常用的 DNS 和 DHCP 服务的配置参数。</p>
<a id="more"></a>
<h3 id="通用配置项"><a href="#通用配置项" class="headerlink" title="通用配置项"></a>通用配置项</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div></pre></td><td class="code"><pre><div class="line"># 服务运行的网卡，如果有多个话，可在再次添加一条记录</div><div class="line">interface=eth1</div><div class="line">interface=wlan0</div><div class="line"></div><div class="line"># 指定服务不在以下网卡上运行</div><div class="line">except-interface=eth0</div><div class="line"></div><div class="line"># 指定监听的 IP 地址，多个 IP 地址可用 `,` 分割(默认是监听所有网卡)</div><div class="line">listen-address=192.168.8.132</div><div class="line"></div><div class="line"># 开启日志选项，记录在 /var/log/debug 中</div><div class="line">log-queries</div><div class="line">  </div><div class="line"># 指定日志文件的路径，路径必须存在，否则会导致服务启动失败</div><div class="line">log-facility=/var/log/dnsmasq.log</div><div class="line"> </div><div class="line"># 异步log，缓解阻塞。</div><div class="line">log-async=20</div></pre></td></tr></table></figure>
<h3 id="DNS-服务配置参数"><a href="#DNS-服务配置参数" class="headerlink" title="DNS 服务配置参数"></a>DNS 服务配置参数</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div></pre></td><td class="code"><pre><div class="line"># 指定 DNS 服务的端口（默认53），设置为 0 表示关闭 DNS 服务，只使用 DHCP 服务</div><div class="line">port=53</div><div class="line"></div><div class="line"># 指定一个 hosts 文件，默认是从 /etc/hosts 中获取</div><div class="line">addn-hosts=/etc/banner_add_hosts</div><div class="line"></div><div class="line"># 表示不使用 /etc/hosts 配置文件来解析域名</div><div class="line">no-hosts</div><div class="line"></div><div class="line"># 指定上游 DNS 服务列表的配置文件，默认是从  /etc/resolv.conf 中获取</div><div class="line">resolv-file=/etc/dnsmasq.d/upstream_dns.conf</div><div class="line"></div><div class="line"># 表示严格按照上游 DNS 服务列表一个一个查询，否则将请求发送到所有 DNS 服务器，使用响应最快的服务器的结果</div><div class="line">strict-order</div><div class="line"></div><div class="line"># 不使用上游 DNS 服务器的配置文件 /etc/resolv.conf 或者 resolv-file 选项</div><div class="line">no-resolv</div><div class="line"></div><div class="line"># 不允许 Dnsmasq 通过轮询 /etc/resolv.conf 或者其他文件来动态更新上游 DNS 服务列表</div><div class="line">no-poll</div><div class="line"></div><div class="line"># 表示对所有 server 发起查询请求，选择响应最快的服务器的结果</div><div class="line">all-servers</div><div class="line"></div><div class="line"># 指定 dnsmasq 默认查询的上游服务器</div><div class="line">server=8.8.8.8</div><div class="line">server=114.114.114.114</div><div class="line"></div><div class="line"># 指定 .cn 的域名全部通过 114.114.114.114 这台国内DNS服务器来解析</div><div class="line">server=/cn/114.114.114.114</div><div class="line"></div><div class="line"># 给 *.apple.com 和 taobao.com 使用专用的 DNS</div><div class="line">server=/taobao.com/223.5.5.5</div><div class="line">server=/.apple.com/223.6.6.6</div><div class="line"></div><div class="line"># 增加一个域名，强制解析到所指定的地址上，dns 欺骗</div><div class="line">address=/taobao.com/127.0.0.1</div><div class="line"></div><div class="line"># 设置DNS缓存大小(单位：DNS解析条数)</div><div class="line">cache-size=500</div></pre></td></tr></table></figure>
<p><code>/etc/resolv.conf</code> 文件样例</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">nameserver 114.114.114.114</div><div class="line">nameserver 8.8.8.8</div></pre></td></tr></table></figure>
<p><code>/etc/hosts</code> 文件样例</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">127.0.0.1         localhost </div><div class="line">192.168.101.107   web.gz.cvte.com web01</div><div class="line">192.168.101.103   hrm.gz.cvte.com web02</div></pre></td></tr></table></figure>
<h3 id="DHCP-服务配置参数"><a href="#DHCP-服务配置参数" class="headerlink" title="DHCP 服务配置参数"></a>DHCP 服务配置参数</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div></pre></td><td class="code"><pre><div class="line"># 指定分配的 IP 端和续约时间</div><div class="line">dhcp-range=192.168.1.50,192.168.1.100,12h</div><div class="line"></div><div class="line"># 同上，指定了子网掩码</div><div class="line">dhcp-range=192.168.8.50,192.168.8.150,255.255.255.0,12h</div><div class="line"></div><div class="line"># 指定网关地址</div><div class="line">dhcp-option=3,192.168.0.1</div><div class="line"></div><div class="line"># 指定 DNS 服务器，net:eth1 用来指定网卡</div><div class="line">dhcp-option=net:eth1,6,114.114.114.114，8.8.8.8</div><div class="line">dhcp-option=net:wlano,6,114.114.114.114，8.8.8.8</div><div class="line"></div><div class="line"># DHCP 所在的 domain</div><div class="line">domain=gz.cvte.com</div><div class="line"></div><div class="line"># 静态地址绑定</div><div class="line">dhcp-host=00:0C:29:5E:F2:6F,192.168.1.201,os02</div><div class="line">dhcp-host=00:0C:29:15:63:CF,192.168.1.202,os03</div><div class="line"></div><div class="line"># 忽略一下 MAC 地址主机的请求</div><div class="line">dhcp-host=11:22:33:44:55:66,ignore</div><div class="line"></div><div class="line"># 租期保存文件</div><div class="line">dhcp-leasefile=/var/lib/dnsmasq/dnsmasq.leases</div></pre></td></tr></table></figure>
<p>dhcp-option 常用取值及含义</p>
<table>
<thead>
<tr>
<th style="text-align:center">option</th>
<th style="text-align:center">option 作用</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">1</td>
<td style="text-align:center">设置子网掩码选项</td>
</tr>
<tr>
<td style="text-align:center">3</td>
<td style="text-align:center">设置网关地址选项</td>
</tr>
<tr>
<td style="text-align:center">6</td>
<td style="text-align:center">设置DNS服务器地址选项</td>
</tr>
<tr>
<td style="text-align:center">12</td>
<td style="text-align:center">设置域名选项</td>
</tr>
<tr>
<td style="text-align:center">15</td>
<td style="text-align:center">设置域名后缀选项</td>
</tr>
<tr>
<td style="text-align:center">33</td>
<td style="text-align:center">设置静态路由选项。该选项中包含一组有分类静态路由（即目的地址的掩码固定为自然掩码，不能划分子网），客户端收到该选项后，将在路由表中添加这些静态路由。如果存在Option121，则忽略该选项</td>
</tr>
<tr>
<td style="text-align:center">44</td>
<td style="text-align:center">设置NetBios服务器选项</td>
</tr>
<tr>
<td style="text-align:center">46</td>
<td style="text-align:center">设置NetBios节点类型选项</td>
</tr>
<tr>
<td style="text-align:center">50</td>
<td style="text-align:center">设置请求IP选项</td>
</tr>
<tr>
<td style="text-align:center">51</td>
<td style="text-align:center">设置IP地址租约时间选项</td>
</tr>
<tr>
<td style="text-align:center">52</td>
<td style="text-align:center">设置Option附加选项</td>
</tr>
<tr>
<td style="text-align:center">53</td>
<td style="text-align:center">设置DHCP消息类型</td>
</tr>
<tr>
<td style="text-align:center">54</td>
<td style="text-align:center">设置服务器标识</td>
</tr>
<tr>
<td style="text-align:center">55</td>
<td style="text-align:center">设置请求参数列表选项。客户端利用该选项指明需要从服务器获取哪些网络配置参数。该选项内容为客户端请求的参数对应的选项值</td>
</tr>
<tr>
<td style="text-align:center">58</td>
<td style="text-align:center">设置续约T1时间，一般是租期时间的50%</td>
</tr>
<tr>
<td style="text-align:center">59</td>
<td style="text-align:center">设置续约T2时间。一般是租期时间的87.5%</td>
</tr>
<tr>
<td style="text-align:center">60</td>
<td style="text-align:center">设置厂商分类信息选项，用于标识DHCP客户端的类型和配置</td>
</tr>
<tr>
<td style="text-align:center">61</td>
<td style="text-align:center">设置客户端标识选项</td>
</tr>
<tr>
<td style="text-align:center">66</td>
<td style="text-align:center">设置TFTP服务器名选项，用来指定为客户端分配的TFTP服务器的域名</td>
</tr>
<tr>
<td style="text-align:center">67</td>
<td style="text-align:center">设置启动文件名选项，用来指定为客户端分配的启动文件名</td>
</tr>
<tr>
<td style="text-align:center">77</td>
<td style="text-align:center">设置用户类型标识</td>
</tr>
<tr>
<td style="text-align:center">121</td>
<td style="text-align:center">设置无分类路由选项。该选项中包含一组无分类静态路由（即目的地址的掩码为任意值，可以通过掩码来划分子网），客户端收到该选项后，将在路由表中添加这些静态路由</td>
</tr>
<tr>
<td style="text-align:center">148</td>
<td style="text-align:center">EasyDeploy中Commander的IP地址</td>
</tr>
<tr>
<td style="text-align:center">149</td>
<td style="text-align:center">SFTP和FTPS服务器的IP地址</td>
</tr>
<tr>
<td style="text-align:center">150</td>
<td style="text-align:center">设置TFTP服务器地址选项，指定为客户端分配的TFTP服务器的地址</td>
</tr>
</tbody>
</table>
<blockquote>
<p>dhcp-option 遵循RFC 2132（Options and BOOTP Vendor Extensions)，可以通过 dnsmasq –help dhcp 来查看具体的配置很多高级的配置，如 iSCSI 连接配置等同样可以由 RFC 2132 定义的 dhcp-option 中给出。</p>
</blockquote>
<h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul>
<li><a href="http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html" target="_blank" rel="external">thekelleys 官网</a></li>
<li><a href="http://www.freeoa.net/osuport/servap/dnsmasq-use-intro-refer_2480.html" target="_blank" rel="external">Dnsmasq使用参考入门</a></li>
<li><a href="https://wiki.archlinux.org/index.php/Dnsmasq_%28%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87%29#DHCP_.E6.9C.8D.E5.8A.A1.E5.99.A8.E8.AE.BE.E7.BD.AE" target="_blank" rel="external">dnsmasq (简体中文)</a></li>
</ul>
]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;上一个星期在查找一个设备无法解析公司内部域名的问题，最终查出来问题是在网关设备上运行的 &lt;code&gt;Dnsmasq&lt;/code&gt; 服务没有正确的配置上游 DNS 服务器，导致内网域名无法被正常的解析。在这期间对 &lt;code&gt;DNS协议&lt;/code&gt; 又重新学习了一番，并且对 &lt;code&gt;Dnsmasq&lt;/code&gt; 服务有了一些了解，结合网上一些资料，对 &lt;code&gt;Dnsmasq&lt;/code&gt; 提供的 DNS 和 DHCP 服务的配置做一些总结和备忘。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;Dnsmasq&lt;/code&gt;  是一个开源的项目，可以在 &lt;a href=&quot;http://www.thekelleys.org.uk/dnsmasq/doc.html&quot;&gt;thekelleys&lt;/a&gt; 上找到最新版本和源码，它能提供 DNS 、DHCP、TFTP、PXE 等功能。&lt;code&gt;Dnsmasq&lt;/code&gt; 的 DNS 服务工作原理是，当接收到一个 DNS 请求是， &lt;code&gt;Dnsmasq&lt;/code&gt; 首先会查找 &lt;code&gt;/etc/hosts&lt;/code&gt; 文件，如果没有查找到，会查询本地 DNS 缓存记录，如果还是未找到对应的记录，则会将请求装发到 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 文件中定义的上游 DNS 服务器中，从而实现对域名的解析。&lt;/p&gt;
&lt;p&gt;基于上述原理，我们可以在 &lt;code&gt;/etc/hosts&lt;/code&gt; 文件中添加本地内网的域名解析，从而实现本地内网的域名解析。同时我们还可以使用 &lt;code&gt;Dnsmasq&lt;/code&gt; 来为一些特定的域名指定 DNS 服务器，或者阻止某些域名的访问。由于 &lt;code&gt;Dnsmasq&lt;/code&gt; 会缓存上游 DNS 服务的查询记录，从而可以提高访问过的网址的连接速度。&lt;/p&gt;
&lt;p&gt;默认情况下，&lt;code&gt;Dnsmasq&lt;/code&gt; 会从 &lt;code&gt;/etc/dnsmasq.conf&lt;/code&gt; 读取配置项，我们也可以使用 &lt;code&gt;-C&lt;/code&gt; 的启动参数来指定配置文件。下面介绍一下常用的 DNS 和 DHCP 服务的配置参数。&lt;/p&gt;
    
    </summary>
    
      <category term="知识整理/总结" scheme="http://enkichen.com/categories/%E7%9F%A5%E8%AF%86%E6%95%B4%E7%90%86-%E6%80%BB%E7%BB%93/"/>
    
    
      <category term="Dnsmasq" scheme="http://enkichen.com/tags/Dnsmasq/"/>
    
  </entry>
  
  <entry>
    <title>WebRTC iOS&amp;OSX 库的编译</title>
    <link href="http://enkichen.com/2017/05/12/webrtc-ios-build/"/>
    <id>http://enkichen.com/2017/05/12/webrtc-ios-build/</id>
    <published>2017-05-12T05:04:36.000Z</published>
    <updated>2017-05-12T05:16:39.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="安装-Depot-tools"><a href="#安装-Depot-tools" class="headerlink" title="安装 Depot_tools"></a>安装 Depot_tools</h3><ul>
<li>git 命令获取 depot_tools：</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git</div></pre></td></tr></table></figure>
<ul>
<li>配置坏境变量</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">$ echo &quot;export PATH=$PWD/depot_tools:$PATH&quot; &gt; $HOME/.bash_profile</div><div class="line">$ source $HOME/.bash_profile</div></pre></td></tr></table></figure>
<ul>
<li>检测配置是否成功</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ echo $PATH</div></pre></td></tr></table></figure>
<h3 id="安装-ninja"><a href="#安装-ninja" class="headerlink" title="安装 ninja"></a>安装 ninja</h3><a id="more"></a>
<p><strong>ninja</strong> 是 <strong>WebRTC</strong> 的编译工具，我们需要对其进行编译，步骤如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">$ git clone git://github.com/martine/ninja.git</div><div class="line">$ cd ninja/</div><div class="line">$ ./bootstrap.py</div></pre></td></tr></table></figure>
<p>复制到系统目录（也可配置坏境变量）</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">$ sudo cp ninja /usr/local/bin/</div><div class="line">$ sudo chmod a+rx /usr/local/bin/ninja</div></pre></td></tr></table></figure>
<h3 id="下载源代码"><a href="#下载源代码" class="headerlink" title="下载源代码"></a>下载源代码</h3><p><strong>WebRTC</strong> 源码托管在 <a href="https://chromium.googlesource.com/external/webrtc" target="_blank" rel="external"><strong>Google Source</strong></a> ，在 <a href="https://webrtc.org/release-notes/" target="_blank" rel="external"><strong>Release Notes</strong></a> 中选择需要的版本，这里选择最新的 <strong>M57</strong> 版本</p>
<ul>
<li>设置要编译的平台到环境变量中：</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ export GYP_DEFINES=&quot;OS=ios&quot;</div></pre></td></tr></table></figure>
<p>不同机型的编译参数：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div></pre></td><td class="code"><pre><div class="line"># 32位真机</div><div class="line">$ export GYP_DEFINES=&quot;OS=ios target_arch=arm&quot;</div><div class="line"># 64位真机</div><div class="line">$ export GYP_DEFINES=&quot;OS=ios target_arch=arm64&quot;</div><div class="line"># 32位模拟器</div><div class="line">$ export GYP_DEFINES=&quot;OS=ios target_arch=ia32&quot;</div><div class="line"># 64位模拟器</div><div class="line">$ export GYP_DEFINES=&quot;OS=ios target_arch=x64&quot;</div><div class="line"># OSX</div><div class="line">$ export GYP_DEFINES=&quot;OS=mac target_arch=x64&quot;</div><div class="line"></div><div class="line"># 配置输出路径</div><div class="line">$ export GYP_GENERATOR_FLAGS=&quot;output_dir=out_xxx&quot;</div></pre></td></tr></table></figure>
<ul>
<li>创建工作路径并执行下面的语句： </li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">$ fetch --nohooks webrtc_ios</div><div class="line">$ gclient sync -r 52b6562a10b495cf771d8388ee51990d56074059 --force</div></pre></td></tr></table></figure>
<blockquote>
<p>上面的 commit id 是 <strong>M57</strong> 版本最后一次 commit id，可以从 <a href="https://webrtc.org/release-notes/" target="_blank" rel="external"><strong>Release Notes</strong></a> 中找到，可替换成自己所需的版本的 commit id 或者直接使用最新的 commit id</p>
</blockquote>
<p>执行上述命令就会去下载对应的 <strong>WebRTC</strong> 的源码、构建工具链以及依赖的第三方库，由于是国外网站，并且是 <strong>Google</strong>，请自备梯子，我这边翻墙后大概 2 个小时就下完了源码。</p>
<h3 id="编译库文件"><a href="#编译库文件" class="headerlink" title="编译库文件"></a>编译库文件</h3><h4 id="iOS-版本的编译"><a href="#iOS-版本的编译" class="headerlink" title="iOS 版本的编译"></a>iOS 版本的编译</h4><p><strong>ninja</strong> 是 <strong>WebRTC</strong> 的编译平台，iOS 版本我们可以使用自带的编译脚本，这样就不需要自己编译和安装 <strong>ninja</strong>，默认情况行，脚本会编译 3 个平台机型的库文件，以及一个各个平台的集合库，脚本也可以指定编译成 <code>.a</code> 的库文件或者 <code>.framework</code>，命令如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">$ cd src/webrtc/build/ios/</div><div class="line">$ ./build_ios_libs.sh</div></pre></td></tr></table></figure>
<p>不同的版本，编译脚本的路径会有不一样。编译完成后，如果没有指定输出路径，则会在  <em><code>out_ios_libs</code></em> 目录下生成所需要的 <strong>WebRTC.framework</strong>，子目录中会有对应平台单独的 <strong>WebRTC.framework</strong>，根目录下的则支持所有平台，目录结构如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">drwxr-xr-x   3 Enki  staff   102  5  9 16:49 WebRTC.dSYM/		[符号表文件]</div><div class="line">drwxr-xr-x   6 Enki  staff   204  5  9 16:49 WebRTC.framework/	[支持所有平台]</div><div class="line">drwx------  14 Enki  staff   476  5  9 16:39 arm64_libs/		[真机 64 位]</div><div class="line">drwx------  14 Enki  staff   476  5  9 16:30 arm_libs/			[真机 32 位]</div><div class="line">drwx------  14 Enki  staff   476  5  9 16:49 x64_libs/			[模拟器 64 位]</div></pre></td></tr></table></figure>
<h4 id="Mac-版本的编译"><a href="#Mac-版本的编译" class="headerlink" title="Mac 版本的编译"></a>Mac 版本的编译</h4><p>下载源码：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">$ export GYP_DEFINES=&quot;OS=mac&quot;</div><div class="line">$ fetch --nohooks webrtc_ios</div><div class="line">$ gclient sync -r 52b6562a10b495cf771d8388ee51990d56074059 --force</div></pre></td></tr></table></figure>
<p>Mac 没有现成的编译脚本，我们只能通过 <code>gn</code> 来生成对应的 ninja 编译脚本，然后通过 ninja 脚本来编译，用如下命令来生成对应的 ninja 项目文件：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ gn gen out/mac_x64 --args=&apos;target_os=&quot;mac&quot; target_cpu=&quot;x64&quot; is_component_build=false&apos;</div></pre></td></tr></table></figure>
<blockquote>
<p>可以添加 –ide=xcode 参数来生成 Xcode 的项目文件，使用 Xcode 来进行编译。</p>
</blockquote>
<p>编译源码：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ ninja -C out/mac_x64 rtc_sdk_objc</div></pre></td></tr></table></figure>
<p>上述命令 <code>out/mac_x64</code> 则是 ninja 项目文件的路径，<code>rtc_sdk_objc</code> 则是要编译的目标，可以通过以下命令来查看目标列表：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ gn ls out/mac_x64</div></pre></td></tr></table></figure>
<h3 id="其他平台源码下载"><a href="#其他平台源码下载" class="headerlink" title="其他平台源码下载"></a>其他平台源码下载</h3><p><strong>Windows</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">$ fetch --nohooks webrtc</div><div class="line">$ gclient sync</div></pre></td></tr></table></figure>
<p><strong>Linux</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">$ export GYP_DEFINES=&quot;OS=linux&quot;</div><div class="line">$ fetch --nohooks webrtc_android</div><div class="line">$ gclient sync</div></pre></td></tr></table></figure>
<p>Windows 和 Linux 都使用 <code>./build/install-build-deps.sh</code> 脚本进行编译即可。</p>
<p><strong>Android</strong></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">$ export GYP_DEFINES=&quot;OS=android&quot;</div><div class="line">$ fetch --nohooks webrtc_android</div><div class="line">$ gclient sync</div></pre></td></tr></table></figure>
<p>Android 使用 <code>./build/install-build-deps-android.sh</code> 脚本进行编译。</p>
<h3 id="生成-Example-并运行"><a href="#生成-Example-并运行" class="headerlink" title="生成 Example 并运行"></a>生成 Example 并运行</h3><p>执行以下命令，用于生成 ninja 的编译脚本 ：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ gn gen out/mac_x64 --args=&apos;target_os=&quot;mac&quot; target_cpu=&quot;x64&quot; is_component_build=false&apos;</div></pre></td></tr></table></figure>
<p>用以下命令可用来编译并生成可执行的二进制文件：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ ninja -C out/mac_x64 AppRTCMobile</div></pre></td></tr></table></figure>
<p>执行之后可在 <em><code>out/mac_x64</code></em> 目录生成可执行的 <code>AppRTCMobile</code> 文件，双击即可运行。效果如下图：</p>
<p><img src="/uploads/AppRTCMobile.png" alt="Example"></p>
<p>通过修改 <code>--args=&#39;target_os=&quot;mac&quot; target_cpu=&quot;x64&quot;</code> 参数来生成其他平台的 Example。</p>
<h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul>
<li><a href="https://webrtc.org" target="_blank" rel="external">WebRTC 官网</a></li>
<li><a href="http://www.cnblogs.com/fulianga/p/5868823.html" target="_blank" rel="external">WebRTC(iOS)下载编译(下载指定版本)</a></li>
</ul>
]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;安装-Depot-tools&quot;&gt;&lt;a href=&quot;#安装-Depot-tools&quot; class=&quot;headerlink&quot; title=&quot;安装 Depot_tools&quot;&gt;&lt;/a&gt;安装 Depot_tools&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;git 命令获取 depot_tools：&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;1&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;配置坏境变量&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;1&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;2&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;$ echo &amp;quot;export PATH=$PWD/depot_tools:$PATH&amp;quot; &amp;gt; $HOME/.bash_profile&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;$ source $HOME/.bash_profile&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;检测配置是否成功&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;1&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;$ echo $PATH&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;h3 id=&quot;安装-ninja&quot;&gt;&lt;a href=&quot;#安装-ninja&quot; class=&quot;headerlink&quot; title=&quot;安装 ninja&quot;&gt;&lt;/a&gt;安装 ninja&lt;/h3&gt;
    
    </summary>
    
      <category term="知识整理/总结" scheme="http://enkichen.com/categories/%E7%9F%A5%E8%AF%86%E6%95%B4%E7%90%86-%E6%80%BB%E7%BB%93/"/>
    
    
      <category term="WebRTC" scheme="http://enkichen.com/tags/WebRTC/"/>
    
  </entry>
  
  <entry>
    <title>iOS 运行时之消息转发机制</title>
    <link href="http://enkichen.com/2017/04/21/ios-message-forwarding/"/>
    <id>http://enkichen.com/2017/04/21/ios-message-forwarding/</id>
    <published>2017-04-21T06:49:54.000Z</published>
    <updated>2017-05-12T05:20:44.000Z</updated>
    
    <content type="html"><![CDATA[<p>今天写一篇老生常谈的话题 —— Objective-C 的消息转发机制。Objective-C 下所有的方法调用都可以理解为，给一个对象发送一个消息。一个对象接收到消息后，会从当前类的方法列表或者父类的方法列表查找到对应的方法实现（IMP）来处理该消息。大致流程如下：</p>
<ol>
<li>通过 <code>NSObject</code> 的 <em>isa</em> 指针找到对应的 Class</li>
<li>在 Class 的方法列表中找到对应的 <em>selector</em></li>
<li>如果在当前 Class 中未能找到 <em>selector</em> 则往父类的方法列表中继续查找</li>
<li>如果能找到对应的 <em>selector</em> 则去执行对象的方法实现（IMP）</li>
</ol>
<p>在上述流程中如果不能找对对应的 <em>selector</em> 时，这时候就会进入消息转发机制。消息转发机制可分为两个阶段，在这两个阶段中，有 3 次机会来处理之前未能处理 <em>selector</em>，越往后所花费的代价将越大，处理的灵活程度也就越高。如下图所示：</p>
<p><img src="/uploads/forwardflow.png" alt="消息转发流程"><br><a id="more"></a> </p>
<h3 id="第一阶段"><a href="#第一阶段" class="headerlink" title="第一阶段"></a>第一阶段</h3><p>第一阶段也可称之为 <strong>动态方法解析</strong> 阶段，在该阶段中，可以动态的为类添加一个方法，从而让动态添加的方法来处理之前未能处理的消息。可重写类以下方法： </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">+ (BOOL)resolveInstanceMethod:(SEL)sel</div></pre></td></tr></table></figure>
<p>如果是类的静态方法，可重写以下方法：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">+ (BOOL)resolveClassMethod:(SEL)sel</div></pre></td></tr></table></figure>
<p><code>SEL</code> 就是未能处理的 <em>selector</em>，返回值为 <code>BOOL</code> 表示是否增加了新的方法来处理该 <em>selector</em>。在当前阶段处理未知 <em>selector</em> 的前提是，你已经准备好了新的方法来处理该 <em>selector</em>，等着运行时将方法动态添加到类中即可，该阶段一般用来实现 <code>@dynamic</code> 属性。</p>
<h3 id="第二阶段"><a href="#第二阶段" class="headerlink" title="第二阶段"></a>第二阶段</h3><p>如果在第一该阶段中为能处理未知的 <em>selector</em>，运行时将进入第二阶段消息的转发，在该阶段中我们可以将未知的 <em>selector</em> 转发给其他对象来处理。运行时提供两次机会，来做消息的转发，第一次是重写以下方法：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">- (id)forwardingTargetForSelector:(SEL)aSelector</div></pre></td></tr></table></figure>
<p>该方法的 <code>SEL</code> 就是未能处理的 <em>selector</em>，返回值类型为 <code>id</code> 用来指定 <em>selector</em> 处理的对象，运行时将会把未能处理的 <code>SEL</code> 转发给该对象。该阶段我们可以将 <em>selector</em> 转发到类中的其他对象来处理，从而实现 <code>代理模式</code>。如果不重写该方法，运行时将把方法调用的所有细节封装到 <code>NSInvocation</code> 对象中，进入完整的消息转发机制中，运行时将继续调用一下方法来进行消息的派发：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector</div><div class="line">- (void)forwardInvocation:(NSInvocation *)anInvocation</div></pre></td></tr></table></figure>
<p>方法中的 <code>NSInvocation</code> 参数，包含了所有方法调用的细节，包括 <code>selector/target/参数</code> 等，重写该方法后我们可以将 <code>anInvocation</code> 转发给多个对象来处理该消息。在该阶段我们可以用来实现 “多重继承” 或者多重代理等。</p>
<p>如果在两个阶段都不做任何处理的话，运行时将会把 <em>selector</em> 交由 <em>doesNotRecognizeSelector</em> 方法来处理，从而抛出异常导致 Crash ，异常信息一般如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">-[*** ***]:unrecognized selector sent to instance 0x*****</div></pre></td></tr></table></figure>
<h3 id="重写-respondsToSelector-方法"><a href="#重写-respondsToSelector-方法" class="headerlink" title="重写 respondsToSelector 方法"></a>重写 respondsToSelector 方法</h3><p>Objective-C 中 <em>respondsToSelector</em> 方法可以用检查类对象是否能够处理对应的 <em>selector</em>，当我们通过消息转发机制来处理 <em>selector</em> 时， <em>respondsToSelector</em> 并不能按原意正常工作了，这时候需要重写类的 <em>respondsToSelector</em> 方法，用来告诉方法调用者对应的 <em>selector</em> 是能够被处理的。如果是在 <strong>动态方法解析</strong> 阶段使用 <em>class_addMethod</em> 来为类动态添加方法，则不需要重写 <em>respondsToSelector</em> 。</p>
<h3 id="消息转发特性的应用场景"><a href="#消息转发特性的应用场景" class="headerlink" title="消息转发特性的应用场景"></a>消息转发特性的应用场景</h3><h4 id="为-dynamic-实现方法"><a href="#为-dynamic-实现方法" class="headerlink" title="为 @dynamic 实现方法"></a>为 @dynamic 实现方法</h4><p>使用 <code>@synthesize</code> 可以为 <code>@property</code> 自动生成 <code>getter</code> 和 <code>setter</code> 方法（现 Xcode 版本中，会自动生成），而 <code>@dynamic</code> 则是告诉编译器，不用生成 <code>getter</code> 和 <code>setter</code> 方法。当使用 <code>@dynamic</code> 时，我们可以使用消息转发机制，来动态添加 <code>getter</code> 和 <code>setter</code> 方法。当然你也用其他的方法来实现。</p>
<h4 id="代理模式实现"><a href="#代理模式实现" class="headerlink" title="代理模式实现"></a>代理模式实现</h4><p>看完 Objective-C 的消息转发机制，相信很多朋友都能想到 <strong>代理模式</strong>。对 <strong>代理模式</strong> 不熟悉或者不明白的应用场景的同学，可以自行去学习一下 <strong>代理模式</strong>。同时 Objective-C 提供了 <code>NSProxy</code> 类可以用来做动态代理。</p>
<h4 id="多重继承"><a href="#多重继承" class="headerlink" title="多重继承"></a>多重继承</h4><p>学过 C++ 的同学都支持，C++ 是支持多继承的，子类可以从多个类继承，从而获得多个类所有的特性。在消息转发的最后一次处理机会中，运行时会产生一个 <code>NSInvocation</code> 对象，进入到完整的消息转发机制中，在该流程中我们将 <em>selector</em> 转发到不同的对象中处理，便可以达到 “多重继承” 的特性。做法是在类的构造方法中，构造一个或者多个需要继承的对象，将未能处理的 <em>selector</em> 转发到对应的对象中处理即可。</p>
<blockquote>
<p><a href="https://github.com/EnkiChen/MsgForwardingSample.git" target="_blank" rel="external">样例代码</a> 在这里</p>
</blockquote>
<h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><ul>
<li>当接收无法处理的 <em>selector</em> 时，则进入消息转发流程</li>
<li>消息转发流程可分为两阶段，一共有 3 次机会来处理未知的 <em>selector</em></li>
<li>第一阶段为 <strong>动态方法解析</strong> 阶段，用来为类动态添加方法，第二阶段才是正在的消息转发阶段，该阶段可以将未知的 <em>selector</em> 转发到一个或者多个对象中来处理</li>
<li>消息转发流程完成后，都不做任何处理的话，这进入 <em>doesNotRecognizeSelector</em> 方法从而抛出异常</li>
<li>如果将消息转发到其他对象来处理，则需要重写 <em>respondsToSelector</em> 方法来保证该方法正常工作</li>
<li><code>NSProxy</code> 类是基于消息转发机制来实现的动态代理模式</li>
<li>消息转发机制可用来实现 <code>@dynamic</code> 属性、代理模式、多重继承等</li>
</ul>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;今天写一篇老生常谈的话题 —— Objective-C 的消息转发机制。Objective-C 下所有的方法调用都可以理解为，给一个对象发送一个消息。一个对象接收到消息后，会从当前类的方法列表或者父类的方法列表查找到对应的方法实现（IMP）来处理该消息。大致流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过 &lt;code&gt;NSObject&lt;/code&gt; 的 &lt;em&gt;isa&lt;/em&gt; 指针找到对应的 Class&lt;/li&gt;
&lt;li&gt;在 Class 的方法列表中找到对应的 &lt;em&gt;selector&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;如果在当前 Class 中未能找到 &lt;em&gt;selector&lt;/em&gt; 则往父类的方法列表中继续查找&lt;/li&gt;
&lt;li&gt;如果能找到对应的 &lt;em&gt;selector&lt;/em&gt; 则去执行对象的方法实现（IMP）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在上述流程中如果不能找对对应的 &lt;em&gt;selector&lt;/em&gt; 时，这时候就会进入消息转发机制。消息转发机制可分为两个阶段，在这两个阶段中，有 3 次机会来处理之前未能处理 &lt;em&gt;selector&lt;/em&gt;，越往后所花费的代价将越大，处理的灵活程度也就越高。如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/forwardflow.png&quot; alt=&quot;消息转发流程&quot;&gt;&lt;br&gt;
    
    </summary>
    
      <category term="知识整理/总结" scheme="http://enkichen.com/categories/%E7%9F%A5%E8%AF%86%E6%95%B4%E7%90%86-%E6%80%BB%E7%BB%93/"/>
    
    
      <category term="运行时" scheme="http://enkichen.com/tags/%E8%BF%90%E8%A1%8C%E6%97%B6/"/>
    
      <category term="消息转发" scheme="http://enkichen.com/tags/%E6%B6%88%E6%81%AF%E8%BD%AC%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>iOS 运行时之 Associative(关联)</title>
    <link href="http://enkichen.com/2017/02/18/ios-associative/"/>
    <id>http://enkichen.com/2017/02/18/ios-associative/</id>
    <published>2017-02-18T04:42:00.000Z</published>
    <updated>2017-02-18T05:03:04.000Z</updated>
    
    <content type="html"><![CDATA[<p>iOS 下有很多运行时特性，这里介绍一下 <code>Associative(关联)</code> 这个运行时特性，以及它一些使用场景。<code>Associative</code> 意思为关联，能够将两个对象建立一种关系。这种关系是一种 <code>从属</code> 关系，也就是说有一个 <code>关联者</code> 和一个 <code>被关联者</code>。比如说我们可以将一个 <code>NSString</code> 对象关联到一个 <code>UIView</code> 对象上。这里的 <code>NSString</code> 对象就是 <code>被关联者</code>, <code>UIView</code> 对象就是 <code>关联者</code>。</p>
<p>在 <code>objc/runtime.h</code> 文件中，找到 <code>Associative</code> 相关的 API 定义，如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div></pre></td><td class="code"><pre><div class="line">/** </div><div class="line"> * Sets an associated value for a given object using a given key and association policy.</div><div class="line"> * </div><div class="line"> * @param object The source object for the association.</div><div class="line"> * @param key The key for the association.</div><div class="line"> * @param value The value to associate with the key key for object. Pass nil to clear an existing association.</div><div class="line"> * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”</div><div class="line"> * </div><div class="line"> * @see objc_setAssociatedObject</div><div class="line"> * @see objc_removeAssociatedObjects</div><div class="line"> */</div><div class="line">OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)</div><div class="line">    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);</div><div class="line"></div><div class="line">/** </div><div class="line"> * Returns the value associated with a given object for a given key.</div><div class="line"> * </div><div class="line"> * @param object The source object for the association.</div><div class="line"> * @param key The key for the association.</div><div class="line"> * </div><div class="line"> * @return The value associated with the key \e key for \e object.</div><div class="line"> * </div><div class="line"> * @see objc_setAssociatedObject</div><div class="line"> */</div><div class="line">OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)</div><div class="line">    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);</div><div class="line"></div><div class="line">/** </div><div class="line"> * Removes all associations for a given object.</div><div class="line"> * </div><div class="line"> * @param object An object that maintains associated objects.</div><div class="line"> * </div><div class="line"> * @note The main purpose of this function is to make it easy to return an object </div><div class="line"> *  to a &quot;pristine state”. You should not use this function for general removal of</div><div class="line"> *  associations from objects, since it also removes associations that other clients</div><div class="line"> *  may have added to the object. Typically you should use \c objc_setAssociatedObject </div><div class="line"> *  with a nil value to clear an association.</div><div class="line"> * </div><div class="line"> * @see objc_setAssociatedObject</div><div class="line"> * @see objc_getAssociatedObject</div><div class="line"> */</div><div class="line">OBJC_EXPORT void objc_removeAssociatedObjects(id object)</div><div class="line">    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);</div></pre></td></tr></table></figure>
<a id="more"></a>
<p>同时还提供以下枚举类型的定义：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div></pre></td><td class="code"><pre><div class="line">/**</div><div class="line"> * Policies related to associative references.</div><div class="line"> * These are options to objc_setAssociatedObject()</div><div class="line"> */</div><div class="line">typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) &#123;</div><div class="line">    OBJC_ASSOCIATION_ASSIGN = 0,           /**&lt; Specifies a weak reference to the associated object. */</div><div class="line">    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**&lt; Specifies a strong reference to the associated object. </div><div class="line">                                            *   The association is not made atomically. */</div><div class="line">    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**&lt; Specifies that the associated object is copied. </div><div class="line">                                            *   The association is not made atomically. */</div><div class="line">    OBJC_ASSOCIATION_RETAIN = 01401,       /**&lt; Specifies a strong reference to the associated object.</div><div class="line">                                            *   The association is made atomically. */</div><div class="line">    OBJC_ASSOCIATION_COPY = 01403          /**&lt; Specifies that the associated object is copied.</div><div class="line">                                            *   The association is made atomically. */</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<h3 id="API-解析"><a href="#API-解析" class="headerlink" title="API 解析"></a>API 解析</h3><p><code>void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)</code> API 为我们提供了将两个对象建立关联关系的能力，参数解析为：</p>
<ul>
<li><strong>id object</strong>：指定 <code>关联者</code></li>
<li><strong>id value</strong>：指定 <code>被关联者</code></li>
<li><strong>const void *key</strong>：<code>被关联者</code> 的 KEY 值，方便后续可以通过该 KEY 值找到该 <code>被关联者</code></li>
<li><strong>objc_AssociationPolicy policy</strong>: 该参数作用用来表示 <code>被关联者</code> 的引用策略，也就是内存如何进行管理的，可通过上述定义的枚举类型来设置。</li>
</ul>
<p><code>id objc_getAssociatedObject(id object, const void *key)</code> API 可以通过之前设置的 KEY 值，来获取 <code>被关联者</code> 对象，参数解析如下：</p>
<ul>
<li><strong>id object</strong>：<code>关联者</code> 对象</li>
<li><strong>const void *key</strong>：要获取的 <code>被关联者</code> 的 KEY 值，一个 <code>关联者</code> 可以被关联多对象，一个 <code>关联者</code> 也可以是 <code>被关联这</code>，可以通过不同的 KEY 来获取不同的 <code>被关联者</code> 对象。</li>
</ul>
<p><code>void objc_removeAssociatedObjects(id object)</code> 该 API 可以移除一个 <code>关联者</code> 对象所有的 <code>被关联者</code>。当需要移除特定的对象时，我们可以使用 <code>objc_setAssociatedObject</code> 方法并指定 <code>id value</code> 参数对象为空即可。</p>
<p>以上就是关于 <code>Associative(关联)</code> 特性相关的 API 介绍了，下面介绍一下常用的使用场景。</p>
<h3 id="Associative-特性的应用"><a href="#Associative-特性的应用" class="headerlink" title="Associative 特性的应用"></a>Associative 特性的应用</h3><h4 id="剪切板的信息复制"><a href="#剪切板的信息复制" class="headerlink" title="剪切板的信息复制"></a>剪切板的信息复制</h4><p>在一些时候我们希望用户可以长按文案信息，弹出系统的复制菜单，提供文案信息的复制功能，比如长按     <code>UITableViewCell</code> 提供复制详情的功能，在 iOS 下我们可以使用 <code>UIMenuController</code> 类来显示系统菜单，同时为该 <code>UITableViewCell</code> 添加长按手势，代码如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div></pre></td><td class="code"><pre><div class="line">// 添加长按手势</div><div class="line">UILongPressGestureRecognizer *longPressGR = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];</div><div class="line">[cell addGestureRecognizer:longPressGR];</div><div class="line"></div><div class="line">// 手势处理</div><div class="line">- (void)handleLongPress:(UILongPressGestureRecognizer *) longPressGR &#123;</div><div class="line">    UIMenuController *menu = [UIMenuController sharedMenuController];</div><div class="line">    [menu setTargetRect:longPressGR.view.frame inView:self.view];</div><div class="line">    [menu setMenuVisible:YES animated:YES];</div><div class="line">&#125;</div><div class="line"></div><div class="line">// UIMenuController 相关</div><div class="line">- (BOOL)canBecomeFirstResponder &#123;</div><div class="line">    return YES;</div><div class="line">&#125;</div><div class="line"></div><div class="line">- (BOOL)canPerformAction:(SEL)action withSender:(id)sender &#123;</div><div class="line">    if ( action == @selector(copy:) ) &#123;</div><div class="line">        return YES;</div><div class="line">    &#125;</div><div class="line">    return NO;</div><div class="line">&#125;</div><div class="line"></div><div class="line">- (void)copy:(UIMenuController *)menu &#123;</div><div class="line">    UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>从上述代码可以看到，复制信息的逻辑处理是在 <code>copy:</code> 方法中，但是在该方法中，并不能访问到 <code>cell.detailTextLabel</code> 对象，在该场景中，我们可以使用 <code>Associative</code> 特性将 <code>UITableViewCell</code> 对象关联到 <code>UIMenuController</code> 对象中，再在 <code>copy:</code> 方法中获取到被关联对象，从而获取到 <code>UITableViewCell</code> 对象，进而访问 <code>cell.detailTextLabel.text</code>。添加代码如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line">// 处理手势时，添加如下代码</div><div class="line">objc_setAssociatedObject(menu, @&quot;UITableViewCell&quot;, longPressGR.view, OBJC_ASSOCIATION_RETAIN_NONATOMIC);</div><div class="line"></div><div class="line">// 处理 copy 时，添加如下代码，来获取被关联的 UITableViewCell 对象</div><div class="line">objc_getAssociatedObject(menu, @&quot;UITableViewCell&quot;);</div><div class="line">pasteboard.string = cell.detailTextLabel.text;</div></pre></td></tr></table></figure>
<p>上述场景中，并不一定非得用 <code>Associative</code> 特性来实现，还有很多可行的方法，这里为大家提供一种方法，并且该方法还算是比较优雅的。</p>
<h4 id="其他一些应用场景"><a href="#其他一些应用场景" class="headerlink" title="其他一些应用场景"></a>其他一些应用场景</h4><p>另一个常见的应用场景就是，为一个系统类或是一个第三方的类添加一个属性时，可以结合 <strong>Category</strong> 为类添加一个属性，当然也可以使用继承来达到目的。在一些特殊场景下，比如想知道一个系统内部对象或者第三方对象是何时被释放时，我们可以为该对象关联一个自定义的对象，并且使用 <code>OBJC_ASSOCIATION_RETAIN_NONATOMIC</code> 来指定内存管理策略，当关联者被释放是，被关联者也会跟着被释放，这样可以在我们自定义的对象中，知道感兴趣的对象何时被释放的。在调试一些内存问题时，该方法还是蛮有用的。</p>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;iOS 下有很多运行时特性，这里介绍一下 &lt;code&gt;Associative(关联)&lt;/code&gt; 这个运行时特性，以及它一些使用场景。&lt;code&gt;Associative&lt;/code&gt; 意思为关联，能够将两个对象建立一种关系。这种关系是一种 &lt;code&gt;从属&lt;/code&gt; 关系，也就是说有一个 &lt;code&gt;关联者&lt;/code&gt; 和一个 &lt;code&gt;被关联者&lt;/code&gt;。比如说我们可以将一个 &lt;code&gt;NSString&lt;/code&gt; 对象关联到一个 &lt;code&gt;UIView&lt;/code&gt; 对象上。这里的 &lt;code&gt;NSString&lt;/code&gt; 对象就是 &lt;code&gt;被关联者&lt;/code&gt;, &lt;code&gt;UIView&lt;/code&gt; 对象就是 &lt;code&gt;关联者&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;objc/runtime.h&lt;/code&gt; 文件中，找到 &lt;code&gt;Associative&lt;/code&gt; 相关的 API 定义，如下：&lt;/p&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;1&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;2&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;3&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;4&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;5&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;6&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;7&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;8&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;9&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;10&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;11&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;12&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;13&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;14&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;15&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;16&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;17&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;18&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;19&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;20&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;21&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;22&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;23&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;24&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;25&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;26&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;27&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;28&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;29&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;30&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;31&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;32&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;33&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;34&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;35&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;36&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;37&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;38&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;39&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;40&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;41&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;42&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;43&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;div class=&quot;line&quot;&gt;/** &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * Sets an associated value for a given object using a given key and association policy.&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @param object The source object for the association.&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @param key The key for the association.&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @param value The value to associate with the key key for object. Pass nil to clear an existing association.&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @see objc_setAssociatedObject&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @see objc_removeAssociatedObjects&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; */&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;/** &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * Returns the value associated with a given object for a given key.&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @param object The source object for the association.&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @param key The key for the association.&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @return The value associated with the key \e key for \e object.&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @see objc_setAssociatedObject&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; */&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;/** &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * Removes all associations for a given object.&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @param object An object that maintains associated objects.&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @note The main purpose of this function is to make it easy to return an object &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; *  to a &amp;quot;pristine state”. You should not use this function for general removal of&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; *  associations from objects, since it also removes associations that other clients&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; *  may have added to the object. Typically you should use \c objc_setAssociatedObject &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; *  with a nil value to clear an association.&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * &lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @see objc_setAssociatedObject&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; * @see objc_getAssociatedObject&lt;/div&gt;&lt;div class=&quot;line&quot;&gt; */&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;OBJC_EXPORT void objc_removeAssociatedObjects(id object)&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);&lt;/div&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
    
    </summary>
    
      <category term="知识整理/总结" scheme="http://enkichen.com/categories/%E7%9F%A5%E8%AF%86%E6%95%B4%E7%90%86-%E6%80%BB%E7%BB%93/"/>
    
    
      <category term="associative" scheme="http://enkichen.com/tags/associative/"/>
    
      <category term="runtime" scheme="http://enkichen.com/tags/runtime/"/>
    
  </entry>
  
  <entry>
    <title>Protocol Buffer 简介与使用</title>
    <link href="http://enkichen.com/2017/02/17/protobuf-introduce/"/>
    <id>http://enkichen.com/2017/02/17/protobuf-introduce/</id>
    <published>2017-02-17T06:58:27.000Z</published>
    <updated>2017-05-12T05:03:40.000Z</updated>
    
    <content type="html"><![CDATA[<p>Protocol Buffer(简称Protobuf或PB)是由 Google 开发的，原本用来解决索引服务器的请求、响应协议，后面才对外使用和开源的，它是一种数据交换格式，与 XML 和 JSON 相比 ，它是一种二进制格式，避免了各种文本格式转换的问题，并且更体积更小、速度更快使用更简单方便，还自带部分数据压缩功能，在网络传输时可以减少数据流量。同时它也是与平台和语言无关的，尤其在网络数据交换方面，使得它越来越流行。</p>
<p>Protobuf 是一个开源项目，项目托管在 GitHub 上，链接为:<a href="https://github.com/google/protobuf" target="_blank" rel="external">https://github.com/google/protobuf</a>，源码包含两部分内容：</p>
<ul>
<li><strong>PB基础库</strong>：用来完成对象模型与数据模型之前的转换</li>
<li><strong>PB编译器</strong>：源码生成器，用来将 <em>.proto</em> 文件转换成对应语言的对象模型的源码</li>
</ul>
<p>Protobuf 截止目前最新版本为 <code>3.2.0-alpha-1</code> 版本，在 <code>3.0.0</code> 版本之前也就是 <code>2.6.1</code> 版本时官方只支持 <code>C++/Java/Python</code> 三种语言，<code>3.0.0</code> 版本之后才逐步支持其他语言。在这之前如果其他语言需要用到 Protobuf 都是通过第三方扩展来实现的，目前官方以及支持以下编程语言：</p>
<a id="more"></a>
<table>
<thead>
<tr>
<th>Language</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td>C++ (include C++ runtime and protoc)</td>
<td><a href="https://github.com/google/protobuf/blob/master/src" target="_blank" rel="external">src</a></td>
</tr>
<tr>
<td>Java</td>
<td><a href="https://github.com/google/protobuf/blob/master/java" target="_blank" rel="external">java</a></td>
</tr>
<tr>
<td>Python</td>
<td><a href="https://github.com/google/protobuf/blob/master/python" target="_blank" rel="external">python</a></td>
</tr>
<tr>
<td>Objective-C</td>
<td><a href="https://github.com/google/protobuf/blob/master/objectivec" target="_blank" rel="external">objectivec</a></td>
</tr>
<tr>
<td>C#</td>
<td><a href="https://github.com/google/protobuf/blob/master/csharp" target="_blank" rel="external">csharp</a></td>
</tr>
<tr>
<td>JavaNano</td>
<td><a href="https://github.com/google/protobuf/blob/master/javanano" target="_blank" rel="external">javanano</a></td>
</tr>
<tr>
<td>JavaScript</td>
<td><a href="https://github.com/google/protobuf/blob/master/js" target="_blank" rel="external">js</a></td>
</tr>
<tr>
<td>Ruby</td>
<td><a href="https://github.com/google/protobuf/blob/master/ruby" target="_blank" rel="external">ruby</a></td>
</tr>
<tr>
<td>Go</td>
<td><a href="https://github.com/golang/protobuf" target="_blank" rel="external">golang/protobuf</a></td>
</tr>
<tr>
<td>PHP</td>
<td><a href="https://github.com/google/protobuf/blob/master/php" target="_blank" rel="external">php</a></td>
</tr>
</tbody>
</table>
<blockquote>
<p>备注：后续将使用最新版本<code>3.2.0-alpha-1</code>以及 Objective-C 语言作为示例</p>
</blockquote>
<h3 id="Protobuf-编译器"><a href="#Protobuf-编译器" class="headerlink" title="Protobuf 编译器"></a>Protobuf 编译器</h3><p>Protobuf 编译器在于将 <em>.proto</em> 文件转成对应语言的对象模型的源代码，可以从 GitHub 上下载源代码在本地进行编译，然后生成 Protobuf 编译器，也可以从 GitHub 上下载已经编译好的编译器。</p>
<p>下载链接为：<a href="https://github.com/google/protobuf/releases" target="_blank" rel="external">https://github.com/google/protobuf/releases</a></p>
<p>如果是源码的话需要确保一下工具已经安装：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">autoconf automake libtool curl make g++ unzip</div></pre></td></tr></table></figure>
<p>执行以下几个步骤进行编译和安装：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div></pre></td><td class="code"><pre><div class="line"># 生成 configure 文件</div><div class="line">$ ./autogen.sh</div><div class="line"></div><div class="line"># 配置 makefile</div><div class="line">$ ./configure</div><div class="line"></div><div class="line"># 编译</div><div class="line">$ make</div><div class="line">$ make check</div><div class="line"></div><div class="line"># 安装编译后的二进制文件</div><div class="line">$ sudo make install</div></pre></td></tr></table></figure>
<blockquote>
<p>如果出现 <code>/usr/local/Cellar/../Library/ENV/4.3/sed: No such file or directory</code> 错误可以尝试使用 <code>brew reinstall libtool</code> 重新安装 libtool</p>
</blockquote>
<p>如果编译成功，就可以使用 <code>proto</code> 命令了，如果是直接下载的 <code>proto</code> 文件，就不需要编译可直接运行。我们可以创建一个 <em>.proto</em> 文件来测试一下环境是否搭建好，样例文件如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line">message User</div><div class="line">&#123;</div><div class="line">  required int32 uid = 1;        // 用户唯一标识</div><div class="line">  required string nickname = 2;  // 用户昵称</div><div class="line">  required int32 age = 3;        // 用户年龄</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>如果需要生成 Objective-C 源文件的话，执行以下命令即可：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">protoc --objc_out=./ ./test.proto</div></pre></td></tr></table></figure>
<p>如果环境正常的话，可以在当前目录下生成 <em>Test.pbobjc.h</em> 和 <em>Test.pbobjc.m</em> 两个文件，由于 Protobuf 编译器生成的 Objective-C 源文件是基于 MRC 的，所以如果在 ARC 工程成需要添加 <code>-fno-objc-arc</code> 标签，否则无法通过编译。</p>
<h3 id="Protobuf-基础库"><a href="#Protobuf-基础库" class="headerlink" title="Protobuf 基础库"></a>Protobuf 基础库</h3><p>Protobuf 基础库是用来做对象的序列化以及反序列化用的库，也可以从以下地址下载基础库的载源码：</p>
<p>下载链接为：<a href="https://github.com/google/protobuf/releases" target="_blank" rel="external">https://github.com/google/protobuf/releases</a></p>
<p>将下载好的源码拖到对应的工程中即可，同样需要注意的是基础库的源码也是基于 MRC 的，所以也得添加 <code>-fno-objc-arc</code> 标签。同时必须保证基础库和编译器的版本一致，否则也会导致工程无法正常编译。</p>
<p>基础库在 iOS 项目中也可以使用 <strong>cocoapods</strong> 方式引入，只要在 <code>Podfile</code> 中添加：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line"># 当前工程支持的 SDK 版本</div><div class="line">platform :ios, &apos;7.1&apos;</div><div class="line">pod &apos;Protobuf&apos;, &apos;~&gt; 3.1.0&apos;</div></pre></td></tr></table></figure>
<h3 id="在项目中使用-Protobuf"><a href="#在项目中使用-Protobuf" class="headerlink" title="在项目中使用 Protobuf"></a>在项目中使用 Protobuf</h3><p>将编译器生成的对象的源文件以及基础库导入到工程后，我们就可以使用它们了，示例如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div></pre></td><td class="code"><pre><div class="line">// 创建一个 User 对象</div><div class="line">User *user = [[User alloc] init];</div><div class="line">user.uid = 10086;</div><div class="line">user.nickname = @&quot;EnkiChen&quot;;</div><div class="line">user.age = 28;</div><div class="line">    </div><div class="line">// 序列化为 Data</div><div class="line">NSData *data = [user data];</div><div class="line">    </div><div class="line">// 反序列化为对象</div><div class="line">User *us = [User parseFromData:data error:NULL];</div><div class="line">NSLog(@&quot;uid:%d nickname:%@ age:%d&quot;, us.uid, us.nickname, us.age);</div></pre></td></tr></table></figure>
<h3 id="Protobuf-的优缺点"><a href="#Protobuf-的优缺点" class="headerlink" title="Protobuf 的优缺点"></a>Protobuf 的优缺点</h3><h4 id="优点"><a href="#优点" class="headerlink" title="优点"></a>优点</h4><ul>
<li><strong>性能好/效率高</strong>： 相比于 XML 和 JSON 而言，Protobuf 序列化反序列化速度很快，生成的数据体积更小，适合网络传输</li>
<li><strong>有代码生成机制</strong>：可以由编译器自动生成对应语言的类对象文件，无需自己编写解析代码</li>
<li><strong>支持向后/向前兼容</strong>：所谓的“向后兼容”（backward compatible），就是说，当模块 B 升级了之后，它能够正确识别模块 A 发出的老版本的协议。 所谓的“向前兼容”（forward compatible），就是说，当模块A升级了之后，模块 B 能够正常识别模块 A 发出的新版本的协议。</li>
<li><strong>支持多种语言跨平台</strong>：如上文所提到，官方已经支持多种主流语言，并且跨平台</li>
</ul>
<h4 id="缺点"><a href="#缺点" class="headerlink" title="缺点"></a>缺点</h4><ul>
<li><strong>可读性差</strong>：相对于 XML 和 JSON 而言，对象序列化后生成的数据可读性差（二进制格式）</li>
<li><strong>缺乏自描述</strong>：XML 和 JSON 是自描述的，而 Protobuf 格式则不是。一段 Protobuf 格式的二进制数据内容，不配合 <em>.proto</em> 文件结构体是看不出来什么作用的。</li>
</ul>
<p>对于 <em>.proto</em> 文件的编写规则及语法等内容就不介绍了，后续有时间再补上。</p>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Protocol Buffer(简称Protobuf或PB)是由 Google 开发的，原本用来解决索引服务器的请求、响应协议，后面才对外使用和开源的，它是一种数据交换格式，与 XML 和 JSON 相比 ，它是一种二进制格式，避免了各种文本格式转换的问题，并且更体积更小、速度更快使用更简单方便，还自带部分数据压缩功能，在网络传输时可以减少数据流量。同时它也是与平台和语言无关的，尤其在网络数据交换方面，使得它越来越流行。&lt;/p&gt;
&lt;p&gt;Protobuf 是一个开源项目，项目托管在 GitHub 上，链接为:&lt;a href=&quot;https://github.com/google/protobuf&quot;&gt;https://github.com/google/protobuf&lt;/a&gt;，源码包含两部分内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PB基础库&lt;/strong&gt;：用来完成对象模型与数据模型之前的转换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PB编译器&lt;/strong&gt;：源码生成器，用来将 &lt;em&gt;.proto&lt;/em&gt; 文件转换成对应语言的对象模型的源码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Protobuf 截止目前最新版本为 &lt;code&gt;3.2.0-alpha-1&lt;/code&gt; 版本，在 &lt;code&gt;3.0.0&lt;/code&gt; 版本之前也就是 &lt;code&gt;2.6.1&lt;/code&gt; 版本时官方只支持 &lt;code&gt;C++/Java/Python&lt;/code&gt; 三种语言，&lt;code&gt;3.0.0&lt;/code&gt; 版本之后才逐步支持其他语言。在这之前如果其他语言需要用到 Protobuf 都是通过第三方扩展来实现的，目前官方以及支持以下编程语言：&lt;/p&gt;
    
    </summary>
    
      <category term="知识整理/总结" scheme="http://enkichen.com/categories/%E7%9F%A5%E8%AF%86%E6%95%B4%E7%90%86-%E6%80%BB%E7%BB%93/"/>
    
    
      <category term="Protobuf" scheme="http://enkichen.com/tags/Protobuf/"/>
    
  </entry>
  
  <entry>
    <title>HTTP 之 Content-Type</title>
    <link href="http://enkichen.com/2016/08/29/http-Content-Type/"/>
    <id>http://enkichen.com/2016/08/29/http-Content-Type/</id>
    <published>2016-08-29T12:27:25.000Z</published>
    <updated>2016-08-29T12:37:38.000Z</updated>
    
    <content type="html"><![CDATA[<p>在移动端开发中 HTTP 协议经常被用到，但是在面试或者工作中问到客户端和服务器传输业务数据，使用的数据的封装格式是什么以及怎么被封装到 HTTP 协议中时，很少有人能讲明白。</p>
<p>在移动开发中 HTTP 协议为客户端到服务器端，提供了一条数据通道，能够将我们的业务数据传输到服务器，并且从服务器上获取响应数据。在 HTTP 协议头中的 <strong>Content-Type</strong> 字段描述了 <strong>HTTP</strong> 的 <strong>BODY</strong> 体的数据格式，而在 <strong>BODY</strong> 中可以定义我们业务数据的数据格式。<strong>Content-Type</strong> 字段可以用很多种类型，具体有哪些可以看 <a href="http://tool.oschina.net/commons" target="_blank" rel="external"><strong>这里</strong></a>，并且也可以根据自身业务来自定义，不过这种做法比较少。这次我主要讲解三种格式分别为 <strong>application/x-www-form-urlencoded</strong>、<strong>application/json</strong> 以及 <strong>multipart/form-data</strong>，其他的格式可以自己理解。</p>
<p>下面给各位吃瓜群众介绍上述三种格式在 <strong>GET</strong> 和 <strong>POST</strong> 两种请求方法中的区别，为了更直观的看到我们的数据的组织方式，我使用了 <strong>Charles</strong> 来进行抓包分析。<br><a id="more"></a><br>我们测试的业务需求为：</p>
<ol>
<li>客户端发起登录请求，服务器进行响应，请求的数据为 <strong>user</strong> 字段值为 <strong>admin</strong>； <strong>pass</strong> 字段值为 <strong>123456</strong>；</li>
<li>客户端上传一张图片到服务器，字段名为 <strong>imageFile</strong>，内容为图片的二进制数据。</li>
</ol>
<p>为了更好的测试，我对 Web 的请求基于 AFNetworking 2.5.0 版本做了一层简单的封装，发起请求的代码：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div></pre></td><td class="code"><pre><div class="line">- (void)webRequest</div><div class="line">&#123;</div><div class="line">    NSDictionary *bParams = @&#123; @&quot;user&quot; : @&quot;admin&quot;,</div><div class="line">                               @&quot;pass&quot; : @&quot;123456&quot;&#125;;</div><div class="line">    </div><div class="line">    NSDictionary *params = @&#123; kWebServiceMethod     : @&quot;POST&quot;,</div><div class="line">                              kWebServiceParams     : bParams,</div><div class="line">                              kWebServiceIdentifier : kPlatformAccountService,</div><div class="line">                              kWebServiceApiUrl     : @&quot;login&quot;&#125;;</div><div class="line">    </div><div class="line">    [CMLWebProxyService webRequestWithParam:params completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) &#123;</div><div class="line">        DDLogInfo(@&quot;responseObject:%@&quot;, responseObject);</div><div class="line">    &#125;];</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>用来指定数据格式的代码封装在了叫 BaseService 的类中，方法如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">- (NSString *)apiContentType</div><div class="line">&#123;</div><div class="line">    return @&quot;application/x-www-form-urlencoded&quot;;</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>Web 请求的封装的代码（基于 AFNetworking 2.5.0 版本）</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div></pre></td><td class="code"><pre><div class="line">+ (NSURLSessionDataTask *)webRequestWithParam:(NSDictionary *) params completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error)) completionHandler</div><div class="line">&#123;</div><div class="line">    id&lt;CMLService&gt; service = [[CMLServiceFactory sharedInstance] serviceWithIdentifier:params[kWebServiceIdentifier]];</div><div class="line">    NSString *urlString = [NSString stringWithFormat:@&quot;%@%@/%@&quot;, service.apiBaseUrl, service.apiVersion, params[kWebServiceApiUrl]];</div><div class="line">    AFHTTPRequestSerializer *requestSerializer = nil;</div><div class="line">    </div><div class="line">    if ( [service.apiContentType isEqualToString:@&quot;application/x-www-form-urlencoded&quot;] ) &#123;</div><div class="line">        </div><div class="line">        requestSerializer = [AFHTTPRequestSerializer serializer];</div><div class="line">        </div><div class="line">    &#125; else if ( [service.apiContentType isEqualToString:@&quot;application/json&quot;] ) &#123;</div><div class="line">        </div><div class="line">        requestSerializer = [AFJSONRequestSerializer serializer];</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    [requestSerializer setValue:@&quot;application/json&quot; forHTTPHeaderField:@&quot;Accept&quot;];</div><div class="line">    requestSerializer.timeoutInterval = kWebServiceTimeoutInterval;</div><div class="line">    </div><div class="line">    NSDictionary *headParams = params[kWebServiceHeadParams];</div><div class="line">    [headParams enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) &#123;</div><div class="line">        [requestSerializer setValue:obj forHTTPHeaderField:key];</div><div class="line">    &#125;];</div><div class="line">    </div><div class="line">    NSURLRequest *request = nil;·</div><div class="line">    NSArray *binParams = params[kWebServiceBinParams];</div><div class="line">    if ( binParams != nil &amp;&amp; binParams.count != 0 ) &#123;</div><div class="line">        request = [requestSerializer multipartFormRequestWithMethod:params[kWebServiceMethod]</div><div class="line">                                                          URLString:urlString</div><div class="line">                                                         parameters:params[kWebServiceParams]</div><div class="line">                                          constructingBodyWithBlock:^(id&lt;AFMultipartFormData&gt; formData) &#123;</div><div class="line">                                              </div><div class="line">                                              for ( NSDictionary *binData in binParams ) &#123;</div><div class="line">                                                  [formData appendPartWithFileData:binData[kWebBinData]</div><div class="line">                                                                              name:binData[kWebBinName]</div><div class="line">                                                                          fileName:binData[kWebBinFileName]</div><div class="line">                                                                          mimeType:binData[kWebBinMimeType]];</div><div class="line">                                              &#125;</div><div class="line">                                              </div><div class="line">                                          &#125; error:nil];</div><div class="line">    &#125; else &#123;</div><div class="line">        request = [requestSerializer requestWithMethod:params[kWebServiceMethod] URLString:urlString parameters:params[kWebServiceParams] error:nil];</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    NSURLSessionDataTask *dataTsk = [[CMLWebProxyService sessionManager] dataTaskWithRequest:request completionHandler:completionHandler];</div><div class="line">    </div><div class="line">    [dataTsk resume];</div><div class="line">    </div><div class="line">    return dataTsk;</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<h3 id="POST-下的数据组织形式"><a href="#POST-下的数据组织形式" class="headerlink" title="POST 下的数据组织形式"></a>POST 下的数据组织形式</h3><p>先看看各个格式的抓包的截图，在做对比分析。</p>
<p><strong>application/x-www-form-urlencoded</strong> 格式的抓包截图</p>
<p><img src="/uploads/http_content_type_1.png" alt="url 编码格式"></p>
<p><strong>application/json</strong> 格式的抓包截图</p>
<p><img src="/uploads/http_content_type_2.png" alt="url 编码格式"></p>
<p><strong>multipart/form-data</strong> 格式的请求代码以及抓包的截图：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div></pre></td><td class="code"><pre><div class="line">NSDictionary *bParams = @&#123; @&quot;user&quot; : @&quot;admin&quot;,</div><div class="line">                              @&quot;pass&quot; : @&quot;123456&quot;&#125;;</div><div class="line">   </div><div class="line">   NSData *imageBin = UIImagePNGRepresentation([UIImage imageNamed:@&quot;avatar_cat&quot;]);</div><div class="line">   </div><div class="line">   NSDictionary *binPar = @&#123; kWebBinData : imageBin,</div><div class="line">                             kWebBinName : @&quot;imageFile&quot;,</div><div class="line">                             kWebBinFileName : @&quot;image.png&quot;,</div><div class="line">                             kWebBinMimeType : @&quot;image/png&quot;</div><div class="line">                            &#125;;</div><div class="line">   </div><div class="line">   NSDictionary *params = @&#123; kWebServiceMethod     : @&quot;POST&quot;,</div><div class="line">                             kWebServiceParams     : bParams,</div><div class="line">                             kWebServiceBinParams  : @[binPar],</div><div class="line">                             kWebServiceIdentifier : kPlatformAccountService,</div><div class="line">                             kWebServiceApiUrl     : @&quot;login&quot;&#125;;</div></pre></td></tr></table></figure>
<p><img src="/uploads/http_content_type_3.png" alt="url 编码格式"></p>
<p><strong>application/x-www-form-urlencoded</strong> 格式对请求参数进行了 <a href="http://deyimsf.iteye.com/blog/1776082" target="_blank" rel="external"><strong>URL 编码</strong></a> 并将参数放在了 <strong>BODY</strong> 中；<strong>application/json</strong> 格式是将参数进行了 <a href="http://www.w3school.com.cn/json/json_syntax.asp" target="_blank" rel="external"><strong>JSON</strong></a> 格式的转换，放在了 <strong>BODY</strong> 中；</p>
<p>前两种都是传的普通的文本数据，如果我们需要同时传输文本数据以及二进制数据时，就得用到 <strong>multipart/form-data</strong> 编码格式了，可以从图中看到在 <strong>HTTP</strong> 的头部的 <strong>Content-Type</strong> 中多了 <strong>Boundary</strong> 字段，该字段的作用为对多项数据进行分割；截图中 3 个参数都使用了指定的字符串进行了分割，在图片的那一项中也包含了 <strong>Content-Type</strong> 字段用来描述该项的数据格式为 <strong>PNG</strong> 图片。如果没有图片参数的话，也可以直接使用 <strong>multipart/form-data</strong> 格式来进行组织。</p>
<blockquote>
<p>用来分割的字符串的值是每次随机生成的。不同的客户生成的字符串长度也会不一样。</p>
</blockquote>
<h3 id="GET-下的数据组织形式"><a href="#GET-下的数据组织形式" class="headerlink" title="GET 下的数据组织形式"></a>GET 下的数据组织形式</h3><p><strong>HTTP</strong> 的 <strong>GET</strong> 请求方式，是将我们要请求的参数放在 <strong>URL</strong> 地址后面，所以 <strong>GET</strong> 方式只支持 <strong>application/x-www-form-urlencoded</strong> 格式，而 <strong>application/json</strong> 和 <strong>multipart/form-data</strong> 不支持的，这个应该比较容易理解。下面是我抓包的截图：</p>
<p><img src="/uploads/http_content_type_4.png" alt="url 编码格式"></p>
<h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>其实抓包截图做对比就很容易理解不同的参数的意义了，在很多情况下后端和前端开发人员都是使用成熟的第三方框架来帮我们做这部分工作，对 <strong>HTTP</strong> 协议了解的还不够细致，有时候发现在调试接口无法解析数据，其实很有可能 <strong>Content-Type</strong> 类型不一致导致的。如果觉得文本协议不够精简，也可以使用二进制协议来传输 <strong>user</strong> 和 <strong>pass</strong> 字段，比如 <strong>protobuf</strong> ，只要和服务器端协商好就可以。</p>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在移动端开发中 HTTP 协议经常被用到，但是在面试或者工作中问到客户端和服务器传输业务数据，使用的数据的封装格式是什么以及怎么被封装到 HTTP 协议中时，很少有人能讲明白。&lt;/p&gt;
&lt;p&gt;在移动开发中 HTTP 协议为客户端到服务器端，提供了一条数据通道，能够将我们的业务数据传输到服务器，并且从服务器上获取响应数据。在 HTTP 协议头中的 &lt;strong&gt;Content-Type&lt;/strong&gt; 字段描述了 &lt;strong&gt;HTTP&lt;/strong&gt; 的 &lt;strong&gt;BODY&lt;/strong&gt; 体的数据格式，而在 &lt;strong&gt;BODY&lt;/strong&gt; 中可以定义我们业务数据的数据格式。&lt;strong&gt;Content-Type&lt;/strong&gt; 字段可以用很多种类型，具体有哪些可以看 &lt;a href=&quot;http://tool.oschina.net/commons&quot;&gt;&lt;strong&gt;这里&lt;/strong&gt;&lt;/a&gt;，并且也可以根据自身业务来自定义，不过这种做法比较少。这次我主要讲解三种格式分别为 &lt;strong&gt;application/x-www-form-urlencoded&lt;/strong&gt;、&lt;strong&gt;application/json&lt;/strong&gt; 以及 &lt;strong&gt;multipart/form-data&lt;/strong&gt;，其他的格式可以自己理解。&lt;/p&gt;
&lt;p&gt;下面给各位吃瓜群众介绍上述三种格式在 &lt;strong&gt;GET&lt;/strong&gt; 和 &lt;strong&gt;POST&lt;/strong&gt; 两种请求方法中的区别，为了更直观的看到我们的数据的组织方式，我使用了 &lt;strong&gt;Charles&lt;/strong&gt; 来进行抓包分析。&lt;br&gt;
    
    </summary>
    
      <category term="开发笔记" scheme="http://enkichen.com/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="HTTP" scheme="http://enkichen.com/tags/HTTP/"/>
    
  </entry>
  
  <entry>
    <title>两则开发小笔记</title>
    <link href="http://enkichen.com/2016/07/22/development-of-small-notes/"/>
    <id>http://enkichen.com/2016/07/22/development-of-small-notes/</id>
    <published>2016-07-22T06:22:53.000Z</published>
    <updated>2016-07-22T06:33:49.000Z</updated>
    
    <content type="html"><![CDATA[<p>好久没有写博客了，介绍一下项目中一个 Core Graphics 绘制图片时的效率优化方法，以及记录一下 GCDAsyncSocket 框架在读取网络数据要注意的一个点。</p>
<h3 id="Core-Graphics-绘图性能对比"><a href="#Core-Graphics-绘图性能对比" class="headerlink" title="Core Graphics 绘图性能对比"></a>Core Graphics 绘图性能对比</h3><p>Core Graphics 框架为我们提供了 2D 绘图能力，在使用 Core Graphics 绘制图片时，不同的实现方法使得渲染图片的效率有很大差异，下面是提供两种不同实现方式的对比，看看 Core Graphics 在绘制图片时效率上的差异。</p>
<p>测试方法为自定义一个 <code>UIView</code>，重写<code>- (void)drawRect:(CGRect)rect</code> 方法，在该方法中使用 Core Graphics 方式绘制图片。然后在开启一个定时器不断的调用 <code>- (void)setNeedsDisplay;</code> 方法来计算绘图所使用的时间。<br><a id="more"></a></p>
<h4 id="方法一"><a href="#方法一" class="headerlink" title="方法一"></a>方法一</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div></pre></td><td class="code"><pre><div class="line">- (void)awakeFromNib &#123;</div><div class="line">    self.coreImage = [UIImage imageNamed:@&quot;IMG_0079.png&quot;];</div><div class="line">&#125;</div><div class="line"></div><div class="line">- (void)drawRect:(CGRect)rect &#123;</div><div class="line">    struct timeval start , end ;</div><div class="line">    gettimeofday(&amp;start, NULL);</div><div class="line">    [self.coreImage drawAtPoint:CGPointZero];</div><div class="line">    // [self.coreImage drawInRect:CGRectMake(0, 0, 512, 512)];</div><div class="line">    gettimeofday(&amp;end, NULL);</div><div class="line">    long cost = ((end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec ) / 1000 ;</div><div class="line"></div><div class="line">    NSLog(@&quot;cost:%ld&quot;, cost);</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<h4 id="方法二"><a href="#方法二" class="headerlink" title="方法二"></a>方法二</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div></pre></td><td class="code"><pre><div class="line">- (void)awakeFromNib &#123;</div><div class="line">    self.coreImage = [UIImage imageNamed:@&quot;IMG_0079.png&quot;];</div><div class="line">    CGSize size = self.frame.size;</div><div class="line">    UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);</div><div class="line">    [self.coreImage drawInRect:CGRectMake(0, 0, size.width, size.height)];</div><div class="line">    self.coreImage = UIGraphicsGetImageFromCurrentImageContext();</div><div class="line">    UIGraphicsEndImageContext();</div><div class="line">&#125;</div><div class="line"></div><div class="line">- (void)drawRect:(CGRect)rect &#123;</div><div class="line">    struct timeval start , end ;</div><div class="line">    gettimeofday(&amp;start, NULL);</div><div class="line">    [self.coreImage drawAtPoint:CGPointZero];</div><div class="line">    // [self.coreImage drawInRect:CGRectMake(0, 0, 512, 512)];</div><div class="line">    gettimeofday(&amp;end, NULL);</div><div class="line">    long cost = ((end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec ) / 1000 ;</div><div class="line"></div><div class="line">    NSLog(@&quot;cost:%ld&quot;, cost);</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<blockquote>
<p>使用 <code>[self.coreImage drawInRect:CGRectMake(0, 0, 512, 512)];</code> 方法来进行图片的缩放，大小可自定义，只要跟图片本身大小不一致即可。</p>
</blockquote>
<h4 id="对比结果"><a href="#对比结果" class="headerlink" title="对比结果"></a>对比结果</h4><table>
<thead>
<tr>
<th style="text-align:center">方法</th>
<th style="text-align:center">是否缩放</th>
<th style="text-align:center">耗时时间</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">方法一</td>
<td style="text-align:center">NO</td>
<td style="text-align:center">10ms ~ 20ms</td>
</tr>
<tr>
<td style="text-align:center">方法一</td>
<td style="text-align:center">YES</td>
<td style="text-align:center">100ms ~ 120ms</td>
</tr>
<tr>
<td style="text-align:center">方法二</td>
<td style="text-align:center">NO</td>
<td style="text-align:center">1ms ~ 2ms</td>
</tr>
<tr>
<td style="text-align:center">方法二</td>
<td style="text-align:center">YES</td>
<td style="text-align:center">30ms ~ 40ms</td>
</tr>
</tbody>
</table>
<p>我测试的手机为 iPhone 6 机型，图片为 iPhone 拍摄的照片，上表的中耗时时间跟图片以及机器有关，看对比结果即可；从对比结果可以知道如果在绘制时绘制的大小跟图片本身大小不一致时，系统会对其进行缩放，从而导致绘制效率变低，如果我们提前将图片缩放到指定大小，从而可以提高在绘制时的效率；使用 <code>UIGraphicsGetImageFromCurrentImageContext</code> 之前有看到过一篇文章提到，该方法会将图片缓存到显存中，从而使得绘制效率的提高，不过这篇文章已经找不到了。</p>
<p>在实际项目中可选择合适的方法的进行优化，比如图片过大可以使用方法二进行缩放，同时可以放到子线程中进行处理，从而提供绘制时的效率。我在实际项目中的需求为图片可能进行缩放，所以我针对图片做了两级缓存，缩放后在子线程中生成缩放后的图片，同时在缩放的过程中使用了 OpenGL 进行渲染。</p>
<h3 id="GCDAsyncSocket-读取数据的注意事项"><a href="#GCDAsyncSocket-读取数据的注意事项" class="headerlink" title="GCDAsyncSocket 读取数据的注意事项"></a>GCDAsyncSocket 读取数据的注意事项</h3><p>今天一同事使用 GCDAsyncSocket 在测试一个网络程序，发现使用我给的代码，无法接收到数据。代码大概如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div></pre></td><td class="code"><pre><div class="line">- (void)push &#123;</div><div class="line">    if (!self.asyncSocket) &#123;</div><div class="line">        self.asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    if (!self.bufferData) &#123;</div><div class="line">        self.bufferData = [[NSMutableData alloc] init];</div><div class="line">    &#125;</div><div class="line">    </div><div class="line">    NSError *err = nil;</div><div class="line">    if (![self.asyncSocket connectToHost:PUSH_HOST onPort:PUSH_PORT error:&amp;err]) &#123;</div><div class="line">        NSLog(@&quot;I goofed: %@&quot;, err);</div><div class="line">        return;</div><div class="line">    &#125;</div><div class="line">&#125;</div><div class="line"></div><div class="line">- (void)socket:(GCDAsyncSocket *)sender didConnectToHost:(NSString *)host port:(UInt16)port &#123;</div><div class="line">    NSString *contentString = @&quot;data&quot;</div><div class="line">    NSData *data = [contentString dataUsingEncoding:NSUTF8StringEncoding];</div><div class="line">    [self.asyncSocket writeData:data withTimeout:-1 tag:PUSH_DATA_TAG];</div><div class="line">    [self.asyncSocket readDataToLength:100 withTimeout:-1 buffer:self.bufferData bufferOffset:0 tag:PUSH_DATA_TAG];</div><div class="line">&#125;</div><div class="line"></div><div class="line">- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err &#123;</div><div class="line">    DDLogWarn(@&quot;SocketDidDisconnect:WithError: %@&quot;, err);</div><div class="line">&#125;</div><div class="line"></div><div class="line">- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag &#123;</div><div class="line">    DDLogWarn(@&quot;socket:didReadData: %ld&quot;, tag);</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>代码流程很简单，客户端在连接上服务器上之后，会给服务器发送一段数据，服务器接收到数据后，在返回一个状态码给到客户端，方法使用上都是正常的；服务器在响应客户端之后，会直接断开连接，上述代码在客户端的实际表现为发送数据到服务器之后，直接就收到断开的回调消息了，并没有接收到服务器响应的数据。</p>
<p>经过测试后发现，<code>- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag;</code> 一定要接收到指定数据长度后，才会回调对应的方法，而实际服务器返回给到客户端的数据只有两个字节，导致接收数据的方法无法得到回调，就算是连接被断开后也是无法被回调的，导致上层是没办法获取到对应的数据，针对该问题我们可以使用 <code>- (void)readDataWithTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag;</code> 来解决该问题。</p>
<p>所以在使用 <code>GCDAsyncSocket</code> 时得要注意使用合适的方法来获取数据，否则会导致特定情况下无法接收数据回调。</p>
]]></content>
    
    <summary type="html">
    
      &lt;p&gt;好久没有写博客了，介绍一下项目中一个 Core Graphics 绘制图片时的效率优化方法，以及记录一下 GCDAsyncSocket 框架在读取网络数据要注意的一个点。&lt;/p&gt;
&lt;h3 id=&quot;Core-Graphics-绘图性能对比&quot;&gt;&lt;a href=&quot;#Core-Graphics-绘图性能对比&quot; class=&quot;headerlink&quot; title=&quot;Core Graphics 绘图性能对比&quot;&gt;&lt;/a&gt;Core Graphics 绘图性能对比&lt;/h3&gt;&lt;p&gt;Core Graphics 框架为我们提供了 2D 绘图能力，在使用 Core Graphics 绘制图片时，不同的实现方法使得渲染图片的效率有很大差异，下面是提供两种不同实现方式的对比，看看 Core Graphics 在绘制图片时效率上的差异。&lt;/p&gt;
&lt;p&gt;测试方法为自定义一个 &lt;code&gt;UIView&lt;/code&gt;，重写&lt;code&gt;- (void)drawRect:(CGRect)rect&lt;/code&gt; 方法，在该方法中使用 Core Graphics 方式绘制图片。然后在开启一个定时器不断的调用 &lt;code&gt;- (void)setNeedsDisplay;&lt;/code&gt; 方法来计算绘图所使用的时间。&lt;br&gt;
    
    </summary>
    
      <category term="开发笔记" scheme="http://enkichen.com/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="Core Graphics" scheme="http://enkichen.com/tags/Core-Graphics/"/>
    
      <category term="GCDAsyncSocket" scheme="http://enkichen.com/tags/GCDAsyncSocket/"/>
    
  </entry>
  
</feed>
