<?xml version="1.0" encoding="utf-8"?>
<search>
  <entry>
    <title>Swift 与C语言交互</title>
    <url>/posts/swift_interacts_with_c_language/</url>
    <content><![CDATA[<p>与C语言交互发生在使用一些C语言编写的API上。C语言的语法会桥接到Swift中对应的语法中。Swift能很好地与C语言交互。</p>
<h2 id="类型">类型</h2>
<p>C语言的基础类型、枚举、结构体、联合体在Swift中都有对应。</p>
<p>其中基础类型是一一对等的。其命名方式对C中的类型采取驼峰式命名之后，加上前缀字母C。</p>
<p>例如：</p>
<ul>
<li><code>int</code> 变成 <code>CInt</code>；</li>
<li><code>unsigned char</code> 变成 <code>CUnsignedChar</code>；</li>
<li><code>unsigned long long</code> 变成 <code>CUnsignedLongLong</code>；</li>
</ul>
<p>其中，只有有三个表示宽字符的类型是特殊的：</p>
<ul>
<li><code>wchat_t</code> 变成 <code>CWideChar</code>；</li>
<li><code>char16_t</code> 变成 <code>CChar16</code>；</li>
<li><code>char32_t</code> 变成 <code>CChar32</code>；</li>
</ul>
<p>这些在Swift对应的基础类型都是typealias，在Swift应应使用typealias类型而不直接使用原生类型。</p>
<h3 id="变量">变量</h3>
<p>全局变量/常量也同样映射到Swift的全局变量/常量中。</p>
<h3 id="枚举结构体联合体">枚举、结构体、联合体</h3>
<p>C中的枚举只是一个普通的基础常量，除非使用<code>NS_×_ENUM</code>、<code>NS_OPTIONS</code>关键字修饰来定义枚举才会映射到Swift中的结构体，但基本使用跟Swift的枚举无异。</p>
<p>结构体则可以直接映射到Swift的结构体。</p>
<p>联合体则是映射到一个结构体中。</p>
<h2 id="函数">函数</h2>
<p>C语言的函数基本能映射到Swift的函数中，除了不支持不固定参数函数，作为替代方案，Swift支持映射<code>va_list</code>表示的可变参数列表。</p>
<p>使用<code>CF_SWIFT_NAME</code>还能把C函数重命名甚至并入extension中。</p>
<h2 id="指针">指针</h2>
<p>Swift的指针都带有Unsafe关键字，表示其使用在编译期可能是不可预测的。</p>
<p>Apple Developer 文档里有 C 指针和 Swift 指针的对应表：</p>
<table>
<thead>
<tr class="header">
<th>C Syntax</th>
<th>Swift Syntax</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>const Type *</code></td>
<td>UnsafePointer</td>
</tr>
<tr class="even">
<td><code>Type *</code></td>
<td>UnsafeMutablePointer</td>
</tr>
<tr class="odd">
<td><code>Type * const *</code></td>
<td>UnsafePointer</td>
</tr>
<tr class="even">
<td><code>Type * __strong *</code></td>
<td>UnsafeMutablePointer</td>
</tr>
<tr class="odd">
<td><code>Type **</code></td>
<td>AutoreleasingUnsafeMutablePointer</td>
</tr>
<tr class="even">
<td><code>const void *</code></td>
<td>UnsafeRawPointer</td>
</tr>
<tr class="odd">
<td><code>void *</code></td>
<td>UnsafeMutableRawPointer</td>
</tr>
</tbody>
</table>
<p>除此以外，Swift还有几种指针表达方式：</p>
<ul>
<li>UnsafeBufferPointer、UnsafeMutableBufferPointer、UnsafeRawBufferPointer、UnsafeMutableRawBufferPointer</li>
<li>OpaquePointer</li>
</ul>
<h3 id="buffer">Buffer</h3>
<p>它相当于在原始内存空间上添加一个view，以步幅（<code>MemoryLayout&lt;T&gt;.stride</code>）单位，以集合的方式访问底层内存。对应C语言的就是数组的访问。</p>
<h3 id="opaquepointer">OpaquePointer</h3>
<p>对于在C回调函数中要传递Swift的对象时，这些对象可能无法桥接到C的类型，这时就要用到OpaquePointer。</p>
<p>转换Swift对象为指针需要用到Unmanaged。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> fooObj <span class="operator">=</span> <span class="type">Foo</span>()</span><br><span class="line"><span class="comment">// 创建Unmanaged&lt;Foo&gt;实例</span></span><br><span class="line"><span class="keyword">let</span> unmanagedFoo <span class="operator">=</span> <span class="type">Unmanaged</span>.passRetained(fooObj)</span><br><span class="line"><span class="comment">// 转换成OpaquePointer，该指针可直接传递到void *的C指针中</span></span><br><span class="line"><span class="keyword">let</span> unmanagedPtr <span class="operator">=</span> unmanagedFoo.toOpaque()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 传递到C函数</span></span><br><span class="line">aFuncWithCallback(unmanagedPtr) &#123;</span><br><span class="line">    (ptr: <span class="type">UnsafeMutableRawPointer</span>?) -&gt; <span class="type">Void</span> <span class="keyword">in</span></span><br><span class="line">    <span class="comment">// 创建Unmanaged&lt;Foo&gt;，并转换为Foo类型</span></span><br><span class="line">    <span class="keyword">let</span> fooObj <span class="operator">=</span></span><br><span class="line">        <span class="type">Unmanaged</span>&lt;<span class="type">Foo</span>&gt;.fromOpaque(ptr<span class="operator">!</span>).takeUnretainedValue()</span><br><span class="line">    <span class="built_in">print</span>(fooObj.foo)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>Unmanaged创建和转换都有Retained和Unretained版本，对应的是指是否引用计数操作。Pass方法是否+1引用计数；take方法是否-1引用计数。</p>
<h3 id="字符串指针">字符串指针</h3>
<p>C中表示字符串的 <code>char *</code>，桥接到Swift会变成 <code>UnsafeMutablePointer&lt;Int8&gt;</code>，可以通过相应的构造方法创建String类型。</p>
<h2 id="指针转换">指针转换</h2>
<h3 id="方法参数是指针">方法参数是指针</h3>
<p>调用将指针作为参数的函数时，可以使用隐式转换来传递兼容的指针类型或使用隐式桥接来传递指向变量或数组内容的指针。</p>
<p>若是常量指针参数（<code>UnsafePointer&lt;Type&gt;</code>），可以直接传递：字符串、指定类型数组、指定类型的inout表达式。</p>
<p>若是可变指针参数（<code>UnsafeMutablePointer&lt;Type&gt;</code>），可以直接传递：指定类型数组的inout表达式、指定类型的inout表达式。</p>
<h4 id="向下隐式转换">向下隐式转换</h4>
<p>指针在作为函数参数传递时，可以隐式转换：</p>
<ul>
<li>不可变指针 -&gt; 可变指针</li>
<li>类型指针 -&gt; 原始指针</li>
</ul>
<p>Raw可以直接兼容没有Raw的类型指针。如：需要<code>UnsafeRawPointer</code>类型参数时，可以直接传递<code>UnsafePointer&lt;Type&gt;</code>。</p>
<h4 id="隐式桥接">隐式桥接</h4>
<p>类型变量/常量、数组、字符串，在传递给指针参数时会进行隐式桥接。</p>
<h3 id="swift基本类型---指针">Swift基本类型 -&gt; 指针</h3>
<p>即用指针访问Swift变量/常量。</p>
<p>使用<code>withUnsafexxxPointer</code>函数，在闭包中用指针临时访问指向的变量/常量。</p>
<p><strong>注意：</strong>在闭包中得到的指针不要返回出去。因为这是随外部变量/常量的生命周期影响，会因为其变量/常量销毁而变成野指针。</p>
<p>顶级函数：</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 指向具体类型变量的类型指针</span></span><br><span class="line"><span class="keyword">func</span> <span class="title function_">withUnsafePointer</span>&lt;<span class="type">T</span>, <span class="type">Result</span>&gt;(<span class="params">to</span> <span class="params">value</span>: <span class="keyword">inout</span> <span class="type">T</span>, <span class="keyword">_</span> <span class="params">body</span>: (<span class="type">UnsafePointer</span>&lt;<span class="type">T</span>&gt;) <span class="keyword">throws</span> -&gt; <span class="type">Result</span>) <span class="keyword">rethrows</span> -&gt; <span class="type">Result</span></span><br><span class="line"><span class="keyword">func</span> <span class="title function_">withUnsafeMutablePointer</span>&lt;<span class="type">T</span>, <span class="type">Result</span>&gt;(<span class="params">to</span> <span class="params">value</span>: <span class="keyword">inout</span> <span class="type">T</span>, <span class="keyword">_</span> <span class="params">body</span>: (<span class="type">UnsafeMutablePointer</span>&lt;<span class="type">T</span>&gt;) <span class="keyword">throws</span> -&gt; <span class="type">Result</span>) <span class="keyword">rethrows</span> -&gt; <span class="type">Result</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 以字节为单位访问，注意闭包中的参数是Buffer类型</span></span><br><span class="line"><span class="keyword">func</span> <span class="title function_">withUnsafeBytes</span>&lt;<span class="type">T</span>, <span class="type">Result</span>&gt;(<span class="params">of</span> <span class="params">value</span>: <span class="keyword">inout</span> <span class="type">T</span>, <span class="keyword">_</span> <span class="params">body</span>: (<span class="type">UnsafeRawBufferPointer</span>) <span class="keyword">throws</span> -&gt; <span class="type">Result</span>) <span class="keyword">rethrows</span> -&gt; <span class="type">Result</span></span><br><span class="line"><span class="keyword">func</span> <span class="title function_">withUnsafeMutableBytes</span>&lt;<span class="type">T</span>, <span class="type">Result</span>&gt;(<span class="params">of</span> <span class="params">value</span>: <span class="keyword">inout</span> <span class="type">T</span>, <span class="keyword">_</span> <span class="params">body</span>: (<span class="type">UnsafeMutableRawBufferPointer</span>) <span class="keyword">throws</span> -&gt; <span class="type">Result</span>) <span class="keyword">rethrows</span> -&gt; <span class="type">Result</span></span><br></pre></td></tr></table></figure>
<h4 id="数组中的应用">数组中的应用</h4>
<ul>
<li><code>withUnsafeBytes</code>、<code>withUnsafeMutableBytes</code></li>
<li><code>withUnsafeBufferPointer</code>、<code>withUnsafeMutableBufferPointer</code></li>
</ul>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// withUnsafeMutableBytes应用</span></span><br><span class="line"><span class="keyword">var</span> numbers: [<span class="type">Int32</span>] <span class="operator">=</span> [<span class="number">0</span>, <span class="number">0</span>]</span><br><span class="line"><span class="keyword">var</span> byteValues: [<span class="type">UInt8</span>] <span class="operator">=</span> [<span class="number">0x01</span>, <span class="number">0x00</span>, <span class="number">0x00</span>, <span class="number">0x00</span>, <span class="number">0x02</span>, <span class="number">0x00</span>, <span class="number">0x00</span>, <span class="number">0x00</span>]</span><br><span class="line"></span><br><span class="line">numbers.withUnsafeMutableBytes &#123; destBytes <span class="keyword">in</span></span><br><span class="line">    byteValues.withUnsafeBytes &#123; srcBytes <span class="keyword">in</span></span><br><span class="line">        destBytes.copyBytes(from: srcBytes)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// numbers == [1, 2]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// withUnsafeBytes应用</span></span><br><span class="line"><span class="keyword">var</span> numbers <span class="operator">=</span> [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]</span><br><span class="line"><span class="keyword">var</span> byteBuffer: [<span class="type">UInt8</span>] <span class="operator">=</span> []</span><br><span class="line">numbers.withUnsafeBytes &#123;</span><br><span class="line">    byteBuffer.append(contentsOf: <span class="variable">$0</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// byteBuffer == [1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, ...]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// withUnsafeBufferPointer应用</span></span><br><span class="line"><span class="keyword">let</span> numbers <span class="operator">=</span> [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>]</span><br><span class="line"><span class="keyword">let</span> sum <span class="operator">=</span> numbers.withUnsafeBufferPointer &#123; buffer -&gt; <span class="type">Int</span> <span class="keyword">in</span></span><br><span class="line">    <span class="keyword">var</span> result <span class="operator">=</span> <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">stride</span>(from: buffer.startIndex, to: buffer.endIndex, by: <span class="number">2</span>) &#123;</span><br><span class="line">        result <span class="operator">+=</span> buffer[i]</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// &#x27;sum&#x27; == 9</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// withUnsafeMutableBufferPointer应用</span></span><br><span class="line"><span class="keyword">var</span> numbers <span class="operator">=</span> [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>]</span><br><span class="line">numbers.withUnsafeMutableBufferPointer &#123; buffer <span class="keyword">in</span></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">stride</span>(from: buffer.startIndex, to: buffer.endIndex <span class="operator">-</span> <span class="number">1</span>, by: <span class="number">2</span>) &#123;</span><br><span class="line">        buffer.swapAt(i, i <span class="operator">+</span> <span class="number">1</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">print</span>(numbers)</span><br><span class="line"><span class="comment">// Prints &quot;[2, 1, 4, 3, 5]&quot;</span></span><br></pre></td></tr></table></figure>
<h4 id="字符串中的应用">字符串中的应用</h4>
<ul>
<li><code>withCString</code></li>
<li><code>withUTF8</code></li>
</ul>
<h4 id="data中的应用">Data中的应用</h4>
<ul>
<li><code>withUnsafeBytes</code></li>
<li><code>withUnsafeMutableBytes</code></li>
</ul>
<h3 id="指针类型转换">指针类型转换</h3>
<p>注意Swift中的指针类型不存在继承关系。</p>
<h4 id="类型指针---原始指针">类型指针 -&gt; 原始指针</h4>
<p>直接用原始指针的构造方法进行转换，如果是作为入参，则无需转换直接传递即可。</p>
<h4 id="原始指针---类型指针">原始指针 -&gt; 类型指针</h4>
<p>永久绑定：<code>bindMemory</code>、<code>assumingMemoryBound</code>，两者操作的类型必须一致。</p>
<p>这种方式需要从原始指针调用，如果是类型指针要转换类型，则须转换为原始指针，再调用绑定类型方法。<code>bindMemory</code>会导致原来的类型指针是未定义的，如果修改后兼容原来。</p>
<p>临时转换访问：<code>withMemoryRebound</code>，该方法的访问发生在闭包参数中。</p>
<h3 id="访问指向的值">访问指向的值</h3>
<p>若是类型指针（<code>UnsafePointer&lt;Type&gt;</code>），可直接访问<code>pointee</code>，可读写。</p>
<p>若是原始指针（<code>UnsafeRawPointer</code>），使用<code>load</code>方法，返回指定偏移的具体类型。只读。</p>
<p>当然对于可变原始指针（<code>UnsafeMutableRawPointer</code>），有对应的写入方法：</p>
<ul>
<li><code>copyMemory</code>：从其他原始指针拷贝字节数据</li>
<li><code>storeBytes</code>：写入具体类型</li>
</ul>
<h2 id="c指针使用注意">C指针使用注意</h2>
<p>在 Core Foundation 里，几乎所有用 Create 和 Copy 开头的函数，只要它们返回一个非托管的对象，我们几乎总是应该使用 takeRetainedValue() 方法来读取。</p>
<p><a href="https://nshipster.cn/unmanaged/">Unmanaged - NSHipster</a></p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://developer.apple.com/documentation/swift/swift_standard_library/manual_memory_management">Manual Memory Management | Apple Developer Documentation</a></li>
<li><a href="https://developer.apple.com/documentation/swift/swift_standard_library/c_interoperability">C Interoperability | Apple Developer Documentation</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Swift</tag>
      </tags>
  </entry>
  <entry>
    <title>Swift 字节序操作</title>
    <url>/posts/swift_byte_order_operation/</url>
    <content><![CDATA[<p>整型提供不同字节序的视图：</p>
<ul>
<li><code>littleEndian</code></li>
<li><code>bigEndian</code></li>
<li><code>byteSwapped</code>：字节序翻转，即大端-&gt;小端，或小端-&gt;大端</li>
</ul>
<p>若是整数值等于小端序的结果，则说明该平台是小端序的。</p>
<p>当然字节序是在该类型大小是大于1字节的才有效果。</p>
<p>Core Fundation也提供了一系列字节序的操作，如较常用的把原本数据的大端序、小端序转换为主机序：</p>
<ul>
<li><code>CFSwapInt32BigToHost</code></li>
<li><code>CFSwapInt32LittleToHost</code></li>
</ul>
<p>一般我们都是事先知道数据是用哪种字节序编码的，这是编码时约定的，而解码时则需要把某些数据还原成可读的值，这就涉及不同平台字节序的转换。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://developer.apple.com/documentation/corefoundation/byte-order_utilities">Byte-Order Utilities | Apple Developer Documentation</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Swift</tag>
      </tags>
  </entry>
  <entry>
    <title>Swift 底层</title>
    <url>/posts/swift_underlying/</url>
    <content><![CDATA[<p>OC的Runtime机制使得它可以被认为是一种动态语言。</p>
<p>Swift则取消了Runtime这个能力，让Swift称为一门静态语言。Swift语言的对象方法调用基本在编译期间就被确定，可以看做是一种硬编码形式的调用实现。这种机制加快了程序的运行速度、减少程序包体积，但在编译连接优化功能开启时反而又会出现增大包提及的情况。Swift在编译连接期间采用的是空间换时间的优化策略，以提高运算行速度为主要优化考虑点。</p>
<h2 id="oc类对象方法调用">OC类对象方法调用</h2>
<p>OC对象调用方法都通过消息发送的<code>objc_msgSend</code>完成，并至少传入调用对象和对象方法名称作为参数，方法根据对象找到类结构信息，通过方法名来找到最终调用的方法函数地址，并最终完成函数的调用。这是OC Runtime的实现机制，同时也是OC对多态的实现。</p>
<h2 id="swift类对象">Swift类对象</h2>
<p>Swift类对象按其基类可分为：</p>
<ul>
<li>从NSObject及其子类派生的类。</li>
<li>SwiftObject派生的类（SwiftObject是隐藏的类，不会在源代码中体现）。</li>
</ul>
<p>Swift类对象内存布局和OC类内存布局相似。都是在最开始部分有一个isa指针，指向类的描述信息。Swift类的描述信息结构继承自OC类的描述信息，但没有完全使用其中的属性，对于方法的调用主要是使用其中扩展的序函数表的区域。</p>
<p>Swift对象实例都是在堆内存中创建，与OC一致。Swift类在实例化时，会生成堆内存分配和初始化函数，形式为：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line">模块名.类名.__allocating_init(类名,初始化参数)</span><br></pre></td></tr></table></figure>
<p>与OC一致，Swift类实例也是通过引用计数管理生命周期，所以也会在编译时插入<code>swift_retain</code>、<code>swift_release</code>函数，当前引用计数为0后，就调用生成的西沟和销毁函数，形式为：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line">模块名.类名.__deallocating_deinit(对象)</span><br></pre></td></tr></table></figure>
<h2 id="swift方法调用">Swift方法调用</h2>
<p>Swift类定义的方法可分为：</p>
<ul>
<li>OC类派生类并重写的方法</li>
<li>扩展中定义的方法</li>
<li>类中定义的一般方法</li>
</ul>
<p>对于这三种方法，系统采用的处理和调用机制是完全不一样的。</p>
<h3 id="oc类派生类并重写的方法">OC类派生类并重写的方法</h3>
<p>这些方法还是与OC类原本方法的调用机制一致，即也是使用<code>objc_msgSend</code>调用。</p>
<h3 id="扩展中定义的方法">扩展中定义的方法</h3>
<p>这里的扩展中定义的方法是不包含重写OC基类的方法。这种方法调用时在编译期就决定的，即在调用方法时，直接使用硬编码的函数地址。这也决定了扩展中的方法无法在运行时做替换和改变。</p>
<p>同时这类方法的符号信息不保存到类的描述信息中，也决定了派生类中不能重写扩展中的定义的方法，即不支持多态。</p>
<h3 id="类中定义的一般方法">类中定义的一般方法</h3>
<p>Swift在未开启编译链接优化时，对象的方法调用实现机制和C++的虚函数调用机制类似。Swift为每个类都建立一个虚函数表的数组结构，保存着所以定义的常规成员方法的地址。</p>
<p>在方法调用时，不再想使用<code>objc_msgSend</code>调用传入调用对象和方法名，而是直接调用函数地址，而调用对象则存在x20寄存器中，让代码更加安全。</p>
<p>虽然类中方法在虚函数表中的索引值是在编译期确定的，但基类和派生类虚函数表中相同索引处的函数地址可以不一样，即当子类重写了父类某个方法时，会分别生成两个类的虚函数表，在相同的索引位置保存不同的函数地址实现多态。</p>
<p>另外，为了实现方法重载和运算符重载，函数名字会进行修饰重命名，规则如下：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line">_$s&lt;模块名长度&gt;&lt;模块名&gt;&lt;类名长度&gt;&lt;类名&gt;C&lt;方法名长度&gt;&lt;方法名&gt;yy&lt;参数类型<span class="number">1</span>&gt;_&lt;参数类型<span class="number">2</span>&gt;_&lt;参数类型N&gt;F</span><br></pre></td></tr></table></figure>
<h2 id="swift的成员变量">Swift的成员变量</h2>
<p>OC类的成员变量根据定义顺序排在isa后面，另外还会生成一张变量偏移表，通过偏移来访问成员变量。</p>
<p>Swift简化了对成员变量的访问。直接在编译链接时确定成员变量在对象的偏移位置。且不生成变量编译信息表。</p>
<h2 id="结构体">结构体</h2>
<p>初始化器与属性默认值在汇编上是一样的。</p>
<p>Swift结构体与C结构体的内存结构是一样的，成员变量内存都是紧挨在一起。</p>
<p>Swift结构体内存结构中没有保存isa，即没有保存结构体信息，因此不支持多态与派生，同时结构体中的所有方法都是通过在编译期硬编码实现的。</p>
<h2 id="类方法和全局函数">类方法和全局函数</h2>
<p>Swift类方法和全局函数不存在对象作为参数，即不需要把对象保存到x20寄存器中，所以它就是个简单的C语言普通函数，只是方法名需要进行修饰重命名，所有对类方法和全局函数的调用都是在编译期硬编码为函数地址来调用的。</p>
<h2 id="开启编译链接优化后">开启编译链接优化后</h2>
<p>开启编译链接优化后，Swift对象方法的调用机制做了很大的优化，最主要是弱化了通过虚函数表来间接调用方法的实现，而是大量改用一些内联的方式来处理方法函数的调用。</p>
<p>对于多态的支持，可能不是通过虚函数来处理，而是通过类型判断，然后用条件语句分支执行方法。</p>
<h2 id="swift其他特性底层">Swift其他特性底层</h2>
<h3 id="引用类型与值类型">引用类型与值类型</h3>
<p>值类型存在栈中，引用类型存在堆中。若值类型中包含引用类型，则存储的只是引用类型的指针。</p>
<h3 id="延迟存储属性">延迟存储属性</h3>
<p>多线程同时第一次访问lazy属性，是无法保证属性只被初始化一次。</p>
<p>当结构体包含一个延迟存储属性时，只有<code>var</code>才能访问延迟存储属性。因为延迟属性初始化时要改变结构体的内存，而let修饰的结构体要求里面的所以成员都完成初始化，两者相悖。</p>
<p>延迟属性也不能添加属性观察器。</p>
<h3 id="类存储属性">类存储属性</h3>
<p>类存储属性默认就是<code>lazy</code>修饰，且能有<code>let</code>修饰成常量，会在第一次使用的时候才初始化。类存储属性时多线程安全的（系统底层会有加锁处理）。这其中调用了<code>swift_once</code>函数，内部调用了<code>dispatch_once</code>函数。</p>
<p>类存储属性实际上是全局变量。</p>
<h3 id="输入输出参数">输入输出参数</h3>
<p>inout参数本质是地址传递（引用传递）。在汇编使用<code>leaq</code>传递地址，而一般参数则是使用<code>movq</code>传递值。</p>
<p>限制：</p>
<ul>
<li>不能有默认值</li>
<li>需要时左值。</li>
</ul>
<p>对于有属性观察器以及计算属性，在get/set输入输出参数时，也会触发相关的方法调用。</p>
<p>所以，如果实参有物理内存地址，且有设置属性观察器，则直接把实参的内存地址传入参数（实参进行引用传递）。</p>
<p>如果实参是计算属性或设置了属性观察器，则采取Copy In Copy Out的做法：在调用该函数时，先复制实参的值，产生副本（可以理解成getter操作），将副本的内存地址传入参数（副本进行引用传递），在函数内部修改副本的值，函数返回后，再将副本的值覆盖实参的值（可以理解为setter操作）。</p>
<h3 id="函数重载">函数重载</h3>
<p>返回值不参与函数重载。</p>
<h3 id="内联函数">内联函数</h3>
<p>开启编译器优化后，编译器会将某些函数转为内联函数，即把函数展开成函数体。但以下情况不会被自动内联：</p>
<ul>
<li>函数体较长</li>
<li>包含递归调用的函数</li>
<li>包含动态派发的函数</li>
</ul>
<h3 id="闭包">闭包</h3>
<p>闭包的定义：一个函数和它所捕获的变量/常量环境组合起来。</p>
<p>闭包函数会把捕获的变量存储到堆中动态申请的内存空间上。存储的信号包含类型描述信息、引用计数信息、具体值。每次闭包函数调用，都会动态申请一段新的堆内存在存放新的捕获变量。若没有捕获变量，则不回分配堆空间。</p>
<p>内存布局如下：</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/3qML8t.jpg" /></p>
<p>所以闭包的内存布局与实例对象很相似：共享方法、各自管理自己的成员变量。</p>
<p>空合并运算符<code>??</code>默认值是个自动闭包，这可以在可以解包的情况下不调用默认值的逻辑。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">??</span> &lt;<span class="type">T</span>&gt;(<span class="params">optional</span>: <span class="type">T</span>?, <span class="params">defaultValue</span>: <span class="keyword">@autoclosure</span> () <span class="keyword">throws</span> -&gt; <span class="type">T</span>?) <span class="keyword">rethrows</span> -&gt; <span class="type">T</span>?</span><br></pre></td></tr></table></figure>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://juejin.cn/post/6966857669508825096">Swift底层原理探索5----闭包 - 掘金</a></li>
<li><a href="https://juejin.cn/post/6844903889523884039">Swift5.0 的 Runtime 机制浅析 - 掘金</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Swift</tag>
      </tags>
  </entry>
  <entry>
    <title>APNs</title>
    <url>/posts/apns/</url>
    <content><![CDATA[<p>基本原理：</p>
<ol type="1">
<li>后端把要发送的消息、目标设备标识打包，发送给APNS。</li>
<li>APNS在已注册推送服务的设备列表中，查找符合标识的设备，把消息发送到设备。</li>
<li>设备把发送来的消息传递给相应的App，按照设定弹出推送通知。</li>
</ol>
<h2 id="细节">细节</h2>
<p>注册过程：</p>
<ol type="1">
<li>设备链接APNs并携带设备UUID；</li>
<li>连接成功后，APNs经过打包和处理产生deviceToken返回到注册的设备。</li>
<li>设备把deviceToken发送到自己服务器。</li>
</ol>
<p>推送过程：</p>
<ol type="1">
<li>设备装有App，且有网络的情况下，APNs会验证deviceToken，成功后会处于一个长连接。</li>
<li>后端发送消息时，后端按照指定格式进行打包，结合deviceToken一起发送到APNs。</li>
<li>APNs把新消息发送到设备，然后根据设定弹出推送通知。</li>
</ol>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
      </tags>
  </entry>
  <entry>
    <title>KVC</title>
    <url>/posts/kvc/</url>
    <content><![CDATA[<p>KVC，Key Value Coding，是一种通过字符串key来访问类属性的机制。不是通过调用setter、getter方法访问。</p>
<p>KVC和KVO都属于键值编程，而且底层实现机制都是<strong>isa-swizzing</strong>。</p>
<h2 id="设值流程">设值流程</h2>
<ol type="1">
<li>按顺序查找<code>setKey:</code>、<code>_setKey:</code>方法，找到方法则传递参数，调用方法。否则继续。</li>
<li>调用<code>accessInstanceVariablesDirectly</code>方法。
<ul>
<li>YES：默认，查找成员变量：
<ol type="1">
<li>按照<code>_key</code>、<code>_isKey</code>、<code>key</code>、<code>isKey</code> 的顺序查找，找到了就直接赋值。</li>
</ol></li>
<li>NO：进入下一步。</li>
</ul></li>
<li>调用<code>setValue:forUndefinedKey:</code>方法，并抛出NSUnknownKeyException异常。</li>
</ol>
<p><code>setValue:forUndefinedKey:</code>方法的默认实现就是抛出异常，所以可以通过重写该方法避免抛出异常。</p>
<h2 id="取值流程">取值流程</h2>
<p>基本与设值流程一致，只是把set关键字改成get。</p>
<ol type="1">
<li>按顺序查找<code>getKey</code>、<code>key</code>、<code>isKey</code>、<code>_key</code>方法，找到方法则直接调用。否则继续。</li>
<li>调用<code>accessInstanceVariablesDirectly</code>方法。
<ul>
<li>YES：默认，查找成员变量：
<ol type="1">
<li>按照<code>_key</code>、<code>_isKey</code>、<code>key</code>、<code>isKey</code> 的顺序查找，找到了就直接取值。</li>
</ol></li>
<li>NO：进入下一步。</li>
</ul></li>
<li>调用<code>valueForUndefinedKey:</code>方法，并抛出NSUnknownKeyException异常。</li>
</ol>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://ityongzhen.github.io/KVC%E9%82%A3%E7%82%B9%E5%84%BF%E4%BA%8B.html/">KVC那点儿事 | 殷永振</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
      </tags>
  </entry>
  <entry>
    <title>KVO</title>
    <url>/posts/kvo/</url>
    <content><![CDATA[<p>KVO用于逻辑隔离对象之间的监听，支持一对一和一对多的属性监听。这里的一对一和一对多是针对监听的属性的，即既可以监听单个属性，也可以监听集合属性。</p>
<p>在OC中，所有NSObject子类的所有属性（包括计算属性）都支持KVO；而在Swift中，只有在<code>@objc dynamic</code>修饰的属性（包括计算属性）才支持KVO，即使用<code>@objc dynamic</code>修饰的属性与OC行为一致。</p>
<p>KVC和KVO都属于键值编程，而且底层实现机制都是<strong>isa-swizzing</strong>。</p>
<p>KVO和NSNotificationCenter都是iOS观察着模式的一种实现。KVO对被监听对象无侵入性，即无需修改代码即可支持监听。</p>
<h2 id="使用">使用</h2>
<p>注册监听：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[bankInstance addObserver:personInstance forKeyPath:@&quot;accountBalance&quot; options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];</span><br></pre></td></tr></table></figure>
<p><code>bankInstance</code>是变更发出的对象。<code>personInstance</code>是响应变更的对象。<code>@"accountBalance"</code>是<code>bankInstance</code>的key path。</p>
<p>选项是多选的，影响change字典内容和生成通知的方式。</p>
<ul>
<li>new：change字典包含新值</li>
<li>old：change字典包含旧值</li>
<li>initial：注册监听时即发送变更通知</li>
<li>prior：在变更通知前发送一次变更前的通知，即每个变更两次通知，一个willChange，一个didChange</li>
</ul>
<p>change字典的键：</p>
<ul>
<li>kind：改变类型
<ul>
<li>setting：对象或集合赋值替换</li>
<li>insertion：对象插入到集合属性中</li>
<li>removal：对象从集合中移除</li>
<li>replacement：对象从集合中替换</li>
</ul></li>
<li>new：新值</li>
<li>old：旧值</li>
<li>indexes：插入/移除/替换对象的索引</li>
<li>prior：标识该通知是willChange的，否则是didChange的</li>
</ul>
<p>new、old可以时单个对象，也可以时对象集合，表示集合时表示移除/替换的对象。</p>
<p>移除监听可在这两个时机完成：</p>
<ul>
<li>不需要监听时。</li>
<li>被观察对象、观察者对象销毁时。被观察对象与观察者对象常常具有相同的生命周期。</li>
</ul>
<p>使用注意：</p>
<ul>
<li><p>移除监听的调用角色与注册监听保持一致。</p></li>
<li><p>多次注册监听导致多次响应。didChnage变更通知的顺序与注册顺序相反，即以先进后出的顺序调用。</p></li>
<li><p>注册监听与移除监听必须保持配对，多了少了都会导致异常崩溃。但iOS 11以上不会崩溃。</p></li>
</ul>
<h3 id="手动处理变更通知">手动处理变更通知</h3>
<ol type="1">
<li>在被观察对象（如上面的BankObject）重写<code>+automaticallyNotifiesObserversForKey:</code>方法（默认返回YES，即默认所有键值都会通知变更），定义需要手动控制的属性。记得在不需要修改的key上返回super方法。</li>
<li>在需要发送通知的地方调用：
<ul>
<li>一对一：<code>willChangeValueForKey:</code>、<code>didChangeValueForKey:</code></li>
<li>有序一对多：<code>willChange:valuesAtIndexes:forKey:</code>、<code>didChange:valuesAtIndexes:forKey:</code></li>
<li>无序一对多：<code>willChangeValueForKey:withSetMutation:usingObjects:</code>、<code>didChangeValueForKey:withSetMutation:usingObjects:</code></li>
</ul></li>
</ol>
<p>如果没有重写相关通知方法，则只会在initial选项生效。</p>
<p>手动处理变更通知的应用场景：</p>
<ul>
<li>筛选通知，如去重、控制通知发送时机。</li>
<li>多个键的通知一起发送。</li>
</ul>
<h3 id="注册依赖键">注册依赖键</h3>
<p>需要在被观察对象（如上面的BankObject）重写<code>+keyPathsForValuesAffectingValueForKey:</code>方法或<code>+keyPathsForValuesAffecting&lt;Key&gt;</code>方法，返回给定key依赖的key path集合。记得在不需要修改的key上返回super方法。</p>
<p>注意：<code>+keyPathsForValuesAffecting&lt;Key&gt;</code>方法在Swift中要声明为<code>@objc</code>。否则不会识别。这种方法还可以用于在分类添加依赖键。</p>
<h3 id="注册集合监听">注册集合监听</h3>
<p>对不可变集合采用一对一的对象赋值方式更新，对于可变集合，访问集合时，不能直接访问对应属性，需要使用<code>mutableArrayValueForKey:</code>类似的方法取出集合，对取出的集合进行增删改都会出发变更通知。</p>
<p>集合注册依赖键比较麻烦，需要使用KVC的方式。</p>
<h3 id="新特性与坑">新特性与坑</h3>
<p>Swift重新封装了注册和移除监听的方式：</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 注册与取消</span></span><br><span class="line"><span class="keyword">func</span> <span class="title function_">observe</span>&lt;<span class="type">Value</span>&gt;(<span class="keyword">_</span> <span class="params">keyPath</span>: <span class="type">KeyPath</span>&lt;_KeyValueCodingAndObserving, <span class="type">Value</span>&gt;, <span class="params">options</span>: <span class="type">NSKeyValueObservingOptions</span> <span class="operator">=</span> [], <span class="params">changeHandler</span>: <span class="keyword">@escaping</span> (_KeyValueCodingAndObserving, <span class="type">NSKeyValueObservedChange</span>&lt;<span class="type">Value</span>&gt;) -&gt; <span class="type">Void</span>) -&gt; <span class="type">NSKeyValueObservation</span></span><br><span class="line"><span class="keyword">func</span> <span class="title function_">invalidate</span>()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用</span></span><br><span class="line">weather.observe(\.text, options: [.initial, .new, .old, .prior], changeHandler: printSwiftKVOReponse)</span><br></pre></td></tr></table></figure>
<p>iOS 11以下使用时，需要主动置空NSKeyValueObservation或调用<code>invalidate</code>方法。</p>
<p>Swift的KVO方式弱化了observer的存在，而是使用block接收变更通知。并返回一个token，用token进行取消注册。进而替代原有的注册与取消监听的方式。而其他方法observing对象的方法基本不变。</p>
<p>另外<code>@objc dynamic</code>只支持与OC共用的类型，像Swift的枚举、结构体、元组都不支持。</p>
<p>监听OC的枚举类型还会出问题，导致change的所有值都为空，而使用传统的注册监听方式是正常的。</p>
<p>即使测试中监听可选类型的属性也会导致上述问题。</p>
<h2 id="kvo实现原理">KVO实现原理</h2>
<p>使用runtime创建被观察对象的子类，重写sttter，附加KVO通知逻辑，然后把<code>isa</code>指针指向创建的子类并重写相关方法，实现对原本对象的替换。</p>
<p>在运行时根据原类创建一个中间类，这个中间类是原类的子类，并动态修改当前对象的isa指向中间类，并将class方法重写，返回原类的Class。当修改实例对象属性时，会调用Foundation的<code>_NSSetXXXValueAndNotify</code>函数，函数先调用<code>willChangeValueForKey:</code>，然后调用父类原来的setter方法修改值，最后是<code>didChangeValueForKey:</code>，这些方法触发Observer的监听方法。</p>
<h3 id="触发机制">触发机制</h3>
<p>所以，基于原理得知，所有调用属性setter的都会触发KVO通知，所以KVC也会触发KVO，但直接修改成员变量则不会。</p>
<p>注意：使用KVC能对没有setter的属性，甚至成员变量修改值，这都会触发相同keyPath的KVO通知。</p>
<h2 id="参考">参考</h2>
<ul>
<li>在Swift中使用KVO：<a href="../Swift/Language%20Interoperability/Cocoa%20Design%20Patterns/Using%20Key-Value%20Observing%20in%20Swift.md">Using Key-Value Observing in Swift</a></li>
<li><a href="https://ityongzhen.github.io/%E5%85%B3%E4%BA%8EKVO%E7%9C%8B%E8%BF%99%E7%AF%87%E5%B0%B1%E5%A4%9F%E4%BA%86.html/">关于KVO看这篇就够了 | 殷永振</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
      </tags>
  </entry>
  <entry>
    <title>Objective-C：Block</title>
    <url>/posts/block/</url>
    <content><![CDATA[<h2 id="变量捕获机制">变量捕获机制</h2>
<ul>
<li>全局变量：不会捕获到block内部，直接访问。</li>
<li>auto基本类型的局部变量，捕获到block内部，生成成员变量来存储，以值传递的方式访问。</li>
<li>static类型的局部变量：捕获到block内部，生成成员变量来存储，以指针方式访问。</li>
<li>对象类型的局部变量：连同它的所有权修饰符一起捕获。</li>
</ul>
<h2 id="变量捕获修饰符">变量捕获修饰符</h2>
<ul>
<li><code>__weak</code>是为了防止循环引用。在block中访问weak指针，当对象被释放时，指针置空，变成nil调用相关方法。</li>
<li><code>__strong</code>是为了延长生命周期。这里用在block和目标对象存在相互引用的关系才有效，且赋值的是weak指针，作用是使其引用计数+1。相当于声明了个局部变量。</li>
<li><code>__block</code>是为了在block内部可以修改外部的变量。
<ul>
<li>如果想用<code>__block</code>解决循环引用，必须要在block中将其修饰的变量置空。</li>
</ul></li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">- (void)touchesBegan:(NSSet&lt;UITouch *&gt; *)touches withEvent:(UIEvent *)event &#123;</span><br><span class="line">    [self dismissViewControllerAnimated:true completion:nil];</span><br><span class="line">    </span><br><span class="line">    __weak typeof(self) _self = self;</span><br><span class="line">    TestViewController *tmp = self;</span><br><span class="line">    self.block = ^&#123;</span><br><span class="line">        // 1⃣️</span><br><span class="line">        // 注意声明是发生在这里，即weak self指向的对象还没被销毁的时候</span><br><span class="line">        __strong typeof(_self) s_self = _self; // 同下</span><br><span class="line">        // TestViewController *s_self = _self;</span><br><span class="line">        // TestViewController *s_self = tmp; // 还是导致循环引用</span><br><span class="line">        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^&#123;</span><br><span class="line">            // 2⃣️</span><br><span class="line">            NSLog(@&quot;block属性延时weak: %@, strong: %@&quot;, _self, s_self);</span><br><span class="line">            // weak == strong != nil</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;;</span><br><span class="line">    self.block();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>注意，给<code>__strong</code>变量赋值的弱引用必须在弱引用指向的对象还没被销毁的时候完成赋值，否则拿到的弱引用是个nil。</p>
<p>另外，如果只是在1⃣️处定义并赋值了强引用对象，而在2⃣️处没有使用，则2⃣️处访问的弱引用也是空的。</p>
<h2 id="底层数据结构">底层数据结构</h2>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/UUBdE6.jpg" /></p>
<p>block本质也是一个OC对象，内部也有个isa指针。封装了函数调用以及调用环境。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://juejin.cn/post/6844904070746996750">OC 底层探索 - Block 详解 - 掘金</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Objective-C</tag>
      </tags>
  </entry>
  <entry>
    <title>Objective-C：内省</title>
    <url>/posts/introspection/</url>
    <content><![CDATA[<ul>
<li><code>isMemberOfClass</code>：对象是否是某类型对象。</li>
<li><code>isKindOfClass</code>：对象时否是某类型或类型子类的对象。</li>
<li><code>isSubclassOfClass</code>、<code>isAncestorOfObject</code>：类对象是否是另一个类型的子类、父类。</li>
<li><code>respondsToSelector</code>：是否能响应某方法。</li>
<li><code>conformsToProtocol</code>：是否遵循某协议。</li>
</ul>
<p><code>class</code>与<code>object_getClass</code>：</p>
<ul>
<li>实例<code>class</code> = <code>object_getClass(self)</code></li>
<li>类<code>class</code>返回自身；<code>object_getClass(类对象)</code>返回元类。</li>
</ul>
<p>实现：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">// 判断当前对象、类的isa指向是不是类、原类</span><br><span class="line">+ (BOOL)isMemberOfClass:(Class)cls &#123;</span><br><span class="line">    return object_getClass((id)self) == cls;</span><br><span class="line">&#125;</span><br><span class="line">- (BOOL)isMemberOfClass:(Class)cls &#123;</span><br><span class="line">    return [self class] == cls;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 判断当前对象、类的isa是不是类、元类或者其子类类型</span><br><span class="line">+ (BOOL)isKindOfClass:(Class)cls &#123;</span><br><span class="line">    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls-&gt;superclass) &#123;</span><br><span class="line">        if (tcls == cls) return YES;</span><br><span class="line">    &#125;</span><br><span class="line">    return NO;</span><br><span class="line">&#125;</span><br><span class="line">- (BOOL)isKindOfClass:(Class)cls &#123;</span><br><span class="line">    for (Class tcls = [self class]; tcls; tcls = tcls-&gt;superclass) &#123;</span><br><span class="line">        if (tcls == cls) return YES;</span><br><span class="line">    &#125;</span><br><span class="line">    return NO;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Class object_getClass(id obj)</span><br><span class="line">&#123;</span><br><span class="line">    if (obj) return obj-&gt;getIsa();</span><br><span class="line">    else return Nil;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>显然<code>isKindOfClass</code>范围更大，当调用对象和参数都是类时，类对象的isa指向元类对象，而其元类的superclass指向class对象，所以满足条件返回YES。所以<code>[instance/class isKindOfClass:[NSObject class]];</code>都返回 1。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">@interface Person : NSObject</span><br><span class="line">@end</span><br><span class="line">......</span><br><span class="line">    BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];</span><br><span class="line">    BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];</span><br><span class="line">    BOOL res3 = [[Person class] isKindOfClass:[Person class]];</span><br><span class="line">    BOOL res4 = [[Person class] isMemberOfClass:[Person class]];</span><br><span class="line"></span><br><span class="line">    NSLog(@&quot;%d,%d,%d,%d&quot;, res1, res2, res3, res4);</span><br><span class="line">......</span><br><span class="line"></span><br><span class="line">    // 1,0,0,0</span><br></pre></td></tr></table></figure>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Objective-C</tag>
      </tags>
  </entry>
  <entry>
    <title>Objective-C：Tagged Pointer</title>
    <url>/posts/tagged_pointer/</url>
    <content><![CDATA[<p>介绍：</p>
<p>TaggedPointer专门用来存储小的对象，如NSNumber、NSDate、NSString。其存储的不是地址，而是真正的值，所以它直接存储到栈中。</p>
<p>引入：</p>
<p>对于指针类型，其长度足够存储一些短小的值，而不必操作堆分配与管理内存。对于大的值，即超过指针类型长度的，则还是在堆中分配内存。所以对于小的值指针内容就包含值，而大的值才是堆内存地址。</p>
<p>存储内容：值+标记</p>
<h2 id="面试题">面试题</h2>
<h4 id="执行以下两段代码有什么区别">执行以下两段代码，有什么区别？</h4>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">dispatch_queue_t</span> queue = dispatch_get_global_queue(<span class="number">0</span>, <span class="number">0</span>);</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; <span class="number">1000</span>; i++) &#123;</span><br><span class="line">    <span class="built_in">dispatch_async</span>(queue, ^&#123;</span><br><span class="line">        <span class="keyword">self</span>.name = [<span class="built_in">NSString</span> stringWithFormat:<span class="string">@&quot;abcdefghij&quot;</span>];</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>会崩溃，因为该<code>name</code>为<code>__NSCFString</code>类型，存储在堆上，是个常规对象，需要维护引用计数。通过setter赋值，异步并发调用会有多条线程执行<code>[_name release]</code>，连续<code>release</code>两次就会造成对象的过度释放。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">dispatch_queue_t queue = dispatch_get_global_queue(0, 0);</span><br><span class="line">for (int i = 0; i &lt; 1000; i++) &#123;</span><br><span class="line">    dispatch_async(queue, ^&#123;</span><br><span class="line">        self.name = [NSString stringWithFormat:@&quot;abcdefghi&quot;];</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>正常，该<code>name</code>为<code>NSTaggedPointerString</code>类型，在<code>objc_release</code>函数中会判断指针是不是<code>TaggedPointer</code>类型，是的话就不对对象进行<code>release</code>操作，也就避免了过度释放对象。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://juejin.cn/post/6844903475562676238">iOS Tagged Pointer (源码阅读必备知识) - 掘金</a></li>
<li><a href="https://blog.devtang.com/2014/05/30/understand-tagged-pointer/">深入理解Tagged Pointer · 唐巧的博客</a></li>
<li><a href="https://juejin.cn/post/6844904132940136462">iOS - 老生常谈内存管理（五）：Tagged Pointer - 掘金</a></li>
<li><a href="https://www.infoq.cn/article/r5s0budukwyndafrivh4">iOS内存管理之Tagged Pointer-InfoQ</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Objective-C</tag>
      </tags>
  </entry>
  <entry>
    <title>Runtime：方法调用与对象本质</title>
    <url>/posts/runtime_method_call_and_object_essence/</url>
    <content><![CDATA[<p>方法调用的流程：</p>
<p><code>objc_msgSend</code>调用方法的本质是通过isa指针找到该类，然后寻找方法，找到后调用。如果没有找到则通过<code>superClass</code>找到父类，继续查找方法。</p>
<p>对象结构体中的isa指向类对象。类对象的isa指向元类。元类的isa指向NSObject的元类。</p>
<p>对象方法是保存在类对象的结构体中，所以调用实例方法时，要去类对象中查找。以此类推，类方法也是如此。</p>
<p>实例对象存放isa指针以及示例变量，通过isa指针可以找到实例对象所属的类对象。类中存放着方法列表。方法列表中SEL作为key，IMP作为value。在编译期间，根据方法名字会生成唯一的标识SEL。IMP是指向最终函数实现的函数指针。整个Runtime的核心是<code>objc_msgSend</code>函数，通过给类发送SEL传递消息，找到匹配的IMP再获得最终的实现，并执行方法。</p>
<p><strong>消息发送阶段</strong>：</p>
<ol type="1">
<li>判断receiver是否为空，是则直接返回，否则继续。</li>
<li>从receiverClass的缓存中，查找方法找到则调用方法，否则继续。</li>
<li>从receiverClass的<code>class_rw_t</code>中查找方法，如果找到了则缓存下来，调用方法，否则继续。</li>
<li>去父类的缓存和<code>class_rw_t</code>中查找，步骤同上，找到了则缓存下来，没有则继续往上找父类，都没有则消息发送阶段结束，进入第二阶段：动态方法解析。</li>
</ol>
<p><strong>动态方法解析阶段</strong>：</p>
<p>调用<code>-/+resolveClassMethod:</code>，在方法中调用<code>class_addMethod</code>函数添加SEL对应的方法实现IMP。以上方法中没有处理，则进入第三阶段：消息转发。</p>
<p><strong>消息转发阶段</strong>：</p>
<ol type="1">
<li>判断<code>-/+forwardingTargetForSelector:</code>的返回值，非空则调用<code>objc_msgSend(返回值, SEL)</code>，向返回值发送消息。返回空则继续。</li>
<li>调用<code>-/+methodSignatureForSelector:</code>方法，如果返回不为空，则调用<code>-/+forwardInvocation:</code>方法中处理。若本类无法处理则继续往父类查询。如果返回空，则继续。</li>
<li>调用<code>-doesNotRecognizeSelector:</code>方法。</li>
</ol>
<p>注意：只能对运行时动态创建的类添加成员变量（ivars），不能向已存在的类添加成员变量。因为在编译时只读的class_ro_t结构体就被确定下来，其包含了分配对象的空间大小，在运行时不可改变。</p>
<h3 id="nsproxy">NSProxy</h3>
<p>NSProxy和NSObject是同一层级的，可以理解为NSProxy是一个基类，都遵循了NSObject协议。NSProxy就是专门用来解决重点对象转发的问题。</p>
<p>与NSObject接收消息流程不一样，NSProxy简化了其中的流程：</p>
<ol type="1">
<li><code>[proxyObj message]</code></li>
<li>到proxyObj类对象寻找对应的方法，找到调用。否则继续。</li>
<li>尝试调用<code>resolveClassMethod</code>进行动态方法解析</li>
<li><del>尝试进入父类对象递归查找方法，找到调用。否则继续。</del></li>
<li><del>尝试调用<code>forwardingTargetForSelector</code>进行消息转发。返回空则继续。</del></li>
<li>尝试调用<code>methodSignatureForSelector</code>+<code>forwardInvocation</code>进行消息转发。</li>
</ol>
<p>所以对于处理消息转发，它比NSObject更高效。也阐明了该类的使用方式就是实现消息转发的两个方法即可。</p>
<p>注意，无需调用init方法。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">#import &quot;CLProxy2.h&quot;</span><br><span class="line"></span><br><span class="line">@implementation CLProxy2</span><br><span class="line"></span><br><span class="line">+(instancetype)proxyWithTarget: (id)target &#123;</span><br><span class="line">	// NSProxy对象不需要调用init，因为它本来就没有init方法，直接alloc之后就可以使用</span><br><span class="line">    CLProxy2 *proxy = [CLProxy2 alloc];</span><br><span class="line">    proxy.target = target;</span><br><span class="line">    return proxy;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel &#123;</span><br><span class="line">    return [self.target methodSignatureForSelector:sel];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">-(void)forwardInvocation:(NSInvocation *)invocation &#123;</span><br><span class="line">    invocation.target = self.target;</span><br><span class="line">    [invocation invoke];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">@end</span><br></pre></td></tr></table></figure>
<h2 id="isa">isa</h2>
<p>从arm64架构开始，isa变成了一个共用体（union）结果，使用位域来存储更多信息。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">union</span> <span class="title">isa_t</span> &#123;</span></span><br><span class="line">    <span class="type">isa_t</span>() &#123; &#125;</span><br><span class="line">    <span class="type">isa_t</span>(<span class="type">uintptr_t</span> value) : bits(value) &#123; &#125;</span><br><span class="line"></span><br><span class="line">    Class cls;</span><br><span class="line">    <span class="type">uintptr_t</span> bits;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">        ISA_BITFIELD;  <span class="comment">// defined in isa.h</span></span><br><span class="line">    &#125;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">define ISA_BITFIELD                                                      \</span><br><span class="line">      <span class="type">uintptr_t</span> nonpointer        : <span class="number">1</span>;   <span class="comment">//指针是否优化过                                   \</span></span><br><span class="line"><span class="comment">      uintptr_t has_assoc         : 1;   //是否有设置过关联对象，如果没有，释放时会更快                                   \</span></span><br><span class="line"><span class="comment">      uintptr_t has_cxx_dtor      : 1; 	 //是否有C++的析构函数（.cxx_destruct），如果没有，释放时会更快                                     \</span></span><br><span class="line"><span class="comment">      uintptr_t shiftcls          : 33; //存储着Class、Meta-Class对象的内存地址信息 \</span></span><br><span class="line"><span class="comment">      uintptr_t magic             : 6;  //用于在调试时分辨对象是否未完成初始化                                     \</span></span><br><span class="line"><span class="comment">      uintptr_t weakly_referenced : 1;  //是否有被弱引用指向过，如果没有，释放时会更快                                     \</span></span><br><span class="line"><span class="comment">      uintptr_t deallocating      : 1;  //对象是否正在释放                                     \</span></span><br><span class="line"><span class="comment">      uintptr_t has_sidetable_rc  : 1;  //引用计数器是否过大无法存储在isa中                                     \</span></span><br><span class="line"><span class="comment">      uintptr_t extra_rc          : 19 //里面存储的值是引用计数器减1</span></span><br><span class="line"><span class="meta">#   	<span class="keyword">define</span> RC_ONE   (1ULL&lt;&lt;45)</span></span><br><span class="line"><span class="meta">#   	<span class="keyword">define</span> RC_HALF  (1ULL&lt;&lt;18)</span></span><br></pre></td></tr></table></figure>
<ul>
<li><code>nonpointer</code>：0：普通指针，存储着Class、Meta-Class对象的内存地址；1：优化过，使用位域存储更多的信息。</li>
<li><code>has_assoc</code>：是否有关联过对象。否：释放更快。</li>
<li><code>has_cxx_dtor</code>：是否有C++析构函数（.cxx_destruct）。否：释放更快。</li>
<li><code>shiftcls</code>：存储着Class、Meta-Class对象的内存地址信息。</li>
<li><code>magic</code>：调试时分辨是否完成初始化。</li>
<li><code>weakly_referenced</code>：是否被弱引用指向过。否：释放更快。</li>
<li><code>deallocating</code>：对象是否正在释放。</li>
<li><code>extra_rc</code>：引用计数器-1。</li>
<li><code>has_sidetable_rc</code>：引用计数器是否过大无法存储在isa中。1：引用计数器会存储在SideTable类的属性中。</li>
</ul>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">void</span> *<span class="title function_">objc_destructInstance</span><span class="params">(id obj)</span> </span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (obj) &#123;</span><br><span class="line">        <span class="comment">//是否有C++的析构函数</span></span><br><span class="line">        <span class="type">bool</span> cxx = obj-&gt;hasCxxDtor();</span><br><span class="line">        <span class="comment">//是否有设置过关联对象</span></span><br><span class="line">        <span class="type">bool</span> assoc = obj-&gt;hasAssociatedObjects();</span><br><span class="line">        <span class="comment">//有C++的析构函数，就去销毁</span></span><br><span class="line">        <span class="keyword">if</span> (cxx) object_cxxDestruct(obj);</span><br><span class="line">         <span class="comment">//有设置过关联对象，就去移除管理对象</span></span><br><span class="line">        <span class="keyword">if</span> (assoc) _object_remove_assocations(obj);</span><br><span class="line">        </span><br><span class="line">        obj-&gt;clearDeallocating();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> obj;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="class">class</h2>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/image-20211111175507815.png" alt="image-20211111175507815" /><figcaption aria-hidden="true">image-20211111175507815</figcaption>
</figure>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">objc_object</span> &#123;</span></span><br><span class="line">    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">objc_class</span> :</span> objc_object &#123;</span><br><span class="line">    <span class="comment">// Class ISA;</span></span><br><span class="line">    Class superclass;</span><br><span class="line">    <span class="type">cache_t</span> cache;    <span class="comment">//方法缓存</span></span><br><span class="line">    <span class="type">class_data_bits_t</span> bits;    <span class="comment">// 用于获取具体的类的信息</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>method_array_t、property_array_t、protocol_array_t是可读写的二维数组，包含了类的初始内容、分类内容。</p>
<p>如：method_array_t包含多个一位数组method_list_t，method_list_t里面存放多个method_t，method_t存放在方法imp指针、名称、类型等信息。</p>
<h3 id="method_t">method_t</h3>
<p>方法、函数的封装。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">method_t</span> &#123;</span></span><br><span class="line">    SEL name; <span class="comment">// 函数名</span></span><br><span class="line">    <span class="type">const</span> <span class="type">char</span> *types; <span class="comment">// Type Encoding 编码(返回值类型，参数类型)</span></span><br><span class="line">    MethodListIMP imp; <span class="comment">// 指向函数的指针(函数地址)</span></span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">SortBySELAddress</span> :</span></span><br><span class="line">        public <span class="built_in">std</span>::binary_function&lt;<span class="type">const</span> <span class="type">method_t</span>&amp;,</span><br><span class="line">                                    <span class="type">const</span> <span class="type">method_t</span>&amp;, <span class="type">bool</span>&gt;</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="type">bool</span> <span class="title function_">operator</span><span class="params">()</span> <span class="params">(<span class="type">const</span> <span class="type">method_t</span>&amp; lhs,</span></span><br><span class="line"><span class="params">                         <span class="type">const</span> <span class="type">method_t</span>&amp; rhs)</span></span><br><span class="line">        &#123; <span class="keyword">return</span> lhs.name &lt; rhs.name; &#125;</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>IMP：函数的具体实现。</p>
<p>SEL：方法、函数名，底层结构与<code>char *</code>类似。可以通过<code>@selector()</code>和<code>sel_registerName()</code>获得。可以通过<code>sel_getName()</code>和<code>NSStringFromSelector()</code>转成字符串。名字相同的方法，SEL也是相同的。</p>
<h3 id="class_ro_t">class_ro_t</h3>
<p>描述的是类的初始内容，其中的<code>baseMethodList</code>、<code>baseProtocols</code>、<code>ivars</code>、<code>baseProperties</code>是只读的一维数组。</p>
<h3 id="cache_t">cache_t</h3>
<p>方法缓存。用哈希表缓存调用过的方法，可以提高方法查找速度。</p>
<p>当方法缓存太多的时候，超过了表容量的3/4的时候，就要扩容为原来的2倍。</p>
<h2 id="类的本质">类的本质</h2>
<p>一个NSObject的本质是包含一个isa指针的结构体：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">struct NSObject_IMPL &#123;</span><br><span class="line">	Class isa;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>而其子类是在isa指针的基础上再加上自身的成员变量：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">struct Student_IMPL &#123;</span><br><span class="line">    struct NSObject_IMPL NSObject_IVARS;</span><br><span class="line">    int _age;</span><br><span class="line">    int _no;  </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>所以一个子类的底层结构体是其父类结构体里的所有成员变量 + 子类自身定义的成员变量所组成的结构体。</p>
<p><code>class_getInstanceSize</code>：获取OC类实例对象的实际大小。这个大小可以理解为该实例对象至少需要的空间大小，实际分配大小需要使用<code>malloc_size</code>。</p>
<p><code>malloc_size</code>：得到一个指针指向的内存空间大小，这是系统为这个对象最终分配的内存大小。</p>
<p>所以回到问题：一个NSObject对象占用多少内存？</p>
<ul>
<li>系统分配了16字节（通过<code>malloc_size</code>获取）；</li>
<li>NSObject内部只使用了8个字节来存放isa指针变量。</li>
</ul>
<h2 id="对象的种类">对象的种类</h2>
<p>OC对象主要分为3类：</p>
<ul>
<li>实例对象（instance）</li>
<li>类对象（class）</li>
<li>元类对象（meta-class）</li>
</ul>
<p>实例对象通过类<code>alloc</code>方法创建出来。存放的信息：</p>
<ul>
<li><code>isa</code>指针（指向类）。</li>
<li>成员变量。</li>
</ul>
<p>类对象在内存中是唯一。的类对象用来描述一个实例对象，存放信息：</p>
<ul>
<li><code>isa</code>指针（指向元类）和<code>superclass</code>指针</li>
<li>属性</li>
<li>对象方法</li>
<li>协议</li>
<li>成员变量</li>
</ul>
<p>元类对象在内存中也是唯一的。元类对象用来描述一个类对象，存放信息：</p>
<ul>
<li><code>isa</code>指针和<code>superclass</code>指针（指向该类父类的元类）</li>
<li>类方法</li>
</ul>
<p>类和元类都是objc_class（继承自objc_object），也有isa指针，也是对象。</p>
<p>元类的<code>superclass</code>指向基类的类对象，者决定了：</p>
<p>当我们调用一个类方法时，会通过类的isa指针找到元类，在元类中查找有无该类方法，如果没有则通过<code>superclass</code>逐级查询父元类，一直找到基类的元类，如果还没有，则去找基类中的同名的实例方法实现。</p>
<h2 id="super">super</h2>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span>: <span class="title class_">NSObject</span> &#123;&#125;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Student</span>: <span class="title class_">Person</span> &#123;</span><br><span class="line">    <span class="keyword">override</span> <span class="keyword">init</span>() &#123;</span><br><span class="line">        <span class="keyword">super</span>.<span class="keyword">init</span>()</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;className: <span class="subst">\(<span class="keyword">self</span>.className)</span>, super.className: <span class="subst">\(<span class="keyword">super</span>.className)</span>&quot;</span>)</span><br><span class="line">        <span class="comment">// FoundationSwift.Student, FoundationSwift.Student</span></span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;superclass: <span class="subst">\(<span class="keyword">self</span>.super<span class="keyword">class</span><span class="operator">!</span>)</span>, super.superclass: <span class="subst">\(<span class="keyword">super</span>.super<span class="keyword">class</span><span class="operator">!</span>)</span>&quot;</span>)</span><br><span class="line">        <span class="comment">// superclass: Person, super.superclass: Person</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>从上面发现， super和self调用的结构都是相同的。</p>
<p>super调用方法实际上是调用了<code>objc_msgSendSuper(arg, SEL)</code>函数。重点是第一个参数，其类型是<code>__rw_objc_super</code>：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">//♥️♥️♥️C++中间代码里的定义</span><br><span class="line">struct __rw_objc_super &#123; </span><br><span class="line">	struct objc_object *object; </span><br><span class="line">	struct objc_object *superClass; </span><br><span class="line">	__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) &#123;&#125; </span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">//⚠️⚠️⚠️objc源码中的定义</span><br><span class="line">/// Specifies the superclass of an instance. </span><br><span class="line">struct objc_super &#123;</span><br><span class="line">    /// Specifies an instance of a class.</span><br><span class="line">    __unsafe_unretained _Nonnull id receiver;</span><br><span class="line"></span><br><span class="line">    /// Specifies the particular superclass of the instance to message. </span><br><span class="line">#if !defined(__cplusplus)  &amp;&amp;  !__OBJC2__</span><br><span class="line">    /* For compatibility with old objc-runtime.h header */</span><br><span class="line">    __unsafe_unretained _Nonnull Class class;</span><br><span class="line">#else</span><br><span class="line">    __unsafe_unretained _Nonnull Class super_class;</span><br><span class="line">#endif</span><br><span class="line">    /* super_class is the first class to search */</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>objc_super结构体成员：</p>
<ul>
<li><code>id receiver</code>：消息接收者，实参传递的就是self，即Student对象。</li>
<li><code>Class super_class</code>：父类。</li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">id objc_msgSendSuper(struct objc_super *super, SEL op, ...)</span><br></pre></td></tr></table></figure>
<ul>
<li><code>struct objc_super *super</code>：结构体指针，内容是<code>&#123;消息接收者（recv）， 消息接收者的父类类对象（[[recv superclass] class]）&#125;</code>。<code>objc_msgSendSuper</code>会将消息接收者的父类对象作为消息查找的起点。</li>
<li><code>SEL op</code>：要查找的方法。</li>
</ul>
<p>所以说，调用super与调用self的不同只是super把查找方法的起点改为从父类开始而已，所以像一些父类没有实现，而NSObject基类实现的方法，两者调用结果无异，因为最终的消息接收者还是self，即当前对象。</p>
<p>若要想super和self调用方法结果不一致，必须是当前类和父类都实现了相同的方法，若只有父类实现了，就都是父类的结果。这与一般方法查找父类实现的逻辑一致。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://juejin.cn/post/6844903793138597896">OC对象的本质（上）：OC对象的底层实现原理 - 掘金</a></li>
<li><a href="https://juejin.cn/post/6963522983105134629">OC对象的本质（中）：OC对象的种类 - 掘金</a></li>
<li><a href="https://juejin.cn/post/6963524896672464903">OC对象的本质（下）—— 详解isa&amp;superclass指针 - 掘金</a></li>
<li><a href="https://juejin.cn/post/6844903474908364808">深入理解 Objective-C Runtime 机制 - 掘金</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Objective-C</tag>
        <tag>Runtime</tag>
      </tags>
  </entry>
  <entry>
    <title>Runtime：综合面试题</title>
    <url>/posts/runtime_interview_question/</url>
    <content><![CDATA[<h1 id="runtime综合面试题">Runtime综合面试题</h1>
<h2 id="isa指针">isa指针</h2>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">//***********♦️♦️CLPerson♦️♦️************</span><br><span class="line">@interface CLPerson : NSObject</span><br><span class="line">@property (nonatomic, copy) NSString *name;</span><br><span class="line">-(void)print;</span><br><span class="line">@end</span><br><span class="line"></span><br><span class="line">@implementation CLPerson</span><br><span class="line">-(void)print &#123;</span><br><span class="line">    NSLog(@&quot;My name&#x27;s %@&quot;, self.name);</span><br><span class="line">&#125;</span><br><span class="line">@end</span><br><span class="line"></span><br><span class="line">//***********🥝🥝ViewController.m🥝🥝************ </span><br><span class="line"></span><br><span class="line">@implementation ViewController</span><br><span class="line">- (void)viewDidLoad &#123;</span><br><span class="line">    [super viewDidLoad];</span><br><span class="line">    </span><br><span class="line">    id cls = [CLPerson class];</span><br><span class="line">    void *obj = &amp;cls;</span><br><span class="line">    [(__bridge id)obj print]; </span><br><span class="line">&#125;</span><br><span class="line">@end</span><br></pre></td></tr></table></figure>
<p>最终输出结果：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">My name&#x27;s &lt;ViewController: 0x7fce43e08aa0&gt;</span><br></pre></td></tr></table></figure>
<h3 id="为什么print可以被调用">为什么<code>print</code>可以被调用</h3>
<p>因为：</p>
<ul>
<li>实例对象 = 指向类的指针</li>
<li>cls指向类，obj指向cls，相当于obj是指向类的指针。</li>
</ul>
<p>所以<code>(__bridge id)obj</code>就相当于实例变量的效果。</p>
<h3 id="为什么打印是viewcontroller-0x7fce43e08aa0">为什么打印是<code>&lt;ViewController: 0x7fce43e08aa0&gt;</code></h3>
<p>首先<code>self.name</code>就是通过指针调用的<code>self-&gt;_name</code>。</p>
<p>实例对象底层是一个结构体，存放isa指针和成员变量列表，因为指针在arm64位上占8位，<code>name</code>又是CLPerson的第一个成员，所以<code>self-&gt;_name</code>就是基于对象地址往高地址偏移8位读取的内存。</p>
<p>栈空间是存放被调用函数内部所定义的局部变量的。先定义的局部变量在栈底高地址。所以上述代码的局部变量布局为：</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/vdgI8O.jpg" /></p>
<p>这里隐藏了个细节：<code>[super viewDidLoad];</code>。该代码的底层调用是：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">objc_msgSendSuper(</span><br><span class="line">    (__rw_objc_super)&#123;</span><br><span class="line">        (id)self, </span><br><span class="line">        (id)class_getSuperclass(objc_getClass(&quot;ViewController&quot;))&#125;,</span><br><span class="line">    @selector(viewDidLoad));</span><br></pre></td></tr></table></figure>
<p>相当于cls的高地址方向还有一个self局部变量，就ViewController实例对象，所以<code>self-&gt;_name</code>指向的就是cls的上一个局部变量，即高地址方向偏移8位——ViewController实例对象。</p>
<p>扩展：类似的，如果没有<code>[super viewDidLoad];</code>就会出现BAD_ACCESS错误。如果cls前面多了个OC对象局部变量，则打印该局部变量。注意还是需要前面有个OC对象，否则还是会BAD_ACCESS。</p>
<p>更多扩展：<a href="https://juejin.cn/post/6844904079953526791">iOS探索 isa面试题分析 - 掘金</a></p>
<h2 id="autoreleasepool">autoreleasepool</h2>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">@interface ViewController ()</span><br><span class="line">&#123;</span><br><span class="line">    __weak NSString *string_weak;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">@end</span><br><span class="line"></span><br><span class="line">@implementation ViewController</span><br><span class="line"></span><br><span class="line">- (void)viewDidLoad &#123;</span><br><span class="line">    [super viewDidLoad];</span><br><span class="line">    </span><br><span class="line">    // 各场景</span><br><span class="line">    </span><br><span class="line">    NSLog(@&quot;string: %@ %s&quot;, string_weak,__func__);</span><br><span class="line">&#125;</span><br><span class="line">- (void)viewWillAppear:(BOOL)animated&#123;</span><br><span class="line">    [super viewWillAppear:animated];</span><br><span class="line">    NSLog(@&quot;string: %@ %s&quot;, string_weak,__func__);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (void)viewDidAppear:(BOOL)animated&#123;</span><br><span class="line">    [super viewDidAppear:animated];</span><br><span class="line">    NSLog(@&quot;string: %@ %s&quot;, string_weak,__func__);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="场景一">场景一</h3>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">NSString *str =  [NSString stringWithFormat:@&quot;https://ityongzhen.github.io/&quot;];</span><br><span class="line">string_weak = str;</span><br><span class="line"></span><br><span class="line">// 输出</span><br><span class="line">string: https://ityongzhen.github.io/ -[ViewController viewDidLoad]</span><br><span class="line">string: https://ityongzhen.github.io/ -[ViewController viewWillAppear:]</span><br><span class="line">string: (null) -[ViewController viewDidAppear:]</span><br></pre></td></tr></table></figure>
<ol type="1">
<li>创建对象，ref=1，并添加到当前的autoreleasepool中；</li>
<li>赋值到局部变量，ref+1=2；</li>
<li><code>viewDidLoad</code>方法返回，局部变量被回收，ref-1=1；</li>
<li><code>viewDidLoad</code>和<code>viewWillAppear</code>在同一个RunLoop中，所以还能访问；<code>viewDidLoad</code>已经是下一个RunLoop，已经被释放。</li>
</ol>
<h3 id="场景二">场景二</h3>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">@autoreleasepool &#123;</span><br><span class="line">    NSString *str =  [NSString stringWithFormat:@&quot;https://ityongzhen.github.io/&quot;];</span><br><span class="line">    string_weak = str;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 输出</span><br><span class="line">string: (null) -[ViewController viewDidLoad]</span><br><span class="line">string: (null) -[ViewController viewWillAppear:]</span><br><span class="line">string: (null) -[ViewController viewDidAppear:]</span><br></pre></td></tr></table></figure>
<ol type="1">
<li>创建对象，ref=1；</li>
<li>赋值到局部变量，ref+1=2；</li>
<li>离开作用域域，ref-1=1；</li>
<li>离开autoreleasepool，调用release，ref-1=0，对象释放。</li>
</ol>
<p>所以后序在<code>viewDidLoad</code>方法中访问的对象已经被释放。</p>
<h3 id="场景三">场景三</h3>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">NSString *str = nil;</span><br><span class="line">@autoreleasepool &#123;</span><br><span class="line">    str =  [NSString stringWithFormat:@&quot;https://ityongzhen.github.io/&quot;];</span><br><span class="line">    string_weak = str;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 输出</span><br><span class="line">string: https://ityongzhen.github.io/ -[ViewController viewDidLoad]</span><br><span class="line">string: (null) -[ViewController viewWillAppear:]</span><br><span class="line">string: (null) -[ViewController viewDidAppear:]</span><br></pre></td></tr></table></figure>
<ol type="1">
<li>创建对象，ref=1；</li>
<li>赋值到局部变量，ref+1=2；</li>
<li>离开autoreleasepool，调用release，ref-1=1，对象释放。</li>
<li><code>viewDidLoad</code>方法返回时，ref-1=0，对象被释放。</li>
</ol>
<p>所以在<code>viewDidLoad</code>方法中访问对象时，还能访问，离开方法后就无法访问。</p>
<h3 id="注意">注意</h3>
<ul>
<li>如果字符串过短，会变成存储在栈的TaggedPointer，无需引用计数管理，在所有方法中都可以访问。</li>
<li>类似的，如果字符串位<code>@"..."</code>形式，则存储到常量区，也无需引用计数管理，在所有方法中也都可以访问。</li>
</ul>
<p>对于栈上的内存，会在离开作用域后被回收。</p>
<h2 id="更多">更多</h2>
<ul>
<li><a href="https://juejin.cn/post/6844904072428912653">深入浅出 Runtime（五）：相关面试题 - 掘金</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Objective-C</tag>
        <tag>Runtime</tag>
      </tags>
  </entry>
  <entry>
    <title>Runtime：Category</title>
    <url>/posts/runtime_category/</url>
    <content><![CDATA[<p>如果原来的类和分类中有相同的方法，那么最终执行的是分类方法。</p>
<p>编译完，每个分类都会生成一个category_t结构体，里面存储名称、对象方法列表、类方法列表、协议方法列表、属性列表。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">category_t</span> &#123;</span></span><br><span class="line">    <span class="type">const</span> <span class="type">char</span> *name;</span><br><span class="line">    <span class="type">classref_t</span> cls;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">method_list_t</span> *<span class="title">instanceMethods</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">method_list_t</span> *<span class="title">classMethods</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">protocol_list_t</span> *<span class="title">protocols</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">property_list_t</span> *<span class="title">instanceProperties</span>;</span></span><br><span class="line">    <span class="comment">// Fields below this point are not always present on disk.</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">property_list_t</span> *_<span class="title">classProperties</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="type">method_list_t</span> *<span class="title function_">methodsForMeta</span><span class="params">(<span class="type">bool</span> isMeta)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (isMeta) <span class="keyword">return</span> classMethods;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">return</span> instanceMethods;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">property_list_t</span> *<span class="title function_">propertiesForMeta</span><span class="params">(<span class="type">bool</span> isMeta, <span class="keyword">struct</span> header_info *hi)</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>在合并分类的时候，其方法列表等不会覆盖原来类中的方法，是共存的。但分类的方法在前面，原来类的方法在后面，调用的时候，就会调用分类中的方法，如果多个分类的相同方法，后编译的分类会被调用。</p>
<p>如果想要执行被“覆盖”的类定义方法，可以逆序遍历方法列表，第一次取得的就是类定义的方法：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">- (void)foo&#123;   </span><br><span class="line">  [类 invokeOriginalMethod:self selector:_cmd];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">+ (void)invokeOriginalMethod:(id)target selector:(SEL)selector &#123;</span><br><span class="line">    uint count;</span><br><span class="line">    Method *list = class_copyMethodList([target class], &amp;count);</span><br><span class="line">    for ( int i = count - 1 ; i &gt;= 0; i--) &#123;</span><br><span class="line">        Method method = list[i];</span><br><span class="line">        SEL name = method_getName(method);</span><br><span class="line">        IMP imp = method_getImplementation(method);</span><br><span class="line">        if (name == selector) &#123;</span><br><span class="line">            ((void (*)(id, SEL))imp)(target, name);</span><br><span class="line">            break;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    free(list);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>类对象/元类对象才是最终存储分类实例/类方法、属性、协议的地方。</p>
<h2 id="扩展问题">扩展问题</h2>
<h3 id="category的原本使用场景">Category的原本使用场景</h3>
<p>区分不同的功能模块，使用分类单独实现。</p>
<h3 id="category的实现原理">Category的实现原理</h3>
<p>分类编译后是category_t结构体，里面存储着分类的对象方法、类方法、属性、协议信息，在程序运行时，Runtime会把分类的数据合并到类信息（类对象、元类对象）中。</p>
<h3 id="category与extension的区别">Category与Extension的区别</h3>
<p>Extension在编译的时候，其数据已经包含在类信息中。Category在运行时才会把数据合并到类信息中。</p>
<h3 id="category为什么不能添加成员变量">Category为什么不能添加成员变量</h3>
<p>category_t结构体只能存储属性，但没有存储objc_ivar_list结构体，没有用存储成员变量的地方，所以不能添加成员变量。</p>
<h3 id="load方法的执行"><code>+load</code>方法的执行</h3>
<p>Runtime在加载类和分类的时候，会调用所有的<code>+load</code>方法，即使没有该类还没使用。</p>
<p>调用方式：函数地址直接调用。</p>
<p>调用时机：加载类和分类时调用一次，只会调用一次。</p>
<p><code>+load</code>方法调用顺序：</p>
<ol type="1">
<li>调用类的<code>+load</code>
<ul>
<li>按照编译顺序进行；</li>
<li>先调父类，再调子类；</li>
</ul></li>
<li>按照编译顺序调用分类的<code>+load</code>方法。</li>
</ol>
<p>先去调用类的<code>+load</code>方法，若有父类则先调用父类的<code>+load</code>方法，再去调用分类的<code>+load</code>方法。</p>
<h3 id="initialize方法的执行"><code>+initialize</code>方法的执行</h3>
<p><code>+initialize</code>需要在使用（调用方法）类的时候才会调用。其调用顺序跟普通方法一致，即若有分类实现的<code>+initialize</code>方法，则调用分类的方法。</p>
<p>调用方式：<code>objc_msgSend</code>调用。</p>
<p>调用时机：在类第一次接收到消息时调用，所以父类可能会执行多次（只有父类实现了<code>+initialize</code>方法，而子类没有实现）。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://ityongzhen.github.io/%E8%AF%A6%E8%A7%A3iOS%E4%B8%AD%E5%88%86%E7%B1%BBCateogry.html/">详解iOS中分类Cateogry | 殷永振</a></li>
<li><a href="https://juejin.cn/post/6872205313678163981">iOS底层系列：Category - 掘金</a></li>
<li><a href="https://juejin.cn/post/6963889820363915300">Objective-C之Category的底层实现原理 - 掘金</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Objective-C</tag>
        <tag>Runtime</tag>
      </tags>
  </entry>
  <entry>
    <title>Runtime：关联对象</title>
    <url>/posts/runtime_associative/</url>
    <content><![CDATA[<p>Associative运行时特性可以给两个对象建立关联关系，这是一种从属关系。</p>
<p>相关API：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">// objc/runtime.h</span><br><span class="line">/** 给一个对象设置关联对象</span><br><span class="line">object: 需要添加关联的源对象</span><br><span class="line">key: 关联值的唯一key</span><br><span class="line">value: 关联的具体值</span><br><span class="line">policy: 关联的策略 (类似声明properties的参数)</span><br><span class="line">*/</span><br><span class="line">void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)</span><br><span class="line"></span><br><span class="line">/** 获取某个对象的某个关联对象</span><br><span class="line">object: 需要获取关联的源对象</span><br><span class="line">key: 关联值的唯一key</span><br><span class="line">*/</span><br><span class="line">id objc_getAssociatedObject(id object, const void *key)</span><br><span class="line"></span><br><span class="line">/** 移除对象的关联对象</span><br><span class="line"> object: 需要移除的源对象</span><br><span class="line">*/</span><br><span class="line">void objc_removeAssociatedObjects(id object) </span><br></pre></td></tr></table></figure>
<p>应用场景：</p>
<ul>
<li>给系统类、第三方类添加属性。</li>
<li>使用<code>RETAIN_NONATOMIC</code>关联类型的对象关联目标对象，以监听目标对象的生命周期。</li>
</ul>
<h2 id="实现原理">实现原理</h2>
<p>组成部分：</p>
<ul>
<li>AssociationsManager：管理一个AssociationsHashMap。</li>
<li>AssociationsHashMap：用<code>objc_setAssociatedObject</code>传入的object为基础，进行一些其他操作后作为Key，ObjectAssociationMap为Value。</li>
<li>ObjectAssociationMap：用<code>objc_setAssociatedObject</code>传入的key作为Key，ObjcAssociation为Value。</li>
</ul>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/VDcTrc.jpg" /></p>
<p>对象关联的对象在<code>-dealloc</code>调用的<code>object_dispose</code>函数中释放。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://juejin.cn/post/6955372421931073572">iOS 底层原理03: Category, 关联对象 - 掘金</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Objective-C</tag>
        <tag>Runtime</tag>
      </tags>
  </entry>
  <entry>
    <title>Runtime：weak</title>
    <url>/posts/runtime_weak/</url>
    <content><![CDATA[<p>当一个对象被weak指针指向时，这个weak指针会以对象作为key，存储到SideTable的<code>weak_table</code>哈希表中。</p>
<ul>
<li>Key：对象</li>
<li>Value：weak指针数组</li>
</ul>
<p>当该对象dealloc方法被调用时，Runtime会以该对象为key，从SideTable的<code>weak_table</code>哈希表中，找到对应的weak指针列表，然后吧其中的weak指针逐个置为nil。</p>
<h2 id="底层细节">底层细节</h2>
<p>使用weak修饰的对象，底层调用了<code>objc_initWeak</code>函数。里面获取weak指针地址和对象地址传递到下一层函数存储。最终存储到SideTable中的<code>weak_table</code>哈希表。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">SideTable</span> &#123;</span></span><br><span class="line">    <span class="type">spinlock_t</span> slock; <span class="comment">// 锁</span></span><br><span class="line">    RefcountMap refcnts; <span class="comment">// 指向对象引用计数的哈希表（仅在未开启isa优化或在isa优化下isa_t引用计数溢出时才会用到）</span></span><br><span class="line">    <span class="type">weak_table_t</span> weak_table; <span class="comment">// 存储对象若引用指针的哈希表</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">weak_table_t</span> &#123;</span></span><br><span class="line">    <span class="type">weak_entry_t</span> *weak_entries; <span class="comment">// 用于存储哈希数组</span></span><br><span class="line">    <span class="type">size_t</span>    num_entries;</span><br><span class="line">    <span class="type">uintptr_t</span> mask;</span><br><span class="line">    <span class="type">uintptr_t</span> max_hash_displacement;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/aUBSTV.jpg" alt="aUBSTV" /><figcaption aria-hidden="true">aUBSTV</figcaption>
</figure>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://juejin.cn/post/6844904101839372295">iOS底层原理：weak的实现原理 - 掘金</a></li>
<li></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Objective-C</tag>
        <tag>Runtime</tag>
      </tags>
  </entry>
  <entry>
    <title>iOS 配置认证证书</title>
    <url>/posts/ios_configuring_the_authentication_certificate/</url>
    <content><![CDATA[<ul>
<li>不支持 <code>.crt</code></li>
</ul>
<p>解决：转换成 <code>.cer</code> 格式。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">openssl x509 -<span class="keyword">in</span> xxx.crt -inform PEM -out xxx.cer -outform DER</span><br></pre></td></tr></table></figure>
<ul>
<li>只支持二进制证书，不支持 base64 证书。</li>
</ul>
<p>若用文本编辑器打开的证书是长这样的：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">-----BEGIN CERTIFICATE-----</span><br><span class="line">MIIGCDCCA/CgAw……</span><br><span class="line">-----END CERTIFICATE-----</span><br></pre></td></tr></table></figure>
<p>iOS 不支持 PEM 格式的证书，需要转换成 DER 二进制格式。</p>
<p>原理：将文本中的 base64 String 解 base64，得出的 data 再转 string。</p>
<p>最保险方法：</p>
<p>使用系统的<code>钥匙串访问</code>，导入证书，再导出即可。</p>
<h2 id="使用代码支持-pem-证书">使用代码支持 PEM 证书</h2>
<p>安卓是直接支持 PEM 格式证书，为了兼容 iOS 以及减少证书文件的维护成本，在 iOS 端，可以通过代码从解密后的 PEM 证书中抽取格式支持的证书二进制数据。</p>
<blockquote>
<p>PEM，Privacy Enhanced Mail，一般为文本格式，以 <code>-----BEGIN...</code> 开头，以 <code>-----END...</code> 结尾。中间的内容是 BASE64 编码。这种格式可以保存证书和私钥，有时我们也把PEM 格式的私钥的后缀改为 .key 以区别证书与私钥。</p>
</blockquote>
<p>可见 PEM 证书是个文本，且既然有 BEGIN END 包裹，那么可能会有多个证书。所以可以给 NSData 增加一个扩展：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">@property (nonatomic, strong, readonly) NSArray&lt;NSData *&gt; *tool_pemBins;</span><br></pre></td></tr></table></figure>
<p>实现也很简单，使用正则表达式析出 BEGIN END 包裹的内容，去除换行，然后 BASE64 解码。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">// NSData+Tool</span><br><span class="line">- (NSArray&lt;NSData *&gt; *)tool_pemBins &#123;</span><br><span class="line">    NSMutableArray *array = NSMutableArray.array;</span><br><span class="line">    </span><br><span class="line">    NSString *string = [[NSString alloc] initWithData:self encoding:NSUTF8StringEncoding];</span><br><span class="line">    for (NSString *substring in [string tool_substringsMatchedRx:@&quot;^-*BEGIN \\w*-*$([\\s\\S]*)^-*END \\w*-*$&quot;]) &#123;</span><br><span class="line">        NSString *content = substring;</span><br><span class="line">        content = [content stringByReplacingOccurrencesOfString:@&quot;\n&quot; withString:@&quot;&quot;];</span><br><span class="line">        NSData *data = [[NSData alloc] initWithBase64EncodedString:content options:0];</span><br><span class="line">        [array addObject:data];</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    return array.copy;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// NSString+Tool</span><br><span class="line">- (NSRegularExpression *)tool_rx &#123;</span><br><span class="line">    NSRegularExpressionOptions options = NSRegularExpressionCaseInsensitive | NSRegularExpressionAnchorsMatchLines;</span><br><span class="line">    return [NSRegularExpression regularExpressionWithPattern:self options:options error:NULL];</span><br><span class="line">&#125;</span><br><span class="line">- (NSArray&lt;NSString *&gt; *)tool_substringsMatchedRx:(NSString *)rx &#123;</span><br><span class="line">    NSMutableArray *array = NSMutableArray.array;</span><br><span class="line">    NSArray *results = [rx.tool_rx matchesInString:self options:0 range:NSMakeRange(0, self.length)];</span><br><span class="line">    for (NSTextCheckingResult *result in results) &#123;</span><br><span class="line">        for (int i = 1; i &lt; result.numberOfRanges; i++) &#123;</span><br><span class="line">            NSRange range = [result rangeAtIndex:i];</span><br><span class="line">            if (range.location == NSNotFound || range.length == 0) continue;</span><br><span class="line">            [array addObject:[self substringWithRange:range]];</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return array.copy;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>做这个的时候，时间都花在正则表达式的匹配上了，因为 iOS 的正则表达式跟 sublime text 编辑器的正则表达式搜索有细微的差别，语法似乎也支持得不够全面，因此需要在 iOS 上做不断修整。</p>
<h2 id="参考资料">参考资料</h2>
<ul>
<li><a href="https://www.jianshu.com/p/74465973da5e" class="uri">https://www.jianshu.com/p/74465973da5e</a></li>
<li><a href="https://blog.freessl.cn/ssl-cert-format-introduce/">SSL 证书格式普及，PEM、CER、JKS、PKCS12</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
      </tags>
  </entry>
  <entry>
    <title>使用对象包装实现多代理</title>
    <url>/posts/implement_multiple_proxies_using_object_wrappers/</url>
    <content><![CDATA[<p>多代理的实现方式有很多，如：</p>
<ul>
<li>使用 NSPointerArray 存储 weak delegate；</li>
<li>使用 NSHashTable 存储 weak delegate；</li>
<li>使用 NSProxy 进行转发；</li>
<li>使用 NSObject 封装 target 和 selector，进行遍历调用。</li>
</ul>
<p>本文讨论的是最后一种，但可以结合 NSProxy 进行高效的转发。</p>
<h2 id="实现原理">实现原理</h2>
<p>对象封装 weak target，和 selector，使用数组存储这个封装对象，在回调的地方，遍历数组调用各个代理方法。</p>
<h2 id="封装对象命名">封装对象命名</h2>
<ul>
<li>Delegator</li>
</ul>
<h2 id="结合-nsproxy-便捷消息转发">结合 NSProxy 便捷消息转发</h2>
<p>NSProxy 可以实现消息的转发，具体可见 YYWeakProxy 的实现，其可以实现消息转发以达到，调用 proxy 的方法，直接就是调用 target 的方法。</p>
<p>如果结合了 NSProxy，则可以少一层存储和调用 selector 的逻辑。</p>
<h2 id="qmui-的多代理">QMUI 的多代理</h2>
<p>QMUI 的多代理实现有点巧妙，其使用方法注入，对 delegate 的 setter 和 getter 属性进行修改，变成调用其容器中的多代理方法。</p>
<p>而其容器也进行了封装，具体看 QMUIMultipleDelegates 的实现。</p>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
      </tags>
  </entry>
  <entry>
    <title>扩展名类型判断</title>
    <url>/posts/extension_type_determination/</url>
    <content><![CDATA[<p>在我们使用操作系统，系统通常可以根据文件扩展名/后缀，来判断文件类型，并显示相应图标。那么，iOS 中可以怎样实现呢？ <span id="more"></span></p>
<p>iOS 可以通过 UTI 来进行转换。UTI 是什么呢，用过 Media 相关的框架的同学可能不会陌生。需要了解的同学，可浏览以下资料： - <a href="https://developer.apple.com/library/content/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html">Uniform Type Identifier Concepts</a> - <a href="https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/UniformTypeIdentifier.html">Uniform Type Identifier</a></p>
<p>具体实现代码如下：</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="comment">/// extension -&gt; UTI</span></span><br><span class="line"><span class="built_in">NSString</span> *UTIForExtension(<span class="built_in">NSString</span> *extension) &#123;</span><br><span class="line">    <span class="comment">//Request the UTI via the file extension</span></span><br><span class="line">    <span class="built_in">NSString</span> *theUTI = (__bridge_transfer <span class="built_in">NSString</span> *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge <span class="built_in">CFStringRef</span>)(extension), <span class="literal">NULL</span>);</span><br><span class="line">    <span class="keyword">return</span> theUTI;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/// 匹配 UTI</span></span><br><span class="line"><span class="built_in">BOOL</span> extensionConformToUTI(<span class="built_in">NSString</span> *extension, <span class="built_in">CFStringRef</span> theUTI) &#123;</span><br><span class="line">    <span class="built_in">NSString</span> *preferredUTI = UTIForExtension(extension);</span><br><span class="line">    <span class="keyword">return</span> (UTTypeConformsTo((__bridge <span class="built_in">CFStringRef</span>) preferredUTI, theUTI));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">BOOL</span> extensionLikelyImage(<span class="built_in">NSString</span> *extension) &#123;</span><br><span class="line">    <span class="keyword">return</span> extensionConformToUTI(extension, <span class="built_in">CFSTR</span>(<span class="string">&quot;public.image&quot;</span>));</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">BOOL</span> extensionLikelyAudio(<span class="built_in">NSString</span> *extension) &#123;</span><br><span class="line">    <span class="keyword">return</span> extensionConformToUTI(extension, <span class="built_in">CFSTR</span>(<span class="string">&quot;public.audio&quot;</span>));</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">BOOL</span> extensionLikelyMovie(<span class="built_in">NSString</span> *extension) &#123;</span><br><span class="line">    <span class="keyword">return</span> extensionConformToUTI(extension, <span class="built_in">CFSTR</span>(<span class="string">&quot;public.movie&quot;</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<hr />
<p>🎁彩蛋</p>
<p>顺便的，给出 UTI 与 mimeType 的转换，以及相关的实用函数。</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="comment">/// UTI -&gt; mimeType</span></span><br><span class="line"><span class="built_in">NSString</span> *mimeTypeForUTI(<span class="built_in">NSString</span> *aUTI) &#123;</span><br><span class="line">    <span class="built_in">CFStringRef</span> theUTI = (__bridge <span class="built_in">CFStringRef</span>) aUTI;</span><br><span class="line">    <span class="built_in">CFStringRef</span> mimeType = UTTypeCopyPreferredTagWithClass(theUTI, kUTTagClassMIMEType);</span><br><span class="line">    <span class="keyword">return</span> (__bridge_transfer <span class="built_in">NSString</span> *)mimeType;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/// 元素唯一字典</span></span><br><span class="line"><span class="built_in">NSArray</span> *uniqueArray(<span class="built_in">NSArray</span> *anArray) &#123;</span><br><span class="line">    <span class="built_in">NSMutableArray</span> *copiedArray = [<span class="built_in">NSMutableArray</span> arrayWithArray:anArray];</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">id</span> object <span class="keyword">in</span> anArray)     &#123;</span><br><span class="line">        [copiedArray removeObjectIdenticalTo:object];</span><br><span class="line">        [copiedArray addObject:object];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> copiedArray;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">NSArray</span> *conformanceArray(<span class="built_in">NSString</span> *aUTI) &#123;</span><br><span class="line">    <span class="built_in">NSMutableArray</span> *results = [<span class="built_in">NSMutableArray</span> arrayWithObject:aUTI];</span><br><span class="line">    <span class="built_in">NSDictionary</span> *dictionary = utiDictionary(aUTI);</span><br><span class="line">    <span class="keyword">id</span> conforms = dictionary[(__bridge <span class="built_in">NSString</span> *)kUTTypeConformsToKey];</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// No conformance</span></span><br><span class="line">    <span class="keyword">if</span> (!conforms) <span class="keyword">return</span> results;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Single conformance</span></span><br><span class="line">    <span class="keyword">if</span> ([conforms isKindOfClass:[<span class="built_in">NSString</span> <span class="keyword">class</span>]]) &#123;</span><br><span class="line">        [results addObjectsFromArray:conformanceArray(conforms)];</span><br><span class="line">        <span class="keyword">return</span> uniqueArray(results);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Iterate through multiple conformance</span></span><br><span class="line">    <span class="keyword">if</span> ([conforms isKindOfClass:[<span class="built_in">NSArray</span> <span class="keyword">class</span>]]) &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="built_in">NSString</span> *eachUTI <span class="keyword">in</span> (<span class="built_in">NSArray</span> *) conforms)</span><br><span class="line">            [results addObjectsFromArray:conformanceArray(eachUTI)];</span><br><span class="line">        <span class="keyword">return</span> uniqueArray(results);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Just return the one-item array</span></span><br><span class="line">    <span class="keyword">return</span> results;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">NSArray</span> *allExtensions(<span class="built_in">NSString</span> *aUTI) &#123;</span><br><span class="line">    <span class="built_in">NSMutableArray</span> *results = [<span class="built_in">NSMutableArray</span> array];</span><br><span class="line">    <span class="built_in">NSArray</span> *conformance = conformanceArray(aUTI);</span><br><span class="line">    <span class="keyword">for</span> (<span class="built_in">NSString</span> *eachUTI <span class="keyword">in</span> conformance)     &#123;</span><br><span class="line">        <span class="built_in">NSDictionary</span> *dictionary = utiDictionary(eachUTI);</span><br><span class="line">        <span class="built_in">NSDictionary</span> *extensions = dictionary[(__bridge <span class="built_in">NSString</span> *)kUTTypeTagSpecificationKey];</span><br><span class="line">        <span class="keyword">id</span> fileTypes = extensions[(__bridge <span class="built_in">NSString</span> *)kUTTagClassFilenameExtension];</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> ([fileTypes isKindOfClass:[<span class="built_in">NSArray</span> <span class="keyword">class</span>]])</span><br><span class="line">            [results addObjectsFromArray:(<span class="built_in">NSArray</span> *) fileTypes];</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> ([fileTypes isKindOfClass:[<span class="built_in">NSString</span> <span class="keyword">class</span>]])</span><br><span class="line">            [results addObject:(<span class="built_in">NSString</span> *) fileTypes];</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> uniqueArray(results);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">NSArray</span> *allMIMETypes(<span class="built_in">NSString</span> *aUTI) &#123;</span><br><span class="line">    <span class="built_in">NSMutableArray</span> *results = [<span class="built_in">NSMutableArray</span> array];</span><br><span class="line">    <span class="built_in">NSArray</span> *conformance = conformanceArray(aUTI);</span><br><span class="line">    <span class="keyword">for</span> (<span class="built_in">NSString</span> *eachUTI <span class="keyword">in</span> conformance) &#123;</span><br><span class="line">        <span class="built_in">NSDictionary</span> *dictionary = utiDictionary(eachUTI);</span><br><span class="line">        <span class="built_in">NSDictionary</span> *extensions = dictionary[(__bridge <span class="built_in">NSString</span> *)kUTTypeTagSpecificationKey];</span><br><span class="line">        <span class="keyword">id</span> fileTypes = extensions[(__bridge <span class="built_in">NSString</span> *)kUTTagClassMIMEType];</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> ([fileTypes isKindOfClass:[<span class="built_in">NSArray</span> <span class="keyword">class</span>]])</span><br><span class="line">            [results addObjectsFromArray:(<span class="built_in">NSArray</span> *) fileTypes];</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> ([fileTypes isKindOfClass:[<span class="built_in">NSString</span> <span class="keyword">class</span>]])</span><br><span class="line">            [results addObject:(<span class="built_in">NSString</span> *) fileTypes];</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> uniqueArray(results);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">NSDictionary</span> *utiDictionary(<span class="built_in">NSString</span> *aUTI) &#123;</span><br><span class="line">    <span class="built_in">NSDictionary</span> *dictionary = (__bridge_transfer <span class="built_in">NSDictionary</span> *)UTTypeCopyDeclaration((__bridge <span class="built_in">CFStringRef</span>) aUTI);</span><br><span class="line">    <span class="keyword">return</span> dictionary;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
      </tags>
  </entry>
  <entry>
    <title>获取时间接口对比</title>
    <url>/posts/get_the_time_api/</url>
    <content><![CDATA[<p>NSDate 、CFAbsoluteTimeGetCurrent、CACurrentMediaTime 的区别</p>
<p>框架层：</p>
<ul>
<li>NSDate 属于Foundation</li>
<li>CFAbsoluteTimeGetCurrent() 属于 CoreFoundatio</li>
<li>CACurrentMediaTime() 属于 QuartzCore</li>
</ul>
<p>本质区别：</p>
<p><code>NSDate</code> 或 <code>CFAbsoluteTimeGetCurrent()</code> 返回的时钟时间将会会网络时间同步，从时钟偏移量的角度。</p>
<p><code>mach_absolute_time()</code> 和 <code>CACurrentMediaTime()</code> 是基于内建时钟的，能够更精确更原子化地测量，并且不会因为外部时间变化而变化（例如时区变化、夏时制、秒突变等），但它和系统的 uptime 有关，系统重启后其值会被重置。CACurrentMediaTime 方法获取到的时间，是手机从开机一直到当前所经过的秒数。类似的，CADisplayLink 的时间戳也是使用该概念的时间（HostTime）。</p>
<p>常见用法：</p>
<p>NSDate、CFAbsoluteTimeGetCurrent()常用于日常时间、时间戳的表示，与服务器之间的数据交互 其中 CFAbsoluteTimeGetCurrent() 相当于 <code>[[NSDate data] timeIntervalSinceReferenceDate];</code></p>
<p><strong>CFAbsoluteTimeGetCurrent() 常用于测试代码的效率。</strong></p>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
      </tags>
  </entry>
  <entry>
    <title>静态库与动态库</title>
    <url>/posts/static_lib_vs_and_dynamic_lib/</url>
    <content><![CDATA[<h2 id="object-file">Object File</h2>
<p>object file是个有结构的位元块。这些位元块包含程序代码】准备给Linker和Loader使用的相关信息。</p>
<p>查看object file：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">objdump -macho -section-headers /bin/ls</span><br></pre></td></tr></table></figure>
<p>object file的形式：</p>
<ul>
<li>Relocatable：包含可以在编译时被其他Relocatable链接的代码和数据，以生成Executable。多个Relocatable可被封装成.a（archive）静态库（static library、static archive）。</li>
<li>Executable：可以载入内存的执行的指令集合。链接器会把静态库中的代码给定一个固定的load地址，并包含（copies and relocates）进Executable中。且每个Executable使用静态库都要拷贝一份静态库。</li>
<li>Shared：一种特殊形式的Relocatable，类似动态库。不并入任何Executable，可在多个Executable之间共享。</li>
<li>Bundle：在macOS中长作为插件使用。</li>
</ul>
<h3 id="macos支持的可执行格式">macOS支持的可执行格式</h3>
<table>
<colgroup>
<col style="width: 11%" />
<col style="width: 36%" />
<col style="width: 51%" />
</colgroup>
<thead>
<tr class="header">
<th>可执行格式</th>
<th>magic</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>脚本</td>
<td><code>\x7FELF</code></td>
<td>主要用于 shell 脚本，但是也常用语其他解释器，如 Perl, AWK 等。也就是我们常见的脚本文件中在 <code>#!</code> 标记后的字符串，即为执行命令的指令方式，以文件的 stdin 来传递命令</td>
</tr>
<tr class="even">
<td>通用二进制格式</td>
<td><code>0xcafebabe</code> <code>0xbebafeca</code></td>
<td>包含多种架构支持的二进制格式，只在 macOS 上支持</td>
</tr>
<tr class="odd">
<td>Mach-O</td>
<td><code>0xfeedface</code>（32 位） <code>0xfeedfacf</code>（64 位）</td>
<td>macOS 的原生二进制格式</td>
</tr>
</tbody>
</table>
<h3 id="通用二进制格式">通用二进制格式</h3>
<p>通用二进制格式（Universal Binary、Fat Binary）。Apple提出这个是为了解决一些历史问题。macOS，更确切地说是OS X，最早是基于PPC架构的，后来才移植到Intel架构（OSX Tiger 10.4.7开始），通用二进制格式可以在PPC和x86两种处理器上执行。即，对多架构二进制文件的打包集合文件。</p>
<p>macOS的多架构二进制文件就是适配不同架构的Mach-O文件。</p>
<h3 id="mach-o">Mach-O</h3>
<p>Mach-O（Mach Object File Format）是苹果平台OS上的可执行文件格式，类似于Linux和大部分UNIX的原声格式ELF（Extensible Firmware Interface）。</p>
<h4 id="文件格式">文件格式</h4>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/mach-o.png" alt="mach-o" /><figcaption aria-hidden="true">mach-o</figcaption>
</figure>
<p>Mach-O格式主要由以下3部分组成：</p>
<ul>
<li>Mach-O头（Mach-O Header）：描述了Mach-O的CPU架构、文件类型、加载命令等信息。</li>
<li>加载命令（Load Command）：描述了文件中数据的具体组织结构，不同的数据类型使用不同的加载命令表示。</li>
<li>数据（Data）：存储每个段（Segment）的数据。段拥有一个或多个Section，存储数据和代码，与ELF文件中的段类似。</li>
</ul>
<h3 id="参考">参考</h3>
<ul>
<li><a href="https://www.desgard.com/iOS-Source-Probe/C/mach-o/Mach-O%20%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F%E6%8E%A2%E7%B4%A2.html">Mach-O 文件格式探索 · GitBook</a></li>
<li><a href="http://satanwoo.github.io/2017/06/13/Macho-1/">深入剖析Macho (1) | SatanWoo</a></li>
<li><a href="https://objccn.io/issue-6-3/">ObjC 中国 - Mach-O 可执行文件</a></li>
</ul>
<h2 id="静态库">静态库</h2>
<p>静态库（Static Libraries），多个目标文件（object file）的打包集合。</p>
<p>特点：</p>
<ul>
<li>静态库会直接嵌入到App的Mach-O中。</li>
<li>编译时已经链接，启动时不需要二次查找。因此App启动更快。</li>
<li>每个Executable使用都要拷贝一份静态库。</li>
</ul>
<figure>
<img src="https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/art/address_space1_2x.png" alt="图1 使用静态库的应用" /><figcaption aria-hidden="true">图1 使用静态库的应用</figcaption>
</figure>
<h3 id="构建设置">构建设置</h3>
<ul>
<li>Linking-Math-O Type: Static Library</li>
<li>Dead Code Stripping: No</li>
</ul>
<h2 id="动态库">动态库</h2>
<p>动态库（动态链接库、Dynamic Libraries、Shared Library、Shared Object），同样也是目标文件的集合，与静态库区别的是嵌入App的方式和在App加载的方式。</p>
<p>特点：</p>
<ul>
<li>以独立文件嵌入App包中。</li>
<li>App的Mach-O中只包含其引用信息，使用的时候才进行动态链接和加载。可在两个时机载入，并动态分配一段地址：
<ul>
<li>App载入时（load time）：启动时加载，称为动态链接库。</li>
<li>App运行时（run time）：启动后加载，称为动态加载库。</li>
</ul></li>
<li>多个Executable使用都不会进行拷贝。可独立更新。</li>
</ul>
<p>只有系统库或在macOS上的动态库才有以上自由选择载入时机的特性，在iOS中，只能通过Embedding Frameworks的方式使用动态库，并在启动时载入与链接动态库。而其链接动态库也是造成启动时间长的原因。</p>
<p>在iOS的多个Executable可以是App和Extension，他们可以共用包中的framework。</p>
<p>列出所有动态链接的库：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">otool -L &lt;PathToArchive&gt;/Products/Applications/&lt;AppName&gt;.app/&lt;AppBinary&gt;</span><br></pre></td></tr></table></figure>
<figure>
<img src="https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/art/address_space2_2x.png" alt="图2 使用动态库的应用程序" /><figcaption aria-hidden="true">图2 使用动态库的应用程序</figcaption>
</figure>
<h2 id="区分">区分</h2>
<p>使用file命令输出对应的Mach-O信息：</p>
<p>静态库：current ar <strong>archive</strong> random library</p>
<p>动态库：<strong>dynamically linked shared library</strong></p>
<h2 id="性能差异">性能差异</h2>
<p>待定。</p>
<h2 id="framework">framework</h2>
<p>framework时一个有着特定结构的文件夹，里面包含各种共享的资源。如：静态库/动态库、头文件、模块信息和资源（例如storyboard、xib、图像文件和本地化字符串）。</p>
<p>其中framework里面的object file类型决定了其可用的资源：</p>
<p>静态库：只能使用其中的头文件、模块信息。</p>
<p>动态库：可全部使用，即除了头文件、模块信息，还能嵌入资源。</p>
<h3 id="集成方式">集成方式</h3>
<p>集成到App时有以下两个选项：</p>
<ul>
<li>Linked：仅链接。启动时链接则勾选，否则要运行时才链接则不勾选。</li>
<li>Embedded：拷贝到App包中的framework目录。</li>
</ul>
<p>对于静态库和动态库framework有不同的选择：</p>
<p>静态库：Linked。因为静态库已经拷贝到App的Executable Mach-O文件中，Embed是没意义的，虽然Xcode允许这样做。</p>
<p>动态库：Linked（iOS可选，macOS必选） + Embedded</p>
<h3 id="动态更新动态库">动态更新动态库</h3>
<p>这里的动态更新是针对于已编译的包的动态更新，不是运行时的动态更新。</p>
<p>首先对于iOS，上App Store的App是不允许动态更新动态库的，因为在iOS中使用动态库只能通过framework形式，而上App Store会进行签名，其中就包含对framework的哈希，即上架后，就不允许改变其framework。而动态更新framework的方式可以在in house和develop模式下使用。</p>
<p><a href="iOS%20利用%20Framework%20进行动态更新.md">iOS 利用 Framework 进行动态更新.md</a></p>
<h2 id="cocoapods中的使用">CocoaPods中的使用</h2>
<p>Podfile：</p>
<ul>
<li><code>use_frameworks!</code>：当前范围使用framework。可以指定动态库、静态库。
<ul>
<li><code>use_frameworks! :linkage =&gt; :dynamic</code></li>
<li><code>use_frameworks! :linkage =&gt; :static</code></li>
</ul></li>
</ul>
<p>Podspec：</p>
<ul>
<li><code>spec.static_framework = true</code>：当使用<code>use_frameworks!</code>标记时，使用静态库framework。</li>
<li>引用系统库：
<ul>
<li><code>spec.frameworks = 'QuartzCore', 'CoreData'</code></li>
<li><code>spec.libraries = 'xml2', 'z'</code></li>
</ul></li>
<li>引用外部库：
<ul>
<li><code>spec.vendored_frameworks = 'MyFramework.framework', 'TheirFramework.framework'</code></li>
<li><code>spec.vendored_libraries = 'libProj4.a', 'libJavaScriptCore.a'</code></li>
</ul></li>
</ul>
<h2 id="动态库巧用">动态库巧用</h2>
<h3 id="减少静态库的依赖拷贝">减少静态库的依赖拷贝</h3>
<p>通过前面我们知道可执文件（主程序或者动态库）在构建的链接阶段，<strong>遇到静态库，吸附进来；遇到动态库，打标记，彼此保持独立。</strong></p>
<p>正因为动态库是保持独立的，那么可以自定义一个动态库把依赖的静态库吸附进来。<strong>对外整体呈现的是动态库特性。其他的组件依赖我们自定义的动态库，由于隔离性的存在，不会出现问题。</strong></p>
<blockquote>
<p>这个思路在处理项目组件化的时候非常有用，尤其是在使用Swift的项目中。</p>
</blockquote>
<h3 id="处理静态库之间的符号冲突">处理静态库之间的符号冲突</h3>
<p>背景：需要知道，在打包IPA的时候，最终静态库会被连接到最终的那个可执行文件中。所以如果多个静态库拥有了相同的符号必定会产生符号冲突。</p>
<p>静态库的符号和动态库库符号可以隔离，进而避免了链接时产生的符号冲突。</p>
<blockquote>
<p>这一点在处理一些由于底层三方库源码不能手动修改（比如boringssl与openssl）的时候，非常有用。</p>
</blockquote>
<h2 id="参考-1">参考</h2>
<ul>
<li><a href="翻译-在应用程序中嵌入framework.md">翻译-在应用程序中嵌入framework.md</a></li>
<li><a href="年轻人，听说你想使用Framework.md">年轻人，听说你想使用Framework.md</a></li>
<li><a href="https://www.jianshu.com/p/4e0fd0214152">iOS动态库、静态库及使用场景、方式 - 简书</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
      </tags>
  </entry>
  <entry>
    <title>Audio Queue Services Programming Guide</title>
    <url>/posts/audio_queue_services_pg_introduction/</url>
    <content><![CDATA[<h1 id="介绍">介绍</h1>
<p>本文档介绍了如何使用音频队列服务（Audio Queue Services），这是Core Audio的Audio Toolbox框架中的一个C语言编程接口。</p>
<span id="more"></span>
<h2 id="什么是音频队列服务">什么是音频队列服务</h2>
<p>在iOS和Mac OS X中，音频队列服务提供了一种直接、低开销的的方式来录制和播放音频。这也是向iOS和Mac OS X程序中添加录制和播放功能所推荐使用的技术。</p>
<p>音频队列服务允许你录制和播放以下格式的音频：</p>
<ul>
<li>Linear PCM（线性PCM）。</li>
<li>任何你正在进行开发的苹果平台所原生支持的压缩格式。</li>
<li>任何用户已经安装相应编码器的其他格式。</li>
</ul>
<p>音频队列服务是高级的。它让程序使用录音和播放设备（比如麦克风和扬声器）而不需要了解硬件接口的知识。也可以让你使用复杂的编码器而不用了解编码器的工作机制。</p>
<p>同时，音频队列服务也支持一些高级功能。提供了高精度的时间控制来支持播放进度和同步。你可以使用它来同步多个音频队列以及让视频和音频同步。</p>
<blockquote>
<p><strong>注意：</strong>音频队列服务提供了一些类似于之前在Mac OS X中Sound Manager提供的功能，它附加了例如同步的功能，Sound Manager在Mac OS X10.5中已经废弃了，并且不能和64位程序一起工作，苹果建议新的Mac OS X程序使用音频队列服务并将旧的程序用音频队列服务来替换Sound Manager。</p>
</blockquote>
<p>音频队列服务是纯C接口的，你可以把它使用在Cocoa和Mac OS X命令行工具中，为了使你更加专注于音频队列服务，本文档中的示例代码通过使用Core Audio SDK中的C++类进行了简化，然而，无论是这个SDK还是C++语言都不是使用音频队列服务所必需的。</p>
]]></content>
      <categories>
        <category>翻译</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Audio Queue Services Programming Guide</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>Audio Queue Services Programming Guide：关于音频队列</title>
    <url>/posts/audio_queue_services_pg_about_audio_queues/</url>
    <content><![CDATA[<p>本章将学习到音频队列的功能、架构和内部工作原理。本文介绍音频队列用来播放或录制所用的音频队列（audio queues）、音频队列缓冲区（audio queue buffers）和回调函数，你还可以找到关于音频队列状态和参数的信息，截至到本章的结尾，你将会获得有效使用该技术的概念性理解。</p>
<span id="more"></span>
<h2 id="什么是音频队列">什么是音频队列？</h2>
<p>在iOS和Mac OS X中，<strong>音频队列</strong>是一个用来录制和播放音频的软件对象，使用<code>AudioQueueRef</code>不透明数据类型来表示（在<code>AudioQueue.h</code>头文件中声明）。</p>
<p>音频队列完成以下工作：</p>
<ul>
<li>连接音频硬件</li>
<li>内存管理</li>
<li>根据需要为已压缩的音频格式引入编码器</li>
<li>媒体的录制或播放</li>
</ul>
<p>你可以将音频队列配合其他Core Audio的接口使用，再加上相对少量的自定义代码就可以在程序中创建一套完整的数字音频录制或播放解决方案。</p>
<h3 id="音频队列架构">音频队列架构</h3>
<p>所有的音频队列都含有相同的基础结构，包含以下几部分：</p>
<ul>
<li>一组<strong>音频队列缓冲区（audio queue buffers）</strong>，每个音频队列缓冲区都是一个存储音频数据的临时仓库。</li>
<li>一个<strong>缓冲区队列（buffer queue）</strong>，一个包含音频队列缓冲区的有序列表。</li>
<li>一个你自己编写的<strong>音频队列回调函数（audio queue callback）</strong>。</li>
</ul>
<p>架构很大程度上依赖于这个音频队列是用来录制还是用来播放的。不同之处在于音频队列如何连接到它的输入和输入，还有它的回调函数所扮演的角色。</p>
<h4 id="用来录制的音频队列">用来录制的音频队列</h4>
<p>用于录制的的音频队列，使用<a href="https://developer.apple.com/documentation/audiotoolbox/1501687-audioqueuenewinput"><code>AudioQueueNewInput</code></a>函数创建，如图1-1的结构。</p>
<p><strong>图1-1</strong> 用于录制的的音频队列</p>
<figure>
<img src="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Art/recording_architecture_2x.png" alt="Architecture for a recording audio queue" /><figcaption aria-hidden="true">Architecture for a recording audio queue</figcaption>
</figure>
<p>用于录制的音频队列的输入端一般连接到外部的音频硬件上，比如说麦克风。在iOS中，音频来自于由用户连接的设备：内置的麦克风或者耳机麦克风，如在Mac OS X下，音频来自于由用户在系统首选项中设置的系统默认音频输入设备。</p>
<p>用于录制的音频队列的输入端利用了你自己写的回调函数，当录制音频到磁盘上的时候，回调函数将存有从音频队列中接收到的新的音频数据的缓冲区写入到音频文件中。然而，用于录制的音频队列也可以用其他方法来使用。你也可以使用其中一种，比如说，在一个实时的分析仪中，在这种情况下，你的回调函数会直接向程序提供音频数据，而不是将它写入磁盘。</p>
<p>更多关于该回调的知识，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW10">The Recording Audio Queue Callback Function</a>。</p>
<p>每一个音频队列，无论是用于录制还是用于播放，都有一个或多个音频队列缓冲区。这些缓冲区排列在一个特殊的被称为缓冲区队列（buffer queue）的序列中。如图所示，音频队列缓冲区是按照他们被填充的顺序编号的——这也是和把他们交付给回调函数的顺序是相同的。有关音频队列是如何使用缓冲区，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW9">The Buffer Queue and Enqueuing</a>。</p>
<h4 id="用于播放的音频队列">用于播放的音频队列</h4>
<p>用于播放的音频队列，使用<a href="https://developer.apple.com/documentation/audiotoolbox/1503207-audioqueuenewoutput"><code>AudioQueueNewOutput</code></a>函数创建，如图1-2结构。</p>
<p><strong>图1-2</strong> 用于播放的音频队列</p>
<figure>
<img src="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Art/playback_architecture_2x.png" alt="Architecture for a playback audio queue" /><figcaption aria-hidden="true">Architecture for a playback audio queue</figcaption>
</figure>
<p>在用于播放的音频队列中，回调函数是在输入端的，这个回调函数的职责就是从磁盘（或其他来源）中获取音频数据，然后将它交付给音频队列。当没有更多音频数据需要播放的时候告诉音频队列停止。更多关于这个回调函数的知识，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW11">The Playback Audio Queue Callback Function</a>。</p>
<p>用于播放的音频队列的输出端一般都是连接到外部的音频设备的，比如说扬声器。在iOS中，音频通过用户选择的设备播放，如接收者是耳机。在Mac OS X中，默认情况下，音频会通过用户在系统首选项中设置的默认音频输出设备中输出。</p>
<h3 id="音频队列缓冲区">音频队列缓冲区</h3>
<p><strong>音频队列缓冲区（audio queue buffer）</strong>是一个<a href="https://developer.apple.com/documentation/audiotoolbox/audioqueuebuffer"><code>AudioQueueBuffer</code></a>类型的数据结构（在<code>AudioQueue.h</code>头文件中声明）：</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="keyword">struct</span> AudioQueueBuffer &#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="built_in">UInt32</span>   mAudioDataBytesCapacity;</span><br><span class="line">    <span class="keyword">void</span> *<span class="keyword">const</span>    mAudioData;</span><br><span class="line">    <span class="built_in">UInt32</span>         mAudioDataByteSize;</span><br><span class="line">    <span class="keyword">void</span>           *mUserData;</span><br><span class="line">&#125; AudioQueueBuffer;</span><br><span class="line"><span class="keyword">typedef</span> AudioQueueBuffer *AudioQueueBufferRef;</span><br></pre></td></tr></table></figure>
<p>上述代码中的<code>mAudioData</code>字段，指向了缓冲区本身：一个用来当作暂时存放录制或播放音频数据的容器的内存，其他字段中的数据用来辅助音频队列管理这个缓冲区。</p>
<p>音频队列可以使用任意数量的缓冲区。一般情况下设置为3，这样就可以让一个缓冲区忙于将数据写入磁盘，同时另一个缓冲区在填充新的音频数据，第三个缓冲区在需要做磁盘I/O延迟补偿的时候使用。图1-3展示了这个过程。</p>
<p>音频队列负责对它的缓冲区进行内存管理：</p>
<ul>
<li>当调用<a href="https://developer.apple.com/documentation/audiotoolbox/1502248-audioqueueallocatebuffer"><code>AudioQueueAllocateBuffer</code></a>函数的时，音频队列创建一个缓冲区。</li>
<li>当通过调用<a href="https://developer.apple.com/documentation/audiotoolbox/1502229-audioqueuedispose"><code>AudioQueueDispose</code></a>函数释放一个音频队列的时，这个音频队列释放掉它拥有的缓冲区。</li>
</ul>
<p>这提高了添加到程序中录制和播放功能的健壮性。同时它也帮助你优化资源的使用。</p>
<p>关于AudioQueueBuffer数据结构的完整描述，参阅_<a href="https://developer.apple.com/documentation/audiotoolbox/audio_queue_services">Audio Queue Services Reference</a>。_</p>
<h3 id="缓冲区队列和入队">缓冲区队列和入队</h3>
<p>传递给音频队列的缓冲区队列，顾名思义就是音频队列服务（Audio Queue Services），在<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW12">Audio Queue Architecture</a>中，将提及缓冲区队列，一个缓冲区的有序列表，其中描述了音频队列对象如何配合回调函数在录制或播放的过程中管理缓冲区队列。尤其是<strong>入队</strong>音频队列，即缓冲区队列对音频队列缓冲区的附加操作。无论是在实现录制或者播放，入队都是你在回调函数中需要执行的任务。</p>
<h4 id="录制过程">录制过程</h4>
<p>当进行录制时，一个音频队列缓冲区填充了从输入设备（如麦克风）中获取的音频数据。缓冲区队列中的其他缓冲区将在当前缓冲区的末尾依次排队等待填充音频数据。</p>
<p>音频队列将按照缓冲区填充的顺序把已填充过音频数据的缓冲区交付给你的回调函数。图1-3展示了当使用音频队列录制时的过程。</p>
<p><strong>图1-3</strong> 录制过程</p>
<figure>
<img src="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Art/recording_callback_function_2x.png" alt="Illustration of the recording process when using an audio queue" /><figcaption aria-hidden="true">Illustration of the recording process when using an audio queue</figcaption>
</figure>
<ol type="1">
<li>录制开始，音频队列用获取的数据填充缓冲区。</li>
<li>第一个缓冲区填充完毕，音频队列调用回调函数来处理这个被填充满的缓冲区（缓冲区一）。</li>
<li>回调函数将缓冲区的内容写到音频文件中。同时，音频队列将另一个缓冲区（缓冲区二）填充新获取的数据。</li>
<li>回调函数将刚刚写入磁盘的缓冲区（缓冲区一）入队，使它重新重新回到被填充的队列。</li>
<li>音频队列再一次调用回调函数，处理下一个填充完毕的缓冲区（缓冲区二）。</li>
<li>回调函数将这个缓冲区的内容写入到音频文件。</li>
</ol>
<p>这种稳定状态会一直持续到用户停止录制。</p>
<h4 id="播放过程">播放过程</h4>
<p>当进行播放的时候，音频队列缓冲区将被传送到输出设备（如扬声器）。缓冲区队列中其他的缓冲区讲按顺序排在当前缓冲区末尾等待播放。</p>
<p>音频队列将已经播放过的音频数据按照他们播放的顺序交付给你的回调函数，回调函数将新的音频数据读取到一个缓冲区中，然后将它入队。图1-4展示了当使用音频队列播放时的过程。</p>
<p><strong>图1-4</strong> 播放过程</p>
<figure>
<img src="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Art/playback_callback_function_2x.png" alt="Illustration of the playback process when using an audio queue" /><figcaption aria-hidden="true">Illustration of the playback process when using an audio queue</figcaption>
</figure>
<ol type="1">
<li>程序启动用于播放的音频队列，程序对每一个音频队列缓冲区调用回调函数，填充这些缓冲区并且将它们加入缓冲区队列。</li>
<li>启动操作会确保当程序调用<a href="https://developer.apple.com/documentation/audiotoolbox/1502689-audioqueuestart"><code>AudioQueueStart</code></a>函数之后，播放可以立即执行。</li>
<li>音频队列将第一个缓冲区（缓冲区一）交付给输出设备。当第一个缓冲区被播放完毕之后，用于播放的音频队列就进入了一个稳定的循环状态。</li>
<li>音频队列开始播放下一个缓冲区（缓冲区二）。</li>
<li>调用回调函数，处理刚刚播放完的那个缓冲区（缓冲区一）。</li>
<li>这个回调函数从音频文件中读取数据填充缓冲区然后入队播放。</li>
</ol>
<h4 id="控制播放过程">控制播放过程</h4>
<p>音频队列缓冲区总是按照他们入队的顺序进行播放，然而，在播放过程中，音频队列服务提供了<a href="https://developer.apple.com/documentation/audiotoolbox/1503258-audioqueueenqueuebufferwithparam"><code>AudioQueueEnqueueBufferWithParameters</code></a>函数来进行一些控制，这个函数有以下功能：</p>
<ul>
<li>设置缓冲区的精确播放时间，这可以实现音频同步。</li>
<li>截断音频队列缓冲区开头或结尾的帧，这可以让你去除开头或结尾的静音。</li>
<li>在缓冲区的粒度上设置播放增益。</li>
</ul>
<p>关于更多播放增益的信息，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW15">Audio Queue Parameters</a>，如果要了解对<code>AudioQueueEnqueueBufferWithParameters</code>函数的完整描述，参阅_<a href="https://developer.apple.com/documentation/audiotoolbox/audio_queue_services">Audio Queue Services Reference</a>_。</p>
<h3 id="音频队列回调函数">音频队列回调函数</h3>
<p>一般来说，使用音频队列服务的大部分编程任务都在编程音频队列回调函数上。</p>
<p>在录制或播放过程中，音频队列将反复调用它所拥有的音频队列回调函数。调用的时间间隔取决于音频队列缓冲区的容量，一般来一说这个时间在半秒到几秒。</p>
<p>无论对于录制或者播放，音频队列回调的一个职责就是返回一个缓冲区队列的音频队列缓冲区。回调函数使用<a href="https://developer.apple.com/documentation/audiotoolbox/1502779-audioqueueenqueuebuffer"><code>AudioQueueEnqueueBuffer</code></a>函数将一个缓冲区加入到缓冲区队列的末尾。对于播放来说，你也可以使用<a href="https://developer.apple.com/documentation/audiotoolbox/1503258-audioqueueenqueuebufferwithparam"><code>AudioQueueEnqueueBufferWithParameters</code></a>函数来获得更多的控制。</p>
<h4 id="用于录制的音频队列的回调函数">用于录制的音频队列的回调函数</h4>
<p>本节介绍了一般情况下（将音频录制到磁盘上）的回调函数。以下是用于录制的回调函数的原型（在<code>AudioQueue.h</code>头文件中声明）：</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AudioQueueInputCallback (</span><br><span class="line">    <span class="keyword">void</span>                               *inUserData,</span><br><span class="line">    AudioQueueRef                      inAQ,</span><br><span class="line">    AudioQueueBufferRef                inBuffer,</span><br><span class="line">    <span class="keyword">const</span> AudioTimeStamp               *inStartTime,</span><br><span class="line">    <span class="built_in">UInt32</span>                             inNumberPacketDescriptions,</span><br><span class="line">    <span class="keyword">const</span> AudioStreamPacketDescription *inPacketDescs</span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>用于录制的音频队列，在调用回调函数的时候，提供了把下一组音频数据写入到文件的一切信息：</p>
<ul>
<li><code>inUserData</code>：通常是一个用来保存音频队列和它的缓冲区状态信息的自定义结构，或者一个音频文件对象 （<code>AudioFileID</code>类型）表示正在写入的文件，或者该文件的音频格式信息。</li>
<li><code>inAQ</code>：是调用回调函数的音频队列。</li>
<li><code>inBuffer</code>：是一个被音频队列填充新的音频数据的音频队列缓冲区，它包含了回调函数写入文件所需要的新数据。数据已经根据你在自己指定的自定义结构（由<code>inUserData</code>参数传入）中指定的格式格式化。更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW14">Using Codecs and Audio Data Formats</a>。</li>
<li><code>inStartTime</code>：是缓冲区中的首个采样的参考时间，对于基本的录制，你的回调函数不会使用这个参数。</li>
<li><code>inNumberPacketDescriptions</code>：是<code>inPacketDescs</code>参数中包描述符（packet descriptions）的数量，如果你正在录制一个VBR（可变比特率（variable bitrate））格式，音频队列将回调该参数给你，这个参数可以让你传递给<a href="https://developer.apple.com/documentation/audiotoolbox/1502135-audiofilewritepackets"><code>AudioFileWritePackets</code></a>函数。CBR（常量比特率（constant bitrate）） 格式不使用包描述。对于CBR录制，音频队列会设置这个参数并且将<code>inPacketDescs</code>这个参数设置为<code>NULL</code>。</li>
<li><code>inPacketDescs</code>：是一组对应于缓冲区中采样的包描述符，音频队列提供了这个参数的值，如果音频文件是VBR格式的，回调函数可以将这个值传递给<code>AudioFileWritePackets</code>函数（在<code>AudioFile.h</code>头文件中声明）。</li>
</ul>
<p>如果要了解更多关于用于录制的回调函数的信息，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW1">Recording Audio</a>和<a href="https://developer.apple.com/documentation/audiotoolbox/audio_queue_services">Audio Queue Services Reference</a>。</p>
<h4 id="用于播放的音频队列的回调函数">用于播放的音频队列的回调函数</h4>
<p>本节介绍了一般情况下（从磁盘文件播放音频的回调函数。 下面是用于播放的回调函数的原型（在<code>AudioQueue.h</code>头文件中声明）：</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AudioQueueOutputCallback (</span><br><span class="line">    <span class="keyword">void</span>                  *inUserData,</span><br><span class="line">    AudioQueueRef         inAQ,</span><br><span class="line">    AudioQueueBufferRef   inBuffer</span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>用于播放的音频队列，在调用回调函数的时候，提供了从文件读取下一组音频数据所需的信息：</p>
<ul>
<li><p><code>inUserData</code>：一般来说是一个你创建的包含音频队列和它的缓冲区的的状态信息的自定义结构；或者一个音频文件对象 （<code>AudioFileID</code>类型）表示要写入的文件；或者文件的音频数据格式信息。 在播放音频队列的情况下，回调函数会在这个结构体中用一个字段保持对当前包的索引。</p></li>
<li><p><code>inAQ</code>：调用这个回调函数的音频队列。</p></li>
<li><p><code>inBuffer</code>：一个音频队列缓冲区，由音频队列提供，回调将填充从正在播放的文件中读取的下一组数据。</p></li>
</ul>
<p>如果程序在播放VBR数据，回调函数需要得到正在播放的音频数据的包数据，它通过调用<a href="https://developer.apple.com/documentation/audiotoolbox/1503274-audiofilereadpackets"><code>AudioFileReadPackets</code></a>函数来实现，这个函数声明于<code>AudioFile.h</code>头文件，回调函数随后把包信息放到自定义的数据结构中，以供音频队列使用。</p>
<p>关于播放回调的更多信息，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW1">Playing Audio</a>和<a href="https://developer.apple.com/documentation/audiotoolbox/audio_queue_services">Audio Queue Services Reference</a>。</p>
<h2 id="使用编码和音频数据格式">使用编码和音频数据格式</h2>
<p>音频队列服务根据采用的编解码器在音频格式之间进行转换。录制或播放程序可以使用任意已经安装过相应编码器的格式，不需要写自定义的代码来处理各种音频格式。尤其是你的回调函数不需要知道其数据格式。</p>
<p>每个音频队列在<code>AudioStreamBasicDescription</code>结构体中都有一个字段表示音频数据格式。当你在<code>mFormatID</code>字段中指定格式时，音频队列会使用相应的解码器。然后指定采样率和声道数，这些就是所有你需要做的。设置音频数据格式的示例，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW1">Recording Audio</a>和<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW1">Playing Audio</a>。</p>
<p>用于录制的音频队列按照图1-5中的流程使用已安装的编码器。</p>
<p><strong>图1-5</strong> 在录制音频的时候进行音频格式转换</p>
<figure>
<img src="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Art/recording_codec_2x.png" alt="Using a code when recording with an audio queue" /><figcaption aria-hidden="true">Using a code when recording with an audio queue</figcaption>
</figure>
<ol type="1">
<li>程序告诉音频队列开始录制，同时也告诉它所要使用的音频格式。</li>
<li>音频队列获取新的音频数据，并且根据你指定的格式使用相应的编码器转换音频数据。然后音频队列调用回调函数，将适当的格式化过的音频数据放进缓冲区中。</li>
<li>回调函数将格式化后的音频数据写入磁盘。回调函数不需要知道数据格式。</li>
</ol>
<p>用于播放的音频队列按照图1-6的流程使用已安装的编码器。</p>
<p><strong>图1-6</strong> 在播放过程中进行音频格式转换</p>
<figure>
<img src="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Art/playback_codec_2x.png" alt="Using a codec when playing a file with an audio queue" /><figcaption aria-hidden="true">Using a codec when playing a file with an audio queue</figcaption>
</figure>
<ol type="1">
<li>程序告诉音频队列开始播放，同时也告诉了它将要播放放的音频文件的数据格式。</li>
<li>音频队列调用回调函数来从音频文件中读取音频数据。回调函数按照它的原始格式将音频数据交付给音频队列。</li>
<li>音频队列使用对应的解码器将音频交付给目标输出设备。</li>
</ol>
<p>音频队列可以使用任意已安装的编码器，无论是Mac OS X原生的还是第三方的。你可以通过指定音频队列的<code>AudioStreamBasicDescription</code>结构体中四字节编码ID来指定将要使用的编码器。该字段的使用示例，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW1">Recording Audio</a>。</p>
<p>Mac OS X包含大量的编码器，在<code>CoreAudioTypes.h</code>头文件中的format IDs枚举值中列出，并且记录在_<a href="https://developer.apple.com/documentation/coreaudio/core_audio_data_types">Core Audio Data Types Reference</a>_中。你可以使用Audio Toolbox框架中<code>AudioFormat.h</code>头文件中的接口来查询当前系统可用的编码器。你可以使用Fiendishthngs程序来显示系统的编码器，该示例代码可以从<a href="http://developer.apple.com/samplecode/Fiendishthngs/.">http://developer.apple.com/samplecode/Fiendishthngs/</a>获得。</p>
<h2 id="音频队列控制和状态">音频队列控制和状态</h2>
<p>音频队列的生命周期从创建到废弃。程序管理器生命周期，且控制音频队列的状态，通过使用<code>AudioQueue.h</code>头文件中的六个函数：</p>
<ul>
<li><strong>Start</strong>（<a href="https://developer.apple.com/documentation/audiotoolbox/1502689-audioqueuestart"><code>AudioQueueStart</code></a>）：初始化录制或者播放。</li>
<li><strong>Prime</strong> （<a href="https://developer.apple.com/documentation/audiotoolbox/1503220-audioqueueprime"><code>AudioQueuePrime</code></a>）：对于播放, 在调用<code>AudioQueueStart</code>之前调用这个函数，以确保有数据可立即用于音频队列的播放。这个函数不在录制中使用。</li>
<li><strong>Stop</strong>（<a href="https://developer.apple.com/documentation/audiotoolbox/1501970-audioqueuestop"><code>AudioQueueStop</code></a>）：调用这个函数来重置音频队列 （参考下面对<code>AudioQueueReset</code>的描述），然后停止录制或播放。当没有更多的数据要播放时，播放音频队列回调调用该函数。</li>
<li><strong>Pause</strong>（<a href="https://developer.apple.com/documentation/audiotoolbox/1502109-audioqueuepause"><code>AudioQueuePause</code></a>）：调用这个函数可以在不影响缓冲区和不重置音频队列的情况下停止录制或播放。如果需要恢复，调用<code>AudioQueueStart</code>函数。</li>
<li><strong>Flush</strong> （<a href="https://developer.apple.com/documentation/audiotoolbox/1502477-audioqueueflush"><code>AudioQueueFlush</code></a>）：在对最后一个音频队列缓冲区进行排队后调用，以确保所有缓冲的数据以及所有正在处理的音频数据被记录或播放。</li>
<li><strong>Reset</strong> （<a href="https://developer.apple.com/documentation/audiotoolbox/1502329-audioqueuereset"><code>AudioQueueReset</code></a>）：调用这个函数可以立即让音频队列静音。移除之前调度过的缓冲区，并且重置所有解码器和DSP状态。</li>
</ul>
<p>你可以在同步或异步模式下使用<code>AudioQueueStop</code>函数：</p>
<ul>
<li><strong>同步</strong>：立刻停止，不考虑之前缓冲的音频数据。</li>
<li><strong>异步</strong>：在所有已入队的缓冲区播放或录制完毕之后再停止。</li>
</ul>
<p>所有这些函数的完整描述和同步异步停止音频队列的更多信息，参阅_<a href="https://developer.apple.com/documentation/audiotoolbox/audio_queue_services">Audio Queue Services Reference</a>_。</p>
<h2 id="音频队列参数">音频队列参数</h2>
<p>音频队列通过<strong>参数（parameters）</strong>调整配置。每个参数都使用枚举值作为键，浮点数作为值。参数一般于播放，不用于录制。</p>
<p>在Mac OS X v10.5中，只有播放增益参数。可以通过使用<a href="https://developer.apple.com/documentation/audiotoolbox/kaudioqueueparam_volume"><code>kAudioQueueParam_Volume</code></a>常量来获取或设置它的值，它的有效范围在0.0（静音）到1.0（单位增益）。</p>
<p>程序可以通过以下两种方法来设置音频队列参数：</p>
<ul>
<li>对于每一个音频队列，使用<a href="https://developer.apple.com/documentation/audiotoolbox/1503293-audioqueuesetparameter"><code>AudioQueueSetParameter</code></a>函数，这可以让你直接改变音频队列的设置，这个改变是立刻生效的。</li>
<li>对于每一个音频队列缓冲区，调用<a href="https://developer.apple.com/documentation/audiotoolbox/1503258-audioqueueenqueuebufferwithparam"><code>AudioQueueEnqueueBufferWithParameters</code></a>函数。这可以让你在将音频队列缓冲区入队的时候设置音频队列设置。这种修改只会在播放音频队列缓冲区的时候生效。</li>
</ul>
<p>这两种情况下，音频队列的参数设置会一直保留到你改变它们为止。</p>
<p>可以通过调用<a href="https://developer.apple.com/documentation/audiotoolbox/1503353-audioqueuegetparameter"><code>AudioQueueGetParameter</code></a>函数来获取音频队列当前的参数。该函数的完整描述和获取和设置参数值的方法，参阅_<a href="https://developer.apple.com/documentation/audiotoolbox/audio_queue_services">Audio Queue Services Reference</a>_。</p>
<h2 id="总结">总结</h2>
<ul>
<li>音频队列工作：
<ul>
<li>连接音频硬件</li>
<li>内存管理</li>
<li>根据需要为已压缩的音频格式引入编码器</li>
<li>媒体的录制或播放</li>
</ul></li>
<li>使用音频队列的基本组成：
<ul>
<li>一组音频队列缓冲区，每个缓冲区临时存储音频数据。</li>
<li>缓冲区队列。</li>
<li>音频队列回调函数。</li>
</ul></li>
<li>音频队列按用途分类：
<ul>
<li>录制
<ul>
<li>输入端：音频输入硬件。</li>
<li>输出/回调：音频数据</li>
</ul></li>
<li>播放
<ul>
<li>输入/回调：获取音频数据并交付给音频队列。且当没有更多音频数据要播放时停止音频队列。</li>
<li>输出：音频输出设备。</li>
</ul></li>
</ul></li>
<li>音频队列缓冲区数量一般设置为3，对应录制：一个用于写入磁盘，一个填充新音频数据，一个在需要磁盘I/O延迟补偿时使用。</li>
<li>音频队列管理了音频队列缓冲区的生命周期/内存：<code>AudioQueueAllocateBuffer</code>创建，<code>AudioQueueDispose</code>释放音频队列时也一起释放其缓冲区。</li>
<li>播放过程通过<code>AudioQueueEnqueueBufferWithParameters</code>来实现播放控制：
<ul>
<li>设置缓冲区的精确播放时间，这可以实现音频同步。</li>
<li>截断音频队列缓冲区开头或结尾的帧，这可以让你去除开头或结尾的静音。</li>
<li>在缓冲区的粒度上设置播放增益。</li>
</ul></li>
<li>音频队列服务的编码大部分都在其回调函数上。录制使用<code>AudioQueueInputCallback</code>函数原型，播放使用<code>AudioQueueOutputCallback</code>函数原型。</li>
<li>回调函数调用的间隔取决于缓冲区的容量。</li>
<li>音频队列回调的任务是返回队列缓冲区。使用<code>AudioQueueEnqueueBuffer</code>入队缓冲区。</li>
<li>音频队列在录制和播放过程中都可以进行格式转换。回调函数不需要知道音频格式，因为音频编码器都是提前给音频队列配置的。</li>
<li>音频队列的状态控制：
<ul>
<li><strong>Start</strong>（<a href="https://developer.apple.com/documentation/audiotoolbox/1502689-audioqueuestart"><code>AudioQueueStart</code></a>）：初始化录制或者播放。</li>
<li><strong>Prime</strong> （<a href="https://developer.apple.com/documentation/audiotoolbox/1503220-audioqueueprime"><code>AudioQueuePrime</code></a>）：仅用于播放, 在调用<code>AudioQueueStart</code>之前调用这个函数，以确保有数据可立即用于音频队列的播放。</li>
<li><strong>Stop</strong>（<a href="https://developer.apple.com/documentation/audiotoolbox/1501970-audioqueuestop"><code>AudioQueueStop</code></a>）：调用这个函数来重置音频队列 （参考下面对<code>AudioQueueReset</code>的描述），然后停止录制或播放。当没有更多的数据要播放时，回调中调用该函数。</li>
<li><strong>Pause</strong>（<a href="https://developer.apple.com/documentation/audiotoolbox/1502109-audioqueuepause"><code>AudioQueuePause</code></a>）：调用这个函数可以在不影响缓冲区和不重置音频队列的情况下停止录制或播放。如果需要恢复，调用<code>AudioQueueStart</code>函数。</li>
<li><strong>Flush</strong> （<a href="https://developer.apple.com/documentation/audiotoolbox/1502477-audioqueueflush"><code>AudioQueueFlush</code></a>）：在对最后一个音频队列缓冲区进行排队后调用，以确保所有缓冲的数据以及所有正在处理的音频数据被记录或播放。</li>
<li><strong>Reset</strong> （<a href="https://developer.apple.com/documentation/audiotoolbox/1502329-audioqueuereset"><code>AudioQueueReset</code></a>）：调用这个函数可以立即让音频队列静音。移除之前调度过的缓冲区，并且重置所有解码器和DSP状态。</li>
</ul></li>
</ul>
]]></content>
      <categories>
        <category>翻译</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Audio Queue Services Programming Guide</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>Audio Queue Services Programming Guide：播放音频</title>
    <url>/posts/audio_queue_services_pg_playing_audio/</url>
    <content><![CDATA[<p>当你使用音频队列服务播放音频时，源几乎可以是任意的——磁盘文件、基于软件音频合成器、内存中的对象等。本章介绍最常见的情况：播放磁盘上的文件。</p>
<span id="more"></span>
<blockquote>
<p><strong>注意：</strong>本章介绍了基于ANSI-C的播放实现，并使用了Mac OS X Core Audio SDK的C++类。有关Objective-C的示例，参阅<a href="http://developer.apple.com/devcenter/ios/">iOS Dev Center</a>中的_SpeakHere_示例代码。</p>
</blockquote>
<p>要把播放功能添加到程序中，通常需要执行以下步骤：</p>
<ol type="1">
<li>定义一个自定义结构体来管理状态、格式和路径信息。</li>
<li>编写音频队列回调函数来执行实际的播放。</li>
<li>编写代码以确定音频队列缓冲区的合适大小。</li>
<li>打开音频文件进行播放，然后确定其音频数据格式。</li>
<li>创建一个播放音频队列并进行相关配置。</li>
<li>分配和排队音频队列缓冲区。告诉音频队列开始播放。完成后，播放回调函数告诉音频队列停止。</li>
<li>处理音频队列，释放资源。</li>
</ol>
<p>本章的剩余部分详细介绍了每个步骤。</p>
<h2 id="定义结构体管理状态">定义结构体管理状态</h2>
<p>首先，定义一个结构体，将用它来管理音频格式和音频队列状态信息，如清单3-1所示：</p>
<p><strong>清单3-1</strong> 播放音频队列的自定义结构体</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">const</span> <span class="keyword">int</span> kNumberBuffers = <span class="number">3</span>;                              <span class="comment">// 1</span></span><br><span class="line"><span class="keyword">struct</span> AQPlayerState &#123;</span><br><span class="line">    AudioStreamBasicDescription   mDataFormat;                    <span class="comment">// 2</span></span><br><span class="line">    AudioQueueRef                 mQueue;                         <span class="comment">// 3</span></span><br><span class="line">    AudioQueueBufferRef           mBuffers[kNumberBuffers];       <span class="comment">// 4</span></span><br><span class="line">    AudioFileID                   mAudioFile;                     <span class="comment">// 5</span></span><br><span class="line">    <span class="built_in">UInt32</span>                        bufferByteSize;                 <span class="comment">// 6</span></span><br><span class="line">    SInt64                        mCurrentPacket;                 <span class="comment">// 7</span></span><br><span class="line">    <span class="built_in">UInt32</span>                        mNumPacketsToRead;              <span class="comment">// 8</span></span><br><span class="line">    AudioStreamPacketDescription  *mPacketDescs;                  <span class="comment">// 9</span></span><br><span class="line">    <span class="keyword">bool</span>                          mIsRunning;                     <span class="comment">// 10</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>结构体中大多数字段与用于录制的自定义结构体几乎相同，如<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW15">Define a Custom Structure to Manage State</a>所述。例如，<code>mDataFormat</code>字段保存正在播放的文件格式。录制时，类似的字段保存了写入磁盘的文件格式。</p>
<p>以下是该结构体各字段介绍：</p>
<ol type="1">
<li>设置要使用的音频队列缓冲区数量。如<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW13">Audio Queue Buffers</a>所述，3个通常是不错的选择。</li>
<li><code>AudioStreamBasicDescription</code>结构体（来自<code>CoreAudioTypes.h</code>）表示正在播放的文件的音频数据格式。该格式由<code>mQueue</code>字段指定的音频队列使用。 <code>mDataFormat</code>字段通过查询音频文件的<code>kAudioFilePropertyDataFormat</code>属性来填充该字段，如<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW25">Obtaining a File’s Audio Data Format</a>所述。 有关<code>AudioStreamBasicDescription</code>结构体的详细信息，参阅_<a href="https://developer.apple.com/documentation/coreaudio/core_audio_data_types">Core Audio Data Types Reference</a>_。</li>
<li>程序创建的播放音频队列。</li>
<li>一个数组，包含指向音频队列管理的音频队列缓冲区的指针。</li>
<li>代表程序播放的音频文件的对象。</li>
<li>每个音频队列缓冲区的大小（以字节为单位）。该值在音频队列创建之后和开始之前，由<code>DeriveBufferSize</code>函数计算。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW23">Write a Function to Derive Playback Audio Queue Buffer Size</a>。</li>
<li>音频文件中下一个要播放的数据包索引。</li>
<li>每次调用音频队列的播放回调函数时，要读取的数据包数量。就像<code>bufferByteSize</code>字段一样，在音频队列创建之后和开始之前，由<code>DeriveBufferSize</code>函数计算该值。</li>
<li>对于VBR音频数据，该字段是正在播放的文件的数据包描述数组。对于CBR数据，该字段为<code>NULL</code>。</li>
<li>一个布尔值，表示音频队列是否正在运行。</li>
</ol>
<h2 id="编写播放音频队列回调函数">编写播放音频队列回调函数</h2>
<p>下面，编写一个播放音频队列回调函数。该回调函数执行三项主要任务：</p>
<ul>
<li>从音频文件中读取指定数量的数据，并将其放入音频队列缓冲区中。</li>
<li>把音频队列缓冲区排队到缓冲区队列中。</li>
<li>当没有更多数据要从音频文件中读取时，告诉音频队列停止。</li>
</ul>
<p>本节展示来一个回调声明示例，分别描述各个任务，最后给出完整的播放回调函数。有关播放回调函数的作用，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW3">图1-4</a>。</p>
<h3 id="播放音频队列回调声明">播放音频队列回调声明</h3>
<p>清单3-2展示了一个播放音频回调函数的示例声明，<code>AudioQueueOutputCallback</code>在<code>AudioQueue.h</code>声明为：</p>
<p><strong>清单3-2</strong> 播放音频队列回调声明</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">void</span> HandleOutputBuffer (</span><br><span class="line">    <span class="keyword">void</span>                 *aqData,                 <span class="comment">// 1</span></span><br><span class="line">    AudioQueueRef        inAQ,                    <span class="comment">// 2</span></span><br><span class="line">    AudioQueueBufferRef  inBuffer                 <span class="comment">// 3</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>通常，<code>aqData</code>是包含定义音频队列状态信息的自定义结构体。如<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW15">Define a Custom Structure to Manage State</a>所述。</li>
<li>持有该回调函数的音频队列。</li>
<li>音频队列缓冲区，回调函数通过从音频文件中读取，来填充数据。</li>
</ol>
<h3 id="从文件读取到音频队列缓冲区">从文件读取到音频队列缓冲区</h3>
<p>播放音频队列回调函数的第一个操作是从音频文件中读取数据并将其放在音频队列缓冲区中，如清单3-3所示。</p>
<p><strong>清单3-3</strong> 从音频文件读取到音频队列缓冲区</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AudioFileReadPackets (                        <span class="comment">// 1</span></span><br><span class="line">    pAqData-&gt;mAudioFile,                      <span class="comment">// 2</span></span><br><span class="line">    <span class="literal">false</span>,                                    <span class="comment">// 3</span></span><br><span class="line">    &amp;numBytesReadFromFile,                    <span class="comment">// 4</span></span><br><span class="line">    pAqData-&gt;mPacketDescs,                    <span class="comment">// 5</span></span><br><span class="line">    pAqData-&gt;mCurrentPacket,                  <span class="comment">// 6</span></span><br><span class="line">    &amp;numPackets,                              <span class="comment">// 7</span></span><br><span class="line">    inBuffer-&gt;mAudioData                      <span class="comment">// 8</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>AudioFileReadPackets</code>函数（在<code>AudioFile.h</code>中声明），从音频文件读取数据并将其放入缓冲区中。</li>
<li>要读取的音频文件。</li>
<li>用<code>false</code>表示该函数在读取时不应缓存数据。</li>
<li>输出时，是从音频文件读取的音频数据字节数。</li>
<li>输出时，是从音频文件中读取的数据包描述数组。对于CBR数据，该参数输入<code>NULL</code>。</li>
<li>从音频文件中读取第一个数据包的索引。</li>
<li>输入时，是要从音频文件读取的数据包数量。输出时，是实际读取的包数量。</li>
<li>在输出时，填充的音频队列缓冲区包含从音频文件读取的数据。</li>
</ol>
<h3 id="排队音频队列缓冲区">排队音频队列缓冲区</h3>
<p>现在已经从音频文件中读取数据并将其放在音频队列缓冲区中，回调函数让缓冲区入队，如清单3-4所示。进入缓冲区队列后，缓冲区的音频数据可用于音频队列发送到输出设备。</p>
<p><strong>清单3-4</strong> 从磁盘中读取后排队音频队列缓冲区</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AudioQueueEnqueueBuffer (                      <span class="comment">// 1</span></span><br><span class="line">    pAqData-&gt;mQueue,                           <span class="comment">// 2</span></span><br><span class="line">    inBuffer,                                  <span class="comment">// 3</span></span><br><span class="line">    (pAqData-&gt;mPacketDescs ? numPackets : <span class="number">0</span>),  <span class="comment">// 4</span></span><br><span class="line">    pAqData-&gt;mPacketDescs                      <span class="comment">// 5</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>AudioQueueEnqueueBuffer</code>函数把音频队列缓冲区添加到缓冲区队列。</li>
<li>持有缓冲区队列的音频队列。</li>
<li>要排队的音频队列缓冲区。</li>
<li>音频队列缓冲区数据中的数据包数量。对于不使用数据包描述的CBR数据，设为<code>0</code>。</li>
<li>对于使用数据描述的压缩音频数据格式，数据包描述在缓冲区中。</li>
</ol>
<h3 id="停止音频队列">停止音频队列</h3>
<p>回调函数最后一个操作是检查是否有更多的数据，要从正在播放的音频文件中读取。在发现文件结尾后，回调函数告诉音频队列停止，如清单3-5所示。</p>
<p><strong>清单3-5</strong>  Stopping an audio queue</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> (numPackets == <span class="number">0</span>) &#123;                          <span class="comment">// 1</span></span><br><span class="line">    AudioQueueStop (                            <span class="comment">// 2</span></span><br><span class="line">        pAqData-&gt;mQueue,                        <span class="comment">// 3</span></span><br><span class="line">        <span class="literal">false</span>                                   <span class="comment">// 4</span></span><br><span class="line">    );</span><br><span class="line">    pAqData-&gt;mIsRunning = <span class="literal">false</span>;                <span class="comment">// 5</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>检查<code>AudioFileReadPackets</code>函数（由之前的回调函数调用）读取的数据包数量是否为<code>0</code>。</li>
<li><code>AudioQueueStop</code>函数停止音频队列。</li>
<li>要停止的音频队列。</li>
<li>播放所有排队的缓冲区后，异步停止音频队列。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW17">Audio Queue Control and State</a>。</li>
<li>设置结构体标志，表示播放已完成。</li>
</ol>
<h3 id="完整播放音频队列回调函数">完整播放音频队列回调函数</h3>
<p>清单3-6展示了完整播放音频队列回调的基本代码。和本文档的其他示例代码一样，该清单代码不包含错误处理。</p>
<p><strong>清单3-6</strong> 一个播放音频队列回调函数</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">void</span> HandleOutputBuffer (</span><br><span class="line">    <span class="keyword">void</span>                *aqData,</span><br><span class="line">    AudioQueueRef       inAQ,</span><br><span class="line">    AudioQueueBufferRef inBuffer</span><br><span class="line">) &#123;</span><br><span class="line">    AQPlayerState *pAqData = (AQPlayerState *) aqData;        <span class="comment">// 1</span></span><br><span class="line">    <span class="keyword">if</span> (pAqData-&gt;mIsRunning == <span class="number">0</span>) <span class="keyword">return</span>;                     <span class="comment">// 2</span></span><br><span class="line">    <span class="built_in">UInt32</span> numBytesReadFromFile;                              <span class="comment">// 3</span></span><br><span class="line">    <span class="built_in">UInt32</span> numPackets = pAqData-&gt;mNumPacketsToRead;           <span class="comment">// 4</span></span><br><span class="line">    AudioFileReadPackets (</span><br><span class="line">        pAqData-&gt;mAudioFile,</span><br><span class="line">        <span class="literal">false</span>,</span><br><span class="line">        &amp;numBytesReadFromFile,</span><br><span class="line">        pAqData-&gt;mPacketDescs, </span><br><span class="line">        pAqData-&gt;mCurrentPacket,</span><br><span class="line">        &amp;numPackets,</span><br><span class="line">        inBuffer-&gt;mAudioData </span><br><span class="line">    );</span><br><span class="line">    <span class="keyword">if</span> (numPackets &gt; <span class="number">0</span>) &#123;                                     <span class="comment">// 5</span></span><br><span class="line">        inBuffer-&gt;mAudioDataByteSize = numBytesReadFromFile;  <span class="comment">// 6</span></span><br><span class="line">       AudioQueueEnqueueBuffer ( </span><br><span class="line">            pAqData-&gt;mQueue,</span><br><span class="line">            inBuffer,</span><br><span class="line">            (pAqData-&gt;mPacketDescs ? numPackets : <span class="number">0</span>),</span><br><span class="line">            pAqData-&gt;mPacketDescs</span><br><span class="line">        );</span><br><span class="line">        pAqData-&gt;mCurrentPacket += numPackets;                <span class="comment">// 7 </span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        AudioQueueStop (</span><br><span class="line">            pAqData-&gt;mQueue,</span><br><span class="line">            <span class="literal">false</span></span><br><span class="line">        );</span><br><span class="line">        pAqData-&gt;mIsRunning = <span class="literal">false</span>; </span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>实例化后提供给音频队列的自定义结构体，包含要播放的音频文件对象（类型为<code>AudioFileID</code>），以及各种状态数据。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW15">Define a Custom Structure to Manage State</a>。</li>
<li>如果音频队列已停止，则立即返回。</li>
<li>一个变量，用于保存从正在播放的文件中读取的音频数据字节数。</li>
<li>使用要从正播放的文件中读取的数据包来初始化<code>numPackets</code>变量。</li>
<li>测试是否从文件中检索了一些音频数据。如果是，则让新填充的缓冲区入队；否则停止音频队列。</li>
<li>告诉音频队列缓冲区结构体已读取数据的字节数。</li>
<li>根据读取的数据包数量增加数据包索引。</li>
</ol>
<h2 id="编写函数计算播放音频队列缓冲区大小">编写函数计算播放音频队列缓冲区大小</h2>
<p>音频队列服务希望你的程序为使用的音频队列缓冲区指定大小，如清单3-7所示。它得出的缓冲区大小足以容纳给定的音频时长。</p>
<p>创建播放音频队列后，你将在程序中调用<code>DeriveBufferSize</code>函数，作为后续音频队列分配缓冲区的先决条件。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW14">Write a Function to Derive Recording Audio Queue Buffer Size</a>。为了播放，还需要：</p>
<ul>
<li>在每次回调函数调用<code>AudioFileReadPackets</code>函数，得出要读取的数据包数量。</li>
<li>设置缓冲区大小的下限，以避免过多的磁盘访问。</li>
</ul>
<p>这里的计算考虑了从磁盘读取的音频数据格式。该格式包括了可能影响缓冲区大小的所有因素，例如音频通道数量。</p>
<p><strong>清单3-7</strong> 得出播放音频队列缓冲区大小</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">void</span> DeriveBufferSize (</span><br><span class="line">    AudioStreamBasicDescription &amp;ASBDesc,                            <span class="comment">// 1</span></span><br><span class="line">    <span class="built_in">UInt32</span>                      maxPacketSize,                       <span class="comment">// 2</span></span><br><span class="line">    Float64                     seconds,                             <span class="comment">// 3</span></span><br><span class="line">    <span class="built_in">UInt32</span>                      *outBufferSize,                      <span class="comment">// 4</span></span><br><span class="line">    <span class="built_in">UInt32</span>                      *outNumPacketsToRead                 <span class="comment">// 5</span></span><br><span class="line">) &#123;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">const</span> <span class="keyword">int</span> maxBufferSize = <span class="number">0x50000</span>;                        <span class="comment">// 6</span></span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">const</span> <span class="keyword">int</span> minBufferSize = <span class="number">0x4000</span>;                         <span class="comment">// 7</span></span><br><span class="line"> </span><br><span class="line">    <span class="keyword">if</span> (ASBDesc.mFramesPerPacket != <span class="number">0</span>) &#123;                             <span class="comment">// 8</span></span><br><span class="line">        Float64 numPacketsForTime =</span><br><span class="line">            ASBDesc.mSampleRate / ASBDesc.mFramesPerPacket * seconds;</span><br><span class="line">        *outBufferSize = numPacketsForTime * maxPacketSize;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;                                                         <span class="comment">// 9</span></span><br><span class="line">        *outBufferSize =</span><br><span class="line">            maxBufferSize &gt; maxPacketSize ?</span><br><span class="line">                maxBufferSize : maxPacketSize;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="keyword">if</span> (                                                             <span class="comment">// 10</span></span><br><span class="line">        *outBufferSize &gt; maxBufferSize &amp;&amp;</span><br><span class="line">        *outBufferSize &gt; maxPacketSize</span><br><span class="line">    )</span><br><span class="line">        *outBufferSize = maxBufferSize;</span><br><span class="line">    <span class="keyword">else</span> &#123;                                                           <span class="comment">// 11</span></span><br><span class="line">        <span class="keyword">if</span> (*outBufferSize &lt; minBufferSize)</span><br><span class="line">            *outBufferSize = minBufferSize;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    *outNumPacketsToRead = *outBufferSize / maxPacketSize;           <span class="comment">// 12</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>音频队列的<code>AudioStreamBasicDescription</code>结构体。</li>
<li>正在播放的音频文件中最大数据包的预估大小。你可以通过<code>kAudioFilePropertyPacketSizeUpperBound</code>属性ID，使用<code>AudioFileGetProperty</code>函数（在<code>AudioFile.h</code>中声明）得出该值。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW9">Set Sizes for a Playback Audio Queue</a>。</li>
<li>为每个音频缓冲区指定大小（以秒为单位）。</li>
<li>在输出时，是每个音频队列缓冲区的大小（以字节为单位）。</li>
<li>在输出时，是在每次播放音频队列回调时，从文件读取的音频数据包的数量。</li>
<li>音频队列缓冲区大小的上限（以字节为单位）。在该示例中，上限设为320 KB。这相等于以96 kHz采样率，大约持续5秒的24位立体声音频。</li>
<li>音频队列缓冲区大小的下限（以字节为单位）。在该示例中，下限设为16 KB。</li>
<li>对于定义每个数据包固定帧数的音频数据格式，需要得出音频队列缓冲区大小。</li>
<li>对于没定义每个数据包固定帧数的音频格式，需要根据最大数据包大小和设置的上限得出合理的音频队列缓冲区大小。</li>
<li>如果得出的缓冲区大小大于设置的上限，则考虑预估的最大数据包大小，并将其调整为边界值。</li>
<li>如果得出的缓冲区大小低于设置的下限，则将其调整为下限。</li>
<li>计算每次调用回调时从音频文件读取的数据包数量。</li>
</ol>
<h2 id="打开音频文件进行播放">打开音频文件进行播放</h2>
<p>现在，使用以下步骤打开音频文件进行播放：</p>
<ol type="1">
<li>获取一个表示要播放的音频文件的CFURL对象。</li>
<li>打开文件。</li>
<li>获取文件的音频数据格式。</li>
</ol>
<h3 id="获取音频文件的cfurl对象">获取音频文件的CFURL对象</h3>
<p>清单3-8展示了如何为要播放的音频文件获取CFURL对象。在下一步中使用CFURL对象，打开文件。</p>
<p><strong>清单3-8</strong> 获取音频文件的CFURL对象</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="built_in">CFURLRef</span> audioFileURL =</span><br><span class="line">    <span class="built_in">CFURLCreateFromFileSystemRepresentation</span> (           <span class="comment">// 1</span></span><br><span class="line">        <span class="literal">NULL</span>,                                           <span class="comment">// 2</span></span><br><span class="line">        (<span class="keyword">const</span> <span class="built_in">UInt8</span> *) filePath,                       <span class="comment">// 3</span></span><br><span class="line">        strlen (filePath),                              <span class="comment">// 4</span></span><br><span class="line">        <span class="literal">false</span>                                           <span class="comment">// 5</span></span><br><span class="line">    );</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>CFURLCreateFromFileSystemRepresentation</code>函数（在<code>CFURL.h</code>中声明），创建一个CFURL对象，该对象表示要播放的文件。</li>
<li>用<code>NULL</code>或<code>kCFAllocatorDefault</code>，表示使用当前默认的内存分配器。</li>
<li>想要转换为CFURL的文件系统路径。在生产代码中，通常会从用户获取<code>filePath</code>值。</li>
<li>文件系统路径中的字节数。</li>
<li><code>false</code>值表示<code>filePath</code>代表文件，而不是目录。</li>
</ol>
<h3 id="打开音频文件">打开音频文件</h3>
<p>清单3-9展示了如何打开音频文件进行播放。</p>
<p><strong>清单3-9</strong> 打开音频文件进行播放</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AQPlayerState aqData;                                   <span class="comment">// 1</span></span><br><span class="line"> </span><br><span class="line">OSStatus result =</span><br><span class="line">    AudioFileOpenURL (                                  <span class="comment">// 2</span></span><br><span class="line">        audioFileURL,                                   <span class="comment">// 3</span></span><br><span class="line">        fsRdPerm,                                       <span class="comment">// 4</span></span><br><span class="line">        <span class="number">0</span>,                                              <span class="comment">// 5</span></span><br><span class="line">        &amp;aqData.mAudioFile                              <span class="comment">// 6</span></span><br><span class="line">    );</span><br><span class="line"> </span><br><span class="line"><span class="built_in">CFRelease</span> (audioFileURL);                               <span class="comment">// 7</span></span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>创建<code>AQPlayerState</code>自定义结构体实例（参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW15">Define a Custom Structure to Manage State</a>）。打开音频文件进行播放时，可以使用该实例存放音频文件对象（类型为<code>AudioFileID</code>）。</li>
<li><code>AudioFileOpenURL</code>函数（在<code>AudioFile.h</code>中声明），打开要播放的文件。</li>
<li>要播放文件的引用。</li>
<li>与正在播放文件一起使用的文件权限。可用权限在文件管理器的<code>File Access Permission Constants</code>枚举中定义。在该示例中，请求读取文件的权限。</li>
<li>可选文件类型hint。这里的<code>0</code>表示该示例未使用该功能。</li>
<li>在输出时，对音频文件的引用将放在自定义结构体的<code>mAudioFile</code>字段。</li>
<li>释放在第一步创建的CFURL对象。</li>
</ol>
<h3 id="获取文件的音频数据格式">获取文件的音频数据格式</h3>
<p>清单3-10展示了如何获取文件的音频数据格式。</p>
<p><strong>清单3-10</strong> 获取文件的音频数据格式</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="built_in">UInt32</span> dataFormatSize = <span class="keyword">sizeof</span> (aqData.mDataFormat);    <span class="comment">// 1</span></span><br><span class="line"> </span><br><span class="line">AudioFileGetProperty (                                  <span class="comment">// 2</span></span><br><span class="line">    aqData.mAudioFile,                                  <span class="comment">// 3</span></span><br><span class="line">    kAudioFilePropertyDataFormat,                       <span class="comment">// 4</span></span><br><span class="line">    &amp;dataFormatSize,                                    <span class="comment">// 5</span></span><br><span class="line">    &amp;aqData.mDataFormat                                 <span class="comment">// 6</span></span><br><span class="line">);</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>获取在查询音频文件有关音频数据格式时要使用的预期属性值大小。</li>
<li><code>AudioFileGetProperty</code>函数（在<code>AudioFile.h</code>中声明），获取音频文件中指定属性的值。</li>
<li>音频文件对象（类型为<code>AudioFileID</code>），表示要获取其音频数据格式的文件。</li>
<li>用户获取音频文件的数据格式的属性ID。</li>
<li>输入时，是描述音频文件的数据格式的<code>AudioStreamBasicDescription</code>结构体的预期大小。输出时，是其实际大小。播放程序不需要使用该值。</li>
<li>在输出时，从音频文件获得<code>AudioStreamBasicDescription</code>结构体的完整音频数据格式。该行通过把文件的音频数据格式存储在音频队列的自定义结构体中，将其应用于音频队列。</li>
</ol>
<h2 id="创建播放音频队列">创建播放音频队列</h2>
<p>清单3-11展示了如何创建播放音频队列。注意，<code>AudioQueueNewOutput</code>函数使用了在之前步骤中配置的自定义结构体和回调函数，以及要播放文件的音频数据格式。</p>
<p><strong>清单3-11</strong> 创建播放音频队列</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AudioQueueNewOutput (                                <span class="comment">// 1</span></span><br><span class="line">    &amp;aqData.mDataFormat,                             <span class="comment">// 2</span></span><br><span class="line">    HandleOutputBuffer,                              <span class="comment">// 3</span></span><br><span class="line">    &amp;aqData,                                         <span class="comment">// 4</span></span><br><span class="line">    <span class="built_in">CFRunLoopGetCurrent</span> (),                          <span class="comment">// 5</span></span><br><span class="line">    kCFRunLoopCommonModes,                           <span class="comment">// 6</span></span><br><span class="line">    <span class="number">0</span>,                                               <span class="comment">// 7</span></span><br><span class="line">    &amp;aqData.mQueue                                   <span class="comment">// 8</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>AudioQueueNewOutput</code>函数创建一个新的播放音频队列。</li>
<li>设置要播放音频队列的音频数据格式。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW25">Obtaining a File’s Audio Data Format</a>。</li>
<li>和播放音频队列一起使用的回调函数。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW2">Write a Playback Audio Queue Callback</a>。</li>
<li>播放音频队列的自定义数据结构体。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW15">Define a Custom Structure to Manage State</a>。</li>
<li>当前的run loop，将在其调用音频队列回调函数。</li>
<li>run loop模式。通常设为<code>kCFRunLoopCommonModes</code>。</li>
<li>保留参数，必需为<code>0</code>。</li>
<li>在输出时，新分配的播放音频队列。</li>
</ol>
<h2 id="设置播放音频队列大小">设置播放音频队列大小</h2>
<p>接下来，设置播放音频队列的一些大小值。在为音频队列分配缓冲区时，以及开始读取音频文件之前，请使用这些大小值。</p>
<p>本节中的代码清单展示了如何设置：</p>
<ul>
<li>音频队列缓冲区大小。</li>
<li>每次调用播放音频队列回调函数时要读取的数据包数量。</li>
<li>数组大小，用于保存一个缓冲区的音频数据的数据包描述。</li>
</ul>
<h3 id="设置缓冲区大小和要读取的数据包数量">设置缓冲区大小和要读取的数据包数量</h3>
<p>清单3-12展示了如何使用之前编写的<code>DeriveBufferSize</code>函数（参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW23">Write a Function to Derive Playback Audio Queue Buffer Size</a>）。这里的目的是为每个音频队列缓冲区设置一个大小（以字节为单位），并确定每次调用播放音频队列回调函数时要读取的包数量。</p>
<p>该代码使用最大数据包大小的保守预估值，Core Audio通过<code>kAudioFilePropertyPacketSizeUpperBound</code>属性提供了该预估值。在大多数情况下，比起花时间读取整个音频文件以获得实际的最大数据包大小，使用这种近似（但快速）的技术更好。</p>
<p><strong>清单3-12</strong> 设置播放音频队列缓冲区的大小和要读取的数据包数量</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="built_in">UInt32</span> maxPacketSize;</span><br><span class="line"><span class="built_in">UInt32</span> propertySize = <span class="keyword">sizeof</span> (maxPacketSize);</span><br><span class="line">AudioFileGetProperty (                               <span class="comment">// 1</span></span><br><span class="line">    aqData.mAudioFile,                               <span class="comment">// 2</span></span><br><span class="line">    kAudioFilePropertyPacketSizeUpperBound,          <span class="comment">// 3</span></span><br><span class="line">    &amp;propertySize,                                   <span class="comment">// 4</span></span><br><span class="line">    &amp;maxPacketSize                                   <span class="comment">// 5</span></span><br><span class="line">);</span><br><span class="line"> </span><br><span class="line">DeriveBufferSize (                                   <span class="comment">// 6</span></span><br><span class="line">    aqData.mDataFormat,                              <span class="comment">// 7</span></span><br><span class="line">    maxPacketSize,                                   <span class="comment">// 8</span></span><br><span class="line">    <span class="number">0.5</span>,                                             <span class="comment">// 9</span></span><br><span class="line">    &amp;aqData.bufferByteSize,                          <span class="comment">// 10</span></span><br><span class="line">    &amp;aqData.mNumPacketsToRead                        <span class="comment">// 11</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>AudioFileGetProperty</code>函数（在<code>AudioFile.h</code>中声明），获取音频文件的指定属性的值。这里，可以用它来获取要播放文件中音频数据包大小的保守上限值（以字节为单位）。</li>
<li>要播放的音频文件对象（类型为<code>AudioFileID</code>）。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW24">Opening an Audio File</a>。</li>
<li>用于获取音频文件中数据包大小的保守上限的属性ID。</li>
<li>输出时，<code>kAudioFilePropertyPacketSizeUpperBound</code>属性的大小（以字节为单位）。</li>
<li>输出时，要播放的文件的数据包大小的保守上限（以字节为单位）。</li>
<li><code>DeriveBufferSize</code>函数（在<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW23">Write a Function to Derive Playback Audio Queue Buffer Size</a>中描述），设置来缓冲区大小和每次调用回调函数时要读取的数据包数量。</li>
<li>要播放的文件的音频数据格式。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW25">Obtaining a File’s Audio Data Format</a>。</li>
<li>来自第5行的音频文件最大数据包大小的预估值。</li>
<li>每次音频队列缓冲区应保留的音频时长（以秒为单位）。此处设置半秒是个不错的选择。</li>
<li>在输出时，每个音频队列缓冲区大小（以字节为单位）。该值放在音频队列的自定义结构体中。</li>
<li>在输出时，是在每次播放音频队列回调时要读取的数据包数量。该值也放在音频队列的自定义结构体中。</li>
</ol>
<h3 id="给数据包描述数组分配内存">给数据包描述数组分配内存</h3>
<p>现在，给数组分配内存，以保存一个缓冲区的音频数据的数据包描述。CBR数据不使用数据包描述，因此CBR的情况（清单3-13中的步骤3）非常简单。</p>
<p><strong>清单3-13</strong> 给数据包描述数组分配内存</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">bool</span> isFormatVBR = (                                       <span class="comment">// 1</span></span><br><span class="line">    aqData.mDataFormat.mBytesPerPacket == <span class="number">0</span> ||</span><br><span class="line">    aqData.mDataFormat.mFramesPerPacket == <span class="number">0</span></span><br><span class="line">);</span><br><span class="line"> </span><br><span class="line"><span class="keyword">if</span> (isFormatVBR) &#123;                                         <span class="comment">// 2</span></span><br><span class="line">    aqData.mPacketDescs =</span><br><span class="line">      (AudioStreamPacketDescription*) malloc (</span><br><span class="line">        aqData.mNumPacketsToRead * <span class="keyword">sizeof</span> (AudioStreamPacketDescription)</span><br><span class="line">      );</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;                                                   <span class="comment">// 3</span></span><br><span class="line">    aqData.mPacketDescs = <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>确定音频文件的数据格式是VBR还是CBR。在VBR数据中，bytes-per-packet或frames-per-packet值的一个或两个是可变的，因此列出在音频队列的<code>AudioStreamBasicDescription</code>结构体中这两个值为<code>0</code>的情况。</li>
<li>对于包含VBR数据的音频文件，则为数据包描述数组分配内存。根据每次播放回调调用时要读取的音频数据包数量，计算所需内存。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW26">Setting Buffer Size and Number of Packets to Read</a>。</li>
<li>对于包含CBR数据的音频文件（例如线性PCM），音频队列不使用数据包描述数组。</li>
</ol>
<h2 id="给播放音频队列设置magic-cookie">给播放音频队列设置Magic Cookie</h2>
<p>某些压缩音频格式（例如MPEG 4 AAC）利用结构体来包含音频元数据。这些结构体称为<strong>magic cookies</strong>。使用音频队列服务以这种格式播放文件时，需要从音频文件中获取magic cookie，然后在开始播放之前应用到音频队列中。</p>
<p>清单3-14展示了如何从文件中获取magic cookie并将其应用到音频队列。你需要在开始播放之前调用该函数。</p>
<p><strong>清单3-14</strong> 为播放音频队列设置magic cookie</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="built_in">UInt32</span> cookieSize = <span class="keyword">sizeof</span> (<span class="built_in">UInt32</span>);                   <span class="comment">// 1</span></span><br><span class="line"><span class="keyword">bool</span> couldNotGetProperty =                             <span class="comment">// 2</span></span><br><span class="line">    AudioFileGetPropertyInfo (                         <span class="comment">// 3</span></span><br><span class="line">        aqData.mAudioFile,                             <span class="comment">// 4</span></span><br><span class="line">        kAudioFilePropertyMagicCookieData,             <span class="comment">// 5</span></span><br><span class="line">        &amp;cookieSize,                                   <span class="comment">// 6</span></span><br><span class="line">        <span class="literal">NULL</span>                                           <span class="comment">// 7</span></span><br><span class="line">    );</span><br><span class="line"> </span><br><span class="line"><span class="keyword">if</span> (!couldNotGetProperty &amp;&amp; cookieSize) &#123;              <span class="comment">// 8</span></span><br><span class="line">    <span class="keyword">char</span>* magicCookie =</span><br><span class="line">        (<span class="keyword">char</span> *) malloc (cookieSize);</span><br><span class="line"> </span><br><span class="line">    AudioFileGetProperty (                             <span class="comment">// 9</span></span><br><span class="line">        aqData.mAudioFile,                             <span class="comment">// 10</span></span><br><span class="line">        kAudioFilePropertyMagicCookieData,             <span class="comment">// 11</span></span><br><span class="line">        &amp;cookieSize,                                   <span class="comment">// 12</span></span><br><span class="line">        magicCookie                                    <span class="comment">// 13</span></span><br><span class="line">    );</span><br><span class="line"> </span><br><span class="line">    AudioQueueSetProperty (                            <span class="comment">// 14</span></span><br><span class="line">        aqData.mQueue,                                 <span class="comment">// 15</span></span><br><span class="line">        kAudioQueueProperty_MagicCookie,               <span class="comment">// 16</span></span><br><span class="line">        magicCookie,                                   <span class="comment">// 17</span></span><br><span class="line">        cookieSize                                     <span class="comment">// 18</span></span><br><span class="line">    );</span><br><span class="line"> </span><br><span class="line">    free (magicCookie);                                <span class="comment">// 19</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>设置magic cookie数据的预估大小。</li>
<li>接收<code>AudioFileGetPropertyInfo</code>函数的结果。如果成功，该函数返回<code>NoErr</code>，等同于布尔值<code>false</code>。</li>
<li><code>AudioFileGetPropertyInfo</code>函数（在<code>AudioFile.h</code>中声明），获取指定属性值的大小。可以用它来设置保存属性值的变量大小。</li>
<li>音频文件对象（类型为<code>AudioFileID</code>），表示要播放的音频文件。</li>
<li>表示音频文件的magic cookie数据的属性ID。</li>
<li>输入时，magic cookie数据的预估大小。输出时，是其实际大小。</li>
<li>用<code>NULL</code>表示不关心该属性的读/写访问权限。</li>
<li>如果音频文件确实包含magic cookie，则分配内存保存它。</li>
<li><code>AudioFileGetProperty</code>函数（在<code>AudioFile.h</code>中声明），获取指定属性的值。在这里，它将获取音频文件的magic cookie。</li>
<li>音频文件对象（类型为<code>AudioFileID</code>），表示要播放的以及要获取magic cookie的音频文件。</li>
<li>音频文件的magic cookie数据的属性ID。</li>
<li>输入时，<code>magicCookie</code>使用<code>AudioFileGetPropertyInfo</code>函数获得变量的大小。输出时，将是magic cookie的实际大小（以写入<code>magicCookie</code>变量的字节数为单位）。</li>
<li>输出时，音频文件的magic cookie。</li>
<li><code>AudioQueueSetProperty</code>函数给音频队列设置属性。在这里，它将给音频队列设置magic cookie，使其于要播放的音频文件中的magic cookie相匹配。</li>
<li>要为其设置magic cookie的音频队列。</li>
<li>音频队列的magic cookie的属性ID。</li>
<li>要播放文件中的magic cookie。</li>
<li>magic cookie的大小（以字节为单位）。</li>
<li>释放分配给magic cookie的内存。</li>
</ol>
<h2 id="分配和准备音频队列缓冲区">分配和准备音频队列缓冲区</h2>
<p>现在，请求之前创建的（参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW5">Create a Playback Audio Queue</a>）音频队列来准备一组音频队列缓冲区，如清单3-15所示。</p>
<p><strong>清单3-15</strong> 分配和准备音频队列缓冲区进行播放</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">aqData.mCurrentPacket = <span class="number">0</span>;                                <span class="comment">// 1</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; kNumberBuffers; ++i) &#123;                <span class="comment">// 2</span></span><br><span class="line">    AudioQueueAllocateBuffer (                            <span class="comment">// 3</span></span><br><span class="line">        aqData.mQueue,                                    <span class="comment">// 4</span></span><br><span class="line">        aqData.bufferByteSize,                            <span class="comment">// 5</span></span><br><span class="line">        &amp;aqData.mBuffers[i]                               <span class="comment">// 6</span></span><br><span class="line">    );</span><br><span class="line"> </span><br><span class="line">    HandleOutputBuffer (                                  <span class="comment">// 7</span></span><br><span class="line">        &amp;aqData,                                          <span class="comment">// 8</span></span><br><span class="line">        aqData.mQueue,                                    <span class="comment">// 9</span></span><br><span class="line">        aqData.mBuffers[i]                                <span class="comment">// 10</span></span><br><span class="line">    );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>数据包索引设为<code>0</code>，以便当前音频队列回调函数填充缓冲区（步骤7）时，是从音频文件的开头开始。</li>
<li>分配和准备一组音频队列缓冲区（<code>kNumberBuffers</code>设置为<code>3</code>，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW15">Define a Custom Structure to Manage State</a>）。</li>
<li><code>AudioQueueAllocateBuffer</code>函数通过为其分配内存来创建音频队列缓冲区。</li>
<li>分配缓冲区的音频队列。</li>
<li>新音频队列缓冲区大小（以字节为单位）。</li>
<li>输出时，把新的音频队列缓冲区添加到自定义结构体的<code>mBuffers</code>数组中。</li>
<li><code>HandleOutputBuffer</code>是播放音频队列的回调函数。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW2">Write a Playback Audio Queue Callback</a>。</li>
<li>音频队列的自定义结构体。</li>
<li>要调用其回调的音频队列。</li>
<li>要传递给音频队列回调的缓冲区。</li>
</ol>
<h2 id="设置音频队列播放增益">设置音频队列播放增益</h2>
<p>在音频队列开始播放之前，通过音频队列参数机制设置其增益，如清单3-16所示。有关参数机制的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW15">Audio Queue Parameters</a>。</p>
<p><strong>清单3-16</strong> 设置音频队列的播放增益</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">Float32 gain = <span class="number">1.0</span>;                                       <span class="comment">// 1</span></span><br><span class="line">    <span class="comment">// Optionally, allow user to override gain setting here</span></span><br><span class="line">AudioQueueSetParameter (                                  <span class="comment">// 2</span></span><br><span class="line">    aqData.mQueue,                                        <span class="comment">// 3</span></span><br><span class="line">    kAudioQueueParam_Volume,                              <span class="comment">// 4</span></span><br><span class="line">    gain                                                  <span class="comment">// 5</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>在<code>0</code>（静音）和<code>1</code>（单元增益）之间设置增益。</li>
<li><code>AudioQueueSetParameter</code>函数设置音频队列的参数值。</li>
<li>要设置参数的音频队列。</li>
<li>要设置的参数ID。<code>kAudioQueueParam_Volume</code>用于设置音频队列增益。</li>
<li>要应用于音频队列的增益设置。</li>
</ol>
<h2 id="启动和运行音频队列">启动和运行音频队列</h2>
<p>前面的代码已经为播放文件做了准备。下面是启动音频队列和维护run loop，如清单3-17所示。</p>
<p><strong>清单3-17</strong> 启动和运行音频队列</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">aqData.mIsRunning = <span class="literal">true</span>;                          <span class="comment">// 1</span></span><br><span class="line"> </span><br><span class="line">AudioQueueStart (                                  <span class="comment">// 2</span></span><br><span class="line">    aqData.mQueue,                                 <span class="comment">// 3</span></span><br><span class="line">    <span class="literal">NULL</span>                                           <span class="comment">// 4</span></span><br><span class="line">);</span><br><span class="line"> </span><br><span class="line"><span class="keyword">do</span> &#123;                                               <span class="comment">// 5</span></span><br><span class="line">    <span class="built_in">CFRunLoopRunInMode</span> (                           <span class="comment">// 6</span></span><br><span class="line">        kCFRunLoopDefaultMode,                     <span class="comment">// 7</span></span><br><span class="line">        <span class="number">0.25</span>,                                      <span class="comment">// 8</span></span><br><span class="line">        <span class="literal">false</span>                                      <span class="comment">// 9</span></span><br><span class="line">    );</span><br><span class="line">&#125; <span class="keyword">while</span> (aqData.mIsRunning);</span><br><span class="line"> </span><br><span class="line"><span class="built_in">CFRunLoopRunInMode</span> (                               <span class="comment">// 10</span></span><br><span class="line">    kCFRunLoopDefaultMode,</span><br><span class="line">    <span class="number">1</span>,</span><br><span class="line">    <span class="literal">false</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>设置自定义结构体标志，表示音频队列正在运行。</li>
<li><code>AudioQueueStart</code>函数在其自身的线程上启动音频队列。</li>
<li>要开始的音频队列。</li>
<li>用<code>NULL</code>表示因队列应立即开始播放。</li>
<li>定义轮询自定义结构体的<code>mIsRunning</code>字段，以检查音频队列是否已经停止。</li>
<li><code>CFRunLoopRunInMode</code>函数运行包含音频队列线程的run loop。</li>
<li>对run loop使用默认模式。</li>
<li>把run loop的运行时间设置为<code>0.25</code>秒。</li>
<li>用<code>false</code>表示run loop应在指定的时间内继续。</li>
<li>音频队列停止后，再运行一次run loop，以确保当前正在播放的音频队列缓冲区有足够时间完成。</li>
</ol>
<h2 id="播放后的清理">播放后的清理</h2>
<p>播放文件后，处理音频队列，关闭音频文件，并释放所有剩余资源，如清单3-18所示。</p>
<p><strong>清单3-18</strong> 播放音频文件后清理</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AudioQueueDispose (                            <span class="comment">// 1</span></span><br><span class="line">    aqData.mQueue,                             <span class="comment">// 2</span></span><br><span class="line">    <span class="literal">true</span>                                       <span class="comment">// 3</span></span><br><span class="line">);</span><br><span class="line"> </span><br><span class="line">AudioFileClose (aqData.mAudioFile);            <span class="comment">// 4</span></span><br><span class="line"> </span><br><span class="line">free (aqData.mPacketDescs);                    <span class="comment">// 5</span></span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>AudioQueueDispose</code>函数处理音频队列及其所有资源，包括缓冲区。</li>
<li>要处理的音频队列。</li>
<li>用<code>true</code>表示同步处理音频队列。</li>
<li>关闭播放的音频文件。<code>AudioFileClose</code>函数在<code>AudioFile.h</code>中声明。</li>
<li>释放用于保存数据包描述的内存。</li>
</ol>
<h2 id="总结">总结</h2>
<ul>
<li>使用AudioQueue实现播放功能，一般步骤：
<ol type="1">
<li>定义一个自定义结构体来管理状态、格式和路径信息。</li>
<li>编写音频队列回调函数来执行实际的播放。</li>
<li>编写代码以确定音频队列缓冲区的合适大小。</li>
<li>打开音频文件进行播放，然后确定其音频数据格式。</li>
<li>创建一个播放音频队列并进行相关配置。</li>
<li>分配和排队音频队列缓冲区。告诉音频队列开始播放。完成后，播放回调函数告诉音频队列停止。</li>
<li>处理音频队列，释放资源。</li>
</ol></li>
</ul>
]]></content>
      <categories>
        <category>翻译</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Audio Queue Services Programming Guide</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>Audio Queue Services Programming Guide：录制音频</title>
    <url>/posts/audio_queue_services_pg_recording_audio/</url>
    <content><![CDATA[<p>当你使用音频队列服务进行录制的时候，你可以将音频录制到任何地方：磁盘文件、网络连接或内存对象等等。本章将介绍中最常见的一种情况，将音频录制到磁盘文件中。</p>
<span id="more"></span>
<blockquote>
<p><strong>注意：</strong>本章介绍了基于ANSI-C的录制的实现，并且使用了MAC OS X中Core Audio SDK中了一些C++类，如果想了解基于Objective-C的例子，请参考<a href="http://developer.apple.com/devcenter/ios/">iOS Dev Center</a>中的_SpeakHere_例子。</p>
</blockquote>
<p>要把录制功能添加到程序中，一般都要进行以下几个步骤：</p>
<ol type="1">
<li>定义一个自定义的结构体来管理状态、格式以及路径信息等。</li>
<li>编写音频队列回调函数来执行实际的录制工作。</li>
<li>（可选）编写代码来为音频队列缓冲区选择一个合适的大小。如果你将要录制的格式使用了magic cookies，你需要编写相应的代码来配合使用。</li>
<li>填充自定义结构体中的各个字段，包括指定音频队列将要录制到的文件的数据流、文件路径。</li>
<li>创建一个用于录制的音频队列并且让音频队列创建一系列的音频队列缓冲区，同时创建一个将要写入的文件。</li>
<li>通知音频队列开始录制。</li>
<li>录制完毕之后，通知音频队列停止录制，然后释放掉它，同时它会释放掉它所拥有的缓冲区。</li>
</ol>
<p>本章的剩余部分将详细描述上述的每一个步骤。</p>
<h2 id="定义一个管理状态的结构体">定义一个管理状态的结构体</h2>
<p>使用音频队列服务来开发一个音频录制解决方案的时候，第一步就是定义一个结构体。将使用这个结构体来管理音频格式和音频队列状态信息。清单2-1展示了这个这样的一个结构体。</p>
<p><strong>清单2-1</strong> 一个用于录制的音频队列的结构体</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">const</span> <span class="keyword">int</span> kNumberBuffers = <span class="number">3</span>;                            <span class="comment">// 1</span></span><br><span class="line"><span class="keyword">struct</span> AQRecorderState &#123;</span><br><span class="line">    AudioStreamBasicDescription  mDataFormat;                   <span class="comment">// 2</span></span><br><span class="line">    AudioQueueRef                mQueue;                        <span class="comment">// 3</span></span><br><span class="line">    AudioQueueBufferRef          mBuffers[kNumberBuffers];      <span class="comment">// 4</span></span><br><span class="line">    AudioFileID                  mAudioFile;                    <span class="comment">// 5</span></span><br><span class="line">    <span class="built_in">UInt32</span>                       bufferByteSize;                <span class="comment">// 6</span></span><br><span class="line">    SInt64                       mCurrentPacket;                <span class="comment">// 7</span></span><br><span class="line">    <span class="keyword">bool</span>                         mIsRunning;                    <span class="comment">// 8</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>下面是这个结构体中每个字段的说明：</p>
<ol type="1">
<li>要使用的音频队列缓冲区的数量。</li>
<li>一个<code>AudioStreamBasicDescription</code>结构体（来自<code>CoreAudioTypes.h</code>），表示将要写入磁盘的音频数据的格式，音频队列缓冲区使用这个格式来指定它的<code>mQueue</code>字段。<code>mDataFormat</code>字段是由你的程序初始化的，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW4">Set Up an Audio Format for Recording</a>。可以通过查询音频队列的<code>kAudioQueueProperty_StreamDescription</code>属性来更新这个字段的值，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW23">Getting the Full Audio Format from an Audio Queue</a>。在Mac OS X v10.5中要使用<code>kAudioConverterCurrentInputStreamDescription</code>。 关于<code>AudioStreamBasicDescription</code>结构体的详细信息，请参阅_<a href="https://developer.apple.com/documentation/coreaudio/core_audio_data_types">Core Audio Data Types Reference</a>_。</li>
<li>由程序创建的音频队列。</li>
<li>一个指向由音频队列所管理的音频队列缓冲区的指针数组。</li>
<li>一个表示程序录制音频时写入的文件的音频文件对象。</li>
<li>每个音频队列缓冲区的字节大小。它的值在随后的例子中的<code>DeriveBufferSize</code>函数中计算出来，它在音频队列创建后，开始录制音频前计算出来，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW14">Write a Function to Derive Recording Audio Queue Buffer Size</a>。</li>
<li>从当前音频队列缓冲区写入文件的第一个包（packet）的索引。</li>
<li>一个布尔值，表示音频队列是否在运行中。</li>
</ol>
<h2 id="编写用于录制的音频队列的回调函数">编写用于录制的音频队列的回调函数</h2>
<p>接下来，编写一个用于录制的回调函数，这个函数主要做两个事情：</p>
<ul>
<li>将新填充进音频队列缓冲区的内容写入你正在录制的文件中。</li>
<li>将刚才已经将内容写入文件的音频队列缓冲区入队到缓冲区队列。</li>
</ul>
<p>下面展示了一个回调函数声明的列子，然后分别描述这两个任务，最后展示一个完整的用于录制的回调函数。关于用于录制的音频队列回调函数所扮演的角色，可以参考<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW2">图1-3</a>。</p>
<h3 id="用于录制的音频队列回调函数的声明">用于录制的音频队列回调函数的声明</h3>
<p>清单2-2是一个用于录制的音频队列回调函数声明，是在<code>AudioQueue.h</code>中声明的<code>AudioQueueInputCallback</code>：</p>
<p><strong>清单2-2</strong>  用于录制的音频队列回调函数声明</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">void</span> HandleInputBuffer (</span><br><span class="line">    <span class="keyword">void</span>                                *aqData,             <span class="comment">// 1</span></span><br><span class="line">    AudioQueueRef                       inAQ,                <span class="comment">// 2</span></span><br><span class="line">    AudioQueueBufferRef                 inBuffer,            <span class="comment">// 3</span></span><br><span class="line">    <span class="keyword">const</span> AudioTimeStamp                *inStartTime,        <span class="comment">// 4</span></span><br><span class="line">    <span class="built_in">UInt32</span>                              inNumPackets,        <span class="comment">// 5</span></span><br><span class="line">    <span class="keyword">const</span> AudioStreamPacketDescription  *inPacketDesc        <span class="comment">// 6</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>一般来说，<code>aqData</code>是一个自定义的数据结构，包含了音频队列的状态信息，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW15">Define a Custom Structure to Manage State</a>。</li>
<li>拥有该回调函数的音频队列。</li>
<li>包含录制数据的音频队列缓冲区。</li>
<li>音频队列缓冲区中第一个采样的时间（对于简单的录制是不需要的）。</li>
<li><code>inPacketDesc</code>字段中包描述的数量，如果是0，表明这是个CBR数据。</li>
<li>对于压缩数据格式如果需要包描述，包描述是由编码器产生的。</li>
</ol>
<h3 id="将音频队列缓冲区中的数据写入磁盘">将音频队列缓冲区中的数据写入磁盘</h3>
<p>用于录制的音频队列回调函数要做的第一件事情就是把音频队列缓冲区中的内容写入磁盘。这个缓冲区就是音频队列从输入设备最新输入的音频数据。这个回调函数使用<code>AudioFile.h</code>中声明的<code>AudioFileWritePackets</code>函数。如清单2-3所示。</p>
<p><strong>清单2-3</strong> 将音频队列缓冲区数据写入磁盘</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AudioFileWritePackets (                     <span class="comment">// 1</span></span><br><span class="line">    pAqData-&gt;mAudioFile,                    <span class="comment">// 2</span></span><br><span class="line">    <span class="literal">false</span>,                                  <span class="comment">// 3</span></span><br><span class="line">    inBuffer-&gt;mAudioDataByteSize,           <span class="comment">// 4</span></span><br><span class="line">    inPacketDesc,                           <span class="comment">// 5</span></span><br><span class="line">    pAqData-&gt;mCurrentPacket,                <span class="comment">// 6</span></span><br><span class="line">    &amp;inNumPackets,                          <span class="comment">// 7</span></span><br><span class="line">    inBuffer-&gt;mAudioData                    <span class="comment">// 8</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>AudioFileWritePackets</code>函数（在<code>AudioFile.h</code>声明），把缓冲区的内容写入音频数据文件中。</li>
<li>音频文件对象（类型为<code>AudioFileID</code>）表示要写到的音频文件。<code>pAqData</code>变量是指向<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW5">清单2-1</a>描述的数据结构的指针。</li>
<li>使用<code>false</code>值来表达写入是函数不应缓存数据。</li>
<li>正在写入的音频数据的字节数。<code>inBuffer</code>变量音频队列传递给回调函数的音频队列缓冲区。</li>
<li>音频数据包描述数组。<code>NULL</code>值表示不需要数据包描述（例如，CBR音频数据）。</li>
<li>要写入的第一个数据包的索引。</li>
<li>输入时，表示要写入的数据包数量。输出时，表示实际写入的数据包数量。</li>
<li>将新的音频数据写入音频文件。</li>
</ol>
<h3 id="排队音频队列缓冲区">排队音频队列缓冲区</h3>
<p>现在，音频队列缓冲区的音频数据已经被写入音频文件，回调对缓冲区进行排队，如清单2-4所示。一旦回到缓冲区队列中，缓冲区就处于排队状态，准备接受更多传入的音频数据。</p>
<p><strong>清单2-4</strong> 写入磁盘后排队音频队列缓冲区</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AudioQueueEnqueueBuffer (                    <span class="comment">// 1</span></span><br><span class="line">    pAqData-&gt;mQueue,                         <span class="comment">// 2</span></span><br><span class="line">    inBuffer,                                <span class="comment">// 3</span></span><br><span class="line">    <span class="number">0</span>,                                       <span class="comment">// 4</span></span><br><span class="line">    <span class="literal">NULL</span>                                     <span class="comment">// 5</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>AudioQueueEnqueueBuffer</code>函数把音频队列缓冲区添加到音频队列的缓冲区队列中。</li>
<li>把指定的音频队列缓冲区添加到音频队列。<code>pAqData</code>变量指向清单2-1描述的数据结构指针。</li>
<li>要排队的音频队列缓冲区。</li>
<li>音频队列缓冲区数据中的数据包描述数量。设为<code>0</code>，因为该参数未用于录制。</li>
<li>数据包描述数组，描述音频队列缓冲区的数据。设为<code>NULL</code>，因为该参数未用于录制。</li>
</ol>
<h3 id="一个完整的音频录制的音频队列回调函数">一个完整的音频录制的音频队列回调函数</h3>
<p>清单2-5展示了完整的音频录制中音频队列回调函数的基本形式。与本文档的其他代码一样，该清单不包括错误处理。</p>
<p><strong>清单2-5</strong> 一个音频录制的音频队列回调函数</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">void</span> HandleInputBuffer (</span><br><span class="line">    <span class="keyword">void</span>                                 *aqData,</span><br><span class="line">    AudioQueueRef                        inAQ,</span><br><span class="line">    AudioQueueBufferRef                  inBuffer,</span><br><span class="line">    <span class="keyword">const</span> AudioTimeStamp                 *inStartTime,</span><br><span class="line">    <span class="built_in">UInt32</span>                               inNumPackets,</span><br><span class="line">    <span class="keyword">const</span> AudioStreamPacketDescription   *inPacketDesc</span><br><span class="line">) &#123;</span><br><span class="line">    AQRecorderState *pAqData = (AQRecorderState *) aqData;               <span class="comment">// 1</span></span><br><span class="line"> </span><br><span class="line">    <span class="keyword">if</span> (inNumPackets == <span class="number">0</span> &amp;&amp;                                             <span class="comment">// 2</span></span><br><span class="line">          pAqData-&gt;mDataFormat.mBytesPerPacket != <span class="number">0</span>)</span><br><span class="line">       inNumPackets =</span><br><span class="line">           inBuffer-&gt;mAudioDataByteSize / pAqData-&gt;mDataFormat.mBytesPerPacket;</span><br><span class="line"> </span><br><span class="line">    <span class="keyword">if</span> (AudioFileWritePackets (                                          <span class="comment">// 3</span></span><br><span class="line">            pAqData-&gt;mAudioFile,</span><br><span class="line">            <span class="literal">false</span>,</span><br><span class="line">            inBuffer-&gt;mAudioDataByteSize,</span><br><span class="line">            inPacketDesc,</span><br><span class="line">            pAqData-&gt;mCurrentPacket,</span><br><span class="line">            &amp;inNumPackets,</span><br><span class="line">            inBuffer-&gt;mAudioData</span><br><span class="line">        ) == noErr) &#123;</span><br><span class="line">            pAqData-&gt;mCurrentPacket += inNumPackets;                     <span class="comment">// 4</span></span><br><span class="line">    &#125;</span><br><span class="line">   <span class="keyword">if</span> (pAqData-&gt;mIsRunning == <span class="number">0</span>)                                         <span class="comment">// 5</span></span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line"> </span><br><span class="line">    AudioQueueEnqueueBuffer (                                            <span class="comment">// 6</span></span><br><span class="line">        pAqData-&gt;mQueue,</span><br><span class="line">        inBuffer,</span><br><span class="line">        <span class="number">0</span>,</span><br><span class="line">        <span class="literal">NULL</span></span><br><span class="line">    );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>实例化时提供给音频队列对象的结构体，包含代表要记录到其中的音频文件的对象，以及各种状态数据。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW15">Define a Custom Structure to Manage State</a>。</li>
<li>如果音频队列缓冲区包含CBR数据，则需要计算缓冲区的数据包数量。该数值等于缓冲区中数据的总字节除以每个数据包固定的字节数。对于VBR数据，音频队列在调用回调时会提供缓冲区中的数据包数量。</li>
<li>把缓冲区的内容写入到音频数据文件中。有关详细的说明，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW3">Writing an Audio Queue Buffer to Disk</a>。</li>
<li>如果成功写入音频数据，需要增加音频数据文件的数据包索引，以准备写入下一个缓冲区的音频数据。</li>
<li>如果音频队列已停止，则返回。</li>
<li>入队该写入音频文件的音频队列缓冲区。有关详细的说明，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW8">Enqueuing an Audio Queue Buffer</a>。</li>
</ol>
<h2 id="编写函数计算用于录制的音频队列缓冲区大小">编写函数计算用于录制的音频队列缓冲区大小</h2>
<p>音频队列服务希望程序为使用的音频队列缓冲区指定大小。清单2-6展示了一种执行该操作的方法。它得出的缓冲区大小足以容纳给定的音频时长。</p>
<p>该计算考虑了要录制到的音频数据格式。该格式包含可能影响缓冲区大小的所有因素，例如音频通道的数量。</p>
<p><strong>清单2-6</strong> 得出音频录制的音频队列缓冲区的大小</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">void</span> DeriveBufferSize (</span><br><span class="line">    AudioQueueRef                audioQueue,                  <span class="comment">// 1</span></span><br><span class="line">    AudioStreamBasicDescription  &amp;ASBDescription,             <span class="comment">// 2</span></span><br><span class="line">    Float64                      seconds,                     <span class="comment">// 3</span></span><br><span class="line">    <span class="built_in">UInt32</span>                       *outBufferSize               <span class="comment">// 4</span></span><br><span class="line">) &#123;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">const</span> <span class="keyword">int</span> maxBufferSize = <span class="number">0x50000</span>;                 <span class="comment">// 5</span></span><br><span class="line"> </span><br><span class="line">    <span class="keyword">int</span> maxPacketSize = ASBDescription.mBytesPerPacket;       <span class="comment">// 6</span></span><br><span class="line">    <span class="keyword">if</span> (maxPacketSize == <span class="number">0</span>) &#123;                                 <span class="comment">// 7</span></span><br><span class="line">        <span class="built_in">UInt32</span> maxVBRPacketSize = <span class="keyword">sizeof</span>(maxPacketSize);</span><br><span class="line">        AudioQueueGetProperty (</span><br><span class="line">                audioQueue,</span><br><span class="line">                kAudioQueueProperty_MaximumOutputPacketSize,</span><br><span class="line">                <span class="comment">// in Mac OS X v10.5, instead use</span></span><br><span class="line">                <span class="comment">//   kAudioConverterPropertyMaximumOutputPacketSize</span></span><br><span class="line">                &amp;maxPacketSize,</span><br><span class="line">                &amp;maxVBRPacketSize</span><br><span class="line">        );</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    Float64 numBytesForTime =</span><br><span class="line">        ASBDescription.mSampleRate * maxPacketSize * seconds; <span class="comment">// 8</span></span><br><span class="line">    *outBufferSize =</span><br><span class="line">    <span class="built_in">UInt32</span> (numBytesForTime &lt; maxBufferSize ?</span><br><span class="line">        numBytesForTime : maxBufferSize);                     <span class="comment">// 9</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>配置缓冲区大小的音频队列。</li>
<li>音频队列的<code>AudioStreamBasicDescription</code>结构体。</li>
<li>为每个音频队列缓冲区指定的大小（以秒为单位）。</li>
<li>在输出时，每个音频队列缓冲区的大小（以字节为单位）。</li>
<li>音频队列缓冲区大小上限（以字节为单位）。在该示例中，上限设为320 KB。这相当于以96 kHz的采样率采集5秒的24位立体声音频。</li>
<li>对于CBR音频数据，则从<code>AudioStreamBasicDescription</code>结构体中获取固定的数据包大小。使用该值作为最大数据包大小。 该赋值会有副作用，这取决于要录制的音频数据时CBR还是VBR。如果是VBR，则音频队列的<code>AudioStreamBasicDescription</code>会把bytes-per-packet设为<code>0</code>。</li>
<li>对于VBR音频数据，查询音频队列以获取最大的数据包估算大小。</li>
<li>得出缓冲区大小（以字节为单位）。</li>
<li>如果需要，把缓冲区大小限制为之前设置的上限。</li>
</ol>
<h2 id="设置音频文件magic-cookie">设置音频文件Magic Cookie</h2>
<p>某些压缩的音频格式（例如MPEG 4 AAC），利用结构体包含音频元数据。这些结构体称为<strong>magic cookies</strong>。使用音频队列服务以这种格式录制时，必须先从音频队列中获取magic cookie，然后再将其添加到音频文件中，然后开始录制。</p>
<p>清单2-7展示了如何从音频队列中获取magic cookie，并将其应用于音频文件中。代码会在录制之前调用该函数，然后在录制后再次调用，因为某些编解码器会在录制停止时更新magic cookie数据。</p>
<p><strong>清单2-7</strong> 给音频文件设置magic cookie</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">OSStatus SetMagicCookieForFile (</span><br><span class="line">    AudioQueueRef inQueue,                                      <span class="comment">// 1</span></span><br><span class="line">    AudioFileID   inFile                                        <span class="comment">// 2</span></span><br><span class="line">) &#123;</span><br><span class="line">    OSStatus result = noErr;                                    <span class="comment">// 3</span></span><br><span class="line">    <span class="built_in">UInt32</span> cookieSize;                                          <span class="comment">// 4</span></span><br><span class="line"> </span><br><span class="line">    <span class="keyword">if</span> (</span><br><span class="line">            AudioQueueGetPropertySize (                         <span class="comment">// 5</span></span><br><span class="line">                inQueue,</span><br><span class="line">                kAudioQueueProperty_MagicCookie,</span><br><span class="line">                &amp;cookieSize</span><br><span class="line">            ) == noErr</span><br><span class="line">    ) &#123;</span><br><span class="line">        <span class="keyword">char</span>* magicCookie =</span><br><span class="line">            (<span class="keyword">char</span> *) malloc (cookieSize);                       <span class="comment">// 6</span></span><br><span class="line">        <span class="keyword">if</span> (</span><br><span class="line">                AudioQueueGetProperty (                         <span class="comment">// 7</span></span><br><span class="line">                    inQueue,</span><br><span class="line">                    kAudioQueueProperty_MagicCookie,</span><br><span class="line">                    magicCookie,</span><br><span class="line">                    &amp;cookieSize</span><br><span class="line">                ) == noErr</span><br><span class="line">        )</span><br><span class="line">            result =    AudioFileSetProperty (                  <span class="comment">// 8</span></span><br><span class="line">                            inFile,</span><br><span class="line">                            kAudioFilePropertyMagicCookieData,</span><br><span class="line">                            cookieSize,</span><br><span class="line">                            magicCookie</span><br><span class="line">                        );</span><br><span class="line">        free (magicCookie);                                     <span class="comment">// 9</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result;                                              <span class="comment">// 10</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>用于录制的音频队列。</li>
<li>录制到的音频文件。</li>
<li>结果变量，表达该函数是成功还是失败。</li>
<li>用于保存magic cookie数据大小的变量。</li>
<li>从音频队列获取magic cookie数据大小，并存储在<code>cookieSize</code>变量中。</li>
<li>分配一个字节数据来保存magic cookie信息。</li>
<li>通过查询音频队列的<code>kAudioQueueProperty_MagicCookie</code>属性获取magic cookie。</li>
<li>设置录制到的音频文件的magic cookie。<code>AudioFileSetProperty</code>函数在<code>AudioFile.h</code>头文件中声明。</li>
<li>释放临时magic cookie变量的内存。</li>
<li>返回该函数的成功或失败状态。</li>
</ol>
<h2 id="设置音频格式进行录制">设置音频格式进行录制</h2>
<p>本节介绍如何为音频队列设置音频数据格式。音频队列使用该格式记录到文件。</p>
<p>要设置音频数据格式，需要指定：</p>
<ul>
<li>音频数据格式类型（如线性PCM、AAC等）</li>
<li>采样率（如44.1 kHz）</li>
<li>音频通道数（如2，立体声）</li>
<li>位深（如16位）</li>
<li>每数据包帧数量（如线性PCM，每包一帧）</li>
<li>音频文件类型（如CAF、AIFF等）</li>
<li>文件类型所需的音频数据格式的详细信息</li>
</ul>
<p>清单2-8写死了用来录制的音频格式的每个属性值。在生产代码中，通常允许用户部分或全部指定音频格式。无论采用哪种方式，目标都是填充<code>AQRecorderState</code>自定义结构体的<code>mDataFormat</code>字段，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW15">Define a Custom Structure to Manage State</a>中的自定义结构体。</p>
<p><strong>清单2-8</strong> 指定音频队列的音频数据格式</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AQRecorderState aqData;                                       <span class="comment">// 1</span></span><br><span class="line"> </span><br><span class="line">aqData.mDataFormat.mFormatID         = kAudioFormatLinearPCM; <span class="comment">// 2</span></span><br><span class="line">aqData.mDataFormat.mSampleRate       = <span class="number">44100.0</span>;               <span class="comment">// 3</span></span><br><span class="line">aqData.mDataFormat.mChannelsPerFrame = <span class="number">2</span>;                     <span class="comment">// 4</span></span><br><span class="line">aqData.mDataFormat.mBitsPerChannel   = <span class="number">16</span>;                    <span class="comment">// 5</span></span><br><span class="line">aqData.mDataFormat.mBytesPerPacket   =                        <span class="comment">// 6</span></span><br><span class="line">   aqData.mDataFormat.mBytesPerFrame =</span><br><span class="line">      aqData.mDataFormat.mChannelsPerFrame * <span class="keyword">sizeof</span> (SInt16);</span><br><span class="line">aqData.mDataFormat.mFramesPerPacket  = <span class="number">1</span>;                     <span class="comment">// 7</span></span><br><span class="line"> </span><br><span class="line">AudioFileTypeID fileType             = kAudioFileAIFFType;    <span class="comment">// 8</span></span><br><span class="line">aqData.mDataFormat.mFormatFlags =                             <span class="comment">// 9</span></span><br><span class="line">    kLinearPCMFormatFlagIsBigEndian</span><br><span class="line">    | kLinearPCMFormatFlagIsSignedInteger</span><br><span class="line">    | kLinearPCMFormatFlagIsPacked;</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>创建<code>AQRecorderState</code>结构体实例。结构体的<code>mDataFormat</code>字段包含一个<code>AudioStreamBasicDescription</code>结构体。在<code>mDataFormat</code>字段中设置的值提供了音频队列的初始音频格式，这也是记录到文件的音频格式。在<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW9">清单2-10</a>中，你可以获得音频格式的完整规范，Core Audio根据格式类型和文件类型提供了相关规范。</li>
<li>把音频数据格式类型定义为线性PCM。有关可用数据格式的完整列表，可参阅_<a href="https://developer.apple.com/documentation/coreaudio/core_audio_data_types">Core Audio Data Types Reference</a>_。</li>
<li>把采样率设为44.1 kHz。</li>
<li>通道数设为2。</li>
<li>每个通道位深设为16。</li>
<li>每个数据包字节数和每帧字节数设为4（即2个通道乘以每个样本2个字节）。</li>
<li>每个数据包帧数量设为1。</li>
<li>文件类型设为AIFF。参阅<code>AudioFile.h</code>头文件的类型，可获得可用类型的完整列表。可以指定任意已安装的解码器的文件类型，如<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW14">Using Codecs and Audio Data Formats</a>所述。</li>
<li>设置指定文件类型所需的格式标志。</li>
</ol>
<h2 id="创建一个录制音频队列">创建一个录制音频队列</h2>
<p>现在，在设置了录制黑白你函数和音频数据格式之后，创建和配置用于录制的音频队列。</p>
<h3 id="创建录制音频队列">创建录制音频队列</h3>
<p>清单2-9展示了如何创建录制音频队列。注意，<code>AudioQueueNewInput</code>函数使用在之前步骤中配置的回调函数、自定义结构体和音频数据格式。</p>
<p><strong>清单2-9</strong> 创建录制音频队列</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AudioQueueNewInput (                              <span class="comment">// 1</span></span><br><span class="line">    &amp;aqData.mDataFormat,                          <span class="comment">// 2</span></span><br><span class="line">    HandleInputBuffer,                            <span class="comment">// 3</span></span><br><span class="line">    &amp;aqData,                                      <span class="comment">// 4</span></span><br><span class="line">    <span class="literal">NULL</span>,                                         <span class="comment">// 5</span></span><br><span class="line">    kCFRunLoopCommonModes,                        <span class="comment">// 6</span></span><br><span class="line">    <span class="number">0</span>,                                            <span class="comment">// 7</span></span><br><span class="line">    &amp;aqData.mQueue                                <span class="comment">// 8</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>AudioQueueNewInput</code>函数创建一个新的录制音频队列。</li>
<li>录制的音频数据格式。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW4">Set Up an Audio Format for Recording</a>。</li>
<li>于录制音频队列一起使用的回调函数。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW24">Write a Recording Audio Queue Callback</a>。</li>
<li>录制音频队列的自定义数据结构体。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW15">Define a Custom Structure to Manage State</a>。</li>
<li>调用回调函数的run loop。使用<code>NULL</code>指定默认行为，回调函数将在内部的音频队列中的线程执行。这时典型的用法，允许音频队列在程序的用户界面线程等待用户停止录制的同时进行录制。</li>
<li>run loop模式。通常使用<code>kCFRunLoopCommonModes</code>。</li>
<li>保留参数，必须为<code>0</code>。</li>
<li>在输出时，新分配的录制音频队列。</li>
</ol>
<h3 id="从音频队列获取完整的音频格式">从音频队列获取完整的音频格式</h3>
<p>当音频队列创建后（参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW25">Creating a Recording Audio Queue</a>），<code>AudioStreamBasicDescription</code>可能比你填写的更完整，尤其是压缩格式。要获取完整的格式描述，调用清单2-10的<code>AudioQueueGetProperty</code>函数。创建要录制到的音频文件时，需使用完整的音频格式（参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW26">Create an Audio File</a>）。</p>
<p><strong>清单2-10</strong> 从音频队列获取音频格式</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="built_in">UInt32</span> dataFormatSize = <span class="keyword">sizeof</span> (aqData.mDataFormat);       <span class="comment">// 1</span></span><br><span class="line"> </span><br><span class="line">AudioQueueGetProperty (                                    <span class="comment">// 2</span></span><br><span class="line">    aqData.mQueue,                                         <span class="comment">// 3</span></span><br><span class="line">    kAudioQueueProperty_StreamDescription,                 <span class="comment">// 4</span></span><br><span class="line">    <span class="comment">// in Mac OS X, instead use</span></span><br><span class="line">    <span class="comment">//    kAudioConverterCurrentInputStreamDescription</span></span><br><span class="line">    &amp;aqData.mDataFormat,                                   <span class="comment">// 5</span></span><br><span class="line">    &amp;dataFormatSize                                        <span class="comment">// 6</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>获取在查询音频队列有关其音频数据格式时要使用的预期属性值大小。</li>
<li><code>AudioQueueGetProperty</code>函数获取音频队列中指定属性的值。</li>
<li>用于获取音频数据格式的音频队列。</li>
<li>用于获取音频队列的数据格式值的属性ID。</li>
<li>在输出时，从音频队列获得的<code>AudioStreamBasicDescription</code>结构体形式的完整音频数据格式。</li>
<li>输入时，是<code>AudioStreamBasicDescription</code>的预期大小。输出时，是其实际大小。在录制程序中不需要使用该值。</li>
</ol>
<h2 id="创建音频文件">创建音频文件</h2>
<p>创建并配置音频队列后，将创建一个音频文件，把音频数据记录到音频文件中，如清单2-11所示。音频文件使用之前存储在音频队列的自定义结构体中的数据格式和文件格式规范。</p>
<p><strong>清单2-11</strong> 创建一个音频文件进行录制</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="built_in">CFURLRef</span> audioFileURL =</span><br><span class="line">    <span class="built_in">CFURLCreateFromFileSystemRepresentation</span> (            <span class="comment">// 1</span></span><br><span class="line">        <span class="literal">NULL</span>,                                            <span class="comment">// 2</span></span><br><span class="line">        (<span class="keyword">const</span> <span class="built_in">UInt8</span> *) filePath,                        <span class="comment">// 3</span></span><br><span class="line">        strlen (filePath),                               <span class="comment">// 4</span></span><br><span class="line">        <span class="literal">false</span>                                            <span class="comment">// 5</span></span><br><span class="line">    );</span><br><span class="line"> </span><br><span class="line">AudioFileCreateWithURL (                                 <span class="comment">// 6</span></span><br><span class="line">    audioFileURL,                                        <span class="comment">// 7</span></span><br><span class="line">    fileType,                                            <span class="comment">// 8</span></span><br><span class="line">    &amp;aqData.mDataFormat,                                 <span class="comment">// 9</span></span><br><span class="line">    kAudioFileFlags_EraseFile,                           <span class="comment">// 10</span></span><br><span class="line">    &amp;aqData.mAudioFile                                   <span class="comment">// 11</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>CFURLCreateFromFileSystemRepresentation</code>函数（在<code>CFURL.h</code>头文件中声明），创建一个CFURL对象，该对象表示要录制到其中的文件。</li>
<li>使用<code>NULL</code>或<code>kCFAllocatorDefault</code>，使用当前默认的内存分配器。</li>
<li>想要转换为CFURL的文件系统路径。在生产代码中，通常会从用户获取<code>filePath</code>值。</li>
<li>文件系统路径中的字节数。</li>
<li><code>false</code>值表示<code>filePath</code>代表文件，而不是目录。</li>
<li><code>AudioFileCreateWithURL</code>函数（来自<code>AudioFile.h</code>头文件），创建一个新的音频文件，或初始化一个现有文件。</li>
<li>用于创建新的音频文件或使用现在文件进行初始化的URL。该URL是从第一步<code>CFURLCreateFromFileSystemRepresentation</code>获得的。</li>
<li>新文件的文件类型。在本章的示例代码中，之前已通过<code>kAudioFileAIFFType</code>设置为AIFF类型。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW4">Set Up an Audio Format for Recording</a>。</li>
<li>将记录到文件中的音频数据格式，指定为<code>AudioStreamBasicDescription</code>结构体。在本章的示例代码中，也已在“<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW4">Set Up an Audio Format for Recording</a>”中进行了设置。</li>
<li>如果文件已存在，则删除该文件。</li>
<li>在输出时，音频文件对象（<code>AudioFileID</code>类型）表示要录制到的音频文件。</li>
</ol>
<h2 id="设置音频队列缓冲区大小">设置音频队列缓冲区大小</h2>
<p>在准备在录制是使用一组音频队列缓冲区之前，调用之前的<code>DeriveBufferSize</code>函数（参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW14">Write a Function to Derive Recording Audio Queue Buffer Size</a>）。可以把该大小分配给正在使用的录制音频队列，如清单2-12所示：</p>
<p><strong>清单2-12</strong> 设置音频队列缓冲区大小</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">DeriveBufferSize (                               <span class="comment">// 1</span></span><br><span class="line">    aqData.mQueue,                               <span class="comment">// 2</span></span><br><span class="line">    aqData.mDataFormat,                          <span class="comment">// 3</span></span><br><span class="line">    <span class="number">0.5</span>,                                         <span class="comment">// 4</span></span><br><span class="line">    &amp;aqData.bufferByteSize                       <span class="comment">// 5</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>DeriveBufferSize</code>函数（定义在<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW14">Write a Function to Derive Recording Audio Queue Buffer Size</a>），设置合适的音频队列缓冲区大小。</li>
<li>配置缓冲区大小的音频队列。</li>
<li>在录制的文件的音频数据格式。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW4">Set Up an Audio Format for Recording</a>。</li>
<li>每个音频队列缓冲区应保留的秒数。此处设置半秒是个不错的选择。</li>
<li>在输出时，每个音频队列缓冲区的大小（以字节为单位）。该值放在音频队列的自定义结构体中。</li>
</ol>
<h2 id="准备一组音频队列缓冲区">准备一组音频队列缓冲区</h2>
<p>现在，请求音频队列（在<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW2">Create a Recording Audio Queue</a>创建的）准备一组音频队列缓冲区。清单2-13展示了如何操作。</p>
<p><strong>清单2-13</strong> 准备一组音频队列缓冲区</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; kNumberBuffers; ++i) &#123;           <span class="comment">// 1</span></span><br><span class="line">    AudioQueueAllocateBuffer (                       <span class="comment">// 2</span></span><br><span class="line">        aqData.mQueue,                               <span class="comment">// 3</span></span><br><span class="line">        aqData.bufferByteSize,                       <span class="comment">// 4</span></span><br><span class="line">        &amp;aqData.mBuffers[i]                          <span class="comment">// 5</span></span><br><span class="line">    );</span><br><span class="line"> </span><br><span class="line">    AudioQueueEnqueueBuffer (                        <span class="comment">// 6</span></span><br><span class="line">        aqData.mQueue,                               <span class="comment">// 7</span></span><br><span class="line">        aqData.mBuffers[i],                          <span class="comment">// 8</span></span><br><span class="line">        <span class="number">0</span>,                                           <span class="comment">// 9</span></span><br><span class="line">        <span class="literal">NULL</span>                                         <span class="comment">// 10</span></span><br><span class="line">    );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>遍历分配和入队每个音频队列缓冲区。</li>
<li><code>AudioQueueAllocateBuffer</code>函数请求音频队列分配音频队列缓冲区。</li>
<li>执行分配并持有缓冲区的音频队列。</li>
<li>分配的新音频队列缓冲区的大小（以字节为单位）。参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQRecord/RecordingAudio.html#//apple_ref/doc/uid/TP40005343-CH4-SW14">Write a Function to Derive Recording Audio Queue Buffer Size</a>。</li>
<li>在输出时，是新分配的音频队列缓冲区。指向缓冲区的指针放在和音频队列一起使用的自定义结构体中。</li>
<li><code>AudioQueueEnqueueBuffer</code>函数把音频队列缓冲区添加到缓冲区队列的末尾。</li>
<li>向其添加缓冲区的缓冲区队列的音频队列。</li>
<li>正在入队的音频队列缓冲区。</li>
<li>缓冲区入队时未使用该参数。</li>
<li>缓冲区入队时未使用该参数。</li>
</ol>
<h2 id="录制音频">录制音频</h2>
<p>有了前面的代码，录制过程显得格外简单，如清单2-14所示。</p>
<p><strong>清单2-14</strong> 录制音频</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">aqData.mCurrentPacket = <span class="number">0</span>;                           <span class="comment">// 1</span></span><br><span class="line">aqData.mIsRunning = <span class="literal">true</span>;                            <span class="comment">// 2</span></span><br><span class="line"> </span><br><span class="line">AudioQueueStart (                                    <span class="comment">// 3</span></span><br><span class="line">    aqData.mQueue,                                   <span class="comment">// 4</span></span><br><span class="line">    <span class="literal">NULL</span>                                             <span class="comment">// 5</span></span><br><span class="line">);</span><br><span class="line"><span class="comment">// Wait, on user interface thread, until user stops the recording</span></span><br><span class="line">AudioQueueStop (                                     <span class="comment">// 6</span></span><br><span class="line">    aqData.mQueue,                                   <span class="comment">// 7</span></span><br><span class="line">    <span class="literal">true</span>                                             <span class="comment">// 8</span></span><br><span class="line">);</span><br><span class="line"> </span><br><span class="line">aqData.mIsRunning = <span class="literal">false</span>;                           <span class="comment">// 9</span></span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li>初始化数据包索引为<code>0</code>，在音频文件的开头开始录制。</li>
<li>在自定义结构体中设置标志，以指示音频队列正在运行。录制音频队列回调函数使用该标志。</li>
<li><code>AudioQueueStart</code>函数在其自己的线程上启动音频队列。</li>
<li>音频队列开始。</li>
<li>使用<code>NULL</code>表示音频队列应立即开始录制。</li>
<li><code>AudioQueueStop</code>函数停止并重置录制音频队列。</li>
<li>音频队列停止。</li>
<li>使用<code>true</code>来同步停止。有关同步和异步的说明，参阅<a href="https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AboutAudioQueues/AboutAudioQueues.html#//apple_ref/doc/uid/TP40005343-CH5-SW17">Audio Queue Control and State</a>。</li>
<li>在自定义结构体中设置标志，以表示音频队列还没运行。</li>
</ol>
<h2 id="录制后清理">录制后清理</h2>
<p>完成录制后，需要处理音频队列并关闭音频文件，如清单2-15所示。</p>
<p><strong>清单2-15</strong> 录制后清理</p>
<figure class="highlight objectivec"><table><tr><td class="code"><pre><span class="line">AudioQueueDispose (                                 <span class="comment">// 1</span></span><br><span class="line">    aqData.mQueue,                                  <span class="comment">// 2</span></span><br><span class="line">    <span class="literal">true</span>                                            <span class="comment">// 3</span></span><br><span class="line">);</span><br><span class="line"> </span><br><span class="line">AudioFileClose (aqData.mAudioFile);                 <span class="comment">// 4</span></span><br></pre></td></tr></table></figure>
<p>下面是该代码的工作方式：</p>
<ol type="1">
<li><code>AudioQueueDispose</code>函数处理音频队列及其所有资源，包括缓冲区。</li>
<li>要处理的音频队列。</li>
<li>使用<code>true</code>来同步（即立即）处理音频队列。</li>
<li>关闭用于录制的音频文件。<code>AudioFileClose</code>函数在<code>AudioFile.h</code>头文件中声明。</li>
</ol>
<h2 id="总结">总结</h2>
<ul>
<li>使用录制功能一般步骤：
<ol type="1">
<li>定义自定义结构体来管理状态、格式以及路径信息等。</li>
<li>编写回调函数来执行实际的录制数据处理。</li>
<li>填充自定义结构体中的各个字段，包括录制到的文件的数据流、文件路径。</li>
<li>创建音频队列、音频队列缓冲区、要写入的文件。</li>
<li>通知音频队列开始录制。</li>
<li>录制完毕后，通知音频队列停止录制，然后释放。这同时会释放它所拥有的所有缓冲区。</li>
</ol></li>
<li>对于使用OC、Swift代码，可以直接把结构体的字段直接分散到类定义中。</li>
<li>录制回调函数任务：
<ul>
<li>把填充进音频队列缓冲区的内容写入到文件。</li>
<li>把写入文件的音频队列缓冲区排队到队列中。这样才可以接受更多数据。</li>
</ul></li>
<li>回调函数中的<code>const AudioStreamPacketDescription  *inPacketDesc</code>包含VBR包描述的数量（<code>mVariableFramesInPacket</code>），如果是0则表示这是CBR数据。该数据来自编码器。</li>
<li>每个音频队列缓冲区时长可以设置为0.5秒。</li>
</ul>
]]></content>
      <categories>
        <category>翻译</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>Audio Queue Services Programming Guide</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>图解HTTP</title>
    <url>/posts/graphical_http/</url>
    <content><![CDATA[<p>这本书是工作两年后买的，虽然说工作中都基本用不上，但对于面试其内容都是必考题。每次换工作面试都会拿出来读一遍，现在已经是第三刷了，每次读《图解HTTP》都有一种仪式感，嗯，面试了。</p>
<p>《图解HTTP》里面的内容虽然不深，但覆盖范围较广，用于面试也基本足够。之前整理在幕布的笔记太精简了，现在再读一遍，要补充的东西还是挺多的。</p>
<p>所以说这本书的内容更多的是一个引子、导读，更多的细节还是需要再去翻查资料。</p>
]]></content>
      <categories>
        <category>读书笔记</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>图解HTTP</tag>
      </tags>
  </entry>
  <entry>
    <title>图解HTTP：网络基础</title>
    <url>/posts/graphical_http_network_fundamentals/</url>
    <content><![CDATA[<h2 id="tcpip-协议族">TCP/IP 协议族</h2>
<p>与互联网相关联的协议集合起来总称。</p>
<h2 id="tcpip-分层管理">TCP/IP 分层管理</h2>
<ol type="1">
<li>应用层。给用户提供应用服务。如：FTP、DNS、HTTP。</li>
<li>传输层。给应用层提供处理网络连接中的两台计算机之间的数据传输。如：TCP、UDP。</li>
<li>网络层/网络互连层。处理在网络上流动的数据包。在众多的传输路线中做出选择。如：IP。</li>
<li>数据链路层/网络接口层。处理连接网络的硬件部分。</li>
</ol>
<p>数据包是网络传输的最小数据单位。</p>
<h3 id="tcpip-通信传输流">TCP/IP 通信传输流</h3>
<p>通过分层顺序与对方网络通信。</p>
<p>发送端从应用层往下走：</p>
<ul>
<li>发送数据：应用层 -&gt; 传输层 -&gt; 网络层 -&gt; 链路层</li>
<li>每经过一层都打上一层的首部信息，层层封装。</li>
</ul>
<p>接收端则从链路层往上走，一直到达服务器：</p>
<ul>
<li>接收到数据：链路层 -&gt; 网络层 -&gt; 传输层 -&gt; 应用层</li>
<li>每经过一层就将对应的首部消掉，层层解封装。</li>
</ul>
<p>以HTTP举例：</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/HTTP%E4%BC%A0%E8%BE%93.jpg" alt="HTTP传输" /><figcaption aria-hidden="true">HTTP传输</figcaption>
</figure>
<ol type="1">
<li>发送端/客户端在应用层（HTTP协议）发出一个HTTP请求。</li>
<li>为了方便传输，在传输层（TCP协议）把应用层收到的数据（HTTP请求报文）进行分割，并在各个报文上标记序号、端口号，然后转发给网络层。</li>
<li>在网络层（IP协议），增加作为通信目的地的MAC地址后转发给链路层。</li>
<li>接收端/服务器在链路层接收到数据，按序往上发送，一直到应用层，才算真正接收到HTTP请求。</li>
</ol>
<p>每一层的交互通过打上首部信息和消去首部信息（封装与解封装），发送端/客户端把应用层数据层层封装，到接收端/服务器才层层解封装拿到应用层数据。</p>
<h3 id="ip协议">IP协议</h3>
<p>IP，Internet Protocol，网络层。负责把各种数据传输到对方。其中两个重要条件是IP地址和MAC地址。</p>
<p>IP地址指明被分配到的地址；MAC地址指明网卡所属的固定地址。IP地址依赖MAC地址，通常不在发送端与接收端不在同一个局域网时，往往通过ARP协议进行多台计算机和网络设备中转。</p>
<p>中转时，利用下一站中转设备的MAC地址来搜索下一个中转目标。</p>
<p>ARP的作用时解析地址，根据通信方的IP地址反查出对应的MAC地址。</p>
<h3 id="tcp-协议">TCP 协议</h3>
<p>TCP协议，传输层，提供可靠的字节流服务。</p>
<p>为确保数据能到达目标，会进行三次握手策略建立连接：</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/TCP.jpg" alt="TCP" /><figcaption aria-hidden="true">TCP</figcaption>
</figure>
<h4 id="为什么握手要三次挥手却要四次呢">为什么握手要三次，挥手却要四次呢？</h4>
<p>那是因为握手的时候并没有数据传输，所以服务端的 SYN 和 ACK 报文可以一起发送，但是挥手的时候有数据在传输，所以 ACK 和 FIN 报文不能同时发送，需要分两步，所以会比握手多一步。</p>
<h3 id="dns服务">DNS服务</h3>
<p>DNS服务，Domain Name System，应用层，提供域名到IP地址之间的解析服务。</p>
<p>若直接通过IP访问则不需要经过DNS服务。</p>
<h2 id="url-与-uri">URL 与 URI</h2>
<ul>
<li>URL（Uniform Resource Location），统一资源定位符。
<ul>
<li>是 URI 的子集。表达的是一个位置。</li>
</ul></li>
<li>URI（Uniform Resource Identifier），统一资源标识符。</li>
</ul>
<p>URI格式：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">http://user:pass@www.example.com:80/dir/index.html?uid=1#ch1</span><br></pre></td></tr></table></figure>
<ul>
<li><code>http</code>：协议方案名</li>
<li><code>user:pass</code>：登录认证信息</li>
<li><code>www.example.com</code>：服务器地址</li>
<li><code>80</code>：服务器端口号</li>
<li><code>dir/index.html</code>：带层级的文件路径</li>
<li><code>uid=1</code>：查询字符串</li>
<li><code>ch1</code>：片段标识符</li>
</ul>
]]></content>
      <categories>
        <category>读书笔记</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>图解HTTP</tag>
      </tags>
  </entry>
  <entry>
    <title>图解HTTP：简单的HTTP协议</title>
    <url>/posts/graphical_http_simple_http_protocol/</url>
    <content><![CDATA[<p>应用HTTP协议时，必定时一端担任客户端角色，一端是担任服务器端角色。请求必定由客户端发出，而服务器端回复响应。由此达成通信。</p>
<h2 id="http是无状态协议">HTTP是无状态协议</h2>
<p>HTTP本质是无状态的。为了更快地处理大量事务，确保协议的可伸缩性。HTTP协议对于发送过的请求或响应都不做持久化处理。有新的请求发送时，就会有新的响应产生。</p>
<p>使用Cookies可以创建有状态的会话。把Cookies添加到头部中，创建一个会话让每次请求都能共享相同的上下文信息，达成相同的状态。</p>
<p>实际的数据状态可能存在服务器中，但通过 Cookie 将一个 id 信息传到客户端，并由客户端持久化，客户端通过 id 就可获得数据的状态。</p>
<h2 id="使用方法告知意图">使用方法告知意图</h2>
<p>通过HTTP请求方法，简单告知请求的意图。</p>
<ul>
<li><code>GET</code>：获取资源。</li>
<li><code>POST</code>：传输实体的主体。通过body传输数据。</li>
<li><code>PUT</code>：传输文件。但该方法不存在验证机制，任何人都可以上传文件，存在安全性问题。</li>
<li><code>HEAD</code>：获取报文首部。用于确认URI的有效性以及资源更新的日期时间等。</li>
<li><code>DELETE</code>：删除文件。与PUT对应，但也没有验证机制，存在安全性问题。</li>
<li><code>OPTIONS</code>：查询服务器支持的方法。</li>
<li><code>TRACE</code>：追踪路径。容易引发跨站攻击，通常不会用到。</li>
<li><code>CONNECT</code>：要求用隧道协议连接代理。要求在与代理服务器通信时建立隧道，实现用隧道协议进行TCP通信。主要是用SSL和TSL协议把通信内容加密后经网络隧道传输。</li>
</ul>
<h3 id="持久连接节省通信量">持久连接节省通信量</h3>
<p>大量的 TCP 连接建立和断开，都会加载通信量的开销。为此，推出了持久连接（HTTP Persistent Connections，也称为 HTTP Keep-alive 或 HTTP connection reuse）方法。</p>
<p>只要任意一端没有明确提出断开连接，则保持 TCP 连接状态。</p>
<p>持久化使得多数请求以管线化（pipelining）方式发送成为可能。这样就能够做到同时并行发送多个请求。</p>
]]></content>
      <categories>
        <category>读书笔记</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>图解HTTP</tag>
      </tags>
  </entry>
  <entry>
    <title>图解HTTP：HTTP报文</title>
    <url>/posts/graphical_http_http_message/</url>
    <content><![CDATA[<p>用于 HTTP 协议的交互信息被称为 HTTP 报文。请求端/客户端的叫请求报文，相应端/服务器端的叫响应报文。报文其实就是个字符串。</p>
<h2 id="组成">组成</h2>
<p>报文首部、报文主体（可选）</p>
<p>报文首部组成：</p>
<ul>
<li>请求行
<ul>
<li>请求方法、URI、HTTP 版本</li>
</ul></li>
<li>状态行
<ul>
<li>响应结果的状态码、原因短语、HTTP 版本</li>
</ul></li>
<li>首部字段
<ul>
<li>通用首部（General Heaader Fields）
<ul>
<li>请求报文和响应报文都会使用的首部。</li>
</ul></li>
<li>请求首部（Request Header Fields）
<ul>
<li>发送请求报文使用的首部。补充了请求的附加内容、客户端信息、响应内容相关优先级等信息。</li>
</ul></li>
<li>响应首部（Response Header Fields）
<ul>
<li>返回响应报文时 使用的首部。补充了响应的附加内容，也会要求客户端附加额外的内容信息。</li>
</ul></li>
<li>实体首部（Entity Header Fields）
<ul>
<li>针对请求报文和响应报文的实体部分使用的首部。补充了资源内容更新时间等与实体有关的信息。</li>
</ul></li>
</ul></li>
<li>其他（HTTP 协议未定义的其他内容）</li>
</ul>
<p>具体的请求报文与响应报文：</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/HTTP_Request.png" /></p>
<p>请求报文组成：</p>
<ul>
<li>请求方法</li>
<li>请求URI</li>
<li>协议版本</li>
<li>可选请求首部字段</li>
<li>内容实体</li>
</ul>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/HTTP_Response.png" /></p>
<p>响应报文组成：</p>
<ul>
<li>协议版本</li>
<li>状态码</li>
<li>解析状态码的原因短语</li>
<li>可选的响应首部字段</li>
<li>实体主体</li>
</ul>
<h2 id="报文主体与实体主体">报文主体与实体主体</h2>
<p>报文（message）：是 HTTP 通信的基本单位，由 8 个组字节流（octet sequence，其中 octet 为 8 个比特）组成，通过 HTTP 通信传输。</p>
<p>实体（entity）：作为请求或响应的有效载荷数据（补充项）被传输，其内容由实体首部和实体主体组成。</p>
<p>通常报文主体等于实体主体，仅当传输中进行编码操作时，实体主体的内容发生变化。</p>
<h2 id="通过编码提升传输速度">通过编码提升传输速度</h2>
<p>HTTP在传输数据时可以原样直接传输，但也可以在传输过程中通过编码提升传输速度。但编码需要计算机来完成，虽然会提升传输速率，但也会因此需要更多的CPU资源。</p>
<h3 id="压缩传输的内容编码">压缩传输的内容编码</h3>
<p>服务器在实体内容上压缩，客户端接收并解压实体。</p>
<p>常用的内容编码：</p>
<ul>
<li>gzip，GUN zip</li>
<li>compress，UNIX系统的标准压缩</li>
<li>deflate，zlib</li>
<li>identity，不进行压缩</li>
</ul>
<h3 id="分割发送的分块传输编码">分割发送的分块传输编码</h3>
<p>把实体内容分块。每一块都会用十六进制来标记块的大小，而实体主体的最后一块会使用“0(CR+LF)”来标记。</p>
<h2 id="多部分对象集合">多部分对象集合</h2>
<p>发送的一份报文主体可含有多类型实体，通常是图片或文本文件等上传时使用。多部份对象集合包含的对象如下：</p>
<ul>
<li>multipart/form-data
<ul>
<li>在 Web 表单文件上传时使用。</li>
</ul></li>
<li>multipaprt/byteranges
<ul>
<li>状态码 206，响应报文包含了多个范围的内容时使用。</li>
</ul></li>
</ul>
<p>iOS中URLSession的uploadTask默认实现只适合PUT方法上传文件，要是要multipart/form-data上传，还需要自己拼接内容实体。</p>
<h2 id="范围请求">范围请求</h2>
<p>应用：断点下载、上传。</p>
<p>用到首部字段Range来指定资源的字节范围。</p>
<h2 id="内容协商">内容协商</h2>
<p>客户端和服务器端就响应的资源内容进行交涉，然后提供给客户端最合适的资源。内容协商会以语言、字符串、编码方式等为基准判断响应的资源。即使用以下首部字段：</p>
<ul>
<li>Accept</li>
<li>Accept-Charset</li>
<li>Accept-Encoding</li>
<li>Accept-Language</li>
<li>Connet-Language</li>
</ul>
<p>协商技术分类：</p>
<ul>
<li>服务器驱动协商（Server-driven Negotiation）
<ul>
<li>以请求的首部字段为参考，在服务端自动处理。</li>
</ul></li>
<li>客户端驱动协商（Agent-driven Negotiation）
<ul>
<li>从浏览器显示的可选项列表中手动选择。</li>
</ul></li>
<li>透明协商（Transparent Negotiation）
<ul>
<li>上面两者的结合体，由各自进行内容协商的一种方法。</li>
</ul></li>
</ul>
]]></content>
      <categories>
        <category>读书笔记</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>图解HTTP</tag>
      </tags>
  </entry>
  <entry>
    <title>图解HTTP：HTTP状态码</title>
    <url>/posts/graphical_http_http_status_code/</url>
    <content><![CDATA[<ul>
<li>1xx，Informational，信息状态码。接收的请求正在处理。</li>
<li>2xx，Success，成功状态码。请求正常处理完毕。</li>
<li>3xx，Redirection，重定向状态码。需要进行附加操作以完成请求。</li>
<li>4xx，Client Error，客户端错误状态码。服务器无法处理请求。</li>
<li>5xx，Server Error，服务器错误状态码。服务器处理请求出错。</li>
</ul>
]]></content>
      <categories>
        <category>读书笔记</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>图解HTTP</tag>
      </tags>
  </entry>
  <entry>
    <title>图解HTTP：Web服务器</title>
    <url>/posts/graphical_http_web_server/</url>
    <content><![CDATA[<p>用单台虚拟主体实现多个域名。相同的 IP 地址可能会有多个不同主机名和域名的 Web 网站，所以在发送 HTTP 请求时，必须在 Host 首部内完整指定主机看或域名的 URI。</p>
<h2 id="通信数据转发程序">通信数据转发程序</h2>
<p>这些应用程序和服务器可以将请求转发给通信线路的下一站服务器，并且能接收从那台服务器发送的响应，再转发给客户端。</p>
<h3 id="代理">代理</h3>
<p>有转发功能的应用程序。接收由客户端发送的请求并转发给服务器，同时也接收服务器返回的响应并转发给客户端。</p>
<p>分类基准：</p>
<ul>
<li>缓存代理，CDN</li>
<li>透明代理
<ul>
<li>不对报文进行任何加工的代理，反之称为非透明代理。</li>
</ul></li>
</ul>
<h3 id="网关">网关</h3>
<p>转发其他服务器通信数据的服务器，接收从客户端发送来的请求时，它就像自己拥有资源的源服务器一样对请求进行处理。</p>
<p>网关能使通信线路上的服务器提供非 HTTP 协议服务。</p>
<h3 id="隧道">隧道</h3>
<p>在相隔甚远的客户端和服务器两者进行中转，并保持双方通信连接的应用程序。</p>
<p>不会去解析 HTTP 请求，保持原样中转给之后的服务器。隧道会在通信双方断开连接时结束。</p>
<h2 id="缓存">缓存</h2>
<p>缓存是指代理服务器或客户端本地磁盘内保存的资源副本。利用缓存可以减少对源服务器的访问，节省了通信流量和通信时间。</p>
<p>缓存分类：</p>
<ul>
<li>服务器缓存，通过缓存代理服务器实现。当代理转发从服务器返回的响应时，本身就会保存一份副本，下次请求就可以从代理服务器响应。</li>
<li>客户端缓存。通过临时网络文件缓存响应资源。</li>
</ul>
<p>若缓存有效则使用缓存，否则请求源服务器，请求新资源。</p>
<p>注意在iOS中，使用URLSession的GET请求，会自动使用URLCache进行缓存响应资源。根据服务器的响应头部字段自动缓存和更新资源。</p>
]]></content>
      <categories>
        <category>读书笔记</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>图解HTTP</tag>
      </tags>
  </entry>
  <entry>
    <title>图解HTTP：HTTPS</title>
    <url>/posts/graphical_http_https/</url>
    <content><![CDATA[<h2 id="http的缺点">HTTP的缺点</h2>
<ul>
<li>通信使用明文（不加密），内容可能会被窃听。</li>
<li>不验证通信的身份，因此有可能遭遇伪装。</li>
<li>无法验证报文的完整性，所以有可能篡改。</li>
</ul>
<p>未加密的协议都会出现类似的问题。</p>
<h2 id="https-http-加密-认证-完整性保护">HTTPS = HTTP + 加密 + 认证 + 完整性保护</h2>
<p>HTTPS 是身披 SSL 外壳的 HTTP。HTTPS 并非是应用层的一种新协议，只是 HTTP 通信接口部分用 SSL 和 TLS 协议代替而已。</p>
<p>原本HTTP直接和TCP通信；使用SSL时，先和SSL通信，再由SSL和TCP通信。</p>
<p>SSL是独立于HTTP协议的，应用层的其他协议也可以配合SSL协议实现网络安全。</p>
<h3 id="相互交换密钥的公开密钥加密技术">相互交换密钥的公开密钥加密技术</h3>
<p>SSL使用公开密钥加密的加密处理方式。公开密钥加密使用一对非对称的密钥——公钥、私钥。</p>
<p>公钥加密，私钥解密。客户端持有公钥，用私钥对发送的信息加密；服务器持有私钥，用私钥对客户端发送的密文解密。</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/%E9%9D%9E%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86.jpg" alt="非对称加密" /><figcaption aria-hidden="true">非对称加密</figcaption>
</figure>
<p>注意：客户端的私钥是服务器事先传递过去的，即服务器先生成一对公钥私钥，把私钥发送到客户端，客户端才能用私钥进行加密。</p>
<p>在HTTPS中，对称加密与非对称加密混合使用。因为非对称加密适合加密小量数据，一般数据应使用对称加密进行加密解密。所以在HTTPS中，使用非对称加密的是对称密钥，即客户端把对称密钥用私钥加密发送到服务器，后续服务器用对称密钥加密解密一般的消息。</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/%E6%B7%B7%E5%90%88%E5%8A%A0%E5%AF%86.jpg" alt="混合加密" /><figcaption aria-hidden="true">混合加密</figcaption>
</figure>
<h3 id="确保服务端公钥安全">确保服务端公钥安全</h3>
<p>因为客户端需要用服务端的公钥进行加密，所以首先要确保客户端能拿到正确的服务端公钥。公钥在下发的时候会被替换劫持，这里通过第三方认证机（CA）构确认公钥的正确性。</p>
<p>认证的方式通过数字签名校验实现，简单来说就是CA私钥加密HASH（通过内容生成），公钥解密得出HASH，比对从收到的内容生成的HASH是否相等。</p>
<p>这个过程如下：</p>
<p>前提准备：</p>
<ul>
<li>客户端提前安装了CA的公钥。</li>
<li>服务端获取CA颁发的证书。
<ol type="1">
<li>服务端生成一对公钥、私钥，私钥自己存着，公钥最终要传给客户端。</li>
<li>把公钥登记到CA，CA对公钥内容做HASH，对HASH值用CA私钥加密，密文+公钥打包成证书发送给服务器。</li>
</ol></li>
</ul>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%BA%8B%E5%85%88%E5%AE%89%E8%A3%85CA%E5%85%AC%E9%92%A5.jpg" alt="客户端事先安装CA公钥" /><figcaption aria-hidden="true">客户端事先安装CA公钥</figcaption>
</figure>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/%E5%85%AC%E9%92%A5%E8%BD%AC%E8%AF%81%E4%B9%A6.jpg" alt="公钥转证书" /><figcaption aria-hidden="true">公钥转证书</figcaption>
</figure>
<p>校验流程：</p>
<ol type="1">
<li>服务端发送公钥证书给客户端；</li>
<li>客户端对证书中的密文用CA的公钥解密得出HASH，对证书中的公钥内做HASH，对比两个HASH值。正确则提取公钥存到客户端。</li>
<li>客户端使用服务端的公钥加密与服务器传输，开始加密通信。使用上述的混合加密方式，即非对称加密交换对称加密密钥，然后双方使用对称密钥进行加密通信。</li>
</ol>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/%E6%8F%90%E5%8F%96%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%85%AC%E9%92%A5.jpg" alt="提取服务端公钥" /><figcaption aria-hidden="true">提取服务端公钥</figcaption>
</figure>
<h3 id="rsa公钥私钥的作用助记">RSA公钥、私钥的作用助记</h3>
<p>既然是加密，那肯定是不希望别人知道我的消息，所以只有我才能解密，所以可得出<strong>公钥负责加密，私钥负责解密</strong>；同理，既然是签名，那肯定是不希望有人冒充我发消息，只有我才能发布这个签名，所以可得出<strong>私钥负责签名，公钥负责验证</strong>。</p>
<p>数字签名就是使用私钥对数据摘要进行签名，并附带和数据一起发送。可以起到防篡改、防伪装、防否认的作用。</p>
<p>证书则是由CA机构自己的私钥签发的数字签名。解决的签名的权威性问题，奠定了信任链的基础。</p>
<h3 id="https安全通信过程">HTTPS安全通信过程</h3>
<p>服务端已经生成公钥、私钥，并通过CA获得公钥证书。客户端已经事先安装CA公钥。</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/HTTPS%E8%BF%87%E7%A8%8B.jpg" alt="HTTPS过程" /><figcaption aria-hidden="true">HTTPS过程</figcaption>
</figure>
<p>具体过程：</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/HTTPS%E8%BF%87%E7%A8%8B_%E5%85%B7%E4%BD%93.jpg" alt="HTTPS过程_具体" /><figcaption aria-hidden="true">HTTPS过程_具体</figcaption>
</figure>
<ol type="1">
<li>客-&gt;服，Handshake: ClientHello</li>
</ol>
<ul>
<li>客户端通过发送 Client Hello 报文开始 SSL 通信。</li>
<li>报文中包含客户端支持的 SSL 的指定版本、加密组件（Clipher Suite）列表（所使用的加密算法及密钥长度等）。</li>
</ul>
<ol start="2" type="1">
<li>服-&gt;客，Handshake: ServerHello</li>
</ol>
<ul>
<li>服务器进行 SSL 通信，以 Server Hello 报文作为应答。</li>
<li>和客户端一样，在报文中包含 SSL 版本以及加密组件。</li>
<li>服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的。</li>
</ul>
<ol start="3" type="1">
<li>服-&gt;客，Handshake: Certificate</li>
</ol>
<ul>
<li>服务器发送 Certificate 报文。报文中包含公钥证书。</li>
</ul>
<ol start="4" type="1">
<li>服-&gt;客，Handshake: ServerHelloDone</li>
</ol>
<ul>
<li>最后服务器发送 Server Hello Done 报文通知客户端，最初阶段的 SSL 握手协商部分结束。</li>
</ul>
<ol start="5" type="1">
<li>客-&gt;服，Handshake: ClientKeyExchange</li>
</ol>
<ul>
<li>第一次握手结束后，客户端以 Client Key Exchange 报文作为回应。</li>
<li>报文中包含通信加密中使用的一种称为 Pre-master secret 的随机密码串，该密码已用步骤 3 的公钥进行加密。</li>
</ul>
<ol start="6" type="1">
<li>客-&gt;服，ChangeCipherSpec</li>
</ol>
<ul>
<li>客户端继续发送 Change Cipher Spec 报文。告诉服务器之后的通信将采用该密码进行加密。</li>
</ul>
<ol start="7" type="1">
<li>客-&gt;服，Handshake: Finished</li>
</ol>
<ul>
<li>客户端发送 Finished 报文。该报文包含链接至今全部报文的整体校验值。</li>
<li>这次握手协商成功的标准是服务器能正确解密该报文。</li>
</ul>
<ol start="8" type="1">
<li>服-&gt;客，ChangeClipherSpec</li>
<li>服-&gt;客，Handshake: Finished</li>
<li>客-&gt;服，Application Data（HTTP）</li>
</ol>
<ul>
<li>客户端和服务端的 Finished 报文交换完毕后，SSL 连接建立完成。从此开始发送 HTTP 请求。</li>
</ul>
<ol start="11" type="1">
<li>服-&gt;客，Application Data（HTTP）</li>
</ol>
<ul>
<li>应用层协议通信，发送 HTTP 响应。</li>
</ul>
<ol start="12" type="1">
<li>客-&gt;服，Alert: warning, close notify</li>
</ol>
<ul>
<li>由客户端断开连接。</li>
</ul>
<p>参考：<a href="https://www.cnblogs.com/fengf233/p/11775415.html">HTTPS加密流程理解 - fengf233 - 博客园</a></p>
<h3 id="单向认证与双向认证">单向认证与双向认证</h3>
<p>上述的HTTPS基本流程就是单向认证，即指认证服务端的证书。单向认证中需要额外代码的情况往往是服务器下发的证书是CA颁发的，而是自签的，所以在检验服务端私钥证书时就要自定义的逻辑。</p>
<p>而双向认证，则是在ServerHelloDone前，发送来自客户端的公钥证书，服务端收到后用根证书解密客户端证书，取出客户端私钥。双向认证在确保了服务端的正确性，也确保了客户端的正确性。</p>
<p>两者过程对比：</p>
<p>单向认证：</p>
<ol type="1">
<li>客-&gt;服，发起HTTPS连接请求，把SSL协议版本发送给服务端。</li>
<li>服-&gt;客，发送服务端把本机公钥证书（server.crt）。</li>
<li>客，校验公钥证书（server.crt），取出服务端公钥。</li>
<li>客-&gt;服，并发送用服务端公钥加密的随机生成密钥R。</li>
<li>服，用私钥（server.key）解密得出密钥R。</li>
<li>服&lt;-&gt;客，用密钥R进行加密通信。</li>
</ol>
<p>双向认证：</p>
<ol type="1">
<li>同上（客-&gt;服，发起HTTPS连接请求，把SSL协议版本发送给服务端）。</li>
<li>同上（服-&gt;客，发送服务端把本机公钥证书（server.crt））。</li>
<li>同上（客，校验公钥证书（server.crt），取出服务端公钥）。</li>
<li><strong>客-&gt;服，把自己的公钥证书（client.crt）发送给服务端。</strong></li>
<li><strong>服，用根证书（root.crt）解密客户端公钥证书，拿到客户端公钥。</strong></li>
<li><strong>客-&gt;服，发送自己支持的加密方案。</strong></li>
<li><strong>服-&gt;客，根据双端能力，选择双方都能接受的加密方案，使用客户端公钥加密后发送。</strong></li>
<li><strong>客-&gt;服，使用私钥解密加密方案，生成随机密钥R，使用服务端公钥加密后发送。</strong></li>
<li>同上5（服，用私钥（server.key）解密得出密钥R）。</li>
<li>同上6（服&lt;-&gt;客，用密钥R进行加密通信）。</li>
</ol>
<p>参考：</p>
<ul>
<li><a href="https://www.jianshu.com/p/2b2d1f511959">HTTPS双向认证指南 - 简书</a></li>
<li><a href="https://www.cnblogs.com/yaphetsfang/articles/12858356.html">HTTPS单向认证、双向认证、抓包原理、反抓包策略 - yaphetsfang - 博客园</a></li>
</ul>
<h3 id="ssl和tls">SSL和TLS</h3>
<p>SSL 先有，TSL 是以 SSL 为原型开发的协议，有时会统一称该协议为 SSL。</p>
<h3 id="不推荐一直使用https">不推荐一直使用HTTPS</h3>
<ul>
<li>与明文通信相比，加密通信会消耗更多的 CPU 及内存资源。所以敏感信息才使用 HTTPS 加密通信。</li>
<li>证书的费用开销。</li>
</ul>
]]></content>
      <categories>
        <category>读书笔记</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>图解HTTP</tag>
      </tags>
  </entry>
  <entry>
    <title>图解HTTP：认证</title>
    <url>/posts/graphical_http_certification/</url>
    <content><![CDATA[<p>认证，即确认对方身份。一般核对这些信息：</p>
<ul>
<li>密码。只有本人才会知道的字符串信息。</li>
<li>动态令牌。仅限本人持有的设备内显示的一次性密码。</li>
<li>数字令牌。仅限本人（终端）持有的信息。</li>
<li>生物认证。指纹和虹膜等本人的生理信息。</li>
<li>IC 卡等。仅限本人持有的信息。</li>
</ul>
<p>HTTP使用的认证方式：</p>
<ul>
<li>BASIC认证，基本认证。</li>
<li>DIGEST认证，摘要认证。</li>
<li>SSL客户端认证。</li>
<li>FromBase认证，基于表单认证。</li>
</ul>
<h2 id="basic-认证">BASIC 认证</h2>
<p>base64 发送明文密码。</p>
<p>问题：</p>
<ul>
<li>明文传输，可窃听。</li>
<li>一般浏览器无法实现认证注销操作。</li>
<li>缺乏灵活，安全性差。</li>
</ul>
<h2 id="digest-认证">DIGEST 认证</h2>
<p>使用质询（challenge/reponse），但不直接发送明文密码。</p>
<p>一开始一方先发送认证要求给对方，接着使用从另一方接收到的质询码计算生成响应码。最后将响应码返回给对方进行认证。</p>
<h2 id="ssl-客户端认证">SSL 客户端认证</h2>
<p>借由 HTTPS 的客户端证书来完成认证的方式，没错，这里就是上面的双向认证。步骤：</p>
<ol type="1">
<li>接收到需要认证资源的请求，服务器会发送 Certificate Request 报文，要求客户端提供客户端证书。</li>
<li>用户选择将发送的客户端证书后，客户端会把客户端证书以 Client Certificate 报文发送给服务器。</li>
<li>服务器验证客户端证书通过后，方可领取证书内客户端的空开密钥，然后开始 HTTPS 加密通信。</li>
</ol>
<p>双因素认证（Tow-factor authentication）</p>
<ul>
<li><p>SSL 客户端认证通常会和基于表单认证组合形成一种双因素认证。</p>
<ul>
<li><p>SSL 客户端证书：认证客户端计算机</p></li>
<li><p>密码：用来确定这是用户本人的行为</p></li>
</ul></li>
</ul>
]]></content>
      <categories>
        <category>读书笔记</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>图解HTTP</tag>
      </tags>
  </entry>
  <entry>
    <title>图解HTTP：基于HTTP的功能追加协议</title>
    <url>/posts/graphical_http_http_based_function_addition_protocol/</url>
    <content><![CDATA[<p>HTTP 的瓶颈：</p>
<ul>
<li>一条连接上只可发送一个请求。</li>
<li>请求只能从客户端开始。客户端不可以接收除响应以外的指令。</li>
<li>请求、响应首部未经压缩就发送。首部信息越多延迟越大。</li>
<li>发冗长的首部。每次相互发送相同的首部造成的浪费较多。</li>
<li>可任意选择数据压缩格式。非强制压缩发送。</li>
</ul>
<h2 id="spdy">SPDY</h2>
<p>SPDY 的出现正是为了解决这些问题，SPDY 没有完全改写 HTTP 协议，而是在 TCP/IP 的应用层与传输层之间通过插入会话层的形式运作。同时，SPDY 规定通信中使用 SSL。</p>
<ul>
<li>HTTP - 应用层</li>
<li>SPDY - 会话层</li>
<li>SSL - 表示层</li>
<li>TCP - 传输层</li>
</ul>
<p>使用 SPDY 后，HTTP 协议额外获得以下功能：</p>
<ul>
<li>多路复用流。一个 TCP 连接上，处理所有的 HTTP 请求。</li>
<li>赋予请求优先级。</li>
<li>压缩 HTTP 首部。</li>
<li>推送功能。这样服务器可直接发送数据，而不必等待客户端请求。</li>
<li>服务器提示功能。服务器可以主动提示客户端请求所需的资源。由于在客户端发现资源之前就可以获知资源的存在，因此在资源已缓存等情况下，可以避免发送不必要的请求。</li>
</ul>
<p>然而 SPDY 也不是完美的，也还有一些未解决的问题：</p>
<ul>
<li>只是将单个域名（IP 地址）的通信多路复用，所以当使用多个域名下的资源时，效果将受到限制。</li>
<li>还有一些不是 HTTP 协议导致的问题，如 web 内容的编写方式。</li>
</ul>
<h2 id="websocket">WebSocket</h2>
<p>WebSocket是Web浏览器与Web服务器之间全双工通信。</p>
<p>这是一套独立协议，一旦 Web 服务器与客户端之间建立起 WebSocket 协议的通信连接，之后所有的通信都依靠这个专用协议进行。通信过程中可相互发送 JSON、XML、HTML 或图片等任意格式的数据。</p>
<p>但由于是建立在 HTTP 基础上的协议，连接的发起方仍是客户端，而一旦 WebSocket 通信连接后，不论服务器还是客户端，任意一端都可直接向对方发送报文。</p>
<p>建立连接时还是用HTTP协议请求、响应完成握手，后续就不再发送HTTP数据帧，而是WebSocket独立数据帧。</p>
<p>特点：</p>
<ul>
<li>推送功能。服务器可直接发送数据，不必等客户端的请求。</li>
<li>减少通信量。只要建立起 WebSocket 连接，就希望一直保持连接状态。而且 WebSocket 的首部信息也少，连接次数、通信量都相应减少。</li>
<li>建立在TCP协议之上，服务端实现比较容易。</li>
<li>与HTTP协议有良好兼容性，默认端口相同，握手阶段也是用HTTP协议，不容易被屏蔽。</li>
<li>可以发送文本，也可以发送二进制。</li>
<li>没有同源限制，客户端可以与任意服务器通信。</li>
</ul>
<h2 id="http2.0">HTTP/2.0</h2>
<h3 id="新的概念">新的概念</h3>
<p>帧：数据通信的最小单位，以二进制压缩格式存放内容。来自不同数据流的帧可以交错发送，然后根据每个帧头的数据流标识符重新组装。帧信息包含：类型、长度、标记、流标识、palyload。</p>
<p>消息：HTTP/2.0中逻辑上的HTTP消息，如请求和响应，消息由一个或多个帧组成。</p>
<p>流：连接中的虚拟信道，可以承载双向消息传输，包含1或多条消息。每个流有唯一整数标识符。为了防止两端双向流标识符冲突，客户端发起的流具有奇数ID，服务端发起的流具有偶数ID。特点：</p>
<ul>
<li>双向性：同一流内，可以同时发送和接收数据。</li>
<li>有序性：流中传输二进制帧，帧在流上的被发送和被接收都是按序进行的。</li>
<li>并行性：流中的二进制帧都并行传输的，无需按序等待。帧可以乱序发送，然后再根据每个帧首部的流标识符重新组装。</li>
<li>流的创建和关闭可以被客户端或服务端任意一方执行。</li>
</ul>
<p>连接：包含1或多个流，所有通信都在一个TCP连接上完成。该连接可以承载任意数量的双向数据流。</p>
<h3 id="二进制分帧">二进制分帧</h3>
<p>把传输信息分为Header帧和Data帧，对应HTTP/1.x的首部信息和实体信息。</p>
<h3 id="多路复用共享连接">多路复用/共享连接</h3>
<p>HTTP/1.x中虽然可以通过长连接在一个连接中发起多个请求，并处理每个请求的响应。但客户端在同一域名下的请求会有一定数量限制，超出会被阻塞，要实现多流并行，只能开启多个TCP连接。</p>
<p>HTTP/2.0单个TCP连接可以承载任意数量的双向数据流，并且可以并处请求和响应，实现单个连接的多路复用、共享连接。</p>
<h3 id="首部压缩">首部压缩</h3>
<p>HTTP/2.0在客户端和服务端使用首部表来跟踪和存储之前发送的键值对，对于相同的数据，不在重复发送。</p>
<p>首部表在HTTP/2.0连接期间始终存在，由客户端和服务端共同渐进更新。</p>
<h3 id="请求优先级">请求优先级</h3>
<p>每个流都可以带上一个31 bit的优先值。服务器可以根据流的优先级，控制资源分配。</p>
<h3 id="服务端推送">服务端推送</h3>
<p>服务端可以对客户端请求发送多个响应，服务端向客户端推送资源无需明确发起请求。这可以让在遵循同源的情况下，不同的页面可以共享缓存资源。</p>
]]></content>
      <categories>
        <category>读书笔记</category>
      </categories>
      <tags>
        <tag>网络</tag>
        <tag>图解HTTP</tag>
      </tags>
  </entry>
  <entry>
    <title>CMSampleBuffer专题</title>
    <url>/posts/cmsamplebuffer/</url>
    <content><![CDATA[<h1 id="cmsamplebuffer专题">CMSampleBuffer专题</h1>
<h2 id="概述">概述</h2>
<p>CMSampleBuffer是一个包含零个或多个压缩或未压缩（compressed or uncompressed），特定媒体类型的样本（音频、视频、多路复用等）。</p>
<p>一个CMSampleBuffer可以包含以下之一的核心数据：</p>
<ul>
<li>CMBlockBuffer，包含一个或多个媒体样本。</li>
<li>CVImageBuffer，是对CMSampleBuffer流格式描述的引用，包含每个媒体样本的大小、时序信息，以及缓冲区级别和样本基本的附件。</li>
</ul>
<p>sample buffer可以包含样本级别和缓冲区级别的附件。样本级别附件与缓冲区的每个样本（帧）相关联，并包含诸如时间戳和视频帧相关信息。缓冲区级别附件提供有关缓冲区整体的信息，如播放速度和消费缓冲区时执行的操作。</p>
<h2 id="数据来源">数据来源</h2>
<ul>
<li>通过采集设备（摄像头、麦克风）采集的音频或视频数据。</li>
</ul>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// AVCaptureVideoDataOutputSampleBufferDelegate、AVCaptureAudioDataOutputSampleBufferDelegate</span></span><br><span class="line"><span class="keyword">optional</span> <span class="keyword">func</span> <span class="title function_">captureOutput</span></span><br><span class="line">(<span class="keyword">_</span> <span class="params">output</span>: <span class="type">AVCaptureOutput</span>, </span><br><span class="line"> <span class="params">didOutput</span> <span class="params">sampleBuffer</span>: <span class="type">CMSampleBuffer</span>, </span><br><span class="line"> <span class="params">from</span> <span class="params">connection</span>: <span class="type">AVCaptureConnection</span>)</span><br></pre></td></tr></table></figure>
<ul>
<li>读取视频文件的输出流AVAssetReaderOutput。</li>
</ul>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">func</span> <span class="title function_">copyNextSampleBuffer</span>() -&gt; <span class="type">CMSampleBuffer</span>?</span><br></pre></td></tr></table></figure>
<ul>
<li>ARKit ARSessionObserver输出捕获的audio sample buffer。</li>
</ul>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">optional</span> <span class="keyword">func</span> <span class="title function_">session</span></span><br><span class="line">(<span class="keyword">_</span> <span class="params">session</span>: <span class="type">ARSession</span>, </span><br><span class="line"> <span class="params">didOutputAudioSampleBuffer</span> <span class="params">audioSampleBuffer</span>: <span class="type">CMSampleBuffer</span>)</span><br></pre></td></tr></table></figure>
<ul>
<li>VTCompressionSession硬编码作为VTCompressionOutputCallback输出。</li>
</ul>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typealias</span> <span class="type">VTCompressionOutputCallback</span> <span class="operator">=</span> </span><br><span class="line">(<span class="type">UnsafeMutableRawPointer</span>?, </span><br><span class="line"> <span class="type">UnsafeMutableRawPointer</span>?, </span><br><span class="line"> <span class="type">OSStatus</span>, </span><br><span class="line"> <span class="type">VTEncodeInfoFlags</span>, </span><br><span class="line"> <span class="type">CMSampleBuffer</span>?) -&gt; <span class="type">Void</span></span><br></pre></td></tr></table></figure>
<p><em>注意：</em></p>
<p>Clients of CMSampleBuffer must explicitly manage the retain count by calling CFRetain and CFRelease, even in processes using garbage collection.</p>
<h2 id="数据输出">数据输出</h2>
<ul>
<li>AVAssetWriter保存视频AVAssetWriterInput。</li>
</ul>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">func</span> <span class="title function_">append</span>(<span class="keyword">_</span> <span class="params">sampleBuffer</span>: <span class="type">CMSampleBuffer</span>) -&gt; <span class="type">Bool</span></span><br></pre></td></tr></table></figure>
<ul>
<li>AVSampleBufferDisplayLayer展示解码后的sample buffer。</li>
</ul>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">func</span> <span class="title function_">enqueue</span>(<span class="keyword">_</span> <span class="params">sampleBuffer</span>: <span class="type">CMSampleBuffer</span>)</span><br></pre></td></tr></table></figure>
<h2 id="数据结构">数据结构</h2>
<p>无论是读取还是采集，output的<code>videoSetting</code>的<code>kCVPixelBufferPixelFormatTypeKey</code>字段控制sample buffer的mediaSubType输出的色彩格式，转换色彩格式有GPU参与，性能较高，可按需设置。</p>
<h2 id="创建">创建</h2>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">- (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime</span><br><span class="line">&#123;</span><br><span class="line">    CMSampleBufferRef sampleBuffer = NULL;</span><br><span class="line">    CMFormatDescriptionRef outputFormatDescription = NULL;</span><br><span class="line">    CMVideoFormatDescriptionCreateForImageBuffer( kCFAllocatorDefault, pixelBuffer, &amp;outputFormatDescription );</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    CMSampleTimingInfo timingInfo = &#123;0,&#125;;</span><br><span class="line">    timingInfo.duration = kCMTimeInvalid;</span><br><span class="line">    timingInfo.decodeTimeStamp = kCMTimeInvalid;</span><br><span class="line">    timingInfo.presentationTimeStamp = presentationTime;</span><br><span class="line"></span><br><span class="line">    OSStatus err = CMSampleBufferCreateForImageBuffer( kCFAllocatorDefault, pixelBuffer, true, NULL, NULL, outputFormatDescription, &amp;timingInfo, &amp;sampleBuffer );</span><br><span class="line">    if ( sampleBuffer ) &#123;</span><br><span class="line">        // do some thing</span><br><span class="line">        CFRelease( sampleBuffer );</span><br><span class="line">    &#125;</span><br><span class="line">    else &#123;</span><br><span class="line">        NSString *exceptionReason = [NSString stringWithFormat:@&quot;sample buffer create failed (%i)&quot;, (int)err];</span><br><span class="line">        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:exceptionReason userInfo:nil];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line">// CMSampleBufferCreateReady与CMSampleBufferCreate相同，只是dataReady始终为true，因此不需要传递makeDataReadyCallback或refcon。</span><br></pre></td></tr></table></figure>
<h2 id="信息存取">信息存取</h2>
<p>get：</p>
<p>按解码顺序排列帧。</p>
<ul>
<li><code>dataBuffer: CMBlockBuffer?</code></li>
<li><code>imageBuffer: CVImageBuffer?</code></li>
<li><code>decodeTimeStamp: CMTime</code>：首个sample的DTS。</li>
<li><code>outputDecodeTimeStamp: CMTime</code>：outputPTS + (DTS - PTS) / SpeedMultiplier</li>
<li><code>presentationTimeStamp: CMTime</code></li>
<li><code>outputPresentationTimeStamp: CMTime</code></li>
<li><code>duration: CMTime</code></li>
<li><code>outputDuration: CMTime</code>：(D - trimDAtStart - trimDAtEnd) / SpeedMultiplier</li>
<li><code>numSamples: Int</code></li>
<li><code>formatDescription: CMFormatDescription?</code></li>
<li><code>sampleTimingInfos() throws -&gt; [CMSampleTimingInfo]</code>：包含DTS、PTS和Duration</li>
<li><code>sampleAttachments: CMSampleBuffer.SampleAttachmentsArray</code></li>
</ul>
<p>set：</p>
<ul>
<li><code>setDataBuffer</code>：视频、音频压缩数据</li>
<li><code>setOutputPresentationTimeStamp</code></li>
</ul>
<h2 id="参考资料">参考资料</h2>
<ul>
<li><a href="https://www.jianshu.com/p/49fa6ac26095">CMSampleBuffer 分析 - 简书</a></li>
</ul>
]]></content>
      <categories>
        <category>AVFoundation</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>CVPixelBuffer专题</title>
    <url>/posts/cvpixelbuffer/</url>
    <content><![CDATA[<p>CVPixelBuffer 类似 Android 的 bitmap，核心是封装了已经解压后的图像数据。保存了像素的 format，图像宽高和 buffer 指针等信息。</p>
<h2 id="cvpixelbuffer-创建与转换">CVPixelBuffer 创建与转换</h2>
<h3 id="读取原始的像素数组">读取原始的像素数组</h3>
<p>通过 CVPixelBufferGetBaseAddress 可以获得像素数组的指针，该数组中的每个元素应该被解释为 unsigned char。参考如下代码：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">CVPixelBufferRef pixelBuffer;</span><br><span class="line">// 假设我们已经有了一个 pixelBuffer</span><br><span class="line">// 通过如下 API 拿到该图像的宽、高、每行的字节数、每个像素的字节数</span><br><span class="line">size_t w = CVPixelBufferGetWidth(pixelBuffer);</span><br><span class="line">size_t h = CVPixelBufferGetHeight(pixelBuffer);</span><br><span class="line">size_t r = CVPixelBufferGetBytesPerRow(pixelBuffer);</span><br><span class="line">size_t bytesPerPixel = r/w;</span><br><span class="line">OSType bufferPixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);</span><br><span class="line">NSLog(@&quot;GEMFIELD whrb: %zu - %zu - %zu - %zu - %u&quot;,w,h,r,bytesPerPixel,bufferPixelFormat);</span><br><span class="line">// 通过如下 API 拿到 CVPixelBufferRef 的图像格式：</span><br><span class="line">// 比如：kCVPixelFormatType_24RGB、kCVPixelFormatType_32BGRA</span><br><span class="line">OSType bufferPixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);</span><br><span class="line">// 准备开始读取裸的像素数组了</span><br><span class="line">CVPixelBufferLockBaseAddress( pixelBuffer, 0 );</span><br><span class="line">// gemfield_buffer 就是裸的数组</span><br><span class="line">const unsigned char* gemfield_buffer = (const unsigned char*)CVPixelBufferGetBaseAddress(pixelBuffer);</span><br><span class="line">// 这里你可以对该数组进行读取和处理</span><br><span class="line">......</span><br><span class="line">// 结束</span><br><span class="line">CVPixelBufferUnlockBaseAddress( pixelBuffer, 0 );</span><br></pre></td></tr></table></figure>
<h3 id="使用原始的像素数组创建">使用原始的像素数组创建</h3>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">CVPixelBufferRef pixelBuffer = NULL;</span><br><span class="line">int width=319;</span><br><span class="line">int height=64;</span><br><span class="line">CVPixelBufferCreateWithBytes(kCFAllocatorDefault,width,height,kCVPixelFormatType_24RGB,x.get(),3 * width, NULL, NULL, NULL, &amp;pixelBuffer);</span><br></pre></td></tr></table></figure>
<h3 id="转换为-uiimage">转换为 UIImage</h3>
<p>可以使用下面的例子来把 CVPixelBufferRef 转换为 UIImage：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">// 假设我们已经有了一个 pixelBuffer</span><br><span class="line">CVPixelBufferRef pixelBuffer;</span><br><span class="line">CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];</span><br><span class="line">CIContext *temporaryContext = [CIContext contextWithOptions:nil];</span><br><span class="line">CGImageRef syszux_cgiimg = [temporaryContext createCGImage:ciImage fromRect:CGRectMake(0, 0,CVPixelBufferGetWidth(pixelBuffer),CVPixelBufferGetHeight(pixelBuffer))];</span><br><span class="line">UIImage *syszux_uiimg = [UIImage imageWithCGImage:syszux_cgiimg];</span><br><span class="line">CGImageRelease(syszux_cgiimg);</span><br></pre></td></tr></table></figure>
<h3 id="使用-uiimage-创建">使用 UIImage 创建</h3>
<p>UIImage 是 CGImage 的 wrapper，通过 CGImage 拿到图像的宽、高信息。然后在一个 context 中，通过 CGContextDrawImage 函数来将 CGImage“渲染”出来，这个时候原始的像素数就保存在了 context 中 CVPixelBufferRef 指向的 baseAddress 上了。</p>
<p>代码如下所示：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">- (CVPixelBufferRef)syszuxPixelBufferFromUIImage:(UIImage *)originImage &#123;</span><br><span class="line">    CGImageRef image = originImage.CGImage;</span><br><span class="line">    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:</span><br><span class="line">                             [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,</span><br><span class="line">                             [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,</span><br><span class="line">                             nil];</span><br><span class="line">    CVPixelBufferRef pxbuffer = NULL;</span><br><span class="line">    CGFloat frameWidth = CGImageGetWidth(image);</span><br><span class="line">    CGFloat frameHeight = CGImageGetHeight(image);</span><br><span class="line">    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault,</span><br><span class="line">                                          frameWidth,</span><br><span class="line">                                          frameHeight,</span><br><span class="line">                                          kCVPixelFormatType_32ARGB,</span><br><span class="line">                                          (__bridge CFDictionaryRef) options,</span><br><span class="line">                                          &amp;pxbuffer);</span><br><span class="line">    NSParameterAssert(status == kCVReturnSuccess &amp;&amp; pxbuffer != NULL);</span><br><span class="line">    CVPixelBufferLockBaseAddress(pxbuffer, 0);</span><br><span class="line">    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);</span><br><span class="line">    NSParameterAssert(pxdata != NULL);</span><br><span class="line">    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();</span><br><span class="line">    CGContextRef context = CGBitmapContextCreate(pxdata,</span><br><span class="line">                                                 frameWidth,</span><br><span class="line">                                                 frameHeight,</span><br><span class="line">                                                 8,</span><br><span class="line">                                                 CVPixelBufferGetBytesPerRow(pxbuffer),</span><br><span class="line">                                                 rgbColorSpace,</span><br><span class="line">                                                 (CGBitmapInfo)kCGImageAlphaNoneSkipFirst);</span><br><span class="line">    NSParameterAssert(context);</span><br><span class="line">    CGContextConcatCTM(context, CGAffineTransformIdentity);</span><br><span class="line">    CGContextDrawImage(context, CGRectMake(0,</span><br><span class="line">                                           0,</span><br><span class="line">                                           frameWidth,</span><br><span class="line">                                           frameHeight),</span><br><span class="line">                       image);</span><br><span class="line">    CGColorSpaceRelease(rgbColorSpace);</span><br><span class="line">    CGContextRelease(context);</span><br><span class="line">    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);</span><br><span class="line">    return pxbuffer;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="深拷贝">深拷贝</h3>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection &#123;</span><br><span class="line">       CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);</span><br><span class="line">       // Get pixel buffer info</span><br><span class="line">       const int kBytesPerPixel = 4;</span><br><span class="line">       CVPixelBufferLockBaseAddress(pixelBuffer, 0);</span><br><span class="line">       int bufferWidth = (int)CVPixelBufferGetWidth(pixelBuffer);</span><br><span class="line">       int bufferHeight = (int)CVPixelBufferGetHeight(pixelBuffer);</span><br><span class="line">       size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer); </span><br><span class="line">       uint8_t *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer);</span><br><span class="line">       // Copy the pixel buffer</span><br><span class="line">       CVPixelBufferRef pixelBufferCopy = NULL;</span><br><span class="line">       CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, bufferWidth, bufferHeight, kCVPixelFormatType_32BGRA, NULL, &amp;pixelBufferCopy);</span><br><span class="line">       CVPixelBufferLockBaseAddress(pixelBufferCopy, 0);</span><br><span class="line">       uint8_t *copyBaseAddress = CVPixelBufferGetBaseAddress(pixelBufferCopy);</span><br><span class="line">       memcpy(copyBaseAddress, baseAddress, bufferHeight * bytesPerRow);</span><br><span class="line">       // Do what needs to be done with the 2 pixel buffers</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>若需要对帧做基本处理，可以只是 vImage 对其解码后数据处理。</p>
<h2 id="cvpixelbufferpool">CVPixelBufferPool</h2>
<p>CVPixelBufferPool，主要是实现了 CVPixelBuffer 中的 IOSurface 的复用与回收。</p>
]]></content>
      <categories>
        <category>AVFoundation</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>Video Toolbox压缩配置</title>
    <url>/posts/video_toolbox_compression_configuration/</url>
    <content><![CDATA[<h1 id="video-toolbox压缩配置">Video Toolbox压缩配置</h1>
<h2 id="压缩属性">压缩属性</h2>
<p>压缩属性都是以<code>kVTCompressionPropertyKey_Xx</code>命名，所以下面的属性名都省略了其前缀<code>kVTCompressionPropertyKey_</code>。</p>
<h3 id="码流配置">码流配置</h3>
<p>Depth：像素深度。</p>
<p>该属性仅由视频编码器支持，用于与特定像素格式（例如，16位RGB、24位RGB）绑定的格式。</p>
<p>ProfileLevel：码流配置和级别。</p>
<p><a href="https://developer.apple.com/documentation/videotoolbox/vtcompressionsession/compression_properties/profile_and_level_constants">配置文件和级别常量</a></p>
<p>H264EntropyMode：用于H.264压缩的熵编码模式。</p>
<p>如果H.264编码器支持，该属性控制编码器是否应使用基于上下文的自适应变长编码（CAVLC）或基于上下文的自适应二进制算术编码（CABAC）。CABAC通常能提供更好的压缩，但代价是更高的计算开销。默认值是针对编码器的，可能会根据其他编码器的设置而改变。</p>
<p><strong>注意：</strong>改变默认的熵模式可能会导致配置与要求的配置文件和级别不兼容。这种情况下的结果是不确定的，可能包括编码错误或不符合要求的输出流。</p>
<ul>
<li>kVTH264EntropyMode_CABAC</li>
<li>kVTH264EntropyMode_CAVLC</li>
</ul>
<h3 id="缓冲区">缓冲区</h3>
<p>NumberOfPendingFrames：压缩会话中待处理的帧的数量。该值可能会异步减少。</p>
<p>PixelBufferPoolIsShared：布尔值，表示视频编码器和会话客户端之间是否共享公共像素缓冲池。</p>
<p>false表示视频编码器和客户端的像素缓冲区属性不兼容，使用单独的缓冲池。</p>
<p>VideoEncoderPixelBufferAttributes：视频编码器的像素缓冲区属性。使用这些属性来为源像素缓冲区创建一个像素缓冲区池。</p>
<h3 id="清洁光圈和像素长宽比">清洁光圈和像素长宽比</h3>
<p>AspectRatio16x9：布尔值，表示DV视频流是否应设置16x9标志。</p>
<p>此属性由DV25/50系列编码器支持。</p>
<p>false时，图片长宽比为4:3。true时，图片长宽比为16:9。无论哪种方式，都会使用一个固定的长宽比（具体数值取决于格式是NTSC还是PAL）。</p>
<p>CleanAperture：编码帧的清洁光圈。</p>
<p>如果视频编码器执行特定的清洁光圈，这个属性是只读的（<code>VTSessionSetProperty(_:key:value:</code>)将返回<code>kVTPropertyReadOnlyErr</code>）。洁净孔径将在输出样本的格式描述中设置，并可能影响源帧的缩放。NULL是这个属性的一个有效值，意味着清洁孔径是全宽和全高。</p>
<p>FieldCount：场类型，表示帧应该是逐行编码（1）还是隔行编码（2）。</p>
<p>在输出样本的格式描述上设置的，可能会影响源帧的缩放。NULL是这个属性的一个有效值。</p>
<p>FieldDetail：隔行扫描帧的场排序。</p>
<p>如果视频编码器执行特定的场排序，这个属性将是只读的（<code>VTSessionSetProperty(_:key:value:)</code>返回<code>kVTPropertyReadOnlyErr</code>）。字段细节是在输出样本的格式描述上设置的，并可能影响源帧的缩放。NULL是这个属性的一个有效值。</p>
<p>PixelAspectRatio：编码帧的像素长宽比。</p>
<p>如果视频编码器强制执行特定的像素长宽比，该属性将是只读的（<code>VTSessionSetProperty(_:key:value:</code>) 返回 <code>kVTPropertyReadOnlyErr</code>）。像素长宽比是在输出样本的格式描述上设置的，并可能影响源帧的缩放。NULL是这个属性的有效值，意味着方形像素（1:1）。</p>
<p>ProgressiveScan：布尔值，表示DV视频流是否应设置逐行标志。</p>
<p>DV25/50系列编码器支持此属性。如果是假的，内容被编码为隔行扫描。如果为真，则内容被编码为渐进式。此属性的值可固定 <code>kVTCompressionPropertyKey_FieldCount</code> 和 <code>kVTCompressionPropertyKey_FieldDetail</code> 属性。</p>
<h3 id="颜色">颜色</h3>
<p>ColorPrimaries：压缩内容的颜色原色。</p>
<p>TransferFunction：压缩内容的转换函数。</p>
<p>YCbCrMatrix：用于压缩内容的YCbCr矩阵。</p>
<p>ICCProfile：用于压缩内容的ICC配置文件。</p>
<h3 id="预期值">预期值</h3>
<p>ExpectedDuration：压缩会话的预期总时长。</p>
<p>ExpectedFrameRate：预期的帧率。</p>
<p>SourceFrameCount：源帧的数量。</p>
<h3 id="帧依赖">帧依赖</h3>
<p>AllowFrameReordering：布尔值，表示是否启用了帧重排。</p>
<p>AllowTemporalCompression：布尔值，表示是否启用了时间压缩。</p>
<p>MaxKeyFrameInterval：关键帧之间的最大间隔，也被称为关键帧率。</p>
<p>MaxKeyFrameIntervalDuration：从一个关键帧到下一个关键帧的最大持续时间，单位是秒。</p>
<h3 id="硬件加速">硬件加速</h3>
<p>UsingHardwareAcceleratedVideoEncoder：布尔值，表示是否使用了硬件加速视频编码器。</p>
<p>kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder：布尔值，表示是否需要硬件加速编码。</p>
<p>kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder：布尔值，表示是否允许硬件加速视频编码（如果可用）。</p>
<h3 id="多重压缩存储">多重压缩存储</h3>
<p>MultiPassStorage：启用多路压缩并为编码器私有数据提供存储。</p>
<h3 id="每帧配置">每帧配置</h3>
<p>kVTEncodeFrameOptionKey_ForceKeyFrame：布尔值，表示当前帧是否被强制为关键帧的。</p>
<p>PixelTransferProperties：用于配置VTPixelTransferSession的属性，以便在必要时将源帧从客户端的图像缓冲区转移到视频编码器的图像缓冲区。</p>
<h3 id="速率控制">速率控制</h3>
<p>AverageBitRate：长期期望的平均比特率，单位是比特/秒。</p>
<p>DataRateLimits：对数据速率的零、一或两个硬限制。</p>
<p>MoreFramesAfterEnd：布尔值，表示一个压缩会话的帧是否以及如何与其他压缩帧串联以形成一个更长的系列。</p>
<p>Quality：希望的压缩质量。</p>
<p>RealTime：布尔值，表示是否建议视频编码器进行实时压缩。</p>
<p>MaxH264SliceBytes：H.264编码的最大分片大小。</p>
<p>MaxFrameDelayCount：压缩器在必须输出一个压缩帧之前允许保留的最大帧数。</p>
]]></content>
      <categories>
        <category>AVFoundation</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>AVFoundation导出API</title>
    <url>/posts/avfoundation_export_api/</url>
    <content><![CDATA[<h2 id="avassetexportsession">AVAssetExportSession</h2>
<p>AVAssetExportSession可以实现简单的导出。</p>
<p>输入：AVAsset</p>
<p>输出：URL</p>
<p>可配置项：</p>
<ul>
<li>格式
<ul>
<li><code>preset</code></li>
<li><code>timeRange: CMTimeRange</code></li>
<li><code>outputFileType: AVFileType</code></li>
</ul></li>
<li>限制与优化
<ul>
<li><code>fileLengthLimit: Int64</code></li>
<li><code>shouldOptimizeForNetworkUse: Bool</code></li>
<li><code>canPerformMultiplePassesOverSourceMediaData: Bool</code>、<code>directoryForTemporaryFiles: URL</code></li>
</ul></li>
<li>附加
<ul>
<li><code>audioMix: AVAudioMix</code></li>
<li><code>videoComposition: AVVideoComposition</code></li>
<li><code>audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm</code></li>
<li><code>metadata: [AVMetadataItem]</code></li>
</ul></li>
</ul>
<p>操作：<code>exportAsynchronously</code>、<code>cancelExport</code></p>
<p>获取状态：<code>progress</code>、<code>status</code>、<code>error</code></p>
<h2 id="readerwriter">Reader+Writer</h2>
<h3 id="avassetreader">AVAssetReader</h3>
<p>管理读取输出。</p>
<p>输入：AVAsset</p>
<p>操作：<code>startReading</code>、<code>cancelReading</code>、添加输出</p>
<p>状态：<code>status</code>、<code>error</code></p>
<h3 id="avassetreaderoutput">AVAssetReaderOutput</h3>
<p>输出帧。</p>
<p>输入：AVAssetTrack</p>
<p>配置：<code>alwaysCopiesSampleData: Bool</code></p>
<p>操作：<code>copyNextSampleBuffer</code>、<code>markConfigurationAsFinal</code>（提前结束）</p>
<p>输出：CMSampleBuffer</p>
<p>具体子类：</p>
<ul>
<li>TrackOutput：最常用
<ul>
<li>输入：AVAssetTrack</li>
<li>配置：
<ul>
<li><code>outputSettings: [String : Any]</code></li>
<li><code>audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm</code></li>
</ul></li>
</ul></li>
<li>AudioMixOutput
<ul>
<li>输入：[AVAssetTrack]</li>
<li>配置：
<ul>
<li><code>audioSettings: [String : Any]</code></li>
<li><code>audioMix: AVAudioMix</code></li>
<li><code>audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm</code></li>
</ul></li>
</ul></li>
<li>VideoCompositionOutput
<ul>
<li>输入：[AVAssetTrack]</li>
<li>配置：
<ul>
<li><code>videoSettings: [String : Any]</code></li>
<li><code>videoComposition: AVVideoComposition</code>、</li>
</ul></li>
</ul></li>
<li>SampleReferenceOutput
<ul>
<li>输入：AVAssetTrack</li>
</ul></li>
</ul>
<h3 id="avassetreaderoutputmetadataadaptor">AVAssetReaderOutputMetadataAdaptor</h3>
<p>从TrackOutput输出元数据。</p>
<p>操作：<code>nextTimedMetadataGroup</code></p>
<h3 id="avassetwriter">AVAssetWriter</h3>
<p>管理写入输入。</p>
<p>输出：URL</p>
<p>配置：</p>
<ul>
<li><code>outputFileType: AVFileType</code></li>
<li><code>directoryForTemporaryFiles: URL</code></li>
<li><code>metadata: [AVMetadataItem]</code></li>
<li><code>movieFragmentInterval: CMTime</code></li>
<li><code>overallDurationHint: CMTime</code></li>
<li><code>movieTimeScale: CMTimeScale</code></li>
<li><code>shouldOptimizeForNetworkUse: Bool</code></li>
</ul>
<p>操作：<code>startWriting</code>、<code>finishWriting</code>、<code>cancelWriting</code>、添加输入（组）、<code>startSession</code>、<code>endSession</code></p>
<p>状态：<code>status</code>、<code>error</code></p>
<h3 id="avassetwriterinput">AVAssetWriterInput</h3>
<p>拼接音视频帧。</p>
<p>输入：CMSampleBuffer</p>
<p>配置：</p>
<ul>
<li><code>mediaType: AVMediaType</code></li>
<li><code>outputSettings: [String : Any]</code></li>
<li><code>sourceFormatHint: CMFormatDescription</code></li>
<li><code>preferredVolume: Float</code></li>
<li><code>transform: CGAffineTransform</code></li>
<li><code>naturalSize: CGSize</code></li>
<li><code>mediaTimeScale: CMTimeScale</code></li>
<li><code>metadata: [AVMetadataItem]</code></li>
<li><strong><code>expectsMediaDataInRealTime: Bool</code></strong></li>
<li><code>marksOutputTrackAsEnabled: Bool</code></li>
<li><code>performsMultiPassEncodingIfSupported: Bool</code></li>
<li><code>preferredMediaChunkAlignment: Int</code></li>
<li><code>preferredMediaChunkDuration: CMTime</code></li>
<li><code>mediaDataLocation: AVAssetWriterInput.MediaDataLocation</code></li>
</ul>
<p>操作：拼接帧、<code>markAsFinished</code>、<code>addTrackAssociation</code></p>
<p>状态：<code>requestMediaDataWhenReady</code>、<code>isReadyForMoreMediaData</code></p>
<h3 id="avassetwriterinputgroup">AVAssetWriterInputGroup</h3>
<p>输入：<a href="#avassetwriterinput">AVAssetWriterInput</a></p>
<h3 id="avassetwriterinputpixelbufferadaptor">AVAssetWriterInputPixelBufferAdaptor</h3>
<p>拼接指定PTS的CVPixelBuffer，并提供CVPixelBufferPool。</p>
<p>输出：AVAssetWriterInput</p>
<p>配置：<code>sourcePixelBufferAttributes: [String : Any]</code></p>
<h3 id="avassetwriterinputmetadataadaptor">AVAssetWriterInputMetadataAdaptor</h3>
<p>拼接AVTimedMetadataGroup到Input。</p>
<p>输出：AVAssetWriterInput</p>
<h3 id="avoutputsettingsassistant">AVOutputSettingsAssistant</h3>
<p>辅助生成音视频配置字典。</p>
<h3 id="组合实现导出">组合实现导出</h3>
<p>先对AVAsset异步加载<code>"tracks"</code>key。</p>
<p>读取流程：</p>
<ol type="1">
<li>用AVAsset构建AssetReader；</li>
<li>根据轨道，使用解码配置字典分别创建ReaderOutput；</li>
<li>添加到AssetReader；</li>
<li>AssetReader调用<code>startReading</code>开始读取；</li>
<li>ReaderOutput循环逐帧调用<code>copyNextSampleBuffer</code>，输出帧，若帧为空则完成或遇到错误。</li>
</ol>
<p>写入流程：</p>
<ol type="1">
<li>用输出URL、文件类型创建AssetWriter；</li>
<li>根据轨道类型，使用编码配置字典创建WriterInput；</li>
<li>添加到AssetWriter；</li>
<li>AssetWriter调用<code>startWriting</code>、<code>startSession</code>开始写入；</li>
<li>WriterInput调用<code>requestMediaDataWhenReady</code>，在其回调中：
<ol type="1">
<li><code>isReadyForMoreMediaData == true</code></li>
<li>WriterInput拼接帧；完成时调用<code>markAsFinished</code>。</li>
</ol></li>
<li>各个轨都写入完成时，AssetWriter调用<code>finishWriting</code>完成写入（这会内部调用<code>endSession</code>）。</li>
</ol>
<p>要想提前结束，调用<code>endSession</code>，再调用<code>finishWriting</code>。</p>
<p><strong>配置字典是关键。</strong></p>
<p>组合使用：</p>
<ol type="1">
<li>异步读取AVAsset key，进入初始化阶段：
<ol type="1">
<li>创建AssetReader，然后AssetWriter。</li>
<li>取出AssetTrack创建分别创建ReaderOutput和WriterInput。</li>
</ol></li>
<li>读取拼接阶段：
<ol type="1">
<li>AssetReader调用<code>startReading</code>开始读取；AssetWriter调用<code>startWriting</code>、<code>startSession</code>开始写入；</li>
<li>ReaderOutput循环逐帧调用<code>copyNextSampleBuffer</code>，输出帧；WriterInput检查是否就绪，拼接帧；</li>
</ol></li>
<li>ReaderOutput没有帧了，进入完成阶段：
<ol type="1">
<li>检查是否有错误；</li>
<li>AssetWriter调用<code>finishWriting</code>完成写入。</li>
</ol></li>
</ol>
<p>多线程应用：</p>
<ul>
<li>都使用串行队列，分别创建：
<ul>
<li>主操作队列 ×1</li>
<li>各轨道读写队列 ×N</li>
</ul></li>
<li>初始化阶段在主队列进行，即在异步读取AVAsset key后进入主操作队列创建各种对象。</li>
<li>每个轨道的读帧、写帧操作都在对应的一个队列中进行。</li>
<li>使用调度组在所有轨道都读写完成后回调通知。进入各轨道队列前enter，各个轨道读写完成后leave。</li>
<li>完成后进入主操作队列，进行收尾工作。</li>
</ul>
]]></content>
      <categories>
        <category>AVFoundation</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>AVFoundation影片编辑API</title>
    <url>/posts/avfoundation_movie_edit_api/</url>
    <content><![CDATA[<p>媒体创作和编辑基本是AVFoundation的高级接口，较少涉及底层接口。整个过程总的来说就是构建AVAsset的过程，而在视频编辑中，构建的是AVMutableComposition的过程。</p>
<h2 id="整体关系">整体关系</h2>
<p><strong>Composition</strong>：作为内容主体，一个抽象的可编辑的AVAsset子类，提供面向对象的多轨操作。</p>
<ul>
<li>CompositionTrack ×N：管理时间，总的音视频轨信息。
<ul>
<li>AssetTrack片段 ×N</li>
<li>轨道的偏好信息：<code>naturalTimeScale</code>、<code>preferredTransform</code>、<code>preferredVolume</code></li>
</ul></li>
</ul>
<p><strong>AudioMix</strong>：附加信息，对音量的描述</p>
<ul>
<li><code>inputParameters</code>：AudioMixInputParameters ×N：描述音量（+时间=渐变）、变速时的音调策略</li>
</ul>
<p>AudioMixInputParameters的<code>audioTapProcessor</code>可以使用AudioUnit给音频增加效果，但似乎查不到具体的额API文档，可参考：<a href="https://developer.apple.com/forums/thread/90879">MTAudioProcessingTap with kMTAudio… | Apple Developer Forums</a>、<a href="https://github.com/gchilds/MTAudioProcessingTap-in-Swift">gchilds/MTAudioProcessingTap-in-Swift: Example of creating an MTAudioProcessingTap in Swift4.2</a>。</p>
<p><strong>VideoComposition</strong>：附加信息，对画面的描述</p>
<ul>
<li><p>视频属性控制：</p>
<ul>
<li>frameDuration</li>
<li>renderSize</li>
<li>colorParimaties</li>
<li>colorTransferFunction</li>
<li>colorYCbCrMatrix</li>
</ul></li>
<li><p>视频操作，通过以下三种方式：</p>
<ul>
<li><p><code>instructions</code>：AVVideoCompositionInstructionProtocol ×N</p>
<ul>
<li><p>VideoCompositionLayerInstruction：提供几种图像以时间点、时间区间/渐变操作</p>
<ul>
<li><p>opacity</p></li>
<li><p>transform</p></li>
<li><p>cropRectangle</p></li>
</ul></li>
</ul></li>
<li><p><code>animationTool</code>：提供与Core Animation的几种交互方式，不能实时预览的，即设置到playerItem看不到效果。</p>
<ul>
<li>新增一个用CALayer表示的视频轨：<code>init(additionalLayer: CALayer, asTrackID: CMPersistentTrackID)</code></li>
<li>用CALayer层级关系管理视频轨：<code>init(postProcessingAsVideoLayer: CALayer, in: CALayer)</code>、<code>init(postProcessingAsVideoLayers: [CALayer], in: CALayer)</code></li>
</ul></li>
<li><p><code>customVideoCompositorClass</code>：自己实现一个VideoComposition，可通过GL、Metal实现自定义的转场</p></li>
</ul></li>
</ul>
<p>以上API的Mutable版本的类才是可编辑的。</p>
<p>以上的Composition，可用于创建PlayerItem、AssetExportSession。AudioMix、VideoComposition作为属性设置到AssetExportSession、AssetReaderAudioMixOutput、playerItem。</p>
<h2 id="具体api">具体API</h2>
<h3 id="avmutablecomposition">AVMutableComposition</h3>
<p>AVAsset的子类，因此这是最后预览、导出操作的数据对象。</p>
<h4 id="整体操作">整体操作</h4>
<p>对整个composition对象进行整体操作，当然这会涉及多个轨道，除了对整体进行时间伸缩，否则较少使用。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/// 插入空占位</span><br><span class="line">func insertEmptyTimeRange(_ timeRange: CMTimeRange)</span><br><span class="line"></span><br><span class="line">/// 插入asset</span><br><span class="line">func insertTimeRange(_ timeRange: CMTimeRange, of asset: AVAsset, at startTime: CMTime) throws</span><br><span class="line"></span><br><span class="line">/// 移除时间段的内容，注意这里不会移除既有的轨道。</span><br><span class="line">func removeTimeRange(_ timeRange: CMTimeRange)</span><br><span class="line"></span><br><span class="line">/// 伸缩时间，即改变时间区间内所有轨道的时长</span><br><span class="line">func scaleTimeRange(_ timeRange: CMTimeRange, toDuration duration: CMTime)</span><br><span class="line"></span><br><span class="line">/// 配置视频画幅尺寸</span><br><span class="line">var naturalSize: CGSize &#123; get set &#125;</span><br></pre></td></tr></table></figure>
<h4 id="轨道操作">轨道操作</h4>
<p>大多数操作都是基于轨道。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/// 添加、移除轨道</span><br><span class="line">func addMutableTrack(withMediaType mediaType: AVMediaType, preferredTrackID: CMPersistentTrackID) -&gt; AVMutableCompositionTrack?</span><br><span class="line">func removeTrack(_ track: AVCompositionTrack)</span><br><span class="line"></span><br><span class="line">// 其他API只是获取轨道等非常用操作</span><br></pre></td></tr></table></figure>
<h3 id="avmutablecompositiontrack">AVMutableCompositionTrack</h3>
<p>一个可修改的轨道。</p>
<h4 id="常用配置属性">常用配置属性</h4>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/// 视频翻转矩阵</span><br><span class="line">var preferredTransform: CGAffineTransform &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 音频轨道音量</span><br><span class="line">var preferredVolume: Float &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 其他不太常用的属性</span><br><span class="line">var languageCode: String? &#123; get set &#125;</span><br><span class="line">var extendedLanguageTag: String? &#123; get set &#125;</span><br><span class="line">var naturalTimeScale: CMTimeScale &#123; get set &#125;</span><br></pre></td></tr></table></figure>
<p>增删改查</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">func insertEmptyTimeRange(_ timeRange: CMTimeRange)</span><br><span class="line">func insertTimeRange(_ timeRange: CMTimeRange, of track: AVAssetTrack, at startTime: CMTime) throws</span><br><span class="line">func insertTimeRanges(_ timeRanges: [NSValue], of tracks: [AVAssetTrack], at startTime: CMTime) throws // 似乎不常用</span><br><span class="line">func removeTimeRange(_ timeRange: CMTimeRange)</span><br><span class="line">func scaleTimeRange(_ timeRange: CMTimeRange, toDuration duration: CMTime)</span><br><span class="line">var segments: [AVCompositionTrackSegment]! &#123; get set &#125;</span><br></pre></td></tr></table></figure>
<h3 id="avmutableaudiomix">AVMutableAudioMix</h3>
<p>包含混音（目前的混音只有音量调节）参数，所以其对象只有一个属性：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">var inputParameters: [AVAudioMixInputParameters] &#123; get set &#125;</span><br></pre></td></tr></table></figure>
<h4 id="avmutableaudiomixinputparameters">AVMutableAudioMixInputParameters</h4>
<p>音频混音参数。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/// 创建</span><br><span class="line">convenience init(track: AVAssetTrack?)</span><br><span class="line">/// 创建后也可以修改trackId更变应用的轨道</span><br><span class="line">var trackID: CMPersistentTrackID &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 设置音量</span><br><span class="line">func setVolume(_ volume: Float, at time: CMTime)</span><br><span class="line">func setVolumeRamp(fromStartVolume startVolume: Float, toEndVolume endVolume: Float, timeRange: CMTimeRange)</span><br><span class="line"></span><br><span class="line">/// 设置音调算法</span><br><span class="line">var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm? &#123; get set &#125;</span><br></pre></td></tr></table></figure>
<h3 id="avmutablevideocomposition">AVMutableVideoComposition</h3>
<p>控制视频轨道组合行为。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/// 创建</span><br><span class="line">init(propertiesOf asset: AVAsset)</span><br><span class="line"></span><br><span class="line">/// 配置视频相关属性</span><br><span class="line">var frameDuration: CMTime &#123; get set &#125;</span><br><span class="line">var renderSize: CGSize &#123; get set &#125;</span><br><span class="line">var renderScale: Float &#123; get set &#125; // 不常用</span><br><span class="line">var colorPrimaries: String? &#123; get set &#125;</span><br><span class="line">var colorTransferFunction: String? &#123; get set &#125;</span><br><span class="line">var colorYCbCrMatrix: String? &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 配置视频操作</span><br><span class="line">var instructions: [AVVideoCompositionInstructionProtocol] &#123; get set &#125;</span><br><span class="line">var animationTool: AVVideoCompositionCoreAnimationTool? &#123; get set &#125;</span><br><span class="line">var customVideoCompositorClass: AVVideoCompositing.Type? &#123; get set &#125;</span><br></pre></td></tr></table></figure>
<h3 id="avmutablevideocompositioninstruction">AVMutableVideoCompositionInstruction</h3>
<p>提供一个时间范围内的视频组织信息。由一组AVMutableVideoCompositionLayerInstruction对象格式定义的指令组成的。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/// 自顶而下排列的layerInstructions</span><br><span class="line">var layerInstructions: [AVVideoCompositionLayerInstruction] &#123; get set &#125;</span><br><span class="line">var timeRange: CMTimeRange &#123; get set &#125;</span><br><span class="line">var enablePostProcessing: Bool &#123; get set &#125;</span><br><span class="line">var backgroundColor: CGColor? &#123; get set &#125;</span><br></pre></td></tr></table></figure>
<h3 id="avmutablevideocompositionlayerinstruction">AVMutableVideoCompositionLayerInstruction</h3>
<p>给视频特效，用于定义给定视频轨道应用的基于时间的模糊、变形、和裁剪效果。从其构建方式可见，其更类似于AVMutableAudioMixInputParameters。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/// 构建</span><br><span class="line">convenience init(assetTrack track: AVAssetTrack)</span><br><span class="line">var trackID: CMPersistentTrackID &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 支持的操作</span><br><span class="line">func setOpacity(_ opacity: Float, at time: CMTime)</span><br><span class="line">func setOpacityRamp(fromStartOpacity startOpacity: Float, toEndOpacity endOpacity: Float, timeRange: CMTimeRange)</span><br><span class="line">func setTransform(_ transform: CGAffineTransform, at time: CMTime)</span><br><span class="line">func setTransformRamp(fromStart startTransform: CGAffineTransform, toEnd endTransform: CGAffineTransform, timeRange: CMTimeRange)</span><br><span class="line">func setCropRectangle(_ cropRectangle: CGRect, at time: CMTime)</span><br><span class="line">func setCropRectangleRamp(fromStartCropRectangle startCropRectangle: CGRect, toEndCropRectangle endCropRectangle: CGRect, timeRange: CMTimeRange)</span><br></pre></td></tr></table></figure>
<p>LayerInstruction是应用于一个轨道的，意味着要想在应用特效的时候能看到底下的视频，则需要多个视频轨道。</p>
<h3 id="videocompositioncoreanimation">VideoComposition+CoreAnimation</h3>
<p>视频编辑中除了可以叠加轨道，还可以叠加CALayer。这通过VideoComposition的animationTool实现。其类是AVVideoCompositionCoreAnimationTool：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/// 添加额外的图层</span><br><span class="line">convenience init(additionalLayer layer: CALayer, asTrackID trackID: CMPersistentTrackID)</span><br><span class="line"></span><br><span class="line">/// 自定义组织视频层与根图层。这里animationLayer是根图层，videoLayer视频层是其子图层，除此以外还可以在animationLayer添加更多子图层。</span><br><span class="line">convenience init(postProcessingAsVideoLayer videoLayer: CALayer, in animationLayer: CALayer)</span><br><span class="line"></span><br><span class="line">/// 拷贝视频帧到多个视频层</span><br><span class="line">convenience init(postProcessingAsVideoLayers videoLayers: [CALayer], in animationLayer: CALayer)</span><br></pre></td></tr></table></figure>
<p>在视频中使用CoreAnimation，常需要设置geometryFlipped属性，让其坐标翻转一遍。</p>
<p>通过AVVideoCompositionCoreAnimationTool使用Core Animation时需要注意：</p>
<ul>
<li>用<code>AVCoreAnimationBeginTimeAtZero</code>表示0时间点；</li>
<li><code>isRemovedOnCompletion</code>设为false；</li>
<li>避免使用与UIView关联的CALayer。</li>
</ul>
<h3 id="avassetexportsession">AVAssetExportSession</h3>
<p>高级导出类，是个高级API，要想更细化地配置，还是需要AVAssetReader+AVAssetWriter。</p>
<h4 id="构建">构建</h4>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">init?(asset: AVAsset, presetName: String)</span><br></pre></td></tr></table></figure>
<p>当然可以直接构建后就开始导出。其音视频的转码配置都囊括在presetName中，即既有的方案中去直接套用。</p>
<h4 id="配置">配置</h4>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/// 输出文件路径</span><br><span class="line">var outputURL: URL? &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 文件类型，准确地来说是容器类型</span><br><span class="line">var outputFileType: AVFileType? &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 文件长度限制</span><br><span class="line">var fileLengthLimit: Int64 &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 导出时间区间</span><br><span class="line">var timeRange: CMTimeRange &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 附带的元数据</span><br><span class="line">var metadata: [AVMetadataItem]? &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 混音</span><br><span class="line">var audioMix: AVAudioMix? &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 音调算法（在伸缩时长时）</span><br><span class="line">var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// 是否为网络播放优化</span><br><span class="line">var shouldOptimizeForNetworkUse: Bool &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// video composition</span><br><span class="line"> var videoComposition: AVVideoComposition? &#123; get set &#125;</span><br><span class="line"></span><br><span class="line">/// custom video compositor</span><br><span class="line">var customVideoCompositor: AVVideoCompositing? &#123; get &#125;</span><br></pre></td></tr></table></figure>
]]></content>
      <categories>
        <category>AVFoundation</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>AVFoundation支持格式</title>
    <url>/posts/avfoundation_movie_format/</url>
    <content><![CDATA[<table>
<thead>
<tr class="header">
<th>定义</th>
<th>扩展名</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>AVFileTypeQuickTimeMovie</td>
<td>.mov 或 .qt</td>
</tr>
<tr class="even">
<td>AVFileTypeMPEG4</td>
<td>.mp4</td>
</tr>
<tr class="odd">
<td>AVFileTypeAppleM4V</td>
<td>.m4v</td>
</tr>
<tr class="even">
<td>AVFileTypeAppleM4A</td>
<td>.m4a</td>
</tr>
<tr class="odd">
<td>AVFileType3GPP</td>
<td>.3gp 或 .3gpp 或 .sdv</td>
</tr>
<tr class="even">
<td>AVFileType3GPP2</td>
<td>.3g2 或 .3gp2</td>
</tr>
<tr class="odd">
<td>AVFileTypeCoreAudioFormat</td>
<td>.caf</td>
</tr>
<tr class="even">
<td>AVFileTypeWAVE</td>
<td>.wav 或 .wave 或 .bwf</td>
</tr>
<tr class="odd">
<td>AVFileTypeAIFF</td>
<td>.aif 或 .aiff</td>
</tr>
<tr class="even">
<td>AVFileTypeAIFC</td>
<td>.aifc 或 .cdda</td>
</tr>
<tr class="odd">
<td>AVFileTypeAMR</td>
<td>.amr</td>
</tr>
<tr class="even">
<td>AVFileTypeWAVE</td>
<td>.wav 或 .wave 或 .bwf</td>
</tr>
<tr class="odd">
<td>AVFileTypeMPEGLayer3</td>
<td>.mp3</td>
</tr>
<tr class="even">
<td>AVFileTypeSunAU</td>
<td>.au 或 .snd</td>
</tr>
<tr class="odd">
<td>AVFileTypeAC3</td>
<td>.ac3</td>
</tr>
<tr class="even">
<td>AVFileTypeEnhancedAC3</td>
<td>.eac3</td>
</tr>
</tbody>
</table>
]]></content>
      <categories>
        <category>AVFoundation</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>AVFoundation视频解码API</title>
    <url>/posts/avfoundation_video_decode_api/</url>
    <content><![CDATA[<h2 id="视频解码api概述">视频解码API概述</h2>
<p>AVFoundation</p>
<ul>
<li>AVAssetReader</li>
<li>AVSampleBufferGenerator</li>
</ul>
<p>Video Toolbox</p>
<ul>
<li>VTDecompressionSession</li>
</ul>
<p>Core Video</p>
<ul>
<li>CVPixelBuffer integration with Metal</li>
</ul>
<p>使用AVFoundation的AVPlayer、AVAssetExportSession、AVAssetReader和Video Toolbox的VTDecompressSesion都自动进行硬件加速以及CMSampleBuffer的RPC优化。</p>
<h2 id="视频核心数据结构">视频核心数据结构</h2>
<p>CVPixelBuffer：原始图像+图像元数据</p>
<p>CMBlockBuffer：任意类型的二进制数据（压缩图像）+元数据</p>
<p>CMSampleBuffer：</p>
<ul>
<li>CVPixelBuffer+时间信息+帧元数据（CMFormatDescription）</li>
<li>CMBlockBuffer+时间信息+帧元数据</li>
<li>只包含元数据</li>
</ul>
<p>IOSurface：不同框架、设备中交换图像数据的高速通道，应用在：</p>
<ul>
<li>不同框架之间：CoreVideo和Metal</li>
<li>不同进程之间：解码进程和App进程</li>
<li>不同的内存区：显存和内存</li>
</ul>
<p>CVPixelBufferPool：实现了CVPixelBuffer中的IOSurface的复用与回收。</p>
<h2 id="解码流程">解码流程</h2>
<figure>
<img src="https://images.xiaozhuanlan.com/photo/2020/155967becb2eb29e9c18bcb86353a8dc.png" alt="解码流程" /><figcaption aria-hidden="true">解码流程</figcaption>
</figure>
<p>在解码的过程中如果配置的输出格式与原始数据格式不一致，还会发生转码，触发内存拷贝，要尽量避免。</p>
<h3 id="获取解封装后的数据">获取解封装后的数据</h3>
<p>要想获得解码前的裸数据，即解封装后的裸数据（输出为CMSampleBuffer），可以通过以下方式实现：</p>
<ul>
<li>AVAssetReader：创建track ouput时把<code>outputSetting</code>设为nil。</li>
<li>AVSampleBufferGenerator：只能读出未解码的裸数据。</li>
<li>自己生成CMSampleBuffer：把数据读取出来，构建CMBlockBuffer，再加上时间信息，构建出CMSampleBuffer。这样的CMSampleBuffer不带RPC优化。</li>
</ul>
<h3 id="使用video-toolbox解码">使用Video Toolbox解码</h3>
<p>VTDecompressionSession包含三部分：</p>
<ul>
<li>解码器</li>
<li>CVPixelBufferPool</li>
<li>VTPixelTransferSession</li>
</ul>
<p>使用VTDecompressionSession的步骤：</p>
<ol type="1">
<li>创建VTDecompressionSession；</li>
<li>通过<code>VTSessionSetProperty</code>配置session；</li>
<li>传递CMSampleBuffer视频帧给session解码。</li>
<li>从回调中获取解码CMSampleBuffer。</li>
</ol>
<p>虽然回调是异步的，但回调中的逻辑仍会反向影响解码器的性能。常见的做法是把回调中的解码帧用队列缓存起来，在另外的线程处理。</p>
<h4 id="cvpixelbuffer与metal交互">CVPixelBuffer与Metal交互</h4>
<p>拿到了解码帧CMSampleBuffer，就可以从中取出CVPixelBuffer，接下来就是如何处理和渲染了，这里使用Metal完成。</p>
<p>CVPixelBuffer与Metal交互方式：</p>
<ul>
<li>直接使用IOSurface。即直接通过从CVPixelBuffer取出的IOSurface创建Metal纹理，但要手动处理IOSurface的内存释放（通过<code>IOSurfaceIncrementUseCount</code>、<code>IOSurfaceDecrementUseCount</code>）。</li>
<li>使用CVMetalTextureCache。
<ol type="1">
<li>创建CVMetalTextureCache：<code>CVMetalTextureCacheCreate</code></li>
<li>通过向CVMetalTextureCache传入CVPixelBuffer创建CVMetalTexture：<code>CVMetalTextureCacheCreateTextureFromImage</code></li>
<li>通过CVMetalTexture创建MTLTexture：<code>CVMetalTextureGetTexture</code></li>
</ol></li>
</ul>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://xiaozhuanlan.com/topic/7219405386">WWDC20 10090 - 使用 AVFoundation 和 VideoToolBox 做视频处理 － 小专栏</a></li>
</ul>
]]></content>
      <categories>
        <category>AVFoundation</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>AVFoundation设备配置</title>
    <url>/posts/avfoundation_device_setup/</url>
    <content><![CDATA[<h2 id="帧率与分辨率">帧率与分辨率</h2>
<p>设备帧率与分辨率在帧率大于30fps时，两者有着绑定关系，即不能自由设置。</p>
<h3 id="低帧率模式fps-30">低帧率模式（fps &lt;= 30）</h3>
<p>低帧率模式（fps &lt;= 30）下，帧率和分辨率可以分别自由设置。即设置：</p>
<ul>
<li>AVCaptureSession的<code>sessionPreset</code>：设置分辨率及其预设格式。</li>
<li>AVCaptureDevice的<code>activeVideoMinFrameDuration</code>、<code>activeVideoMaxFrameDuration</code></li>
</ul>
<h3 id="高帧率模式fps-30">高帧率模式（fps &gt; 30）</h3>
<p>高帧率模式（fps &gt; 30）下，帧率不能自由设置，需要遍历设备支持的格式，在格式支持的帧率范围选择合适的帧率。为了统一操作，低帧率模式下也可以应用该设置方式。</p>
<ol type="1">
<li>获取设备对象的<code>formats</code>数组，并进行遍历（也可以进一步获取其中的CMFormatDescription）：
<ol type="1">
<li>获取Format的<code>videoSupportedFrameRateRanges</code>数组的首个元素。</li>
<li>比对AVFrameRateRange的<code>maxFrameRate</code>是否 &gt;= 目标帧率，满足继续，否则跳过循环。</li>
<li>比对Format的<code>formatDescription.dimensions</code>是否满足要求，满足继续，否则跳过循环。</li>
<li>锁定设备进入配置：
<ol type="1">
<li>设置AVCaptureDevice的<code>activeFormat</code>为当前Format对象；</li>
<li>设置AVCaptureDevice的<code>activeVideoMinFrameDuration</code>、<code>activeVideoMaxFrameDuration</code>为目标帧率。</li>
</ol></li>
<li>解锁设备完成配置。</li>
</ol></li>
</ol>
]]></content>
      <categories>
        <category>AVFoundation</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>AVFoundation采集API</title>
    <url>/posts/avfoundation_capture_api/</url>
    <content><![CDATA[<h2 id="核心类">核心类</h2>
<h3 id="avcapturesession">AVCaptureSession</h3>
<p>排插，用于建立输入、输出图的关系。</p>
<p>提供操作：</p>
<ul>
<li>预设配置</li>
<li>增删查输入、输出</li>
<li>增删查连接</li>
<li>开始、停止运行</li>
<li>开始、提交配置</li>
</ul>
<p>使用注意：</p>
<ul>
<li>方法调用应在一个独立的串行队列中进行，以防止影响主线程和实现同步操作。</li>
<li>中断通过通知进行监听。</li>
</ul>
<h3 id="avcapturedevice">AVCaptureDevice</h3>
<p>采集设备硬件功能的封装。</p>
<p>使用注意：</p>
<ul>
<li>使用硬件功能时，需要判断该功能是否可用；</li>
<li>修改设备配置前，需要调用<code>lockForConfiguration</code>进行锁定，以防止外界修改；对应的修改完毕后，调用<code>unlockForConfiguration</code>。</li>
</ul>
<h3 id="avcapturedeviceinput">AVCaptureDeviceInput</h3>
<p>采集设备作为输入的封装。需要封装成 Input 才能添加会话中。</p>
<p>使用注意：</p>
<ul>
<li>切换设备其逻辑要包裹在AVCaptureSession的<code>beginConfiguration</code>和<code>engdConfiguration</code>中，需要根据设备创建Input，先移除后添加。</li>
</ul>
<h3 id="avcaptureoutput">AVCaptureOutput</h3>
<p>抽象输出类，其根据实际的目标数据的需求会有对应的具体类。 + StillImageOutput：静态图片输出 + 图片输出配置 + 拍照操作 + MovieFileOutput：音视频文件输出 + 文件大小限制 + 录制开始和停止操作 + AudioFileOutput：音频文件输出 + 音频格式配置 + 元数据存取 + AudioDataOutput：原始音频帧输出 + 音频格式配置 + VideoDataOutput：原始音频帧输出 + 视频格式配置 + MetadataOutput：元数据输出，可以实现二维码、人脸识别 + DepthDataOutput：深度数据输出</p>
<p>提供connection获取和坐标转换。具体的类通过不同的代理异步输出数据。</p>
<h3 id="avcaptureconnection">AVCaptureConnection</h3>
<p>建立输入和输出的连接，用于控制数据流。只要Input和Output都添加到Session，则可以直接向Output获取Connection。否则需要手动建立连接。</p>
<p>可配置与设备硬件无关的软件处理：videoOrientation、videoScaleAndCropFactor、videoMirroring、videoStabilization。其他的参数需要配置AVCaptureDevice。</p>
<h3 id="avcapturevideopreviewlayer">AVCaptureVideoPreviewLayer</h3>
<p>可直接关联（强引用）AVCaptureSession实现预览。</p>
<h2 id="采集案例">采集案例</h2>
<h3 id="摄像头坐标转换">摄像头坐标转换</h3>
<p>AVCaptureVideoPreviewLayer 提供摄像头坐标和屏幕坐标的转换方法：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">// 屏幕坐标 -&gt; 摄像头坐标</span><br><span class="line">- (CGPoint)captureDevicePointOfInterestForPoint:(CGPoint)pointInLayer;</span><br><span class="line"></span><br><span class="line">// 摄像头坐标 -&gt; 屏幕坐标</span><br><span class="line">- (CGPoint)pointForCaptureDevicePointOfInterest:(CGPoint)captureDevicePointOfInterest;</span><br></pre></td></tr></table></figure>
<h3 id="采集会话配置">采集会话配置</h3>
<ol type="1">
<li>创建 AVCaptureSession；</li>
<li>设置分辨率；</li>
<li>使用 AVCaptureDevice 方法获取其对应类型的对象；</li>
<li>为设备对象创建 Input；</li>
<li>判断会话能否添加该 Input（因为有可能其他应用在使用该设备），是则添加；可将设备对象/input 存到属性，以备切换设备时做判断。</li>
<li>创建 Output 对象，设置其格式。</li>
<li>判断+添加。可以添加多个 Output。</li>
</ol>
<p>开始与停止就是调用 Running 相关的方法。</p>
]]></content>
      <categories>
        <category>AVFoundation</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>音视频通用技术</title>
    <url>/posts/audio_and_video_general_technology/</url>
    <content><![CDATA[<h2 id="直接-alpha-与-预乘-alpha">直接 alpha 与 预乘 alpha</h2>
<h3 id="直接-alpha">直接 alpha</h3>
<p>使用直接 alpha 描述 RGBA 颜色时，颜色的 alpha 值会存储在 alpha 通道中。例如，若要描述具有 60% 不透明度的红色，使用以下值：<span class="math inline">\((255, 0, 0, 255 × 0.6) = (255, 0, 0, 153)\)</span>。其中153（<span class="math inline">\(153 = 255 × 0.6\)</span>）指示颜色应具有 60% 的不透明度。</p>
<h3 id="预乘-alpha">预乘 alpha</h3>
<p>使用预乘 alpha 描述 RGBA 颜色时，每种颜色都会与 alpha 值相乘：<span class="math inline">\((255 × 0.6, 0 × 0.6, 0 × 0.6, 255 × 0.6) = (153, 0, 0, 153)\)</span>。</p>
<p>预乘的好处：</p>
<ul>
<li>混合时可以少一次乘法；</li>
<li>关键：只有预测的纹理才能进行Texture Filtering（除非使用最近邻插值）。使得带透明度的图片纹理可以正常进行线性插值。</li>
</ul>
<p>对于直接alpha转换为预乘alpha的过程，要么预先使用工具进行处理，要么交由GPU处理。</p>
<h2 id="视频或音频数据存储的2种格式packed和planar">视频或音频数据存储的2种格式packed和planar</h2>
<p>假设有一路音频流，有左右两声道的数据。左声道用L表示，右声道用R表示。</p>
<p>存储时，如果是左右声道数据交替存储成一维数组，这种格式称为packed。格式为LRLRLR....LRLR</p>
<p>如果是分开存储成二维数组，这种格式称为planar。格式为LLLLLLLLLLLLLL和RRRRRRRRRRRRR</p>
<p>视频也是如此，但是对于YUV格式的数据，比音频多一种存储方法叫semi-planar，也就是半planar。一共2路存储，Y一路，UV一路，其中UV交叉存储。</p>
<h2 id="视频播放器原理">视频播放器原理</h2>
<p>视频播放器播放一个互联网上的视频文件，需要经过以下几个步骤：解协议，解封装，解码视音频，视音频同步。如果播放本地文件则不需要解协议，为以下几个步骤：解封装，解码视音频，视音频同步。他们的过程如图所示。</p>
<pre class="mermaid">flowchart TB
流数据 --解协议--> 封装格式数据;
封装格式数据 --解封装--> 音频压缩数据 --音频解码--> 音频原始数据 --> 音视频同步 --> 视频驱动/设备;
封装格式数据 --解封装--> 视频压缩数据 --视频解码--> 视频原始数据 --> 音视频同步 --> 音频驱动/设备;</pre>
<p>其中各个阶段的具体格式：</p>
<ul>
<li>流数据/协议层：HTTP、RTMP、<strong>FILE</strong>……</li>
<li>封装格式：MKV、MP4、FLV、MPEG-TS、AVI……</li>
<li>压缩数据：H264、H265、MPEG2、AAC……</li>
<li>原始数据：YUV420P、YUV422P、RGB24、PCM……</li>
</ul>
<p><strong>解协议</strong>：将流媒体协议的数据，解析为标准的相应的封装格式数据。视音频在网络上传播的时候，常常采用各种流媒体协议，例如HTTP、RTMP或是MMS等等。这些协议在传输视音频数据的同时，也会传输一些信令数据。这些信令数据包括对播放的控制（播放，暂停，停止），或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如，采用RTMP协议传输的数据，经过解协议操作后，输出FLV格式的数据。</p>
<p><strong>解封装</strong>：将输入的封装格式的数据，分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多，例如MP4，MKV，RMVB，TS，FLV，AVI等等，它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如，FLV格式的数据，经过解封装操作后，输出H.264编码的视频码流和AAC编码的音频码流。</p>
<p><strong>解码</strong>：将视频/音频压缩编码数据，解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC，MP3，AC-3等等，视频的压缩编码标准则包含H.264，MPEG2，VC-1等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码，压缩编码的视频数据输出成为非压缩的颜色数据，例如YUV420P，RGB等等；压缩编码的音频数据输出成为非压缩的音频抽样数据，例如PCM数据。</p>
<p><strong>视音频同步</strong>：根据解封装模块处理过程中获取到的参数信息，同步解码出来的视频和音频数据，并将视频音频数据送至系统的显卡和声卡播放出来。</p>
<h2 id="音视频压缩与传统数据压缩">音视频压缩与传统数据压缩</h2>
<p>无论是视频还是音频，在传统压缩算法看来，文件中基本么有什么冗余信息，音视频的压缩都是人们对音视频针对性开发压缩算法，去掉实际的冗余信息。</p>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>流媒体传输协议</title>
    <url>/posts/streaming_transfer_protocol/</url>
    <content><![CDATA[<p>流媒体协议是服务器与客户端之间通信遵循的规定。</p>
<h2 id="rtsp">RTSP</h2>
<p>该协议定义了一对多应用程序如何有效地通过IP网络传送多媒体数据。RTSP提供了一个可扩展框架，使实时数据，如音频与视频的受控、点播成为可能。数据源包括现场数据与存储在剪辑中的数据。该协议目的在于控制多个数据发送连接，为选择发送通道，如UDP、多播UDP与TCP提供途径，并为选择基于RTP上发送机制提供方法。</p>
<h2 id="rtmp">RTMP</h2>
<p>RTMP是Adobe Systems公司为Flash播放器和服务器之间音频、视频和数据实时传输开发的开放协议，因为是开放协议所以都可以使用。</p>
<ul>
<li>RTMP协议用于对象、视频、音频的传输，这个协议建立在TCP协议或者轮询HTTP协议之上。</li>
<li>RTMP协议就像一个用来装数据包的容器，这些数据可以是FLV中的视音频数据。一个单一的连接可以通过不同的通道传输多路网络流，这些通道中的包都是按照固定大小的包传输的。</li>
</ul>
<h2 id="hls">HLS</h2>
<p>HTTP Live Streaming 把整个流分成一个个基于 HTTP 的文件来下载，每次只下载一些。HLS 协议由三部分组成：HTTP（传输协议）、M3U8（索引文件）、TS（音视频媒体信息）。</p>
<p>编码格式要求：</p>
<ul>
<li><p>视频编码格式：H264</p></li>
<li><p>音频的编码格式：AAC、MP3、AC-3</p></li>
<li><p>视频的封装格式：ts</p></li>
<li><p>保存 ts 索引的 M3U8 文件</p></li>
</ul>
<p>优势：</p>
<ul>
<li><p>相对于 RTMP 来讲使用了标准的 HTTP 协议来传输数据，可以避免在一些特殊的网络环境下被屏蔽</p></li>
<li><p>在服务端做负载均衡要简单。因为 HLS 是基于无协议的 HTTP 实现的，客户端只需要按照顺序下载存储在服务器的普通 ts 文件进行播放即可。而 RTMP 是一种有状态协议，很难对视频服务器进行平滑扩展，因为需要为每一个播放视频流的客户端维护状态。</p></li>
<li><p>HLS 协议本身实现了码率自适应，在不同的带宽情况下，设备可以自动切换到最适合自己码率的视频播放。</p></li>
</ul>
<p>劣势：</p>
<p>延迟，很难做到 10s 以下，而 RTMP 可以降到 3s~4s。</p>
<h3 id="m3u8">M3U8</h3>
<ul>
<li><p>EXTM3U</p>
<ul>
<li>首行</li>
</ul></li>
<li><p>EXT-X-VERSION</p>
<ul>
<li>格式版本</li>
</ul></li>
<li><p>EXT-TARGETDURATION</p>
<ul>
<li>最大切片时长的四舍五入值</li>
</ul></li>
<li><p>EXT-X-MEDIA-SEQUENCE</p>
<ul>
<li>直播切片序列</li>
</ul></li>
<li><p>EXTINF</p>
<ul>
<li>每个切片时长</li>
<li>下方为分片路径</li>
</ul></li>
<li><p>EXT-X-ENDLIST</p>
<ul>
<li>不会产生更多切片，该 M3U8 停止更新</li>
</ul></li>
<li><p>EXT-X-STREAM-INF</p>
<ul>
<li><p>多级 M3U8 文件，支持二级</p></li>
<li><p>后接参数：</p>
<ul>
<li>BANDWIDTH，最高码率值</li>
<li>AVERAGE-BANDWIDTH，平均码率值</li>
</ul></li>
<li><p>下面接子 M3U8 路径</p></li>
</ul></li>
</ul>
<h3 id="客户端逻辑">客户端逻辑</h3>
<ol type="1">
<li>通过给定URI获取播放列表。若是Master Playlist，客户端选择一个Variant Stream来播放。</li>
<li>客户端检查<code>#EXT-X-VERSION</code>版本是否满足。</li>
<li>客户端忽略不可识别的tags、属性键值对。</li>
<li>加载Media Playlist，选择一个segment开始播放。</li>
<li>播放完一个segment后，根据客户端当前的具体情况选择一个新的segment，并重复执行播放操作。</li>
<li>对与直播，需要定期刷新Media Playlist，并选择合适的segment播放。</li>
</ol>
<h3 id="客户端码率切换">客户端码率切换</h3>
<p>HLS服务器提供几种可选的码率。客户端需要自主完成码率切换。客户端判断是否切换码率的因素：</p>
<ul>
<li>设备实际的下载速度（与Master Playlist的Variant Stream标签中的码率/带宽做比较）</li>
<li>设备运行情况（CPU、内存、屏幕分辨率）</li>
</ul>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://blog.csdn.net/bingqingsuimeng/article/details/79184948">简述HLS,HTTP,RTSP,RTMP协议的区别_bingqingsuimeng的专栏-CSDN博客</a></li>
<li></li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>RTMP</title>
    <url>/posts/rtmp/</url>
    <content><![CDATA[<p>RTMP，Real Time Messaging Protocol，使用TCP，默认在1935端口上传输一般的FLV格式流。</p>
<p>优点：</p>
<ul>
<li>支持加密</li>
<li>隐私性好</li>
<li>实时性好</li>
<li>延迟相对较低</li>
</ul>
<p>缺点：</p>
<ul>
<li>使用非公共端口，可能被防火墙拦截；</li>
<li>跨平台差</li>
</ul>
<p>常用应用领域：</p>
<ul>
<li>娱乐直播</li>
<li>点播</li>
</ul>
<p>应用会比HLS更为广泛，主要还是因为传输效率较高、基建比较成熟。</p>
<p>发展方向：使用UDP逐渐替代TCP方案，把传输做薄。</p>
<p>RTMP是在TCP建立连接的基础之上传输，即底层使用使用TCP的。经过RTMP握手后，建立RTMP <strong>Connetction</strong>，然后<strong>stream</strong>传输。</p>
<p>创建流的基本流程：</p>
<ol type="1">
<li>通过socket建立TCP连接；</li>
<li>RTMP握手；</li>
<li>建立RTMP连接；</li>
<li>创建RTMP流。</li>
</ol>
<p>握手过程：</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/截屏2021-06-03%20下午6.17.58.png" alt="握手" /><figcaption aria-hidden="true">握手</figcaption>
</figure>
<p>建立连接的过程：</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/r4TCIf.png" alt="建立连接" /><figcaption aria-hidden="true">建立连接</figcaption>
</figure>
<p>创建流的过程：</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/CjhJhz.png" alt="创建流" /><figcaption aria-hidden="true">创建流</figcaption>
</figure>
<p>推流过程：</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/QvkEbr.png" alt="推流" /><figcaption aria-hidden="true">推流</figcaption>
</figure>
<p>拉流过程：</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/XQBrlm.png" alt="拉流" /><figcaption aria-hidden="true">拉流</figcaption>
</figure>
<h2 id="消息格式">消息格式</h2>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/S8cLlD.png" /></p>
<p>当连接建立好后，就可以发送消息了，RTMP消息有固定的格式，如上图，这是RTMP协议中最复杂的部分。</p>
<p>如一般网络协议，整个RTMP的消息由Header和Body组成。</p>
<p>Header分为三个部分：</p>
<ul>
<li>Basic Header，必有</li>
<li>Message Header，可选</li>
<li>Extended Timestamp，可选</li>
</ul>
<p>后两者是否存在是根据Basic Header的值决定的。</p>
<p>Basic Header的大小也是动态变化的，其大小由第一个字节决定。Basic Header第一个字节组成：</p>
<ul>
<li>前2位：fmt。</li>
<li>后6位：取值为0、1、2～63。约束当前或后续字节是否为chunk string id（CSID）。
<ul>
<li>0，则表示整个Basic Header占2个字节，即使用第2个字节表达CSID。</li>
<li>1，则表示整个Basic Header占4个字节，即使用后续3个字节表达CSID。</li>
<li>2～63，则表示整个Basic Header占1个字节，即fmt后面的6位是CSID。自己使用的话基本够用。</li>
</ul></li>
</ul>
<p>Message Header是可选的，也是动态大小的。这两者都是由Basic Header的fmt决定的，fmt是2位，可表示以下状态：</p>
<ul>
<li>00，Message Header最长，占11字节，即包含：TimeStamp(3) + MegLength(3) + TypeID(1) + StreamID(4)。</li>
<li>01，Message Header占7字节，即包含：TimeStamp(3) + MegLength(3) + TypeID(1)。</li>
<li>10，Message Header占3字节，即包含：TimeStamp(3)。</li>
<li>11，没有Message Header。</li>
</ul>
<p>之所以是可变的，是因为同一个流、同一个包分为多个消息/chunk/块传输，有很多信息只需要传一次就可以，后续客户端收到的信息复用前面的信息即可。</p>
<p>当用Message Header还表达信息的时候，就需要Extended Timestamp。当Message Header中的TimeStamp值为0xFFFFFF时，就存在Extended Timestamp。</p>
<h3 id="消息类型typeid">消息类型/TypeID</h3>
<table>
<thead>
<tr class="header">
<th>TypeID</th>
<th>作用</th>
<th>SID</th>
<th>CSID</th>
<th>分类</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>1</td>
<td>Set Chunk Size</td>
<td>0</td>
<td>2</td>
<td>控制消息</td>
</tr>
<tr class="even">
<td>2</td>
<td>Abort Message</td>
<td>0</td>
<td>2</td>
<td>控制消息</td>
</tr>
<tr class="odd">
<td>3</td>
<td>Acknowledgement</td>
<td>0</td>
<td>2</td>
<td>控制消息</td>
</tr>
<tr class="even">
<td>5</td>
<td>Window Acknowlegement Size</td>
<td>0</td>
<td>2</td>
<td>控制消息</td>
</tr>
<tr class="odd">
<td>6</td>
<td>Set Peer Bandwidth</td>
<td>0</td>
<td>2</td>
<td>控制消息</td>
</tr>
<tr class="even">
<td>8</td>
<td>音频数据</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr class="odd">
<td>9</td>
<td>视频数据</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr class="even">
<td>15（AMF3），18（AFM0）</td>
<td>Data Message</td>
<td></td>
<td></td>
<td>命令消息</td>
</tr>
<tr class="odd">
<td>16（AFM3），19（AFM0）</td>
<td>Shared Object Message</td>
<td></td>
<td></td>
<td>命令消息</td>
</tr>
<tr class="even">
<td>17（AFM3），20（AFM0）</td>
<td>Command Message</td>
<td></td>
<td></td>
<td>命令消息</td>
</tr>
<tr class="odd">
<td>22</td>
<td>Aggregate Message</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p>AMF是Flash的编码数据格式，其形式像KLV（Key+Length+Value）。</p>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>多媒体容器</title>
    <url>/posts/multimedia_container/</url>
    <content><![CDATA[<p>我们常说的视频文件格式常常只是多媒体封装格式，里面不仅包含了视频，还有音频、字幕等媒体信息。而纯视频码流的格式更多使用使用编码格式表达。</p>
<p><strong>多媒体封装格式</strong>（<strong>M</strong>ultimedia <strong>C</strong>ontainer <strong>F</strong>ormat，简称MCF、多媒体容器），是一种开放（没有身份规限，免费）、自由的数据格式。</p>
<p>多媒体文件是个容器。容器里面存在多个<strong>流（stream/track）</strong>。每种流是由不同的编码器编程生成的。从流中读出的数据称为<strong>包</strong>。在一个包中包含一个或多个<strong>帧</strong>。</p>
<p>容器格式内部对音视频数据的处理都是大同小异，区别点并不大。更多的差距在于它们对于不同编码格式的支持程度、元数据的详细程度以及对于是否能够支持音视频以外的数据。</p>
<p>不同的容器具有不同的特点，下面简单介绍常用的多媒体容器。</p>
<h2 id="avi">AVI</h2>
<p>AVI，Audio Video Interleave。</p>
<ul>
<li>机构：Mircrosoft</li>
<li>不支持流媒体</li>
<li>支持编解码器：几乎所有</li>
<li>BT下载视频</li>
</ul>
<p>一种RIFF（Resource Interchange File Format）文件格式。同样使用RIFF文件格式还有WAV格式文件。RIFF使用小端序存储。</p>
<p>主体中的图像数据和声音数据是交叉存放的，以此达到音视频同步。从尾部的索引可以跳转到要播放的位置。</p>
<p>播放时间没有直接的字段由读取的帧数和帧率计算得出。</p>
<p>缺点：</p>
<ul>
<li>由于索引在文件尾部，所以不适合用来流传输。</li>
<li>容器中我没有时间戳，只能通过帧数和帧率计算得出。在索引中也没有写明时间戳和媒体位置的信息，所以在播放AVI时seek操作还需要额外的技术手段。</li>
<li>由于媒体数据分块存放，使得它对很多使用运动预测特定的视频编码的支持不是很好，因为预测帧需要访问帧外的数据。</li>
</ul>
<h3 id="二进制构成">二进制构成</h3>
<p>AVI文件是一个类型为<code>AVI</code>的RIFF块，主要有三个subchunk构成：</p>
<ul>
<li><code>hdrl</code> LIST，信息块：元数据</li>
<li><code>movi</code> LIST，数据块：保存音视频序列数据</li>
<li><code>idxl</code> LIST，索引块（可选）</li>
</ul>
<p>结构示意：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">RIFF (‘AVI ’</span><br><span class="line">      LIST (‘hdrl’</span><br><span class="line">            ‘avih’(主AVI信息头数据)</span><br><span class="line">            LIST (‘strl’</span><br><span class="line">                  ‘strh’ (流的头信息数据)</span><br><span class="line">                  ‘strf’ (流的格式信息数据)</span><br><span class="line">                  [‘strd’ (可选的额外的头信息数据) ]</span><br><span class="line">                  [‘strn’ (可选的流的名字) ]</span><br><span class="line">                  ...</span><br><span class="line">                 )</span><br><span class="line">             ...</span><br><span class="line">           )</span><br><span class="line">      LIST (‘movi’</span><br><span class="line">            &#123; SubChunk | LIST (‘rec ’</span><br><span class="line">                              SubChunk1</span><br><span class="line">                              SubChunk2</span><br><span class="line">                              ...</span><br><span class="line">                             )</span><br><span class="line">               ...</span><br><span class="line">            &#125;</span><br><span class="line">            ...</span><br><span class="line">           )</span><br><span class="line">      [‘idx1’ (可选的AVI索引块数据) ]</span><br><span class="line">     )</span><br></pre></td></tr></table></figure>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/Gb5mrn.jpg" /></p>
<p>参考：</p>
<ul>
<li><a href="https://www.cnblogs.com/tocy/p/media_container_8-avi.html">多媒体文件格式之AVI - Tocy - 博客园</a></li>
<li><a href="https://sp4n9x.github.io/2020/12/30/AVI%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F%E5%88%86%E6%9E%90/">AVI文件格式分析 | Sp4n9x's Blog</a></li>
</ul>
<h2 id="mov">MOV</h2>
<p>MOV，QuickTime File Format（QTFF）</p>
<ul>
<li>机构：Apple</li>
<li>支持流媒体</li>
<li>支持编解码器：
<ul>
<li>音：AAC、MPEG-1 Layers I/II/III、AC-3等</li>
<li>视：MPEG-2/4、H.264等</li>
</ul></li>
<li>点播、直播</li>
</ul>
<h3 id="相关概念">相关概念</h3>
<p>atom：QTFF的基本数据单元，可以来容纳实际的音视频数据，也可以放置元数据和字幕等信息，通过层层嵌套方式形成树状结构。atom容纳的数据类型和大小在atom头部进行描述。</p>
<h2 id="mp4">MP4</h2>
<p>MP4，MPEG-4 Part14</p>
<ul>
<li>机构：MPEG</li>
<li>支持流媒体</li>
<li>支持编解码器：
<ul>
<li>音：AAC、MPEG-1 Layers I/II/III、AC-3等</li>
<li>视：MPEG-2/4、H.264、H.263等</li>
</ul></li>
<li>互联网视频网站</li>
</ul>
<p>MP4由多个包含不同信息的box，以树形式组织构成，与MOV的atom几乎一致。</p>
<p>根结点下包含三个box：</p>
<ul>
<li><code>ftyp</code>：文件类型</li>
<li><code>moov</code>：元数据</li>
<li><code>mdat</code>：媒体数据</li>
</ul>
<p>把moov放到mdat前面可以更快准备播放。</p>
<h3 id="box结构">box结构</h3>
<p>构成：</p>
<ul>
<li>header：指明box大小和类型；
<ul>
<li>增加了<code>version</code>（8位）和<code>flags</code>（24位）字段的成为FullBox。</li>
<li>当size等于0时，代表这个box是文件最后一个box。当size等于1时，说明box长度需要更多的位来描述，在后面会定义一个64位的largesize来描述box的长度。</li>
</ul></li>
<li>body：数据或box</li>
</ul>
<h3 id="常用容器">常用容器</h3>
<ul>
<li><p>moov，音视频数据的元数据信息</p>
<ul>
<li><p>mvhd，影片文件头信息，未压缩过的影片信息的头容器</p></li>
<li><p>trak，多个，各轨道信息容器</p>
<ul>
<li><p>tkhd，轨道元数据（TrackID、Duration、音量等等）</p></li>
<li><p>edts</p>
<ul>
<li>如果没有该表，那么这个轨道会立即开始播放，一个空的 edts 数据用来定位对轨道的起始时间偏移位置</li>
</ul></li>
<li><p>mdia</p>
<ul>
<li><p>mdhd，媒体头</p></li>
<li><p>hdlr，句柄参考</p></li>
<li><p>minf，媒体信息</p>
<ul>
<li><p>vmhd，视频信息头</p></li>
<li><p>smhd，音频信息头</p></li>
<li><p>dinf，数据信息</p></li>
<li><p>stdl，采样表</p></li>
</ul></li>
</ul></li>
</ul></li>
</ul></li>
</ul>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/BkC2zj.jpg" /></p>
<p><code>mdat</code> box中的多媒体数据是没有结构的，是参考<code>moov</code>的<code>track</code> box解析。<code>moov</code>包含了整个多媒体文件的元数据，seek也是通过该box实现，通过其中的各个表查到数据偏移位置。</p>
<h2 id="flv">FLV</h2>
<p>FLV，FLash Video。</p>
<ul>
<li>机构：Adobe</li>
<li>支持流媒体</li>
<li>支持编解码器：
<ul>
<li>音：MP3、ADPCM、Linear PCM、AAC等</li>
<li>视：Sorenson、VP6、H.264</li>
</ul></li>
<li>互联网视频</li>
</ul>
<p>FLV常用做流媒体。</p>
<p>结构：</p>
<ul>
<li><p>FLV Header</p>
<ul>
<li><p>字符 FLV 签名字段</p></li>
<li><p>版本</p></li>
<li><p>保留标记</p></li>
<li><p>音视频标记</p></li>
<li><p>数据偏移</p></li>
</ul></li>
<li><p>FLV Body，这里body只是一个概念，具体直接就是PreviousTagSize和Tag</p>
<ul>
<li><p>PreviousTagSize #0：0</p></li>
<li><p>TAG #1</p>
<ul>
<li><p>TAG Header</p>
<ul>
<li><p>Type</p></li>
<li><p>DataSize</p></li>
<li><p><strong>TimeStamp</strong></p></li>
</ul></li>
<li><p>TAG Data</p>
<ul>
<li><p>Audio Tag Data</p>
<ul>
<li>第一个字节包含音频数据的参数信息</li>
<li>第二个字节开始为音频流数据</li>
</ul></li>
<li><p>Video Tag Data（同上）</p></li>
<li><p>Script Tag Data</p>
<ul>
<li>常用于展示元数据，存储的数据格式一般为 AMF 格式</li>
</ul></li>
</ul></li>
</ul></li>
<li><p>PreviousTagSize #1：上一个TAG大小。</p></li>
<li><p>TAG #2</p></li>
<li><p>PreviousTagSize #2</p></li>
<li><p>...</p></li>
</ul></li>
</ul>
<p>参考：</p>
<ul>
<li><a href="https://juejin.cn/post/6844903555027959822">Flv格式解析 - 掘金</a></li>
</ul>
<h2 id="ts">TS</h2>
<p>TS，MPEG2-TS</p>
<ul>
<li>机构：MPEG</li>
<li>支持流媒体</li>
<li>支持编解码器：
<ul>
<li>音：MPEG-1 Layers I/II/III、AAC</li>
<li>视：MPEG-1/2/4、H.264</li>
</ul></li>
<li>IPTV，低延时直播</li>
</ul>
<p>TS文件为传输流文件，其特点是要求从视频流的任意片段开始都是可以独立解码的。TS容器是为了流传输而设计的。</p>
<p>在MPEG-2标准中，有两种不同类型的码流输出到信道：一种是节目码流（Program Stream, PS），适用于没有误差产生的媒体存储，如DVD等存储介质（.vob）。另一种是传送流（Transport stream, TS)，适用于有信道噪声产生的传输，目前TS流广泛应用于广播电视中，如机顶盒等。</p>
<p>TS文件分层：</p>
<ul>
<li>ES，Elementary Stream：原始流，直接从编码器出来的裸数据。</li>
<li>PES，Packet Elemental Stream：分割打包的ES流，加入了PES头（PTS、DTS等）。PES由包头和playload组成。</li>
<li>TS层，Transport Stream：传输流。是在PES层的基础上加入数据流的识别和传输必须的信息。<strong>固定包长度为188字节</strong>，以便于找到帧的起始位置，易于从丢包中恢复。</li>
</ul>
<p>为了便于传输，实现时分复用，基本流ES必须打包，就是将顺序连续、连续传输的数据流按一定的时间长度进行分割，分割的小段叫包，因此打包也称为分组。</p>
<p>参考：<a href="https://www.cnblogs.com/jiayayao/p/6832614.html">TS流基本概念</a>、<a href="https://blog.csdn.net/dxpqxb/article/details/79654004">ts流格式详解</a></p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/0QxdDS.jpg" /></p>
<h2 id="mkv">MKV</h2>
<p>MKV，Matroska Video File</p>
<ul>
<li>机构：Matroska</li>
<li>支持流媒体</li>
<li>支持编解码器：几乎所有</li>
<li>点播、直播</li>
</ul>
<p>开放标准、免费使用，可放入多种媒体信息，且不限数量。而且是目前唯一一个支持封装ASS字幕的格式。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Comparison_of_video_container_formats">Comparison of video container formats - Wikipedia</a></li>
<li><a href="https://www.jianshu.com/p/529c3729f357">mp4文件格式解析 - 简书</a></li>
<li><a href="https://blog.xinoassassin.me/2019/10/Media-Containers/">多媒体容器格式变迁史 | 随写 - XinoAssassin's Blog</a></li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FLV格式</title>
    <url>/posts/flv/</url>
    <content><![CDATA[<p>FLV与RTMP协议有密切的联系。每个RTMP的数据加个头就是FLV了。</p>
<p>FLV文件是以FLV格式存储的。</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/edhpT8.png" /></p>
<p>FLV文件 = FLV header + 数据</p>
<p>FLV header，占9字节：</p>
<ul>
<li>前3字节：F、L、V</li>
<li>版本，值为1。</li>
<li>类型
<ul>
<li>0～5位，保留，必须是0。</li>
<li>6位，是否有音频tag。ii</li>
<li>7位，保留，必须是0。</li>
<li>8位，是否有视频tag。</li>
</ul></li>
<li>偏移量，占4字节，Header的大小，必须是9。</li>
</ul>
<p>数据是由一个个分组组成，一个分组的结构：</p>
<ul>
<li>pre tagsize，占4字节，前一个tag的大小，即tag大小在tag的后面存放。</li>
<li>Tag</li>
</ul>
<p>Tag的结构：</p>
<ul>
<li>TT，1字节，Tag类型。0x08音频，0x09视频，0x12script脚本。</li>
<li>DataSize，3字节，Tag body数据大小（PreTagSize - Tag Header Size）</li>
<li>TimeSta，3字节，时间戳（毫秒）</li>
<li>E，1字节，扩展时间戳。</li>
<li>SID，3字节，StreamID，始终是0。</li>
<li>Tag DATA</li>
</ul>
<p>Tag DATA可以保存两种类型数据：音频、视频。</p>
<ul>
<li>音频Tag DATA
<ul>
<li>header
<ul>
<li>SF，采样率</li>
<li>SR</li>
<li>SS</li>
<li>ST</li>
</ul></li>
<li><h2 id="data">data</h2></li>
</ul></li>
<li>视频Tag DATA</li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>常见音频格式</title>
    <url>/posts/common_audio_formats/</url>
    <content><![CDATA[<p>电视广播离不开声音信号，随着人们对电视质量的要求越来越高，在数字电视广播、高清晰数字电视和数字电影中不仅应有高质量的图像，还要保证有高质量的伴音。</p>
<p>音频文件格式往往包含了对音频编码格式的表达，两者一般是一对一的关系，或者说音频文件格式也是音频编码格式。</p>
<h2 id="pcm">PCM</h2>
<p>PCM (PulseCode Modulation) 被称为脉码编码调制。PCM中的声音数据没有被压缩，如果是单声道的文件，采样数据按时间的先后顺序依次存入(它的基本组织单位是BYTE(8bit)或WORD(16bit))，如果是双声道的文件，采样数据按时间先后顺序交叉地存入。如图所示：</p>
<figure>
<img src="https://gitee.com/bqlin/image-land/raw/master/pcm_storage.png" alt="PCM存储方式" /><figcaption aria-hidden="true">PCM存储方式</figcaption>
</figure>
<p>PCM的每个样本值包含在一个整数i中，i的长度为容纳指定样本长度所需的最小字节数。8位和16位的PCM波形样本的数据格式：</p>
<table>
<thead>
<tr class="header">
<th>样本大小</th>
<th>数据格式</th>
<th>最小值</th>
<th>最大值</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>8bit PCM</td>
<td>unsigned int</td>
<td>0</td>
<td>255</td>
</tr>
<tr class="even">
<td>16bit PCM</td>
<td>int</td>
<td>-32768</td>
<td>32767</td>
</tr>
</tbody>
</table>
<p>PCM没有保存元数据信息，播放时要准确指定采样格式、采样率和声道才能播放。</p>
<h2 id="wav">WAV</h2>
<p>实现方式很多，在原 PCM 数据格式前面加上 44 字节元数据描述 PCM：采样率、声道数、数据格式等。</p>
<p>特点：保留原始PCM，音质好，大量软件都支持；</p>
<p>适合场景：高比特率下对兼容性有要求的音乐欣赏。</p>
<p>WAV格式符合资源交换文件格式(RIFF，ResourceInterchange File Format)规范。WAV文件分为两个部分：头信息和PCM音频数据。</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772960502-9cc13cf5-f355-467b-98e7-ff90a3db7246.png" /></p>
<h2 id="有损压缩编码格式">有损压缩编码格式</h2>
<h3 id="mp3">MP3</h3>
<p>MP3，MPEG-1 Audio Layer III，也可以是MPEG-2 Audio Layer III。</p>
<p>技术细节：</p>
<ul>
<li>使用MDCT算法，修正了DCT算法上的一些缺陷。</li>
<li>使用声学心理模型：
<ul>
<li>人耳听觉范围是20Hz～20kHz，去掉高频信息；</li>
<li>人耳对2000Hz～5000Hz最灵敏，两端下降比较厉害，尤其是高频，在去掉部分高频信息；</li>
<li>人耳有遮蔽效应，去掉频域和时域遮蔽的部分；</li>
</ul></li>
<li>使用哈夫曼编码压缩音频数据。</li>
</ul>
<p>缺点：</p>
<ul>
<li>CBR编码对20kHz以上的声音一刀切。当然也可以使用VBR规避。</li>
<li>最初使用的ID3标签没有统一的文本编码。ID3 v2对此做了修正。</li>
<li>多声道支持较差。非主流的MPEG-2 Audio Layer III才支持了5.1声道。</li>
</ul>
<h3 id="aac">AAC</h3>
<p>AAC，Advanced Audio Coding。为了取代MP3。目前较热门的有损压缩编码技术，衍生了 LC-AAC、HE-AAC、HE-AAC v2 这三种主要编码格式。</p>
<p>特点：在小于 128Kbit/s 的码率下表现优异，并且多用于视频中的音频编码。</p>
<p>适用场景：128Kbit/s 以下的音频编码，多用于视频中音频轨的编码。</p>
<p>技术细节：</p>
<ul>
<li>使用了完整的MDCT算法，编码效率上更胜一筹。一般同等码率下，AAC质量比MP3更好一些。</li>
<li>支持更大的采样率（16～48kHz=&gt;8~96kHz）。</li>
<li>支持高达48个声道。</li>
<li>对频率高于16kHz的音质更好。</li>
</ul>
<h3 id="ogg">Ogg</h3>
<p>Ogg是Vorbis编码的容器。</p>
<ul>
<li>非常有潜力，各种码率下都有比较优秀的表现。</li>
<li>尤其在低码率情况下，编码算法出色，可以用更小码率达到更好的音质。</li>
</ul>
<p>特点：可以用比MP3更小的码率实现比MP3更好的音质，高中低码率下均有良好的表现，兼容不够好，流媒体特性不支持。</p>
<p>使用场景：语言聊天的音频消息场景</p>
<p>技术细节：</p>
<p>基于MDCT时频转换，然后通过心理声学进行频段舍弃。后续使用矢量量化算法，使得在低码率下有着很好的表现，接近AAC HE，但还没能超越。</p>
<h3 id="opus">Opus</h3>
<ul>
<li>编码比Vorbis更好的低码率表现，并在同码率下超越了AAC HE。</li>
<li>低延时。使得在数字语音通信领域中应用广泛。</li>
</ul>
<h3 id="ac-3">AC-3</h3>
<p>AC-3，Dolby Digital。</p>
<ul>
<li>首个使用MDCT算法进行压缩的编码，同时还使用音频心理学研究成果对压缩算法进行优化，使得最终压缩后的产物仍拥有影院基本效果。</li>
<li>DD编码一般有6个声道，称为DD 5.1。</li>
<li>元数据中带有对解码过程进行控制的相关信息，使得它在支持的播放器上可以还原出制片方想要的效果。</li>
</ul>
<p>缺点：</p>
<ul>
<li>只支持固定码率编码，使得码率比较高。</li>
</ul>
<p>升级版本：E-AC-3，Dolby Digital Plus。</p>
<h3 id="dts">DTS</h3>
<p>Dolby的竞争对手。</p>
<p>选择ADPCM作为算法基础，采用自适应采样大小记录电平值。对存储空间利用率更高。同时，相对于使用MDCT算法算出不同频率段再砍掉人耳不敏感部分的做法，基于AFPCM的算法虽压缩比低一些，但对声音细节保留得更好。</p>
<p>但也是因为使用自适应采样大小，使得体积控制比DD要差一些，这限制了它的使用。</p>
<h2 id="总结">总结</h2>
<p>大部分编码格式都是基于PCM进行无损、有损的压缩编码。使用的编码技术都是类似的。有损压缩编码基于MDCT，然后经过心理声学模型取出人耳不敏感的信息，最后经过哈夫曼编码压缩。无损压缩编码都是基于线性预测编码。不同的格式可能更多是机构、厂商竞争的产物。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://blog.xinoassassin.me/2019/11/Audio-Codecs/">音频编码变迁录</a></li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>AAC 编解码器</title>
    <url>/posts/aac/</url>
    <content><![CDATA[<h2 id="编码规格">编码规格</h2>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615773129050-3a7142f2-f0e9-451c-b9b9-22716478e3b7.png" /></p>
<ul>
<li><p>AAC LC：<strong>Low Complexity</strong>。低复杂度规格，码流是 128k，音质好。主要应用于中高编码率的场景编码（&gt;= 80Kbit/s）。</p></li>
<li><p>AAC HE V1：AAC LC + <strong>SBR（Spectral Band Replication）</strong>。其核心思想是按频谱保存，低频编码保存主要部分，高频单独放大编码保存音质。码流在 64k 左右。主要应用于低码率的编码（&lt;= 48Kbit/s）</p></li>
<li><p>AAC HE V2：AAC LC + SBR + <strong>PS（Parametric Stereo）</strong>。其核心思想是双声道中的声音存在某种相似度，只需存储一个声道的全部信息，然后，花很少的字节用参数描述另一个声道和它不同的地方。</p></li>
</ul>
<p>一般码流越大，其保真度越高。码流越小，压缩比高，去除的冗余信息就多，其就会对声音造成一定的损失。</p>
<h2 id="编码格式">编码格式</h2>
<h4 id="adifaudio-data-interchange-format">ADIF（Audio Data Interchange Format）</h4>
<p>可以确定地找到这个音频数据的开始，相当于 AAC 数据加上数据头。只能从头开始解码，不能在音频数据流中间开始。应用于磁盘文件。</p>
<h4 id="adtsaudio-data-transport-stream">ADTS（Audio Data Transport Stream）</h4>
<p>每一帧都有一个同步字，所以可以在音频流的任意位置开始解码。会比 ADIF 的数据大。应用于流媒体。</p>
<h3 id="adts-结构">ADTS 结构</h3>
<p>ADTS由7或9个字节组成，但其字段排列是按照位来排列。以下每段（即图的一行）是一个字节（8位），每个字母表示一位（0/1），每四位可用一个十六进制表示。</p>
<p>二进制表示：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)</span><br></pre></td></tr></table></figure>
<p>Header consists of 7 or 9 bytes (without or with CRC).</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1622170367857-03c1ed4e-cb1e-49fd-b9c0-b06137675a3f.png" /></p>
<table>
<colgroup>
<col style="width: 7%" />
<col style="width: 16%" />
<col style="width: 75%" />
</colgroup>
<thead>
<tr class="header">
<th>Letter</th>
<th>Length (bits)</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>A</td>
<td>12</td>
<td>syncword 0xFFF, all bits <strong>must</strong> be 1<br />同步字，表示这是个 ADTS 数据，都为1。</td>
</tr>
<tr class="even">
<td>B</td>
<td>1</td>
<td>MPEG Version: 0 for MPEG-4, 1 for MPEG-2<br />使用的编码规范。</td>
</tr>
<tr class="odd">
<td>C</td>
<td>2</td>
<td>Layer: always 0</td>
</tr>
<tr class="even">
<td>D</td>
<td>1</td>
<td>protection absent, <strong>Warning</strong>, set to 1 if there is no CRC and 0 if there is CRC<br />决定是7位还是9位。</td>
</tr>
<tr class="odd">
<td>E</td>
<td>2</td>
<td>profile, the <a href="https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Audio_Object_Types">MPEG-4 Audio Object Type</a> minus 1 使用的编码规格↑。</td>
</tr>
<tr class="even">
<td>F</td>
<td>4</td>
<td><a href="https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Sampling_Frequencies">MPEG-4 Sampling Frequency Index</a> (15 is forbidden)<br />采样率</td>
</tr>
<tr class="odd">
<td>G</td>
<td>1</td>
<td>private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding</td>
</tr>
<tr class="even">
<td>H</td>
<td>3</td>
<td><a href="https://wiki.multimedia.cx/index.php/MPEG-4_Audio#Channel_Configurations">MPEG-4 Channel Configuration</a> (in the case of 0, the channel configuration is sent via an inband PCE)</td>
</tr>
<tr class="odd">
<td>I</td>
<td>1</td>
<td>originality, set to 0 when encoding, ignore when decoding</td>
</tr>
<tr class="even">
<td>J</td>
<td>1</td>
<td>home, set to 0 when encoding, ignore when decoding</td>
</tr>
<tr class="odd">
<td>K</td>
<td>1</td>
<td>copyrighted id bit, the next bit of a centrally registered copyright identifier, set to 0 when encoding, ignore when decoding</td>
</tr>
<tr class="even">
<td>L</td>
<td>1</td>
<td>copyright id start, signals that this frame's copyright id bit is the first bit of the copyright id, set to 0 when encoding, ignore when decoding</td>
</tr>
<tr class="odd">
<td>M</td>
<td>13</td>
<td>frame length, this value must include 7 or 9 bytes of header length: FrameLength = (ProtectionAbsent == 1 ? 7 : 9) + size(AACFrame)</td>
</tr>
<tr class="even">
<td>O</td>
<td>11</td>
<td>Buffer fullness</td>
</tr>
<tr class="odd">
<td>P</td>
<td>2</td>
<td>Number of AAC frames (RDBs) in ADTS frame <strong>minus 1</strong>, for maximum compatibility always use 1 AAC frame per ADTS frame</td>
</tr>
<tr class="even">
<td>Q</td>
<td>16</td>
<td>CRC if <em>protection absent</em> is 0</td>
</tr>
</tbody>
</table>
<p>这些值一般不是具体的数值，而是一个约定的编号，根据编号获得其对应具体数值。</p>
<p>可以根据<a href="https://www.p23.nl/projects/aac-header/">该工具</a>，输入对应的 ADTS 头，就可以解析出具体的含义。</p>
<p>可见，找到一个同步字<code>0xFFF</code>即找到一个ADTS的开头。</p>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>视频基础技术</title>
    <url>/posts/basic_video_technology/</url>
    <content><![CDATA[<p>视频是什么：</p>
<ul>
<li><p>视频由一组<strong>图像</strong>组成；</p>
<ul>
<li>视频的基本单元是图像。</li>
</ul></li>
<li><p>为了传输、占用更小的空间，常常被<strong>压缩</strong>存储与传输；</p></li>
<li><p>最终需要解压位图像在<strong>显示设备</strong>上展示。</p></li>
</ul>
<h2 id="码流计算">码流计算</h2>
<ul>
<li><p>分辨率。X轴像素个数×Y轴像素个数。</p></li>
<li><p>颜色分量（分量个数、分量大小）</p></li>
<li><p>帧率。每秒采集/播放图像的个数。</p></li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">未编码视频的RGB码流</span><br><span class="line">= 宽×高 // 分辨率</span><br><span class="line">×3 // 颜色分量，每个像素3字节大小</span><br><span class="line">×帧率</span><br><span class="line">(×8 // 按位计算)</span><br></pre></td></tr></table></figure>
<h2 id="编码解码转码">编码、解码、转码</h2>
<h3 id="编码encode">编码（encode）</h3>
<p>通过特定的压缩技术，将某个视频的视频流格式转换成另一种视频格式的视频流方式。</p>
<p>视频：</p>
<ul>
<li>YUV420/422 -&gt; H264</li>
<li>RGB888 -&gt; H264</li>
<li>YUV420 -&gt; H265</li>
</ul>
<p>音频：</p>
<ul>
<li>PCM -&gt; AAC</li>
<li>PCM -&gt; G726</li>
<li>PCM -&gt; G711</li>
</ul>
<h3 id="解码decode">解码（decode）</h3>
<p>通过特定的解压缩技术，将某个视频格式的视频流转换成另一种视频格式的视频流方式。</p>
<p>视频编码针对图片序列</p>
<p>视频：</p>
<ul>
<li>H264 -&gt; YUV420/422</li>
<li>H264 -&gt; RGB888</li>
<li>H265 -&gt; YUV420</li>
</ul>
<p>音频：</p>
<ul>
<li>AAC -&gt; PCM</li>
<li>G726 -&gt; PCM</li>
<li>G711 -&gt; PCM</li>
</ul>
<p>转码（transcode）：视频转码技术将视频信号从一种格式转换成另一种格式。</p>
<h3 id="转码">转码</h3>
<p>视频：</p>
<ul>
<li><p>改变分辨率（resolution）</p></li>
<li><p>改变帧率（frame rate）</p></li>
<li><p>改变比特率（bit rate）等编码参数</p></li>
</ul>
<p>音频：</p>
<ul>
<li><p>改变采样率（sample rate）</p></li>
<li><p>改变通道数（channels）</p></li>
<li><p>改变位宽（sample format）</p></li>
</ul>
<h2 id="封装解封装">封装、解封装</h2>
<p>封装（mux）：复用，按一定格式组织原音视频流</p>
<p>解封装（demux）：分解，解复用，按一定格式解析出原始音视频流</p>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>音频基础技术</title>
    <url>/posts/audio_basic_technology/</url>
    <content><![CDATA[<h2 id="人耳听觉范围">人耳听觉范围</h2>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615773041050-3cf23e1f-cd52-469b-b648-de0888eeed57.png" /></p>
<p>次声波，听觉范围：20Hz~20kHz，超声波</p>
<p>Hz，赫兹，一秒内振动的次数。</p>
<p>人发声范围：85Hz~1100Hz</p>
<h2 id="声音三要素">声音三要素</h2>
<ul>
<li>音调（频率）：音频的快慢。男生 &gt; 女生 &gt; 儿童</li>
<li>音量（强度）：振动的幅度</li>
<li>音色：谐波</li>
</ul>
<h3 id="音调频率">音调（频率）</h3>
<p>在一个波中，周期是完成一个周期所需的时间，频率是周期的倒数，以赫兹表示每秒周期数。从本质上说，完成一个周期所需的时间越短，频率越高；从视觉上看，峰值彼此靠近的波比峰值远的波具有更高的频率。虽然频率描述了波形循环重复率的数值度量，单音调更像是我们用来描述声音的主观术语。</p>
<h3 id="音量强度">音量（强度）</h3>
<p>强度是理解声音成分的另一个维度。声音强度描述了声音在一个区域内位移的声功率，以瓦特/平方米为单位。声音的功率是声音在某个单位时间内传递能量的速率，即强度本质上是声音置换的能量。</p>
<p>听者的持续时间、频率和年龄等混杂因素会影响声音的响度。</p>
<h3 id="音色">音色</h3>
<p>银色描述了赋予声音特征的多种属性。</p>
<h2 id="模拟信号数字化过程">模拟信号数字化过程</h2>
<p>模拟音频信号转化为数字音频信号：模拟音频信号是一个在时间上和幅度上都连续的信号，它的数字化过程如下所述。</p>
<p>模拟信号数字化的结果产物是PCM或WAV文件。</p>
<h3 id="采样">采样</h3>
<p>在时间轴上对信号进行数字化。</p>
<p>按照固定的时间间隔抽取模拟信号的值，这样，采样后就可以使一个时间连续的信息波变为在时间上取值数目有限的离散信号。</p>
<p>采样过程决定采样率（sample rate）。</p>
<h3 id="量化">量化</h3>
<p>在幅度轴上对信号进行数字化。用有限个幅度值近似还原原来连续变化的幅度值，把模拟信号的连续幅度变为有限数量的有一定间隔的离散值。</p>
<p>量化过程决定位深/采样大小，这是通过采样格式（sample format）体现的。</p>
<h3 id="编码">编码</h3>
<p>用二进制数表示每个采样的量化值（十进制数）。</p>
<p>原始音频数据：</p>
<ul>
<li><p>PCM，脉冲编码调制。</p>
<ul>
<li>纯粹的音频数据，不带音频格式。所以PCM音频流的码率计算方式如上所述。</li>
</ul></li>
<li><p>WAV，在PCM上添加音频信息的头。</p>
<ul>
<li>但除了存储PCM原始数据，它还可以存储压缩数据。</li>
<li>如下图可见，WAV存储的音频格式就是量化的信息：采样大小、采样率、声道数。</li>
</ul></li>
</ul>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615773052644-80b40cf3-456c-44e8-955c-f25777c8246f.png" /></p>
<p>上述的<strong>编码</strong>是模数转换过程中的编码成二进制文件的过程，注意与<strong>音频编码</strong>分开。一般所说的音频编码更多是指对音频的压缩。</p>
<h3 id="小结">小结</h3>
<p>音频由波形组成，包括不同频率和振幅的波的叠加。为了在数字媒体内表示这些波形，需要对波形进行采样，其<strong>采样率</strong>需要（至少）可以表示您要复制的最高频率的声音；同时还需要存储足够的<strong>位深</strong>，以表示声音样本中波形的适当振幅（响度和柔度）。</p>
<ul>
<li><p>位深/采样大小：一个采样用多少 bit 存放。能够表达的数值范围。使用8位、16位表达。</p>
<ul>
<li>位深影响给定音频样本的动态范围。位深越高，表示的振幅越精确。如果在同一音频样本内有很多响亮和柔和的声音，则需要更大的位深才能正确表示这些声音。</li>
<li>增高位深还会降低音频样本内的信噪比。CD 音乐音频使用 16 位的位深。DVD 音频使用 24 位的位深，而大多数电话设备使用 8 位的位深。（某些压缩技术可以补偿较小位深的不足，但往往会有损耗。）</li>
</ul></li>
<li><p>采样率：采样频率，即一秒内采样的个数。8k、16k、32k、44.1k、48k。越高越精细，高保真。</p>
<ul>
<li>声音以模拟波形的形式存在。数字音频片段以足够快的速率对模拟波的振幅进行采样，模仿波的固有频率，达到高度接近这种模拟波的效果。数字音频片段的采样率指定了（每秒）从音频的源素材中采集的样本数；采样率越高，数字音频如实表示高频的能力就越强。</li>
<li>根据 <a href="https://en.wikipedia.org/wiki/Nyquist–Shannon_sampling_theorem">Nyquist-Shannon 定理</a>，对于您要以数字形式采集的任何声波，您的采样率通常需要高于其最高频率的两倍。例如，要表示人类听觉范围 (20-20000 Hz) 内的音频，数字音频格式必须至少每秒采样 40000 次（CD 音频使用 44100 Hz 的采样率，部分原因也在于此）。</li>
</ul></li>
<li><p>声道：单声道、双声道、多声道。</p></li>
</ul>
<p>通过以上三者可以计算出原始音频的码率（一秒内的比特数）：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">未经压缩的码率 = 采样率 × 采样大小 × 声道数</span><br><span class="line">44100 * 16 * 2 / 1000 = 1378.125kbps</span><br><span class="line">一分钟的存储空间：</span><br><span class="line">1378.125 * 60 / 8 / 1024 = 10.09MB</span><br></pre></td></tr></table></figure>
<h3 id="直接读取处理pcm">直接读取处理PCM</h3>
<p>对于PCM，其每个采样的都是固定的，可以直接通过指针进行访问操作，如把PCM16LE双声道（采样大小16位，即每个声道的采样大小为2字节；LE：使用小端方式存储）分离声道：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line">FILE *fpcm = fopen(url, <span class="string">&quot;rb+&quot;</span>);</span><br><span class="line">FILE *fl = fopen(<span class="string">&quot;output_l.pcm&quot;</span>, <span class="string">&quot;wb+&quot;</span>);</span><br><span class="line">FILE *fr = fopen(<span class="string">&quot;output_r.pcm&quot;</span>, <span class="string">&quot;wb+&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 包含左右声道的采样</span></span><br><span class="line"><span class="type">unsigned</span> <span class="type">char</span> *sample = (<span class="type">unsigned</span> <span class="type">char</span> *)<span class="built_in">malloc</span>(<span class="number">4</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (!feof(fpcm)) &#123;</span><br><span class="line">    fread(sample, <span class="number">1</span>, <span class="number">4</span>, fpcm);</span><br><span class="line">    <span class="comment">// L</span></span><br><span class="line">    fwrite(sample, <span class="number">1</span>, <span class="number">2</span>, fl);</span><br><span class="line">    <span class="comment">// R</span></span><br><span class="line">    fwrite(sample + <span class="number">2</span>, <span class="number">1</span>, <span class="number">2</span>, fr);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">free</span>(sample);</span><br><span class="line">fclose(fpcm);</span><br><span class="line">fclose(fl);</span><br><span class="line">fclose(fr);</span><br></pre></td></tr></table></figure>
<p>类似的，将左声道音量降低一半：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line">FILE *fin = fopen(url, <span class="string">&quot;rb+&quot;</span>);</span><br><span class="line">FILE *fout = fopen(<span class="string">&quot;output_halfleft.pcm&quot;</span>, <span class="string">&quot;wb+&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="type">unsigned</span> <span class="type">char</span> *sample = (<span class="type">unsigned</span> <span class="type">char</span> *)<span class="built_in">malloc</span>(<span class="number">4</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (!feof(fin)) &#123;</span><br><span class="line">    <span class="type">short</span> *samplel = <span class="literal">NULL</span>;</span><br><span class="line">    fread(sample, <span class="number">1</span>, <span class="number">4</span>, fin);</span><br><span class="line"></span><br><span class="line">    samplel = (<span class="type">short</span> *)sample;</span><br><span class="line">    *samplel = *samplel / <span class="number">2</span>;</span><br><span class="line">    fwrite(sample, <span class="number">1</span>, <span class="number">4</span>, fout);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">free</span>(sample);</span><br><span class="line">fclose(fin);</span><br><span class="line">fclose(fout);</span><br></pre></td></tr></table></figure>
<p>而加速，而可以采用隔位采样的方式实现，但这样的效果音调也会上去。例如把速度提升一倍：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line">FILE *fin = fopen(url, <span class="string">&quot;rb+&quot;</span>);</span><br><span class="line">FILE *fout = fopen(<span class="string">&quot;output_doublespeed.pcm&quot;</span>, <span class="string">&quot;wb+&quot;</span>);</span><br><span class="line"><span class="type">int</span> cnt = <span class="number">0</span>;</span><br><span class="line"><span class="type">unsigned</span> <span class="type">char</span> *sample = (<span class="type">unsigned</span> <span class="type">char</span> *)<span class="built_in">malloc</span>(<span class="number">4</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (!feof(fin)) &#123;</span><br><span class="line">    fread(sample, <span class="number">1</span>, <span class="number">4</span>, fin);</span><br><span class="line">    <span class="keyword">if</span> (cnt % <span class="number">2</span> != <span class="number">0</span>) &#123; fwrite(sample, <span class="number">1</span>, <span class="number">4</span>, fout); &#125;</span><br><span class="line">    cnt++;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">free</span>(sample);</span><br><span class="line">fclose(fin);</span><br><span class="line">fclose(fout);</span><br></pre></td></tr></table></figure>
<p>还可以进行采样格式的转换，例如简单把PCM16LE转换为PCM8，由于是降采样，所以音质也会下降。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line">FILE *fin = fopen(url, <span class="string">&quot;rb+&quot;</span>);</span><br><span class="line">FILE *fout = fopen(<span class="string">&quot;output_8.pcm&quot;</span>, <span class="string">&quot;wb+&quot;</span>);</span><br><span class="line"><span class="type">int</span> cnt = <span class="number">0</span>;</span><br><span class="line"><span class="type">unsigned</span> <span class="type">char</span> *sample = (<span class="type">unsigned</span> <span class="type">char</span> *)<span class="built_in">malloc</span>(<span class="number">4</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (!feof(fin)) &#123;</span><br><span class="line">    <span class="type">short</span> *s16 = <span class="literal">NULL</span>;</span><br><span class="line">    <span class="type">char</span> s8 = <span class="number">0</span>;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">char</span> s8_u = <span class="number">0</span>;</span><br><span class="line">    fread(sample, <span class="number">1</span>, <span class="number">4</span>, fin);</span><br><span class="line">    <span class="comment">//(-32768-32767)</span></span><br><span class="line">    s16 = (<span class="type">short</span> *)sample;</span><br><span class="line">    s8 = (*s16) &gt;&gt; <span class="number">8</span>;</span><br><span class="line">    <span class="comment">//(0-255)</span></span><br><span class="line">    s8_u = s8 + <span class="number">128</span>;</span><br><span class="line">    <span class="comment">// L</span></span><br><span class="line">    fwrite(&amp;s8_u, <span class="number">1</span>, <span class="number">1</span>, fout);</span><br><span class="line"></span><br><span class="line">    s16 = (<span class="type">short</span> *)(sample + <span class="number">2</span>);</span><br><span class="line">    s8 = (*s16) &gt;&gt; <span class="number">8</span>;</span><br><span class="line">    s8_u = s8 + <span class="number">128</span>;</span><br><span class="line">    <span class="comment">// R</span></span><br><span class="line">    fwrite(&amp;s8_u, <span class="number">1</span>, <span class="number">1</span>, fout);</span><br><span class="line">    cnt++;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">free</span>(sample);</span><br><span class="line">fclose(fin);</span><br><span class="line">fclose(fout);</span><br></pre></td></tr></table></figure>
<h2 id="音频压缩编码">音频压缩编码</h2>
<p>与所有数据一样，音频数据通常会进行压缩，以便更易于存储和传输。音频编码中的压缩可能为无损或有损。无损压缩经过解包后可以将数字数据恢复为原始形式。有损压缩在压缩和解压缩过程中必然会移除某些信息，并且进行参数化，以便表明在多大容限范围内允许压缩技术移除数据。</p>
<p>音频压缩往往追求两个极端：压缩比尽可能大、压缩速度尽可能快。</p>
<h3 id="基本过程">基本过程</h3>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615773063221-2e1a0503-20ab-4284-bf61-961677beadf7.png" /></p>
<p>以上过程包含了有损压缩和无损压缩的过程。主要还是有损压缩。</p>
<p>时域转频域：去除被遮蔽掉的音频信号；心理声学模型：去除人耳听觉范围以外的音频信号。</p>
<h3 id="压缩方式">压缩方式</h3>
<ul>
<li>有损压缩（清除后无法恢复）：消除冗余信息
<ul>
<li>在保证信号在听觉方面不产生失真的前提下，对音频数据信号尽可能大的压缩。这些冗余信息：
<ul>
<li>人耳听觉范围外的音频信号</li>
<li>被遮蔽掉的音频信号
<ul>
<li>频域遮蔽</li>
<li>时域遮蔽</li>
</ul></li>
</ul></li>
</ul></li>
<li>无损压缩</li>
</ul>
<h3 id="有损压缩编码">有损压缩编码</h3>
<p>有损压缩则会在构建压缩数据期间清除或减少某些类型的信息，从而压缩音频数据。</p>
<h4 id="频域遮蔽效应">频域遮蔽效应</h4>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615773071671-80a1ca9f-4f41-4b11-990e-ef7710cf2120.png" /></p>
<p>音量高的会遮蔽附近音调的声音。</p>
<h4 id="时域遮蔽效应">时域遮蔽效应</h4>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615773078659-0b7012e9-17a0-41a8-bffa-7838d70324f9.png" /></p>
<p>音量高的会遮蔽附近时间的声音。</p>
<h3 id="无损压缩编码">无损压缩编码</h3>
<p>无损压缩对存储的数据进行复杂的重排，从而压缩数字音频数据，但不会导致原始数字样本的质量下降。如果采用无损压缩，在将数据解包为原始数字形式时，不会丢失任何信息。</p>
<p>那么，无损压缩技术为什么有时会具有优化参数？这些参数通常用来控制文件大小和解压缩时间。例如，FLAC 使用 0（最快）到 8（文件大小最小）的压缩级别参数。与较低级别的压缩相比，较高级别的 FLAC 压缩不会丢失任何信息。压缩算法只是需要在构建或解构原始数字音频时消耗更多的计算能量。</p>
<p>从技术上讲，<code>LINEAR16</code> 不是“无损压缩”，因为首先它并未涉及压缩。</p>
<ul>
<li><p>熵编码</p>
<ul>
<li>哈夫曼编码。使用很小的二进制数代表一个较长的字符，频率越高编码越小，频率越低编码越长。</li>
</ul></li>
<li><p>算术编码</p>
<ul>
<li>通过二进制的小数来进行编码。</li>
</ul></li>
<li><p>香农编码</p>
<ul>
<li>算术编码的改进。</li>
</ul></li>
</ul>
<h2 id="常见音频编码器">常见音频编码器</h2>
<p>常见的音频编码器包括 OPUS、AAC、Ogg、Speex、iLBC、AMR、G.711 等。</p>
<ul>
<li><p>OPUS</p>
<ul>
<li>延迟小，压缩比高。WebRTC 默认使用。</li>
</ul></li>
<li><p>AAC</p>
<ul>
<li>应用广泛，移动设备支持硬编解码。用于取代 mp3。</li>
</ul></li>
<li><p>Ogg</p>
<ul>
<li>收费，因此导致应用不广。</li>
</ul></li>
<li><p>Speex</p>
<ul>
<li>直接支持回音消除功能。</li>
</ul></li>
<li><p>G.711</p>
<ul>
<li>窄带音频，编码后数据非常小，但声音损坏较大。固话。</li>
</ul></li>
</ul>
<p>网上评测结果：OPUS &gt; AAC &gt; Ogg</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615773098988-be49753b-73f4-4d8a-a59e-8694a89767f0.png" /></p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615773110640-28723744-dc39-4a10-ac20-8e83a91f9c05.png" /></p>
<h2 id="音频重采样">音频重采样</h2>
<p>含义：将音频三元组（采样率、采样大小、通道数）的值转换成另外一组值。即只要改变这三元组的任意值都是进行重采样。</p>
<p>重采样的应用场景：</p>
<ul>
<li><p>从设备采集的音频数据与编码器要求的数据不一致。</p></li>
<li><p>输出设备要求的音频数据与要播放的音频数据不一致。</p></li>
<li><p>更方便计算，如回音消除要把双声道转换成单声道。</p></li>
</ul>
<h2 id="码控">码控</h2>
<ul>
<li>In CBR (constant bit rate) formats, such as linear PCM and IMA/ADPCM, all packets are the same size.</li>
<li>In VBR (variable bit rate) formats, such as AAC, Apple Lossless, and MP3, all packets have the same number of frames but the number of bits in each sample value can vary.</li>
<li>In VFR (variable frame rate) formats, packets have a varying number of frames. There are no commonly used formats of this type.</li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>图像基础技术</title>
    <url>/posts/image_basic_technology/</url>
    <content><![CDATA[<h2 id="图像与屏幕">图像与屏幕</h2>
<p>一个像素可以有两层含义：</p>
<ul>
<li>图像数据的一个点。</li>
<li>屏幕上的一个点。</li>
</ul>
<p>图像与屏幕的关系：</p>
<ul>
<li><p>图像是数据；</p></li>
<li><p>屏幕是显示设备；</p></li>
<li><p>图像数据经过驱动程序让屏幕显示图像。</p></li>
</ul>
<p>屏幕指标：</p>
<ul>
<li>PPI，pixel per inch</li>
<li>DPI，Dots per inch</li>
</ul>
<p>PPI 和 DPI 一般都相同。如果 PPI &gt; 300 就属于视网膜屏（人眼区分不出每个都像素点）。</p>
<p>每个像素具有位深，即使用多少位来保存一个像素。可以根据位深维度划分不同的像素格式：</p>
<ul>
<li>RGB888（24位）</li>
<li>RGBA（32位）</li>
</ul>
<p>存储模式：</p>
<ul>
<li>RGB565：使用16位表示一个像素。R：5位，G：6位，B：5位。</li>
<li>RGB888：使用24位来表示一个像素，每个分量都用8位表示。</li>
<li>ARGB8888：使用32位来表示一个像素，R、G、B都用8位表示，另外A(Alpha)表示透明度，也用8位表示。</li>
</ul>
<h2 id="图像的像素信息">图像的像素信息</h2>
<p>https://www.yuque.com/quandong/pqm3wg/nfzu3o</p>
<p>像素格式包含以下信息：</p>
<ol type="1">
<li><p>每个分量的位数，即在一个像素中每个独立颜色分量的位数。对于一个图像遮罩，这个值是源像素中遮罩bit的数目。例如，如果源图片是8-bit的遮罩，则指定每个分量是8位。</p></li>
<li><p>每个像素的位数，即一个源像素所占的总的位数。这个值必须至少是每个分量的位数乘以每个像素中分量的数目。</p></li>
<li><p>每行的字节数，即图像中水平行的字节数。</p></li>
</ol>
<h3 id="位图布局">位图布局</h3>
<p>https://www.yuque.com/quandong/pqm3wg/nfzu3o</p>
<p>以下的常量指定了alpha分量的位置及颜色分量是否做预处理：</p>
<ol type="1">
<li><p>kCGImageAlphaLast：alpha分量存储在每个像素中最不显著的位置，如RGBA。</p></li>
<li><p>kCGImageAlphaFirst：alpha分量存储在每个像素中最显著的位置，如ARGB。</p></li>
<li><p>kCGImageAlphaPremultipliedLast：alpha分量存储在每个像素中最不显著的位置，但颜色分量已经乘以了alpha值。</p></li>
<li><p>kCGImageAlphaPremultipliedFirst：alpha分量存储在每个像素中最显著的位置，同时颜色分量已经乘以了alpha值。</p></li>
<li><p>kCGImageAlphaNoneSkipLast：没有alpha分量。如果像素的总大小大于颜色空间中颜色分量数目所需要的空间，则最不显著位置的位将被忽略。</p></li>
<li><p>kCGImageAlphaNoneSkipFirst：没有alpha分量。如果像素的总大小大于颜色空间中颜色分量数目所需要的空间，则最显著位置的位将被忽略。</p></li>
<li><p>kCGImageAlphaNone：等于kCGImageAlphaNoneSkipLast。</p></li>
</ol>
<p>图11-2演示了一个像素在使用16-或32-bit整型像素格式的CMYK和RGB颜色空间中如何表示。32-bit整型像素格式中，每个分量占8位。16-bit整型像素格式中每个分量占5位。Quartz同样支持128-bit浮点像素格式，每个分量占32位。128-bit格式没有显示在下图中。</p>
<p><strong>Figure 11-2</strong> 32-bit and 16-bit pixel formats for CMYK and RGB color spaces in Quartz 2D<img src="https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/Art/colorformatrgba32.gif" alt="16- and 32-bit pixel formats for CMYK and RGB color spaces in Quartz 2D" /></p>
<h2 id="色彩空间">色彩空间</h2>
<p><a href="a904a9146e560770e4c6b36917573db0">https://www.cnblogs.com/leisure_chn/p/10290575.html</a></p>
<p>颜色是不同波长的光对人眼刺激产生的色彩感觉。色彩空间（Color Space）是颜色的数学表示，根据不同的表示方法分为不同的色彩模型。最常用的色彩模型有三类：RGB（用于计算机图形学）， YUV（用于视频系统）， CMYK（用于彩色印刷）。</p>
<p>描述光的常用物理量有四个：光通量、光强、照度、亮度。</p>
<p><strong>彩色三要素</strong></p>
<p>光的颜色取决于客观和主观两方面的因素。客观因素是光的功率波谱分布，它影响光源的颜色。主观因素是人眼视频特性，它影响人眼对色彩的感觉。 彩色三要素指亮度(Lightness)、色调(Hue)和饱和度(Saturation)，任一色彩都可以用这三个基本参量来表示：</p>
<p><strong><em>亮度</em></strong>：表示颜色明暗的程度，是光作用于人眼时引起的明亮程度的感觉。</p>
<p><strong><em>色调</em></strong>：是指颜色的类别，例如红色、蓝色、绿色指的就是色调。</p>
<p><strong><em>饱和度</em></strong>：指颜色的深浅程度，也称彩度。例如深绿、浅绿指的就是绿色这个色调的饱和度，饱和度越高，颜色越深。</p>
<h3 id="rgb色彩空间">RGB色彩空间</h3>
<p>人眼看到的物体颜色，是光源照射到物体，物体吸收(还有透射)部分颜色的光，然后从物体表面反射的光线进入人眼后人眼得到的色彩感觉。</p>
<p>人眼看到物体为黑色，是因为物体将光线完全吸收，没有光从物体表面反射出来(例如白天我们看一件黑衣服)；或者没有任何光线照射到物体(例如黑底我们看一张白纸)。</p>
<p>人眼看到物体为白色，是因为在白光源照射下，物体不吸收光线而将光线全部反射(例如白天我们看一张白纸)。</p>
<p>颜色与光源和物体的吸色特性密切相关，基于此，引出混色方法中的加色法和减色法。</p>
<p>加色法利用光源发射特性，将各分色的光谱成分相加得到混合颜色。RGB色彩空间采用加色法。当无任何光线照射时，R、G、B三种颜色分量都为0时，物体呈现黑色；当R、G、B三种颜色分量达到最大时，物体不吸收光线只反射的情况下，物体呈现白色。我们称黑色为最暗，白色为最亮，要达到最亮状态，需要三色分量最大程度混合，因此称为加色。</p>
<p>加色法用于自发光物体。RGB颜色空间主要应用于计算机显示器、电视机、舞台灯光等，都具有发光特性。彩色像素在显示器屏幕上不会重叠，但足够的距离时，光线从像素扩散到视网膜上会重叠，人眼会感觉到重叠后的颜色效果。</p>
<p>减色法是利用颜料吸色特性，每加一种颜色的颜料，会吸收掉对应的补色成分。CMYK色彩空间采用减色法。例如，我们在白纸(白光照射、不吸收、全反射)上涂颜料，黄色颜料能吸收蓝色(黄色的补色)，因此在白光照射下显示黄色，当黄(Y)、青(C)、品红(M)三色混在一起且颜色分量都为最大时，它们的补色成分被吸收掉，变成了黑色；当三色分量为0即什么也不涂时，白纸显现白色。要达到最大亮度，需要三色分量完全消失，因此称为减色。</p>
<p>印刷时，无法达到理想程度，C、M、Y最大程度混合后无法得到纯黑色，只能得到深灰色，因此在C、M、Y三色之外引入了K(黑色)。</p>
<p>减色法用于无法发光的物体。CMYK颜色空间主要应用于印刷、绘画、布料染色等。</p>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
  </entry>
  <entry>
    <title>YUV</title>
    <url>/posts/yuv/</url>
    <content><![CDATA[<h2 id="色彩空间">色彩空间</h2>
<p>像素格式描述了像素数据存储所用的格式，定义了像素在内存中的编码方式。RGB和YUV为两种经常使用的像素格式。</p>
<h3 id="rgb格式">RGB格式</h3>
<p>RGB图像具有三个通道R、G、B，分别对应红、绿、蓝三个分量，由三个分量的值决定颜色；通常，会给RGB图像加一个通道alpha，即透明度，于是共有四个分量共同控制颜色。</p>
<p><strong>RGB用于屏幕图像的展示。</strong></p>
<h3 id="yuv格式">YUV格式</h3>
<p>YUV颜色空间是PAL、NTSC、SCEAM三大视频标准使用的颜色空间，主要应用于视频系统。使用YUV色彩空间，后期出现的彩色电视系统和早期的黑白电视系统兼容，黑白电视机可以只处理彩色电信信号中的Y分量，而彩色电视机接收黑白电视信号并显示也没有任何问题。</p>
<p><span class="math inline">\(Y&#39;UV\)</span>、<span class="math inline">\(YUV\)</span>、<span class="math inline">\(YC_bC_r\)</span>、<span class="math inline">\(YP_bP_r\)</span>等都可以称为YUV，它们所指涉的范围，常有混淆或重叠的情况。从历史的演变来说，其中<span class="math inline">\(YUV\)</span>和<span class="math inline">\(Y&#39;UV\)</span>通常用来编码电视的模拟信号，而<span class="math inline">\(YC_bC_r\)</span>则是用来描述数字的视频信号，适合影片与图片压缩以及传输，例如MPEG、JPEG。 但在现今，YUV通常已经在计算机系统上广泛使用。</p>
<p><strong>YUV用于采集与编码。</strong></p>
<ul>
<li>YUV
<ul>
<li>Y：亮度/灰阶</li>
<li>U：色调/色度</li>
<li>V：饱和度/浓度</li>
</ul></li>
<li><span class="math inline">\(YP_bP_r\)</span>，模拟份量信号/接口
<ul>
<li>P：Paralle，并行</li>
<li>b下标：蓝</li>
<li>r下标：红</li>
</ul></li>
<li><span class="math inline">\(YC_bC_r\)</span>，数字分量信号/接口
<ul>
<li>C，Chroma：色度</li>
<li><span class="math inline">\(YC_bC_r\)</span>还可指色彩空间，<span class="math inline">\(YC_bC_r\)</span>色彩空间是YUV色彩空间的缩放和偏移版本。</li>
</ul></li>
</ul>
<p>YUV 在对照片或影片编码时，考虑到人类的感知能力，允许降低色度的带宽。YUV可以通过抛弃色差来进行带宽优化。比如yuv420格式图像相比RGB来说，要节省一半的字节大小，抛弃相邻的色差对于人眼来说，差别不大。</p>
<p>YUV颜色空间和RGB颜色空间可以根据公式相互转换。凡是渲染到屏幕上的东西，都要转换为RGB形式。</p>
<p>标清电视使用标准BT.601： <span class="math display">\[
{\left[\begin{array}{l}
Y&#39; \\
U \\
V
\end{array}\right]\\
= \left[\begin{array}{ccc}
0.299 &amp; 0.587 &amp; 0.114 \\
-0.14713 &amp; -0.28886 &amp; 0.436 \\
0.615 &amp; -0.51499 &amp; -0.10001
\end{array}\right]
\left[\begin{array}{l}
R \\
G \\
B
\end{array}\right]}
\]</span></p>
<p><span class="math display">\[
{\left[\begin{array}{l}
R \\
G \\
B
\end{array}\right]\\
= \left[\begin{array}{ccc}
1 &amp; 0 &amp; 1.13983 \\
1 &amp; -0.39465 &amp; -0.58060 \\
1 &amp; 2.03211 &amp; 0
\end{array}\right]\\
\left[\begin{array}{l}
Y&#39; \\
U \\
V
\end{array}\right]}
\]</span></p>
<p>高清电视使用标准BT.709： <span class="math display">\[
{\left[\begin{array}{l}
Y^{\prime} \\
U \\
V
\end{array}\right]=\left[\begin{array}{ccc}
0.2126 &amp; 0.7152 &amp; 0.0722 \\
-0.09991 &amp; -0.33609 &amp; 0.436 \\
0.615 &amp; -0.55861 &amp; -0.05639
\end{array}\right]\left[\begin{array}{l}
R \\
G \\
B
\end{array}\right]}
\]</span></p>
<p><span class="math display">\[
{\left[\begin{array}{l}
R \\
G \\
B
\end{array}\right]=\left[\begin{array}{ccc}
1 &amp; 0 &amp; 1.28033 \\
1 &amp; -0.21482 &amp; -0.38059 \\
1 &amp; 2.12798 &amp; 0
\end{array}\right]\left[\begin{array}{l}
Y^{\prime} \\
U \\
V
\end{array}\right]}
\]</span></p>
<p>对于iOS采集的CMSampleBufferRef，调用<code>CVBufferGetAttachment</code>获取YCbCrMatrix，决定使用BT.601还是BT.709。</p>
<h2 id="采样方式">采样方式</h2>
<p>YUV相比于RGB格式最大的好处是可以做到在保持图像质量降低不明显的前提下，减小文件大小。YUV格式之所以能够做到，是因为进行了采样操作。</p>
<p>YUV图像存储模式与采样方式密切相关。主流的采样方式有三种，YUV4:4:4<strong>(YUV444)</strong>、YUV4:2:2<strong>(YUV422)</strong>、YUV4:2:0<strong>(YUV420)</strong>（所有设备都支持）。这些采样方式，不压缩Y分量，对UV分量的压缩程度不同，这是由人眼的特性决定的，人眼对亮度Y更敏感，对色度UV没有那么敏感，压缩UV分量可以降低数据量，但并不会人眼主观感觉造成太大影响。</p>
<p>YUV后面接的数字就是<span class="math inline">\(Y\)</span>、<span class="math inline">\(C_b\)</span>、<span class="math inline">\(C_r\)</span>三个分量的比例。</p>
<p>若以以黑点表示采样该像素点的Y分量，以空心圆圈表示采用该像素点的UV分量，则这三种采样方式如下：</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772067597-256abbba-34ae-452b-8070-2b4685f65386.png" /></p>
<p>即：</p>
<ul>
<li><p>YUV4:4:4采样，每一个Y对应一组UV分量。</p></li>
<li><p>YUV4:2:2采样，每两个Y共用一组UV分量。</p></li>
<li><p>YUV4:2:0采样，每四个Y共用一组UV分量。</p></li>
</ul>
<h3 id="yuv444">YUV4:4:4</h3>
<p>4:4:4，表示完全取样。每个 Y 对应一组 UV 分量。</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772067684-94dd4489-e95f-4483-9573-7fe5c4139ecb.png" /></p>
<p>相邻的4个像素里有4个Y、4个U、4个V。每1个Y使用1组UV分量。如下(每个[]为一个像素点)：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[ Y U V ]  [ Y U V ]  [ Y U V ]  [ Y U V ]</span><br><span class="line">[ Y U V ]  [ Y U V ]  [ Y U V ]  [ Y U V ]</span><br><span class="line">[ Y U V ]  [ Y U V ]  [ Y U V ]  [ Y U V ]</span><br><span class="line">[ Y U V ]  [ Y U V ]  [ Y U V ]  [ Y U V ]</span><br></pre></td></tr></table></figure>
<p>在这种采样方式下，一个像素点包含的完整的信息。</p>
<p><strong>每个像素大小是3字节（24位），与RGB一致。</strong></p>
<h3 id="yuv422">YUV4:2:2</h3>
<p>4:2:2，表示 2:1 水平取样，垂直完全采样。每两个 Y 共用一组 UV 分量。</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772067808-3f48509f-c288-4451-8aa1-43af153ea027.png" /></p>
<p>相邻的4个像素里有4个Y、2个U、2个V。每2个Y共用1组UV分量。平均算来，一个像素占用的数据宽度为16b，其中Y占8b，U占4b，V占4b。后面存储模式命名中的数字16指的就是16b。<strong>平均每个像素大小是2字节，比RGB少⅓。</strong></p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[ Y U ]  [ Y V ]  [ Y U ]  [ Y V ]</span><br><span class="line">[ Y V ]  [ Y U ]  [ Y V ]  [ Y U ]</span><br><span class="line">[ Y U ]  [ Y V ]  [ Y U ]  [ Y V ]</span><br><span class="line">[ Y V ]  [ Y U ]  [ Y V ]  [ Y U ]</span><br></pre></td></tr></table></figure>
<p>在这种采样方式下，还原出一个像素点，需要相邻的两个像素点数据，如下：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[ Y U ]  [ Y V ]</span><br></pre></td></tr></table></figure>
<h3 id="yuv420">YUV4:2:0</h3>
<p>4:2:0，表示 2:1 水平取样，垂直 2:1 采样。</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772067907-bf5c524e-747c-41a4-be38-cbc8a40ed36f.png" /></p>
<p>4:1:0并不意味着只有<span class="math inline">\(Y\)</span>、<span class="math inline">\(C_b\)</span>两个分量，而没有<span class="math inline">\(C_r\)</span>分量。实际指的是对每行扫描线来说，只有一中色度分量，相邻的扫描线存储不同的色度分量。</p>
<p>相邻的4个像素里有4个Y、2个U、0个V，或4个Y、2个V，0个U。每4个Y共用1组UV分量。平均算来，一个像素占用的数据宽度为12bit，其中Y占8bit，U占2bit，V占2bit。后面存储模式命名中的数字12指的就是12b。<strong>平均每个像素是12b，比RGB少½。</strong></p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[ Y U ]  [ Y ]  [ Y U ]  [ Y ]</span><br><span class="line">[ Y V ]  [ Y ]  [ Y V ]  [ Y ]</span><br><span class="line">[ Y U ]  [ Y ]  [ Y U ]  [ Y ]</span><br><span class="line">[ Y V ]  [ Y ]  [ Y V ]  [ Y ]</span><br></pre></td></tr></table></figure>
<p>在这种采样方式下，还原出一个像素点，需要相邻的四个像素点数据，如下：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[ Y U ]  [ Y ]</span><br><span class="line">[ Y V ]  [ Y ]</span><br></pre></td></tr></table></figure>
<p><span class="math display">\[
YUV4:2:0 数据量 = Y × 1.5 = RGB ÷ 2
\]</span></p>
<h2 id="存储格式">存储格式</h2>
<p>在同一采样模式下，根据分量元素排列顺序的不同，又分为不同的存储模式。</p>
<ul>
<li><p><strong>packed</strong>，紧缩格式（packed formats）：将 Y、U、V 值存储成一个 Macro Pixels 数组，和 RGB 的存放方式类似。</p>
<ul>
<li><p>内存中排列形式类似：YVYUYVYUYVYUYVYU...。</p></li>
<li><p>在具体的存储模式命名中，packed格式不带后缀P。</p></li>
<li><p>适合YUV 4:4:4</p></li>
</ul></li>
<li><p><strong>planar</strong>，平面格式（planar formats）：将 Y、U、V 3个分量分别存放在不同的矩阵中（3个字节数组）。</p>
<ul>
<li><p>内存中排列形式类似：YYYYYY...，UUUUUU...，VVVVVV...。</p></li>
<li><p>在具体的存储模式命名中，planar格式带后缀P。</p></li>
<li><p>适合I420（YUV420p）、YV12（YUV420p）</p></li>
</ul></li>
<li><p><strong>semi-planar</strong>，将Y、U、V三个分量放在2个矩阵(平面)中（2个字节数组）。Y占用一个平面，UV共用一个平面。</p></li>
<li><p>内存中排列形式类似：YYYYYY...，UVUVUV...。</p>
<ul>
<li>在具体的存储模式命名中，semi-planar格式带后缀SP。</li>
</ul></li>
<li><p>适合NV12（YUV420sp）、NV21（YUV420sp）</p></li>
</ul>
<h2 id="像素格式">像素格式</h2>
<h3 id="yuv422-1">YUV422</h3>
<p>内存分布：YUYV、YVYU、UYVY、VYUY</p>
<p>这四种格式每一种又可以分为2类（packed和planar），以<strong>YUYV</strong>为例，一个6*4的图像的存储方式如下：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Y Y Y Y Y Y                   </span><br><span class="line">Y Y Y Y Y Y                  </span><br><span class="line">Y Y Y Y Y Y                   </span><br><span class="line">Y Y Y Y Y Y                    </span><br><span class="line">U U U U U U                  Y U Y V Y U Y V Y U Y V</span><br><span class="line">U U U U U U                  Y U Y V Y U Y V Y U Y V</span><br><span class="line">V V V V V V                  Y U Y V Y U Y V Y U Y V</span><br><span class="line">V V V V V V                  Y U Y V Y U Y V Y U Y V</span><br><span class="line">- Planar -                          - Packed -</span><br></pre></td></tr></table></figure>
<h3 id="yuv420-1">YUV420</h3>
<ul>
<li>YUV420p：I420、YV12，planar，分别存储在三个字节数组中。</li>
<li>YUV420sp：NV12、NV21，semi-planar，Y存储在一个数组中，UV存储在一个数组中。</li>
</ul>
<p>同样，对于一个6*4的图像，这四种像素格式的存储方式如下：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y</span><br><span class="line">Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y</span><br><span class="line">Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y</span><br><span class="line">Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y</span><br><span class="line">U U U U U U      V V V V V V      U V U V U V      V U V U V U</span><br><span class="line">V V V V V V      U U U U U U      U V U V U V      V U V U V U</span><br><span class="line"> - I420 -         - YV12 -         - NV12 -         - NV21 -</span><br></pre></td></tr></table></figure>
<h2 id="占用字节数">占用字节数</h2>
<p>YUV420图像占用字节数为 ：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">size = width * height + (width * height) / 4 + (width * height) / 4 = width * height * 1.5 // 刚好是 RGB 的一半</span><br></pre></td></tr></table></figure>
<p>RGB格式的图像占用字节数为:</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">size = width * height * 3</span><br></pre></td></tr></table></figure>
<p>RGBA格式的图像占用字节数为:</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">size = width * height * 4</span><br></pre></td></tr></table></figure>
<h2 id="yuv数据访问">YUV数据访问</h2>
<p>对于一个YUV格式存放的文件，可以直接读取并分离成Y、U、V各个分量的文件：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 读取YUV420P的文件，每个Y、U、V值都是用一个字节存储，所以使用刚好是一字节大小的char表示</span></span><br><span class="line"><span class="type">int</span> yuv_size = w * h * <span class="number">3</span> / <span class="number">2</span>;</span><br><span class="line"><span class="type">unsigned</span> <span class="type">char</span> *pic = (<span class="type">unsigned</span> <span class="type">char</span> *)<span class="built_in">malloc</span>(yuv_size);</span><br><span class="line">fread(pic, <span class="number">1</span>, yuv_size, fyuv);</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> written_byte, writting_byte = <span class="number">0</span>;</span><br><span class="line"><span class="comment">// Y</span></span><br><span class="line">writting_byte = w * h;</span><br><span class="line">fwrite(pic + written_byte, <span class="number">1</span>, writting_byte, fy);</span><br><span class="line">written_byte += writting_byte;</span><br><span class="line"><span class="comment">// U</span></span><br><span class="line">writting_byte = w * h / <span class="number">4</span>;</span><br><span class="line">fwrite(pic + written_byte, <span class="number">1</span>, writting_byte, fu);</span><br><span class="line">written_byte += writting_byte;</span><br><span class="line"><span class="comment">// V</span></span><br><span class="line">writting_byte = w * h / <span class="number">4</span>;</span><br><span class="line">fwrite(pic + written_byte, <span class="number">1</span>, writting_byte, fv);</span><br><span class="line">written_byte += writting_byte;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 对于YUV444P的分离也是类似，只是：</span></span><br><span class="line"><span class="comment">// yuv_size = w * h * 3</span></span><br><span class="line"><span class="comment">// writting_byte = w * h</span></span><br></pre></td></tr></table></figure>
<p>对于YUV格式数据变成灰度图也很简单，只需要把U、V值都置空即可：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 读取YUV420P的文件</span></span><br><span class="line"><span class="type">int</span> yuv_size = w * h * <span class="number">3</span> / <span class="number">2</span>;</span><br><span class="line"><span class="type">unsigned</span> <span class="type">char</span> *pic = (<span class="type">unsigned</span> <span class="type">char</span> *)<span class="built_in">malloc</span>(yuv_size);</span><br><span class="line">fread(pic, <span class="number">1</span>, yuv_size, fyuv);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Gray</span></span><br><span class="line"><span class="built_in">memset</span>(pic + w * h, <span class="number">0</span>, w * h / <span class="number">2</span>);</span><br><span class="line">fwrite(pic, <span class="number">1</span>, yuv_size, fyuv);</span><br></pre></td></tr></table></figure>
<h2 id="ios相机支持输出图片格式">iOS相机支持输出图片格式</h2>
<ul>
<li><p>420v</p>
<ul>
<li><p><code>kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange</code></p></li>
<li><p>表示输出的视频格式为NV12（YUV420sp）</p></li>
<li><p>范围：(luma = [16,235], chroma = [16,240])</p></li>
</ul></li>
<li><p>420f</p>
<ul>
<li><p><code>kCVPixelFormatType_420YpCbCr8BiPlanarFullRange</code></p></li>
<li><p>表示输出的视频格式为NV12（YUV420sp）</p></li>
<li><p>范围：(luma = [0,255], chroma = [1,255])</p></li>
</ul></li>
<li><p>BGRA</p>
<ul>
<li><code>kCVPixelFormatType_32BGRA</code></li>
<li>输出的是BGRA的格式</li>
</ul></li>
</ul>
<p>Android从摄像头采集的预览数据一般都是NV21，iOS一般采集的数据都是NV12。</p>
<h2 id="参考资料">参考资料</h2>
<ul>
<li><p>https://juejin.im/post/5a572730f265da3e2c3803ad</p></li>
<li><p><a href="https://en.wikipedia.org/wiki/YUV">YUV - Wikipedia</a></p></li>
<li><p><a href="https://www.fourcc.org/yuv.php">YUV pixel formats</a></p></li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>视频压缩技术</title>
    <url>/posts/video_compression_technology/</url>
    <content><![CDATA[<p>与音频压缩编码不同，视频的压缩编码基本都是有损压缩。H.264是多种视频压缩技术的集大成者，其使用技术的还要追溯到H.261。</p>
<h3 id="宏块">宏块</h3>
<p>宏块是视频压缩操作的基本单元。无论是帧内压缩还是帧间压缩，它们都是以宏块为单位。</p>
<p>宏块是按像素进行划分的。宏块划分得小，压缩的控制力就大一些，处理速度也会降下来。</p>
<p>宏块还可以划分为子块。</p>
<p>宏块划分尺寸：</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772066040-cda01df7-9404-4968-a184-51c3b57d18a4.png" /></p>
<h3 id="帧内预测">帧内预测</h3>
<p>理论基础：</p>
<ul>
<li>相邻像素差别不大，可以进行<strong>宏块</strong>预测。宏块与宏块之间进行比较，而不是像素对比。</li>
<li>人对亮度的敏感度超过色度。YUV 很容易将亮度与色度分离。组合上面的点，可以将亮度与色度区分处理。以下的帧内预测，亮度与色度是区分处理的。</li>
</ul>
<p>H.264将单个宏块内的像素颜色变化规律规范成了公式，编码时只要写此处应用哪个公式就行了。</p>
<h5 id="选择帧内预测模式">1. 选择帧内预测模式</h5>
<p>帧内预测模式有9种，通过与目标宏块对比选择最适合的模式，将预测的宏块变成预测模式编号。</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772066130-90d3dc38-05c1-4288-affd-bc149ce06f3b.png" /></p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772066253-12d77bff-0f01-4394-b508-18ef64f42acc.png" /></p>
<h5 id="叠加残差">2. 叠加残差</h5>
<p>帧内预测残差值，通过预测出来的与原始图像进行对比得出。</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772066362-562246ee-ae43-4ba4-bfde-8e1093499588.png" /></p>
<p>预测模式信息+残差值。预测得出的图像颜色是基于宏块的，平滑度有限，叠加残差值来磨平这些色差。</p>
<h3 id="帧间预测">帧间预测</h3>
<p>对每帧图像压缩，压缩比始终有限，因此提出了对一组图片做消除冗余。</p>
<figure>
<img src="https://blog.xinoassassin.me/images/mc-vector.jpg" alt="img" /><figcaption aria-hidden="true">img</figcaption>
</figure>
<ol type="1">
<li>把强相关的帧进行分组，形成GOP（<strong>G</strong>roup <strong>o</strong>f <strong>P</strong>ictures）。</li>
<li>进行运动估计（宏块查找/匹配 -&gt; 运动矢量）。这是一个过程，通过宏块匹配，得出运动矢量，最终存储的是初始状态和运动矢量。</li>
<li>进行运动补偿（解码），补偿的是残差值。</li>
</ol>
<p>引入基于运动补偿帧间预测算法后，视频中的帧就分为两类：</p>
<ul>
<li>关键帧：完整的静态图像，可以被直接解码。</li>
<li>预测帧/参考帧：通过运动补偿算法在关键帧之上计算得到。根据帧的依赖方向还可以分为：
<ul>
<li>预测编码图像帧，P帧（Predictive-coded picture）：只能参考前面的关键帧和P帧。</li>
<li>双向预测编码图像帧，B帧（Bidirectionally predicted picture）：能参考前后的关键帧和P帧，但不能参考前后的B帧。</li>
</ul></li>
</ul>
<p>进入帧间预测编码的常见问题：</p>
<ul>
<li>花屏。GOP分组有帧（P、B）丢失，会造成解码端端图像发生错误，出现马赛克。</li>
<li>卡顿。其实是为了避免花屏问题的发生而导致的新的问题，当发现有帧丢失的时候，就丢弃GOP内所有的帧，直到下一个IDR帧重新刷新图像。I帧是按照周期来的，需要一个较长的时间周期才到达下一个I帧。如果在下一个I帧之前不显示后面的图像，视频久静止不动了，出现卡顿。</li>
</ul>
<h3 id="dct">DCT</h3>
<p>DCT，Discrete Cosine Transform，离散余弦变换。帧内编码算法。在图像压缩算法上，H.261使用了DCT算法，把图像从空间域转换到频率域，然后做量化，减少人眼不敏感的高频信息，保留绝大部分低频信息，从而减少图像体积。然后再用高效的数据编码方式把处理后的数据进一步压缩。</p>
<p>DCT将图像分成由不同频率组成的小块，然后进行量化。在量化过程中，舍弃高频分量，剩下的低频分量被保存下来用于后面的图像重建。</p>
<p>DCT具备<strong>去相关性</strong>和<strong>能量集中</strong>的特性。DCT本身并不会压缩数据，它为随后的量化之类的操作，提供了一个良好的基础。</p>
<p>DCT在后来的JPEG编码中起主要作用。</p>
<h3 id="cabac">CABAC</h3>
<p>在编码的最后阶段，即可以去除的冗余信息都去除后，对数据进行无损压缩。H.264除了支持在H.261中就存在的VLC编码外，新增加了两种无损数据压缩编码，一种是VLC的升级版——CAVLC，另一种是复杂程度更高的CABAC（前文参考之适应性二元算术编码，<strong>C</strong>ontext-based <strong>A</strong>daptive <strong>B</strong>inary <strong>A</strong>rithmetic <strong>C</strong>oding）。</p>
<figure>
<img src="https://blog.xinoassassin.me/images/CABAC.jpg" alt="img" /><figcaption aria-hidden="true">img</figcaption>
</figure>
<p>CABAC也是一种熵编码，主要原理也是用长编码替换掉出现频率少的数据，而用短编码替换出现频率高的数据，但它引入了更多统计学优化，并且具有动态适应能力。虽然在解码时需要更多计算，但它能够比CAVLC节省更多的数据量，通常能有10%。</p>
<h3 id="编码树单元">编码树单元</h3>
<p>HEVC引入了新的编码树单元（<strong>C</strong>oding <strong>T</strong>ree <strong>U</strong>nits）概念，取代掉了存在于视频编码中多年的宏块概念，它的单块面积大了许多，达到了64x64，但仍然保留了可变大小和可分割特性，最小单元为16x16。单个编码树中包含了小的编码单元，它们可以由四分树形式呈现，并很快地可以确定下其中的单元是否可被再分割，内部编码单元最小可以被分割为8x8大小，精细程度仍然是非常高的。</p>
<p>单个编码单元也可以继续被切割、分类，可以成为预测单元（Prediction Units），后者可以指示该单元的预测形式，是画面内预测还是画面间预测或者甚至是根本没有变化、可以被跳过的单元；也可以成为转换单元（Transform Units），它可以做DCT转换或是量化。</p>
<p>编码树单元的引入让HEVC既可以用大面积单元来提高编码效率，也可在需要的时候细化，保留更精细的细节。所谓该粗略的地方就粗略，该精细的地方就精细，HEVC在它的帮助下让码流的效率更高。</p>
<h2 id="趋势">趋势</h2>
<p>H.261奠定宏块和帧间预测的基础，H.264/AVC是多种压缩技术的集大成者。HEVC主要是针对高清及超清分辨率视频而开发的，相比起前代AVC，它在低码率时拥有更好的画质表现，同时在面对高分辨率视频时，也能提供超高的压缩比，帮助4K视频塞入蓝光光盘。</p>
<h2 id="h.264编码流程">H.264编码流程</h2>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772066655-5b9dfeca-bd92-4022-9ef7-fef968120e26.png" /></p>
<figure>
<img src="https://pic3.zhimg.com/80/v2-6ab263372cec8b4a544d858d3a7fdc32_1440w.jpg" alt="img" /><figcaption aria-hidden="true">img</figcaption>
</figure>
<figure>
<img src="https://pic3.zhimg.com/80/v2-048c202f6368d7f2cc5d3d2a59f7fd2a_1440w.jpg" alt="img" /><figcaption aria-hidden="true">img</figcaption>
</figure>
<ol type="1">
<li>帧类型分析 -&gt; I帧、P帧、B帧、GOP</li>
<li>划分宏块及其子块</li>
<li>I帧进行帧内预测，最终存储预测模式、残差（去除空间冗余）
<ol type="1">
<li>选择帧内预测模式</li>
<li>叠加残差</li>
</ol></li>
<li>P/B帧进行帧间预测，最终存储帧间预测模式标志位、运动矢量、残差（去除时间冗余）
<ol type="1">
<li>运动估计：宏块查找、匹配 -&gt; 运动矢量</li>
<li>运动补偿：叠加残差</li>
</ol></li>
<li>DCT变换、量化，丢弃高频信息（去除空间冗余）</li>
<li>滤波，通过滤波修正并提升主观质量</li>
<li>熵编码（如：CAVLC、CABAC）压缩最终数据（去除编码冗余）</li>
</ol>
<h2 id="参考">参考</h2>
<ul>
<li><a href="b8de6d0c93705a606b91010096b22446">视频编解码基础概念 - 叶余 - 博客园</a></li>
<li><a href="https://blog.xinoassassin.me/2020/03/Video-Codecs/">数字视频编码的发展历程</a></li>
<li><a href="https://mp.weixin.qq.com/s?__biz=MzU1NTEzOTM5Mw==&amp;mid=2247512538&amp;idx=1&amp;sn=57f46386002cf5554681f8ef9f61a3e0&amp;chksm=fbda19f4ccad90e219bf224db522e9999086dff886bae09562e1aeba4450d4ba0247a73c3138&amp;scene=21#wechat_redirect">视频压缩标准简史：从1929到2020</a></li>
<li><a href="http://zhaoxuhui.top/blog/2018/05/26/DCTforImageDenoising.html">DCT变换与图像压缩、去燥</a></li>
<li><a href="https://en.wikipedia.org/wiki/Advanced_Video_Coding">Advanced Video Coding - Wikipedia</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/158392753">H.264编解码原理浅析 - 知乎</a></li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>H.264/MPEG-4 AVC</title>
    <url>/posts/h264/</url>
    <content><![CDATA[<p>H.264（MPEG-4 Part 10， Advanced Video Coding），缩写为 MPEG-4 AVC。是一种<strong>面向块</strong>，<strong>基于运动补偿</strong>的视频编码标准。</p>
<p>压缩比：对于YUV420的裸流，压缩比约为1/100。</p>
<p>新特性：</p>
<ul>
<li>多参考帧的运动补偿。可以带来一定的码率降低或者质量提高。</li>
<li>变块尺寸运动补偿。可使用最大 16x16 到最小 4x4 的块来进行运动估计与运动补偿，能够对运动区域进行更精确的分割。</li>
<li>使用六阶数字滤波器来产生二分之一像素的亮度分量预测值，可以较少混叠（Aliasing）并得到更锐化的图像。</li>
<li>灵活的隔行扫描视频编码。</li>
<li>1/4 像素精度的运动补偿，能够提供更高精度的运动块预测。</li>
<li>加权的运动预测。在一些特殊场合，如淡出、淡入等情况提供相当大的编码增益。</li>
<li>等等。。。https://zh.wikipedia.org/wiki/H.264/MPEG-4_AVC</li>
</ul>
<h2 id="编码原理">编码原理</h2>
<p>对一段变化不大的图像画面，先编码出一个完整的图像帧A，随后的B帧就不编码全部图像，只写入与A帧的差别，然后继续以B的方式编码C帧，这样循环下去得到一段序列。当某幅图像与之前的图像变化很大时，无法参考前面的帧来生成，就结束上一个序列，开始下一段序列的生成。</p>
<p>H.264 协议里定义了三种帧：</p>
<ul>
<li><p>完整编码的 I 帧；</p></li>
<li><p>参考之前的 I 帧只生成的包含差异部分编码的 P 帧；</p></li>
<li><p>参考前后的帧编码的 B 帧；</p></li>
</ul>
<p>H.264 采用的核心算法是帧内压缩和帧间压缩，帧内压缩是生成 I 帧的算法，帧间压缩是生成 B 帧和 P 帧的算法。</p>
<h2 id="gop序列">GOP序列</h2>
<p>提出的意义：按照相关性进行分组便于进行帧间压缩。这样分组的一组图像差别较少，去除的冗余/重复数据就多，压缩比就高。</p>
<p>H.264 中以图像序列为单位进行组织，一个序列是一段图像编码后的数据流，从 I 帧开始，到下一个 I 帧结束。</p>
<p>序列的第一个图像叫 IDR 图像（立即刷新图像），IDR 图像都是 I 帧图像。当遇到 IDR 图像时，立即将参考帧队列清空，将已解码的数据全部输出或抛弃，重新查找参数集，开始一个新的序列。这样在前一个序列出现重大错误时，可以获得重新同步的机会。</p>
<p>一个序列就是一段内容差异不太大的图像编码后生成的一串数据流。当运动变化较少时，一个序列可以很长，所以就可以编一个 I 帧，让后一直 P 帧、B 帧了。当运动变化较多时，一个序列就可能比较短了。</p>
<p>GOP（Group Of Pictures，图像组）是一组连续的图像，由一个I帧和多个B/P帧组成，是编解码器存取的基本单位。GOP 中帧与帧之间的差别较小。</p>
<p>GOP结构常用的两个参数M和N，M指定GOP中首个P帧和I帧之间的距离，N指定一个GOP的大小。例如M=1，N=15，GOP结构为：</p>
<p><span class="math display">\[
IPBBPBBPBBPBBPB
\]</span></p>
<p>GOP分两种：闭合式GOP和开放式GOP：</p>
<ul>
<li>闭合式 GOP：闭合式GOP只需要参考本GOP内的图像即可，不需参考前后GOP的数据。这种模式决定了，闭合式GOP的显示顺序总是以I帧开始以P帧结束。</li>
<li>开发式 GOP：开放式GOP中的B帧解码时可能要用到其前一个GOP或后一个GOP的某些帧。码流里面包含B帧的时候才会出现开放式GOP。</li>
</ul>
<p>如图特征，开发式GOP末尾是B帧，闭合式GOP末尾是P帧。但不管如果要称为GOP，首帧必须是I帧。</p>
<p>开放式GOP和闭合式GOP中I帧、P帧、B帧的依赖关系如下图所示：</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772065108-8aeea8d4-e483-4f35-90c9-27b9fc8816eb.jpeg" /></p>
<p>GOP 指两个 I 帧之间的距离。参考周期（Reference）指两个 P 帧之间中距离。I、B、P 帧所占字节数大小：I &gt; P &gt; B。</p>
<p>所以在码率不变的前提下，GOP 值越大，P、B 帧的数量会越多，平均每个 I、P、B 帧所占的字节数就越多，也就容易获得较好的图像质量。通过提高 GOP 值来提高图像质量是有限度的，在遇到场景切换的情况下，编码器会自动强制插入一个 I 帧，此时实际的 GOP 值被缩短。另一方面，在一个 GOP 中，P 帧由 I 帧预测得到的，当 I 帧的图像质量比较差时，会影响一个 GOP 中后续 P、B 帧的图像质量。直到下一个 GOP 开始才有可能得到恢复，所以 GOP 值也不宜设置过大。</p>
<p>同时，由于 P、B 帧的复杂度大于 I 帧，所以过多的 P、B 帧会影响编码效率，使得编码效率降低。另外，过长的 GOP 还会影响 seek 操作的响应速度，因为 P、B 帧是由前面的 I 或 P 帧预测得到的，所以 seek 操作需要直接定位、解码某一个 P 或 B 帧时，需要先解码得到本 GOP 内的 I 帧及之前的 N 个预测帧才可以，GOP 值越长，需要解码的预测帧就越多，seek 响应的时间也就越长。</p>
<h2 id="帧">帧</h2>
<h3 id="i-帧">I 帧</h3>
<p>Intra-coded picture，帧内编码图像帧，常称为关键帧。</p>
<p>不参考其他图像帧，只利用本帧信息进行编码。包含一幅完整的图像信息，属于帧内编码图像，不含运动矢量，在解码时不需要参考其他帧图像。因此在I帧图像处可以切换频道，而不会导致图像丢失或无法解码。I帧图像用于阻止误差的累积和扩散。在闭合式GOP中，每个GOP的第一个帧一定是I帧（IDR帧），且当前GOP的数据不会参考前后GOP的数据。</p>
<h4 id="idr-帧">IDR 帧</h4>
<p>Instantaneous Decoding Refresh picture，即时解码刷新帧，是一种特殊的I帧。当解码器解码到IDR帧时，会将DPB（Decoded Picture Buffer，指前后向参考帧列表）清空，将已解码的数据全部输出或抛弃，然后开始一次全新的解码序列。IDR帧之后的图像不会参考IDR帧<strong>之前</strong>的图像。</p>
<p>在编码解码中为了方便，将GOP中首个I帧要和其他I帧区别开，把第一个I帧叫IDR，这样方便控制编码和解码流程，所以IDR帧一定是I帧，但I帧不一定是IDR帧；IDR帧的作用是立刻刷新，<strong>使错误不致传播</strong>，从IDR帧开始算新的序列开始编码。I帧有被跨帧参考的可能，IDR不会。</p>
<p>I帧不用参考任何帧，但是之后的P帧和B帧是有可能参考这个I帧之前的帧的。IDR就不允许这样，例如：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">IDR1 P4 B2 B3 P7 B5 B6 I10 B8 B9 P13 B11 B12 P16 B14 B15</span><br><span class="line">这里的B8可以跨过I10去参考P7</span><br><span class="line">IDR1 P4 B2 B3 P7 B5 B6 IDR8 P11 B9 B10 P14 B11 B12</span><br><span class="line">这里的B9就只能参照IDR8和P11，不可以参考IDR8前面的帧</span><br></pre></td></tr></table></figure>
<p>总结：</p>
<ul>
<li>解码器立即刷新，清空参考帧缓冲区（DPB），防止错误传播。</li>
<li>GOP第一帧是IDR帧，是特殊的I帧。</li>
<li>GOP只有一个IDR帧，但可能还有多个I帧。</li>
<li>IDR帧之后的图像不会参考其之前的帧，但普通I帧就没有这个限制。</li>
</ul>
<h3 id="p-帧">P 帧</h3>
<p>Predictive-coded picture，预测编码图像帧，帧间编码帧。</p>
<p>利用之前的 I 帧或 P 帧，采用运动预测的方式进行预测编码。<strong>不会参考B帧。</strong></p>
<ul>
<li>P 帧属于前向预测的帧间编码，只参考最靠近它的 I 帧或 P 帧。它只占 I 帧大小的一半。</li>
<li>P 帧可以作为后面 P 帧的参考帧，也可以作为其前后的 B 帧的参考帧。</li>
</ul>
<h3 id="b-帧">B 帧</h3>
<p>Bidirectionally predicted picture，双向预测编码图像帧，帧间编码帧。</p>
<p>提供最高的压缩比，她既需要之前的图像帧（I 帧或 P 帧），也需要后来的图像帧（P 帧），采用运动预测的方式进行帧间双向预测编码。<strong>不会参考附近的 B 帧。</strong></p>
<p>占 I 帧的 1/4 大小，压缩比最大，但解码的时候占用资源和耗时也是最大的。在实时通信中较少使用 B 帧，点播、存储的视频则可以较多地使用 B 帧。</p>
<ul>
<li>B 帧值反映两参考帧间运动主体的变化情况，预测比较准确。</li>
<li>B 帧会比附近的 P 帧后解码，即先解码附近的帧才能解码当前B帧。</li>
</ul>
<h3 id="帧与分组的关系">帧与分组的关系</h3>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772065852-0b6f6459-35d3-4cd8-9b81-597632f1a3d8.png" /></p>
<p>箭头指向的帧参考箭头起点的帧。解码 B 帧需要解码参考的前后帧。解码的顺序与播放的顺序是不一致的，因此就有了下面的话题。</p>
<h3 id="spspps">SPS、PPS</h3>
<p>SPS、PPS 不称为帧，只是在 IDR 帧前的参数数据，这两个信息一般同时出现。</p>
<ul>
<li>SPS，Sequence Parameter Set，序列参数集
<ul>
<li>作用于一串连续的视频图像，对帧组 GOP 的参数设置。</li>
<li>如：seq_parameter_set_id、帧数、POC（picture order count）的约束、参考帧数量、解码图像尺寸、场编码模式选择标志等。</li>
</ul></li>
<li>PPS，Picture Parameter Set，图像参数集
<ul>
<li>作用于视频序列中的图像，GOP 中每一幅图像等参数设置。</li>
<li>如：pic_parameter_set_id、熵编码模式选择标志、片组数目、初始量化参数、去方块滤波系数调整标志等。</li>
</ul></li>
</ul>
<h4 id="sps">SPS</h4>
<ul>
<li>H264 Profile：对视频压缩特性的描述，profile 越高，说明采用了越高级的压缩特性，对应的压缩比也越高。</li>
<li>H264 Level：对视频规格的描述，level 越高，视频的码率、分辨率、帧率越高。</li>
</ul>
<p>H264 Profile：</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772065292-fa686316-81fa-4e5d-bd6b-4f6ade8241e3.png" /></p>
<p>如上图，从Constrained Baseline Profile为核心发展出两个方向的分支，一个是Main Profile；另一个是Baseline Profile和Extended Profile。Main比Constrained Baseline压缩比高（有B帧和更高压缩比的CABAC无损压缩算法）。我们用得更多的是Main Profile方向的分支，下面是该分支的发展的具体Profile。</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772065501-b33be986-f0aa-4e30-9b01-dce4e0faf2a9.png" /></p>
<p>High应该是压缩比最高的profile，后面不断增加的是质量方面的特性。</p>
<p>H264 Level：</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772065708-481862b1-d535-4e12-b98c-6897a4e51d5f.png" /></p>
<p>其他重要参数：</p>
<p><strong>分辨率</strong>相关参数：</p>
<ul>
<li>pic_width_in_mbs_minus1：图像宽度包含的宏块个数-1（这里好获取具体宽度还要获取宏块宽度，默认是16）</li>
<li>pic_height_in_mbs_minus1：图像高度包含的宏块个数-1</li>
<li>frame_mbs_only_flag：帧编码还是场编码（场是隔行扫描，产生两张图，该参数会影响分辨率的计算）</li>
<li>frame_cropping_flag：图像是否需要裁剪（有裁剪的，还要减去裁剪的尺寸）
<ul>
<li>frame_crop_left_offset：减去左侧的偏移量</li>
<li>frame_crop_right_offset：减去右侧的偏移量</li>
<li>frame_crop_top_offset：减去顶部的偏移量</li>
<li>frame_crop_bottom_offset：减去底部的偏移量</li>
</ul></li>
</ul>
<p>通过 pic_width_in_mbs_minus1、pic_height_in_mbs_minus1、宏块宽高（默认 16x16）以及考虑 frame_mbs_only_flag、frame_cropping_flag，可以得出分辨率。</p>
<p><strong>GOP帧信息</strong>参数：</p>
<ul>
<li>log2_max_frame_num_minus4：可得出GOP的最大帧数：2的该值次方+4。
<ul>
<li>可通过该值与 slice header 的 frame_num，得出被解码的帧的序号。</li>
</ul></li>
<li>max_num_ref_frames：参考帧的数量。
<ul>
<li>用于设置解码时候的缓冲队列大小。</li>
</ul></li>
<li>pic_order_cnt_type：显示帧的序号类型。
<ul>
<li>通过计算可得出显示的顺序。</li>
</ul></li>
</ul>
<p>帧率计算：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">framerate = (float)(sps-&gt;vui.vui_time_scale) /</span><br><span class="line">            (float)(sps-&gt;vui.vui_num_units_in_tick) /</span><br><span class="line">            2.0</span><br></pre></td></tr></table></figure>
<h4 id="pps">PPS</h4>
<ul>
<li>entropy_coding_mode_flag：熵编码类型，1表示使用 CABAC，0则为CAVLC。</li>
<li>num_slice_groups_minus1：分片数量。</li>
<li>weighted_pred_flag：在 P/SP Slice 中是否开启权重预测。</li>
<li>weighted_bipred_idc：在 B Slice 中加权预测的方法类型。</li>
<li>pic_init_qp_minus26/pic_init_qs_minus26：初始化量化参数，实际参数在 Slice Header 中。</li>
<li>chroma_qp_index_offset：用于计算色度的量化参数。</li>
<li>deblocking_filter_control_present_flag：表示 Slice Header 中是否存在去块滤波器控制的信息。</li>
<li>constrained_intra_pred_flag：若为1，表示 I 宏块在进行帧内预测时只能使用来自 I 和 SI 类型的宏块的信息。</li>
<li>redundant_pic_cnt_present_flag：用于表示 Slice Header 中是否存在 redundant_pic_cnt 语法元素。</li>
</ul>
<h3 id="slice-header">Slice Header</h3>
<ul>
<li>帧类型</li>
<li>GOP中解码帧序号（当有 B 帧的时候，并不是顺序解码的）</li>
<li>预测权重</li>
<li>滤波</li>
</ul>
<h2 id="dtspts">DTS、PTS</h2>
<ul>
<li>DTS（Decode Time Stamp，解码时间戳）：表示packet的解码时间，主要用于视频的编码，在编码阶段使用。主要标识内存的包什么时候送入解码器中解码。解码阶段使用。</li>
<li>PTS（Presentation Time Stamp，显示时间戳）：表示packet解码后数据的显示时间，主要用于视频的同步和输出。显示阶段使用。</li>
</ul>
<p>音频中DTS和PTS是相同的。视频中由于B帧需要双向预测，B帧依赖于其前和其后的帧，因此含B帧的视频解码顺序与显示顺序不同，即DTS与PTS不同。当然，不含B帧的视频，其DTS和PTS是相同的。下图以一个开放式GOP示意图为例，说明视频流的解码顺序和显示顺序。</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772065948-58a4d622-11bb-4fa8-8421-258c29beb872.jpeg" /></p>
<ul>
<li><code>采集顺序</code>：指图像传感器采集原始信号得到图像帧的顺序。</li>
<li><code>编码顺序</code>：指编码器编码后图像帧的顺序。存储到磁盘的本地视频文件中图像帧的顺序与编码顺序相同。</li>
<li><code>传输顺序</code>：指编码后的流在网络中传输过程中图像帧的顺序。</li>
<li><code>解码顺序</code>：指解码器解码图像帧的顺序。</li>
<li><code>显示顺序</code>：指图像帧在显示器上显示的顺序。</li>
</ul>
<p><strong><em>采集顺序与显示顺序相同。编码顺序、传输顺序和解码顺序相同。</em></strong></p>
<p>图中“B[1]”帧依赖于“I[0]”帧和“P[3]”帧，因此“P[3]”帧必须比“B[1]”帧先解码。这就导致了解码顺序和显示顺序的不一致，后显示的帧需要先解码。</p>
<p>可见，在没有B帧的时候，DTS和PTS是是一致的。</p>
<h2 id="码流">码流</h2>
<p>H.264原始码流（裸流）是由一个接一个NALU组成。</p>
<p>按其功能可能将其分层：</p>
<ul>
<li>VCL层，Video Coding Layer，视频数据编码层。
<ul>
<li>保存视频压缩后的数据。</li>
</ul></li>
<li>NAL层，Network Abstraction Layer，视频数据网络抽象层，最外层。
<ul>
<li>对VCL视频编码层数据拆成多个包传输，并提供header等信息。</li>
<li>为解决网络传输中丢包、乱序、重传问题提供标记。</li>
</ul></li>
</ul>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/wmxrDf.jpg" /></p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/471463-20171214170746310-1770557571.png" /></p>
<p>相关术语：</p>
<ul>
<li>SODB，String Of Data Bits：位串。
<ul>
<li>原始数据比特流/二进制数据串，以位编码在一起，长度不一定是8的倍数，故需要补齐。</li>
<li>由 VCL 层产生。</li>
</ul></li>
<li>RBSP，Raw Byte Sequence Payload：字节序列负载数据。
<ul>
<li>SODB + trailing bits，如果SODB最后一个字节不对齐，则补1和多个0。</li>
</ul></li>
<li>EBSP，Encapsulated Byte Sequence Payload：扩展字节序列负载数据。
<ul>
<li>在每一帧开头添加起始位。</li>
</ul></li>
<li>NALU，NAL 单元：多个NAL单元组成H264码流。</li>
</ul>
<p>关系：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">RBSP = SODB + RBSP Stop bit + 0 bits</span><br><span class="line">EBSP = RBSP part1 + 0x03 + RBSP part2 + 0x03 + ... + RBSP partN</span><br><span class="line">NALU = NALU Header(1 byte) + EBSP</span><br></pre></td></tr></table></figure>
<h3 id="nal-unit">NAL Unit</h3>
<p>码流的总体结构：</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772066961-b44bfe42-118c-4865-8053-7ad490bde618.png" /></p>
<ul>
<li>Annexb 格式用于文件存储、本地播放，是在NALU前面增加了StartCode。</li>
<li>RTP 格式用于网络传播，直接就是传输NALU。</li>
</ul>
<p>NALU有两种格式：</p>
<ul>
<li>Annex B/Elementary Stream：以<code>0x00_00_01</code>或<code>0x00_00_00_01</code>开头。</li>
<li>AVCC/MPEG-4：以所在NALU长度开头。</li>
</ul>
<p>宏块存储数据：</p>
<ul>
<li>mb_type：宏块类型</li>
<li>mb_pred：预测类型值</li>
<li>coded residual，残差值</li>
</ul>
<p>宏块与帧的关系：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">1片 = N宏块</span><br><span class="line">1帧 = N片</span><br><span class="line">常常一个NALU只包含1个片</span><br></pre></td></tr></table></figure>
<p>片的出现：设置片的目的是为了限制误码的扩散和传输，让编码片之间相互独立，如某片的预测不能以其他片中的宏块为参考图像，以防止某片中的预测错误传播到其他片中。</p>
<p>层级划分：</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772066806-5f003f52-a8ae-452d-bc4a-ecce266ff637.png" /></p>
<p>VCL结构关系：</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1615772066728-9a1d776d-e9b2-4d8e-a190-e883685645bf.png" /></p>
<p>其中，SPS、PPS不是VCL产生的，但以NALU传输，对于正确解码非常重要。可以通过独立的服务来发送参数集。</p>
<h2 id="分析工具">分析工具</h2>
<ul>
<li>Elecard Stream Eye
<ul>
<li>https://www.elecard.com/products/video-analysis</li>
</ul></li>
<li>CodecVisa</li>
<li>雷神开发的工具
<ul>
<li>https://jaist.dl.sourceforge/project/h254streamanalysis/binary/SpecialVH264.exe</li>
<li>https://sourceforge.net/projects/videoeye/files/</li>
</ul></li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>AVC Profile &amp; Level</title>
    <url>/posts/avc_profile_and_level/</url>
    <content><![CDATA[<h1 id="avc-profile">AVC Profile</h1>
<p>The standard defines several sets of capabilities, which are referred to as <em>profiles</em>, targeting specific classes of applications. These are declared using a profile code (profile_idc) and sometimes a set of additional constraints applied in the encoder. The profile code and indicated constraints allow a decoder to recognize the requirements for decoding that specific bitstream. (And in many system environments, only one or two profiles are allowed to be used, so decoders in those environments do not need to be concerned with recognizing the less commonly used profiles.) By far the most commonly used profile is the High Profile.</p>
<p>针对特定类别的应用程序，标准定义了几组功能，称为配置文件（profile）。这些功能是用一个配置文件代码（profile_idc）来声明的，有时还在编码器中应用一组额外的约束。配置文件代码和指定的约束条件允许解码器识别解码该特定比特流的要求。在许多系统环境中，只允许使用一个或两个配置文件，所以这些环境中的解码器不需要关注识别不太常用的配置文件）。到目前为止，最常用的配置文件是High Profile。</p>
<p>Profiles for non-scalable 2D video applications include the following:</p>
<p>用于非可扩展的二维视频应用的配置文件包括以下内容。</p>
<h2 id="constrained-baseline-profile-cbp-66-with-constraint-set-1">Constrained Baseline Profile (CBP, 66 with constraint set 1)</h2>
<p>Primarily for low-cost applications, this profile is most typically used in videoconferencing and mobile applications. It corresponds to the subset of features that are in common between the Baseline, Main, and High Profiles.</p>
<p>主要用于低能耗的应用，这个配置文件最典型的应用场景是视频会议和移动应用。它对应于Baseline、Main和High配置文件之间的共同特征子集。</p>
<h2 id="baseline-profile-bp-66">Baseline Profile (BP, 66)</h2>
<p>Primarily for low-cost applications that require additional data loss robustness, this profile is used in some videoconferencing and mobile applications. This profile includes all features that are supported in the Constrained Baseline Profile, plus three additional features that can be used for loss robustness (or for other purposes such as low-delay multi-point video stream compositing). The importance of this profile has faded somewhat since the definition of the Constrained Baseline Profile in 2009. All Constrained Baseline Profile bitstreams are also considered to be Baseline Profile bitstreams, as these two profiles share the same profile identifier code value.</p>
<p>主要用于需要额外数据损失稳健性的低能耗应用，该配置文件用于一些视频会议和移动应用。该配置文件包括Constrained Baseline Profile中支持的所有功能，加上三个额外的功能，可用于损失稳健性（或用于其他目的，如低延迟多点视频流合成）。自2009年定义约束基线配置文件以来，该配置文件的重要性已有所淡化。所有受限基线配置文件的比特流也被认为是基线配置文件的比特流，因为这两个配置文件共享相同的配置文件标识码值。</p>
<h2 id="extended-profile-xp-88">Extended Profile (XP, 88)</h2>
<p>Intended as the streaming video profile, this profile has relatively high compression capability and some extra tricks for robustness to data losses and server stream switching.</p>
<p>作为流媒体视频配置文件，该配置文件具有相对较高的压缩能力和一些额外的技巧，以增强对数据损失和服务器流切换的稳定性。</p>
<h2 id="main-profile-mp-77">Main Profile (MP, 77)</h2>
<p>This profile is used for standard-definition digital TV broadcasts that use the MPEG-4 format as defined in the DVB standard.[<a href="https://en.wikipedia.org/wiki/Advanced_Video_Coding#cite_note-49">49]</a> It is not, however, used for high-definition television broadcasts, as the importance of this profile faded when the High Profile was developed in 2004 for that application.</p>
<p>这个配置文件用于使用DVB标准中定义的MPEG-4格式的标清数字电视广播。然而，它不用于高清电视广播，因为当2004年为该应用开发出High Profile时，这个配置文件的重要性就消失了。</p>
<h2 id="high-profile-hip-100">High Profile (HiP, 100)</h2>
<p>The primary profile for broadcast and disc storage applications, particularly for high-definition television applications (for example, this is the profile adopted by the <a href="https://en.wikipedia.org/wiki/Blu-ray_Disc">Blu-ray Disc</a> storage format and the <a href="https://en.wikipedia.org/wiki/Digital_Video_Broadcasting">DVB</a> HDTV broadcast service).</p>
<p>广播和光盘存储应用的主要配置文件，特别是高清电视应用（例如，这是蓝光光盘存储格式和DVB高清广播服务采用的配置文件）。</p>
<h2 id="progressive-high-profile-phip-100-with-constraint-set-4">Progressive High Profile (PHiP, 100 with constraint set 4)</h2>
<p>Similar to the High profile, but without support of field coding features.</p>
<p>类似于High profile，但不支持现场编码功能。</p>
<h2 id="constrained-high-profile-100-with-constraint-set-4-and-5">Constrained High Profile (100 with constraint set 4 and 5)</h2>
<p>Similar to the Progressive High profile, but without support of B (bi-predictive) slices.</p>
<p>类似于Progressive High profile，但不支持B（双预测）切片。</p>
<h2 id="high-10-profile-hi10p-110">High 10 Profile (Hi10P, 110)</h2>
<p>Going beyond typical mainstream consumer product capabilities, this profile builds on top of the High Profile, adding support for up to 10 bits per sample of decoded picture precision.</p>
<p>超越了典型的主流消费产品的能力，这个配置文件建立在高配置文件的基础上，增加了对每个样本高达10比特的解码图像精度的支持。</p>
<h2 id="high-422-profile-hi422p-122">High 4:2:2 Profile (Hi422P, 122)</h2>
<p>Primarily targeting professional applications that use interlaced video, this profile builds on top of the High 10 Profile, adding support for the 4:2:2 <a href="https://en.wikipedia.org/wiki/Chroma_sampling">chroma sampling</a> format while using up to 10 bits per sample of decoded picture precision.</p>
<p>主要针对使用隔行扫描视频的专业应用，该配置文件建立在High 10 Profile的基础上，增加了对4:2:2色度采样格式的支持，同时使用高达10比特/采样的解码图像精度。</p>
<h2 id="high-444-predictive-profile-hi444pp-244">High 4:4:4 Predictive Profile (Hi444PP, 244)</h2>
<p>This profile builds on top of the High 4:2:2 Profile, supporting up to 4:4:4 chroma sampling, up to 14 bits per sample, and additionally supporting efficient lossless region coding and the coding of each picture as three separate color planes.</p>
<p>这个配置文件建立在High 4:2:2 Profile的基础上，支持高达4:4:4的色度采样，每个采样高达14比特，另外还支持高效的无损区域编码和每个图片作为三个独立的颜色平面的编码。</p>
<p>For camcorders, editing, and professional applications, the standard contains four additional <a href="https://en.wikipedia.org/wiki/Intra-frame">Intra-frame</a>-only profiles, which are defined as simple subsets of other corresponding profiles. These are mostly for professional (e.g., camera and editing system) applications:</p>
<p>对于摄像机、编辑和专业应用，该标准包含四个额外的全I帧的配置文件，它们被定义为其他相应配置文件的简单子集。这些主要是针对专业（如摄像机和编辑系统）应用：</p>
<h3 id="high-10-intra-profile-110-with-constraint-set-3">High 10 Intra Profile (110 with constraint set 3)</h3>
<p>The High 10 Profile constrained to all-Intra use.</p>
<p>被约束为全I帧的High 10 Profile。</p>
<h3 id="high-422-intra-profile-122-with-constraint-set-3">High 4:2:2 Intra Profile (122 with constraint set 3)</h3>
<p>The High 4:2:2 Profile constrained to all-Intra use.</p>
<p>被约束为全I帧的High 4:2:2 Profile。</p>
<h3 id="high-444-intra-profile-244-with-constraint-set-3">High 4:4:4 Intra Profile (244 with constraint set 3)</h3>
<p>The High 4:4:4 Profile constrained to all-Intra use.</p>
<p>被约束为全I帧的High 4:4:4 Profile。</p>
<h2 id="cavlc-444-intra-profile-44">CAVLC 4:4:4 Intra Profile (44)</h2>
<p>The High 4:4:4 Profile constrained to all-Intra use and to CAVLC entropy coding (i.e., not supporting CABAC).</p>
<p>High 4:4:4 Profile限制在所有内部使用和CAVLC熵编码（即，不支持CABAC）。</p>
<p>As a result of the <a href="https://en.wikipedia.org/wiki/Scalable_Video_Coding">Scalable Video Coding</a> (SVC) extension, the standard contains five additional <em>scalable profiles</em>, which are defined as a combination of a H.264/AVC profile for the base layer (identified by the second word in the scalable profile name) and tools that achieve the scalable extension:</p>
<p>由于可扩展视频编码（SVC）的扩展，该标准包含五个额外的可扩展配置文件，它们被定义为基础层的H.264/AVC配置文件（由可扩展配置文件名称中的第二个字标识）和实现可扩展的工具的组合：</p>
<h3 id="scalable-baseline-profile-83">Scalable Baseline Profile (83)</h3>
<p>Primarily targeting video conferencing, mobile, and surveillance applications, this profile builds on top of the Constrained Baseline profile to which the base layer (a subset of the bitstream) must conform. For the scalability tools, a subset of the available tools is enabled.</p>
<p>主要针对视频会议、移动和监控应用，该配置文件建立在Constrained Baseline profile之上，基础层（比特流的一个子集）必须符合该配置文件。对于可扩展性工具，启用了可用工具的一个子集。</p>
<h3 id="scalable-constrained-baseline-profile-83-with-constraint-set-5">Scalable Constrained Baseline Profile (83 with constraint set 5)</h3>
<p>A subset of the Scalable Baseline Profile intended primarily for real-time communication applications.</p>
<p>Scalable Baseline Profile的一个子集，主要用于实时通信应用。</p>
<h3 id="scalable-high-profile-86">Scalable High Profile (86)</h3>
<p>Primarily targeting broadcast and streaming applications, this profile builds on top of the H.264/AVC High Profile to which the base layer must conform.</p>
<p>主要针对广播和流媒体应用，该配置文件建立在H.264/AVC High Profile之上，基础层必须符合该配置文件。</p>
<h3 id="scalable-constrained-high-profile-86-with-constraint-set-5">Scalable Constrained High Profile (86 with constraint set 5)</h3>
<p>A subset of the Scalable High Profile intended primarily for real-time communication applications.</p>
<p>Scalable High Profile的一个子集，主要用于实时通信应用。</p>
<h3 id="scalable-high-intra-profile-86-with-constraint-set-3">Scalable High Intra Profile (86 with constraint set 3)</h3>
<p>Primarily targeting production applications, this profile is the Scalable High Profile constrained to all-Intra use.</p>
<p>主要针对生产应用，该配置文件是Scalable High Profile，限制为全I帧。</p>
<p>As a result of the <a href="https://en.wikipedia.org/wiki/Multiview_Video_Coding">Multiview Video Coding</a> (MVC) extension, the standard contains two <em>multiview profiles</em>:</p>
<p>由于多视角视频编码（MVC）的扩展，该标准包含两个多视角配置文件：</p>
<h4 id="stereo-high-profile-128">Stereo High Profile (128)</h4>
<p>This profile targets two-view <a href="https://en.wikipedia.org/wiki/Stereoscopic">stereoscopic</a> 3D video and combines the tools of the High profile with the inter-view prediction capabilities of the MVC extension.</p>
<p>该配置文件针对双视角立体3D视频，并结合了High profile的工具和MVC扩展的视角间预测能力。</p>
<h4 id="multiview-high-profile-118">Multiview High Profile (118)</h4>
<p>This profile supports two or more views using both inter-picture (temporal) and MVC inter-view prediction, but does not support field pictures and macroblock-adaptive frame-field coding.</p>
<p>该配置文件支持两个或多个视图，使用帧间（时间）和MVC帧间预测，但不支持场图和宏块自适应帧场编码。</p>
<p>The Multi-resolution Frame-Compatible (MFC) extension added two more profiles:</p>
<p>多分辨率帧兼容（MFC）扩展又增加了两个配置文件：</p>
<h5 id="mfc-high-profile-134">MFC High Profile (134)</h5>
<p>A profile for stereoscopic coding with two-layer resolution enhancement.</p>
<p>用于具有两层分辨率增强的立体编码的配置文件。</p>
<h5 id="mfc-depth-high-profile-135">MFC Depth High Profile (135)</h5>
<p>The 3D-AVC extension added two more profiles:</p>
<p>3D-AVC扩展又增加了两个配置文件：</p>
<h6 id="multiview-depth-high-profile-138">Multiview Depth High Profile (138)</h6>
<p>This profile supports joint coding of depth map and video texture information for improved compression of 3D video content.</p>
<p>该配置文件支持深度图和视频纹理信息的联合编码，以改善3D视频内容的压缩。</p>
<h6 id="enhanced-multiview-depth-high-profile-139">Enhanced Multiview Depth High Profile (139)</h6>
<p>An enhanced profile for combined multiview coding with depth information.</p>
<p>一个增强的配置文件，用于结合深度信息的多视图编码。</p>
<h2 id="特定配置支持的配置">特定配置支持的配置</h2>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1622517263932-03d8907d-7464-4816-9a42-812e6af46ad1.png" /></p>
<h2 id="软件编码器实现">软件编码器实现</h2>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1622518153891-f9be5173-e900-41dd-aba5-d258922ade51.png" /></p>
<h1 id="avc-level">AVC Level</h1>
<p>As the term is used in the standard, a "<em>level</em>" is a specified set of constraints that indicate a degree of required decoder performance for a profile. For example, a level of support within a profile specifies the maximum picture resolution, frame rate, and bit rate that a decoder may use. A decoder that conforms to a given level must be able to decode all bitstreams encoded for that level and all lower levels.</p>
<p>正如标准中所使用的术语，level是一组特定的约束条件，表明一个配置文件所要求的解码器性能的程度。例如，一个配置文件中的支持级别规定了解码器可以使用的最大图片分辨率、帧率和比特率。符合某一等级的解码器必须能够解码为该等级和所有更低等级编码的所有比特流。</p>
<h2 id="各level的最大属性值">各Level的最大属性值</h2>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/1622542621052-d4760bc4-0db9-4fef-b049-1bf9c11b1265.png" /></p>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>视频码率控制</title>
    <url>/posts/video_bit_rate_control/</url>
    <content><![CDATA[<p>视频编码过程中，量化（有损压缩）决定了视频的码率，视频码率又一定程度决定了视频的质量。</p>
<p>量化值QP越大则量化的粒度越高，压缩比越大，码率越小，视频质量越低，呈现的画面马赛克较大、模糊不细腻。反之亦然。</p>
<p>选择一个适合场景的视频码控方案很重要，调整视频输出码率就是在视频编码速度、网络带宽以及视频质量之间做平衡。总体来说，选择视频码率控制方案，可通过一下因素权衡得出：</p>
<ul>
<li>画质，视觉质量稳定性。如清晰度、流畅度、细节等，这与人眼的视觉原理有关，选择人眼主动质量感受最高的模型。</li>
<li>输出码率。要考虑网络带宽因素。</li>
<li>视频文件大小。利于传输、存储，还要看系统的空间大小。</li>
<li>编码速度。不同的码控模型影响了编码速度。</li>
</ul>
<h2 id="码控因素">码控因素</h2>
<p>GOP长度</p>
<p>Maximun B-frame，最大B帧数量</p>
<p>Reference frame，设定一个P帧所能参考的帧数量，会影响播放相容性。</p>
<p>QP，Constant Quantizer，恒定量化值：控制图像画质，数值越低画质越高。</p>
<h2 id="码控模型">码控模型</h2>
<p>码率控制实际上是一种编码的优化算法，它用于实现对视频流码流大小的控制。目的在于同样的视频编码格式，码流大，它包含的信息也就越多，那么对应的图像也就越清晰，反之亦然。</p>
<h3 id="cqp">CQP</h3>
<p>固定QP。最简单的码控方式，每帧图像都按照一个特定的QP来编码，每帧编码后的数据量有多大仍是未知的，既不是码率优先模型，也不是质量优先模型。</p>
<h4 id="特点">特点</h4>
<ul>
<li>瞬时码率会随场景复杂度波动。</li>
<li>编码速度快，调控最简单。</li>
<li>x264和x265中支持CQP模式，libvpx不支持。
<ul>
<li>H.264中QP范围是[0, 51]。QP值越大表示越大的量化步长，编码视频的质量越低。QP为0表示进行无损编码。</li>
</ul></li>
</ul>
<h4 id="适用场景">适用场景</h4>
<p>一般不建议使用这种方式，因为这没有考虑编码内容的复杂性，用相同的压缩比处理每一帧。出来的视频质量和码率都不固定。适合于非常简单的运动量很小的画面，因为一遇到复杂场景，其码率波动就非常大。</p>
<h3 id="crf">CRF</h3>
<p>Constant Rate Factor，恒定码率系数。把某一个“视觉质量”作为输出目标。通过降低那些耗费码率但是又单一用肉眼察觉的帧（高速运动或纹理丰富）的质量，提升那些静态帧码率来达到目的。</p>
<p>帧间QP变化，帧内宏块QP变化，输出码率未知，各帧输出的视觉质量基本恒定，相当于固定质量模式+限制码率峰值的方式。</p>
<h4 id="特点-1">特点</h4>
<ul>
<li>与恒定QP类似，单追求主观感知的质量恒定，瞬时码率也会随场景复杂度波动，视频帧之间或者内部宏块之间的QP值都不一样。</li>
<li>对于快速运动或细节丰富的场景会适当增大量化失真（因为人眼不敏感）；反之对于静止或平坦区域则减少量化失真。</li>
<li>CRF是x264和x265的默认码率控制方式，也可用于libvpx。
<ul>
<li>RF值越大视频压缩比越高，但视频质量越低，各codec的CRF取值范围一般[0-51]，但是一般默认值x264用23，x265默认为28。</li>
<li>如果你不确定要使用什么RF，从默认值开始，并根据对输出的主观印象进行更改。如果质量没有足够好则较低的RF。如果文件太大了则选择更高的RF。更改±6会导致码率大小的一半/两倍左右的变化，±1会导致码率10%左右的变化。</li>
</ul></li>
</ul>
<h4 id="适用场景-1">适用场景</h4>
<p>对视频质量又一定要求的场景。CRF值可以简单理解为对视频质量最期望的一个输出固定值，无论是在运动复杂场景还是在静止简单的场景下，都希望一个稳定的主观视频质量时，选择该模式。该模式时视频质量优先模型。视频质量可简单理解为视频清晰度、像素的细腻程度和视频的流畅度。</p>
<h3 id="cbr">CBR</h3>
<p>Constant Bit Rate，恒定码率。一定时间范围内码率基本保持恒定，属于码率优先模型。</p>
<h4 id="特定">特定</h4>
<ul>
<li>码率稳定，但质量不稳定。带宽有效利用率不高，特别当该值设置不合理，在复杂运动场景下，画面会非常模糊，非常影响观看体验。</li>
<li>输出视频码率稳定，便于计算视频体积大小。</li>
</ul>
<h4 id="适用场景-2">适用场景</h4>
<p>一般也不建议使用这种方式，因为这种模型不考虑视频内容的复杂性，吧所有视频帧都统一对待。但有些编码软件只支持固定质量或固定码率，有时不得不用。使用的时候，在允许的带宽范围内尽可能吧带宽设置大些，以防止复杂场景下视频质量的降低，如果设置不合理，在运动场景下就糊得看不成了。</p>
<h3 id="vbr">VBR</h3>
<p>Variable Bit Rate，可变码率。</p>
<p>简单场景分配较大QP，复杂场景分配较大QP，得到基本稳定的视频质量。确定时输出码率不可控。</p>
<p>有两种调控模式：</p>
<ul>
<li>质量优先模式：不考虑视频文件大小，完全按照视频内容复杂度来分配码率，这样视频的播放效果最佳。</li>
<li>二次编码方式，2PASS：第一次编码检测视频内容简单和复杂的部分，同时确定简单和复杂的比例。第二遍编码让视频的平均码率不变，复杂的地方分配更多比特，简单地方分配更少比特。缺点时速度较慢。</li>
</ul>
<h4 id="特点-2">特点</h4>
<ul>
<li>码率不稳定，但质量稳定且非常高。</li>
<li>编码速度较慢。点播、下载和存储系统优先使用，不适合低延迟直播系统。</li>
<li>该模型完全不考虑输出视频带宽，为了质量，需要多少码率就占用多少，也不考虑编码速度。</li>
</ul>
<h4 id="适用场景-3">适用场景</h4>
<p>适用于那些对带宽和编码速度不太限制，但对视频质量很高要求的场景。特别是在运动复杂场景下也能保持较高的清晰度，且输出质量较稳定。适合延时不敏感的点播、录播或存储系统。</p>
<h3 id="abr">ABR</h3>
<p>Average Bit Rate，恒定平均目标码率。</p>
<p>简单场景分配较低码率，复杂场景分配足够码率，使得有限的码率在不同场景下都能合理分配，类似于VBR。同意时间内，平均码率又接近设置的目标码率，这样可以控制输出文件的大小，这又类似于CBR。可以认为是CBR和VBR的折中方案，也是大多人的选择。特别是在对质量和视频带宽都有要求的情况下，可以优先选择该模式。一般速度是VBR的2～3倍，相同体积的视频文件质量却比CBR好得多。</p>
<h4 id="特点-3">特点</h4>
<ul>
<li>视频质量整体可控，同时兼顾率视频码率和编码速度。</li>
<li>使用过程中一般要设置最低码率、最高码率和平均码率，这些值的设置尽可能合理。</li>
</ul>
<h4 id="适用场景-4">适用场景</h4>
<p>ABR在直播和低延时系统使用较多，因为只编码了一次，所以速度快。同时兼顾了视频质量和带宽，对于转码速度有要求的情况下也可能选择该模式。B站大部分视频选择了该模式。</p>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>常用视频参数设置（经验值）</title>
    <url>/posts/common_video_parameter_settings/</url>
    <content><![CDATA[<p><a href="https://docs.agora.io/cn/Interactive%20Broadcast/video_profile_apple?platform=iOS">设置视频编码属性</a></p>
<h4 id="视频属性参考表">视频属性参考表</h4>
<p>视频能否达到 960 × 720 及以上的分辨率还取决于用户的设备。分辨率 1290 × 1080 及以上的视频属性仅适用于 macOS 平台。</p>
<table>
<thead>
<tr class="header">
<th>分辨率 (宽 × 高)</th>
<th>帧率 (fps)</th>
<th>基准码率 (Kbps，适用于通信)</th>
<th>直播码率 (Kbps，适用于直播)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>160 × 120</td>
<td>15</td>
<td>65</td>
<td>130</td>
</tr>
<tr class="even">
<td>120 × 120</td>
<td>15</td>
<td>50</td>
<td>100</td>
</tr>
<tr class="odd">
<td>320 × 180</td>
<td>15</td>
<td>140</td>
<td>280</td>
</tr>
<tr class="even">
<td>180 × 180</td>
<td>15</td>
<td>100</td>
<td>200</td>
</tr>
<tr class="odd">
<td>240 × 180</td>
<td>15</td>
<td>120</td>
<td>240</td>
</tr>
<tr class="even">
<td>320 × 240</td>
<td>15</td>
<td>200</td>
<td>400</td>
</tr>
<tr class="odd">
<td>240 × 240</td>
<td>15</td>
<td>140</td>
<td>280</td>
</tr>
<tr class="even">
<td>424 × 240</td>
<td>15</td>
<td>220</td>
<td>440</td>
</tr>
<tr class="odd">
<td>640 × 360</td>
<td>15</td>
<td>400</td>
<td>800</td>
</tr>
<tr class="even">
<td>360 × 360</td>
<td>15</td>
<td>260</td>
<td>520</td>
</tr>
<tr class="odd">
<td>640 × 360</td>
<td>30</td>
<td>600</td>
<td>1200</td>
</tr>
<tr class="even">
<td>360 × 360</td>
<td>30</td>
<td>400</td>
<td>800</td>
</tr>
<tr class="odd">
<td>480 × 360</td>
<td>15</td>
<td>320</td>
<td>640</td>
</tr>
<tr class="even">
<td>480 × 360</td>
<td>30</td>
<td>490</td>
<td>980</td>
</tr>
<tr class="odd">
<td>640 × 480</td>
<td>15</td>
<td>500</td>
<td>1000</td>
</tr>
<tr class="even">
<td>480 × 480</td>
<td>15</td>
<td>400</td>
<td>800</td>
</tr>
<tr class="odd">
<td>640 × 480</td>
<td>30</td>
<td>750</td>
<td>1500</td>
</tr>
<tr class="even">
<td>480 × 480</td>
<td>30</td>
<td>600</td>
<td>1200</td>
</tr>
<tr class="odd">
<td>848 × 480</td>
<td>15</td>
<td>610</td>
<td>1220</td>
</tr>
<tr class="even">
<td>848 × 480</td>
<td>30</td>
<td>930</td>
<td>1860</td>
</tr>
<tr class="odd">
<td>640 × 480</td>
<td>10</td>
<td>400</td>
<td>800</td>
</tr>
<tr class="even">
<td>1280 × 720</td>
<td>15</td>
<td>1130</td>
<td>2260</td>
</tr>
<tr class="odd">
<td>1280 × 720</td>
<td>30</td>
<td>1710</td>
<td>3420</td>
</tr>
<tr class="even">
<td>960 × 720</td>
<td>15</td>
<td>910</td>
<td>1820</td>
</tr>
<tr class="odd">
<td>960 × 720</td>
<td>30</td>
<td>1380</td>
<td>2760</td>
</tr>
<tr class="even">
<td>1920 × 1080</td>
<td>15</td>
<td>2080</td>
<td>4160</td>
</tr>
<tr class="odd">
<td>1920 × 1080</td>
<td>30</td>
<td>3150</td>
<td>6300</td>
</tr>
<tr class="even">
<td>1920 × 1080</td>
<td>60</td>
<td>4780</td>
<td>6500</td>
</tr>
<tr class="odd">
<td>2560 × 1440</td>
<td>30</td>
<td>4850</td>
<td>6500</td>
</tr>
<tr class="even">
<td>2560 × 1440</td>
<td>60</td>
<td>6500</td>
<td>6500</td>
</tr>
<tr class="odd">
<td>3840 × 2160</td>
<td>30</td>
<td>6500</td>
<td>6500</td>
</tr>
<tr class="even">
<td>3840 × 2160</td>
<td>60</td>
<td>6500</td>
<td>6500</td>
</tr>
</tbody>
</table>
<h4 id="常用分辨率帧率和码率">常用分辨率、帧率和码率</h4>
<p>通常来讲，视频参数的选择要根据产品实际情况来确定，比如，如果一对一，老师和学生的窗口比较大，要求分辨率会高一点，随之帧率和码率也要高一点；如果是一对四， 老师和学生的窗口都比较小，分辨率可以低一点，对应的码率帧率也会低一点，以减少编解码的资源消耗和缓解下行带宽压力。一般可按下列场景中的推荐值进行设置。</p>
<ul>
<li>二人视频通话场景：
<ul>
<li>分辨率 320 x 240、帧率 15 fps、码率 200 Kbps</li>
<li>分辨率 640 x 360、帧率 15 fps、码率 400 Kbps</li>
</ul></li>
<li>多人视频通话场景：
<ul>
<li>分辨率 160 x 120、帧率 15 fps、码率 65 Kbps</li>
<li>分辨率 320 x 180、帧率 15 fps、码率 140 Kbps</li>
<li>分辨率 320 x 240、帧率 15 fps、码率 200 Kbps</li>
</ul></li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>x264编码器参数设置</title>
    <url>/posts/x264_encoder_parameter_setting/</url>
    <content><![CDATA[<p>参数分类：</p>
<ul>
<li>预设值</li>
<li>帧相关参数</li>
<li>码流的控制</li>
<li>编码分析</li>
<li>输出</li>
</ul>
<h2 id="预设值">预设值</h2>
<ul>
<li>preset：速度/实时性维度的配置方案
<ul>
<li>fast、slow 等</li>
</ul></li>
<li>tune：视频质量维度的配置方案。逐级递减。</li>
</ul>
<p>两者不互斥，tune 参数的优先级在 preset 参数之后，在其他参数之前。</p>
<h2 id="帧相关参数">帧相关参数</h2>
<ul>
<li>keyint/min-keyint：GOP 大小，默认是250。</li>
<li>scenecut：判断为场景切换的阈值，为场景切换时插入一个 I 帧。</li>
<li>bframes：B 帧数量，默认设置3。</li>
<li>ref：参考帧数量，决定了解码时候缓冲区的大小。</li>
<li>no-deblock/deblock：是否启用去块化。在编码预测的时候会发生出现块。</li>
<li>no-cabac：是否使用 CABAC 进行熵编码。</li>
</ul>
<h2 id="流控">流控</h2>
<ul>
<li>qp：量化器等级，比 crf 码流大且与 bitrate/crf 互斥。</li>
<li>bitrate：码流，无法控制质量。</li>
<li>crf：质量等级，默认是23，数值越低越好。</li>
<li>qmin：默认10。</li>
<li>qmax：默认51。</li>
<li>qpstep：两帧之间量化器的最大变化，默认是4。</li>
</ul>
<h2 id="编码分析">编码分析</h2>
<ul>
<li>partitions：宏块划分。如：p8x8、b8x8、i8x8、i4x4</li>
<li>me：运动评估算法。如：钻石、六边形等。</li>
</ul>
<h2 id="输出">输出</h2>
<ul>
<li>sar：宽高比。</li>
<li>fps：帧率。</li>
<li>level：输出等规则。720P等。</li>
</ul>
<h2 id="参考资料">参考资料</h2>
<ul>
<li><a href="http://www.chaneru.com/Roku/HLS/X264_Settings.htm">X264 Settings - MeWiki</a></li>
<li><a href="https://sites.google.com/site/linuxencoding/x264-ffmpeg-mapping">x264 FFmpeg Options Guide - Linux Encoding</a></li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>音视频同步</title>
    <url>/posts/audio_and_video_synchronization/</url>
    <content><![CDATA[<p>在短视频与直播APP中，采集端作为音视频的生产者，如果采集端产生的音视频源本身就无法保证同步，那么后面不管经过什么处理，都很难再让用户看到音视频同步的画面了，因此，在采集端保证音视频同步上尤其重要。</p>
<h2 id="基本概念">基本概念</h2>
<h3 id="时间基">时间基</h3>
<p>时间基是指时间刻度。因为时间信息是以整数存储的，而我们常使用的秒是浮点数，为了存储浮点数则把浮点数使用分数表达。时间基就是其中的分母，时间值是分子，得出浮点型的时间。</p>
<p>分类：</p>
<ul>
<li>tbr，time base of rate: 通常所说的帧率。</li>
<li>tbn，time base of stream: 视频流的时间基。</li>
<li>tbc，time base of codec: 视频解码的时间基。</li>
</ul>
<p>不同场景的时间戳对应不同的时间基，对于视频渲染则使用视频流的时间基。</p>
<h2 id="音视频同步方式">音视频同步方式</h2>
<ul>
<li>视频同步到音频。适用于音频各种参数固定，即其PTS是可以简单计算的，所以很方便地与视频帧的PTS对比进行同步。</li>
<li>音频同步到视频。在音视频流长度不一致时，要考虑对音频进行丢帧和补帧。</li>
<li>音频和视频都同步到系统时钟。</li>
</ul>
<p>基本思路：</p>
<ol type="1">
<li>展示第一帧视频帧后，获得要显示的下一个视频帧的PTS；</li>
<li>设置一个定时器；</li>
<li>当定时器超时后刷新新的视频帧。</li>
<li>循环反复。</li>
</ol>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>CDN网络</title>
    <url>/posts/cnd/</url>
    <content><![CDATA[<p>CDN（Content Delivery Network），内容分发网络。最初的目的是解决静态页面的加速问题。通过就近接入的方式解决访问网络资源的问题。</p>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/REaPnH.png" /></p>
<p>电信和联通互相通信的时候会发生主动的丢包。</p>
<ul>
<li>源节点。源节点直接也有连接，互通资源。</li>
<li>主干节点。连接不同运营商的节点。</li>
<li>边缘节点。离用户最近的节点。边缘节点没有资源的时候，会向源节点获取。</li>
</ul>
<p>用户通过域名解析，连接到合适的边缘节点。所以用户是通过边缘节点逐步向服务器传递数据。而服务器把数据推送到源节点，然后通过CDN逐步扩散到各个边缘节点。</p>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>压缩率和压缩比</title>
    <url>/posts/compression_rate_and_compression_ratio/</url>
    <content><![CDATA[<p>在看资料的时候，经常看到压缩率和压缩比这两个术语，仔细一看，发现两者竟然是完全相反的概念，虽然常常看到资料中并没有明确区分。</p>
<p>数据压缩比，data compression ratio。例如：预测帧比关键帧具有更高的压缩比。 <span class="math display">\[
{\rm {Compression\;Ratio}}={\frac {\rm {Uncompressed\;Size}}{\rm {Compressed\;Size}}}
\]</span> 注意，维基百科中只提到了数据压缩比。</p>
<p>而压缩率是在百度百科中找到的（虽然从翻译角度两者意思似乎一致？）。</p>
<p>压缩率（Compression rate），描述压缩文件的效果名，是文件压缩后的大小与压缩前的大小之比。即：</p>
<p><span class="math display">\[
{\rm {压缩率}}={\frac {\rm {压缩后大小}}{\rm {原始大小}}}
\]</span></p>
<p>刚好相反？！这，怎么使用还得见仁见智吧。。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Data_compression_ratio">Data compression ratio - Wikipedia</a></li>
<li><a href="https://baike.baidu.com/item/%E5%8E%8B%E7%BC%A9%E7%8E%87/6435712">压缩率_百度百科</a></li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>直播优化</title>
    <url>/posts/live_optimization/</url>
    <content><![CDATA[<h3 id="秒开优化">1. 秒开优化</h3>
<p>改写播放器逻辑让播放器拿到第一个关键帧后就给予显示。GOP的第一帧通常都是关键帧，由于加载的数据较少，可以达到“首帧秒开”。如果直播服务器支持GOP缓存，意味着播放器在和服务器建立连接后可立即拿到数据，从而省却跨地域和跨运营商的回源传输时间。</p>
<p>GOP体现了关键帧的周期，也就是两个关键帧之间的距离，即一个帧组的最大帧数。假设一个视频的恒定帧率是24fps（即1秒24帧图像），关键帧周期为2s，那么一个GOP就是48张图像。一般而言，每一秒视频至少需要使用一个关键帧。</p>
<p>增加关键帧个数可改善画质（GOP通常为FPS的倍数），但是同时增加了带宽和网络负载。这意味着，客户端播放器下载一个GOP，毕竟该GOP存在一定的数据体积，如果播放端网络不佳，有可能不是能够快速在秒级以内下载完该GOP，进而影响观感体验。</p>
<h3 id="马赛克卡顿">2. 马赛克、卡顿</h3>
<p>如果GOP分组中的P帧丢失会造成解码端的图像发生错误,其实这个错误表现出来的就是马赛克。因为中间连续的运动信息丢失了，H.264在解码的时候会根据前面的参考帧来补齐，但是补齐的并不是真正的运动变化后的数据，这样就会出现颜色色差的问题，这就是所谓的马赛克现象，如图：</p>
<figure>
<img src="https://upload-images.jianshu.io/upload_images/2971276-e1ffc3bb3132aa73.png" alt="img" /><figcaption aria-hidden="true">img</figcaption>
</figure>
<p>这种现象不是我们想看到的。为了避免这类问题的发生，一般如果发现P帧或者I帧丢失，就不显示本GOP内的所有帧，直到下一个I帧来后重新刷新图像。但是I帧是按照帧周期来的，需要一个比较长的时间周期，如果在下一个I帧来之前不显示后来的图像，那么视频就静止不动了，这就是出现了所谓的卡顿现象。如果连续丢失的视频帧太多造成解码器无帧可解，也会造成严重的卡顿现象。视频解码端的卡顿现象和马赛克现象都是因为丢帧引起的，最好的办法就是让帧尽量不丢。</p>
<h3 id="传输协议优化">3. 传输协议优化</h3>
<ul>
<li>在服务端节点和节点之间尽量使用 RTMP 而非基于 HTTP 的 HLS 协议进行传输，这样可以降低整体的传输延迟。这个主要针对终端用户使用 HLS 进行播放的情况。</li>
<li>如果终端用户使用 RTMP 来播放，尽量在靠近推流端的收流节点进行转码，这样传输的视频流比原始视频流更小。</li>
<li>如果有必要，可以使用定制的 UDP 协议来替换 TCP 协议，省去弱网环节下的丢包重传可以降低延迟。它的主要缺点在于，基于 UDP 协议进行定制的协议的视频流的传输和分发不够通用，CDN 厂商支持的是标准的传输协议。另一个缺点在于可能出现丢包导致的花屏或者模糊（缺少关键帧的解码参考），这就要求协议定制方在 UDP 基础之上做好丢包控制。</li>
</ul>
<h3 id="传输网络优化">4. 传输网络优化</h3>
<ul>
<li>在服务端节点中缓存当前 GOP，配合播放器端优化视频首开时间。</li>
<li>服务端实时记录每个视频流流向每个环节时的秒级帧率和码率，实时监控码率和帧率的波动。</li>
<li>客户端（推流和播放）通过查询服务端准实时获取当前最优节点（5 秒一次），准实时下线当前故障节点和线路。</li>
</ul>
<h3 id="推流播放优化">5. 推流、播放优化</h3>
<ul>
<li>考察发送端系统自带的网络 buffer 大小，系统可能在发送数据之前缓存数据，这个参数的调优也需要找到一个平衡点。</li>
<li>播放端缓存控制对于视频的首开延迟也有较大影响，如果仅优化首开延迟，可以在 0 缓存情况下在数据到达的时候立即解码。但如果在弱网环境下为了消除网络抖动造成的影响，设置一定的缓存也有必要，因此需要在直播的稳定性和首开延迟优化上找到平衡，调整优化缓冲区大小这个值。</li>
<li>播放端动态 buffer 策略，这是上面播放端缓存控制的改进版本。如果只是做 0 缓存和固定大小的缓存之间进行选择找到平衡，最终还是会选择一个固定大小的缓存，这对亿级的移动互联网终端用户来说并不公平，他们不同的网络状况决定了这个固定大小的缓存并不完全合适。因此，我们可以考虑一种「动态 buffer 策略」，在播放器开启的时候采用非常小甚至 0 缓存的策略，通过对下载首片视频的耗时来决定下一个时间片的缓存大小，同时在播放过程中实时监测当前网络，实时调整播放过程中缓存的大小。这样即可做到极低的首开时间，又可能够尽量消除网络抖动造成的影响。</li>
<li>动态码率播放策略。除了动态调整 buffer 大小的策略之外，也可以利用实时监测的网络信息来动态调整播放过程中的码率，在网络带宽不足的情况下降低码率进行播放，减少延迟。</li>
</ul>
]]></content>
      <categories>
        <category>音视频概念</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FFmpeg基本概念</title>
    <url>/posts/ffmpeg_basic/</url>
    <content><![CDATA[<h2 id="模块结构">模块结构</h2>
<ul>
<li>libavformat：实现在流协议，容器格式及其本地IO访问。多媒体格式解析、解封装、封装。</li>
<li>libavutil：简化编程的工具函数库。包括随机数生成器，数据结构，数学函数，多媒体核心工具函数等等。</li>
<li>libavcodec：各种编解码器的封装。自身不做编解码，编解码器是通过插件插入的。</li>
<li>libavdevice：输入/输出设备接口封装。</li>
<li>libavfilter：音视频的后期处理。</li>
<li>libswresample：实现混音和重采样。</li>
<li>libswscale：用于执行高性能的图像缩放，颜色空间或像素格式转换的库。</li>
</ul>
<h2 id="ffmpeg处理音视频流程">FFmpeg处理音视频流程</h2>
<pre class="mermaid">flowchart LR
输入文件
--demuxer-->编码数据包0
--decoder-->解码后数据帧
--encoder-->编码数据包1
--muxer-->输出文件</pre>
<p>分解器把输入的文件（一般为容器媒体），分解为多路流，这些流都是编码数据包。把编码数据包传给解码器（如果选择流拷贝则跳过），解码器产生未压缩的帧（原始视频、PCM音频等）。通过滤镜进一步处理。然后帧被传递到编码器，编码然后输出编码数据包。最后，这些数据传给复用器，把编码的数据写入输出文件。</p>
<p>处理流数据的基本步骤：</p>
<pre class="mermaid">flowchart LR
解复用 --> 获取流 --> 读数据包 --> 释放资源</pre>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FFmpeg常用命令</title>
    <url>/posts/ffmpeg_command/</url>
    <content><![CDATA[<p>下载已编译的静态库：https://evermeet.cx/ffmpeg/</p>
<h2 id="命令分类">命令分类</h2>
<ul>
<li>基本信息查询</li>
<li>录制</li>
<li>分解、复用</li>
<li>处理原始数据</li>
<li>裁剪与合并</li>
<li>图片/视频互转</li>
<li>直播</li>
<li>滤镜</li>
</ul>
<h2 id="基本信息查询">基本信息查询</h2>
<p>版本信息：</p>
<ul>
<li><code>-version</code></li>
</ul>
<p>支持的分解、复用：</p>
<ul>
<li><code>-demuxers</code></li>
<li><code>-muxers</code></li>
</ul>
<p>支持的设备：</p>
<ul>
<li><code>-devices</code></li>
</ul>
<p>支持的（libavcodec已知的）编解码器：</p>
<ul>
<li><code>-codecs</code></li>
<li><code>-decoders</code></li>
<li><code>-encoders</code></li>
</ul>
<p>libavfilter支持的码流滤镜：</p>
<ul>
<li><code>-bsfs</code></li>
</ul>
<p>支持的格式：</p>
<ul>
<li><code>-formats</code>，支持的文件格式</li>
<li><code>-protocols</code>，网络协议</li>
<li><code>-pix_fmts</code>，像素格式</li>
<li><code>-sample_fmts</code>，采样格式</li>
<li><code>-layouts</code>，声道布局</li>
</ul>
<p>支持的滤镜：</p>
<ul>
<li><code>-filters</code></li>
</ul>
<p>支持的颜色名称：</p>
<ul>
<li><code>-colors</code></li>
</ul>
<h2 id="命令基本格式与参数">命令基本格式与参数</h2>
<p>ffmpeg命令基本格式：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">ffmpeg [全局参数] &#123;[输入文件参数] -i 输入文件URL&#125; ...\</span><br><span class="line">       &#123;[输出文件参数] 输出文件URL&#125; ...</span><br></pre></td></tr></table></figure>
<p>默认情况下，FFmpeg只包含输入文件中每种媒体类型（音频、视频、字幕）的一个流，并将其添加到每个输出文件中。它根据一下规则选择每种媒体类型中的流：</p>
<ul>
<li>视频，选择最高分辨率的流。</li>
<li>音频，选择声道最多的流。</li>
<li>字幕，选择第一个流。</li>
</ul>
<p>当然上述规则相等，则优先选取最低索引值的流。</p>
<p>可以通过<code>-vn</code>、<code>-an</code>、<code>-sn</code>、<code>-dn</code>来禁用某些媒体类型。要进行全面的手动控制，则使用<code>-map</code>选项。</p>
<p>ffmpeg通过<code>-i</code>选项读取任意数量的输入（可以是文件、管道、网络流、抓取设备等），并写入任意数量的输出。原则上，每个输入/输出都可以包含任意数量的不同类型的媒体流（视频、音频、字幕、附件/数据）。流的数量和/或类型是由容器格式来限制，选择从哪个输入进来到哪个输出将自动完成，如需控制则使用<code>-map</code>选项。</p>
<p>要引用选项中的输入，必须使用索引（从0开始）。例如，第一个输入是0。文件中的媒体流也可以通过索引引用，如<code>2:3</code>是指第三个输入中的第四个流。</p>
<h3 id="主要参数">主要参数</h3>
<ul>
<li><code>-f fmt</code>，输入/输出强制的文件格式。格式通常可以通过扩展名中猜测出来，因此不常使用。</li>
<li><code>-i url</code>，输入URL。</li>
<li><code>-y</code>，全局参数，覆盖输出文件而不询问。</li>
<li><code>-n</code>，全局参数，不覆盖输出文件，如果存在指定文件则退出。</li>
<li><code>-c [:stream_specifier] codec</code>、<code>-codec [:stream_specifier] codec</code>，输入、输出、单个流，选择一个解码器（在输入文件之前使用）或编码器（在输出文件前使用），可用于一个或多个流。传递编码器名称或<code>copy</code>（仅输出）表示不重新编码。</li>
<li><code>-t duration</code>，输入、输出，当用作输入选项（在<code>-i</code>之前），表示限制从输入文件读取的数据的时长；当用作输出选项时（在输出url之前），表示在到达时长之后停止输出。</li>
<li><code>-ss position</code>，输入、输出，当用作输入选项（在<code>-i</code>之前），表示在输入文件中寻找位置。注意，在大多数格式中，不可能精确搜索，因此ffmpeg将在位置之前寻找最近的点。当转码和<code>-accureate_seek</code>被启用时（默认），搜索点和指定的位置之间的额外分段将被解码和丢弃。当进行流式复制或使用<code>-noaccureate_seek</code>时，它将被保留。当用作输出选项（在输出url之前），解码但丢弃输入，知道时间戳到达指定的位置。</li>
<li><code>frames [:stream_specifier] framecount</code>，输出、单个流，停止在给定帧数量后写入流。</li>
<li><code>filter [:stream_specifier] filtergraph</code>，输出、单个流，创建由filtergraph指定的滤镜链图，并将其进行处理流。filtergraph的流必须具有相同类型的单个输入和单个输出。在filtergraph中，输入与标签相关联，标签与输出相关联。</li>
</ul>
<h3 id="视频参数">视频参数</h3>
<ul>
<li><code>-vframes num</code>，输出，设置要输出的视频帧数量。</li>
<li><code>-r [:stream_specifier] fps</code>，输入、输出、单个流，设置帧率（单位Hz）。
<ul>
<li>作为输入选项，忽略存储文件中的让和时间戳，根据速率生成新的时间戳。这与用于<code>-framerate</code>选项不同（旧版是相同的）。</li>
<li>作为输出选项，复制或丢弃输入帧以实现很定输出帧率。</li>
</ul></li>
<li><code>-s [:stream_specifier] size</code>，输入、输出、单个流，设置窗口大小。
<ul>
<li>作为输入选项，与<code>video_size</code>相同，由某些分帧器识别，其帧尺寸未存储在文件中。</li>
<li>作为输出选项，这将会把缩放视频滤镜传输到相应的滤镜链图末尾。请直接使用比例滤镜插入滤镜链图开头或其他地方。格式是<code>宽x高</code>。</li>
</ul></li>
<li><code>-aspect [:stream_specifier] ratio</code>，输出、单个流，指定视频的宽高比。值可以是浮点数字也可以是<code>w:h</code>字符串，如<code>4:3</code>、<code>16:9</code>。如果与<code>-vcodec</code>副本一起使用，会影响存储在容器级别的宽高比，但不会影响存储在编码帧中的宽高比（如果存在的话）。</li>
<li><code>-vn</code>，输出，禁用视频轨道。</li>
<li><code>-vf filtergraph</code>，输出，创建由filtergraph指定的滤镜链图，并使用它来处理流。</li>
</ul>
<h3 id="音频参数">音频参数</h3>
<ul>
<li><code>-aframes num</code>，输出，设置输出音频的帧数。</li>
<li><code>-ar [:stream_specifier] freq</code>，输入、输出、单个流，设置音频采样率。对于输入流，默认设置为输入流的采样率。对于输出流，该选项仅适用于音频设备采集和原始分路器，并映射到相应的分路器选件。</li>
<li><code>-an</code>，输出，禁用音频轨道。</li>
<li>-acodec name，输入、输出，设置音频编解码器。</li>
<li><code>-sample_fmt [:stream_specifier] fmt</code>，输出、单个流，设置音频采样格式。使用<code>-sample_fmts</code>可以获得支持的采样格式列表。</li>
<li><code>-af filtergraph</code>，输出，创建由filtergraph指定的滤镜链图，并用它来处理音频。</li>
</ul>
<h2 id="录制">录制</h2>
<p>录制屏幕：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 录制纯视频</span></span><br><span class="line">ffmpeg -f avfoundation -i 1 -r 30 out.yuv</span><br><span class="line"><span class="comment"># 播放纯视频</span></span><br><span class="line">ffplay -s 3360x2100 -pix_fmt uyvy422 out.yuv</span><br><span class="line"></span><br><span class="line"><span class="comment"># 录制音视频</span></span><br><span class="line">ffmpeg -f avfoundation -i 1:0 -r 29.97 </span><br><span class="line">       -c:v libx264 -crf 0</span><br><span class="line">       -c:a libfdk_aac -profile:a aac_he_v2 -b:a 32k</span><br><span class="line">       out.flv</span><br></pre></td></tr></table></figure>
<ul>
<li><code>-f</code>，指定使用avfoundation进行采集。</li>
<li><code>-i</code>，指定输入索引。</li>
<li><code>-r</code>，指定帧率。</li>
<li><code>-crf</code>，x264参数，0表示无损压缩。</li>
<li><code>-b:a</code>，指定音频码率。</li>
</ul>
<p>录制后，会输出录制的格式，后面进行播放要传入这些信息才能正确播放。</p>
<p>列出支持的设备：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ffmpeg -f avfoundation -list_devices <span class="literal">true</span> -i <span class="string">&quot;&quot;</span></span><br><span class="line">[AVFoundation indev @ 0x7fa856d0c600] AVFoundation video devices:</span><br><span class="line">[AVFoundation indev @ 0x7fa856d0c600] [0] FaceTime高清摄像头（内建）</span><br><span class="line">[AVFoundation indev @ 0x7fa856d0c600] [1] Capture screen 0</span><br><span class="line">[AVFoundation indev @ 0x7fa856d0c600] [2] Capture screen 1</span><br><span class="line">[AVFoundation indev @ 0x7fa856d0c600] AVFoundation audio devices:</span><br><span class="line">[AVFoundation indev @ 0x7fa856d0c600] [0] Built-in Microphone</span><br></pre></td></tr></table></figure>
<p>摄像头录制：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ffmpeg -framerate 30 -f avfoundation -i 0:0 out.mp4</span><br></pre></td></tr></table></figure>
<p>录制音频：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 录制</span></span><br><span class="line">ffmpeg -f avfoundation -i :0 output.wav</span><br><span class="line"><span class="comment"># 播放，由于存成wav，所以已经带上了音频的格式，直接播放即可。</span></span><br><span class="line">ffplay output.wav</span><br></pre></td></tr></table></figure>
<h2 id="分解与复用">分解与复用</h2>
<p>对容器内的数据重新组装，当然这样做的前提是对容器媒体文件进行分解形成编码数据包，完成重新组装后还需要进行重新封装。整个过程不对编码数据包做修改，只是换了个马甲。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 换媒体容器</span></span><br><span class="line">ffmpeg -i local.mp4 -vcodec copy -acodec copy local_output.mov</span><br><span class="line">ffmpeg -i local.mp4 -vcodec copy -acodec copy local_output.mkv</span><br><span class="line"></span><br><span class="line"><span class="comment"># 抽取视频，当然h264包含了SPS、PPS，可以直接播放</span></span><br><span class="line">ffmpeg -i local.mp4 -vcodec copy -an local_output.h264</span><br><span class="line"></span><br><span class="line"><span class="comment"># 抽取音频</span></span><br><span class="line">ffmpeg -i input.mp4 -acodec copy -vn out.aac</span><br></pre></td></tr></table></figure>
<p>这里面的关键是<code>codec copy</code>，即忽略指定流的编解码步骤，但同时功能也会受限（例如不能使用滤镜，因为滤镜是作用于未压缩的数据），只能进行多路分解和多路复用。对更改容器格式或修改容器级元数据很有用。</p>
<p>整个过程非常快，且没有质量损失。</p>
<h2 id="处理原始数据">处理原始数据</h2>
<p>这里的原始数据是指解码后的数据。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 提取YUV数据，关键是使用 -c:v rawvideo</span></span><br><span class="line">ffmpeg -i input.mp4 -an -c:v rawvideo -pix_fmt yuv420p out.yuv</span><br><span class="line"><span class="comment"># 播放YUV</span></span><br><span class="line">ffplay -s 1280x720 -pix_fmt yuv420p out.yuv</span><br><span class="line"></span><br><span class="line"><span class="comment"># 提取PCM数据</span></span><br><span class="line">ffmpeg -i local.mp4 -vn -ar 44100 -ac 2 -f s16le out.pcm</span><br><span class="line"><span class="comment"># 播放，PCM是不带元数据的，所以播放时需要执行音频格式</span></span><br><span class="line">ffplay -ar 44100 -ac 2 -f s16le out.pcm</span><br></pre></td></tr></table></figure>
<ul>
<li><code>-c:v</code>，指定的是使用视频编码器，把它换成<code>-vcodec</code>也是同样的效果。</li>
<li><code>-pix_fmt</code>，指定像素格式。</li>
<li><code>-ar</code>，音频采样率。</li>
<li><code>-ac</code>，声道数。</li>
<li><code>-f</code>，数据存储格式。
<ul>
<li><code>s16le</code>表示sign的16位小端模式整数。</li>
</ul></li>
</ul>
<p>注意这里的<code>-vcodec</code>一定要选择<code>rawvideo</code>，而不是<code>copy</code>；类似的，音频则直接不设置<code>-acodec</code>，而是直接接音频的参数。否则它们输出的结果都不是原始数据。</p>
<h2 id="滤镜">滤镜</h2>
<p>使用简单的视频画幅裁剪滤镜：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ffmpeg -i input.mov</span><br><span class="line">       -vf crop=in_w-200:in_h-200</span><br><span class="line">       -c:v libx264 -c:a copy out.mp4</span><br></pre></td></tr></table></figure>
<ul>
<li><code>-vf</code>，视频简单滤镜。
<ul>
<li><code>crop</code>滤镜名称，后面等号接滤镜参数，<code>in_w</code>、<code>in_h</code>引用了原视频的宽高，并使用冒号拼接参数。</li>
</ul></li>
</ul>
<h2 id="裁剪与合并">裁剪与合并</h2>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 裁剪</span></span><br><span class="line">ffmpeg -i input.mp4</span><br><span class="line">       -ss 00:00:00 -t 10</span><br><span class="line">       out.ts</span><br><span class="line"></span><br><span class="line"><span class="comment"># 合并，文本内容每一行必须为 `file &#x27;文件路径&#x27;`</span></span><br><span class="line">ffmpeg -f concat -i inputs.txt out.flv</span><br></pre></td></tr></table></figure>
<h2 id="图片视频互转">图片/视频互转</h2>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 视频 -&gt; 图片</span></span><br><span class="line">ffmpeg -i input.flv -r 1 -f image2 output-%3d.jpeg</span><br><span class="line"></span><br><span class="line"><span class="comment"># 图片 -&gt; 视频</span></span><br><span class="line">ffmpeg -i input-%3d.jpg out.mp4</span><br></pre></td></tr></table></figure>
<ul>
<li><code>-r</code>，fps，每秒转一张图片。</li>
<li><code>-f</code>，转换的格式。</li>
</ul>
<h2 id="直播推流拉流">直播推流/拉流</h2>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 直播推流</span></span><br><span class="line">ffmpeg -re -i input.mp4 -c copy -f flv rtmp://server/live/stream_name</span><br><span class="line"></span><br><span class="line"><span class="comment"># 拉流</span></span><br><span class="line">ffmpeg -i rtmp://server/live/stream_name -c copy dump.flv</span><br></pre></td></tr></table></figure>
<ul>
<li><code>-re</code>，减慢帧率使其与播放时的帧率保持同步。</li>
<li><code>-c copy</code>，音视频不转码。</li>
<li><code>-f</code>，指定推流的文件格式。</li>
</ul>
<p>推流的格式一定需要与拉取的格式对应上。</p>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FFmpeg基本API：常用操作</title>
    <url>/posts/ffmpeg_api/</url>
    <content><![CDATA[<p>API处理套路：</p>
<ul>
<li>方法一般返回值小于0表示失败。</li>
<li>使用上下文连接多个API。
<ul>
<li>上下文包含大量相关信息。</li>
<li>上下文一般对应的创建与释放方法，且注释里有说明，例如：open-close、alloc-free。</li>
</ul></li>
<li>要复用结构体时，调用对应的unref方法，以重置信息。</li>
<li>所有压缩包、未压缩帧操作都要循环操作。</li>
</ul>
<h2 id="日志系统">日志系统</h2>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;libavuitl/log.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 设置输出的日志等级</span></span><br><span class="line">av_log_set_level(<span class="type">int</span> level);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 打日志，参1一般为NULL，参2：AV_LOG_DEBUG等常量</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">av_log</span>	<span class="params">(<span class="type">void</span>* avcl, <span class="type">int</span> level, <span class="type">const</span> <span class="type">char</span>* fmt, ...)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 错误码转详细信息</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> av_err2str(errnum) \</span></span><br><span class="line"><span class="meta">     av_make_error_string((char[AV_ERROR_MAX_STRING_SIZE])&#123;0&#125;, AV_ERROR_MAX_STRING_SIZE, errnum)</span></span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">char</span> *<span class="title function_">av_make_error_string</span><span class="params">(<span class="type">char</span> *errbuf, <span class="type">size_t</span> errbuf_size, <span class="type">int</span> errnum)</span></span><br><span class="line">&#123;</span><br><span class="line">    av_strerror(errnum, errbuf, errbuf_size);</span><br><span class="line">    <span class="keyword">return</span> errbuf;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="流">流</h2>
<h3 id="获取流信息">获取流信息</h3>
<p>获取流信息基本是基于<code>AVFormatContext</code>进行获取的。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 检查是否支持格式</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">avformat_find_stream_info</span><span class="params">(AVFormatContext* ic, AVDictionary** options)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取指定类型的流索引，只关心前两两个参数即可</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">av_find_best_stream</span><span class="params">(AVFormatContext* ic, <span class="keyword">enum</span> AVMediaType type, <span class="type">int</span> wanted_stream_nb, <span class="type">int</span> related_stream, AVCodec** decoder_ret, <span class="type">int</span> flags)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 打印格式信息</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">av_dump_format</span><span class="params">(AVFormatContext* ic, <span class="type">int</span> index, <span class="type">const</span> <span class="type">char</span>* url, <span class="type">int</span> is_output)</span>;</span><br></pre></td></tr></table></figure>
<h3 id="基本流操作框架">基本流操作框架</h3>
<ol type="1">
<li>创建并打开输入上下文。<code>avformat_open_input</code></li>
<li>检查输入格式。<code>avformat_find_stream_info</code>、<code>av_dump_format</code></li>
<li>根据路径创建输出上下文。<code>avformat_alloc_output_context2</code></li>
<li>创建流，因为不参与编解码过程，所以拷贝编解码参数。`<code>avformat_new_stream</code>、<code>avcodec_parameters_copy</code>、<code>out_stream-&gt;codecpar-&gt;codec_tag = 0</code></li>
<li>检查输出格式。<code>av_dump_format</code></li>
<li>打开输出IO。<code>avio_open</code></li>
<li>写入头部。<code>avformat_write_header</code></li>
<li>==从输入流中读取并写入输出数据包。==</li>
<li>写入尾部。<code>av_write_trailer</code></li>
<li>释放上面创建的上下文。<code>avformat_close_input</code>、<code>avio_close</code>、<code>avformat_free_context</code></li>
</ol>
<p>流的操作按照FFmpeg的API流程大多只需要更改高亮的步骤，其余的基本是固定流程。数据包读取与写入的过程一般是这样的：</p>
<ol type="1">
<li>创建数据包结构体。<code>av_packet_alloc</code></li>
<li>循环读取帧，进入帧处理。<code>av_read_frame</code>
<ol type="1">
<li>处理数据包细节：<code>pkt.pts</code>、<code>pkt.dts</code>、<code>pkt.duration</code>、<code>pkt.pos</code></li>
<li>写入。<code>av_interleaved_write_frame</code></li>
<li>减少数据包引用。<code>av_packet_unref</code></li>
</ol></li>
<li>释放数据包结构体。<code>av_packet_unref</code></li>
</ol>
<p>要点：</p>
<h4 id="avformatcontext">AVFormatContext</h4>
<p>从输入URL得出格式信息：</p>
<ol type="1">
<li>建立输入格式上下文。<code>avformat_open_input</code></li>
<li>检查格式是否支持。<code>avformat_find_stream_info</code>、<code>av_dump_format</code></li>
</ol>
<p>从输出URL得出格式信息：</p>
<ol type="1">
<li>从输出URL猜测得出。<code>avformat_alloc_output_context2</code></li>
</ol>
<h4 id="avstream">AVStream</h4>
<p>输入流信息是输入格式上下文的信息，且是完整的：</p>
<ol type="1">
<li>找到指定格式的流索引：<code>av_find_best_stream</code></li>
<li>直接取出：<code>fmt_ctx-&gt;streams[audio_idx]</code></li>
<li>外加异步格式检查：<code>assert_condition(in_stream-&gt;codecpar-&gt;codec_type == AVMEDIA_TYPE_AUDIO, "媒体类型不匹配");</code></li>
</ol>
<p>输出流信息则是要自己创建的：</p>
<ol type="1">
<li>根据输出格式上下文创建输出流：<code>avformat_new_stream</code></li>
<li>从输入流拷贝编解码参数到输出流：<code>avcodec_parameters_copy</code></li>
</ol>
<h4 id="aviocontext">AVIOContext</h4>
<p>存在输出格式上下文中，只需要在写入前后开启和关闭即可。<code>avio_open</code>、<code>avio_close</code>。</p>
<h4 id="avpacket">AVPacket</h4>
<ol type="1">
<li>从输入格式上下文读取数据包：<code>av_read_frame</code></li>
<li>交错写入数据包到输出格式上下文：<code>av_interleaved_write_frame</code></li>
</ol>
<h3 id="应用导出音频流视频流">应用：导出音频流/视频流</h3>
<p>可以用两种方式：</p>
<ul>
<li>读取数据包，写入数据包到文件，补充文件头（这里存在要自己实现文件头的逻辑）。</li>
<li>走FFmpeg整个流程。</li>
</ul>
<p>在以上的基本流程上做修改：</p>
<ul>
<li>获取指定媒体类型，并获取输入流。
<ol type="1">
<li><code>av_find_best_stream</code> -&gt; stream_id</li>
<li>in_stream = <code>fmt_ctx-&gt;streams[stream_idx]</code></li>
</ol></li>
<li>循环读取帧，只处理stream_idx匹配的数据包。</li>
</ul>
<h3 id="应用时间裁剪">应用：时间裁剪</h3>
<ul>
<li>读取数据包之前进行跳转，并获取跳转后pts、dts。<code>av_seek_frame</code></li>
<li>读取数据包时：
<ul>
<li>减去起始的pts、dts。</li>
<li>不处理结束时间之后的数据包。<code>av_q2d(in_stream-&gt;time_base) * pts &lt;= end_time</code></li>
</ul></li>
</ul>
<h2 id="编解码">编解码</h2>
<h3 id="编码">编码</h3>
<p>基本步骤：</p>
<ol type="1">
<li>打开编码器。<code>avcodec_find_encoder_by_name</code></li>
<li>设置编码参数。须手动设置，因为没有参照的来源。</li>
<li>打开编码器。<code>avcodec_open2</code></li>
<li>编码。<code>avcodec_encode_video2</code></li>
</ol>
<p>具体步骤：</p>
<ol type="1">
<li>查询编码器，并创建编码上下文。<code>avcodec_find_encoder_by_name</code>、<code>avcodec_alloc_context3</code></li>
<li>设置编码参数。</li>
<li>打开编码器。<code>avcodec_open2</code></li>
<li>创建文件、AVFrame并把编码上下文的参数设置到AVFrame中。</li>
<li>AVFrame分配缓冲区空间。<code>av_frame_get_buffer</code></li>
<li>编码并写入数据。
<ol type="1">
<li>发送帧。<code>avcodec_send_frame</code></li>
<li>循环接收数据包，并写入文件。<code>avcodec_send_frame</code>、<code>fwrite</code></li>
</ol></li>
<li>编码空的AVFrame以刷新编码器。</li>
<li>按需写入结束码。</li>
<li>关闭文件、释放相关资源。</li>
</ol>
<p>编码细节：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">encode_to_file</span><span class="params">(AVCodecContext* enc_ctx,</span></span><br><span class="line"><span class="params">                           AVFrame* frame,</span></span><br><span class="line"><span class="params">                           AVPacket* pkt,</span></span><br><span class="line"><span class="params">                           FILE* outfile)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> ret;</span><br><span class="line">    <span class="keyword">if</span> (!!frame) &#123;</span><br><span class="line">        av_log(<span class="literal">NULL</span>, AV_LOG_INFO, <span class="string">&quot;Send frame %3&quot;</span> PRId64 <span class="string">&quot;\n&quot;</span>, frame-&gt;pts);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    ret = avcodec_send_frame(enc_ctx, frame);</span><br><span class="line">    assert_errnum(ret, <span class="string">&quot;发送编码帧失败&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (ret &gt;= <span class="number">0</span>) &#123;</span><br><span class="line">        ret = avcodec_receive_packet(enc_ctx, pkt);</span><br><span class="line">        <span class="keyword">if</span> (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) &#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        assert_errnum(ret, <span class="string">&quot;编码失败&quot;</span>);</span><br><span class="line"></span><br><span class="line">        av_log(<span class="literal">NULL</span>, AV_LOG_INFO, <span class="string">&quot;Write frame %3&quot;</span> PRId64 <span class="string">&quot; (size=%5d)\n&quot;</span>,</span><br><span class="line">               pkt-&gt;pts, pkt-&gt;size);</span><br><span class="line">        fwrite(pkt-&gt;data, <span class="number">1</span>, pkt-&gt;size, outfile);</span><br><span class="line">        av_packet_unref(pkt);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>注意：这里是直接把编码后的数据包数据直接写到文件中，并没有写头尾。即没有使用格式上下文的AVIOContext。</p>
<p>要点：</p>
<h4 id="avcodec">AVCodec</h4>
<p>通过id和名称查找，后续通过AVCodecContext进行管理。<code>avcodec_find_encoder_by_name</code></p>
<h4 id="avcodeccontext">AVCodecContext</h4>
<ol type="1">
<li>通过AVCodec创建：<code>avcodec_alloc_context3</code></li>
<li>重点在于格式配置，都设置到该上下文中。</li>
<li>打开后才能使用编解码器：<code>avcodec_open2</code></li>
<li>使用完毕后释放：<code>avcodec_free_context</code></li>
</ol>
<p>编码，两层循环：</p>
<ol type="1">
<li>给编码器上下文塞帧：<code>avcodec_send_frame</code>；</li>
<li>循环从编码器上下文获取数据包：<code>avcodec_receive_packet</code></li>
</ol>
<h4 id="avframe">AVFrame</h4>
<ul>
<li>创建与释放：<code>av_frame_alloc</code>、<code>av_frame_free</code></li>
<li>若是自己填充数据，则要先从codec上下文获取格式设置：<code>pix_fmt</code>、<code>width</code>、<code>height</code></li>
<li>填充数据前要分配空间：<code>av_frame_get_buffer</code></li>
<li>填充数据前要确认帧是否可写入：<code>av_frame_make_writable</code></li>
</ul>
<p>设置了格式决定了分配空间的大小以及后续填充数据的方式。</p>
<h3 id="解码">解码</h3>
<p>基本步骤：</p>
<ol type="1">
<li>查找解码器。<code>avcodec_find_decoder</code></li>
<li>从输入拷贝相关解码参数。<code>avcodec_parameters_to_context</code></li>
<li>打开解码器。<code>avcodec_open2</code></li>
<li>解码。<code>avcodec_decode_video2</code></li>
</ol>
<p>具体步骤：</p>
<ol type="1">
<li>打开文件。<code>avformat_open_input</code></li>
<li>检查格式，获取视频流。<code>avformat_find_stream_info</code>、<code>av_dump_format</code>、<code>av_find_best_stream</code></li>
<li>根据读取的流信息查询编解码器并创建对应上下文。<code>avcodec_find_decoder</code>、<code>avcodec_alloc_context3</code></li>
<li>从读取的流信息中拷贝相关的编解码器参数。<code>avcodec_parameters_to_context</code></li>
<li>打开编解码器。<code>avcodec_open2</code></li>
<li>若要转换图像格式，则创建<code>SwsContext</code>。</li>
<li>循环读取帧。<code>av_read_frame</code></li>
<li>循环解码。
<ol type="1">
<li>发送数据包。<code>avcodec_send_packet</code></li>
<li>循环获得解码帧。<code>avcodec_receive_frame</code></li>
</ol></li>
</ol>
<p>解码细节：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">decode</span><span class="params">(AVCodecContext *dec_ctx, AVFrame *frame, AVPacket *pkt,</span></span><br><span class="line"><span class="params">                   <span class="type">const</span> <span class="type">char</span> *filename)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> ret;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 发送数据包</span></span><br><span class="line">    ret = avcodec_send_packet(dec_ctx, pkt);</span><br><span class="line">    <span class="keyword">if</span> (ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;Error sending a packet for decoding\n&quot;</span>);</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (ret &gt;= <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">// 获得解码帧</span></span><br><span class="line">        ret = avcodec_receive_frame(dec_ctx, frame);</span><br><span class="line">        <span class="keyword">if</span> (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;Error during decoding\n&quot;</span>);</span><br><span class="line">            <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;saving frame %3d\n&quot;</span>, dec_ctx-&gt;frame_number);</span><br><span class="line">        fflush(<span class="built_in">stdout</span>);</span><br><span class="line">		</span><br><span class="line">        <span class="comment">// 帧处理业务</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>与编码不同，解码的参数是由之前编码的决定的，所以这里直接把读取的流中拷贝编码器参数。</p>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FFmpeg基本API：源码分析</title>
    <url>/posts/ffmpeg_api_source_code_analysis/</url>
    <content><![CDATA[<h2 id="查找编解码器">查找编解码器</h2>
<p><code>avcodec_find_decoder</code>和<code>avcodec_find_encoder</code> 主要是查找 FFmpeg 的解码器和编码器。</p>
<p>avcodec_find_decoder 和 avcodec_find_encoder 主要是利用 AVCodecID 来查找编解码器。 其实质是遍历AVCodec 链表并且获得符合AVCodecID的元素。</p>
<h2 id="初始化io上下文">初始化IO上下文</h2>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">avio_open2</span><span class="params">(AVIOContext **s, <span class="type">const</span> <span class="type">char</span> *url, <span class="type">int</span> flags,</span></span><br><span class="line"><span class="params">               <span class="type">const</span>  AVIOInterruptCB *int_cb, AVDictionary **options)</span>;</span><br></pre></td></tr></table></figure>
<p>avio_open2 主要实现创建并初始化一个 AVIOContext，用于访问由 url 指定文件。</p>
<p>各个参数的含义如下：</p>
<ul>
<li><code>AVIOContext **s</code>：函数调用成功后，创建并初始化该<code>AVIOContext</code>结构体。</li>
<li><code>const char *url</code>：输入输出协议的地址。</li>
<li><code>int flags</code>：打开地址的方式(只读、只写、读写)。AVIO_FLAG_READ/AVIO_FLAG_WRITE/AVIO_FLAG_READ_WRITE.</li>
<li><code>const AVIOInterruptCB *int_cb</code>：调用函数。</li>
<li><code>AVDictionary **options</code>：一般为NULL。</li>
</ul>
<p>与<code>avio_open2</code>相似的还有<code>avio_open</code>函数，<code>avio_open</code>会调用<code>avio_open2</code>，并将 int_cb 和 options 设置为 NULL。</p>
<p><code>avio_open2</code>的调用函数关系如下：</p>
<figure>
<img src="http://lazybing.github.io/images/avio_open2/avio_open2.png" alt="img" /><figcaption aria-hidden="true">img</figcaption>
</figure>
<h2 id="初始化编解码上下文">初始化编解码上下文</h2>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">avcodec_open2</span><span class="params">(AVCodecContext *avctx, <span class="type">const</span> AVCodec *codec, AVDictionary **options)</span>;</span><br></pre></td></tr></table></figure>
<p><code>avcodec_open2</code>函数实现的功能为利用给定的<code>AVCodec</code>结构初始化<code>AVCodecContext</code>结构。</p>
<p>函数参数说明：</p>
<ul>
<li><code>avctx</code>：需要初始化的context.</li>
<li><code>codec</code></li>
<li><code>options</code></li>
<li>返回值：如果返回0，正确。失败则返回负数。</li>
</ul>
<p>该函数利用给定的<code>AVCodec</code>结构初始化<code>AVCodecContext</code>结构，在使用该函数之前，<code>AVCodecContext</code> 必须已经用<code>avcodec_alloc_context3()</code>函数分配出来。</p>
<p><code>AVCodec</code>结构在使用该函数之前，由<code>avcodec_find_decoder_by_name``avcodec_find_encoder_by_name</code> <code>avcodec_find_decoder</code>或<code>avcodec_find_encoder</code>提前得到。</p>
<p>注意，在正式解码之前(比如使用<code>avcodec_decode_video2()</code>之前)，必须调用<code>avcodec_open2</code>函数。</p>
<p><code>avcodec_open2</code>的逻辑非常简单，首先是进行一些参数检测、之后调动<code>AVCodec</code>的init函数。大概步骤如下：</p>
<ul>
<li>各种函数参数检测。</li>
<li>各种结构体分配内存。</li>
<li>将输入的<code>AVDictionary</code>形式的选项设置到<code>AVCodecContext</code>。</li>
<li>其他一些零散的检查，检查输入参数是否符合编码器的要求。</li>
<li>调用<code>AVCodec</code>的init函数初始化具体的解码器。</li>
</ul>
<p>此处重点分析调用<code>AVCodec</code>的init函数处。 以 HEVC 解码器为例。</p>
<h2 id="读取压缩数据包">读取压缩数据包</h2>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">av_read_frame</span><span class="params">(AVFormatContext *s, AVPacket *pkt)</span>;</span><br></pre></td></tr></table></figure>
<p><code>av_read_frame</code>函数的作用是返回文件中保存的数据。它会文件中保存的数据分成不同的帧， 每次调用都会返回一帧。注意，该函数不会忽略帧与帧之间无效数据(非帧数据)，目的是给解码器 最多的信息用于解码。</p>
<p>如果<code>pkt-&gt;buf</code>是 NULL，包直到下一次调用<code>av_read_frame</code>或<code>avformat_close_input</code>时都是有效的。 不需要时，包必须通过<code>av_free_packet</code>释放。对于视频，<code>packet</code>只包含一帧；对于音频，如果每帧有固定大小(如 PCM 或 ADPCM 数据)， <code>packet</code>可以包含多个音频帧（必须是整数帧），如果音频帧大小可变(如MPEG 音频)，它只能包含一帧数据。</p>
<p><code>pkt-&gt;pts</code>、<code>pkt-&gt;dts</code>、<code>pkt-&gt;duration</code>都是以<code>AVStream.time_base_units</code>为单位的。 如果视频格式里包含 B 帧，<code>pkt-&gt;pts</code>可以是<code>AV_NOPTS_VALUE</code>，因此如果不解压缩数据，最好查看<code>pkt-&gt;dts</code>。</p>
<p>如果函数返回0，正确；小于0，则为到文件尾或出错。</p>
<p>函数调用关系：</p>
<figure>
<img src="http://lazybing.github.io/images/av_read_frame/av_read_frame.png" alt="img" /><figcaption aria-hidden="true">img</figcaption>
</figure>
<p><code>av_read_frame</code>函数会判断在未解码缓存中是否有数据，如果有数据则调用<code>read_from_packet_buffer</code>。</p>
<h2 id="提取流信息">提取流信息</h2>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">avformat_find_stream_info</span><span class="params">(AVFormatContext *ic, AVDictionary **options)</span>;</span><br></pre></td></tr></table></figure>
<p><code>avformat_find_stream_info</code>主要是读媒体文件的包(packets)，然后从中提取出流的信息。 对于没有头部信息的文件格式尤其有用，比如<code>MPEG</code>。文件的逻辑位置不会被改变，读取出来 的包会被缓存起来供以后处理。</p>
<p>返回值：&gt;=0–&gt;OK,或出错返回AVERROR_xxx</p>
<p>注意，该函数并不保证能够打开所有的 codec，因此将options 设置为非NULL用于返回一些信息是非常好的行为。</p>
<p>调用关系：</p>
<figure>
<img src="http://lazybing.github.io/images/avformat_find_stream_info/avformat_find_stream_info.png" alt="img" /><figcaption aria-hidden="true">img</figcaption>
</figure>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FFmpeg新老API</title>
    <url>/posts/ffmpeg_api_new_deprecated/</url>
    <content><![CDATA[<p>变化：</p>
<ul>
<li>不需要要调用注册方法。</li>
<li>简化了流程。</li>
<li>语义更准确。</li>
</ul>
<h3 id="avcodec_decode_video2">avcodec_decode_video2</h3>
<p>原本的解码函数被拆解为两个函数<code>avcodec_send_packet()</code>和<code>avcodec_receive_frame()</code>具体用法如下：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// old:</span></span><br><span class="line">avcodec_decode_video2(pCodecCtx, pFrame, &amp;got_picture, pPacket);</span><br><span class="line"></span><br><span class="line"><span class="comment">// new:</span></span><br><span class="line">avcodec_send_packet(pCodecCtx, pPacket);</span><br><span class="line">avcodec_receive_frame(pCodecCtx, pFrame);</span><br></pre></td></tr></table></figure>
<h3 id="codec_encode_video2">codec_encode_video2</h3>
<p>对应的编码函数也被拆分为两个函数<code>avcodec_send_frame()</code>和<code>avcodec_receive_packet()</code>具体用法如下：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// old:</span></span><br><span class="line">avcodec_encode_video2(pCodecCtx, pPacket, pFrame, &amp;got_picture);</span><br><span class="line"></span><br><span class="line"><span class="comment">// new:</span></span><br><span class="line">avcodec_send_frame(pCodecCtx, pFrame);</span><br><span class="line">avcodec_receive_packet(pCodecCtx, pPacket);</span><br></pre></td></tr></table></figure>
<h3 id="avpicture_get_size">avpicture_get_size</h3>
<p>现在改为使用av_image_get_size() 具体用法如下：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// old:</span></span><br><span class="line">avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx-&gt;width, pCodecCtx-&gt;height);</span><br><span class="line"></span><br><span class="line"><span class="comment">// new:</span></span><br><span class="line"><span class="comment">// 最后一个参数align这里是置1的，具体看情况是否需要置1</span></span><br><span class="line">av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx-&gt;width, pCodecCtx-&gt;height, <span class="number">1</span>);</span><br></pre></td></tr></table></figure>
<h3 id="avpicture_fill">avpicture_fill</h3>
<p>现在改为使用av_image_fill_arrays 具体用法如下：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// old:</span></span><br><span class="line">avpicture_fill((AVPicture *)pFrame, buffer, AV_PIX_FMT_YUV420P, pCodecCtx-&gt;width, pCodecCtx-&gt;height);</span><br><span class="line"></span><br><span class="line"><span class="comment">// new:</span></span><br><span class="line"><span class="comment">// 最后一个参数align这里是置1的，具体看情况是否需要置1</span></span><br><span class="line">av_image_fill_arrays(pFrame-&gt;data, pFrame-&gt;linesize, buffer, AV_PIX_FMT_YUV420P,  pCodecCtx-&gt;width, pCodecCtx-&gt;height,<span class="number">1</span>);</span><br></pre></td></tr></table></figure>
<h3 id="codec">codec</h3>
<p>关于codec问题有的可以直接改为codecpar，但有的时候这样这样是不对的，所以我也还在探索，这里记录一个对pCodecCtx和pCodec赋值方式的改变</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// old:</span></span><br><span class="line">pCodecCtx = pFormatCtx-&gt;streams[video_index]-&gt;codec;</span><br><span class="line">pCodec = avcodec_find_decoder(pFormatCtx-&gt;streams[video_index]-&gt;codec-&gt;codec_id);</span><br><span class="line"></span><br><span class="line"><span class="comment">// new:</span></span><br><span class="line"><span class="comment">// 把参数从AVCodecParameters拷贝到AVCodecContext</span></span><br><span class="line">pCodecCtx = avcodec_alloc_context3(<span class="literal">NULL</span>);</span><br><span class="line">avcodec_parameters_to_context(pCodecCtx,pFormatCtx-&gt;streams[video_index]-&gt;codecpar);</span><br><span class="line">pCodec    = avcodec_find_decoder(pCodecCtx-&gt;codec_id);</span><br></pre></td></tr></table></figure>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FFmpeg基本API：重要结构体</title>
    <url>/posts/ffmpeg_api_struct/</url>
    <content><![CDATA[<p>关键的结构体可分成以下几类：</p>
<p><strong>解协议（http、rtsp、rtmp、mms）</strong></p>
<p><code>AVIOContext</code>、<code>URLProtocol</code>、<code>URLContext</code>主要存储音视频使用的协议类型以及状态。</p>
<p><code>URLProtocol</code>存储输入音视频使用的封装格式。每种协议都会有对应的<code>URLProtocol</code>结构体，文件也不例外。</p>
<p><strong>解封装（flv、avi、rmvb、mp4）</strong></p>
<p><code>AVFormatContext</code>主要存储音视频封装格式中的包含的信息。</p>
<p><code>AVInputFormat</code>存储输入音视频使用的封装格式，每种音视频封装格式都对应一个<code>AVInputFormat</code>结构体。</p>
<p><strong>解码（h264、mpeg2、aac、mp3）</strong></p>
<p>每个<code>AVStream</code>存储一个音频流/视频流的相关数据。</p>
<p>每个<code>AVStream</code>对应一个<code>AVCodecContext</code>，存储该流使用的解码方式的相关数据。</p>
<p>每个<code>AVCodecContext</code>对应一个<code>AVCodec</code>，包含流对应的解码器器。每种解码器对应一个<code>AVCodec</code>结构体。</p>
<p><strong>存数据</strong></p>
<p>解码前的数据结构：<code>AVPacket</code>；解码后的数据结构：<code>AVFrame</code>。每个结构体有一帧或多帧。</p>
<p>关系：</p>
<figure>
<img src="https://img-blog.csdn.net/20130914204051125?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGVpeGlhb2h1YTEwMjA=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="img" /><figcaption aria-hidden="true">img</figcaption>
</figure>
<h2 id="文件操作">文件操作</h2>
<p><code>&lt;libavformat/avio.h&gt;</code></p>
<h3 id="aviodircontext">AVIODirContext</h3>
<p>操作目录上下文，承载目录信息。</p>
<h3 id="aviodirentry">AVIODirEntry</h3>
<p>目录内容项，承载文件/目录详细信息。用于存放文件名、文件大小等信息。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;libavformat/avio.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 删除文件</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">avpriv_io_delete</span><span class="params">(<span class="type">const</span> <span class="type">char</span>* url)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 移动或重命名</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">avpriv_io_move</span><span class="params">(<span class="type">const</span> <span class="type">char</span>* url_src, <span class="type">const</span> <span class="type">char</span>* url_dst)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 打开目录，会分配AVIODirContext</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">avio_open_dir</span><span class="params">(AVIODirContext** s, <span class="type">const</span> <span class="type">char</span>* url, AVDictionary** options)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 读取目录，结果输出到AVIODirEntry</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">avio_read_dir</span><span class="params">(AVIODirContext* s, AVIODirEntry** next)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 关闭目录（释放资源）</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">avio_close_dir</span><span class="params">(AVIODirContext** s)</span>;</span><br></pre></td></tr></table></figure>
<h2 id="数据包操作">数据包操作</h2>
<p><code>&lt;libavformat/avformat.h&gt;</code></p>
<h3 id="avformatcontext">AVFormatContext</h3>
<p>统领全局的基本结构体。主要用于处理封装格式。</p>
<ul>
<li><code>struct AVInputFormat *iformat</code>：输入数据的封装格式，由<code>avformat_open_input</code>设置，仅仅在解封装时使用。</li>
<li><code>struct AVOutputFormat *oformat</code>：输出数据的封装格式，必须由使用者在<code>avformat_write_header</code>前设置，由封装时使用。</li>
<li><code>priv_data</code>：格式私有数据。在封装中，由<code>avformat_write_header</code>设置；在解封装中，由<code>avformat_open_input</code>设置。</li>
<li><code>AVIOContext *pb</code>：输入输出上下文。如果<code>iformat/oformat.flags</code>设置为<code>AVFMT_NOFILE</code>的话，该字段不需要设置。对于解封装，需要在<code>avformat_open_input</code>前设置，或由<code>avformat_open_input</code>设置；对于封装，在<code>avformat_write_header</code>前设置。</li>
<li><code>ctx_flags</code>：码流的信息，表明码流属性的的信号。由<code>libavformat</code>设置，例如<code>AVFMTCTX_NOHEADER</code>。</li>
<li><code>nb_streams</code>：指<code>AVFormatContext.streams</code>的数量，必须由<code>avformat_new_stream</code>设置，不能由其他代码改动。</li>
<li><code>AVStream **streams</code>：文件中所有码流的列表，新的码流创建使用<code>avformat_new_stream</code>函数。解封装中，码流由<code>avformat_open_input</code>创建。 如果<code>AVFMTCTX_NOHEADER</code>被设置，新的码流可以出现在<code>av_read_frame</code>中。封装中，码流在<code>avformat_write_header</code>之前由用户创建。它的释放是由<code>avformat_free_context</code>完成的。</li>
<li><code>filename</code>：输入或输出的文件名，解封装中由<code>avformat_open_input</code>设置，封装中在使用<code>avformat_write_header</code>前由调用者设置。</li>
<li><code>int64_t duration</code>：码流的时长。</li>
<li><code>bit_rate</code>：比特率。</li>
<li><code>enum AVCodecID video_codec_id</code></li>
<li><code>AVDictionary *metadata</code>：元数据，适用于整个文件。</li>
</ul>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 创建与销毁</span></span><br><span class="line">AVFormatContext* <span class="title function_">avformat_alloc_context</span><span class="params">()</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">avformat_free_context</span><span class="params">(AVFormatContext* s)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/// 其他创建方式</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 打开现有的媒体文件</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">avformat_open_input</span><span class="params">(AVFormatContext **ps, <span class="type">const</span> <span class="type">char</span> *url, AVInputFormat *fmt, AVDictionary **options)</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">avformat_close_input</span><span class="params">(AVFormatContext* s)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 根据路径猜测并创建。其内部为avformat_alloc_context+av_guess_format+赋值</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">avformat_alloc_output_context2</span><span class="params">(AVFormatContext **ctx, AVOutputFormat *oformat, <span class="type">const</span> <span class="type">char</span> *format_name, <span class="type">const</span> <span class="type">char</span> *filename)</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">avformat_free_context</span><span class="params">(AVFormatContext *s)</span>；</span><br></pre></td></tr></table></figure>
<h3 id="avstream">AVStream</h3>
<p>流/轨信息（不包含数据）。</p>
<ul>
<li><code>index</code>：在AVFormatContext的流索引。</li>
<li><del><code>AVCodecContext *codec</code></del>：已被弃用，改成<code>AVCodecParameters *codecpar</code>：编解码信息。</li>
<li><code>AVRational time_base</code>：时间单位。</li>
<li><code>duration</code>：流长度。</li>
<li><code>AVDictionary *metadata</code>：元数据信息。</li>
<li><code>AVRational avg_frame_rate</code>：平均帧率。</li>
</ul>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 创建与销毁</span></span><br><span class="line">AVStream* <span class="title function_">avformat_new_stream</span><span class="params">(AVFormatContext* s, <span class="type">const</span> AVCodec* c)</span>;</span><br><span class="line"><span class="comment">// 随avformat_free_context一起销毁</span></span><br></pre></td></tr></table></figure>
<h3 id="aviocontext">AVIOContext</h3>
<p>输入输出对应的结构体。</p>
<ul>
<li><code>unsigned char *buffer</code>：缓存开始位置。</li>
<li><code>buffer_size</code>：缓存大小。</li>
<li><code>unsigned char *buf_ptr</code>：当前指针读取到的位置。</li>
<li><code>unsigned char *buf_end</code>：缓存结束的位置。</li>
</ul>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 创建与销毁</span></span><br><span class="line">AVIOContext* <span class="title function_">avio_alloc_context</span><span class="params">(</span></span><br><span class="line"><span class="params">    <span class="type">unsigned</span> <span class="type">char</span>* buffer, <span class="type">int</span> buffer_size,</span></span><br><span class="line"><span class="params">    <span class="type">int</span> write_flag, <span class="type">void</span>* opaque,</span></span><br><span class="line"><span class="params">    <span class="type">int</span>(*)(<span class="type">void</span> *opaque, <span class="type">uint8_t</span> *buf, <span class="type">int</span> buf_size) read_packet,</span></span><br><span class="line"><span class="params">    <span class="type">int</span>(*)(<span class="type">void</span> *opaque, <span class="type">uint8_t</span> *buf, <span class="type">int</span> buf_size) write_packet,</span></span><br><span class="line"><span class="params">    <span class="type">int64_t</span>(*)(<span class="type">void</span> *opaque, <span class="type">int64_t</span> offset, <span class="type">int</span> whence) seek</span></span><br><span class="line"><span class="params">)</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">avio_context_free</span><span class="params">(AVIOContext** s)</span>;</span><br></pre></td></tr></table></figure>
<h3 id="avpacket">AVPacket</h3>
<p>压缩数据包，压缩域结构体，一个或多个压缩数据帧。这是流操作中的核心数据。</p>
<p>对于视频数据，只包含一帧压缩数据；对于音频数据，可能包含多帧压缩数据。</p>
<p>定义：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">AVPacket</span>&#123;</span></span><br><span class="line">	AVBufferRef *buf;</span><br><span class="line">	<span class="type">int64_t</span>      pts;</span><br><span class="line">	<span class="type">int64_t</span>      dts;</span><br><span class="line">	<span class="type">uint8_t</span>    *data;</span><br><span class="line">	<span class="type">int</span>         size;</span><br><span class="line">	<span class="type">int</span> stream_index;</span><br><span class="line">	<span class="type">int</span>        flags;</span><br><span class="line">	AVPacketSideData *side_data;</span><br><span class="line">	<span class="type">int</span> side_data_elems;</span><br><span class="line">	<span class="type">int</span>   duration;</span><br><span class="line">	<span class="type">int64_t</span> pos;</span><br><span class="line">	<span class="type">int64_t</span> convergence_duration;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<ul>
<li><code>pts</code>：显示时间戳，它的单位是 <code>AVStream-&gt;time_base</code>；如果在文件中没有保存这个值，它被设置为 <code>AV_NOPTS_VALUE</code>。由于图像显示不可能早于图像解压，因此 PTS 必须比 DTS（解码时间戳）大或者相等。某些文件格式中可能会使用 PTS/DTS 表示其他含义，此时时间戳必须转为真正的时间戳才能保存到 AVPacket 结构中。</li>
<li><code>dts</code>：解码时间戳，它的单位是 <code>AVStream-&gt;time_base</code>，表示压缩视频解码的时间，如果文件中没有保存该值，它被设置为 <code>AV_NOPTS_VALUE</code>。</li>
<li><code>data</code>：指向真正的压缩编码的数据。</li>
<li><code>size</code>：表示该 AVPacket 结构中 data 字段所指向的压缩数据的大小。</li>
<li><code>stream_index</code>：标识该 AVPacket 结构所属的视频流或音频流。</li>
<li><code>duration</code>：该 AVPacket 包以 <code>AVStream-&gt;time_base</code> 为单位，所持续的时间，0 表示未知，或者为显示时间戳的差值(next_pts - this pts)。</li>
<li><code>pos</code>：表示该 AVPacket 数据在媒体中的位置，即字节偏移量。</li>
</ul>
<p>直接创建：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 可在栈或堆中创建</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 类似的，如果AVPacket是在堆创建的，则要配合使用这两个API</span></span><br><span class="line">AVPacket *<span class="title function_">av_packet_alloc</span><span class="params">()</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">av_packet_free</span><span class="params">(AVPacket** pkt)</span>;</span><br></pre></td></tr></table></figure>
<h4 id="填充方式一av_read_frame">填充方式一：av_read_frame</h4>
<p>通过AVFormatContext可读取一般的媒体容器文件。</p>
<p>这里的读取数据包的方法命名为<code>av_read_frame</code>只是历史遗留问题，以前的数据包也是用frame命名的，后面才改了过来。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 每次读取帧后，要对应减引用计数。</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">av_read_frame</span><span class="params">(AVFormatContext* s, AVPacket *pkt)</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">av_packet_unref</span><span class="params">(AVPacket* pkt)</span>;</span><br></pre></td></tr></table></figure>
<h4 id="填充方式二av_parser_parse2">填充方式二：av_parser_parse2</h4>
<p>通过AVCodecParserContext、AVCodecContext解析buffer，得出数据包的主要信息。只能针对音视频裸流进行解析。该方法只是解析，即还要借助其他API读取获得buffer。</p>
<p>AVCodecParser用于解析输入的数据流并把它们分成一帧一帧的压缩编码数据。比较形象的说法就是把长长的一段连续的数据“切割”成一段段的数据。 <code>av_parser_parse2()</code>：解析数据获得一个Packet， 从输入的数据流中分离出一帧一帧的压缩编码数据。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">av_parser_parse2</span><span class="params">(AVCodecParserContext *s, AVCodecContext *avctx, <span class="type">uint8_t</span> **poutbuf, <span class="type">int</span> *poutbuf_size, <span class="type">const</span> <span class="type">uint8_t</span> *buf, <span class="type">int</span> buf_size, <span class="type">int64_t</span> pts, <span class="type">int64_t</span> dts, <span class="type">int64_t</span> pos)</span></span><br></pre></td></tr></table></figure>
<p>由于传入的buffer可以有多个数据包，所以需要循环读取：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">while</span> (data_size &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment">// 返回以解析的大小，若小于buffer的大小，则会再次进行循环解析</span></span><br><span class="line">    ret = av_parser_parse2(parser, c, &amp;pkt-&gt;data, &amp;pkt-&gt;size,</span><br><span class="line">                           data, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, <span class="number">0</span>);</span><br><span class="line">    <span class="keyword">if</span> (ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;Error while parsing\n&quot;</span>);</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    data      += ret;</span><br><span class="line">    data_size -= ret;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (pkt-&gt;size) &#123;</span><br><span class="line">        <span class="comment">// 这时获得一个合法的数据包</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="编解码">编解码</h2>
<p><code>&lt;libavcodec/avcodec.h&gt;</code></p>
<h3 id="avcodec">AVCodec</h3>
<p>编码器结构体，通过它转换AVFrame/AVPacket。</p>
<p>定义：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">AVCodec</span>&#123;</span></span><br><span class="line">	<span class="type">const</span> <span class="type">char</span> *name;</span><br><span class="line">    <span class="type">const</span> <span class="type">char</span> *long_name;</span><br><span class="line">    <span class="class"><span class="keyword">enum</span> <span class="title">AVMediaType</span> <span class="title">type</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">enum</span> <span class="title">AVCodecID</span> <span class="title">id</span>;</span></span><br><span class="line">    <span class="type">int</span> capabilities;</span><br><span class="line">    <span class="type">const</span> AVRational *supported_framerates; <span class="comment">///&lt; array of supported framerates, or NULL if any, array is terminated by &#123;0,0&#125;</span></span><br><span class="line">    <span class="type">const</span> <span class="class"><span class="keyword">enum</span> <span class="title">AVPixelFormat</span> *<span class="title">pix_fmts</span>;</span>     <span class="comment">///&lt; array of supported pixel formats, or NULL if unknown, array is terminated by -1</span></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> *supported_samplerates;       <span class="comment">///&lt; array of supported audio samplerates, or NULL if unknown, array is terminated by 0</span></span><br><span class="line">    <span class="type">const</span> <span class="class"><span class="keyword">enum</span> <span class="title">AVSampleFormat</span> *<span class="title">sample_fmts</span>;</span> <span class="comment">///&lt; array of supported sample formats, or NULL if unknown, array is terminated by -1</span></span><br><span class="line">    <span class="type">const</span> <span class="type">uint64_t</span> *channel_layouts;         <span class="comment">///&lt; array of support channel layouts, or NULL if unknown. array is terminated by 0</span></span><br><span class="line">    <span class="type">uint8_t</span> max_lowres;                     <span class="comment">///&lt; maximum value for lowres supported by the decoder, no direct access, use av_codec_get_max_lowres()</span></span><br><span class="line">    <span class="type">const</span> AVClass *priv_class;              <span class="comment">///&lt; AVClass for the private context</span></span><br><span class="line">    <span class="type">const</span> AVProfile *profiles;              <span class="comment">///&lt; array of recognized profiles, or NULL if unknown, array is terminated by &#123;FF_PROFILE_UNKNOWN&#125;</span></span><br><span class="line">    <span class="type">int</span> priv_data_size;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">AVCodec</span> *<span class="title">next</span>;</span></span><br><span class="line">    <span class="type">int</span> (*init_thread_copy)(AVCodecContext *);</span><br><span class="line">    <span class="type">int</span> (*update_thread_context)(AVCodecContext *dst, <span class="type">const</span> AVCodecContext *src);</span><br><span class="line">    <span class="type">const</span> AVCodecDefault *defaults;</span><br><span class="line">    <span class="type">void</span> (*init_static_data)(<span class="keyword">struct</span> AVCodec *codec);</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> (*init)(AVCodecContext *);</span><br><span class="line">    <span class="type">int</span> (*encode_sub)(AVCodecContext *, <span class="type">uint8_t</span> *buf, <span class="type">int</span> buf_size,</span><br><span class="line">    <span class="type">int</span> (*encode2)(AVCodecContext *avctx, AVPacket *avpkt, <span class="type">const</span> AVFrame *frame,</span><br><span class="line">                   <span class="type">int</span> *got_packet_ptr);</span><br><span class="line">    <span class="type">int</span> (*decode)(AVCodecContext *, <span class="type">void</span> *outdata, <span class="type">int</span> *outdata_size, AVPacket *avpkt);</span><br><span class="line">    <span class="type">int</span> (*close)(AVCodecContext *);</span><br><span class="line">    <span class="type">void</span> (*flush)(AVCodecContext *);</span><br><span class="line">    <span class="type">int</span> caps_internal;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<ul>
<li><code>name</code>：具体的 CODEC 的名称的简短描述，比如“HEVC”、“H264”等。</li>
<li><code>long_name：CODEC</code> 名称的详细描述，比如“HEVC (High Efficiency Video Coding)”。</li>
<li><code>id</code>：唯一标识的 CODEC 类型，比如 AV_CODEC_ID_HEVC。</li>
<li><code>type</code>：媒体类型的字段，它是 enum 型的，表示视频、音频、字幕等，比如<code>AVMEDIA_TYPE_VIDEO</code>、<code>AVMEIDA_TYPE_AUDIO</code>。</li>
<li><code>supported_framerates</code>：支持的视频帧率的数组，以{0，0}作为结束。</li>
<li><code>pix_fmts</code>：编解码器支持的图像格式的数组，以 -1 作为结束。</li>
<li><code>profiles</code>：编解码器支持的 Profile，以 HEVC 为例，包含“Main”、“Main10”、“Main Still Picture”。</li>
</ul>
<p>每一个编解码器对应一个 AVCodec 结构体，对应一种编解码方式，比如 HEVC、AVC、MPEG2、MPEG4、VP6、VP8、VP9等。以 HEVC 为例，FFMpeg中关于 AVCodec 的定义如下：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line">AVCodec ff_hevc_decoder = &#123;</span><br><span class="line">    .name                  = <span class="string">&quot;hevc&quot;</span>,</span><br><span class="line">    .long_name             = NULL_IF_CONFIG_SMALL(<span class="string">&quot;HEVC (High Efficiency Video Coding)&quot;</span>),</span><br><span class="line">    .type                  = AVMEDIA_TYPE_VIDEO,</span><br><span class="line">    .id                    = AV_CODEC_ID_HEVC,</span><br><span class="line">    .priv_data_size        = <span class="keyword">sizeof</span>(HEVCContext),</span><br><span class="line">    .priv_class            = &amp;hevc_decoder_class,</span><br><span class="line">    .init                  = hevc_decode_init,</span><br><span class="line">    .close                 = hevc_decode_free,</span><br><span class="line">    .decode                = hevc_decode_frame,</span><br><span class="line">    .flush                 = hevc_decode_flush,</span><br><span class="line">    .update_thread_context = hevc_update_thread_context,</span><br><span class="line">    .init_thread_copy      = hevc_init_thread_copy,</span><br><span class="line">    .capabilities          = AV_CODEC_CAP_DR1 | AV_CODEC_CAP_DELAY |</span><br><span class="line">                             AV_CODEC_CAP_SLICE_THREADS | AV_CODEC_CAP_FRAME_THREADS,</span><br><span class="line">    .profiles              = NULL_IF_CONFIG_SMALL(profiles),</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>AVCodec通常用法：</p>
<ol type="1">
<li>根据特定ID找到特定的编解码器；</li>
<li>根据特定编解码器分配出特定的描述编解码上下文的 AVCodecContext 结构体；</li>
<li>打开编解码器；</li>
<li>调用编解码器进行编解码。</li>
</ol>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line">AVCodec *codec = <span class="literal">NULL</span>;</span><br><span class="line">AVCodecContext *ctx = <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">codec = avcodec_find_decoder(origin_ctx-&gt;codec_id);</span><br><span class="line">ctx = avcodec_alloc_context3(codec);</span><br><span class="line">avcodec_open2(ctx, codec, <span class="literal">NULL</span>);</span><br><span class="line">...</span><br></pre></td></tr></table></figure>
<h3 id="avcodeccontext">AVCodecContext</h3>
<p>编解码上下文。连接编解码各个过程。最复杂的结构体，里面定义的变量有些是编码时候用到，有些是解码时候用到。</p>
<ul>
<li><code>codec_type</code>：编解码器的类型，如音频、视频、字幕。</li>
<li><code>AVCdec *codec</code>：编解码器对象。</li>
<li><code>bit_rate</code>：平均比特率。</li>
<li><code>width</code>、<code>height</code>：视频的宽高。</li>
<li><code>refs</code>：运动估计参考帧的个数。</li>
<li><code>sample_rate</code>：采样率。</li>
<li><code>channels</code>：声道数。</li>
</ul>
<p>AVCodecContext 使用 <code>avcodec_alloc_context3</code> 分配，该函数除了分配 AVCodecContext 外，还会初始化默认的字段。分配的内存必须通过 <code>avcodec_free_context</code> 释放。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 创建与销毁</span></span><br><span class="line">AVCodecContext** <span class="title function_">avcodec_alloc_context3</span><span class="params">(<span class="type">const</span> AVCodec* codec)</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">avcodec_free_context</span><span class="params">(AVCodecContext** avctx)</span>;</span><br></pre></td></tr></table></figure>
<h3 id="avframe">AVFrame</h3>
<p>未压缩数据帧，像素域结构体，（视频对应RGB/YUV像素数据，音频对应PCM采样数据）。</p>
<p>定义：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">AVFrame</span>&#123;</span></span><br><span class="line">	<span class="type">uint8_t</span> *data[AV_NUM_DATA_POINTERS];</span><br><span class="line">	<span class="type">int</span> linesize[AV_NUM_DATA_POINTERS];</span><br><span class="line">	<span class="type">uint8_t</span> **extended_data;</span><br><span class="line">	<span class="type">int</span> width, height;</span><br><span class="line">	<span class="type">int</span> nb_samples; <span class="comment">/* number of audio samples(per channel) described by this frame */</span></span><br><span class="line">	<span class="type">int</span> format;</span><br><span class="line">	<span class="type">int</span> key_frame; <span class="comment">/* 1-&gt;keyframe, 0-&gt;not*/</span></span><br><span class="line">	<span class="class"><span class="keyword">enum</span> <span class="title">AVPictureType</span> <span class="title">pict_type</span>;</span></span><br><span class="line">	AVRational sample_aspect_ratio;</span><br><span class="line">	<span class="type">int64_t</span> pts;</span><br><span class="line">	<span class="type">int64_t</span> pkt_pts;</span><br><span class="line">	<span class="type">int64_t</span> pkt_dts;</span><br><span class="line">	<span class="type">int</span> coded_picture_number;</span><br><span class="line">	<span class="type">int</span> display_picture_number;</span><br><span class="line">	<span class="type">int</span> quality;</span><br><span class="line">	<span class="type">void</span> *opaque; <span class="comment">/* for some private data of the user */</span></span><br><span class="line">	<span class="type">uint64_t</span> error[AV_NUM_DATA_POINTERS];</span><br><span class="line">	<span class="type">int</span> repeat_pict;</span><br><span class="line">	<span class="type">int</span> interlaced_frame;</span><br><span class="line">	<span class="type">int</span> top_field_first;	<span class="comment">/* If the content is interlaced, is top field displayed first */</span></span><br><span class="line">	<span class="type">int</span> palette_has_changed;</span><br><span class="line">    <span class="type">int64_t</span> reordered_opaque;</span><br><span class="line">    <span class="type">int</span> sample_rate;    <span class="comment">/*Sample rate of the audio data*/</span></span><br><span class="line">    <span class="type">uint64_t</span> channel_layout; <span class="comment">/*channel layout of the audio data*/</span></span><br><span class="line">    AVBufferRef *buf[AV_NUM_DATA_POINTERS];</span><br><span class="line">    AVBufferRef **extended_buf;</span><br><span class="line">    <span class="type">int</span> nb_exteneded_buf;</span><br><span class="line">    AVFrameSideData **side_data;</span><br><span class="line">    <span class="type">int</span> nb_side_data;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> flags;</span><br><span class="line">    <span class="class"><span class="keyword">enum</span> <span class="title">AVColorRange</span> <span class="title">color_range</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">enum</span> <span class="title">AVColorPrimaries</span> <span class="title">color_primaries</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">enum</span> <span class="title">AVColorTransferCharacteristic</span> <span class="title">color_trc</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">enum</span> <span class="title">AVColorSpace</span> <span class="title">colorspace</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">enum</span> <span class="title">AVChromaLocation</span> <span class="title">chroma_location</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="type">int64_t</span> best_effort_timestamp;</span><br><span class="line">    <span class="type">int64_t</span> pkt_pos;</span><br><span class="line">    <span class="type">int64_t</span> pkt_duration;</span><br><span class="line">    AVDictionary *metadata;</span><br><span class="line">    <span class="type">int</span> decode _error_flags;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> channels;</span><br><span class="line">    <span class="type">int</span> pkt_size;</span><br><span class="line">    AVBufferRef *qp_table_buf;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<ul>
<li><code>data</code>：指向图片或信道的指针，与初始化时分配的大小可能不同，一些解码器取数据范围超出(0,0)-(width, height)，具体请查看<code>avcodec_align_dimensions2()</code>方法。一些过滤器或扫描器读数据时可能会超过 16 字节，所以当它们使用时，必须额外分配 16 字节。对于 packed 格式的数据(例如 RGB24)，会存放到 data[0] 里面；对于 planar 格式的数据(例如 YUV420P)，则会分开 data[0]/data[1]/data[2]（YUV420P 中 data[0] 存放 Y，data[1] 存放 U，data[2] 存放 V）。</li>
<li><code>linesize</code>：对于视频数据，表示每个图像行的字节大小；对于音频数据，表示每个 Plane 的字节大小，只有linesize[0]可以设置，对于plane 音频，每个信道 channel 必须是相同的。对于视频的 linesize 应为 CPU 的对准要求的倍数，一般为 32 的倍数。注意 linesize 可大于可用的数据的尺寸，有可能存在由于性能原因额外填充。</li>
<li><code>width</code>/<code>height</code>：视频的宽高。</li>
<li><code>format</code>：帧格式，-1表示未设置的帧格式。对于视频帧，该值为 enum 类型的 AVPixelFormat，例如 <code>AV_PIX_FMT_YUV420P</code>；对于音频帧，该值为 enum 型的 AVSampleFormat，例如 <code>AV_SAMPLE_FMT_S16</code>。</li>
<li><code>key_frame</code>：关键帧，1 表示关键帧，0 表示非关键帧。</li>
<li><code>pict_type</code>：帧图片类型，例如 I/P/B。</li>
<li><code>sample_aspect_ration</code>：帧像素的宽高比，使用 AVRational 表示。</li>
<li><code>pts</code>：显示时间戳，单位为 <code>time_base</code>。</li>
<li><code>pkt_pts</code>：该 PTS 是从 AVPacket 结构中拷贝过来的；与之对应的是<code>pkt_dts</code>。</li>
<li><code>coded_picture_number</code>/<code>display_picture_number</code>：解码序列号和显示序列号（Display Order/Decoded Order）。</li>
<li><code>interlaced_frame</code>：表示该帧为隔行（interlace）码流或者为逐行（progressive）码流。</li>
<li><code>top_field_first</code>：对于隔行码流，表示该它是 top first or bottom first。</li>
</ul>
<p>AVFrame 结构体通常只需分配一次，之后即可通过保存不同的数据来重复多次使用，比如一个 AVFrame 结构可以保存从解码器中解码出的多帧数据。此时，就可以使用<code>av_frame_unref()</code>释放任何由 Frame 保存的参考帧并还原回最原始的状态。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 创建与销毁</span></span><br><span class="line">AVFrame* <span class="title function_">av_frame_alloc</span><span class="params">()</span>;</span><br><span class="line"><span class="type">void</span> <span class="title function_">av_frame_free</span><span class="params">(AVFrame** frame)</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 填充空间</span></span><br><span class="line">av_image_fill_arrays(</span><br><span class="line">    <span class="type">uint8_t</span>* dst_data[<span class="number">4</span>], <span class="type">int</span> dst_linesize[<span class="number">4</span>], <span class="type">const</span> <span class="type">uint8_t</span>* src,</span><br><span class="line">    <span class="keyword">enum</span> AVPixelFormat pix_fmt, <span class="type">int</span> width, <span class="type">int</span> height, <span class="type">int</span> align</span><br><span class="line">);</span><br></pre></td></tr></table></figure>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FFmpeg编译</title>
    <url>/posts/ffmpeg_compilation/</url>
    <content><![CDATA[<p>这里讨论的是使用源码方式编译按照，而不是使用brew安装（后期不可裁剪）。</p>
<h2 id="编译依赖准备">编译依赖准备</h2>
<ul>
<li>gas-preprocessor。Perl 脚本，将 GNU 的一个子集实现为 Apple 没有的预处理</li>
<li>yams
<ul>
<li><code>brew install yasm</code>安装</li>
</ul></li>
<li>pkg-config</li>
</ul>
<h2 id="基本编译命令">基本编译命令</h2>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 生成配置，执行后生成一些配置文件。</span></span><br><span class="line">./configure --prefix=/opt/ffmpeg --enable-debug=3 --disable-static --enable-shared --enable-libfdk_aac --enable-nonfree --enable-libopus --enable-libvpx --enable-libx264 --enable-gpl --enable-libx265 </span><br><span class="line"><span class="comment"># 设定使用CPU数量</span></span><br><span class="line">make -j 4</span><br><span class="line"><span class="comment"># 编译</span></span><br><span class="line">sudo make install</span><br></pre></td></tr></table></figure>
<p>编译配置：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 指定编译结果根目录，因为要装到/目录，所以才需要sudo</span></span><br><span class="line">--prefix=/opt/ffmpeg</span><br><span class="line"><span class="comment"># 调试时可以更详细地输出符号信息</span></span><br><span class="line">--enable-debug=3</span><br><span class="line"><span class="comment"># 关闭静态库而使用动态库，默认生成静态库</span></span><br><span class="line">--disable-static --enable-shared</span><br></pre></td></tr></table></figure>
<p>prefix参数目前设在一个共享目录，生成的动态库是共享用的，即不是随app的。另一方面，这个参数也是直接设置了dyld的install name，直接拷贝包会导致运行时找不到包。</p>
<p>似乎编解码都没有启用，所以都要一一启用。</p>
<p>要启用的编解码器不在FFmpeg代码中，因此启用编码器时，也不会下载，需要额外用brew安装。</p>
<p>使用这种方式安装意味着app要想到其他机器运行，也需要配置一样的环境。</p>
<h3 id="ios平台编译">iOS平台编译</h3>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">./ffmpeg-4.3.2/configure \</span><br><span class="line">--target-os=darwin \</span><br><span class="line">--<span class="built_in">arch</span>=arm64 \</span><br><span class="line">--cc=<span class="string">&quot;xcrun -sdk iphoneos clang&quot;</span> \</span><br><span class="line">--as=<span class="string">&quot;gas-preprocessor.pl -arch aarch64 -- xcrun -sdk iphoneos clang&quot;</span> \</span><br><span class="line">--enable-cross-compile --disable-debug --disable-programs \</span><br><span class="line">--disable-doc --enable-pic \</span><br><span class="line">--extra-cflags=<span class="string">&quot;-arch arm64 -mios-version-min=8.0 -fembed-bitcode&quot;</span> \</span><br><span class="line">--extra-ldflags=<span class="string">&quot;-arch arm64 -mios-version-min=8.0 -fembed-bitcode&quot;</span> \</span><br><span class="line">--prefix=/Users/bq/Workspace/Git/FFmpeg/BuildScript/kewlbear/FFmpeg-iOS-build-script/thin/arm64</span><br></pre></td></tr></table></figure>
<h2 id="编译后的目录">编译后的目录</h2>
<ul>
<li>bin｜所有命令工具</li>
<li>include｜头文件</li>
<li>lib｜生成的动态库、静态库</li>
<li>share｜文档、例子</li>
</ul>
<h2 id="pkg-config">pkg-config</h2>
<p>查找系统库路径：pkg-config --libs libavformat</p>
<p>如果没找到，首先确认编译后的lib目录下是否有pkgconfig目录，然后添加全局变量：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">export</span> PKG_CONFIG_PATH=/opt/ffmpeg/lib/pkgconfig/:<span class="variable">$PKG_CONFIG_PATH</span></span><br></pre></td></tr></table></figure>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FFmpeg音视频同步</title>
    <url>/posts/ffmpeg_audio_and_video_synchronization/</url>
    <content><![CDATA[<h3 id="dtspts">DTS、PTS</h3>
<p>FFmpeg中获取PTS：</p>
<ul>
<li>AVPacket中</li>
<li>AVFrame中（其获取PTS的av_frame_get_best_effort_timestamp已经弃用且不需要使用）</li>
</ul>
<h3 id="时间基">时间基</h3>
<p>相关计算公式：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 时间戳转秒</span></span><br><span class="line">time_in_seconds = pts * av_q2d(time_base)</span><br><span class="line"><span class="comment">// 秒转时间戳</span></span><br><span class="line">timestamp = timebase * time_in_seconds</span><br></pre></td></tr></table></figure>
<p>FFmpeg内部的时间基：<code>AV_TIME_BASE</code></p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 定义</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span>         AV_TIME_BASE   1000000</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 分数形式</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span>         AV_TIME_BASE_Q   (AVRational)&#123;1, AV_TIME_BASE&#125;</span></span><br></pre></td></tr></table></figure>
<p>相关定义与转换函数。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 表示时间基的结构体</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">AVRational</span>&#123;</span></span><br><span class="line">    <span class="type">int</span> num; <span class="comment">//numerator</span></span><br><span class="line">    <span class="type">int</span> den; <span class="comment">//denominator</span></span><br><span class="line">&#125; AVRational;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 转换成分数形式</span></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">double</span> <span class="title function_">av_q2d</span><span class="params">(AVRational a)</span>｛</span><br><span class="line">    <span class="keyword">return</span> a.num / <span class="params">(<span class="type">double</span>)</span> a.den;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 转换时间基。把a的时间戳从bq时间基转换到cq时间基。就是简单地a * bq / cq</span></span><br><span class="line"><span class="type">int64_t</span> <span class="title function_">av_rescale_q</span><span class="params">(<span class="type">int64_t</span> a, AVRational bq, AVRational cq)</span>;</span><br></pre></td></tr></table></figure>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FFmpeg实战 H264</title>
    <url>/posts/ffmpeg_coding_h264/</url>
    <content><![CDATA[<p>基本步骤：</p>
<ol type="1">
<li>打开编码器，设置编码器参数；</li>
<li>转换图像格式，NV12-&gt;YUV420P；</li>
<li>准备编码数据AVFrame；</li>
<li>进行编码；</li>
</ol>
<p>重点是前三步。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">alloc_data_for_encode</span><span class="params">(AVFrame **in_frame, AVPacket **out_pkt)</span> &#123;</span><br><span class="line">    <span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 定义编码器输入</span></span><br><span class="line">    AVFrame *frame = <span class="literal">NULL</span>;</span><br><span class="line">    <span class="comment">// 创建</span></span><br><span class="line">    frame = av_frame_alloc();</span><br><span class="line">    <span class="keyword">if</span> (!frame) &#123;</span><br><span class="line">        log_error(ret);</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;Error, No Memory!\n&quot;</span>);</span><br><span class="line">        <span class="keyword">goto</span> __ERROR;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 配置输入参数</span></span><br><span class="line">    frame-&gt;width = kWidth;</span><br><span class="line">    frame-&gt;height = kHeight;</span><br><span class="line">    frame-&gt;format = in_fmt;</span><br><span class="line">    ret = av_frame_get_buffer(frame, <span class="number">32</span>); <span class="comment">// 按32位对齐</span></span><br><span class="line">    <span class="keyword">if</span> (!frame-&gt;buf[<span class="number">0</span>]) &#123;</span><br><span class="line">        log_error(ret);</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;无法读取输入数据\n&quot;</span>);</span><br><span class="line">        <span class="keyword">goto</span> __ERROR;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    *in_frame = frame;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 创建编码器输出</span></span><br><span class="line">    AVPacket *newpkt = av_packet_alloc();</span><br><span class="line">    <span class="keyword">if</span> (!newpkt) &#123;</span><br><span class="line">        log_error(ret);</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;无法创建输出数据\n&quot;</span>);</span><br><span class="line">        <span class="keyword">goto</span> __ERROR;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    *out_pkt = newpkt;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// !!!: 一定要在标签前返回</span></span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">    </span><br><span class="line">    __ERROR:</span><br><span class="line">    <span class="keyword">if</span> (frame) av_frame_free(&amp;frame);</span><br><span class="line">    <span class="keyword">if</span> (newpkt) av_packet_free(&amp;newpkt);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">int</span></span><br><span class="line"><span class="title function_">encode_frame</span><span class="params">(AVFrame *frame, AVPacket *newpkt, FILE *output_file)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (frame) <span class="built_in">printf</span>(<span class="string">&quot;send frame to encoder. pts=%lld\n&quot;</span>, frame-&gt;pts);</span><br><span class="line">    </span><br><span class="line">    <span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line">    <span class="comment">// 输入frame到编码器</span></span><br><span class="line">    ret = avcodec_send_frame(c_ctx, frame);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">while</span> (ret &gt;= <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">// 获取编码后的数据，如果成功，则重复获取</span></span><br><span class="line">        ret = avcodec_receive_packet(c_ctx, newpkt);</span><br><span class="line">        <span class="keyword">if</span> (ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) &#123;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="built_in">printf</span>(<span class="string">&quot;error in encoding audio frame\n&quot;</span>);</span><br><span class="line">                <span class="keyword">goto</span> __ERROR;</span><br><span class="line">                <span class="comment">//exit(-1);</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 写入文件</span></span><br><span class="line">        fwrite(newpkt-&gt;data, <span class="number">1</span>, (<span class="type">size_t</span>)newpkt-&gt;size, output_file);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">    </span><br><span class="line">    __ERROR:</span><br><span class="line">    <span class="comment">// 释放编码输入输出</span></span><br><span class="line">    <span class="keyword">if</span> (frame) av_frame_free(&amp;frame);</span><br><span class="line">    <span class="keyword">if</span> (newpkt) av_packet_free(&amp;newpkt);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">read_video</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!fmt_ctx) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;不能使用设备\n&quot;</span>);</span><br><span class="line">        <span class="keyword">goto</span> __ERROR;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/// 准备文件</span></span><br><span class="line">    <span class="comment">// 创建文件，权限：w（写入）b（写入二进制）+（若文件不存在则创建）</span></span><br><span class="line">    FILE *output_yuv = fopen(<span class="string">&quot;/Users/bq/Movies/test/video.yuv&quot;</span>, <span class="string">&quot;wb+&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (!output_yuv) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;文件创建失败\n&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    FILE *output_h264 = fopen(<span class="string">&quot;/Users/bq/Movies/test/video.h264&quot;</span>, <span class="string">&quot;wb+&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (!output_h264) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;文件创建失败\n&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 使用栈空间分配AVPacket</span></span><br><span class="line">    AVPacket pkt;</span><br><span class="line">    av_init_packet(&amp;pkt);</span><br><span class="line">    </span><br><span class="line">    <span class="type">int</span> count = <span class="number">0</span>;</span><br><span class="line">    <span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/// 编码输入、输出数据</span></span><br><span class="line">    AVFrame *frame = <span class="literal">NULL</span>;</span><br><span class="line">    AVPacket *newpkt = <span class="literal">NULL</span>;</span><br><span class="line">    alloc_data_for_encode(&amp;frame, &amp;newpkt);</span><br><span class="line">    </span><br><span class="line">    <span class="type">int</span> base_pts = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">while</span> ((ret == <span class="number">0</span> || ret == AVERROR(EAGAIN)) &amp;&amp; count &lt; <span class="number">100</span>) &#123;</span><br><span class="line">        <span class="comment">// 读取视频数据</span></span><br><span class="line">        ret = av_read_frame(fmt_ctx, &amp;pkt);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> (ret == AVERROR(EAGAIN)) <span class="keyword">continue</span>;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;[%d] pkt size is %d\n&quot;</span>, count, pkt.size);</span><br><span class="line">        count++;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * NV12 -&gt; YUV420</span></span><br><span class="line"><span class="comment">         * NV12:   YYYYYYYYUVUV</span></span><br><span class="line"><span class="comment">         * YUV420: YYYYYYYYUUVV</span></span><br><span class="line"><span class="comment">         * */</span></span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 拷贝数据到输入</span></span><br><span class="line">        <span class="type">const</span> <span class="type">size_t</span> y_length = kWidth * kHeight;</span><br><span class="line">        <span class="type">const</span> <span class="type">size_t</span> u_v_length = y_length / <span class="number">4</span>;</span><br><span class="line">        <span class="comment">// 拷贝Y数据</span></span><br><span class="line">        <span class="built_in">memcpy</span>(frame-&gt;data[<span class="number">0</span>], pkt.data, y_length);</span><br><span class="line">        <span class="comment">// 处理UV，Y数据后面是UV，对YUV数据分层</span></span><br><span class="line">        <span class="type">const</span> <span class="type">int</span> stride = <span class="number">2</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; u_v_length; i++) &#123;</span><br><span class="line">            <span class="type">const</span> <span class="type">size_t</span> base = y_length + i * stride;</span><br><span class="line">            frame-&gt;data[<span class="number">1</span>][i] = pkt.data[base];</span><br><span class="line">            frame-&gt;data[<span class="number">2</span>][i] = pkt.data[base + <span class="number">1</span>];</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 输出YUV</span></span><br><span class="line">        fwrite(frame-&gt;data[<span class="number">0</span>], <span class="number">1</span>, y_length, output_yuv);</span><br><span class="line">        fwrite(frame-&gt;data[<span class="number">1</span>], <span class="number">1</span>, u_v_length, output_yuv);</span><br><span class="line">        fwrite(frame-&gt;data[<span class="number">2</span>], <span class="number">1</span>, u_v_length, output_yuv);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 编码，需将pts重置为顺序的值</span></span><br><span class="line">        frame-&gt;pts = base_pts++;</span><br><span class="line">        ret = encode_frame(frame, newpkt, output_h264);</span><br><span class="line">        </span><br><span class="line">        av_packet_unref(&amp;pkt);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 再次调用一次编码，防止丢帧</span></span><br><span class="line">    encode_frame(<span class="literal">NULL</span>, newpkt, output_h264);</span><br><span class="line">    </span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;完成写入\n&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 这里不写return，会一直执行下去</span></span><br><span class="line">    __ERROR:</span><br><span class="line">    <span class="comment">// 关闭文件</span></span><br><span class="line">    fclose(output_yuv);</span><br><span class="line">    fclose(output_h264);</span><br><span class="line">    <span class="comment">// 释放编码输入输出</span></span><br><span class="line">    <span class="keyword">if</span> (frame)av_frame_free(&amp;frame);</span><br><span class="line">    <span class="keyword">if</span> (newpkt) av_packet_free(&amp;newpkt);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FFmpeg实战 YUV</title>
    <url>/posts/ffmpeg_coding_yuv/</url>
    <content><![CDATA[<p>命令生成YUV：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ffmpeg -i input.mp4 \</span><br><span class="line">       -an \</span><br><span class="line">       -c:v rawvideo \</span><br><span class="line">       -pix_fmt yuv420p out.yuv</span><br><span class="line"></span><br><span class="line"><span class="comment"># 提取各分量</span></span><br><span class="line">ffmpeg -i input.mp4 \</span><br><span class="line">       -filter_complex <span class="string">&#x27;extractplanes=y+u+v[y][u][v]&#x27;</span> \</span><br><span class="line">       -map <span class="string">&#x27;[y]&#x27;</span> y.yuv \</span><br><span class="line">       -map <span class="string">&#x27;[u]&#x27;</span> u.yuv \</span><br><span class="line">       -map <span class="string">&#x27;[v]&#x27;</span> v.yuv</span><br></pre></td></tr></table></figure>
<p>播放YUV：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ffplay -pix_fmt yuv420p -s 宽x高 out.yuv</span><br><span class="line"></span><br><span class="line"><span class="comment"># 只播放Y分量</span></span><br><span class="line">ffplay -pix_fmt yuv420p -s 宽x高 -vf extractplanes=<span class="string">&#x27;y&#x27;</span> out.yuv</span><br><span class="line"></span><br><span class="line"><span class="comment"># 播放各分量文件，y是全尺寸，u、v分量要使用一般的分辨率</span></span><br><span class="line">ffplay -pix_fmt gray -s 宽x高 out.yuv</span><br></pre></td></tr></table></figure>
<p>播放速度会不一样。</p>
<ul>
<li>c:v，视频编解码器</li>
<li>vf，简单滤镜/滤波</li>
<li>filter_complex，复杂滤波器</li>
</ul>
<h2 id="代码实现">代码实现</h2>
<p>可以在音频采集的基础上，增加：</p>
<ul>
<li>修改设备名称</li>
<li>增加参数</li>
<li>修改文件名及文件数据的大小</li>
</ul>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 打开设备</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">open_device</span><span class="params">()</span> &#123;</span><br><span class="line">    av_log_set_level(AV_LOG_DEBUG);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 小于零则出错</span></span><br><span class="line">    <span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line">    <span class="comment">// &lt;video device&gt;:&lt;audio device&gt;</span></span><br><span class="line">    <span class="comment">// 0，本机摄像头；1，桌面</span></span><br><span class="line">    <span class="type">char</span> *device_name = <span class="string">&quot;0&quot;</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 设置摄像头选项</span></span><br><span class="line">    AVDictionary *options = <span class="literal">NULL</span>;</span><br><span class="line">    av_dict_set(&amp;options, <span class="string">&quot;video_size&quot;</span>, <span class="string">&quot;640x480&quot;</span>, <span class="number">0</span>);</span><br><span class="line">    av_dict_set(&amp;options, <span class="string">&quot;framerate&quot;</span>, <span class="string">&quot;30&quot;</span>, <span class="number">0</span>);</span><br><span class="line">    av_dict_set(&amp;options, <span class="string">&quot;pixel_format&quot;</span>, <span class="string">&quot;nv12&quot;</span>, <span class="number">0</span>); <span class="comment">// FFmpeg会默认指定YUV420P，但会不支持会切换到摄像头支持的第一种格式，mac的摄像头只支持uyvy422、yuyv422、nv12、0rgb、bgr0</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1 注册设备</span></span><br><span class="line">    avdevice_register_all();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2 获取格式</span></span><br><span class="line">    AVInputFormat *inputFormat = av_find_input_format(<span class="string">&quot;avfoundation&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3 打开设备。这会同时创建上下文。</span></span><br><span class="line">    ret = avformat_open_input(&amp;fmt_ctx, device_name, inputFormat, &amp;options);</span><br><span class="line">    <span class="keyword">if</span> (ret &lt; <span class="number">0</span> || !fmt_ctx) &#123;</span><br><span class="line">        <span class="keyword">goto</span> __ERROR;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;成功打开视频设备\n&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">    __ERROR:</span><br><span class="line">    log_error(ret);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/// 采集视频并写入文件</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">read_video</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!fmt_ctx) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;不能使用设备\n&quot;</span>);</span><br><span class="line">        <span class="keyword">goto</span> __ERROR;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/// 准备文件</span></span><br><span class="line">    <span class="comment">// 创建文件，权限：w（写入）b（写入二进制）+（若文件不存在则创建）</span></span><br><span class="line">    FILE *output_yuv = fopen(<span class="string">&quot;/Users/bq/Movies/test/video.yuv&quot;</span>, <span class="string">&quot;wb+&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (!output_yuv) &#123;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;文件创建失败\n&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 使用栈空间分配AVPacket</span></span><br><span class="line">    AVPacket pkt;</span><br><span class="line">    av_init_packet(&amp;pkt);</span><br><span class="line">    </span><br><span class="line">    <span class="type">int</span> count = <span class="number">0</span>;</span><br><span class="line">    <span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">while</span> ((ret == <span class="number">0</span> || ret == AVERROR(EAGAIN)) &amp;&amp; count &lt; <span class="number">100</span>) &#123;</span><br><span class="line">        <span class="comment">// 读取视频数据</span></span><br><span class="line">        ret = av_read_frame(fmt_ctx, &amp;pkt);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> (ret == AVERROR(EAGAIN)) <span class="keyword">continue</span>;</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;[%d] pkt size is %d\n&quot;</span>, count, pkt.size);</span><br><span class="line">        count++;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * NV12 -&gt; YUV420</span></span><br><span class="line"><span class="comment">         * NV12:   YYYYYYYYUVUV</span></span><br><span class="line"><span class="comment">         * YUV420: YYYYYYYYUUVV</span></span><br><span class="line"><span class="comment">         * */</span></span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 拷贝数据到输入</span></span><br><span class="line">        <span class="type">const</span> <span class="type">size_t</span> y_length = kWidth * kHeight;</span><br><span class="line">        <span class="type">const</span> <span class="type">size_t</span> u_v_length = y_length / <span class="number">4</span>;</span><br><span class="line">        <span class="comment">// 拷贝Y数据</span></span><br><span class="line">        <span class="built_in">memcpy</span>(frame-&gt;data[<span class="number">0</span>], pkt.data, y_length);</span><br><span class="line">        <span class="comment">// 处理UV，Y数据后面是UV，对YUV数据分层</span></span><br><span class="line">        <span class="type">const</span> <span class="type">int</span> stride = <span class="number">2</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; u_v_length; i++) &#123;</span><br><span class="line">            <span class="type">const</span> <span class="type">size_t</span> base = y_length + i * stride;</span><br><span class="line">            frame-&gt;data[<span class="number">1</span>][i] = pkt.data[base];</span><br><span class="line">            frame-&gt;data[<span class="number">2</span>][i] = pkt.data[base + <span class="number">1</span>];</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 输出YUV</span></span><br><span class="line">        fwrite(frame-&gt;data[<span class="number">0</span>], <span class="number">1</span>, y_length, output_yuv);</span><br><span class="line">        fwrite(frame-&gt;data[<span class="number">1</span>], <span class="number">1</span>, u_v_length, output_yuv);</span><br><span class="line">        fwrite(frame-&gt;data[<span class="number">2</span>], <span class="number">1</span>, u_v_length, output_yuv);</span><br><span class="line">        </span><br><span class="line">        av_packet_unref(&amp;pkt);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;完成写入\n&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 这里不写return，会一直执行下去</span></span><br><span class="line">    __ERROR:</span><br><span class="line">    <span class="comment">// 关闭文件</span></span><br><span class="line">    fclose(output_yuv);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>FFmpeg实战 音频录制、编码、重采样</title>
    <url>/posts/ffmpeg_coding_audio_recording_encoding_resampling/</url>
    <content><![CDATA[<p>命令行方式采集：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ffmpeg -f avfoundation -i :0 output/out.wav</span><br></pre></td></tr></table></figure>
<h2 id="准备">准备</h2>
<p>这里创建的是Mac App，以及引入的是动态库。由于动态库存放在一个共享位置，编译时就固定了它所在的位置，所以不需要拷贝到目录中。</p>
<ol type="1">
<li>引入并链接动态库文件（General/Frameworks, Libraries, and Embedded Content））。</li>
<li>添加头文件搜索目录（Build Settings/User Header Search Paths）。</li>
<li>创建C语言头文件以及实现文件，并创建Bridging-Header。</li>
</ol>
<h2 id="采集音频">采集音频</h2>
<h3 id="打开设备">打开设备</h3>
<p>步骤：</p>
<ol type="1">
<li>注册设备。</li>
<li>设置采集方式（avfouncdation✔️/dshow/alsa）。</li>
<li>打开音频设备。</li>
</ol>
<p>打开之后就可以录制音频流。</p>
<p>必要头文件：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;libavutil/avutil.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;libavdevice/avdevice.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;libavformat/avformat.h&quot;</span></span></span><br></pre></td></tr></table></figure>
<p>记得要引入的是动态库啊，静态库会有一堆符号找不到。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 小于零则出错</span></span><br><span class="line"><span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line">AVFormatContext *context = <span class="literal">NULL</span>;</span><br><span class="line"><span class="comment">// &lt;video device&gt;:&lt;audio device&gt;</span></span><br><span class="line"><span class="type">char</span> *deviceName = <span class="string">&quot;:0&quot;</span>;</span><br><span class="line">AVDictionary *options = <span class="literal">NULL</span>;</span><br><span class="line"><span class="type">size_t</span> errorBufferLength = <span class="number">1024</span>;</span><br><span class="line"><span class="type">char</span> errorBuffer[errorBufferLength];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 1 注册设备</span></span><br><span class="line">avdevice_register_all();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2 获取格式</span></span><br><span class="line">AVInputFormat *inputFormat = av_find_input_format(<span class="string">&quot;avfoundation&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3 打开设备。这会同时创建上下文。</span></span><br><span class="line">ret = avformat_open_input(&amp;context, deviceName, inputFormat, &amp;options);</span><br><span class="line"><span class="keyword">if</span> (ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment">// 输出到错误</span></span><br><span class="line">    av_strerror(ret, errorBuffer, errorBufferLength);</span><br><span class="line">    <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;Failed to open audio device, [%d]%s\n&quot;</span>, ret, errorBuffer);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>注意要先获取麦克风权限。</p>
<p>上下文创建后，记得要对应进行释放。</p>
<h3 id="读取音频数据">读取音频数据</h3>
<p><code>av_read_frame</code>：该方法既可以读取音频数据，也可以读取视频数据。</p>
<p><code>AVFormatContext</code>：格式上下文，上面打开设备也用到。上下文是后续处理的基础，在打开设备的时候就可以获取上下文。</p>
<p>AVPacket，音视频数据包结构体。</p>
<p>返回0则表示成功。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 使用栈空间分配AVPacket</span></span><br><span class="line">AVPacket pkt;</span><br><span class="line">av_init_packet(&amp;pkt);</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> count = <span class="number">0</span>;</span><br><span class="line"><span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">while</span> ((ret == <span class="number">0</span> || ret == <span class="number">-35</span>) &amp;&amp; count &lt; <span class="number">5</span>) &#123;</span><br><span class="line">    ret = av_read_frame(context, &amp;pkt);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (ret != <span class="number">0</span>) &#123;</span><br><span class="line">        av_strerror(ret, error_buffer, kErrorLength);</span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;Failed to reading, [%d]%s\n&quot;</span>, ret, error_buffer);</span><br><span class="line">        <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;pkt size is %d\n&quot;</span>, pkt.size);</span><br><span class="line">    count++;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">av_packet_unref(&amp;pkt);</span><br></pre></td></tr></table></figure>
<p>要注意，采集时有时会出现-35返回，是设备临时不可用，需要忽略，并重试。</p>
<p>记得<code>av_read_frame</code>后要释放对应资源，避免内存泄漏。</p>
<h4 id="avpacket">AVPacket</h4>
<p>头文件：libavcodec/avcodec.h</p>
<p>重要成员</p>
<ul>
<li><code>data</code>：音视频具体数据。</li>
<li><code>size</code>：缓冲区数据大小。</li>
</ul>
<p>相关API（成对使用）：</p>
<p>如果只在栈空间使用AVPacket，则可以只用以下两个方法。</p>
<ul>
<li><code>av_init_packet</code>：AVPacket初始化方法，但不会填充<code>data</code>、<code>size</code>。</li>
<li><code>av_packet_unref</code>：释放AVPacket资源。内部会释放<code>buffer</code>的内存占用。</li>
</ul>
<p>堆空间分配：</p>
<ul>
<li><code>av_packet_alloc</code>：分配空间并初始化AVPacket，同样不会填充data、size。即包含了av_init_packet的调用。</li>
<li><code>av_packet_free</code>：释放AVPacket对应的资源，同样也包含了av_packet_unref调用。</li>
</ul>
<h3 id="写入到文件">写入到文件</h3>
<p>基本步骤：</p>
<ol type="1">
<li>创建文件；</li>
<li>把音频写入到文件中；</li>
<li>关闭文件。</li>
</ol>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 创建文件，权限：w（写入）b（写入二进制）+（若文件不存在则创建）</span></span><br><span class="line"><span class="type">char</span> *output_path = <span class="string">&quot;/Users/bq/Workspace/test/audio.pcm&quot;</span>;</span><br><span class="line">FILE *output_file = fopen(output_path, <span class="string">&quot;wb+&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> ((ret == <span class="number">0</span> || ret == <span class="number">-35</span>) &amp;&amp; count &lt; <span class="number">500</span>) &#123;</span><br><span class="line">    ret = av_read_frame(context, &amp;pkt);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;[%d] pkt size is %d\n&quot;</span>, count, pkt.size);</span><br><span class="line">    count++;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 写入文件</span></span><br><span class="line">    fwrite(pkt.data, pkt.size, <span class="number">1</span>, output_file);</span><br><span class="line">    fflush(output_file);</span><br><span class="line"></span><br><span class="line">    av_packet_unref(&amp;pkt);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// 关闭文件</span></span><br><span class="line">fclose(output_file);</span><br><span class="line"><span class="built_in">printf</span>(<span class="string">&quot;完成写入&quot;</span>);</span><br></pre></td></tr></table></figure>
<p>播放测试：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ffplay -ar 44100 -ac 2 -f f32le audio.pcm</span><br></pre></td></tr></table></figure>
<h2 id="编码音频">编码音频</h2>
<p>FFmpeg编码基本过程：</p>
<ol type="1">
<li>创建编码器；</li>
<li>创建上下文；</li>
<li>打开编码器；</li>
<li>送数据给编码器；编码器一般是要缓冲一部分帧，才能编码输出帧。</li>
<li>编码；</li>
<li>释放资源。</li>
</ol>
<p>打开编码器API：</p>
<ol type="1">
<li><code>avcodec_find_encoder</code>：查找编码器。通过id或名字查找。</li>
<li><code>avcodec_alloc_context3</code>：创建上下文。</li>
<li><code>avcodec_open2</code>：打开编码器。</li>
</ol>
<p>fdk_aac，支持的采样大小是16位的，不能设置为FLT。设置了profile后，需要bit_rate置0，否则profile设置不生效。</p>
<p>传输数据API：</p>
<p><code>avcodec_send_frame</code>，把帧输入到编码器。顾名思义，其传入的是AVFrame。会先缓冲一部分数据。</p>
<p><code>avcodec_receive_packet</code>，获取编码后的数据。顾名思义，其输出的是AVPacket。</p>
<p>AVFrame与AVPacket，从命名上看，似乎frame是解压后的帧、packet是压缩后的数据包。之前打开设备并从中<code>av_read_frame</code>出来的却是个packet，这是因为FFmpeg把设备视为媒体文件处理。而从媒体文件读取的就是packet数据。即按照正规流程，从设备读取帧获得packet后，还需要走解码的步骤，最后得出AVFrame。我们是知道从设备读取的帧就是未压缩的帧，所以就直接从packet里面拿数据了。这其实也是种投机取巧的方式。</p>
<h2 id="重采样音频">重采样音频</h2>
<p>基本步骤：</p>
<ol type="1">
<li>创建重采样上下文；</li>
<li>设置参数；</li>
<li>初始化重采样；</li>
</ol>
<p>对应API：</p>
<ol type="1">
<li><code>swr_alloc_set_opts</code>：创建了重采样的上下文，并进行了初始化。</li>
<li><code>swr_init</code></li>
<li><code>swr_convert</code></li>
<li><code>swr_free</code></li>
</ol>
<p>需要头文件：</p>
<p>libswresample/swresample.h</p>
<p>channel layout：指扬声器的布局，用它来表示声道信息。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 创建文件，权限：w（写入）b（写入二进制）+（若文件不存在则创建）</span></span><br><span class="line"><span class="type">char</span> *output_path = <span class="string">&quot;/Users/bq/Workspace/test/audio.pcm&quot;</span>;</span><br><span class="line">FILE *output_file = fopen(output_path, <span class="string">&quot;wb+&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建重采样上下文</span></span><br><span class="line">SwrContext *swr_ctx = swr_alloc_set_opts(<span class="literal">NULL</span>, <span class="comment">// ctx</span></span><br><span class="line">    AV_CH_LAYOUT_MONO, AV_SAMPLE_FMT_S16, <span class="number">44100</span>, <span class="comment">// 输出格式</span></span><br><span class="line">    AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_FLT, <span class="number">44100</span>, <span class="comment">// 输入格式</span></span><br><span class="line">    <span class="number">0</span>, <span class="literal">NULL</span></span><br><span class="line">);</span><br><span class="line"><span class="comment">// 初始化</span></span><br><span class="line"><span class="keyword">if</span> (!swr_ctx || swr_init(swr_ctx) &lt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;重采样上下文创建失败&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 重采样输入数据</span></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> ch_length = <span class="number">4096</span> / <span class="number">4</span> / <span class="number">2</span>;</span><br><span class="line"><span class="type">uint8_t</span> **src_data = <span class="literal">NULL</span>;</span><br><span class="line"><span class="type">int</span> src_data_length = <span class="number">0</span>;</span><br><span class="line"><span class="comment">// 根据格式生成缓冲区</span></span><br><span class="line">av_samples_alloc_array_and_samples(&amp;src_data, &amp;src_data_length, <span class="number">2</span>, ch_length, AV_SAMPLE_FMT_FLT, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 重采样输出数据</span></span><br><span class="line"><span class="type">uint8_t</span> **dst_data = <span class="literal">NULL</span>;</span><br><span class="line"><span class="type">int</span> dst_data_length = <span class="number">0</span>;</span><br><span class="line"><span class="comment">// 根据格式生成缓冲区</span></span><br><span class="line">av_samples_alloc_array_and_samples(&amp;dst_data, &amp;dst_data_length, <span class="number">1</span>, ch_length, AV_SAMPLE_FMT_S16, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> ((ret == <span class="number">0</span> || ret == <span class="number">-35</span>) &amp;&amp; count &lt; <span class="number">500</span>) &#123;</span><br><span class="line">    <span class="comment">// 读取音频数据</span></span><br><span class="line">    ret = av_read_frame(context, &amp;pkt);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (ret == <span class="number">-35</span>) <span class="keyword">continue</span>;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;[%d] pkt size is %d\n&quot;</span>, count, pkt.size);</span><br><span class="line">    count++;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 拷贝数据到输入</span></span><br><span class="line">    <span class="built_in">memcpy</span>(src_data[<span class="number">0</span>], pkt.data, pkt.size);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 重采样，转换的数据量是每个通道的采样数</span></span><br><span class="line">    swr_convert(swr_ctx,</span><br><span class="line">        dst_data, <span class="number">512</span>, <span class="comment">// 输出</span></span><br><span class="line">        (<span class="type">const</span> <span class="type">uint8_t</span> **)src_data, <span class="number">512</span> <span class="comment">// 输入</span></span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 写入文件</span></span><br><span class="line">    <span class="comment">//fwrite(pkt.data, (size_t)pkt.size, 1, output_file);</span></span><br><span class="line">    fwrite(dst_data[<span class="number">0</span>], <span class="number">1</span>, dst_data_length, output_file);</span><br><span class="line">    fflush(output_file);</span><br><span class="line"></span><br><span class="line">    av_packet_unref(&amp;pkt);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (src_data) &#123;</span><br><span class="line">    av_freep(&amp;src_data[<span class="number">0</span>]);</span><br><span class="line">&#125;</span><br><span class="line">av_freep(&amp;src_data);</span><br><span class="line"><span class="keyword">if</span> (dst_data) &#123;</span><br><span class="line">    av_freep(&amp;dst_data[<span class="number">0</span>]);</span><br><span class="line">&#125;</span><br><span class="line">av_freep(&amp;dst_data);</span><br><span class="line">swr_free(&amp;swr_ctx);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 关闭文件</span></span><br><span class="line">fclose(output_file);</span><br><span class="line"><span class="built_in">printf</span>(<span class="string">&quot;完成写入&quot;</span>);</span><br></pre></td></tr></table></figure>
<p>播放测试：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ffplay -ar 44100 -ac 1 -f s16le audio.pcm</span><br></pre></td></tr></table></figure>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>编译基础概念</title>
    <url>/posts/compilation_basic_concepts/</url>
    <content><![CDATA[<p>本地编译：当前平台上编译用于当前平台的程序或库。</p>
<p>交叉编译：用特定的交叉编译器编译用于其他平台的程序或库。</p>
<p>一般的交叉编译工具链有：</p>
<ul>
<li>CC：C语言编译器</li>
<li>CXX：C++编译器</li>
<li>AS：汇编语言编译器</li>
<li>AR：打包器，将.o文件打包（CC/CXX/AS编译器生成的为.o文件）</li>
<li>LD：连接器，将库文件和.o文件连接成可执行程序（如.out文件）</li>
<li>NM：查看静态库文件中的符号表</li>
<li>GDB：调试工具</li>
<li>STRIP：通过优化减小可执行文件或者库文件体积</li>
<li>Objdump：查看静态库或者动态库的方法签名</li>
</ul>
<h2 id="make">make</h2>
<p>make工具用于简化编译命令，生成想要的库或程序。</p>
<p>make由<code>configure</code>文件（可执行）来配置，常用参数：</p>
<ul>
<li><code>prefix</code>：指定编译生成的库、可执行文件的路径</li>
<li><code>host</code>：指定运行平台</li>
<li><code>cc</code>：指定编译器</li>
<li><code>cflags</code>：指定编译时所带的参数</li>
<li><code>ldflags</code>：指定链接时所带的参数</li>
</ul>
<p>一般使用步骤：</p>
<ol type="1">
<li>调用<code>./configure ...</code>命令配置make相关参数。</li>
<li>调用<code>make...</code>或者<code>make install</code>命令进行编译、链接，并生成可执行程序或库文件。</li>
</ol>
<h2 id="clang选项">clang选项</h2>
<h3 id="控制错误和警告信息选项">控制错误和警告信息选项</h3>
<ul>
<li><code>-Werror</code>：将警告转换成错误。</li>
<li><code>-Wno-error=foo</code>：保持警告“foo”不被转换成错误，即使-Werror被指定。</li>
<li><code>-Wfoo</code>：使能警告“foo”。</li>
<li><code>-w</code>：禁用所有警告。</li>
<li><code>-Weverything</code>：使能所有警告。</li>
<li><code>-pedantic</code>：警告语言扩展。</li>
<li><code>-pedantic-errors</code>：把语言扩展视作错误。</li>
<li><code>-Wsystem-headers</code>：使能来自系统头文件的警告。</li>
<li><code>-ferror-limit=123</code>：在诊断出123个错误之后停止诊断。默认是20，错误限制可以通过<code>-ferror-limit=0</code>来禁用。</li>
<li><code>-ftemplate-backtrace-limit=123</code>：最多实例化123个模板在模板实例化回溯对于单个警告或错误。限制的默认是10，也可以通过<code>-ftemplate-backtrace-limit=0</code>来禁用。</li>
</ul>
<h3 id="控制调试信息">控制调试信息</h3>
<p>clang的调试信息生成可设置以下选项，如果有多个标志，则只使用最后一个：</p>
<ul>
<li><code>-g0</code>：不生成任何调试信息（默认）。</li>
<li><code>-gline-tables-only</code>：只生成行号表。</li>
<li><code>-g</code>：生成完整的调试信息。</li>
</ul>
<h3 id="编译相关">编译相关</h3>
<ul>
<li><code>-D&lt;macro&gt;=&lt;value&gt;</code>、<code>--define-macro &lt;arg&gt;</code>、<code>--define-macro=&lt;arg&gt;</code>：添加宏定义。将 <code>&lt;macro&gt;</code> 定义为 <code>&lt;value&gt;</code>（如果 <code>&lt;value&gt;</code> 省略则为 1）。</li>
<li><code>-U&lt;macro&gt;</code>、<code>--undefine-macro &lt;arg&gt;</code>、<code>--undefine-macro=&lt;arg&gt;</code>：取消定义宏 <code>&lt;macro&gt;</code>，相当于<code>#undef macro</code>。</li>
<li><code>-llib</code>：指定编译的源文件中所引用的外部库名称，-l和lib之间可加空格也可不加,该选项在编译阶段可加可不加，连接阶段才有效。</li>
<li><code>-Ldir</code>：指定编译的源文件中所引用的外部库的搜索路径，-L和lib之间可加空格也可不加,该选项在编译阶段可加可不加，连接阶段才有效。连接器默认会在当前目录，系统目录搜索库，优先使用动态库，如果指定了此选项，那么将优先在dir目录下搜索库，未找到则按默认规则搜索。</li>
</ul>
<p>备注：如果最终可执行程序是动态链接生成的，那么程序加载时默认到系统目录(一般是/usr/local/lib下)下搜索所引用的动态库(并非会到上面的dir中搜索)，如果设置了LD_LIBRAY_PATH环境变量的值，那么程序加载时动态库将优先去该路径搜索，然后按默认规则搜索。</p>
<p>示例：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pkg-config --cflags --libs x264</span><br><span class="line">-DX264_API_IMPORTS -I/usr/local/Cellar/x264/r3049/include -L/usr/local/Cellar/x264/r3049/lib -lx264</span><br></pre></td></tr></table></figure>
<ul>
<li><code>-O0</code>、<code>-O1</code>、<code>-O2</code>、<code>-O3</code>：编译器的优化级别，<code>-O0</code> 表示没有优化, <code>-O1</code> 为默认值，<code>-O3</code> 优化级别最高。</li>
<li><code>-static</code>：编译器将采用静态链接。</li>
<li><code>-shared</code>：动态链接，编译器默认。</li>
</ul>
]]></content>
      <categories>
        <category>FFmpeg</category>
      </categories>
      <tags>
        <tag>音视频</tag>
      </tags>
  </entry>
  <entry>
    <title>Atomic也不安全</title>
    <url>/posts/atomic_is_not_safe_either/</url>
    <content><![CDATA[<p>property属性加上atomic属性后，可以<strong>一定程度</strong>地保障多线程安全。</p>
<p>不安全的定义：多线程访问时出现意料之外的结果。</p>
<p>atomic的作用：给getter、setter加了锁，保障了进入这两个方法时是安全的。但一旦离开这两个方法，atomic就没法保障线程安全了。</p>
<p>加了atomic也不安全的表现：</p>
<ul>
<li>只是getter和setter是原子操作，但使用属性进行操作的时候，这个语句不是原子的。</li>
<li>如果属性是指针（如类实例），对内存地址的访问，即对对象的操作，不是线程安全的。</li>
</ul>
<p>要做到线程安全，首先要明确需要怎样粒度的线程安全，即要确定哪些代码是要线程安全的，然后对其进行同步访问，具体可以使用锁和同步队列。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://zhuanlan.zhihu.com/p/23998703">iOS多线程到底不安全在哪里？ - 知乎</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>多线程</tag>
      </tags>
  </entry>
  <entry>
    <title>RunLoop</title>
    <url>/posts/runloop/</url>
    <content><![CDATA[<p>一般来说，一个线程只能执行一个任务，执行完成后线程就会退出。而事件循环（即一个while循环）能让线程能随时处理事件但不退出。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">while</span> (alive) &#123;</span><br><span class="line">  performTask() <span class="comment">//执行任务</span></span><br><span class="line">  callout_to_observer() <span class="comment">//通知外部</span></span><br><span class="line">  sleep() <span class="comment">//休眠</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>RunLoop是一种事件循环。它可以让线程有任务时忙碌，没有任务时睡眠。RunLoop提供一个入口函数执行事件循环，执行后，就一直处于“接受消息-&gt;等待-&gt;处理” 的循环中，直到这个循环结束，该函数返回。</p>
<p>RunLoop作用：</p>
<ul>
<li><strong>保持程序持续运行，而不是执行完任务退出；</strong></li>
<li>处理事件，这是保持运行的目的；</li>
<li>节省CPU资源。当RunLoop休眠的时候，CPU可以吧时间片分配给其他事务。如果RunLoop在某次循环之后，发现程序突然没有收集到更多事件供它处理，它就会休眠，停在RunLoop循环里面的某段代码上。过一会程序为RunLoop接收到了新来的事件，其循环就被系统重新激活以继续运行。</li>
</ul>
<p>CFRunLoopRef是在CoreFoundation框架中，提供了C函数API，都是线程安全的。</p>
<p>NSRunLoop是CFRunLoopRef的封装，但不是线程安全的。</p>
<p>RunLoop的线程休眠是通过<code>__CFRunLoopServiceMachPort</code>函数实现的，内部使用了<code>mach_msg</code>函数，这是内核提供的API，实现内核层面的线程休眠。而一般while循环，CPU还是会一直执行指令，占用CPU资源。</p>
<h2 id="与线程的关系">与线程的关系</h2>
<ul>
<li>RunLoop就是用来管理线程的，当线程RunLoop开启后，线程在执行完任务后不会退出，而是处于休眠状态，随时等待接受新的任务。没有RunLoop，就不可能执行多任务，延时任务也不会执行。</li>
<li>线程和RunLoop一一对应，其关系存在一个全局字典中。</li>
<li>只能在当前线程中操作当前线程的RunLoop，而不能去操作其他线程的。</li>
<li>RunLoop在首次获取时创建（通过<code>current</code>和<code>main</code>获取），在线程结束时销毁。</li>
<li>主线程的RunLoop时系统已经创建好了；但子线程的则要自己主动创建，并启动。</li>
</ul>
<h2 id="api使用">API使用</h2>
<h4 id="cfrunloopsourceref">CFRunLoopSourceRef</h4>
<p>事件产生的地方。包含两个版本：</p>
<ul>
<li>Source0：只包含一个回调函数指针，不能主动触发事件。使用时需先调用<code>CFRunLoopSourceSignal(source)</code>标记Source为待处理，然后手动调用<code>CFRunLoopWakeUp(runloop)</code>来唤醒RunLoop处理这个事件。
<ul>
<li>包含触摸事件处理、<code>performSelector</code>。</li>
</ul></li>
<li>Source1：包含一个mach_port和回调函数指针，被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop线程。
<ul>
<li>包含给予port的线程间通信、系统事件捕捉。</li>
</ul></li>
</ul>
<p>如触摸事件，手指点击屏幕，首先产生一个系统事件，通过Source1来接受捕捉，然后由Springboard程序包装成Source0分发到App处理，因此在App内接收到的触摸事件就是Source0的。</p>
<h4 id="cfrunlooptimerref">CFRunLoopTimerRef</h4>
<p>基于时间的触发器。包含一个时长和回调函数指针。</p>
<h4 id="cfrunloopobserverref">CFRunLoopObserverRef</h4>
<p>观察者，每个Observer包含一个回调函数指针，当RunLoop状态发生变化时，观察者能通过回调接收到这个变化。可以监听：</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="title function_">CF_OPTIONS</span><span class="params">(CFOptionFlags, CFRunLoopActivity)</span> &#123;</span><br><span class="line">    kCFRunLoopEntry         = (<span class="number">1UL</span> &lt;&lt; <span class="number">0</span>), <span class="comment">// 即将进入Loop</span></span><br><span class="line">    kCFRunLoopBeforeTimers  = (<span class="number">1UL</span> &lt;&lt; <span class="number">1</span>), <span class="comment">// 即将处理 Timer</span></span><br><span class="line">    kCFRunLoopBeforeSources = (<span class="number">1UL</span> &lt;&lt; <span class="number">2</span>), <span class="comment">// 即将处理 Source</span></span><br><span class="line">    kCFRunLoopBeforeWaiting = (<span class="number">1UL</span> &lt;&lt; <span class="number">5</span>), <span class="comment">// 即将进入休眠</span></span><br><span class="line">    kCFRunLoopAfterWaiting  = (<span class="number">1UL</span> &lt;&lt; <span class="number">6</span>), <span class="comment">// 刚从休眠中唤醒</span></span><br><span class="line">    kCFRunLoopExit          = (<span class="number">1UL</span> &lt;&lt; <span class="number">7</span>), <span class="comment">// 即将退出Loop</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h3 id="mode">Mode</h3>
<p>一个RunLoop包含若干个Mode，每个Mode又包含若干个Source/Timer/Observer。每次调用RunLoop的主函数时，只能指定其中一个Mode，这个Mode被称作CurrentMode。如果需要切换Mode，只能退出Loop，再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer，让其互不影响。Source/Timer/Observer称为mode item，一个item可以被同时加入多个mode。如果一个mode中没有item，则RunLoop会直接退出，不进入循环。</p>
<p>CommonModes：一个Mode把自己标记为“Common”属性（通过将其ModeName添加到RunLoop的<code>commonModes</code>中）。每当RunLoop内容发生变化时，RunLoop都会自动将<code>_commonModeItems</code>里的Source/Observer/Timer同步到具有“Common”标记的所有mode里。</p>
<p>主线程的RunLoop有两个预设的mode：<code>kCFRunLoopDefaultMode</code>、<code>UITrackingRunLoopMode</code>。这两个Mode都被标记为“Common”属性。defaultMode是App平时所处的状态，trackingRunLoopMode是追踪ScrollView滚动时的状态。所以把Timer添加到defaultMode时，在滚动列表时，RunLoop会将mode切换为trackingRunLoopMode，使得Timer不会回调。</p>
<p>要让Timer在滚动时也能回调，可以把Timer分别添加到defaultMode和trackingRunLoopMode中，或者加入到顶层的commonModeItems中。</p>
<h2 id="具体应用">具体应用</h2>
<h3 id="dispatch_get_main_queue">dispatch_get_main_queue</h3>
<p>在主线程中转交给RunLoop调起该方法。注意，只是回到主线程这一步是交给RunLoop处理。</p>
<h3 id="autoreleasepool">AutoreleasePool</h3>
<p>NSAutoreleasePool是对象引用计数自动处理器。当对象加入到NSAutoreleasePool时，会对其<code>retain</code>，当NSAutoreleasePool结束时，会对其所有对象发送一次<code>release</code>消息。NSAutoreleasePool可以以栈的方式组织。</p>
<p>使用容器的block版本的枚举器时会自动添加AutoreleasePool。for循环则没有。</p>
<p>iOS在主线程的RunLoop中注册了2个Observer：</p>
<ul>
<li>第1个Observer监听<code>kCFRunLoopEntry</code>（即将进入RunLoop）事件，会调用<code>objc_autoreleasePoolPush()</code>创建自动释放池，使用最高优先级保证创建在其他回调之前进行。</li>
<li>第2个Observer
<ul>
<li>监听<code>kCFRunLoopBeforeWaiting</code>（即将进入休眠）事件，会调用<code>objc_autoreleasePoolPop()</code>、<code>objc_autoreleasePoolPush()</code>释放旧的池并创建新的池。</li>
<li>监听<code>kCFRunLoopExit</code>（即将退出Runloop）事件，会调用<code>objc_autoreleasePoolPop()</code>释放自动释放池，使用最低优先级保证释放池在其他所有回调之后进行。</li>
</ul></li>
</ul>
<h4 id="autoreleasepool的释放时机">AutoreleasePool的释放时机</h4>
<p>系统在每个runloop中都创建一个Autorelease Pool，并在runloop的末尾进行释放，所以，一般情况下，每个接受autorelease消息的对象，都会在下个runloop开始前被释放。也就是说，在一段同步的代码中执行过程中，生成的对象接受autorelease消息后，一般是不会在作用域结束前释放的。Autorelease对象出了作用域之后，会被添加到最近一次创建的自动释放池中，并会在当前的 runloop 迭代结束时释放。</p>
<p>所以在AutoreleasePool声明的局部变量，在外面就释放了。</p>
<p>子线程会默认包裹一个AutoreleasePool，当线程退出时才释放其中的变量。</p>
<h3 id="事件响应">事件响应</h3>
<p>苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件，其回调函数为 __IOHIDEventSystemClientQueueCallback()。</p>
<h3 id="手势识别">手势识别</h3>
<h3 id="界面更新">界面更新</h3>
<h3 id="定时器">定时器</h3>
<p>CFRunLoopTimerRef。CADisplayLink。</p>
<h3 id="performselecterafterdelay">performSelecter:afterDelay:</h3>
<p>内部会创建定时器添加到当前的RunLoop中。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://blog.ibireme.com/2015/05/18/runloop/">深入理解RunLoop | Garan no dou</a></li>
<li><a href="https://www.jianshu.com/p/d3d3196f5edb">AutoreleasePool详解和runloop的关系 - 简书</a></li>
<li><a href="http://blog.sunnyxx.com/2014/10/15/behind-autorelease/">黑幕背后的Autorelease · sunnyxx的技术博客</a></li>
<li><a href="https://juejin.cn/post/6965790003951566861">Runloop的内部结构与运行原理 - 掘金</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>多线程</tag>
      </tags>
  </entry>
  <entry>
    <title>锁</title>
    <url>/posts/lock/</url>
    <content><![CDATA[<h2 id="概念">概念</h2>
<p>锁：在执行多线程时，用于强行限制资源访问的同步机制，即用于并发控制中保证对互斥要求的满足。</p>
<p>锁都是为了互斥（Mutual exclusion，缩写 Mutex）。即防止两条线程同时对同一公共资源（如全局变量）进行读写的机制。</p>
<h3 id="分类">分类</h3>
<p>锁从实现上分两种：</p>
<ul>
<li>自旋锁。效率高、安全性不足、占用CPU资源大。</li>
<li>非自旋锁。安全性突出、占用CPU资源小，但休眠、唤醒过程要消耗CPU资源。</li>
</ul>
<h3 id="自旋转锁">自旋转锁</h3>
<p>忙等待的，会一直在那空转（循环），直到使用锁的一方释放。自旋锁不会让线程状态发生切换，一直处于用户态，即线程一直都是active的，不会让线程进入阻塞/休眠，减少了不必要的上下文切换，执行速度快。</p>
<p>以下情况选用自旋锁：</p>
<ul>
<li>预计线程等待锁的时间很短；</li>
<li>加锁代码（临界区）经常被调用，但竞争情况发生概率很小，对安全性要求不高；</li>
<li>CPU资源不紧张或多核处理器</li>
</ul>
<h3 id="非自旋锁">非自旋锁</h3>
<p>非忙等待的，操作内核，将自己的状态改为阻塞挂起来，从待执行队列中移出，等待其他线程唤醒。在获取不到锁的时候会进入阻塞状态，从而进入内核态，当获得锁的时候需要从内核态恢复，需要线程上下文切换，影响锁的性能。</p>
<p>以下情况使用非自旋锁：</p>
<ul>
<li>预计线程等待锁的时间比较长；</li>
<li>单核处理器；</li>
<li>临界区有IO操作；</li>
<li>临界区代码复杂度、循环量大</li>
<li>临界区竞争非常激烈，对安全性要求高</li>
</ul>
<h3 id="阻塞与休眠">阻塞与休眠</h3>
<ul>
<li>阻塞：等待一个中断事件的到来</li>
<li>休眠：等待一个超时事件的到来</li>
</ul>
<h2 id="ios中的锁">iOS中的锁</h2>
<p>互斥锁：</p>
<ul>
<li>pthread_mutex_t</li>
<li>NSLock、NSConditionLock（封装了pthread_mutex_t，attr = 普通）</li>
<li>NSDistributedLock（封装了pthread_mutex_t，attr = 递归）
<ul>
<li>引用计数表的数据结构中使用到，对一张表的多个部分进行同时操作。</li>
</ul></li>
<li>NSCondition（封装了pthread_mutex_t和pthread_cond_t）</li>
<li><span class="citation" data-cites="synchronized">@synchronized</span></li>
</ul>
<p>递归锁（基于互斥锁）：</p>
<ul>
<li>NSRecursiveLock</li>
</ul>
<p>自旋锁：</p>
<ul>
<li>OSSpinLock</li>
</ul>
<h4 id="nslock">NSLock</h4>
<p>普通的互斥锁。通过阻塞线程实现。</p>
<h4 id="nsconditionlock">NSConditionLock</h4>
<p>条件锁。比NSLock多了个<code>NSInteger condition</code>作为相等的条件。与<code>condition</code>相等则加锁。</p>
<h4 id="nsrecursivelock">NSRecursiveLock</h4>
<p>递归锁。与NSLock类似，但可以在同一线程重复加锁而不死锁。实现递归过程原子性。</p>
<h3 id="osspinlock">OSSpinLock</h3>
<p>自旋锁。</p>
<h3 id="os_unfair_lock">os_unfair_lock</h3>
<p>用于替代OSSpinLock，解决了优先级反转的问题，但其本质是互斥锁。atomic内部也使用该锁。</p>
<h4 id="nscondition">NSCondition</h4>
<p>协调线程间的顺序执行。wait-signal。先执行一部分任务，然后跳转到其他地方执行，完了以后再回来。</p>
<h3 id="synchronizeobject"><span class="citation" data-cites="synchronize">@synchronize</span>(object)</h3>
<p>通过判断传入的对象是否相同，才满足互斥。</p>
<h3 id="dispatch_semaphore">dispatch_semaphore</h3>
<p>信号量。限制有限数量的资源使用。</p>
<h3 id="pthread_mutex">pthread_mutex</h3>
<p>互斥锁。</p>
<h2 id="实现多线程多读单写">实现多线程多读单写</h2>
<p>实现方案：</p>
<ul>
<li>pthread_rwlock，读写锁</li>
<li>dispatch_barrier_async，异步栅栏调用
<ul>
<li>需要在一个自己创建的并发队列中执行屏障。</li>
</ul></li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">//手动创建一个并发队列</span><br><span class="line">dispatch_queue_t queue = dispatch_queue_create(&quot;rw_queue&quot;, DISPATCH_QUEUE_CONCURRENT);</span><br><span class="line">dispatch_async(queue, ^&#123; // 普通异步</span><br><span class="line">    /*</span><br><span class="line">     读操作代码</span><br><span class="line">     */</span><br><span class="line">&#125;);</span><br><span class="line">dispatch_barrier_async(queue, ^&#123; // 屏障异步</span><br><span class="line">    /*</span><br><span class="line">     写操作代码</span><br><span class="line">     */</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p><img src="https://gitee.com/bqlin/image-land/raw/master/7POMTw.jpg" /></p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://juejin.cn/post/6844903993563414535">iOS-关于锁的总结 - 掘金</a></li>
<li><a href="https://www.jianshu.com/p/9447e55fa79d">IOS - 自旋锁和atomic - 简书</a></li>
<li><a href="https://ityongzhen.github.io/%E5%85%B3%E4%BA%8EiOS%E4%B8%AD%E7%9A%8413%E7%A7%8D%E5%8A%A0%E9%94%81%E6%96%B9%E6%A1%88.html/">关于iOS中的13种加锁方案 | 殷永振</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/335166736">iOS进阶-细数iOS中的锁 - 知乎</a></li>
<li><a href="https://juejin.cn/post/6965770220921159694">如何保证iOS的多线程安全 - 掘金</a></li>
</ul>
]]></content>
      <categories>
        <category>iOS</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>多线程</tag>
      </tags>
  </entry>
  <entry>
    <title>Concurrency Programming Guide</title>
    <url>/posts/concurrency_pg_introduction/</url>
    <content><![CDATA[<h1 id="介绍">介绍</h1>
<p><a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1">原文</a></p>
<p>并发是多个事情同时发生的概念。随着 CPU 核数的增加，开发者需要新的方式去利用它们。尽管像OS X和iOS这样的操作系统能够并行地运行多个程序，但这些程序大多在后台运行，执行的任务几乎不需要持续的处理器时间。当前的前台程序才是既能吸引用户的注意力，又能让计算机忙碌的程序。如果一个程序有很多任务要执行，但只保持一小部分可用的内核被占用，这些额外的处理资源就被浪费了。</p>
<span id="more"></span>
<p>以前程序引入多线程需要创建一个或多个线程。不幸的是写多线程代码很具挑战。线程是必须手动管理的底层技术。考虑到系统不同的负载和底层硬件，程序的最优线程数会动态变化，实现一个正确的线程方案变得异常困难。另外，通常与线程使用的同步机制会增加软件设计的复杂性和风险，而无法保证性能的提高。</p>
<p>与传统的基于线程的系统和程序相比，OS X和iOS都采用了一种更加异步的方法来执行并发任务。程序不需要直接创建线程，而只需要定义特定的任务，然后让系统执行这些任务。通过让系统管理线程，程序获得了原始线程不可能达到的可扩展性水平。程序开发人员也获得了一个更简单、更有效的编程模型。</p>
<p>本文描述了在程序中实现并发应使用的技术和工艺。本文描述的技术在OS X和iOS中都可用。</p>
<h2 id="关于术语的说明">关于术语的说明</h2>
<p>在进入关于并发性的讨论之前，有必要定义一些相关的术语以防止混淆。对UNIX系统或较早的OS X技术比较熟悉的开发者可能会发现本文中的术语“任务”、“进程”和“线程”的用法有些不同。本文档以下列方式使用这些术语：</p>
<ul>
<li>术语<em>线程</em>是用来指代码的独立执行路径。OS X中线程的底层实现是基于POSIX线程API的。</li>
<li>术语<em>进程</em>是指一个正在运行的可执行文件，它可以包含多个线程。</li>
<li>术语<em>任务</em>是用来指需要执行的工作的抽象概念。</li>
</ul>
<p>关于这些术语和本文所使用的其他关键术语的完整定义，可参阅<a href=".%2F06%20Glossary.md">术语表</a>。</p>
<h2 id="扩展阅读">扩展阅读</h2>
<p>本文重点介绍了在程序中实现并发性的首选技术，并不包括线程的使用。如果你需要关于使用线程和其他线程相关技术的信息，参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/Introduction/Introduction.html#//apple_ref/doc/uid/10000057i">Threading Programming Guide</a>。</p>
]]></content>
      <categories>
        <category>翻译</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>多线程</tag>
        <tag>Concurrency Programming Guide</tag>
      </tags>
  </entry>
  <entry>
    <title>Concurrency Programming Guide：并发与程序设计</title>
    <url>/posts/concurrency_pg_concurrency_and_application_design/</url>
    <content><![CDATA[<p>计算机的早期，单位时间内它可以做的工作是由 CPU 的时钟速度决定的。但随着技术的进步，处理器的设计变得更加紧凑，热量和其他物理限制开始限制处理器的最大时钟速度。于是，芯片制造商寻找其他方法来提高其芯片的总性能。他们最终选择的解决方案是增加每个芯片上的处理器内核数量。通过增加内核数量，单个芯片可以在不增加CPU速度或改变芯片尺寸或热特性的情况下每秒执行更多指令。唯一的问题是如何利用这些额外的内核。</p>
<span id="more"></span>
<p>为了利用多核，计算机需要软件能够同时做多件事。对于像 OS X 或 iOS 这样的现代操作系统，同时能有上百个程序在跑，在不同的核调度是可能的。但是了，大部分程序是系统守护程序（system daemons）或后台程序，这些程序消耗很少的资源。然而对于每个程序来说，真正需要的是如何更有效的使用多余的核。</p>
<p>传统的使用多核的方式是创建多个线程。然而随着核数目的提高，线程方案有其自身的问题。最大的问题是，线程代码不能很好地扩展到任意数量的内核。你不能创建与核心等量的线程，然后期望程序跑得很好。程序自身去计算使用多少核心是很高效的本身是一件很有挑战的事。即使知道了数目，给这么多线程编写代码也是很有挑战的。</p>
<p>总的来说，程序需要一种方式来利用可变的核心。一个程序进行的工作也需要根据变化的系统情况来自动伸缩。方便必须足够简单，不增加利用这些核心做工作的总量。Apple 的操作系统提供了这样的解决方案，这章将会讲讲构成该方案的技术，以及一些你可以使用的设计调整。</p>
<h2 id="远离线程">远离线程</h2>
<p>尽管线程已经存在多年，也还有人在用，但是它们没有解决可伸缩执行多个任务的普遍问题。使用线程的话，实现可伸缩方案的负担落在了开发者自身上。你必须决定使用多少个线程，并根据系统条件的变化动态调节。另一个问题是你的程序承担着创建和维护线程的大部分成本。</p>
<p>OS X 和 iOS 采用了 <em>asynchronous design approach</em> 来解决并发的问题，而不是依赖于线程。异步函数在操作系统中已经存在多年，并被使用来启动需要长时间的任务，如从硬盘中读取数据。当被调用时，一个异步函数在幕后会做些工作来启动一个任务，并在任务真正启动前返回。往往，这些工作设计到获得一个后台线程，在这个线程上执行上述任务，当任务完成的时候发送一个通知给调用者（通常通过回调函数）。在过去，如果某个你想用的异步函数不存在的话，你就需要编写你自己的异步函数和创建你自己的线程。但是现在，OS X 和 iOS 提供了允许你执行异步任务，但不需要你管理任何线程的技术。</p>
<p>其中的一个启动异步任务的技术叫做 <em>Grand Central Dispatch （GCD）</em>。这项技术将你经常在自己程序中写的管理线程的代码提出来，移到系统的层级里。你所需要做的是定义你的任务，将这些任务添加到相应的调度队列中 (dispatch queue)。GCD 负责创建需要的线程，并在这些线程上规划这些任务。由于线程管理现在是系统的一部分，GCD提供了一个整体的任务管理和执行方案，提供了比传统线程更好的效率。</p>
<p><em>操作队列</em>是行为跟调度队列 (dispatch queues) 非常像的 Objective-C 对象。你定义自己想要执行的任务，并把它们添加到操作队列中，操作队列会替你负责线程管理，保证任务在系统上的执行尽可能地迅速和高效。</p>
<h3 id="调度队列">调度队列</h3>
<p>调度队列是基于 C 的一个执行自定义任务的机制。一个<em>调度队列</em>要么串行 (serially) 要么并行 (concurrently) 地执行任务，但始终是先入先出的顺序（换句话说，一个调度队列总是按照进入队列的顺序从队列中取出执行任务）。串行调度队列一次只运行一个任务，等到该任务完成后再去排队并启动新的任务。相比之下，并发调度队列会尽可能多地启动任务，而不等待已经启动的任务完成。</p>
<p>调度队列有些其他好处：</p>
<ul>
<li>它们提供了直接并简单的编程接口。</li>
<li>它们提供了自动全面的线程池管理。</li>
<li>它们提供了汇编性能优化。</li>
<li>内存使用更高效（因为线程栈不会在程序内存中停留）。</li>
<li>在负载下不会损害内核。</li>
<li>异步地调度任务到调度队列不会导致死锁。</li>
<li>在资源 contention 的时候可以自由伸缩。</li>
<li>比锁和其他同步原语更高效。</li>
</ul>
<p>提交给调度队列的任务必须封装在一个函数或 block 对象中。 block对象是 OS X v10.6 和 iOS 4.0 引入的一个跟函数指针概念相似的 C 语言特性，但相对于函数指针，它有其他优点。除了在 block 自身的词法域定义 block 外，你通常可以在另一个函数或方法中定义 block，这样 block 就可以访问函数或方法内的变量了。当把 block 提交到调度队列时，block 同样可以从原有的作用域中移出，并拷贝到堆中。所有这些语义使得使用较少代码实现非常动态的任务变得可能。</p>
<p>调度队列是Grand Central Dispatch技术的一部分，是C语言运行时的一部分。关于在程序中使用调度队列的更多信息，可参阅<a href=".%2F03%20Dispatch%20Queues.md">调度队列</a>。关于block的更多信息和它们的优势，可参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html#//apple_ref/doc/uid/TP40007502">Blocks Programming Topics</a>。</p>
<h3 id="调度源">调度源</h3>
<p>调度源是一种基于 C 的异步处理特定系统事件的机制。一个调度源封装了一个特定系统事件类型的信息，并在该事件发生时将特定的block对象或函数提交给调度队列。你可以使用调度源来监听以下系统事件：</p>
<ul>
<li>Timers</li>
<li>Signal handlers</li>
<li>Descriptor-related events</li>
<li>Process-related events</li>
<li>Mach port events</li>
<li>Custom events that you trigger</li>
</ul>
<p>调度源是Grand Central Dispatch技术的一部分。关于使用调度源在程序中接收事件的信息，可参阅<a href=".%2F04%20Dispatch%20Sources.md">调度源</a>。</p>
<h3 id="操作队列">操作队列</h3>
<p>操作队列相当于一个并发的调度队列，由<code>NSOperationQueue</code>类实现。尽管调度队列总是以先进先出的顺序执行任务，而操作队列在确定任务的执行顺序时会考虑到其他因素。主要因素是任务之间配置的依赖。配置依赖关系可以用其构建复杂的执行顺序。</p>
<p>提交给操作队列的任务必须是 <code>NSOperation</code> 类的实例。一个<em>操作对象</em>是一个你需要执行的任务和任务所需数据 Objective-C 封装的对象。因为 <code>NSOperation</code> 类本质上是一个抽象基类，你通常需要自定义子类来执行你的任务。但Foundation框架也提供了一些具体子类可直接使用。</p>
<p>操作对象会产生 KVO 通知，你可以用它来监听你的任务的进度。尽管操作队列总是并发地执行操作对象，但你可以使用依赖关系来确保它们在需要时被串行执行。</p>
<p>关于如何使用操作队列，以及如何定义自定义操作对象的更多信息，可参阅<a href=".%2F02%20Operation%20Queues.md">操作队列</a>。</p>
<h2 id="异步设计技术">异步设计技术</h2>
<p>在你考虑重新设计你的代码来支持并发的时候，你应该问下你自己这样做是否值得。并发可以通过让你的主线程专门响应用户事件来保证程序的响应性；通过使用多个核心可以让你的代码给定时间内做更多的工作。然而，并发也会增加开销，增加代码的整体复杂性，使得代码难以编写和调试。</p>
<p>除了增加复杂性外，并发并不是一个你在程序的产品周期最后可以移接的特性。正确的使用它需要仔细的考虑你的程序所做的任务和这些任务需要的数据结构。做的不对的时候，反而会降低你代码的效率和响应性。因此，在设计开始的时候很有必要花些时间来设定你的目标，设计执行的方案。</p>
<p>每一个程序有不同的要求和不同的任务需要。几乎不可能有一个文档来告诉你怎么设计你的程序和相关的任务。不过，下面的章节试图提供一些指南，帮助你在设计过程中做出正确的选择。</p>
<h3 id="定义程序的预期行为">定义程序的预期行为</h3>
<p>在你开始考虑给你的应用添加并发之前，你应首先定义正确的程序行为。理解你应用的期望行为给你稍后验证你的设计可能，同样给你关于引入并发可能带来的性能提升的想法。</p>
<p>你应该做的第一件事是遍历程序要做的任务和每个任务所需要的对象或数据结构。这些任务可能包含用户行为引起的，也可能是定时器引起的。</p>
<p>之后列出优先级高的任务，细分认为到小的步骤。在这个层级，你应该主要关注你对数据结构的修改和这些对象的修改怎么影响全局状态。你应该注意到不同的对象、数据结构间的依赖。一个对象的修改是否会影响其他对象。如果这些对象可以相互独立地进行修改，这可能是一个可以同时进行修改的地方。</p>
<h3 id="分解出可执行的工作单元">分解出可执行的工作单元</h3>
<p>从你对程序任务的理解，你应该已经可以确定哪些地方你可以使用并发来优化你的代码。如果改变任务执行的步骤会影响最终的结果的话，可需要继续维持这些步骤的顺序；否则如果改变步骤不影响最终的结果的话，你可以考虑并发执行这些步骤。这两种情况下，都要定义可执行的工作单元来代替你任务中需要执行的步骤。然后使用block或操作对象封装工作单元内容，并分发到合适的队列中。</p>
<p>对于每个确定的可执行工作单元，一开始不必太担心工作量的大小。尽管启动线程总有一定的开销，但使用调度队列和操作队列在大多情况下，其开销会比传统的线程要小很多。因此，使用队列比使用线程可以更高效的执行这些比较小的工作单元。当然你应常测量实际性能，并根据需要调整任务的大小。但是还是那句话，开始的时候，没有任务应该被视为太小。</p>
<h3 id="确定需要的队列">确定需要的队列</h3>
<p>现在你的任务已经被分解为不同的工作单元，使用block或操作对象进行封装，你需要定义执行任务的队列。对于一个给定的任务，你需要检查创建的block或操作对象和它们的执行顺序，以正确完成任务。</p>
<p>如果你使用block来完成任务，你可以添加block到串行或并行调度队列。如果需要特定的顺序，则将block添加到串行调度队列。如果顺序不重要，则可以将block添加到并行调度队列，或根据你的需要，把它们添加到多个不同的调度队列中。</p>
<p>如果你通过操作对象来实现你的任务，队列的选择往往没配置这些对象有趣。要串行的执行这些任务，你必须配置这些对象间的依赖。依赖可以确保在依赖的操作对象完成任务时才执行后续的操作。</p>
<h2 id="提高效率的技巧">提高效率的技巧</h2>
<p>除了重构你的代码成较小的任务，将任务加到队列，还有其他的方式使用队列来提高代码的整体效率：</p>
<ul>
<li><strong>如果内存使用是关键因素，考虑直接在任务中计算值。</strong>直接计算数值会使用给定处理器内核的寄存器和缓存，这比主内存快得多。当然要经过测试确定这一优化是否能提高性能。</li>
<li><strong>尽早找出出串行的任务，尽可能使它们更并发。</strong>如果任务因为资源共享而必须串行，则可以考虑移除共享资源，或为每个任务分配资源的副本以消除共享。</li>
<li><strong>避免使用锁。</strong>有了调度队列和操作队列，锁在大多数情况下是不需要的。与其使用锁来保护一些共享资源，不如指定一个串行队列（或使用操作对象依赖）来以正确的顺序执行任务。</li>
<li><strong>尽可能的依赖系统框架。</strong>使用系统提供的API可以节省精力，并能最大限度地提高并发性。</li>
</ul>
<h2 id="性能影响">性能影响</h2>
<p>操作队列、调度队列和调度源是为了让开发者更容易地并发执行更多的代码。然而，这些技术并不保证能给提高程序的执行和响应效率。以技能有效满足需求，又不会对程序的其他资源造成过度负担的方式来使用队列，仍是开发者的任务。例如，尽管你可以创建 10,000 个操作对象，并将它们提交给操作队列，但是这么做会让程序分配大量的内存，最终降低程序的性能和体验。</p>
<p>在引入任何并发到你的代码之前，不论是通过队列还是线程，你都应该收集衡量影响应用当前性能的基本标准。在引入了这些机制后，你需要重新收集这些信息，然后对比以确定程序的整体效率是否得到了提高。如果引入并发导致了程序的执行和响应效率降低，则应使用性能工具来检查潜在的原因。</p>
<p>关于性能和可用的性能工具的介绍，以及更高级的性能相关主题的链接，可参阅<a href="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/PerformanceOverview/Introduction/Introduction.html#//apple_ref/doc/uid/TP40001410">Performance Overview</a>。</p>
<h2 id="并发和其他技术">并发和其他技术</h2>
<p>把代码分解成模块化的任务，是试图该缠程序并发性的最好方法。然而这种设计方法并不能满足所有的场景。根据你的任务，可能还有其他选择为程序的整体并发性提供额外的改进。</p>
<h3 id="opencl和并发性">OpenCL和并发性</h3>
<p>OS X 中 <code>Open Computing Language (OpenCL)</code> 是一个基于标准的技术，用来在 GPU 上进行通用计算。如果你有定义好的计算需要应用在大型数据上，OpenCL 是不错的技术。例如，你也许用 OpenCL 在图像的像素上进行滤镜操作，或者在多个值上进行复杂的数学计算。换句话说，OpenCL 更多是用于处理数据可被并行操作的问题。</p>
<p>尽管 OpenCL 很适合执行大规模的并行数据操作，除此之外可能并不适合其他场景的计算。需要大量的精力来准备和转移数据和 the required work kernel (不知道咋翻译) 到显卡上，以便显卡可以计算。同样需要大量的精力才能从 OpenCL 获取操作结果。因此，任何与系统交互的任务一般都不建议使用OpenCL。例如，你不会使用OpenCL来处理文件或网络流的数据。相反，使用OpenCL执行的工作必须足够的独立，以便它可以被传输到GPU并独立计算。</p>
<p>关于OpenCL和如何使用它的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/OpenCL_MacProgGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008312">OpenCL Programming Guide for Mac</a>。</p>
<h3 id="何时使用线程">何时使用线程</h3>
<p>尽管操作队列和调度队列是并发执行任务的首选方式，但它们并不是万金油。根据你的程序，有时仍可能需要创建自定义线程。当你确实需要创建线程的时候，你应该尽量创建少的线程。同时你只应用线程解决那些用其他方式解决不了的问题。</p>
<p>线程仍是实现实时运行代码的方案。调度队列会尽可能以最快速度执行它们的任务，但它仍没有解决实时的问题。如果你需要在后台执行的代码要求更多可预测的行为，线程可能仍是更好的选择。</p>
<p>与任何线程编程一样，你应总是理智地使用线程，只有在绝对必要时才使用。关于线程包以及如何使用它们的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/Introduction/Introduction.html#//apple_ref/doc/uid/10000057i">Threading Programming Guide</a>.。</p>
<h2 id="总结">总结</h2>
<ul>
<li><p>单位时间内的工作量是由CPU时钟速度决定的。</p></li>
<li><p>随着技术的进步，制造商通过提高CPU核数来提高性能。</p></li>
<li><p>使用线程最大的问题是，线程代码如何充分利用内核。</p></li>
<li><p>对于开发者而言，使用线程的挑战：</p>
<ul>
<li>可伸缩执行多个任务的问题要开发者自行解决。</li>
<li>程序承担着创建和维护线程的大部分成本。</li>
</ul></li>
<li><p>GCD是通过由系统管理的线程池，可以替代直接使用线程实现的绝大部分功能，并提供更高的效率。开发者的任务次需要定义任务，并添加到相应的调度队列中。</p></li>
<li><p>调度队列的调度单元是函数或block；操作队列的调度单元是操作对象。工作单元的代码都是顺序执行的。</p></li>
<li><p>调度队列有些其他好处：</p>
<ul>
<li>它们提供了直接并简单的编程接口。</li>
<li>它们提供了自动全面的线程池管理。</li>
<li>它们提供了汇编性能优化。</li>
<li>内存使用更高效（因为线程栈不会在程序内存中停留）。</li>
<li>在负载下不会损害内核。</li>
<li>异步地调度任务到调度队列不会导致死锁。</li>
<li>在资源 contention 的时候可以自由伸缩。</li>
<li>比锁和其他同步原语更高效。</li>
</ul></li>
<li><p>操作队列相当于一个并发的调度队列。其执行顺序除了队列的先进先出外，主要还考虑操作对象之间的依赖。</p></li>
<li><p>经测试，对于异步队列，无论是使用调度队列还是操作队列，执行任务的顺序都不能依赖于任务入队的顺序。</p></li>
<li><p>OpenCL只适合并行处理大规模数据，不适合一般多线程场景。</p></li>
<li><p>使用队列相比直接使用线程，最大的优势是可预测性。二使用线程则是为了追求实时执行。</p></li>
</ul>
<p>使用技巧：</p>
<ul>
<li>提高效率的技巧（以下技巧都要经过性能测试）：
<ul>
<li>想要最快，就直接计算值，这会直接使用处理器内涵额的寄存器和缓存。</li>
<li>尽可能地并发。</li>
<li>避免使用锁。</li>
<li>尽量用系统API。</li>
</ul></li>
</ul>
]]></content>
      <categories>
        <category>翻译</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>多线程</tag>
        <tag>Concurrency Programming Guide</tag>
      </tags>
  </entry>
  <entry>
    <title>Concurrency Programming Guide：操作队列</title>
    <url>/posts/concurrency_pg_operation_queues/</url>
    <content><![CDATA[<p>Cocoa操作对象是一种以面向对象的方式来封你需要异步执行的任务。操作对象被设计成跟操作队列队列一起使用，或者单独使用。因为是基于Objective-C实现的，操作对象可同时在 OS X 和 iOS 中使用。</p>
<span id="more"></span>
<h2 id="关于操作对象">关于操作对象</h2>
<p>一个<em>操作对象</em>是一个 <code>NSOperation</code> 类的实例，用其封装需要执行的任务。<code>NSOperation</code> 是抽象基类，如果你要做啥具体的任务，必须要通过其子类来完成。尽管是抽象基类，<code>NSOperation</code> 仍然提供了重要的基础设施，以减少子类的工作量。另外，Foundation框架提供了两个具体的子类供开发者直接使用。</p>
<table>
<colgroup>
<col style="width: 27%" />
<col style="width: 72%" />
</colgroup>
<thead>
<tr class="header">
<th>类</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>NSInvocationOperation</code></td>
<td>该类可以直接使用，通过程序对象和selector直接创建一个操作对象。你可以对已有的任务方法使用该类。因为其不需要子类化，所以也可以用该类以更动态的方式创建操作对象。关于如何使用该类的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW6">Creating an NSInvocationOperation Object</a>。</td>
</tr>
<tr class="even">
<td><code>NSBlockOperation</code></td>
<td>该类可以直接使用，可以执行一到多个block。因为其可以执行一到多个block，所以该操作对象使用组的语义进行操作，只有当相关的block都执行完毕时，操作对象本身才算完成。关于使用该类的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW2">Creating an NSBlockOperation Object</a>。</td>
</tr>
<tr class="odd">
<td><code>NSOperation</code></td>
<td>该类是用于自定义操作对象的基类。通过子类化<code>NSOperation</code>，你可以完全控制自己的操作实现，包括改变操作执行和汇报状态的默认方式。关于如何自定义操作对象的更多信息，可参阅<a href="#自定义操作对象">自定义操作对象</a>。</td>
</tr>
</tbody>
</table>
<p>所有操作对象都支持以下特性：</p>
<ul>
<li>支持在操作对象间建立基于图的依赖关系。关于如何配置依赖，可参阅<a href="#配置交互依赖">配置交互依赖</a>。</li>
<li>支持一个可选的完成回调block，该block在任务结束后执行。(仅限OS X v10.6及以后版本。）关于如何设置完成回调block，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW33">Setting Up a Completion Block</a>。</li>
<li>支持通过 KVO 观察任务执行的状态。关于如何观察KVO通知，可参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html#//apple_ref/doc/uid/10000177i">Key-Value Observing Programming Guide</a>。</li>
<li>支持对操作对象优先级排序，从而改变操作对象间相对执行的顺序。了解更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW31">Changing an Operation’s Execution Priority</a>。</li>
<li>支持取消的语义，当任务在执行的过程中可以取消任务。有关如何取消操作对象，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW39">Canceling Operations</a>。有关如何在自己的操作对象中支持取消，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW24">Responding to Cancellation Events</a>。</li>
</ul>
<p>操作对象被设计来提高应用的并发水平。操作对象也是组织和封装程序行为到简单离散块的方式。你可以把一到多个操作提交到一个队列，让相应的工作在一到多个单独的线程上异步执行，而不是全都集中在程序主线程上执行。</p>
<h2 id="并发与非并发操作对象">并发与非并发操作对象</h2>
<p>尽管通常把操作添加操作队列来执行，但这样做不是必须的。也可以直接手动调用<code>start</code>来执行一个操作对象，但这样做并不能保证与其他代码并行执行。<code>NSOperation</code>类的<code>isConcurrent</code>告诉你该操作对象相对于调用 <code>start</code> 方法的线程是否是异步的。默认返回 <code>NO</code>，表示操作对象同步地跑在调用的线程上。</p>
<p>如果你需要实现一个<em>并发的操作对象</em>，也就是说，相对于调用线程而言是异步执行的，你必须写额外的代码异步的启动操作对象。例如，你可能创建一个线程，调用异步系统函数，或者任何保证 <code>start</code> 方法启动任务，并立即返回，而且很有可能在任务完成之前返回。</p>
<p>大部分开发者应该绝不需要实现并发操作对象。如果你总是将操作对象添加到队列中，你不需要实现并发操作对象。当你提交一个非并发操作对象到操作队列的时候，队列自身会创建一个执行操作对象的线程。因此，添加一个非并发操作对象到操作队列仍然导致了操作对象的异步执行。你应只在需要异步执行操作对象但又不添加到操作队列的情况下才定义并发操作对象。</p>
<p>关于如何创建一个并发操作对象，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW8">Configuring Operations for Concurrent Execution</a>和<a href="https://developer.apple.com/documentation/foundation/nsoperation">NSOperation Class Reference</a>。</p>
<h2 id="创建nsinvocationoperation对象">创建NSInvocationOperation对象</h2>
<p><code>NSInvocationOperation</code> 类是 <code>NSOperation</code> 的具体子类，运行时调用你指定对象的selector。使用这个类可以减少大量的自定义操作对象的需求，尤其是修改程序已实现的对象和任务方法时。当你希望调用的方法可以修改时也可以使用该类。例如，你可以使用一个调用操作来执行一个基于用户输入动态选择的selector。</p>
<p>创建invocation操作对象很简单。创建并初始化该类的实例，把需要执行的对象和selector传递给初始化方法。清单2-1展示了一个自定义类的两个方法，演示了创建过程：</p>
<p><strong>清单2-1</strong> 创建一个<code>NSInvocationOperation</code>对象</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">MyCustomClass</span></span></span><br><span class="line">- (<span class="built_in">NSOperation</span>*)taskWithData:(<span class="keyword">id</span>)data &#123;</span><br><span class="line">    <span class="built_in">NSInvocationOperation</span>* theOp = [[<span class="built_in">NSInvocationOperation</span> alloc] initWithTarget:<span class="keyword">self</span></span><br><span class="line">                    selector:<span class="keyword">@selector</span>(myTaskMethod:) object:data];</span><br><span class="line"> </span><br><span class="line">   <span class="keyword">return</span> theOp;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// This is the method that does the actual work of the task.</span></span><br><span class="line">- (<span class="keyword">void</span>)myTaskMethod:(<span class="keyword">id</span>)data &#123;</span><br><span class="line">    <span class="comment">// Perform the task.</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure>
<h2 id="创建nsblockoperation对象">创建NSBlockOperation对象</h2>
<p><code>NSBlockOperation</code> 同样是 <code>NSOperation</code> 的具体子类，用来封装一个或多个block。这个类给那些已经使用了操作队列并不想创调度发队列的应用提供了面向对象的封装。通过操作队列可以使用那些调度队列没有的一些特性，如操作对象依赖、KVO通知等。</p>
<p>当你创建一个block操作对象的时，通常在初始化的时候你添加一个 block；后续你还可以添加多个block。当执行一个 <code>NSBlockOperation</code> 对象的时候，该对象会把所有的 block提交给默认优先级的并发调度队列上。对象会等待所有的 block 执行完毕。当最后的一个 block 执行完后，对象会置自己的状态为完成。因此，你可以使用一个 block操作对象来追踪一组执行的 block，就像使用一个线程 join merge 多个线程执行的结果。因为 block操作对象跑在独立的线程上，程序的其他线程中的任务不受影响，同时可以等待 block操作对象的完成。</p>
<p>清单2-2显示了一个如何创建<code>NSBlockOperation</code>对象的简单示例。该block本身没有参数，也没有重要的返回结果。</p>
<p><strong>清单2-2</strong> 创建一个<code>NSBlockOperation</code>对象</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">NSBlockOperation</span>* theOp = [<span class="built_in">NSBlockOperation</span> blockOperationWithBlock: ^&#123;</span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@&quot;Beginning operation.\n&quot;</span>);</span><br><span class="line">    <span class="comment">// Do some work.</span></span><br><span class="line">&#125;];</span><br></pre></td></tr></table></figure>
<p>创建了 block 操作对象之后，你可以使用 <code>addExecutionBlock:</code> 方法添加更多的 block。如果你需要串行执行 block，你必须直接把 block 提交给指定的调度队列。</p>
<h2 id="自定义操作对象">自定义操作对象</h2>
<p>如果 block 操作对象和 invocation 操作对象都不能满足程序的需求，你可以直接实现 <code>NSOperation</code> 的子类，添加需要的行为。<code>NSOperation</code> 类对所有操作对象提供了通用的子类，也提供了大量的基础设施来处理依赖管理和 KVO 通知。然而，仍然有些时候你需要补充先有的基础设施以确保操作行为的正确。要做的额外工作量取决于你在实现一个非并发还是并发操作对象。</p>
<p>定义一个非并发操作比并发操作简单得多。对于非并发操作对象而言，所有你需要做的是 main task 和合理的响应取消事件；已经存在的基础设施已经为你完成了其他工作。对于一个并发操作对象而言，你必须使用自定义的代码替换掉现有的基础设施。下来的部分将要说明怎么实现这两种类型。</p>
<h3 id="执行main-task">执行Main Task</h3>
<p>每个操作对象至少实现以下方法：</p>
<ul>
<li>一个自定义的初始化方法</li>
<li><code>main</code>方法</li>
</ul>
<p>你需要一个自定义的初始化方法将你的操作对象放入已知的状态，一个 <code>main</code> 方法来执行的你的任务。当然可以根据需要实现额外的方法，如下：</p>
<ul>
<li>打算从 <code>main</code> 方法调用的自定义方法</li>
<li>设置数据和获取结果的属性访问器</li>
<li><code>NSCoding</code>中的归档和解档方法</li>
</ul>
<p>下例展示了一个自定义 <code>NSOperation</code> 的启动模板（代码中没有展示在怎么处理 cancellation，但展示了你通常需要的方法）。</p>
<p>清单2-3展示了一个自定义<code>NSOperation</code>子类的初始模板。(这个清单没有显示如何处理取消，但显示了你通常实现的方法。关于处理取消，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW24">Responding to Cancellation Events</a>）。) 该类的初始化方法需要一个单一的对象作为数据参数，并在操作对象中存储对它的引用。<code>main</code>方法表面上是对该数据对象进行处理，然后将结果返回给程序。</p>
<p><strong>清单2-3</strong> 定义一个简单的操作对象</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">MyNonConcurrentOperation</span> : <span class="title">NSOperation</span></span></span><br><span class="line"><span class="keyword">@property</span> <span class="keyword">id</span> (<span class="keyword">strong</span>) myData;</span><br><span class="line">-(<span class="keyword">id</span>)initWithData:(<span class="keyword">id</span>)data;</span><br><span class="line"><span class="keyword">@end</span></span><br><span class="line"> </span><br><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">MyNonConcurrentOperation</span></span></span><br><span class="line">- (<span class="keyword">id</span>)initWithData:(<span class="keyword">id</span>)data &#123;</span><br><span class="line">   <span class="keyword">if</span> (<span class="keyword">self</span> = [<span class="keyword">super</span> init])</span><br><span class="line">      myData = data;</span><br><span class="line">   <span class="keyword">return</span> <span class="keyword">self</span>;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line">-(<span class="keyword">void</span>)main &#123;</span><br><span class="line">   <span class="keyword">@try</span> &#123;</span><br><span class="line">      <span class="comment">// Do some work on myData and report the results.</span></span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">@catch</span>(...) &#123;</span><br><span class="line">      <span class="comment">// Do not rethrow exceptions.</span></span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure>
<h3 id="响应取消事件">响应取消事件</h3>
<p>在操作对象开始执行后，它要么持续执行到任务完成，要么被显式取消。取消可以发生在任何时候，甚至在操作对象开始执行之前。尽管 <code>NSOperation</code> 类给用户提供了一种方式来取消一个操作对象，但是否识别取消事件是还是开发者决定的。如果一个操作对象被错误地停止了，可能就没有办法回收已经分配的资源。所以，操作对象应该在执行的过程中检查取消事件，并在操作过程中发生取消事件时优雅地退出。</p>
<p>为了支持操作对象的取消，你所要做的就是定期从你的自定义代码中调用对象的<code>isCancelled</code>方法，如果它返回<code>YES</code>就立即返回。无论你的操作持续时间长短，也无论你是直接对<code>NSOperation</code>进行子类化还是使用其具体的子类，支持取消都很重要。<code>isCancelled</code>方法本身是非常轻量的，可以在不影响性能的情况下频繁调用。当设计你的操作对象时，你应考虑在代码中的以下地方调用<code>isCancelled</code>方法：</p>
<ul>
<li>在执行任何实际工作前立即调用；</li>
<li>每次循环迭代至少调用一次，如果单次循环确实很长的话，可以多次检查；</li>
<li>在代码中任何一个相对容易中止操作的地方；</li>
</ul>
<p>清单2-4展示了一个非常简单的示例，说明如何在一个操作对象的<code>main</code>方法中响应取消事件。在这种情况下，每次通过<code>while</code>循环调用<code>isCancelled</code>方法，允许在工作开始前快速退出，并以一定的间隔再次退出。</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)main &#123;</span><br><span class="line">   <span class="keyword">@try</span> &#123;</span><br><span class="line">      <span class="built_in">BOOL</span> isDone = <span class="literal">NO</span>;</span><br><span class="line"> </span><br><span class="line">      <span class="keyword">while</span> (![<span class="keyword">self</span> isCancelled] &amp;&amp; !isDone) &#123;</span><br><span class="line">          <span class="comment">// Do some work and set isDone to YES when finished</span></span><br><span class="line">      &#125;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">@catch</span>(...) &#123;</span><br><span class="line">      <span class="comment">// Do not rethrow exceptions.</span></span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>尽管上述代码中没有包含清理资源的代码，但是你自己的代码中应该清理任何你分配的资源。</p>
<h3 id="为并发执行配置操作对象">为并发执行配置操作对象</h3>
<p>操作对象默认以同步方式执行，也就是说，它们在调用其<code>start</code>方法的线程中执行任务。因为操作队列为非并发操作提供了线程，尽管如此，大多数操作仍然以异步方式运行。然而，如果你打算手动执行操作，并且仍然希望它们异步运行，你就可以通过把操作对象定义为一个并发操作来达到目的。</p>
<p>下表列出了在实现并发操作对象时需要 override 的方法：</p>
<table>
<colgroup>
<col style="width: 34%" />
<col style="width: 65%" />
</colgroup>
<thead>
<tr class="header">
<th>方法</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>start</code></td>
<td><strong>必须</strong> 所有的并发操作都必须覆盖这个方法，用自定义实现替换默认行为。要手动执行一个操作，要调用其<code>start</code>方法。因此，你对这个方法的实现是你的操作的起点，是你设置线程或其他执行环境来执行你的任务的地方。自定义实现在任何时候都不能调用<code>super</code>方法。</td>
</tr>
<tr class="even">
<td><code>main</code></td>
<td><strong>可选</strong> 这个方法通常用于实现与操作对象相关的任务。尽管你可以在<code>start</code>方法中执行任务，但使用这个方法实现任务可以使你的设置和任务代码更清晰地分开。</td>
</tr>
<tr class="odd">
<td><code>isExecuting</code> <br> <code>isFinished</code></td>
<td><strong>必须</strong> 并发操作负责设置其执行环境并向外部客户报告该环境的状态。因此，一个并发操作必须维护一些状态信息，以知道它何时在执行任务，何时完成了该任务。然后，它必须使用这些方法汇报该状态。<br />对这些方法的实现必须是安全的，可以从其他线程同时调用。当改变这些方法所汇报的值时，你还必须为预期的key path生成适当的KVO通知。</td>
</tr>
<tr class="even">
<td><code>isConcurrent</code></td>
<td><strong>必须</strong> 要确定操作对象是一个并发操作，覆盖这个方法并返回<code>YES</code>。</td>
</tr>
</tbody>
</table>
<p>这节剩余的部分展示 <code>MyOperation</code> 类的实现示例，展示了实现一个并发操作所需的基本代码。 <code>MyOperation</code> 只是简单在它创建的线程上执行 <code>main</code> 方法。<code>main</code> 方法的具体内容在这里是不相关的。示例的意义在于展示在定义一个并发操作时需要提供的基础设施。</p>
<p>清单2-5显示了<code>MyOperation</code>类的接口和部分实现。<code>MyOperation</code>类的<code>isConcurrent</code>、<code>isExecuting</code>和<code>isFinished</code>方法的实现相对简单。<code>isConcurrent</code>方法应该简单地返回<code>YES</code>，表示这是一个并发操作。<code>isExecuting</code>和<code>isFinished</code>方法只是返回存储在类本身的实例变量中的值。</p>
<p><strong>清单2-5</strong> 定义一个并发操作队列</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">MyOperation</span> : <span class="title">NSOperation</span> </span>&#123;</span><br><span class="line">    <span class="built_in">BOOL</span>        executing;</span><br><span class="line">    <span class="built_in">BOOL</span>        finished;</span><br><span class="line">&#125;</span><br><span class="line">- (<span class="keyword">void</span>)completeOperation;</span><br><span class="line"><span class="keyword">@end</span></span><br><span class="line"> </span><br><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">MyOperation</span></span></span><br><span class="line">- (<span class="keyword">id</span>)init &#123;</span><br><span class="line">    <span class="keyword">self</span> = [<span class="keyword">super</span> init];</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">self</span>) &#123;</span><br><span class="line">        executing = <span class="literal">NO</span>;</span><br><span class="line">        finished = <span class="literal">NO</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">self</span>;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line">- (<span class="built_in">BOOL</span>)isConcurrent &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">YES</span>;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line">- (<span class="built_in">BOOL</span>)isExecuting &#123;</span><br><span class="line">    <span class="keyword">return</span> executing;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line">- (<span class="built_in">BOOL</span>)isFinished &#123;</span><br><span class="line">    <span class="keyword">return</span> finished;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure>
<p>清单2-6显示了<code>MyOperation</code>的<code>start</code>方法。这个方法的实现是最小的，以便展示你绝对必须执行的任务。在这种情况下，该方法只是启动了一个新的线程，并配置它来调用<code>main</code>方法。该方法还更新了<code>executing</code>成员变量，并为<code>isExecuting</code> key path生成KVO通知，以反映该值的变化。完成工作后，这个方法就简单地返回，让新分离的线程来执行实际的任务。</p>
<p><strong>清单2-6</strong> <code>start</code>方法</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)start &#123;</span><br><span class="line">   <span class="comment">// Always check for cancellation before launching the task.</span></span><br><span class="line">   <span class="keyword">if</span> ([<span class="keyword">self</span> isCancelled])</span><br><span class="line">   &#123;</span><br><span class="line">      <span class="comment">// Must move the operation to the finished state if it is canceled.</span></span><br><span class="line">      [<span class="keyword">self</span> willChangeValueForKey:<span class="string">@&quot;isFinished&quot;</span>];</span><br><span class="line">      finished = <span class="literal">YES</span>;</span><br><span class="line">      [<span class="keyword">self</span> didChangeValueForKey:<span class="string">@&quot;isFinished&quot;</span>];</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">   &#125;</span><br><span class="line"> </span><br><span class="line">   <span class="comment">// If the operation is not canceled, begin executing the task.</span></span><br><span class="line">   [<span class="keyword">self</span> willChangeValueForKey:<span class="string">@&quot;isExecuting&quot;</span>];</span><br><span class="line">   [<span class="built_in">NSThread</span> detachNewThreadSelector:<span class="keyword">@selector</span>(main) toTarget:<span class="keyword">self</span> withObject:<span class="literal">nil</span>];</span><br><span class="line">   executing = <span class="literal">YES</span>;</span><br><span class="line">   [<span class="keyword">self</span> didChangeValueForKey:<span class="string">@&quot;isExecuting&quot;</span>];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>清单2-7显示了<code>MyOperation</code>类的其余实现。正如在清单2-6中看到的，<code>main</code>方法是一个新线程的入口。它执行与操作对象相关的工作，并在工作最终完成时调用自定义的<code>completeOperation</code>方法。然后<code>completeOperation</code>方法为<code>isExecuting</code>和<code>isFinished</code> key path生成所需的KVO通知，以反映操作状态的变化。</p>
<p><strong>清单2-7</strong> 在完成时更新操作对象状态</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)main &#123;</span><br><span class="line">   <span class="keyword">@try</span> &#123;</span><br><span class="line"> </span><br><span class="line">       <span class="comment">// Do the main work of the operation here.</span></span><br><span class="line"> </span><br><span class="line">       [<span class="keyword">self</span> completeOperation];</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">@catch</span>(...) &#123;</span><br><span class="line">      <span class="comment">// Do not rethrow exceptions.</span></span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line">- (<span class="keyword">void</span>)completeOperation &#123;</span><br><span class="line">    [<span class="keyword">self</span> willChangeValueForKey:<span class="string">@&quot;isFinished&quot;</span>];</span><br><span class="line">    [<span class="keyword">self</span> willChangeValueForKey:<span class="string">@&quot;isExecuting&quot;</span>];</span><br><span class="line"> </span><br><span class="line">    executing = <span class="literal">NO</span>;</span><br><span class="line">    finished = <span class="literal">YES</span>;</span><br><span class="line"> </span><br><span class="line">    [<span class="keyword">self</span> didChangeValueForKey:<span class="string">@&quot;isExecuting&quot;</span>];</span><br><span class="line">    [<span class="keyword">self</span> didChangeValueForKey:<span class="string">@&quot;isFinished&quot;</span>];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>即使一个操作被取消了，你也应始终通知KVO观察者你的操作现在已经完成了它的工作。当一个操作对象依赖于其他操作对象的完成时，它将监听这些对象的<code>isFinished</code> key path。只有当所有对象都汇报已经完成时，依赖的操作才会发出信号说它已经准备好运行。因此，未能生成一个完成通知可能会阻止程序中其他操作对象的执行。</p>
<h3 id="维护kvo">维护KVO</h3>
<p><code>NSOperation</code> 类对以下 key path 是 KVO 的：</p>
<ul>
<li><code>isCancelled</code></li>
<li><code>isConcurrent</code></li>
<li><code>isExecuting</code></li>
<li><code>isFinished</code></li>
<li><code>isReady</code></li>
<li><code>dependencies</code></li>
<li><code>queuePriority</code></li>
<li><code>completionBlock</code></li>
</ul>
<p>如果你覆盖了 <code>start</code> 方法或大幅度的自定义一个 <code>NSOperation</code> 对象，而不是覆盖 <code>main</code> 方法，你需确保自定义对象仍然保持着这些 key path 的 KVO 兼容性。当你覆盖 <code>start</code> 方法的时候，你应该关心的 key path 是 <code>isExecuting</code> 和 <code>isFinished</code>。这些 key paths 是重写 <code>start</code> 方法最常影响到的。</p>
<p>如果你想实现对其他操作对象以外的依赖关系的支持，你也可以覆盖<code>isReady</code>方法并强制它返回<code>NO</code>直到你的自定义依赖关系得到满足。（如果你实现了自定义的依赖关系，如果你仍然支持由<code>NSOperation</code>类提供的默认依赖关系管理系统，确保从<code>isReady</code>方法中调用<code>super</code>）。当操作对象的准备状态发生变化时，为<code>isReady</code> key path生成KVO通知以报告这些变化。除非你覆盖了<code>addDependency:</code>或<code>removeDependency:</code>方法，否则你不需要担心为<code>dependencies</code>关键路径产生KVO通知。</p>
<p>尽管你可以为<code>NSOperation</code>的其他key path生成KVO通知，但你不太可能需要这样做。如果你需要取消一个操作，你可以简单地调用现有的<code>cancel</code>方法来完成。同样地，你应该很少需要修改操作对象中的队列优先级信息。最后，除非你的操作能够动态地改变其并发状态，否则你不需要为<code>isConcurrent</code> key path提供KVO通知。</p>
<h2 id="自定义操作对象执行行为">自定义操作对象执行行为</h2>
<p>操作对象的配置发生在创建之后，添加到队列之前。本节描述的配置类型可用于所有的操作对象，无论是对<code>NSOperation</code>进行子类化还是使用现有的<code>NSOperation</code>子类。</p>
<h3 id="配置交互依赖">配置交互依赖</h3>
<p>依赖可以串行不同操作对象。依赖其他操作对象的操作对象在其他操作对象完成之前不能开始执行。因此，你可以使用依赖在两个操作对象之间建立简单的一对一的依赖关系，或者建立复杂的对象依赖关系图。</p>
<p>使用<code>NSOperation</code>的<code>addDependency:</code>方法可以创建依赖关系。这个方法创建单向的依赖关系，当前操作对象依赖于参数给定的操作对象。依赖不限于同一个队列的操作对象。操作对象管理着它们自己的依赖，所以它不受队列局限，但不能创建在操作之间创建循环依赖关系。这是一个开发者的错误，会导致受影响的操作永远无法执行。</p>
<p>当一个操作对象的所有依赖都结束执行时，通常该操作变成准备执行中状态。（如果你自定义了 <code>isReady</code> 方法的话，操作对象的就绪状态就由你自定义行为决定了）。如果操作对象在队列中，队列可能随时启动执行其中的操作对象。否则如果你想手动执行该操作，则由你来调用该操作的<code>start</code>方法。</p>
<p><strong>重要提醒：</strong>你应总是在运行操作对象或将其添加到操作队列之前配置依赖关系。在这之后添加的依赖关系可能不会阻止某个操作对象的运行。</p>
<p>依赖机制依赖于每个操作对象在对象的状态发生变化时发送适当的KVO通知。如果你自定义了操作对象的行为，你可能需要从你自定义代码中生成适当的KVO通知，以避免引起依赖关系的问题。关于KVO通知和操作对象的更多信息，可参阅<a href="#维护KVO">维护KVO</a>。关于配置依赖关系的其他信息，可参阅<a href="https://developer.apple.com/documentation/foundation/nsoperation">NSOperation Class Reference</a>。</p>
<h3 id="修改操作对象执行的优先级">修改操作对象执行的优先级</h3>
<p>对于添加到队列中的操作对象，执行顺序首先由队列中的操作的准备状态决定，然后由其相对优先级决定。准备状态由一个操作对象对其他操作对象的依赖决定，但优先级是操作对象本身的一个属性。默认情况下，所有新的操作对象都有一个normal的优先级，你可以调用操作对象的<code>setQueuePriority:</code>方法来增加或减少优先级。</p>
<p>优先级只适用于同一操作队列中的操作操作对象。如果程序有多个操作队列，每个队列都会独立于其他队列来确定自己操作的优先级。因此，低优先级的操作仍有可能在不同队列的高优先级操作之前执行。</p>
<p>优先级不能替代依赖关系。优先级只是决定了操作队列中的那些处于就绪状态的操作对象的执行顺序。例如，如果一个队列同时包含高优先级和低优先级的操作，并且这两个操作都就绪了，那么这个队列会先执行高优先级的操作。但是，如果高优先级的操作对象还没就绪，而低优先级的操作对象已经就绪了，那么队列就会先执行低优先级的操作对象。如果你想阻止一个操作在另一个操作完成之前开始，你必须使用依赖实现。</p>
<h3 id="修改底层线程的优先级">修改底层线程的优先级</h3>
<p>在OS X v10.6及以后的版本中，可以配置操作对象的底层线程的执行优先级。系统中的线程策略本身由内核管理，但一般来说，高优先级的线程比低优先级的线程有更多机会运行。在一个操作对象中，可以把线程优先级设置为0.0到1.0范围内的浮点值，0.0为最低优先级，1.0为最高优先级。如果没有设置一个明确的线程优先级，操作对象将以默认的线程优先级0.5运行。</p>
<p>要设置操作对象的线程优先级，必须在操作对象添加到队列（或手动执行）之前调用操作对象的<code>setThreadPriority:</code>方法。当执行操作的时候，默认的<code>start</code>方法使用你指定的值来修改当前线程的优先级。这个新的优先级只在操作对象的<code>main</code>方法期间保持有效。所有其他代码（包括操作对象的完成block）都以默认的线程优先级运行。如果你创建了一个并发的操作对象，并因此覆盖了<code>start</code>方法，你必须自己配置线程优先级。</p>
<h3 id="设置完成block">设置完成Block</h3>
<p>在OS X v10.6和更高版本中，当一个操作对象的主任务执行完毕时，可以执行一个完成block。你可以使用一个完成block来执行任何你认为不属于主任务的工作。例如，你可以使用这个block来通知感兴趣的对象，操作对象本身已经完成。一个并发的操作对象可能会使用这个block来生成其最终的KVO通知。</p>
<p>要设置完成block，使用<code>NSOperation</code>的<code>setCompletionBlock:</code>方法。该block没有参数也没有返回值。</p>
<h2 id="实现操作对象的技巧">实现操作对象的技巧</h2>
<p>尽管操作对象的实现相当容易，但在编写代码时，有几件事你应该注意。下面几节描述了在编写操作对象的代码时应该考虑的一些因素。</p>
<h3 id="在操作对象中管理内存">在操作对象中管理内存</h3>
<p>下面的章节描述了操作对象中内存管理的关键。关于Objective-C程序中内存管理的一般信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html#//apple_ref/doc/uid/10000011i">Advanced Memory Management Programming Guide</a>。</p>
<h4 id="避免按线程存储">避免按线程存储</h4>
<p>尽管大多数操对象作是在一个线程上执行的，但在非并发操作对象的情况下，这个线程通常是由一个操作队列提供的。如果一个操作队列为你提供了一个线程，你应该认为这个线程是由队列所持有的，而不会被你的操作对象所访问。具体来说，你不应该将任何数据与非自己创建或管理的线程联系起来。由操作队列管理的线程会根据系统和程序的需要而创建和销毁。因此，使用按线程存储在操作之间传递数据是不可靠的，很可能会失败。</p>
<p>就操作对象而言，无论在什么情况下都不应使用按线程存储。当初始化一个操作对象时，你应该为该对象提供它所需要的一切来完成其工作。因此，操作对象本身提供了你需要的上下文存储。所有传入和传出的数据都应该存储在操作对象中，直到它可以被整合回程序或不再需要的时候。</p>
<h4 id="根据需要持有操作对象">根据需要持有操作对象</h4>
<p>仅仅因为操作对象是异步运行的，你不该只是简单地完成它的创建。它们仍只是个对象，你应管理好它的生命周期。如果你需要在一个操作完成后检索结果数据，保持对操作对象的引用尤其重要。</p>
<p>你应该始终保持对操作的引用，原因是你以后可能没有机会从队列获取到该对象。队列会尽一切努力尽可能快地调度和执行操作。在许多情况下，队列在添加操作对象后几乎立即开始执行操作。当你自己的代码回到队列中获取对操作对象的引用时，该操作可能已经完成并从队列中移除了。</p>
<h3 id="处理错误和异常">处理错误和异常</h3>
<p>因为操作对象本质上是程序中的离散实体，它们负责处理任何出现的错误或异常。在OS X v10.6及以后的版本中，<code>NSOperation</code>类提供的默认<code>start</code>方法并不捕捉异常。（在OS X v10.5中，start方法可以捕捉和抑制异常。）代码应该直接捕捉和抑制异常。它还应该检查错误代码，并根据需要通知到程序中合适的地方。如果替换了<code>start</code>方法，你必须在自定义实现中捕捉任何异常，以防止它们离开底层线程的作用域。</p>
<p>你应该处理以下类型的错误：</p>
<ul>
<li>检查和处理UNIX <code>errno</code>形式的error code。</li>
<li>检查由方法和函数返回的显式error code。</li>
<li>捕获由自己的代码或其他系统框架抛出的异常。</li>
<li>捕捉由<code>NSOperation</code>类本身抛出的异常，在以下情况下它会抛出异常：
<ul>
<li>当操作对象还没有就绪执行，但它的<code>start</code>方法被调用时；</li>
<li>当操作对象正在执行或完成时（可能是因为它被取消了），而它的<code>start</code>方法被再次调用时；</li>
<li>当你试图给一个已经执行或完成的操作对象添加一个完成block时；</li>
<li>当你试图检索一个被取消的<code>NSInvocationOperation</code>对象的结果时；</li>
</ul></li>
</ul>
<p>如果自定义代码确实遇到了异常或错误，你应该采取任何必要的步骤将该错误传播到程序的其他位置。<code>NSOperation</code>类没有提供明确的方法来实现这部分工作。因此，如果这些信息对程序很重要，你必须提供必要的代码。</p>
<h2 id="为操作对象确定合适的范围">为操作对象确定合适的范围</h2>
<p>尽管存在在一个操作队列中添加任意多操作的可能，但这样做往往是不切实际的。像任何对象一样，<code>NSOperation</code>类的实例会消耗内存，执行也有相应的开销。如果每个操作对象只做少量的工作，而你创建了数以万计的操作对象，你可能会发现花在调度操作对象上的时间比做真正的操作任务要多。如果程序已经受到了内存的限制，你可能会发现，仅仅在内存中拥有成千上万的操作对象可能会进一步降低性能。</p>
<p>有效使用操作对象的关键是在你需要在具体操作任务和保持计算机持续工作之间找到一个适当的平衡点。尽量确保操作对象完成合理的工作量。例如，如果程序创建了100个操作对象来对100个不同的值执行相同的任务，可以考虑改成创建10个操作对象，每个操作对象处理10个值。</p>
<p>你还应避免一次向队列中添加大量的操作对象，或者避免向队列中添加操作对象的速度超过它们的处理速度。与其一次性添加大量的操作对象，不如分批创建这些对象。当一个批次执行完毕后，使用一个完成block来告诉程序创建一个新的批次。这种方案适用于由大量的任务要进行，想让队列填充足够多的操作对象，来让计算机持续执行的情况。一次性创建大量的操作对象，让直接让程序耗尽内存。</p>
<p>当然，创建操作对象的数量，以及你在每个操作中执行的工作量是可变的，完全取决于你的程序。你应该总是使用诸如Instruments这样的工具来帮助你在效率和速度之间找到一个适当的平衡点。关于Instruments和其他性能工具的概述，可以用来为你的代码收集指标，可参阅<a href="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/PerformanceOverview/Introduction/Introduction.html#//apple_ref/doc/uid/TP40001410">Performance Overview</a>。</p>
<h2 id="执行操作对象">执行操作对象</h2>
<p>最终，你的应用程序需要执行操作对象，以完成相关的工作。在本节中，将学习几种执行操作对象的方法，以及如何在运行时控制操作对象的执行行为。</p>
<h3 id="添加操作对象到操作队列中">添加操作对象到操作队列中</h3>
<p>到目前为止，执行操作对象的最简单方法是使用一个操作队列，它是<code>NSOperationQueue</code>类的实例。程序负责创建和维护使用的任何操作队列。程序可以有任何数量的队列，但在一个给定的时间点上操作对象可以执行的数量是有实际限制的。操作队列与系统配合工作，将并发操作的数量限制在一个适合可用内核和系统负载的数值上。因此，创建更多的队列并不意味着你可以执行更多的操作对象。</p>
<p>创建队列跟创建其他的对象是一样的：</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">NSOperationQueue</span>* aQueue = [[<span class="built_in">NSOperationQueue</span> alloc] init];</span><br></pre></td></tr></table></figure>
<p>要向队列添加操作，可以使用<code>addOperation:</code>方法。在OS X v10.6及更高的版本中，你可以使用<code>addOperations:waitUntilFinished:</code>方法添加操作组，或者使用<code>addOperationWithBlock:</code>方法直接向队列添加block对象（不会有相应的操作对象）。这些方法都是排队一个或多个操作对象，并通知队列应该开始处理这些操作对象。在大多数情况下，操作对象在被添加到队列后不久就会被执行，但是操作队列可能会因为一些原因而延迟执行队列中的操作。具体来说，如果排队的操作对象依赖于其他尚未完成的操作，执行可能会被延迟。如果操作队列本身被暂停或已经在执行其最大数量的并发操作，执行也可能被延迟。下面的例子显示了向队列添加操作对象的基本语法：</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">[aQueue addOperation:anOp]; <span class="comment">// Add a single operation</span></span><br><span class="line">[aQueue addOperations:anArrayOfOps waitUntilFinished:<span class="literal">NO</span>]; <span class="comment">// Add multiple operations</span></span><br><span class="line">[aQueue addOperationWithBlock:^&#123;</span><br><span class="line">   <span class="comment">/* Do something. */</span></span><br><span class="line">&#125;];</span><br></pre></td></tr></table></figure>
<p><strong>重要提醒：</strong>在将操作对象添加到队列之前，你应该对其完成所有必要的配置和修改，因为一旦添加，操作对象可能会在任何时候被运行，这可能会让修改的时间太晚，无法产生预期的效果。</p>
<p>虽然<code>NSOperationQueue</code>类是为操作对象的并发执行而设计的，但也可以强制队列一次只运行一个操作。<code>setMaxConcurrentOperationCount:</code>方法可以让你配置操作队列对象的最大并发操作对象数。给这个方法传递1，会使队列一次只执行一个操作。虽然一次只能执行一个操作对象，但执行的顺序仍然是基于其他因素，比如每个操作对象的就绪状态和分配的优先级。因此，一个串行的操作队列所提供的行为与Grand Central Dispatch中的串行调度队列不完全相同。如果操作对象的执行顺序对你很重要，你应该在把操作对象添加到队列之前，使用依赖来建立这个顺序。关于配置依赖关系的信息，可参阅<a href="#配置交互依赖">配置交互依赖</a>。</p>
<p>关于使用操作队列的信息，可参阅<a href="https://developer.apple.com/documentation/foundation/nsoperationqueue">NSOperationQueue Class Reference</a>。关于串行调度队列的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW6">Creating Serial Dispatch Queues</a>。</p>
<h3 id="手动执行操作对象">手动执行操作对象</h3>
<p>虽然操作队列是运行操作对象的最方便的方式，但也可以不通过队列来执行操作对象。然而，如果你选择手动执行操作，你应该在你的代码中采取一些预防措施。特别是，操作必须准备好运行，你必须始终使用它的<code>start</code>方法来启动它。</p>
<p>一个操作在它的<code>isReady</code>方法返回<code>YES</code>时才被认为能够运行。<code>isReady</code>方法被集成到<code>NSOperation</code>类的依赖管理系统中，以提供操作的依赖关系的状态。只有当它的依赖关系被清除后，一个操作才可以自由地开始执行。</p>
<p>当手动执行一个操作时，你应该总是使用<code>start</code>方法来开始执行。而不是<code>main</code>或其他方法，因为<code>start</code>方法在实际运行自定义代码之前会执行一些安全检查。特别是，默认的<code>start</code>方法会生成操对象作所需的KVO通知，以正确处理其依赖关系。如果操作对象已经被取消了，这个方法也会正确地避免执行你的操作，如果操作对象实际上没有就绪运行，则会抛出一个异常。</p>
<p>如果你的程序定义了并发的操作对象，你也应该考虑在启动操作对象之前调用操作的<code>isConcurrent</code>方法。在该方法返回<code>NO</code>的情况下，本地代码可以决定是在当前线程中同步执行操作还是先创建一个单独的线程。然而，实现这种检查完全由你决定。</p>
<p>清单2-8显示了一个简单的示例，以说明手动执行操作之前应该进行什么样的检查。如果该方法返回<code>NO</code>，你可以安排一个定时器并在稍后再次调用该方法。然后你会不断地重新安排定时器，直到方法返回<code>YES</code>，这可能是因为操作被取消了。</p>
<p><strong>清单2-8</strong> 手动执行一个操作对象</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">- (<span class="built_in">BOOL</span>)performOperation:(<span class="built_in">NSOperation</span>*)anOp</span><br><span class="line">&#123;</span><br><span class="line">   <span class="built_in">BOOL</span>        ranIt = <span class="literal">NO</span>;</span><br><span class="line"> </span><br><span class="line">   <span class="keyword">if</span> ([anOp isReady] &amp;&amp; ![anOp isCancelled])</span><br><span class="line">   &#123;</span><br><span class="line">      <span class="keyword">if</span> (![anOp isConcurrent])</span><br><span class="line">         [anOp start];</span><br><span class="line">      <span class="keyword">else</span></span><br><span class="line">         [<span class="built_in">NSThread</span> detachNewThreadSelector:<span class="keyword">@selector</span>(start)</span><br><span class="line">                   toTarget:anOp withObject:<span class="literal">nil</span>];</span><br><span class="line">      ranIt = <span class="literal">YES</span>;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">else</span> <span class="keyword">if</span> ([anOp isCancelled])</span><br><span class="line">   &#123;</span><br><span class="line">      <span class="comment">// If it was canceled before it was started,</span></span><br><span class="line">      <span class="comment">//  move the operation to the finished state.</span></span><br><span class="line">      [<span class="keyword">self</span> willChangeValueForKey:<span class="string">@&quot;isFinished&quot;</span>];</span><br><span class="line">      [<span class="keyword">self</span> willChangeValueForKey:<span class="string">@&quot;isExecuting&quot;</span>];</span><br><span class="line">      executing = <span class="literal">NO</span>;</span><br><span class="line">      finished = <span class="literal">YES</span>;</span><br><span class="line">      [<span class="keyword">self</span> didChangeValueForKey:<span class="string">@&quot;isExecuting&quot;</span>];</span><br><span class="line">      [<span class="keyword">self</span> didChangeValueForKey:<span class="string">@&quot;isFinished&quot;</span>];</span><br><span class="line"> </span><br><span class="line">      <span class="comment">// Set ranIt to YES to prevent the operation from</span></span><br><span class="line">      <span class="comment">// being passed to this method again in the future.</span></span><br><span class="line">      ranIt = <span class="literal">YES</span>;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">return</span> ranIt;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="取消操作对象">取消操作对象</h3>
<p>一旦被添加到一个操作队列中，操作对象就有效地被队列所拥有，并且不能被移除。取消一个操作对象的唯一方法是取消它。你可以通过调用一个单独的操作对象的<code>cancel</code>方法来取消它，或者通过调用队列对象的<code>cancelAllOperations</code>方法来取消队列中的所有操作对象。</p>
<p>只有当你确定不再需要这些操作对象时，才取消它们。发出取消命令会使操作对象进入canceled状态，这将使它永远无法运行。因为一个被取消的操作仍然被认为是finished的，依赖于它的对象会收到适当的KVO通知来清除这种依赖关系。因此，更常见的情况是，在某些重要事件中取消所有排队的操作对象，比如程序退出或用户特别要求取消，而不是选择性地取消某个操作对象。</p>
<h3 id="等待操作对象完成">等待操作对象完成</h3>
<p>为了获得最佳性能，你应该把操作对象设计成尽可能的异步，让程序在操作对象执行时可以自由地做其他工作。如果创建一个操作对象的代码也处理该对象的结果，你可以使用<code>NSOperation</code>的<code>waitUntilFinished</code>方法来阻塞代码，直到操作完成。不过一般来说，如果可以的话，最好避免调用这个方法。阻塞当前线程可能是一个方便的解决方案，但它给你的代码引入了更多的串行，并限制了整体的并发水平。</p>
<p><strong>重要提醒：</strong>你不应该在程序的主线程中等待一个操作。你只应该从子线程或其他操作对象中进行等待。阻塞你的主线程会阻止程序对用户事件做出响应，并可能使程序看起来没有反应。</p>
<p>除了等待单个操作完成，你还可以通过调用<code>NSOperationQueue</code>的<code>waitUntilAllOperationsAreFinished</code>方法来等待一个队列中的所有操作对象的完成。当等待整个队列完成时，要注意程序的其他线程仍然可以向队列添加操作，但因此也会延长等待时间。</p>
<h3 id="暂停和恢复队列">暂停和恢复队列</h3>
<p>如果要暂停操作对象的执行，你可以使用<code>setSuspended:</code>方法暂停相应的操作队列。暂停一个队列并不会导致已经执行的操作对象在其任务中暂停。它只是阻止队列安排新的操作对象执行。你可以暂停一个队列，以响应用户的请求，暂停任何正在进行的工作，因为预期用户最终可能想要恢复该对队列工作。</p>
<h2 id="总结">总结</h2>
<ul>
<li>NSBlockOperation可以添加多个block，该操作对象使用组的语义进行操作，只有当相关的block都执行完毕时，操作对象本身才算完成。</li>
<li>对于单个block中的代码来说，其执行都是同步的。</li>
<li>操作对象的任务要自行处理异常。</li>
<li>操作对象的配置发生在创建之后，添加到队列之前。</li>
<li>若要保持对操作对象的检索，最好自己添加对操作对象的引用。</li>
<li>要手动执行操作对象，则执行<code>start</code>方法。</li>
<li>取消往往用于对队列的行为，而非个别操作对象。</li>
</ul>
<p>使用技巧：</p>
<ul>
<li>应只在需要单独异步执行操作对象但又不添加到操作队列的情况下才定义并发操作对象。</li>
<li>操作对象的执行顺序主要是基于依赖建立的。不建议通过优先级改变操作对象执行顺序。</li>
<li>操作队列可以通过<code>setMaxConcurrentOperationCount:</code>方法设置并发数量，即使设置为1，其行为也与串行调度队列不完全一致。例如，经测试，操作队列即使并发限制为1，单每次使用的线程可能不同。</li>
<li>操作对象的完成block执行的语义应是不属于主任务的工作。</li>
</ul>
]]></content>
      <categories>
        <category>翻译</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>多线程</tag>
        <tag>Concurrency Programming Guide</tag>
      </tags>
  </entry>
  <entry>
    <title>Concurrency Programming Guide：调度队列</title>
    <url>/posts/concurrency_pg_dispatch_queues/</url>
    <content><![CDATA[<p>Grand Central Dispatch（GCD）调度队列是执行任务的强大工具。调度队列让你可以相对于调用者异步或同步地执行任意的代码块。你可以使用调度队列来执行几乎所有你过去在独立线程上执行的任务。调度队列的优点是使用起来更简单，执行任务的效率相比线程代码高得多。</p>
<p>本章介绍了调度队列，以及如何执行程序中的一般任务。如果你想用调度队列替换现有的线程代码，可参阅<a href=".%2F05%20Migrating%20Away%20from%20Threads.md">迁移线程代码</a>。</p>
<span id="more"></span>
<h2 id="关于调度队列">关于调度队列</h2>
<p>调度队列是在程序中异步和并发地执行任务的一种简单方法。一个<em>任务</em>只是程序需要执行的一些工作。例如，你可以定义一个任务来执行一些计算，创建或修改一个数据结构，处理从文件中读取的一些数据，或任何数量的事情。定义任务的方式是将相应的代码放在一个函数或一个block对象中，并将其添加到一个调度队列中。</p>
<p>调度队列是一个类似于对象的结构，管理提交给它的任务。所有调度队列都是先入先出的数据结构。因此，添加到队列中的任务总是按照它们被添加的相同顺序启动。GCD已经提供了一些调度队列，但你也可以为特定的目的而创建其他调度队列。表3-1列出了程序可用的调度队列的类型及其用法。</p>
<p><strong>表3-1</strong> 调度队列类型</p>
<table>
<colgroup>
<col style="width: 14%" />
<col style="width: 85%" />
</colgroup>
<thead>
<tr class="header">
<th>类型</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>串行</td>
<td>串行队列（也称为<em>私有调度队列</em>）按照添加到队列的顺序，一次执行一个任务。当前执行的任务在一个独立的线程上运行（可以因任务而异），该线程由调度队列管理。串行队列通常用于同步访问特定的资源。<br />你可以根据需要创建足够多的串行队列，每个队列相对于所有其他队列都是并发执行的。换句话说，如果你创建了四个串行队列，每个队列一次只执行一个任务，但最多可以有四个任务同时执行，每个队列一个。有关如何创建串行队列的信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW6">Creating Serial Dispatch Queues</a>。</td>
</tr>
<tr class="even">
<td>并发</td>
<td>并发队列（也被称为<em>全局调度队列</em>的一种类型）同时执行一或多个任务，但任务仍然按照它们被添加到队列的顺序启动。当前执行的任务在不同的线程上运行，这些线程由调度队列管理。在任何时候执行的任务的确切数量是根据系统条件而决定。<br />在iOS 5和更高版本中，可以通过指定<code>DISPATCH_QUEUE_CONCURRENT</code>队列类型，自己创建并发的调度队列。此外，还有四个预定义的全局并发队列供程序使用。关于如何获得全局并发队列的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW5">Getting the Global Concurrent Dispatch Queues</a>。</td>
</tr>
<tr class="odd">
<td>主调度队列</td>
<td>主调度队列是一个全局可用的串行队列，在程序的主线程上执行任务。这个队列与程序的run loop（如果有的话）配合工作，将队列任务的执行与连接到run loop的其他事件源的执行交错进行。因为它在程序的主线程上运行，所以主队列经常被用作程序的关键同步点。<br />虽然你不需要创建主调度队列，但你需要确保程序适当地使用它。关于如何管理该队列的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW15">Performing Tasks on the Main Thread</a>。</td>
</tr>
</tbody>
</table>
<p>当涉及到向程序添加并发特性时，调度队列比线程有几个优势。最直接的优势是工作队列编程模型的简单性。对于线程，你必须为要执行的工作以及线程本身的创建和管理编写代码。调度队列让你专注于你真正想要执行的工作，而不必担心线程的创建和管理。相反，系统为你处理所有的线程创建和管理。这样做的好处是，系统能够比任何单个程序更有效地管理线程。系统可以根据可用的资源和当前的系统条件，动态地扩展线程的数量。此外，系统通常能够比你自己创建的线程更快地开始运行你的任务。</p>
<p>尽管你可能认为为调度队列重写代码会很困难，但为调度队列编写代码往往比为线程写代码要容易。编写代码的关键是设计自成一体且能够异步运行的任务。（这对线程和调度队列都是如此。）然而，调度队列的优势在于可预测性。如果你有两个访问同一共享资源的任务，但在不同的线程上运行，任何一个线程都可以先修改资源，你需要使用一个锁来确保两个任务不会同时修改该资源。有了调度队列，你可以将两个任务添加到一个串行调度队列中，以确保在任何时候只有一个任务修改资源。这种基于队列的同步比锁更有效，因为锁在有争议和无争议的情况下总是需要一个昂贵的内核陷阱（kernel trap），而调度队列主要在程序的进程空间工作，只有在绝对必要时才会向下调用内核。</p>
<p>尽管你会正确地指出，在一个串行队列中运行的两个任务不会并发运行，但你必须记住，如果两个线程同时取得一个锁，那么线程提供的任何并发性都会丢失或大大降低。更重要的是，线程模型需要创建两个线程，这需要占用内核和用户空间的内存。调度队列不会为它们的线程而消耗同样的内存，而且他们使用的线程会保持持续工作，不会被阻塞。</p>
<p>关于调度队列，需要记住的其他一些关键点：</p>
<ul>
<li><p>调度队列相对于其他调度队列来说，是同时执行其任务的。任务的串行是相对于一个调度队列而言的。</p></li>
<li><p>系统决定了在任何时候执行的任务总数。因此，一个在100个不同队列中有100个任务的程序可能不会并发地执行所有这些任务（除非它有100个或更多的有效内核）。</p></li>
<li><p>系统在选择启动哪些新任务时，会考虑到队列的优先级。关于如何设置一个串行队列的优先级，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW7">Providing a Clean Up Function For a Queue</a>。</p></li>
<li><p>队列中的任务在被添加到队列时，必须准备好执行。（如果之前使用过Cocoa操作对象，请注意这种行为与操作对象使用的模型不同。）</p></li>
<li><p>私有调度队列是引用计数的对象。除了在你自己的代码中保留队列外，要注意调度源也可以附加到队列上，也会增加其保留计数。因此，你必须确保所有的调度源都被取消，所有的保留调用都与适当的释放调用相平衡。关于保留和释放队列的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW11">Memory Management for Dispatch Queues</a>。关于调度源的更多信息，可参阅<a href=".%2F04%20Dispatch%20Sources.md">调度源</a>。</p></li>
</ul>
<h2 id="队列相关技术">队列相关技术</h2>
<p>除了调度队列之外，Grand Central Dispatch还提供了一些使用队列来帮助管理代码的技术。表3-2列出了这些技术，并提供了链接，你可以在那里找到关于它们的更多信息。</p>
<p><strong>表3-2</strong> 使用调度队列的技术</p>
<table>
<colgroup>
<col style="width: 14%" />
<col style="width: 85%" />
</colgroup>
<thead>
<tr class="header">
<th>技术</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>调度组</td>
<td>调度组是一种监视一组block对象完成的方式。你可以根据你的需要同步或异步地监视这些block。对于依赖其他任务完成的代码，组提供了一种有用的同步机制。关于使用组的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW25">Waiting on Groups of Queued Tasks</a>。</td>
</tr>
<tr class="even">
<td>调度信号量</td>
<td>调度信号与传统的信号量类似，但通常更有效率。只有当调用线程因为信号量不可用而需要被阻塞时，调度信号量才会向下调用内核。如果信号量是可用的，则不会调用内核。关于如何使用调度信号量的例子，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW24">Using Dispatch Semaphores to Regulate the Use of Finite Resources</a>。</td>
</tr>
<tr class="odd">
<td>调度源</td>
<td>调度源在响应特定类型的系统事件时生成通知。你可以使用调度源来监控事件，如进程通知、信号和描述符事件等等。当一个事件发生时，调度源会将你的任务代码异步提交给指定的调度队列进行处理。关于创建和使用调度源的更多信息，可参阅<a href=".%2F04%20Dispatch%20Sources.md">调度源</a>。</td>
</tr>
</tbody>
</table>
<h2 id="使用block来实现任务">使用Block来实现任务</h2>
<p>Block对象是一种基于C语言的特性，你可以在C、Objective-C和C++代码中使用。Block使得定义一个独立的工作单元变得容易。尽管它们看起来类似于函数指针，但block实际上是由一个类似于对象的底层数据结构表示的，并由编译器为你创建和管理。编译器将你提供的代码（以及任何相关的数据）打包，并将其封装成一种可以存在堆中并在程序中传递的形式。</p>
<p>Block的关键优势之一是它们能够使用其自身词法范围之外的变量。当你在一个函数或方法中定义一个block时，该block在某些方面就像一个传统的代码块一样。例如，block可以读取定义在父作用域中的变量的值。被block访问的变量被复制到堆上的block数据结构中，这样block就可以在以后访问它们。当block被添加到调度队列时，这些值通常必须以只读的格式留下。然而，同步执行的block也可以使用预加了<code>__block</code>关键字的变量，将数据返回到父类的调用范围。</p>
<p>你可以使用类似于函数指针的语法，在你的代码中内联地声明block。Block和函数指针的主要区别是，block名称前面有一个<code>^</code>而不是<code>*</code>。像函数指针一样，你可以向block传递参数，并从它那里接收一个返回值。清单3-1显示了如何在代码中同步声明和执行block。变量<code>aBlock</code>被声明为一个block，它接受一个整数参数，不返回任何值。然后，一个符合该原型的实际block被分配给<code>aBlock</code>，并被声明为内联。最后一行立即执行该block，将指定的整数打印到标准输出：</p>
<p><strong>清单3-1</strong> block简单示例</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="keyword">int</span> x = <span class="number">123</span>;</span><br><span class="line"><span class="keyword">int</span> y = <span class="number">456</span>;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// Block declaration and assignment</span></span><br><span class="line"><span class="keyword">void</span> (^aBlock)(<span class="keyword">int</span>) = ^(<span class="keyword">int</span> z) &#123;</span><br><span class="line">    printf(<span class="string">&quot;%d %d %d\n&quot;</span>, x, y, z);</span><br><span class="line">&#125;;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// Execute the block</span></span><br><span class="line">aBlock(<span class="number">789</span>);   <span class="comment">// prints: 123 456 789</span></span><br></pre></td></tr></table></figure>
<p>下面是你在设计block时应该考虑的一些关键准则：</p>
<ul>
<li>对于你计划使用调度队列异步执行的block，从父函数或方法中捕获标量变量并在block中使用是安全的。然而，你不应该试图捕获大型结构体或其他基于指针的变量，这些变量是由调用上下文分配和删除的。当你的block被执行时，该指针所引用的内存可能已经被回收。当然，自己分配内存（或对象）并明确地将该内存的所有权移交给block是安全的。</li>
<li>调度队列会复制被添加到其中的block，并在执行完毕后释放block。换句话说，你不需要在将block添加到队列之前显式地复制它们。</li>
<li>尽管队列在执行小任务时比原始线程更有效，但在队列中创建block并执行它们仍然存在开销。如果一个block做的工作太少，直接执行它可能比把它调度到队列中更节省开销。判断一个block是否做得太少的方法是使用性能工具收集每个路径的指标并进行比较。</li>
<li>不要缓存相对于底层线程的数据，并期望该数据能从不同的block中访问。如果同一队列中的任务需要共享数据，请使用调度队列的上下文指针来代替存储数据。关于如何访问调度队列的上下文数据的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW13">Storing Custom Context Information with a Queue</a>。</li>
<li>如果block创建了几个以上的Objective-C对象，你可以把block的部分代码包裹在@autorelease中，以处理这些对象的内存管理。尽管GCD调度队列有他们自己的自动释放池，但他们不保证这些池何时被耗尽。如果程序有内存限制，创建自己的自动释放池可以让你在更有规律的时间间隔内释放自动释放对象的内存。</li>
</ul>
<p>关于block的更多信息，包括如何声明和使用它们，可参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html#//apple_ref/doc/uid/TP40007502">Blocks Programming Topics</a>。关于如何将block添加到调度队列中，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW20">Adding Tasks to a Queue</a>。</p>
<h2 id="创建和管理调度队列">创建和管理调度队列</h2>
<p>在你把任务添加到队列中之前，你必须确定使用的队列类型以及后续打算如何使用它。调度队列可以串行或并发地执行任务。此外，如果你对队列有一个特定的用途，你可以相应地配置队列属性。下面几节告诉你如何创建调度队列并配置它们的用途。</p>
<h3 id="获得全局并发调度队列">获得全局并发调度队列</h3>
<p>当你有多个可以并行运行的任务时，并发调度队列很有用。并发队列仍然是一个队列，因为它以先进先出的顺序对任务进行排队；但是，并发队列可能在任何先前的任务完成之前就排队等候其他任务。并发队列在任何给定时刻执行的实际任务数是可变的，可以随着程序的条件变化而动态变化。许多因素会影响并发队列执行的任务数量，包括可用的内核数量、其他进程正在完成的工作量，以及其他串行调度队列中的任务数量和优先级。</p>
<p>系统为每个程序提供了4个并发的调度队列。这些队列对程序来说是全局性的，仅由其优先级来区分。因为它们是全局的，所以你不需要明确地创建它们。相反，你可以使用<code>dispatch_get_global_queue</code>函数来请求获取其中的队列，如下所示：</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">dispatch_queue_t</span> aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>);</span><br></pre></td></tr></table></figure>
<p>除了获得默认的并发队列，你还可以通过向函数传递<code>DISPATCH_QUEUE_PRIORITY_HIGH</code>和<code>DISPATCH_QUEUE_PRIORITY_LOW</code>常量来获得高优先级和低优先级的队列，或者通过传递<code>DISPATCH_QUEUE_PRIORITY_BACKGROUND</code>常量来获得一个后台队列。正如你所期望的，高优先级并发队列中的任务在默认队列和低优先级队列中的任务之前执行。同样地，默认队列中的任务在低优先级队列中的任务之前执行。</p>
<p><strong>注意：</strong> <code>dispatch_get_global_queue</code>函数的第二个参数是为将来的扩展保留的。现在，你应该总是为这个参数传递<code>0</code>。</p>
<p>尽管调度队列是引用计数的对象，你不需要保留和释放全局并发队列。因为它们对程序是全局的，对这些队列的保留和释放调用应被忽略。因此，你不需要存储对这些队列的引用。你只需在需要其中一个队列的引用时调用<code>dispatch_get_global_queue</code>函数。</p>
<h3 id="创建串行调度队列">创建串行调度队列</h3>
<p>当你想让你的任务以特定的顺序执行时，串行队列很有用。串行队列一次只执行一个任务，并且总是从队列的头部取出任务。你可以用一个串行队列代替锁来保护一个共享资源或可变数据结构。与锁不同，串行队列确保任务以可预测的顺序执行。<strong>只要你以异步方式向串行队列提交任务，该队列就不会出现死锁。</strong></p>
<p>与并发队列不同，你必须明确地创建和管理你想使用的任何串行队列。你可以为程序创建任何数量的串行队列，但应避免仅仅作为一种同时执行尽可能多的任务的手段来创建大量的串行队列。如果你想同时执行大量的任务，请将它们提交给全局并发队列。在创建串行队列时，尽量为每个队列确定一个目的，如保护资源或同步程序的一些关键行为。</p>
<p>清单3-2显示了创建一个自定义串行队列所需的步骤。<code>dispatch_queue_create</code>函数需要两个参数：队列名称和一组队列属性。调试器和性能工具会显示设置的队列名称，以帮助你跟踪任务的执行情况。队列属性是为将来使用而保留的，应该是<code>NULL</code>。</p>
<p><strong>清单3-2</strong> 创建一个串行队列</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">dispatch_queue_t</span> queue;</span><br><span class="line">queue = dispatch_queue_create(<span class="string">&quot;com.example.MyQueue&quot;</span>, <span class="literal">NULL</span>);</span><br></pre></td></tr></table></figure>
<p>除了创建的任何自定义队列外，系统还自动创建一个串行队列，并将其绑定到程序的主线程。关于获取主线程的队列的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW3">Getting Common Queues at Runtime</a>。</p>
<h3 id="在运行时获取通用队列">在运行时获取通用队列</h3>
<p>Grand Central Dispatch提供了一些函数，可以让你从程序中访问几个常见的调度队列：</p>
<ul>
<li>使用<code>dispatch_get_current_queue</code>函数用于调试目的或测试当前队列的身份。从一个block对象内部调用这个函数，会返回该block被提交到的队列（以及它现在可能正在运行的队列）。在block外调用此函数会返回程序的默认并发队列。</li>
<li>使用<code>dispatch_get_main_queue</code>函数来获取与程序主线程相关的串行调度队列。这个队列是为Cocoa程序和那些调用<code>dispatch_main</code>函数或在主线程上配置run loop（使用<code>CFRunLoopRef</code>类型或<code>NSRunLoop</code>对象）的程序自动创建的。</li>
<li>使用<code>dispatch_get_global_queue</code>函数来获取任何共享的并发队列。更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW5">Getting the Global Concurrent Dispatch Queues</a>。</li>
</ul>
<h3 id="调度队列的内存管理">调度队列的内存管理</h3>
<p>调度队列和其他调度对象是引用计数的数据类型。你可以使用<code>dispatch_retain</code>和<code>dispatch_release</code>函数根据需要增加和减少该引用计数。当一个队列的引用计数达到0时，系统会异步地释放队列。</p>
<p>保留和释放调度对象，如队列，以确保它们在被使用时仍在内存中，这一点很重要。与内存管理的Cocoa对象一样，一般的规则是，如果你打算使用传递给你的代码的队列，你应该在使用它之前保留该队列，当你不再需要它时释放它。这种基本模式可以确保只要你在使用队列，它就会一直留在内存中。</p>
<p><strong>注意：</strong>你不需要保留或释放任何全局调度队列，包括并发的调度队列或主调度队列。任何试图保留或释放队列的行为都会被忽略。</p>
<p>即使你实现了一个垃圾收集的程序，你仍然必须保留和释放你的调度队列和其他调度对象。Grand Central Dispatch不支持用于回收内存的垃圾收集模型。</p>
<h3 id="用队列存储自定义上下文信息">用队列存储自定义上下文信息</h3>
<p>所有的调度对象（包括调度队列）都允许将自定义上下文数据与该对象相关联。为了在一个给定的对象上设置和获取这些数据，你可以使用<code>dispatch_set_context</code>和<code>dispatch_get_context</code>函数。系统不会以任何方式使用你的自定义数据，而是由你在适当的时候分配和释放该数据。</p>
<p>对于队列，你可以使用上下文数据来存储一个指向Objective-C对象或其他数据结构的指针，以帮助识别队列或其他预期用途。你可以使用队列的析构函数，在队列被释放之前，将上下文数据从队列中释放（或取消关联）。如何编写一个清除队列上下文数据的析构函数的例子，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW8">清单3-3</a>。</p>
<h3 id="为队列提供一个清理函数">为队列提供一个清理函数</h3>
<p>在创建了一个串行调度队列后，可以附加一个析构函数，以便在队列被释放时执行任何自定义的清理工作。调度队列是引用计数的对象，你可以使用 <code>dispatch_set_finalizer_f</code> 函数来指定一个当队列的引用计数达到零时要执行的函数。你用这个函数来清理与队列相关的上下文数据，只有当上下文指针不是<code>NULL</code>时才会调用这个函数。</p>
<p>清单3-3显示了一个自定义的析构函数和一个创建队列并配置析构函数的函数。队列使用析构函数来释放存储在队列上下文指针中的数据。代码中引用的<code>myInitializeDataContextFunction</code>和<code>myCleanUpDataContextFunction</code>函数是你提供的自定义函数，用于初始化和清理数据结构本身的内容。传递给析构函数的上下文指针包含与队列相关的数据对象。</p>
<p><strong>清单3-3</strong> 给队列配置清理函数</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="keyword">void</span> myFinalizerFunction(<span class="keyword">void</span> *context)</span><br><span class="line">&#123;</span><br><span class="line">    MyDataContext* theData = (MyDataContext*)context;</span><br><span class="line"> </span><br><span class="line">    <span class="comment">// Clean up the contents of the structure</span></span><br><span class="line">    myCleanUpDataContextFunction(theData);</span><br><span class="line"> </span><br><span class="line">    <span class="comment">// Now release the structure itself.</span></span><br><span class="line">    free(theData);</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="built_in">dispatch_queue_t</span> createMyQueue()</span><br><span class="line">&#123;</span><br><span class="line">    MyDataContext*  data = (MyDataContext*) malloc(<span class="keyword">sizeof</span>(MyDataContext));</span><br><span class="line">    myInitializeDataContextFunction(data);</span><br><span class="line"> </span><br><span class="line">    <span class="comment">// Create the queue and set the context data.</span></span><br><span class="line">    <span class="built_in">dispatch_queue_t</span> serialQueue = dispatch_queue_create(<span class="string">&quot;com.example.CriticalTaskQueue&quot;</span>, <span class="literal">NULL</span>);</span><br><span class="line">    dispatch_set_context(serialQueue, data);</span><br><span class="line">    dispatch_set_finalizer_f(serialQueue, &amp;myFinalizerFunction);</span><br><span class="line"> </span><br><span class="line">    <span class="keyword">return</span> serialQueue;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="添加任务到队列">添加任务到队列</h2>
<p>要执行一个任务，你必须把它调度到一个适当的调度队列中。你可以同步或异步地调度任务，你可以单独或分组地调度任务。一旦进入队列，考虑到队列的限制和队列中已有的任务，队列将负责尽快执行你的任务。本节向你展示了一些向队列调度任务的技术，并介绍了每一种技术的优点。</p>
<h3 id="添加单个任务到队列">添加单个任务到队列</h3>
<p>有两种方法可以将任务添加到队列中：异步或同步。在可能的情况下，使用<code>dispatch_async</code>和<code>dispatch_async_f</code>函数的异步执行比同步执行要好。当你在队列中添加一个block对象或函数时，没有办法知道该代码何时执行。因此，异步添加block或函数可以让你安排代码的执行，并继续从调用线程做其他工作。如果从程序的主线程调度任务（也许是为了响应一些用户事件）这一点就特别重要。</p>
<p>尽管你应该尽可能地异步添加任务，但有时你仍然需要同步添加任务，以防止竞态条件或其他同步错误。在这些情况下，你可以使用<code>dispatch_sync</code>和<code>dispatch_sync_f</code>函数来将任务添加到队列中。这些函数会阻塞当前的执行线程，直到指定的任务执行完毕。</p>
<p><strong>重要提醒：</strong>你不应该从一个正在执行的任务中调用<code>dispatch_sync</code>或<code>dispatch_sync_f</code>函数，而这个任务是你计划传递给该函数的同一个队列。这对串行队列特别重要，因为这样做必然导致死锁。同样对并发队列也应避免这样做。</p>
<p>下面的例子显示了如何使用基于block的变体来进行异步和同步的任务调度：</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">dispatch_queue_t</span> myCustomQueue;</span><br><span class="line">myCustomQueue = dispatch_queue_create(<span class="string">&quot;com.example.MyCustomQueue&quot;</span>, <span class="literal">NULL</span>);</span><br><span class="line"> </span><br><span class="line"><span class="built_in">dispatch_async</span>(myCustomQueue, ^&#123;</span><br><span class="line">    printf(<span class="string">&quot;Do some work here.\n&quot;</span>);</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">printf(<span class="string">&quot;The first block may or may not have run.\n&quot;</span>);</span><br><span class="line"> </span><br><span class="line"><span class="built_in">dispatch_sync</span>(myCustomQueue, ^&#123;</span><br><span class="line">    printf(<span class="string">&quot;Do some more work here.\n&quot;</span>);</span><br><span class="line">&#125;);</span><br><span class="line">printf(<span class="string">&quot;Both blocks have completed.\n&quot;</span>);</span><br></pre></td></tr></table></figure>
<h3 id="在任务完成时执行完成block">在任务完成时执行完成Block</h3>
<p>就其性质而言, 调度到队列中的任务是独立于创建它们的代码运行的。然而，当任务完成后，程序可能仍然希望被通知这一事实，以便它能够纳入结果。在传统的异步编程中，你可能会使用回调机制来做到这一点，但对于调度队列，你可以使用完成block实现。</p>
<p>完成block只是另一段普通代码而已，在原始任务结束后将其调度到队列中。调用代码通常在启动任务时提供完成block作为参数。任务代码所要做的就是在完成工作时将指定的block或函数提交到指定的队列中。</p>
<p>清单3-4显示了一个用block实现的求平均值函数。求平均值函数的最后两个参数允许调用者在报告结果时指定一个队列和block。在求平均值函数计算出它的值后，它将结果传递给指定的block，并将其调度给队列。为了防止队列过早地被释放，在开始的时候保留该队列并在完成block被调度时进行释放。</p>
<p><strong>清单3-4</strong> 在一个任务完成后执行回调</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="keyword">void</span> average_async(<span class="keyword">int</span> *data, size_t len,</span><br><span class="line">   <span class="built_in">dispatch_queue_t</span> queue, <span class="keyword">void</span> (^block)(<span class="keyword">int</span>))</span><br><span class="line">&#123;</span><br><span class="line">   <span class="comment">// Retain the queue provided by the user to make</span></span><br><span class="line">   <span class="comment">// sure it does not disappear before the completion</span></span><br><span class="line">   <span class="comment">// block can be called.</span></span><br><span class="line">   dispatch_retain(queue);</span><br><span class="line"> </span><br><span class="line">   <span class="comment">// Do the work on the default concurrent queue and then</span></span><br><span class="line">   <span class="comment">// call the user-provided block with the results.</span></span><br><span class="line">   <span class="built_in">dispatch_async</span>(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>), ^&#123;</span><br><span class="line">      <span class="keyword">int</span> avg = average(data, len);</span><br><span class="line">      <span class="built_in">dispatch_async</span>(queue, ^&#123; block(avg);&#125;);</span><br><span class="line"> </span><br><span class="line">      <span class="comment">// Release the user-provided queue when done</span></span><br><span class="line">      dispatch_release(queue);</span><br><span class="line">   &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="并发执行循环迭代">并发执行循环迭代</h3>
<p>并发调度队列可以提高性能的一个地方是，一个执行固定次数迭代的循环。例如，假设你有一个for循环，在每次循环迭代中都做一些工作：</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; count; i++) &#123;</span><br><span class="line">   printf(<span class="string">&quot;%u\n&quot;</span>,i);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>如果在每个迭代过程中执行的工作与所有其他迭代过程中执行的工作不同，并且每个连续的循环完成的顺序不重要，你可以用调用<code>dispatch_apply</code>或<code>dispatch_apply_f</code>函数来代替循环。这些函数在每次循环迭代时将指定的block或函数提交给一个队列。当调度到一个并发队列时，因此有可能同时执行多个循环迭代。</p>
<p>在调用<code>dispatch_apply</code>或<code>dispatch_apply_f</code>时，你可以指定一个串行队列或并发队列。传递一个并发队列允许你同时执行多个循环迭代，这是使用这些函数的最常见方式。尽管使用一个串行队列是允许的，并且对你的代码来说是正确的，但使用这样的队列与原有的循环相比没有真正的性能优势。</p>
<p><strong>重要提醒：</strong>和普通的for循环一样，<code>dispatch_apply</code>和<code>dispatch_apply_f</code>函数在所有循环迭代完成之前不会返回。因此，当已经从队列的上下文中执行的代码中调用它们时，应该小心。如果作为参数传递给函数的队列是一个串行队列，并且是执行当前代码的同一个队列，调用这些函数将使队列陷入死锁。</p>
<p>因为它们实际上阻塞了当前线程，所以当你从主线程中调用这些函数时也要小心，它们可能会阻止你的事件处理循环及时地响应事件。如果循环代码需要明显的处理时间，你可能想从不同的线程调用这些函数。</p>
<p>清单3-5显示了如何用<code>dispatch_apply</code>语法替换前面的for循环。传递给<code>dispatch_apply</code>函数的block必须包含一个识别当前循环迭代的参数。当block被执行时，这个参数的值对于第一次迭代是<code>0</code>，对于第二次迭代是<code>1</code>，以此类推。最后一次迭代的参数值是<code>count - 1</code>，其中<code>count</code>是迭代的总次数。</p>
<p><strong>清单3-5</strong> 并发执行for循环迭代</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">dispatch_queue_t</span> queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>);</span><br><span class="line"> </span><br><span class="line">dispatch_apply(count, queue, ^(size_t i) &#123;</span><br><span class="line">   printf(<span class="string">&quot;%u\n&quot;</span>,i);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>你应该确保任务代码在每次迭代中都能完成合理的工作量。就像你调度到队列的任何block或函数一样，调度该代码的执行是有开销的。如果你的循环的每个迭代只执行少量的工作，调度代码的开销可能会超过你从调度它到队列中可能获得的性能优势。如果你在测试过程中发现这种情况，你可以使用striding来增加每个循环迭代中执行的工作量。通过striding，将原始循环的多次迭代合并成一个block，并按比例减少迭代次数。例如，如果你最初执行了100次迭代，但决定使用4的跨度，你现在从每个block中执行4次循环迭代，迭代次数是25。关于如何实现striding的例子，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/ThreadMigration/ThreadMigration.html#//apple_ref/doc/uid/TP40008091-CH105-SW2">Improving on Loop Code</a>。</p>
<h3 id="在主线程上执行任务">在主线程上执行任务</h3>
<p>Grand Central Dispatch提供了一个特殊的调度队列，你可以用它来在程序的主线程上执行任务。这个队列是为所有程序自动提供的，任何在其主线程上设置run loop（由<code>CFRunLoopRef</code>类型或<code>NSRunLoop</code>对象管理）的程序都会自动drained。如果你没有创建一个Cocoa应用程序，并且不想明确设置一个run loop，你必须调用<code>dispatch_main</code>函数来明确消费主调度队列。你仍然可以向队列添加任务，但如果你不调用这个函数，这些任务就永远不会被执行。</p>
<p>你可以通过调用<code>dispatch_get_main_queue</code>函数获得程序主线程的调度队列。添加到这个队列的任务是在主线程本身上串行进行的。因此，你可以把这个队列作为一个用于同步其他部分进行的工作的同步点。</p>
<h3 id="在任务中使用objective-c对象">在任务中使用Objective-C对象</h3>
<p>GCD提供了对Cocoa内存管理技术的内置支持，因此你可以在提交给调度队列的block中自由使用Objective-C对象。每个调度队列都维护它自己的自动释放池，以确保自动释放的对象在某一时刻被释放；队列不保证它们何时真正释放这些对象。</p>
<p>如果程序有内存限制，并且block创建了超过几个自动释放的对象，创建自己的自动释放池是确保对象被及时释放的唯一方法。如果你的block创建了数以百计的对象，你可能需要创建多个自动释放池，或者定期清空池。</p>
<h2 id="暂停和恢复队列">暂停和恢复队列</h2>
<p>可以通过暂停一个队列来阻止暂时执行block对象。你可以使用<code>dispatch_suspend</code>函数暂停一个调度队列，并使用<code>dispatch_resume</code>函数恢复它。调用<code>dispatch_suspend</code>会增加队列的暂停引用计数，而调用<code>dispatch_resume</code>会减少引用计数。当引用计数大于0时，队列仍然暂停。因此，你必须用一个匹配的恢复调用来平衡所有的暂停调用，以便恢复处理block。</p>
<p><strong>重要提醒：</strong>暂停和恢复调用是异步的，只在block的执行之间生效。暂停队列不会停止已经执行的block。</p>
<h2 id="使用调度信号量来规范有限资源的使用">使用调度信号量来规范有限资源的使用</h2>
<p>如果你提交给调度队列的任务要访问一些有限的资源，你可以使用调度信号量来调节同时访问该资源的任务数量。调度信号量的工作方式与普通信号量一样，但有一个例外。当资源可用时，获取一个调度信号量的时间比获取一个传统系统信号量的时间要短。这是因为Grand Central Dispatch在这种特殊情况不会向下调用内核。唯一一次调用内核是当资源不可用时，系统需要暂停（park）线程直到发出信号。</p>
<p>使用调度信号量的语义如下：</p>
<ol type="1">
<li>创建信号量时（使用<code>dispatch_semaphore_create</code>函数），可以指定一个正整数，表示可用资源的数量。</li>
<li>在每个任务中，调用<code>dispatch_semaphore_wait</code>等待信号量。</li>
<li>当等待调用返回时，获取资源并完成工作。</li>
<li>使用完资源后，释放它并通过调用<code>dispatch_semaphore_signal</code>函数发出信号量。</li>
</ol>
<p>关于这些步骤如何工作的例子，可以考虑系统中文件描述符的使用。每个程序都有有限的文件描述符可以使用。如果有一个处理大量文件的任务，你不希望一次打开这么多文件，以至于你的文件描述符用完。相反，你可以使用信号量来限制文件处理代码使用的文件描述符的数量。你可以在任务中加入以下的基本代码：</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Create the semaphore, specifying the initial pool size</span></span><br><span class="line">dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / <span class="number">2</span>);</span><br><span class="line"> </span><br><span class="line"><span class="comment">// Wait for a free file descriptor</span></span><br><span class="line">dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);</span><br><span class="line">fd = open(<span class="string">&quot;/etc/services&quot;</span>, O_RDONLY);</span><br><span class="line"> </span><br><span class="line"><span class="comment">// Release the file descriptor when done</span></span><br><span class="line">close(fd);</span><br><span class="line">dispatch_semaphore_signal(fd_sema);</span><br></pre></td></tr></table></figure>
<p>创建信号量时，指定可用资源的数量。该值成为信号量的初始计数变量。每次等待信号量时，<code>dispatch_semaphore_wait</code>函数都会将计数变量递减 1。如果结果值为负，该函数会告诉内核阻塞线程。另一方面，<code>dispatch_semaphore_signal</code>函数将 count 变量加 1 以指示资源已被释放。如果有任务被阻塞并等待资源，其中一个任务随后会被解除阻塞并被允许进行工作。</p>
<h2 id="等待排队的任务组">等待排队的任务组</h2>
<p>调度组是一种阻塞线程，直到一个或多个任务执行完毕的方式。你可以在需要等待所有指定的任务都完成才能进行某些任务的地方使用这种行为。例如，在调度了几个任务来计算一些数据后，你可以使用一个组来等待这些任务，然后在它们完成后处理结果。使用调度组的另一种方式是作为线程连接的替代。你可以将相应的任务添加到一个调度组中，并等待整个组，而不是启动几个子线程，然后与每个子线程联合起来。</p>
<p>清单3-6显示了建立一个组，向其调度任务并等待结果的基本过程。没有使用<code>dispatch_async</code>函数将任务调度到队列，而是使用<code>dispatch_group_async</code>函数。这个函数将任务与组相关联，并排队等待执行。要等待一组任务的完成，你就使用<code>dispatch_group_wait</code>函数，传入适当的组。</p>
<p><strong>清单3-6</strong> 等待异步任务</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">dispatch_queue_t</span> queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>);</span><br><span class="line">dispatch_group_t group = dispatch_group_create();</span><br><span class="line"> </span><br><span class="line"><span class="comment">// Add a task to the group</span></span><br><span class="line">dispatch_group_async(group, queue, ^&#123;</span><br><span class="line">   <span class="comment">// Some asynchronous work</span></span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line"><span class="comment">// Do some other work while the tasks execute.</span></span><br><span class="line"> </span><br><span class="line"><span class="comment">// When you cannot make any more forward progress,</span></span><br><span class="line"><span class="comment">// wait on the group to block the current thread.</span></span><br><span class="line">dispatch_group_wait(group, DISPATCH_TIME_FOREVER);</span><br><span class="line"> </span><br><span class="line"><span class="comment">// Release the group when it is no longer needed.</span></span><br><span class="line">dispatch_release(group);</span><br></pre></td></tr></table></figure>
<h2 id="调度队列和线程安全">调度队列和线程安全</h2>
<p>在调度队列的背景下谈论线程安全可能看起来很奇怪，但线程安全仍然是一个相关的话题。任何时候，当你在程序中实现并发时，有几件事你应该知道：</p>
<ul>
<li>调度队列本身是线程安全的。换句话说，你可以从系统中的任何线程向调度队列提交任务，而不必首先取得锁或同步访问队列。</li>
<li>不要从一个正在执行的任务中调用<code>dispatch_sync</code>函数，并传递当前函数调用的队列。这样做会使队列陷入死锁。如果你需要对当前队列进行调度，请使用<code>dispatch_async</code>函数进行异步调度。</li>
<li>避免从你提交给调度队列的任务中获取锁。尽管从任务中使用锁是安全的，但当你获得锁时，如果该锁不可用，你有可能完全阻塞一个串行队列。同样，对于并发队列来说，等待一个锁可能反而会阻止其他任务的执行。如果你需要同步你的部分代码，则使用一个串行调度队列而不是锁。</li>
<li>尽管你可以获得关于运行任务的底层线程的信息，但最好还是不要这样做。关于调度队列与线程的兼容性的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/ThreadMigration/ThreadMigration.html#//apple_ref/doc/uid/TP40008091-CH105-SW18">Compatibility with POSIX Threads</a>。</li>
</ul>
<p>关于如何将现有的线程代码改为使用调度队列的其他技巧，可参阅<a href=".%2F05%20Migrating%20Away%20from%20Threads.md">迁移线程代码</a>。</p>
<h2 id="总结">总结</h2>
<p>队列：</p>
<ul>
<li>在使用上，与操作队列较大的不同的是，调度队列是基于block添加任务的，而操作队列是基于操作对象添加任务的，所以调度队列会少了一些对每个任务的控制，例如任务添加到调度队列时，必须是就绪执行的。</li>
<li>对于操作对象之间的依赖，在调度队列中的替代方案是串行队列和调度组。</li>
<li>对于操作队列的完成block，在调度队列中可以简单在添加的工作单元block中插入执行完成回调block的代码。</li>
<li>如果block创建了几个以上的Objective-C对象，你可以把block的部分代码包裹在@autorelease中，以处理这些对象的内存管理。尽管GCD调度队列有他们自己的自动释放池，但他们不保证这些池何时被耗尽。如果程序有内存限制，创建自己的自动释放池可以让你在更有规律的时间间隔内释放自动释放对象的内存。</li>
<li>全局队列除了主队列，其他4个都是并发队列，并按照优先级区分。</li>
<li>只要以异步方式向队列提交任务，该队列就不会出现死锁。同步进入正在执行的队列，必然造成死锁。</li>
<li>如果你想同时执行大量的任务，请将它们提交给全局并发队列。</li>
<li>当给调度队列设置了自己的上下文数据是（<code>dispatch_set_context</code>），要相应地设置清理函数（<code>dispatch_set_finalizer_f</code>）以释放自己的上下文数据。</li>
<li>在使用调度函数<code>dispatch_apply</code>或<code>dispatch_apply_f</code>优化循环时，传入并发队列才是有意义的，不然根直接执行没有区别。</li>
<li>避免从提交给调度队列的任务重获取锁。这不仅会阻塞串行队列，也会让并发队列阻止其他任务的执行。即会让队列不可预测，要同步代码，应使用串行队列而不是锁。</li>
</ul>
<p>使用技巧：</p>
<ul>
<li>基于队列的同步比锁更有效，因为锁在有争议和无争议的情况下总是需要一个昂贵的内核陷阱（kernel trap），而调度队列主要在程序的进程空间工作，只有在绝对必要时才会向下调用内核。</li>
<li>对于并发队列，若不是需要操作队列，如挂起，否则使用全局并发队列即可。</li>
<li>在创建串行队列时，尽量为每个队列确定一个目的，如保护资源或同步程序的一些关键行为。</li>
<li>除非遇到竞态条件或其他同步错误，否则都尽可能地异步添加任务。异步添加任务到串行队列实现了异步锁。</li>
</ul>
<h3 id="objective-c-api---swift-api">Objective-C API -&gt; Swift API</h3>
<p><code>dispatch_barrier_async</code> -&gt; <code>async</code>设置flags值为<code>.barrier</code></p>
<p><code>dispatch_after</code> -&gt; <code>asyncAfter</code></p>
<p><code>dispatch_once</code> -&gt; 无</p>
<p><code>dispatch_apply</code> -&gt; <code>concurrentPerform</code></p>
<h3 id="一次执行">一次执行</h3>
<p>Swift中没有提供，可自己实现：</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">extension</span> <span class="title class_">DispatchQueue</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">var</span> _onceTracker <span class="operator">=</span> [<span class="type">String</span>]()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">class</span> <span class="title class_">func</span> <span class="title class_">once</span>(<span class="title class_">file</span>: <span class="title class_">String</span> = #<span class="title class_">file</span>, <span class="title class_">function</span>: <span class="title class_">String</span> = #<span class="title class_">function</span>, <span class="title class_">line</span>: <span class="title class_">Int</span> = #<span class="title class_">line</span>, <span class="title class_">block</span>: () -&gt; <span class="title class_">Void</span>) &#123;</span><br><span class="line">        <span class="keyword">let</span> token <span class="operator">=</span> <span class="string">&quot;<span class="subst">\(file)</span>:<span class="subst">\(function)</span>:<span class="subst">\(line)</span>&quot;</span></span><br><span class="line">        once(token: token, block: block)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">class</span> <span class="title class_">func</span> <span class="title class_">once</span>(<span class="title class_">token</span>: <span class="title class_">String</span>, <span class="title class_">block</span>: () -&gt; <span class="title class_">Void</span>) &#123;</span><br><span class="line">        objc_sync_enter(<span class="keyword">self</span>)</span><br><span class="line">        <span class="keyword">defer</span> &#123;</span><br><span class="line">            objc_sync_exit(<span class="keyword">self</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">guard</span> <span class="operator">!</span>_onceTracker.contains(token) <span class="keyword">else</span> &#123; <span class="keyword">return</span> &#125;</span><br><span class="line">        _onceTracker.append(token)</span><br><span class="line">        block()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><code>objc_sync_enter</code>和<code>objc_sync_exit</code>共同实现<code>@sychronized</code>递归锁。</p>
<h3 id="屏障barrier">屏障（barrier）</h3>
<p>注意：在全局并发队列中插入屏障无效，跟普通的<code>async</code>效果一致，起不到阻塞的作用。</p>
<p>插入屏障任务对并发队列的作用：</p>
<ol type="1">
<li>等待在屏障任务之前的任务完成；</li>
<li>执行屏障任务，并等待完成；</li>
<li>继续执行后续其他任务。</li>
</ol>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">concurrentQueue.async &#123;</span><br><span class="line">    <span class="type">DispatchQueueExp</span>.testTask(<span class="string">&quot;A&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">concurrentQueue.async &#123;</span><br><span class="line">    <span class="type">DispatchQueueExp</span>.testTask(<span class="string">&quot;B&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">concurrentQueue.async(flags: .barrier) &#123;</span><br><span class="line">    <span class="type">DispatchQueueExp</span>.testTask(<span class="string">&quot;Barrier-C&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">concurrentQueue.async &#123;</span><br><span class="line">    <span class="type">DispatchQueueExp</span>.testTask(<span class="string">&quot;D&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">concurrentQueue.async &#123;</span><br><span class="line">    <span class="type">DispatchQueueExp</span>.testTask(<span class="string">&quot;F&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>输出：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">A开始-testTask(_:):&lt;NSThread: 0x6000025adec0&gt;&#123;number = 5, name = (null)&#125;</span><br><span class="line">B开始-testTask(_:):&lt;NSThread: 0x6000025e8340&gt;&#123;number = 4, name = (null)&#125;</span><br><span class="line">B结束-testTask(_:):&lt;NSThread: 0x6000025e8340&gt;&#123;number = 4, name = (null)&#125;</span><br><span class="line">A结束-testTask(_:):&lt;NSThread: 0x6000025adec0&gt;&#123;number = 5, name = (null)&#125;</span><br><span class="line">Barrier-C开始-testTask(_:):&lt;NSThread: 0x6000025adec0&gt;&#123;number = 5, name = (null)&#125;</span><br><span class="line">Barrier-C结束-testTask(_:):&lt;NSThread: 0x6000025adec0&gt;&#123;number = 5, name = (null)&#125;</span><br><span class="line">D开始-testTask(_:):&lt;NSThread: 0x6000025adec0&gt;&#123;number = 5, name = (null)&#125;</span><br><span class="line">F开始-testTask(_:):&lt;NSThread: 0x6000025962c0&gt;&#123;number = 3, name = (null)&#125;</span><br><span class="line">D结束-testTask(_:):&lt;NSThread: 0x6000025adec0&gt;&#123;number = 5, name = (null)&#125;</span><br><span class="line">F结束-testTask(_:):&lt;NSThread: 0x6000025962c0&gt;&#123;number = 3, name = (null)&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<h3 id="调度信号量">调度信号量</h3>
<ul>
<li>调度信号量用于在访问一些有限资源时，用它来控制同时访问资源的任务数量。</li>
<li>调度信号量比传统信号量有更好的性能。传统信号量总是需要调用内核来测试信号量。因为当资源可用的时候，获取一个信号量比获取传统信号量更快，因为当资源可用时调度信号量不会向下调用内核。唯一调用内核的实际时机时资源不可用时，系统暂停线程直到发出信号。</li>
<li>如果是为了对资源加锁，那么使用串行队列可能性能更优。</li>
<li>信号量值 ≤ 0，则阻塞当前线程进入休眠等待，直到信号量值 &gt; 0。</li>
<li>使用调度信号量的步骤：
<ol type="1">
<li>创建信号量，传入可用资源数量。</li>
<li><code>wait</code>让信号量-1，表示已占用一个资源。</li>
<li>执行任务。完成时，调用<code>signal</code>让信号量+1，表释放资源。</li>
</ol></li>
<li>注意，若在销毁时信号量的值小于初始值，则会崩溃（BUG IN CLIENT OF LIBDISPATCH: Semaphore object deallocated while in use）。</li>
<li>当初始值为0的信号量，可以用作锁，即调用<code>wait</code>马上阻塞线程，<code>signal</code>才解开线程。例如可以让异步操作变成同步操作。</li>
</ul>
<p>应用：</p>
<ul>
<li>异步变同步</li>
<li>控制并发量，Metal绘制经常使用。</li>
</ul>
<p>控制并发量</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">func</span> <span class="title function_">doSomething</span>(<span class="params">label</span>: <span class="type">String</span>, <span class="params">cost</span>: <span class="type">UInt32</span>, <span class="params">complete</span>:<span class="keyword">@escaping</span> ()-&gt;())&#123;</span><br><span class="line">    <span class="type">NSLog</span>(<span class="string">&quot;Start task%@&quot;</span>,label)</span><br><span class="line">    sleep(cost)</span><br><span class="line">    <span class="type">NSLog</span>(<span class="string">&quot;End task%@&quot;</span>,label)</span><br><span class="line">    complete()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/////////////////////////////////////////////////////////////////////////////</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> semaphore <span class="operator">=</span> <span class="type">DispatchSemaphore</span>(value: <span class="number">3</span>)</span><br><span class="line"><span class="keyword">let</span> queue <span class="operator">=</span> <span class="type">DispatchQueue</span>(label: <span class="string">&quot;&quot;</span>, qos: .default, attributes: .concurrent)</span><br><span class="line"></span><br><span class="line">queue.async &#123;</span><br><span class="line">    semaphore.wait()</span><br><span class="line">    <span class="keyword">self</span>.doSomething(label: <span class="string">&quot;1&quot;</span>, cost: <span class="number">2</span>, complete: &#123;</span><br><span class="line">        <span class="built_in">print</span>(<span class="type">Thread</span>.current)</span><br><span class="line">        semaphore.signal()</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">queue.async &#123;</span><br><span class="line">    semaphore.wait()</span><br><span class="line">    <span class="keyword">self</span>.doSomething(label: <span class="string">&quot;2&quot;</span>, cost: <span class="number">2</span>, complete: &#123;</span><br><span class="line">        <span class="built_in">print</span>(<span class="type">Thread</span>.current)</span><br><span class="line">        semaphore.signal()</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">queue.async &#123;</span><br><span class="line">    semaphore.wait()</span><br><span class="line">    <span class="keyword">self</span>.doSomething(label: <span class="string">&quot;3&quot;</span>, cost: <span class="number">4</span>, complete: &#123;</span><br><span class="line">        <span class="built_in">print</span>(<span class="type">Thread</span>.current)</span><br><span class="line">        semaphore.signal()</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">queue.async &#123;</span><br><span class="line">    semaphore.wait()</span><br><span class="line">    <span class="keyword">self</span>.doSomething(label: <span class="string">&quot;4&quot;</span>, cost: <span class="number">2</span>, complete: &#123;</span><br><span class="line">        <span class="built_in">print</span>(<span class="type">Thread</span>.current)</span><br><span class="line">        semaphore.signal()</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">queue.async &#123;</span><br><span class="line">    semaphore.wait()</span><br><span class="line">    <span class="keyword">self</span>.doSomething(label: <span class="string">&quot;5&quot;</span>, cost: <span class="number">3</span>, complete: &#123;</span><br><span class="line">        <span class="built_in">print</span>(<span class="type">Thread</span>.current)</span><br><span class="line">        semaphore.signal()</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="调度组">调度组</h3>
<ul>
<li>调度组是一种在一或多个任务执行完毕之前阻塞线程的方式。</li>
</ul>
<p>DispatchGroup两种用法:</p>
<p>一、调度队列调度时传入调度组</p>
<p>最简单的用法。</p>
<p>notify：对组做完成监听。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> group <span class="operator">=</span> <span class="type">DispatchGroup</span>()</span><br><span class="line">myQueue<span class="operator">?</span>.async(group: group, qos: .default, flags: [], execute: &#123; </span><br><span class="line">    <span class="keyword">for</span> <span class="keyword">_</span> <span class="keyword">in</span> <span class="number">0</span><span class="operator">...</span><span class="number">10</span> &#123;</span><br><span class="line">       <span class="built_in">print</span>(<span class="string">&quot;耗时任务一&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;)</span><br><span class="line">myQueue<span class="operator">?</span>.async(group: group, qos: .default, flags: [], execute: &#123;</span><br><span class="line">    <span class="keyword">for</span> <span class="keyword">_</span> <span class="keyword">in</span> <span class="number">0</span><span class="operator">...</span><span class="number">10</span> &#123;</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;耗时任务二&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;)</span><br><span class="line"><span class="comment">//执行完上面的两个耗时操作, 回到myQueue队列中执行下一步的任务</span></span><br><span class="line">group.notify(queue: myQueue<span class="operator">!</span>) &#123;</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;回到该队列中执行&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>wait：阻塞线程，同步访问。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">//等待上面任务执行，会阻塞当前线程，超时就执行下面的，上面的继续执行。可以无限等待 .distantFuture</span></span><br><span class="line"><span class="keyword">let</span> result <span class="operator">=</span> group.wait(timeout: .now() <span class="operator">+</span> <span class="number">2.0</span>)</span><br><span class="line"><span class="keyword">switch</span> result &#123;</span><br><span class="line">    <span class="keyword">case</span> .success:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;不超时, 上面的两个任务都执行完&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> .timedOut:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;超时了, 上面的任务还没执行完执行这了&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;接下来的操作&quot;</span>)</span><br></pre></td></tr></table></figure>
<p>二、enter-leave</p>
<p>手动管理调度组计数，<code>enter</code>和<code>leave</code>必须配对。</p>
<p>应用更为自由，不用给队列调用传入调度组，可在任意的队列操作调度组计数。同样最后通过notify监听完成回调。</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> group <span class="operator">=</span> <span class="type">DispatchGroup</span>()</span><br><span class="line">group.enter()<span class="comment">//把该任务添加到组队列中执行</span></span><br><span class="line">myQueue<span class="operator">?</span>.async &#123;</span><br><span class="line">    <span class="keyword">for</span> <span class="keyword">_</span> <span class="keyword">in</span> <span class="number">0</span><span class="operator">...</span><span class="number">10</span> &#123;</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;耗时任务一&quot;</span>)</span><br><span class="line">        group.leave()<span class="comment">//执行完之后从组队列中移除</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line">group.enter()<span class="comment">//把该任务添加到组队列中执行</span></span><br><span class="line">myQueue<span class="operator">?</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> <span class="keyword">_</span> <span class="keyword">in</span> <span class="number">0</span><span class="operator">...</span><span class="number">10</span> &#123;</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;耗时任务二&quot;</span>)</span><br><span class="line">        group.leave()<span class="comment">//执行完之后从组队列中移除</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//当上面所有的任务执行完之后通知</span></span><br><span class="line">group.notify(queue: .main) &#123;</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;所有的任务执行完了&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>调度组和调度信号量都可以实现在异步调用中进行计数，除了用法不一样外，调度信号量只能用于阻塞，而调度组除了阻塞外也提供了异步监听完成的回调。</p>
]]></content>
      <categories>
        <category>翻译</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>多线程</tag>
        <tag>Concurrency Programming Guide</tag>
      </tags>
  </entry>
  <entry>
    <title>Concurrency Programming Guide：调度源</title>
    <url>/posts/concurrency_pg_dispatch_sources/</url>
    <content><![CDATA[<p>每当你与底层系统打交道时，必须准备好该任务可能需要花费大量的时间。对内核或其他系统层的调用涉及到上下文的改变，与发生在进程中的调用相比，这种改变是相当昂贵的。因此，许多系统库提供了异步接口，允许你的代码向系统提交一个请求，并在处理该请求时继续做其他工作。Grand Central Dispatch建立在这种一般行为的基础上，允许你提交请求，并使用block和调度队列将结果反馈给你的代码。</p>
<span id="more"></span>
<h2 id="关于调度源">关于调度源</h2>
<p><em>调度源</em>是一个基本的数据类型，它协调特定的底层系统事件的处理。Grand Central Dispatch支持以下类型的调度源：</p>
<ul>
<li><em>计时器调度源</em>产生定期通知。</li>
<li><em>信号调度源</em>在UNIX信号到达时发出通知。</li>
<li><em>描述符源</em>通知你各种基于文件和套接字的操作，例如：
<ul>
<li>当数据可供读取时；</li>
<li>当可以写入数据时；</li>
<li>当文件在文件系统中被删除、移动或重命名时；</li>
<li>当文件元信息发生变化时；</li>
</ul></li>
<li><em>进程调度源</em>通知你与进程有关的事件，如：
<ul>
<li>当一个进程退出时；</li>
<li>当一个进程发出一个<code>fork</code>或<code>exec</code>类型的调用时；</li>
<li>当一个信号被传递给进程时；</li>
</ul></li>
<li><em>机器端口调度源</em>通知与机器有关的事件。</li>
<li><em>自定义调度源</em>可以由自己定义和触发。</li>
</ul>
<p>调度源取代通常用于处理系统相关事件的异步回调函数。当你配置一个调度源时，指定你想监控的事件和调度队列，以及用来处理这些事件的代码。你可以使用block对象或函数指定你的代码。当一个感兴趣的事件到来时，调度源会将你的block或函数提交给指定的调度队列来执行。</p>
<p>与手动提交到队列的任务不同，调度源为程序提供了一个持续的事件源。在你明确取消它之前，一个调度源一直连接到它的调度队列。在连接期间，每当相应的事件发生时，它都会向调度队列提交其相关的任务代码。有些事件，如定时器事件，会定期发生，但大多数事件只是在特定条件出现时零星地发生。出于这个原因，调度源保留其相关的调度队列，以防止它在事件可能仍在等待时被过早释放。</p>
<p>为了防止事件积压在调度队列中，调度源实施了一个事件合并（coalescing）方案。如果一个新的事件在前一个事件的handler被取消排队并执行之前到达，调度源就会将新的事件数据与旧事件的数据合并起来。根据事件的类型，合并可能会取代旧事件或更新其持有的信息。例如，一个基于信号的调度源只提供关于最近的信号信息，但也报告自上次调用事件handler以来，总共有多少信号被传递。</p>
<h2 id="创建调度源">创建调度源</h2>
<p>创建一个调度源包括创建事件源和调度源本身。事件源是处理这些事件所需的任何本地数据结构。例如，对于一个基于描述符的调度源，你需要打开描述符，而对于一个基于进程的源，你需要获得目标程序的进程ID。当你有了你的事件源，你就可以按以下方法创建相应的调度源：</p>
<ol type="1">
<li>使用 <code>dispatch_source_create</code> 函数创建调度源。</li>
<li>配置调度源：
<ul>
<li>为调度源分配一个事件handler；可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/GCDWorkQueues/GCDWorkQueues.html#//apple_ref/doc/uid/TP40008091-CH103-SW13">Writing and Installing an Event Handler</a>。</li>
<li>对于定时器源，使用<code>dispatch_source_set_timer</code>函数设置定时器信息；可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/GCDWorkQueues/GCDWorkQueues.html#//apple_ref/doc/uid/TP40008091-CH103-SW2">Creating a Timer</a>。</li>
</ul></li>
<li>可以选择给调度源分配一个取消handler；可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/GCDWorkQueues/GCDWorkQueues.html#//apple_ref/doc/uid/TP40008091-CH103-SW14">Installing a Cancellation Handler</a>。</li>
<li>调用<code>dispatch_resume</code>函数开始处理事件；可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/GCDWorkQueues/GCDWorkQueues.html#//apple_ref/doc/uid/TP40008091-CH103-SW8">Suspending and Resuming Dispatch Sources</a>。</li>
</ol>
<p>由于调度源在使用前需要一些额外的配置，<code>dispatch_source_create</code>函数在暂停状态下返回调度源。在暂停状态下，调度源接收事件但不处理它们。这使你有时间配置一个事件handler，并执行处理实际事件所需的其他配置。</p>
<p>下面的章节向你展示了如何配置调度源。关于展示如何配置特定类型的调度源的详细例子，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/GCDWorkQueues/GCDWorkQueues.html#//apple_ref/doc/uid/TP40008091-CH103-SW22">Dispatch Source Examples</a>。关于用来创建和配置调度源的函数的其他信息，可参阅<em>Grand Central Dispatch (GCD) Reference</em>。</p>
<h3 id="编写和配置一个事件handler">编写和配置一个事件Handler</h3>
<p>为了处理由调度源产生的事件，你必须定义一个事件handler来处理这些事件。事件handler是一个函数或block对象，用<code>dispatch_source_set_event_handler</code>或<code>dispatch_source_set_event_handler_f</code>函数将其配置在调度源上。当一个事件到来时，调度源会将事件handler提交给指定的调度队列进行处理。</p>
<p>你的事件handler的主体负责处理任何到达的事件。如果你的事件handler已经在队列中并等待处理一个事件，当一个新的事件到达时，调度源会将这两个事件合并起来。一个事件handler通常只看到最近的事件的信息，但根据调度源的类型，它也可以获得其他已经发生并被合并的事件的信息。如果一个或多个新的事件在事件handler开始执行后到达，调度源会保留这些事件，直到当前事件handler执行完毕。这时，它将事件handler与新的事件一起再次提交给队列。</p>
<p>基于函数的事件handler接受一个单一的上下文指针，包含调度源对象，并且不返回任何值。基于block的事件handler不接受参数，也没有返回值。</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Block-based event handler</span></span><br><span class="line"><span class="keyword">void</span> (^dispatch_block_t)(<span class="keyword">void</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Function-based event handler</span></span><br><span class="line"><span class="keyword">void</span> (*dispatch_function_t)(<span class="keyword">void</span> *)</span><br></pre></td></tr></table></figure>
<p>在事件handler中，你可以从调度源本身获得关于给定事件的信息。尽管基于函数的事件handler被传递一个指向调度源的指针作为参数，但基于block的事件handler必须自己捕获这个指针。你可以通过正常引用包含调度源的变量来实现捕获指针。例如，下面的代码片段捕获了<code>source</code>变量，它被声明在block的范围之外。</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,</span><br><span class="line">                                 myDescriptor, <span class="number">0</span>, myQueue);</span><br><span class="line">dispatch_source_set_event_handler(source, ^&#123;</span><br><span class="line">   <span class="comment">// Get some data from the source variable, which is captured</span></span><br><span class="line">   <span class="comment">// from the parent context.</span></span><br><span class="line">   size_t estimated = dispatch_source_get_data(source);</span><br><span class="line"> </span><br><span class="line">   <span class="comment">// Continue reading the descriptor...</span></span><br><span class="line">&#125;);</span><br><span class="line">dispatch_resume(source);</span><br></pre></td></tr></table></figure>
<p>在block内捕获变量通常是为了获得更大的灵活性和动态性。当然，捕获的变量在block内默认为只读。尽管block功能提供了对特定情况下修改捕获变量的支持，但你不应该试图在与调度源相关的事件handler中这样做。调度源总是异步地执行它们的事件handler，所以当你的事件handler执行时，你捕获的任何变量的定义作用域很可能已经消失。关于如何在block内捕获和使用变量的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html#//apple_ref/doc/uid/TP40007502">Blocks Programming Topics</a>。</p>
<p>表4-1列出了可以从事件handler代码中调用的函数，以获取事件的信息。</p>
<p><strong>Table 4-1</strong> 从调度源获取数据</p>
<p><code>dispatch_source_get_handle</code>：该函数返回调度源所管理的底层系统数据类型。</p>
<ul>
<li>对于描述符调度源，该函数返回一个包含与调度源相关的描述符的<code>int</code>类型。</li>
<li>对于一个信号调度源，该函数返回一个<code>int</code>类型，包含最近事件的信号编号。</li>
<li>对于一个进程调度源，此函数返回一个<code>pid_t</code>数据结构，用于被监控的进程。</li>
<li>对于一个Mach端口调度源，此函数返回一个<code>mach_port_t</code>数据结构。</li>
<li>对于其他调度源，此函数返回的值是未定义的。</li>
</ul>
<p><code>dispatch_source_get_data</code> ：此函数返回与事件相关的任何未决（pending）数据。</p>
<ul>
<li><p>对于从文件中读取数据的描述符调度源，该函数返回可供读取的字节数。</p></li>
<li><p>对于向文件写数据的描述符调度源，如果有空间可供写入，该函数返回一个正整数。</p></li>
<li><p>对于监视文件系统活动的描述符调度源，该函数返回一个<code>dispatch_source_vnode_flags_t</code>枚举，表示所发生的事件的类型。</p></li>
<li><p>对于一个进程调度源，这个函数返回一个<code>dispatch_source_proc_flags_t</code>枚举，表示发生的事件类型。</p></li>
<li><p>对于Mach端口调度源，此函数返回一个<code>dispatch_source_machport_flags_t</code>枚举，表示发生的事件类型。</p></li>
<li><p>对于自定义调度源，此函数返回从现有数据和传递给<code>dispatch_source_merge_data</code>函数的新数据创建的新数据值。</p></li>
</ul>
<p><code>dispatch_source_get_mask</code>：该函数返回用于创建调度源的事件标志。</p>
<ul>
<li><p>对于一个进程调度源，该函数返回调度源所接收的事件的掩码（<code>dispatch_source_proc_flags_t</code>）。</p></li>
<li><p>对于具有发送权限的Mach端口调度源，此函数返回所需事件的掩码（<code>dispatch_source_mach_send_flags_t</code>）。</p></li>
<li><p>对于一个自定义OR调度源，此函数返回用于合并数据值的掩码。</p></li>
</ul>
<p>关于如何为特定类型的调度源编写和配置事件handler的例子，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/GCDWorkQueues/GCDWorkQueues.html#//apple_ref/doc/uid/TP40008091-CH103-SW22">Dispatch Source Examples</a>。</p>
<h3 id="配置取消handler">配置取消Handler</h3>
<p>取消handler用于在调度源被释放之前对其进行清理。对于大多数类型的调度源，取消handler是可选的，只有当你有一些与调度源绑定的自定义行为也需要被更新时才有必要。然而，对于使用描述符或Mach端口的调度源，你必须提供一个取消handler来关闭描述符或释放Mach端口。如果不这样做，这些结构体被你的代码和系统的其他部分无意地重用，可能会导致代码中出现微妙的错误。</p>
<p>可以在任何时候配置取消handler，但通常在创建调度源时进行配置。你可以使用<code>dispatch_source_set_cancel_handler</code>或<code>dispatch_source_set_cancel_handler_f</code>函数来配置取消handler，这取决于你想在实现中使用一个block对象还是一个函数。下面的例子显示了一个简单的取消handler，它关闭了一个为调度源打开的描述符。<code>fd</code>变量是一个包含描述符的捕获变量。</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">dispatch_source_set_cancel_handler(mySource, ^&#123;</span><br><span class="line">   close(fd); <span class="comment">// Close a file descriptor opened earlier.</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<h3 id="修改目标队列">修改目标队列</h3>
<p>尽管你在创建调度源时指定了运行事件和取消handler的队列，但你可以在任何时候使用 <code>dispatch_set_target_queue</code> 函数改变该队列。通过这样你可以改变调度源的事件处理的优先级。</p>
<p>修改调度源的队列是一个异步操作，调度源会尽最大努力尽快做出修改。如果一个事件handler已经在队列中并等待处理，它将在之前的队列中执行。然而，在你修改的时候，其他到达的事件可以在任一队列中处理。</p>
<h3 id="关联自定义数据与调度源">关联自定义数据与调度源</h3>
<p>像Grand Central Dispatch中的许多其他数据类型一样，你可以使用<code>dispatch_set_context</code>函数来将自定义数据与调度源关联起来。可以使用上下文指针来存储事件handler在处理事件时需要的任何数据。如果你确实在上下文指针中存储了任何自定义数据，你也应该设置一个取消handler，以便在不再需要调度源时释放这些数据。</p>
<p>如果你使用block来实现你的事件handler，也可以捕获局部变量并在基于block的代码中使用它们。尽管这可能减轻了在调度源的上下文指针中存储数据的需要，但你应该始终谨慎地使用这一功能。因为调度源在程序中可能是长期存在的，在捕获包含指针的变量时应该小心。如果指针所指向的数据在任何时候都可能被释放，你应该复制该数据或保留它。在这两种情况下，你都需要配置一个取消handler来释放这些数据。</p>
<h3 id="调度源的内存管理">调度源的内存管理</h3>
<p>像其他调度对象一样，调度源也是有引用计数的数据类型。一个调度源的初始引用计数为1，可以使用<code>dispatch_retain</code>和<code>dispatch_release</code>函数保留和释放。当一个队列的引用计数达到0时，系统会自动释放调度源的数据结构。</p>
<p>由于它们的使用方式，调度源的所有权可以由内部管理，也可以由外部管理。对于外部所有权，另一个对象或一段代码拥有调度源的所有权，并负责在不再需要它时将其释放。对于内部所有权，调度源持有自己，并负责在适当的时候释放自己。尽管外部所有权非常普遍，但在你想创建一个自主的调度源并让它管理你的代码的某些行为而不进行任何进一步的交互的情况下，你可能会使用内部所有权。例如，如果一个调度源被设计为响应一个单一的全局事件，你可能会让它处理该事件，然后立即退出。</p>
<h2 id="调度源示例">调度源示例</h2>
<p>下面的章节向你展示了如何创建和配置一些更常用的调度源。关于配置特定类型的调度源的更多信息，可参阅<em>Grand Central Dispatch (GCD) Reference</em>。</p>
<h3 id="创建定时器">创建定时器</h3>
<p>定时器调度源以定期、基于时间的间隔产生事件。你可以使用定时器来启动需要定期执行的特定任务。例如，游戏和其他图形密集型的程序可以使用定时器来启动屏幕或动画的更新。你也可以设置一个定时器并使用产生的事件来检查经常更新的服务器上的新信息。</p>
<p>所有的定时器调度源都是间隔性的定时器，也就是说，一旦创建，它们就会按照你指定的时间间隔定期发送事件。当你创建一个定时器调度源时，你必须指定的一个值是一个leeway值，以使系统知道定时器事件的所需精度。leeway值让系统在如何管理电源和唤醒内核方面有一定的灵活性。例如，系统可能会使用leeway值来提前或推迟启动时间，并使其与其他系统事件更好地协调。因此，你应该尽可能为你自己的定时器指定一个leeway值。</p>
<p><strong>注意：</strong>即使你指定了一个0的leeway值，你也不应该期望定时器在你要求的精确纳秒处启动。系统会尽力满足你的需求，但不能保证精确的启动时间。</p>
<p>当计算机进入睡眠状态时，所有的定时器调度源都被暂停。当计算机唤醒时，这些定时器调度源也会被自动唤醒。根据定时器的配置，这种性质的暂停可能会影响定时器下一次触发的时间。如果你使用<code>dispatch_time</code>函数或<code>DISPATCH_TIME_NOW</code>常数来设置你的定时器调度源，定时器调度源会使用默认的系统时钟来决定何时启动。然而，当计算机处于睡眠状态时，默认的时钟不会前进。相比之下，当你使用<code>dispatch_walltime</code>函数设置你的定时器调度源时，定时器调度源会跟踪其触发时间到绝对（wall）的时钟时间。后者通常适用于触发间隔比较大的定时器，因为它可以防止事件时间之间有太大的漂移。</p>
<p>清单4-1显示了一个定时器的例子，它每30秒触发一次，leeway为1秒。因为定时器的时间间隔比较大，所以使用<code>dispatch_walltime</code>函数来创建调度源。计时器的第一次触发立即发生，随后的事件每30秒到达。<code>MyPeriodicTask</code>和<code>MyStoreTimer</code>符号代表自定义函数，编写这些函数来实现定时器行为，并将定时器存储在程序数据结构的某个地方。</p>
<p>下面展示了一个间隔 30s leeaway 1s 的timer。因为间隔较大，dispatch source 使用 <code>dispatch_walltime</code> 来创建的。timer 初次会立即 fire，之后每 30s 到达一次。</p>
<p><strong>清单4-1</strong> 创建定时器数据源</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">dispatch_source_t CreateDispatchTimer(uint64_t interval,</span><br><span class="line">              uint64_t leeway,</span><br><span class="line">              <span class="built_in">dispatch_queue_t</span> queue,</span><br><span class="line">              dispatch_block_t block)</span><br><span class="line">&#123;</span><br><span class="line">   dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,</span><br><span class="line">                                                     <span class="number">0</span>, <span class="number">0</span>, queue);</span><br><span class="line">   <span class="keyword">if</span> (timer)</span><br><span class="line">   &#123;</span><br><span class="line">      dispatch_source_set_timer(timer, dispatch_walltime(<span class="literal">NULL</span>, <span class="number">0</span>), interval, leeway);</span><br><span class="line">      dispatch_source_set_event_handler(timer, block);</span><br><span class="line">      dispatch_resume(timer);</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">return</span> timer;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">void</span> MyCreateTimer()</span><br><span class="line">&#123;</span><br><span class="line">   dispatch_source_t aTimer = CreateDispatchTimer(<span class="number">30</span>ull * <span class="built_in">NSEC_PER_SEC</span>,</span><br><span class="line">                               <span class="number">1</span>ull * <span class="built_in">NSEC_PER_SEC</span>,</span><br><span class="line">                               dispatch_get_main_queue(),</span><br><span class="line">                               ^&#123; MyPeriodicTask(); &#125;);</span><br><span class="line"> </span><br><span class="line">   <span class="comment">// Store it somewhere for later use.</span></span><br><span class="line">    <span class="keyword">if</span> (aTimer)</span><br><span class="line">    &#123;</span><br><span class="line">        MyStoreTimer(aTimer);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>尽管创建一个定时器调度源是接收基于时间的事件的主要方式，但也有其他选择。如果你想在指定的时间间隔后执行一次block，你可以使用<code>dispatch_after</code>或<code>dispatch_after_f</code>函数。这个函数的作用与<code>dispatch_async</code>函数很相似，只是它允许你指定一个时间值，在这个时间值上将block提交给队列。根据你的需要，时间值可以指定为一个相对的或绝对的时间值。</p>
<h3 id="从描述符中读取数据">从描述符中读取数据</h3>
<p>要从文件或套接字中读取数据，你必须打开文件或套接字，并创建一个<code>DISPATCH_SOURCE_TYPE_READ</code>类型的调度源。指定的事件handler应该能够读取和处理文件描述符的内容。在处理文件的情况下，这相当于读取文件数据（或该数据的一个子集），并为程序创建适当的数据结构。对于网络套接字，这涉及到处理新收到的网络数据。</p>
<p>每当读取数据时，你应该始终将描述符配置为使用非阻塞操作。尽管可以使用<code>dispatch_source_get_data</code>函数来查看有多少数据可供读取，但该函数返回的值在你调用时和你实际读取数据时可能发生变化。如果底层文件被截断或发生网络错误，从描述符中读取的数据会阻塞当前线程，从而使你的事件handler在执行过程中卡死，阻塞调度队列其他任务。对于一个串行队列，这可能会使队列造成死锁，甚至对于一个并发队列，这也会削减可启动的新任务的数量。</p>
<p>清单4-2显示了一个配置调度源以从文件中读取数据的例子。在这个例子中，事件handler将指定文件的全部内容读入一个缓冲区，并调用一个自定义函数来处理这些数据。该函数的调用者将使用返回的调度源，在读取操作完成后取消它。为了确保调度队列在没有数据可读时不会出现不必要的阻塞，本例使用<code>fcntl</code>函数来配置文件描述符，使其执行非阻塞操作。配置在调度源上的取消handler确保文件描述符在数据被读取后被关闭。</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">dispatch_source_t ProcessContentsOfFile(<span class="keyword">const</span> <span class="keyword">char</span>* filename)</span><br><span class="line">&#123;</span><br><span class="line">   <span class="comment">// Prepare the file for reading.</span></span><br><span class="line">   <span class="keyword">int</span> fd = open(filename, O_RDONLY);</span><br><span class="line">   <span class="keyword">if</span> (fd == <span class="number">-1</span>)</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">   fcntl(fd, F_SETFL, O_NONBLOCK);  <span class="comment">// Avoid blocking the read operation</span></span><br><span class="line"> </span><br><span class="line">   <span class="built_in">dispatch_queue_t</span> queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>);</span><br><span class="line">   dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,</span><br><span class="line">                                   fd, <span class="number">0</span>, queue);</span><br><span class="line">   <span class="keyword">if</span> (!readSource)</span><br><span class="line">   &#123;</span><br><span class="line">      close(fd);</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">   &#125;</span><br><span class="line"> </span><br><span class="line">   <span class="comment">// Install the event handler</span></span><br><span class="line">   dispatch_source_set_event_handler(readSource, ^&#123;</span><br><span class="line">      size_t estimated = dispatch_source_get_data(readSource) + <span class="number">1</span>;</span><br><span class="line">      <span class="comment">// Read the data into a text buffer.</span></span><br><span class="line">      <span class="keyword">char</span>* buffer = (<span class="keyword">char</span>*)malloc(estimated);</span><br><span class="line">      <span class="keyword">if</span> (buffer)</span><br><span class="line">      &#123;</span><br><span class="line">         ssize_t actual = read(fd, buffer, (estimated));</span><br><span class="line">         Boolean done = MyProcessFileData(buffer, actual);  <span class="comment">// Process the data.</span></span><br><span class="line"> </span><br><span class="line">         <span class="comment">// Release the buffer when done.</span></span><br><span class="line">         free(buffer);</span><br><span class="line"> </span><br><span class="line">         <span class="comment">// If there is no more data, cancel the source.</span></span><br><span class="line">         <span class="keyword">if</span> (done)</span><br><span class="line">            dispatch_source_cancel(readSource);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;);</span><br><span class="line"> </span><br><span class="line">   <span class="comment">// Install the cancellation handler</span></span><br><span class="line">   dispatch_source_set_cancel_handler(readSource, ^&#123;close(fd);&#125;);</span><br><span class="line"> </span><br><span class="line">   <span class="comment">// Start reading the file.</span></span><br><span class="line">   dispatch_resume(readSource);</span><br><span class="line">   <span class="keyword">return</span> readSource;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>上面的例子中，自定义的<code>MyProcessFileData</code>函数决定了什么时候已经读取了足够的文件数据，什么时候取消调度源。默认情况下，为从描述符中读取数据而配置的调度源会在仍有数据需要读取时重复调度其事件handler。如果套接字连接关闭或到达文件的末尾，调度源会自动停止调度事件handler。如果确定不需要一个调度源，可以自己直接取消它。</p>
<h3 id="把数据写入描述符中">把数据写入描述符中</h3>
<p>向文件或套接字写数据的过程与读数据的过程非常相似。在为写操作配置描述符后，你要创建一个<code>DISPATCH_SOURCE_TYPE_WRITE</code>类型的调度源。一旦该调度源被创建，系统就会调用你的事件handler，让它有机会开始向文件或套接字写入数据。当你写完数据后，使用<code>dispatch_source_cancel</code>函数来取消调度源。</p>
<p>无论什么时候写数据，你都应该将文件描述符配置为使用非阻塞操作。尽管你可以使用<code>dispatch_source_get_data</code>函数来查看有多少空间可供写入，但该函数返回的值只是指导性的，在你调用时和你实际写入数据时可能发生变化。如果发生错误，向一个阻塞的文件描述符写入数据可能会使你的事件handler在执行过程中卡死，并阻塞调度队列其他任务。对于一个串行队列，这可能会使你的队列造成死锁，甚至对于一个并发队列，这也会削减可以启动的新任务的数量。</p>
<p>清单4-3显示了使用调度源向文件写入数据的基本方法。在创建新文件后，该函数将产生的文件描述符传递给其事件handler。被放入文件的数据是由<code>MyGetData</code>函数提供的，你可以用需要的任何代码来替换它，以生成文件的数据。将数据写入文件后，事件handler取消了调度源，以防止它被再次调用。然后，调度源的所有者将负责释放它。</p>
<p><strong>清单4-3</strong> 向文件写入数据</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">dispatch_source_t WriteDataToFile(<span class="keyword">const</span> <span class="keyword">char</span>* filename)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">int</span> fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC,</span><br><span class="line">                      (S_IRUSR | S_IWUSR | S_ISUID | S_ISGID));</span><br><span class="line">    <span class="keyword">if</span> (fd == <span class="number">-1</span>)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    fcntl(fd, F_SETFL); <span class="comment">// Block during the write.</span></span><br><span class="line"> </span><br><span class="line">    <span class="built_in">dispatch_queue_t</span> queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>);</span><br><span class="line">    dispatch_source_t writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE,</span><br><span class="line">                            fd, <span class="number">0</span>, queue);</span><br><span class="line">    <span class="keyword">if</span> (!writeSource)</span><br><span class="line">    &#123;</span><br><span class="line">        close(fd);</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"> </span><br><span class="line">    dispatch_source_set_event_handler(writeSource, ^&#123;</span><br><span class="line">        size_t bufferSize = MyGetDataSize();</span><br><span class="line">        <span class="keyword">void</span>* buffer = malloc(bufferSize);</span><br><span class="line"> </span><br><span class="line">        size_t actual = MyGetData(buffer, bufferSize);</span><br><span class="line">        write(fd, buffer, actual);</span><br><span class="line"> </span><br><span class="line">        free(buffer);</span><br><span class="line"> </span><br><span class="line">        <span class="comment">// Cancel and release the dispatch source when done.</span></span><br><span class="line">        dispatch_source_cancel(writeSource);</span><br><span class="line">    &#125;);</span><br><span class="line"> </span><br><span class="line">    dispatch_source_set_cancel_handler(writeSource, ^&#123;close(fd);&#125;);</span><br><span class="line">    dispatch_resume(writeSource);</span><br><span class="line">    <span class="keyword">return</span> (writeSource);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="监控文件系统对象">监控文件系统对象</h3>
<p>如果你想监视一个文件系统对象的变化，你可以设置一个<code>DISPATCH_SOURCE_TYPE_VNODE</code>类型的调度源。你可以使用这种类型的调度源，在文件被删除、写入或重命名时接收通知。你也可以用它在文件的特定类型的元信息（如它的大小和链接数）发生变化时得到通知。</p>
<p><strong>注意：</strong>你为调度源指定的文件描述符必须在源本身处理事件时保持打开。</p>
<p>清单4-4显示了一个例子，它监视一个文件名变化，并在它发生变化时执行一些自定义行为。(你可以提供实际的行为来代替例子中调用的 <code>MyUpdateFileName</code> 函数。)因为一个描述符是专门为调度源打开的，所以调度源包含一个关闭描述符的取消handler。因为本例创建的文件描述符与底层文件系统对象相关联，相同的调度源可以用来检测任何数量的文件名变化。</p>
<p><strong>清单4-4</strong> 观察文件名变化</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">dispatch_source_t MonitorNameChangesToFile(<span class="keyword">const</span> <span class="keyword">char</span>* filename)</span><br><span class="line">&#123;</span><br><span class="line">   <span class="keyword">int</span> fd = open(filename, O_EVTONLY);</span><br><span class="line">   <span class="keyword">if</span> (fd == <span class="number">-1</span>)</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"> </span><br><span class="line">   <span class="built_in">dispatch_queue_t</span> queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>);</span><br><span class="line">   dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE,</span><br><span class="line">                fd, DISPATCH_VNODE_RENAME, queue);</span><br><span class="line">   <span class="keyword">if</span> (source)</span><br><span class="line">   &#123;</span><br><span class="line">      <span class="comment">// Copy the filename for later use.</span></span><br><span class="line">      <span class="keyword">int</span> length = strlen(filename);</span><br><span class="line">      <span class="keyword">char</span>* newString = (<span class="keyword">char</span>*)malloc(length + <span class="number">1</span>);</span><br><span class="line">      newString = strcpy(newString, filename);</span><br><span class="line">      dispatch_set_context(source, newString);</span><br><span class="line"> </span><br><span class="line">      <span class="comment">// Install the event handler to process the name change</span></span><br><span class="line">      dispatch_source_set_event_handler(source, ^&#123;</span><br><span class="line">            <span class="keyword">const</span> <span class="keyword">char</span>*  oldFilename = (<span class="keyword">char</span>*)dispatch_get_context(source);</span><br><span class="line">            MyUpdateFileName(oldFilename, fd);</span><br><span class="line">      &#125;);</span><br><span class="line"> </span><br><span class="line">      <span class="comment">// Install a cancellation handler to free the descriptor</span></span><br><span class="line">      <span class="comment">// and the stored string.</span></span><br><span class="line">      dispatch_source_set_cancel_handler(source, ^&#123;</span><br><span class="line">          <span class="keyword">char</span>* fileStr = (<span class="keyword">char</span>*)dispatch_get_context(source);</span><br><span class="line">          free(fileStr);</span><br><span class="line">          close(fd);</span><br><span class="line">      &#125;);</span><br><span class="line"> </span><br><span class="line">      <span class="comment">// Start processing events.</span></span><br><span class="line">      dispatch_resume(source);</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">else</span></span><br><span class="line">      close(fd);</span><br><span class="line"> </span><br><span class="line">   <span class="keyword">return</span> source;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="监控信号">监控信号</h3>
<p>UNIX信号允许从一个程序外对其进行操纵。一个程序可以接收许多不同类型的信号，从不可恢复的错误（如非法指令）到重要信息的通知（如一个子进程退出时）。传统上，程序使用<code>sigaction</code>函数来配置一个信号处理函数，该函数在信号到达后立即同步处理。如果你只是想得到信号到达的通知，而不是真的想处理信号，你可以使用一个信号调度源来异步处理信号。</p>
<p>信号调度源不能替代使用<code>sigaction</code>函数配置的同步信号handler。同步信号handler实际上可以捕获一个信号并防止它终止程序。信号调度源允许你只监控信号的到达。此外，你不能使用信号调度源来检索所有类型的信号。具体来说，你不能用它们来监控<code>SIGILL</code>、<code>SIGBUS</code>和<code>SIGSEGV</code>信号。</p>
<p>因为信号调度源是在调度队列上异步执行的，所以它们不受一些与同步信号handler的限制。例如，你可以从信号调度源的事件handler中调用的函数。这种灵活性增加的代价是，在信号到达和调度源的事件handler被调用之间可能会有一些延迟。</p>
<p>清单4-5显示了如何配置一个信号调度源来处理<code>SIGHUP</code>信号。调度源的事件handler调用了<code>MyProcessSIGHUP</code>函数，你可以在此实现自己的处理信号逻辑。</p>
<p><strong>清单4-5</strong> 配置block监控信号</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="keyword">void</span> InstallSignalHandler()</span><br><span class="line">&#123;</span><br><span class="line">   <span class="comment">// Make sure the signal does not terminate the application.</span></span><br><span class="line">   signal(SIGHUP, SIG_IGN);</span><br><span class="line"> </span><br><span class="line">   <span class="built_in">dispatch_queue_t</span> queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>);</span><br><span class="line">   dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGHUP, <span class="number">0</span>, queue);</span><br><span class="line"> </span><br><span class="line">   <span class="keyword">if</span> (source)</span><br><span class="line">   &#123;</span><br><span class="line">      dispatch_source_set_event_handler(source, ^&#123;</span><br><span class="line">         MyProcessSIGHUP();</span><br><span class="line">      &#125;);</span><br><span class="line"> </span><br><span class="line">      <span class="comment">// Start processing signals</span></span><br><span class="line">      dispatch_resume(source);</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>如果你正在为一个自定义的框架开发代码，使用信号调度源的一个好处是代码可以独立于任何链接到它的程序来监控信号。信号调度源不会干扰其他调度源或程序可能配置的任何同步信号handler。</p>
<h3 id="监控进程">监控进程</h3>
<p>进程调度源可以让你监控一个特定进程的行为，并作出适当的响应。一个父进程可以使用这种调度源来监视它所创建的任何子进程。例如，父进程可以用它来监视一个子进程的结束。同样地，一个子进程可以用它来监视它的父进程，并在父进程退出时退出。</p>
<p>清单4-6显示了配置一个调度源以监视父进程终止的步骤。当父进程终止时，调度源设置一些内部状态信息，让子进程知道它应该退出。(程序需要实现<code>MySetAppExitFlag</code>函数来为终止设置一个适当的标志。) 由于调度源自主运行，因此持有自己，它也会在预期程序关闭的情况下取消和释放自己。</p>
<p><strong>清单4-6</strong> 监控父进程的终止</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="keyword">void</span> MonitorParentProcess()</span><br><span class="line">&#123;</span><br><span class="line">   pid_t parentPID = getppid();</span><br><span class="line"> </span><br><span class="line">   <span class="built_in">dispatch_queue_t</span> queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>);</span><br><span class="line">   dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,</span><br><span class="line">                                                      parentPID, DISPATCH_PROC_EXIT, queue);</span><br><span class="line">   <span class="keyword">if</span> (source)</span><br><span class="line">   &#123;</span><br><span class="line">      dispatch_source_set_event_handler(source, ^&#123;</span><br><span class="line">         MySetAppExitFlag();</span><br><span class="line">         dispatch_source_cancel(source);</span><br><span class="line">         dispatch_release(source);</span><br><span class="line">      &#125;);</span><br><span class="line">      dispatch_resume(source);</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="取消调度源">取消调度源</h2>
<p>调度源一直处于活动状态，直到你使用<code>dispatch_source_cancel</code>函数显式取消它们。取消一个调度源会停止新事件的传递，并且不能被撤销。因此，通常取消一个调度源，然后就立即释放它，如下所示：</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="keyword">void</span> RemoveDispatchSource(dispatch_source_t mySource)</span><br><span class="line">&#123;</span><br><span class="line">   dispatch_source_cancel(mySource);</span><br><span class="line">   dispatch_release(mySource);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>取消一个调度源是一个异步操作。尽管在你调用<code>dispatch_source_cancel</code>函数后，没有新的事件被处理，但已经被调度源处理的事件还是继续被处理。在处理完任何最终事件后，如果有取消handler，调度源会执行其取消handler。</p>
<p>取消handler是你释放内存或清理代表调度源获取的任何资源的机会。如果调度源使用描述符或mach端口，你必须提供一个取消handler，以便在取消发生时关闭描述符或销毁端口。其他类型的调度源不需要取消handler，但如果你将任何内存或数据与调度源关联，仍应提供。例如，如果你在调度源的上下文指针中存储数据，你应提供取消handler。关于取消handler的更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/GCDWorkQueues/GCDWorkQueues.html#//apple_ref/doc/uid/TP40008091-CH103-SW14">Installing a Cancellation Handler</a>。</p>
<h2 id="暂停和恢复调度源">暂停和恢复调度源</h2>
<p>你可以使用<code>dispatch_suspend</code>和<code>dispatch_resume</code>方法暂停和恢复调度源事件的传递。这些方法为调度对象增加和减少暂停计数。因此，你必须在每次平衡调用<code>dispatch_suspend</code>与调用<code>dispatch_resume</code>。</p>
<p>当暂停一个调度源时，任何在该调度源被暂停时发生的事件都会被收集起来，直到队列恢复。当队列恢复时，不是发送所有的事件，而是在发送前将这些事件合并成一个单一的事件。例如，如果你正在监控一个文件的名称变化，发送的事件将只包括最后的名称变化。以这种方式合并事件，可以防止它们在队列中堆积，并在工作恢复时让你的程序应付不来。</p>
<h2 id="总结">总结</h2>
<ul>
<li>调度源用于监听底层（系统、内核）事件，实现处理事件异步回调。既然是用于监听，对应的就该主动取消。</li>
<li>在Swift中，根据调度源类型，有对应的协议。但创建都是使用DispatchSource对应的make类工厂方法创建特定类型的调度源。这样接收的调度源返回值就可以调用特定类型协议的具体方法。</li>
<li>调度源的通用方法都定义在DispatchSourceProtocol。
<ul>
<li>配置：<code>setRegistrationHandler</code>、<code>setEventHandler</code>、<code>setCancelHandler</code></li>
<li>基本操作：<code>activate</code>、<code>cancel</code>、<code>suspend</code>、<code>resume</code>、</li>
</ul></li>
</ul>
]]></content>
      <categories>
        <category>翻译</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>多线程</tag>
        <tag>Concurrency Programming Guide</tag>
      </tags>
  </entry>
  <entry>
    <title>Concurrency Programming Guide：迁移线程代码</title>
    <url>/posts/concurrency_pg_migrating_away_from_threads/</url>
    <content><![CDATA[<p>有很多方法可以调整现有的线程代码，以利用Grand Central Dispatch和操作对象的优势。虽然不是在所有情况下都能摆脱线程，但在你进行转换的地方，性能（以及代码的简单性）可以得到极大的改善。具体来说，使用调度队列和操作队列而取代线程有几个优势：</p>
<span id="more"></span>
<ul>
<li>减少了程序为在内存空间中存储线程堆栈的内存占用。</li>
<li>消除了创建和配置线程所需的代码。</li>
<li>消除了管理和安排线程工作所需的代码。</li>
<li>减少了代码量。</li>
</ul>
<p>本章提供了一些技巧和指南，说明如何替换现有的基于线程的代码，转而使用调度队列和操作队列来实现相同类型的行为。</p>
<h2 id="用调度队列替换线程">用调度队列替换线程</h2>
<p>要了解如何用调度队列替换线程，首先要考虑在程序中使用线程的一些方式：</p>
<ul>
<li><strong>单一任务线程</strong>。创建一个线程来执行一个单一的任务，当任务完成后释放该线程。</li>
<li><strong>工作线程</strong>。创建一个或多个工作线程，每个线程都有特定的任务。定期向每个线程调度任务。</li>
<li><strong>线程池</strong>。创建一个通用线程池，并为每个线程设置run loop。当你有任务要执行时，从池子里取一个线程，把任务调度给它。如果没有空闲的线程，就把任务排入队列，等待可用的线程。</li>
</ul>
<p>尽管这些看起来是截然不同的技术，但它们实际上只是同一原则的变种。在以上的每种使用方式，线程都被用来运行程序必须执行的一些任务。它们之间唯一的区别是用于管理线程和任务队列的代码。通过使用调度队列和操作队列，可以消除所有线程和线程通信的代码，让你专注于要执行的任务。</p>
<p>如果你正在使用上述线程模型，你应该和清楚程序要执行任务类型。与其将一个任务提交给你的一个自定义线程，不如尝试将该任务封装在一个操作对象或一个block对象中，并将其调度到适当的队列中。对于那些不是特别有争议的任务（不需要锁的任务），你应该能进行以下的直接替换：</p>
<ul>
<li>对于单个任务线程，将任务封装在一个block或操作对象中，并将其提交给一个并发队列。</li>
<li>对于工作线程，你需要决定是使用一个串行队列还是一个并发队列。如果你使用工作现场来同步执行特定的任务集，请使用串行队列。如果你确实使用工作现场来执行没有相互依赖关系的任意任务，则使用并发队列。</li>
<li>对于线程池，将你的任务封装在一个block或操作对象中，并将它们调度到一个并发队列中执行。</li>
</ul>
<p>当然，像这样简单的替换可能并不是在所有情况下都适用。如果你正在执行的任务存在争夺共享资源，理想的解决方案是首先尝试消除或尽量减少这种争夺。如果你有办法重构你的代码以消除对共享资源的相互依赖，这当然是最好的。但是，如果做不到，或者效率较低，那么还是有办法利用队列的优势。队列的一大优势是，它们提供了一种更可预测的方式来执行你的代码。这种可预测性意味着仍有办法在不使用锁或其他重量级同步机制的情况下同步执行你的代码。你可以使用队列来执行许多相同的任务，而不是使用锁。</p>
<ul>
<li>如果是必须按特定顺序执行的任务，可以把它们提交给一个串行调度队列。或使用操作对象依赖来确保以特定的顺序执行。</li>
<li>如果目前使用锁来保护一个共享资源，创建一个串行队列来执行任何修改该资源的任务。然后，使用串行队列将取代现有的锁作为同步机制的代码。关于摆脱锁的更多技术，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/ThreadMigration/ThreadMigration.html#//apple_ref/doc/uid/TP40008091-CH105-SW3">Eliminating Lock-Based Code</a>。</li>
<li>如果在用线程连接来等待后台任务的完成，可以考虑使用调度组来替换。也可以使用<code>NSBlockOperation</code>对象或操作对象依赖来实现类似的组完成行为。关于如何跟踪执行任务的组，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/ThreadMigration/ThreadMigration.html#//apple_ref/doc/uid/TP40008091-CH105-SW6">Replacing Thread Joins</a>。</li>
<li>如果在使用生产者-消费者算法来管理有限资源池，可以考虑将实现改为<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/ThreadMigration/ThreadMigration.html#//apple_ref/doc/uid/TP40008091-CH105-SW7">修改生产者-消费者实现</a>中所述的方案。</li>
<li>如果在使用线程从描述符中读写，或监视文件操作，可以改用调度源实现。</li>
</ul>
<p>重要的是要记住，队列并不是取代线程的万金油。队列提供的异步编程模型适用于允许延迟的场景。即使队列提供了配置任务执行优先级的方法，但较高的执行优先级并不能保证任务在特定的时间执行。因此，在需要尽可能避免延迟的情况下，线程仍然是一个更合适的选择，例如在音频和视频播放的场景。</p>
<h2 id="消除基于锁的代码">消除基于锁的代码</h2>
<p>对于线程代码，锁是同步访问线程间共享资源的传统方式之一。然而，锁的使用是有代价的。即使在无竞态条件的情况下，使用锁也会有性能损失。而在竞态条件的情况下，一或多个线程有可能在等待锁被释放的过程中阻塞不确定的时间。</p>
<p>用队列取代基于锁的代码，可以消除许多与锁相关的损耗，同时也简化了剩余的代码。你可以创建一个队列来串行访问该资源，而不是使用锁来保护一个共享资源。队列不会像锁那样带来性能损耗。例如，排队的任务不需要进入内核来获取互斥锁。</p>
<p>当排队任务时，你只需决定是同步还是异步进行。异步提交任务可以让当前线程在执行任务时继续运行。同步提交任务则会阻塞当前线程的运行，直到任务完成。这两个情况都有适当的用途，但只要有可能，异步提交任务肯定是更优的。</p>
<p>下面几节将向你展示如何用等价的基于队列的代码来替换现有的基于锁的代码。</p>
<h3 id="实现异步锁">实现异步锁</h3>
<p>异步锁是一种保护共享资源的方式，它不会阻塞任何修改该资源的代码。当你需要修改一个数据结构，会影响其他的任务时，你可能会使用异步锁。使用传统的线程，通常的方式是为共享资源加锁，然后进行必要的修改，释放锁，然后继续完成任务。然而，使用调度队列，调用的代码可以异步地进行修改，而不必等待这些修改完成。</p>
<p>清单5-1显示了一个异步锁实现的例子。在这个例子中，受保护的资源定义了自己的串行调度队列。调用代码向这个队列提交一个block对象，其中包含需要对资源进行的修改。因为队列本身是串行执行block的，所以对资源的修改保证按照接收的顺序进行；但是，因为任务是异步执行的，所以调用线程不会阻塞。</p>
<p><strong>清单5-1</strong> 异步修改保护的资源</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">dispatch_async</span>(obj-&gt;serial_queue, ^&#123;</span><br><span class="line">   <span class="comment">// Critical section</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<h3 id="同步执行关键代码">同步执行关键代码</h3>
<p>如果当前的代码在某个任务完成之前不能继续，你可以使用<code>dispatch_sync</code>函数同步提交该任务。这个函数将任务添加到一个调度队列中，然后阻塞当前线程，直到任务执行完毕。根据你的需要，调度队列本身可以是一个串行或并发队列。因为这个函数会阻塞当前线程，所以你应该只在必要时使用它。清单5-2显示了使用<code>dispatch_sync</code>来包装代码的关键部分的技术。</p>
<p><strong>清单5-2</strong> 同步执行关键代码</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">dispatch_sync</span>(my_queue, ^&#123;</span><br><span class="line">   <span class="comment">// Critical section</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>如果你已经在使用一个串行队列来保护共享资源，同步调度到该队列并不会比异步调度更能保护共享资源。使用同步调度的是为了阻塞当前代码，直到关键部分完成。例如，如果你想从共享资源中获取一些值并立即使用它，你就需要同步调度。如果当前代码不需要等待关键部分的完成，或者它可以简单地提交后续任务到同一个串行队列中，那么异步提交往往是首选。</p>
<h2 id="改进循环代码">改进循环代码</h2>
<p>如果代码有循环，并且每次通过循环所做的工作与其他迭代中的工作无关，你可以考虑使用<code>dispatch_apply</code>或<code>dispatch_apply_f</code>函数重新实现该循环代码。这些函数把循环的每个迭代单独提交给一个调度队列进行处理。当与并发队列一起使用时，这个功能可以让你并发地执行循环迭代。</p>
<p>如果你的循环的每次迭代都是相互独立的话，你也许应该考虑使用 <code>dispatch_apply</code> 或 <code>dispatch_apply_f</code> 重新实现你的循环。这两个函数将每个迭代提交给队列处理。当和并行队列一起使用的时候，这个特性让你能够同时进行多个迭代。</p>
<p><code>dispatch_apply</code>和<code>dispatch_apply_f</code>函数是同步函数调用，它们会阻塞当前执行线程，直到所有的循环迭代完成。当提交给一个并发队列时，循环迭代的执行顺序不被保证。运行每个迭代的线程可能会阻塞，导致一个给定的迭代在它周围的其他迭代之前或之后完成。因此，在为每个循环迭代使用的block对象或函数必须是可重入的。</p>
<p>清单5-3显示了如何用基于GCD来替换for循环。传递给<code>dispatch_apply</code>或<code>dispatch_apply_f</code>的block或函数必须取一个整数值，表示当前循环的迭代。在这个例子中，代码只是将当前的循环编号打印到控制台。</p>
<p><strong>清单5-3</strong> 逐步替换for循环</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>);</span><br><span class="line">dispatch_apply(count, queue, ^(size_t i) &#123;</span><br><span class="line">   printf(<span class="string">&quot;%u\n&quot;</span>, i);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>尽管前面的例子是一个简单的例子，但它展示了使用调度队列替换循环的基本技术。尽管这可能是提高基于循环的代码性能的一个好方法，但你仍必须辨证地使用这种技术。尽管调度队列的开销很低，但在一个线程上调度每个循环迭代仍有成本。因此，你应该确保你的循环代码做了足够多的工作来抵消这些成本。确切地说，需要做多少工作是你必须使用性能工具来衡量的事情。</p>
<p>增加每个循环迭代的工作量的一个简单方法是使用striding。使用striding重写你的block，以每次执行原始循环的多个迭代。然后，将指定给<code>dispatch_apply</code>函数的计数值按比例减少。清单5-4显示了如何为清单5-3中的循环代码实现striding。在清单5-4中，该block调用<code>printf</code>语句的次数与stride值相同，在本例中是137。(实际的stride值是你应该根据你的代码所做的工作来配置的)。因为在将总的迭代次数除以stride值时，会有剩余的部分，所以任何剩余的迭代都是直接执行的。</p>
<p><strong>清单5-4</strong> 向调度的for循环增加步幅</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">int stride = 137;</span><br><span class="line">dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);</span><br><span class="line"> </span><br><span class="line">dispatch_apply(count / stride, queue, ^(size_t idx)&#123;</span><br><span class="line">    size_t j = idx * stride;</span><br><span class="line">    size_t j_stop = j + stride;</span><br><span class="line">    do &#123;</span><br><span class="line">       printf(&quot;%u\n&quot;, (unsigned int)j++);</span><br><span class="line">    &#125;while (j &lt; j_stop);</span><br><span class="line">&#125;);</span><br><span class="line"> </span><br><span class="line">size_t i;</span><br><span class="line">for (i = count - (count % stride); i &lt; count; i++)</span><br><span class="line">   printf(&quot;%u\n&quot;, (unsigned int)i);</span><br></pre></td></tr></table></figure>
<p>使用stride有一些明确的性能优势。尤其是当原始循环迭代次数较多时。同时调度较少的block意味着花在执行这些block的代码上的时间比调度它们的时间多。不过和任何性能指标一样，你可能要调整striding的值来达到最佳性能。</p>
<h2 id="替换线程连接">替换线程连接</h2>
<p>线程连接允许你生成一个或多个线程，然后让当前线程等待，直到这些线程完成。为了实现线程连接，一个父线程会创建一个子线程作为<em>可连接线程</em>。当父线程在没有子线程的结果的情况下不能再取得进展时，它就与子线程连接。这个过程会阻塞父线程，直到子线程完成其任务并退出，这时，父线程可以从子线程中收集结果并继续原来的工作。如果父线程需要与多个子线程连接，它只能逐个进行。</p>
<p>调度组提供了类似于线程连接的语义，但也有一些额外的优势。与线程连接一样，调度组是一种让线程阻塞的方式，直到一个或多个子任务执行完毕。与线程连接不同，调度组同时等待其所有子任务。因为调度组使用调度队列来执行工作，所以它们非常高效。</p>
<p>要使用调度组来执行由可连接线程执行的相同工作，你要做的是：</p>
<ol type="1">
<li>使用<code>dispatch_group_create</code>函数创建一个调度组。</li>
<li>使用<code>dispatch_group_async</code>或<code>dispatch_group_async_f</code>函数向该组添加任务。提交给组的每个任务都表示在一个可加入的线程上执行的工作。</li>
<li>当当前线程不能再向前推进时，调用<code>dispatch_group_wait</code>函数来等待该组。这个函数会阻止当前线程，直到该组中的所有任务完成执行。</li>
</ol>
<p>如果你使用操作对象来实现你的任务，你也可以使用依赖关系实现线程连接。与其让一个父线程等待一个或多个任务完成，不如将父线程的代码移到一个操作对象中。然后，你将在父操作对象和任何数量的子操作对象之间建立依赖关系，以完成通常由可连接线程执行的工作。对其他操作对象的依赖关系可以阻塞父操作对象的执行，直到所有的操作都完成。</p>
<p>关于如何使用调度组的例子，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW25">Waiting on Groups of Queued Tasks</a>。关于设置操作对象之间的依赖关系，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW17">Configuring Interoperation Dependencies</a>。</p>
<h2 id="改变生产者-消费者的实现方式">改变生产者-消费者的实现方式</h2>
<p>生产者-消费者模型可以让你管理有限动态生产的资源。当生产者创建新的资源（或任务）时，一个或多个消费者等待这些资源（或任务）就绪，并在它们就绪时消费它们。实现生产者-消费者模型的典型机制是条件（conditions）或信号量。</p>
<p>使用条件，生产者线程通常做以下事情：</p>
<ol type="1">
<li>锁定与条件相关的互斥锁（使用<code>pthread_mutex_lock</code>）。</li>
<li>生产将被消费的资源或任务。</li>
<li>向条件变量发出信号，表示有资源要消耗（使用<code>pthread_cond_signal</code>）。</li>
<li>解锁互斥锁（使用<code>pthread_mutex_unlock</code>）。</li>
</ol>
<p>相应的消费线程会做以下事情：</p>
<ol type="1">
<li>锁定与该条件相关的互斥锁（使用<code>pthread_mutex_lock</code>）。</li>
<li>设置一个<code>while</code>循环，做以下工作：
<ol type="1">
<li>检查是否真的有任务要执行。</li>
<li>如果没有任务要执行（或者没有可用的资源），调用<code>pthread_cond_wait</code>来阻塞当前线程，直到有相应的信号量出现。</li>
</ol></li>
<li>获取生产者提供的任务（或资源）。</li>
<li>解锁互斥锁（使用<code>pthread_mutex_unlock</code>）。</li>
<li>处理任务。</li>
</ol>
<p>通过调度队列，你可以将生产者和消费者的实现简化为单一的调用：</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">dispatch_async</span>(queue, ^&#123;</span><br><span class="line">   <span class="comment">// Process a work item.</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>当你的生产者有任务要执行时，它所要做的就是将该任务添加到队列中，让队列处理该任务。前面的代码中唯一改变的部分是队列类型。如果生产者生成的任务需要按照特定的顺序执行，就使用一个串行队列。如果生产者生成的任务可以并发执行，就把它们添加到一个并发队列中，让系统尽可能地同时执行它们。</p>
<h2 id="替换信号量代码">替换信号量代码</h2>
<p>如果你目前在使用信号量来限制对共享资源的访问，你应考虑使用调度信号量来代替。传统的信号量总是需要调用内核来测试信号量。相反，调度信号量在用户空间中快速测试信号量的状态，并且只有在测试失败和调用线程需要被阻塞时才会进入内核。这种行为的结果是，在没有竞态条件的情况下，调度信号量比传统信号量快得多。不过在其他方面，调度信号量提供了与传统信号量相同的行为。</p>
<p>关于如何使用调度信号的例子，可参阅<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW24">Using Dispatch Semaphores to Regulate the Use of Finite Resources</a>。</p>
<h2 id="替换run-loop代码">替换Run-Loop代码</h2>
<p>如果你正在使用run loop来管理一个或多个线程上执行的工作，你可能会发现队列的实现和维护要简单得多。设置一个自定义的run loop包括设置底层线程和run loop本身。run loop的代码包括设置一个或多个run loop源，并编写回调来处理到达这些源的事件。所有的这些，你可以简单地创建一个串行队列，并向其调度任务。因此，你可以用一行代码取代所有的线程和run loop创建代码。</p>
<figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="built_in">dispatch_queue_t</span> myNewRunLoop = dispatch_queue_create(<span class="string">&quot;com.apple.MyQueue&quot;</span>, <span class="literal">NULL</span>);</span><br></pre></td></tr></table></figure>
<p>因为队列会自动执行添加的任务，所以你不需要额外的代码来管理队列。你不需要创建或配置线程，也不需要创建或附加任何run loop源。此外，你可以通过简单地将任务添加到队列中来执行新的工作类型。要对run loop做同样的事情，你需要修改你现有的run loop源或创建一个新的run loop源来处理新的数据。</p>
<p>run loop的一个常见配置是处理异步到达网络套接字上的数据。与其为这种类型的行为配置一个run loop，你可以为所需的队列附加一个调度源。与传统的run loop源相比，调度源还提供了更多处理数据的选项。除了处理定时器和网络端口事件外，你还可以使用调度源来读写文件、监控文件系统对象、监控进程和监控信号。你甚至可以定义自定义调度源，从你代码的其他部分异步触发它们。关于设置调度源的更多信息，可参阅<a href=".%2F04%20Dispatch%20Sources.md">调度源</a>。</p>
<h2 id="兼容posix线程">兼容POSIX线程</h2>
<p>由于Grand Central Dispatch管理着你提供的任务和这些任务运行的线程之间的关系，你一般应该避免从你的任务代码中调用POSIX线程例程。如果你因为某些原因需要调用它们，你应该非常小心地对待你所调用的例程（routines）。本节为你提供了一个指南，说明哪些例程可以安全调用，哪些例程不可以从你的队列任务中调用。这个列表并不完整，但应该给你一个指示，哪些是安全的调用，哪些是不安全的。</p>
<p>一般来说，程序不能删除或改变不是它创建的对象或数据结构。因此，使用调度队列执行的block对象不能调用以下函数：</p>
<ul>
<li><code>pthread_detach</code></li>
<li><code>pthread_cancel</code></li>
<li><code>pthread_join</code></li>
<li><code>pthread_kill</code></li>
<li><code>pthread_exit</code></li>
</ul>
<p>尽管在任务运行时是可以修改一个线程的状态的，但你必须在你的任务返回之前将线程返回到它的原始状态。因此，只要你把线程返回到它的原始状态，调用以下函数是安全的：</p>
<ul>
<li><code>pthread_setcancelstate</code></li>
<li><code>pthread_setcanceltype</code></li>
<li><code>pthread_setschedparam</code></li>
<li><code>pthread_sigmask</code></li>
<li><code>pthread_setspecific</code></li>
</ul>
<p>用于执行一个给定block的底层线程可以在不同的调用中进行修改。因此，程序不应该依赖以下函数在block的调用之间返回可预测的结果：</p>
<ul>
<li><code>pthread_self</code></li>
<li><code>pthread_getschedparam</code></li>
<li><code>pthread_get_stacksize_np</code></li>
<li><code>pthread_get_stackaddr_np</code></li>
<li><code>pthread_mach_thread_np</code></li>
<li><code>pthread_from_mach_thread_np</code></li>
<li><code>pthread_getspecific</code></li>
</ul>
<p><strong>重要提醒：</strong>block必须捕获并抑制在其中抛出的任何语言级异常。在block的执行过程中发生的其他错误同样应该由block来处理，或者用来通知程序的其他部分。</p>
<p>关于POSIX线程和本节中提到的函数的更多信息，可参阅<code>pthread</code> man pages。</p>
<h2 id="总结">总结</h2>
<ul>
<li>异步添加任务到串行队列实现了异步锁。</li>
<li>关键代码使用同步方式进入串行、并发队列中执行。</li>
<li>用调度组替换线程连接。</li>
<li>生产者-消费者模型可以直接用给队列添加任务实现。如果生产者生成的任务需要按照特定的顺序执行，就使用一个串行队列。如果生产者生成的任务可以并发执行，就把它们添加到一个并发队列中，让系统尽可能地同时执行它们。</li>
<li>run loop代码可以直接用一个串行队列或调度源实现。</li>
<li>如果对实时性要求非常严格，那么还是建议使用线程实现。</li>
</ul>
]]></content>
      <categories>
        <category>翻译</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>多线程</tag>
        <tag>Concurrency Programming Guide</tag>
      </tags>
  </entry>
  <entry>
    <title>Concurrency Programming Guide：术语表</title>
    <url>/posts/concurrency_pg_glossary/</url>
    <content><![CDATA[<p><strong>程序 application</strong></p>
<p>A specific style of <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW13">program</a> that displays a graphical interface to the user.</p>
<p>一种特定风格的向用户显示图形界面的<a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW13">program</a>。</p>
<span id="more"></span>
<hr />
<p><strong>异步设计法 asynchronous design approach</strong></p>
<p>The principle of organizing an application around blocks of code that can be run concurrently with an application’s main thread or other threads of execution. Asynchronous tasks are started by one thread but actually run on a different thread, taking advantage of additional processor resources to finish their work more quickly.</p>
<p>围绕可与程序主线程或其他执行线程同时运行的block来组织程序的原则。异步任务由一个线程启动，但实际上在不同的线程上运行，利用额外的处理器资源，更快完成工作。</p>
<hr />
<p><strong>block object</strong></p>
<p>A C construct for encapsulating inline code and data so that it can be performed later. You use blocks to encapsulate tasks you want to perform, either inline in the current thread or on a separate thread using a dispatch queue. For more information, see <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html#//apple_ref/doc/uid/TP40007502">Blocks Programming Topics</a>.</p>
<p>一种C结构，用于封装内联代码和数据，以便以后执行。你可以使用block来封装你想执行的任务，可以在当前线程中内联，也可以在一个单独的线程中使用调度队列。了解更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html#//apple_ref/doc/uid/TP40007502">Blocks Programming Topics</a>。</p>
<hr />
<p><strong>并发操作 concurrent operation</strong></p>
<p>An operation object that does not perform its task in the thread from which its <code>start</code> method was called. A concurrent operation typically sets up its own thread or calls an interface that sets up a separate thread on which to perform the work.</p>
<p>一个操作对象，它不在调用其<code>start</code>方法的线程中执行其任务。一个并发操作通常会设置自己的线程，或者调用一个接口，设置一个单独的线程来执行工作。</p>
<hr />
<p><strong>条件 condition</strong></p>
<p>A construct used to synchronize access to a resource. A thread waiting on a condition is not allowed to proceed until another thread explicitly signals the condition.</p>
<p>一个用于同步访问资源的结构。在一个条件下等待的线程不允许继续进行，直到另一个线程明确发出条件信号。</p>
<hr />
<p><strong>关键部分 critical section</strong></p>
<p>A portion of code that must be executed by only one thread at a time.</p>
<p>一次只能由一个线程执行的部分代码。</p>
<hr />
<p><strong>自定义源 custom source</strong></p>
<p>A <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW19">dispatch source</a> used to process application-defined events. A custom source calls your custom event handler in response to events that your application generates.</p>
<p>一个用于handler定义的事件的调度源。自定义源调用自定义事件handler，以响应程序产生的事件。</p>
<hr />
<p><strong>描述符 descriptor</strong></p>
<p>An abstract identifier used to access a file, socket, or other system resource.</p>
<p>用于访问文件、套接字或其他系统资源的一个抽象标识符。</p>
<hr />
<p><strong>调度队列 dispatch queue</strong></p>
<p>A <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW23">Grand Central Dispatch (GCD)</a> structure that you use to execute your application’s tasks. GCD defines dispatch queues for executing tasks either serially or concurrently.</p>
<p>一个GCD数据结构，用它来执行程序的任务。GCD定义了用于串行或并发执行任务的调度队列。</p>
<hr />
<p><strong>调度源 dispatch source</strong></p>
<p>A <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW23">Grand Central Dispatch (GCD)</a> data structure that you create to process system-related events.</p>
<p>一个GCD数据结构，创建它来处理系统相关事件。</p>
<hr />
<p><strong>描述符调度源 descriptor dispatch source</strong></p>
<p>A <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW19">dispatch source</a> used to process file-related events. A file descriptor source calls your custom event handler either when file data is available for reading or writing or in response to file system changes.</p>
<p>一个用于处理文件相关事件的调度源。文件描述符源在文件数据可供读写时或在文件系统变化时调用自定义事件处理器。</p>
<hr />
<p><strong>动态共享库 dynamic shared library</strong></p>
<p>A binary executable that is loaded dynamically into an application’s process space rather than linked statically as part of the application binary.</p>
<p>一个二进制可执行文件，它被动态加载到程序的进程空间，而不是作为程序二进制的一部分静态链接。</p>
<hr />
<p><strong>framework</strong></p>
<p>A type of bundle that packages a dynamic shared library with the resources and header files that support that library. For more information, see <a href="https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Frameworks.html#//apple_ref/doc/uid/10000183i">Framework Programming Guide</a>.</p>
<p>一种捆绑类型，将动态共享库与支持该库的资源和头文件打包。更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Frameworks.html#//apple_ref/doc/uid/10000183i">Framework Programming Guide</a>。</p>
<hr />
<p><strong>全局调度队列 global dispatch queue</strong></p>
<p>A <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW9">dispatch queue</a> provided to your application automatically by <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW23">Grand Central Dispatch (GCD)</a>. You do not have to create global queues yourself or retain or release them. Instead, you retrieve them using the system-provided functions.</p>
<p>由GCD自动提供给程序的一个调度队列。你不需要自己创建全局队列，也不需要保留或释放它们。相反，你可以使用系统提供的函数来检索它们。</p>
<hr />
<p><strong>Grand Central Dispatch (GCD)</strong></p>
<p>A technology for executing asynchronous tasks concurrently. GCD is available in OS X v10.6 and later and iOS 4.0 and later.</p>
<p>一种用于并发执行异步任务的技术。GCD在OS X v10.6及以后版本和iOS 4.0及以后版本中可用。</p>
<hr />
<p><strong>输入源 input source</strong></p>
<p>A source of asynchronous events for a thread. Input sources can be port based or manually triggered and must be attached to the thread’s run loop.</p>
<p>一个线程的异步事件的来源。输入源可以是基于端口的，也可以是手动触发的，必须连接到线程的run loop。</p>
<hr />
<p><strong>可连接线程 joinable thread</strong></p>
<p>A thread whose resources are not reclaimed immediately upon termination. Joinable threads must be explicitly detached or be joined by another thread before the resources can be reclaimed. Joinable threads provide a return value to the thread that joins with them.</p>
<p>一个线程，其资源在终止时不会被立即回收。可加入的线程必须明确地被分离或被另一个线程加入，然后才可以回收资源。可加入的线程为与之加入的线程提供一个返回值。</p>
<p><strong>库 library</strong></p>
<p>A UNIX feature for monitoring low-level system events. For more information see the <code>kqueue</code> man page.</p>
<p>一个UNIX功能，用于监控低级别的系统事件。更多信息可参阅 <code>kqueue</code> man page。</p>
<hr />
<p><strong>Mach port dispatch source</strong></p>
<p>A <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW19">dispatch source</a> used to process events arriving on a Mach port.</p>
<p>一个用于处理到达Mach端口事件的调度源。</p>
<hr />
<p><strong>主线程 main thread</strong></p>
<p>A special type of <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW1">thread</a> created when its owning process is created. When the main thread of a program exits, the process ends.</p>
<p>一个特殊类型的线程，在其所属的进程被创建时创建。当一个程序的主线程退出时，该进程就结束了。</p>
<hr />
<p><strong>互斥锁 mutex</strong></p>
<p>A lock that provides mutually exclusive access to a shared resource. A mutex lock can be held by only one thread at a time. Attempting to acquire a mutex held by a different thread puts the current thread to sleep until the lock is finally acquired.</p>
<p>一个提供对共享资源的互斥访问的锁。一个互斥锁在同一时间只能由一个线程持有。试图获取一个由不同线程持有的互斥锁会使当前线程陷入休眠状态，直到最终获得该锁。</p>
<hr />
<p><strong>Open Computing Language (OpenCL)</strong></p>
<p>A standards-based technology for performing general-purpose computations on a computer’s graphics processor. For more information, see <a href="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/OpenCL_MacProgGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008312">OpenCL Programming Guide for Mac</a>.</p>
<p>一种基于标准的技术，用于在计算机的图形处理器上进行通用计算。更多信息，可参阅<a href="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/OpenCL_MacProgGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008312">OpenCL Programming Guide for Mac</a>。</p>
<hr />
<p><strong>操作对象 operation object</strong></p>
<p>An instance of the <code>NSOperation</code> class. Operation objects wrap the code and data associated with a task into an executable unit.</p>
<p><code>NSOperation</code>类的一个实例。操作对象将与一个任务相关的代码和数据包装成一个可执行的单元。</p>
<hr />
<p><strong>操作队列 operation queue</strong></p>
<p>An instance of the <code>NSOperationQueue</code> class. Operation queues manage the execution of operation objects.</p>
<p><code>NSOperationQueue</code>类的一个实例。操作队列管理操作对象的执行。</p>
<hr />
<p><strong>私有调度队列 private dispatch queue</strong></p>
<p>A <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW9">dispatch queue</a> that you create, retain, and release explicitly.</p>
<p>自己明确创建、保留和释放的调度队列。</p>
<hr />
<p><strong>进程 process</strong></p>
<p>The runtime instance of an application or program. A process has its own virtual memory space and system resources (including port rights) that are independent of those assigned to other programs. A process always contains at least one thread (the main thread) and may contain any number of additional threads.</p>
<p>一个程序或程序的运行时实例。一个进程有自己的虚拟内存空间和系统资源（包括端口权限），独立于分配给其他程序的资源。一个进程总是包含至少一个线程（主线程），并可能包含任何数量的附加线程。</p>
<hr />
<p><strong>进程调度源 process dispatch source</strong></p>
<p>A <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW19">dispatch source</a> used to handle process-related events. A process source calls your custom event handler in response to changes to the process you specify.</p>
<p>用于处理与进程有关的事件的调度源。进程源在响应指定的进程的变化时调用自定义事件handler。</p>
<hr />
<p><strong>程序 program</strong></p>
<p>A combination of code and resources that can be run to perform some task. Programs need not have a graphical user interface, although graphical applications are also considered programs.</p>
<p>一个代码和资源的组合，可以运行以执行一些任务。程序不需要有图形用户界面，尽管图形应用程序也被认为是程序。</p>
<hr />
<p><strong>可重入的 reentrant</strong></p>
<p>Code that can be started on a new thread safely while it is already running on another thread.</p>
<p>当代码已经在一个线程上运行时，可以在另一个新的线程上安全启动。</p>
<hr />
<p><strong>run loop</strong></p>
<p>An event-processing loop, during which events are received and dispatched to appropriate handlers.</p>
<p>一个事件处理的循环，在这个循环中，事件被接收并调度给适当的handler。</p>
<hr />
<p><strong>run loop mode</strong></p>
<p>A collection of input sources, timer sources, and run loop observers associated with a particular name. When run in a specific “mode,” a run loop monitors only the sources and observers associated with that mode.</p>
<p>一个输入源、定时器源和run loop观察者的集合，与一个特定的名称相关联。当在一个特定的模式下运行时，一个run loop只监控与该模式相关的源和观察者。</p>
<hr />
<p><strong>run loop object</strong></p>
<p>An instance of the <code>NSRunLoop</code> class or <code>CFRunLoopRef</code> opaque type. These objects provide the interface for implementing an event-processing loop in a thread.</p>
<p><code>NSRunLoop</code>类或<code>CFRunLoopRef</code>不透明类型的实例。这些对象提供了在线程中实现事件处理循环的接口。</p>
<hr />
<p><strong>run loop observer</strong></p>
<p>A recipient of notifications during different phases of a run loop’s execution.</p>
<p>在run loop执行的不同阶段，是通知的接收者。</p>
<hr />
<p><strong>信号量 semaphore</strong></p>
<p>A protected variable that restricts access to a shared resource. Mutexes and conditions are both different types of semaphore.</p>
<p>一个受保护的变量，限制对共享资源的访问。互斥锁和条件都是不同类型的信号量。</p>
<hr />
<p><strong>信号 signal</strong></p>
<p>A UNIX mechanism for manipulating a process from outside its domain. The system uses signals to deliver important messages to an application, such as whether the application executed an illegal instruction. For more information see the <code>signal</code> man page.</p>
<p>一种UNIX机制，用于从一个进程的域外操纵该进程。系统使用信号向程序传递重要信息，例如程序是否执行了非法指令。更多信息可参阅 <code>signal</code> man page。</p>
<hr />
<p><strong>信号调度源 signal dispatch source</strong></p>
<p>A <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW19">dispatch source</a> used to process UNIX signals. A signal source calls your custom event handler whenever the process receives a UNIX signal.</p>
<p>用于处理UNIX信号的调度源。当进程收到UNIX信号时，信号源会调用自定义事件handler。</p>
<hr />
<p><strong>任务 task</strong></p>
<p>A quantity of work to be performed. Although some technologies (most notably Carbon Multiprocessing Services) use this term differently, the preferred usage is as an abstract concept indicating some quantity of work to be performed.</p>
<p>一个要执行的工作数量。尽管一些技术（最明显的是Carbon多处理服务）以不同的方式使用这个术语，但首选的用法是作为一个抽象的概念，表示要执行的一些工作的数量。</p>
<hr />
<p><strong>线程 thread</strong></p>
<p>A flow of execution in a process. Each thread has its own stack space but otherwise shares memory with other threads in the same process.</p>
<p>一个进程中的执行流。每个线程都有自己的堆栈空间，但在其他方面与同一进程中的其他线程共享内存。</p>
<hr />
<p><strong>定时器调度源 timer dispatch source</strong></p>
<p>A <a href="https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Glossary/Glossary.html#//apple_ref/doc/uid/TP40008091-CH104-SW19">dispatch source</a> used to process periodic events. A timer source calls your custom event handler at regular, time-based intervals.</p>
<p>用于处理周期性事件的调度源。定时器源定期、基于时间的间隔调用自定义事件handler。</p>
]]></content>
      <categories>
        <category>翻译</category>
      </categories>
      <tags>
        <tag>Apple</tag>
        <tag>多线程</tag>
        <tag>Concurrency Programming Guide</tag>
      </tags>
  </entry>
</search>
