<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>ShawnFoo杂货屋</title>
  
  <subtitle>技术、杂谈</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://shawnfoo.github.io/"/>
  <updated>2021-05-13T06:49:39.315Z</updated>
  <id>https://shawnfoo.github.io/</id>
  
  <author>
    <name>ShawnFoo</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>三年iOS面试小计</title>
    <link href="https://shawnfoo.github.io/2018/11/19/%E4%B8%89%E5%B9%B4iOS%E9%9D%A2%E8%AF%95%E5%B0%8F%E8%AE%A1/"/>
    <id>https://shawnfoo.github.io/2018/11/19/三年iOS面试小计/</id>
    <published>2018-11-18T16:02:24.000Z</published>
    <updated>2021-05-13T06:49:39.315Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>从十月中旬面试至今已满一个月, 尘埃即将落地, 在此对这段面试经历做个小结.</p><p>内容主要围绕<code>面试准备</code>以及<code>职业发展</code>两方面展开, 虽不包含具体面试题等信息, 但会列出个人归纳的<code>面试考纲</code>以及<code>注意事项</code>等供参考.</p><a id="more"></a><h2 id="面试经历"><a href="#面试经历" class="headerlink" title="面试经历"></a>面试经历</h2><h3 id="面试结果"><a href="#面试结果" class="headerlink" title="面试结果"></a>面试结果</h3><p>老规矩, 先上大家关心的结果</p><ul><li>上海:<ul><li>喜马拉雅FM: offer call</li><li>小红书: offer call</li><li>携程: 二轮游卒</li><li>英语流利说: offer call</li><li>饿了么: 三轮游+hr面卒</li><li>B站: offer call</li><li>美团: 二轮游 卒</li><li>抖音: 三轮游+hr面卒</li></ul></li><li>北京:<ul><li>百度贴吧: offer call</li><li>蚂蚁金服: 5面完, 等hr面</li><li>高德地图: offer call</li><li>百度凤巢: offer call</li><li>西瓜视频: offer call</li></ul></li></ul><h3 id="投递建议"><a href="#投递建议" class="headerlink" title="投递建议"></a>投递建议</h3><p>首先, 无论是走内推、猎头或自投(某直聘、某钩、官网/公众号)等方式, 在简历投递次数和频率上都要克制, 比如只安排一周内的面试、每天至多面1家、预约下午面试等等.</p><p>其次, 看清楚JD职位要求, 以及错开对同派系公司不同岗位的投递. 比如, 可能公司内部共用同一套招聘系统, 某个岗位进入面试流程后, 那么其他岗位是无法同时进行面试的. </p><p>最后, 不要太在意投递结果, 2~3天没回复就尝试投递其他岗位.</p><p>另外, 可能有同学注意到, 我面过同派系不同bu的岗位, 据我个人<strong>不靠谱</strong>的猜测, 这块逻辑可能是这样的:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">if (内推 + 之前面试记录性质良好) &#123;</span><br><span class="line">  // 可能有机会</span><br><span class="line">&#125; else if (自己继续投 + 多争取一番) &#123;</span><br><span class="line">  // 机会很小, 但想去就得尝试</span><br><span class="line">&#125; else &#123;</span><br><span class="line">  // 可能得半年后了</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="面试准备"><a href="#面试准备" class="headerlink" title="面试准备"></a>面试准备</h2><p>准备方面我分为两部分, 首先是心理层面的准备, 然后才是战备”物资”</p><h3 id="心理准备"><a href="#心理准备" class="headerlink" title="心理准备"></a>心理准备</h3><p>1.明确动机</p><p>   比如为什么要跳槽或你想从事什么内容? 马爸爸曾提及过三要素(钱、心、事), 你是否完全认同? 或是有其他的原因? </p><p>   不鼓励轻易跳槽, 跳槽未必有利于你长期的发展, 还有就是像某东在就职经历这块卡的很严</p><p>2.优势劣势</p><p>   面对其他候选人, 是否清楚自己的优势劣势？比如你某方面技术突出、抗压能力更好、沟通能力更好等. 认清不足以及想好未来具体的提高的计划</p><p>3.最坏打算</p><p>   若决定离开, 那么便坚决离开, 没有回头路. 决定前做好最坏打算, 比如连续面试下来结果都不好, 是否能承受, 有无备选方案? </p><p>4.学会健忘</p><p>   可能某几次面试结果不尽人意, 或是因为准备不足, 或是因为”气场不和”等等. 总之摆正心态, 忘记之前的面试结果, 对面试中发现的问题有针对性的去总结和提高, 然后接着面就好</p><p>尤其对于”放长线作战”的同学, 个人推荐花时间搞清楚以上4点</p><h3 id="物资准备"><a href="#物资准备" class="headerlink" title="物资准备"></a>物资准备</h3><p>“物资”直接决定面试的成败. 主要分为硬实力与软实力的体现</p><h4 id="硬实力"><a href="#硬实力" class="headerlink" title="硬实力"></a>硬实力</h4><p>相比于记面试题, 不如夯实题目后边的知识点, 面试遇到原题的几率还是不要赌了. 掌握知识点无论对面试或工作成长都大有裨益</p><p>此处奉上三年iOS开发面试考纲, 个人愚见, 仅供参考</p><ul><li>通用技能<ul><li>数据结构、算法(排序、字符串、数组、位操作、回溯、双指针、DFS、BFS、DP、分治、二分查找..)</li><li>设计模式(创建型、结构型、行为型设计模式)</li><li>计算机网络(应用层/传输层协议、网络分层..)</li><li>操作系统(进程、线程、内存布局..)</li><li>编译原理(编译过程..)</li></ul></li><li>iOS技能<ul><li>修饰符, 可变不可变对象等基础</li><li>Runtime</li><li>RunLoop</li><li>KVC、KVO原理</li><li>block本质</li><li>category本质</li><li>内存管理</li><li>事件传递</li><li>App、VC、View、CALayer生命周期</li><li>多线程(队列、锁)</li><li>性能优化(体验优化、启动优化、网络优化、编译优化)</li><li>主流组件化、模块化、架构方案</li><li>Core Animation、屏幕渲染等</li><li>数据持久化方案</li><li>动态化方案(Hybird/RN/Weex/Flutter)</li></ul></li><li>项目经历<ul><li>简历上写的</li><li>主流三方库</li></ul></li></ul><p>所有列举的知识点, 本次面试均有涉及. 深度方面需个人进行挖掘</p><p>算法貌似大厂必考项, 技术面几乎每轮都有, 推荐leetcode分类型进行算法思路的训练, 一般可以秒杀medium的题目足矣, 仅一次问到hard难度的题目</p><p>然后iOS知识点考察, 仅知道是什么远不够, 更多的是为什么, 实现原理这些. 平时需要多积累, 比如从某个问题深入挖掘, 看源码, 博客文章(内容未必都对, 带着辩证思维去看)等. 另外印象笔记剪藏功能用于收藏回顾真的相当不错.</p><p>面试官往往会由浅入深进行考察, 若不会就明确表示出来(吃过强答的亏, 印象分那是卡卡卡的掉) 一定要多与面试官沟通, 倾听面试官把问题描述完, 若只是忘了细节争取能说下思路也好</p><h4 id="软实力"><a href="#软实力" class="headerlink" title="软实力"></a>软实力</h4><ul><li>沟通能力</li><li>价值观</li><li>学习方式</li><li>职业规划</li><li>EQ</li><li>…</li></ul><p>最后, 有时间不妨了解下面试岗位的相关产品, 比如对产品体验一番, 或做个逆向, 面试时也会多一笔谈资</p><h2 id="职业发展"><a href="#职业发展" class="headerlink" title="职业发展"></a>职业发展</h2><p>这方面结合各位大佬赠与的宝贵建议简单概括一下</p><ul><li><p>围绕核心</p><p>对于走技术路线的同学来说, 毫无疑问就是不断提升技术方面的深度以及广度</p></li><li><p>提高软实力</p><p>专业以外的能力, 具体前边也有提及, 比如学习方式的改进, 沟通能力的提高, 变得更靠谱等</p></li><li><p>承担更多</p><p>不单单专注于完成分类之事或提高个人, 可以尝试放大格局, 主动去承担本分以外的职责, 比如思考并实践对整个团队或者整个公司层面有益的事 </p></li></ul><h2 id="The-End"><a href="#The-End" class="headerlink" title="The End"></a>The End</h2><p>全文观点皆主观想法, 各位见仁见智</p><p>最后祝大家早日拿到期望offer, 未来发展上越走越远!</p><p>还特别感谢老东家的知遇之恩以及各位领导同事对我的关照, 谢谢所有给与我面试机会的公司跟遇到的每位面试官和hr同学. </p><p>很多大佬(达文哥、官钦哥、东哥、亮哥等等)在面试中或私下就职业发展方面赠与了非常非常宝贵的建议, 受益匪浅, 大恩不言谢!</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;从十月中旬面试至今已满一个月, 尘埃即将落地, 在此对这段面试经历做个小结.&lt;/p&gt;
&lt;p&gt;内容主要围绕&lt;code&gt;面试准备&lt;/code&gt;以及&lt;code&gt;职业发展&lt;/code&gt;两方面展开, 虽不包含具体面试题等信息, 但会列出个人归纳的&lt;code&gt;面试考纲&lt;/code&gt;以及&lt;code&gt;注意事项&lt;/code&gt;等供参考.&lt;/p&gt;
    
    </summary>
    
      <category term="杂记" scheme="https://shawnfoo.github.io/categories/%E6%9D%82%E8%AE%B0/"/>
    
      <category term="2018" scheme="https://shawnfoo.github.io/categories/%E6%9D%82%E8%AE%B0/2018/"/>
    
    
      <category term="面试" scheme="https://shawnfoo.github.io/tags/%E9%9D%A2%E8%AF%95/"/>
    
  </entry>
  
  <entry>
    <title>驴妈妈客户端频道页模块化设计思路及实践</title>
    <link href="https://shawnfoo.github.io/2018/05/10/%E9%A9%B4%E5%A6%88%E5%A6%88%E5%AE%A2%E6%88%B7%E7%AB%AF%E9%A2%91%E9%81%93%E9%A1%B5%E6%A8%A1%E5%9D%97%E5%8C%96%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%B7%AF%E5%8F%8A%E5%AE%9E%E8%B7%B5/"/>
    <id>https://shawnfoo.github.io/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/</id>
    <published>2018-05-10T15:24:24.000Z</published>
    <updated>2021-05-13T07:11:47.506Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一、引言"><a href="#一、引言" class="headerlink" title="一、引言"></a>一、引言</h2><p>为了满足运营同学动态配置<strong>频道页的内容排版</strong>, 以及产品同学<strong>一次开发, 各频道复用</strong>的需求, 要开发一个框架来满足以下两点:</p><ul><li>内容灵活排版: 某个频道页展示的内容及其顺序, 甚至说一个新的频道页, 皆可由运营同学在cms后台直接配置</li><li>模块全局复用: 一个承载内容的 <code>模块</code> 开发完后, 可在全部频道页配置使用</li></ul><p>页面模块化的好处:</p><ol><li>方便运营同学在线上cms后台直接创建新的界面或动态调整界面(导航栏、页头脚、内容元素等). 缩短内容上线周期</li><li>在我们不同的业务代码组件化后, 是相互隔离的. 不同的业务线开发好的业务组件难以复用(数据模型、命名、方法定义等都不统一). 而且并非所有业务组件都能封装成通用型基础组件下沉到基础组件库中. 开发这个框架, 就可以同时创建一个统一规范的业务组件库</li></ol><a id="more"></a><h2 id="二、模块定义"><a href="#二、模块定义" class="headerlink" title="二、模块定义"></a>二、模块定义</h2><p>上文提及的 <code>内容</code> , 就是我们各频道页看到的 <code>模块</code>. 不同的 <code>模块</code> 具有其独特的产品功能与运营目的. </p><p>以驴妈妈首页频道为例, 如下图:</p><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/1_a.png" alt=""></p><p>每个框所圈区域为一个独立模块. 比如:</p><ul><li>banner模块(产品推荐、活动推广、广告投放等)</li><li>频道入口、主题列表模块(用户分流导向)</li><li>旅行头条模块(热门游记推荐)</li><li>…</li></ul><p>此外, 每个模块可以包含单个或多个不同的模块组件:</p><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/1_b.png" alt=""></p><h2 id="三、模块化设计原则"><a href="#三、模块化设计原则" class="headerlink" title="三、模块化设计原则"></a>三、模块化设计原则</h2><p>除了考虑SOLID(六大原则)外, 框架设计还会围绕以下三点.</p><h3 id="3-1-面向接口"><a href="#3-1-面向接口" class="headerlink" title="3.1 面向接口"></a>3.1 面向接口</h3><p>通过定义 <code>接口(即协议)</code> 抽象和规范框架所关心的类或事. 框架与模块间低耦合.</p><p>举个例子, 对于框架来说, 它并不关心配置数据是什么结构或如何获取, 它仅关心的是有多少个模块、每个模块在容器中所占大小以及位置等数据.</p><p>可为此定义一个数据源协议, 来规范充当框架数据源对象所必须遵循的行为. 至于数据源对象的具体类型是什么不重要, 只要遵循协议即可充当框架中的某个角色.</p><p><code>接口</code> 就好比一份 <code>合同</code>, 拟定好的 <code>合同</code> 就不能轻易修改, 如果贸然修改原有的条款, 那势必波及到所有遵循 <code>合同</code> 的人. 所以, <code>合同</code> 制定阶段尤为重要, 不能好高骛远也不能鼠目寸光, 定好了大家就按照 <code>合同</code> 来. </p><p>当然, 面向接口与面向对象并不冲突, 反而是相辅相成, 此处不展开讨论.</p><h3 id="3-2-数据驱动"><a href="#3-2-数据驱动" class="headerlink" title="3.2 数据驱动"></a>3.2 数据驱动</h3><p>数据决定并驱动内容的展示与响应.</p><ul><li><p>数据决定展示内容, 即数据与内容一一对应:</p><p>框架根据数据源提供的相关数据, 决定每个模块该创建的组件类型, 模块组件的展示大小及布局位置等.</p></li><li><p>数据驱动内容变化. 关注点为数据, 而非事件. </p><p>举个例子, 对于框架中模块发生的任意事件, 其结果也就两种:</p><ol><li>事件发生后, 数据有变化</li><li>事件发生后, 数据无变化</li></ol><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/3_a.png" alt=""></p><p>也就说框架不会去管具体发生什么事件, 只”盯着”它所关心的数据有没有变</p><p>另外, 事件驱动中一个事件往往对应一个响应操作, 是1对1的关系. 而数据驱动可以是1对N的关系, 可能是多个事件修改同个数据.</p></li></ul><h3 id="3-3-模块隔离"><a href="#3-3-模块隔离" class="headerlink" title="3.3 模块隔离"></a>3.3 模块隔离</h3><p>模块间相互隔离, 模块独立自治, 其相关事务自行处理.</p><p>模块可单独开发, 注册到配置中. 模块内可自行使用MVX、VIPER等结构型设计模式(Structual Design Pattern).</p><p>此外, 模块联合开发中, 框架与模块也应该适当隔离. 遵循 <a href="https://en.wikipedia.org/wiki/Dependency_inversion_principle" target="_blank" rel="noopener">依赖倒置原则</a> . 模块(高层)依赖也不应该直接依赖框架(低层)进行开发, 框架仅知道我们抽象出来模块接口, 模块也仅知道框架接口, 双方都遵循接口进行实现.</p><p>通俗来说就是框架的功能开发与模块的开发是两条平行线, 除非修改接口, 否则双方修改实现都不会影响到另一方.</p><p>目前这块实现是传统型架构(4.6架构图), 即模块的开发是直接依赖了框架的实现(具体的基类), 框架没有抽象出暴露给上层的接口. 在遵循 <code>依赖倒置原则</code> 后, 模块(高层)在访问低层(框架)时, 就只能接触到遵循框架接口的某个对象(UIViewController\&lt;XxxProtocol> *), 而非具体某个XXXClass类. </p><p>还是取舍的问题, 选择性的开闭. 若完全遵循 <code>依赖倒置原则</code>, 那高层也没法直接依赖低层实现进行继承了, 包括哪些不允许修改, 哪些要使用公共实现, 哪些留给上层去拓展等等.</p><h2 id="四、模块化框架设计"><a href="#四、模块化框架设计" class="headerlink" title="四、模块化框架设计"></a>四、模块化框架设计</h2><p>以iOS平台举例, 阐述对整个框架的具体设计. 抛开Android和iOS平台系统编码的风格习惯和具体实现上存在的不同, 整体思想大同小异.</p><h3 id="4-1-数据源"><a href="#4-1-数据源" class="headerlink" title="4.1 数据源"></a>4.1 数据源</h3><p>一个频道页由若干个模块组成, 一个模块包含1个或多个不同的组件. 框架根据数据源提供的信息, 创建和安置模块组件. </p><h4 id="4-1-1-数据源协议"><a href="#4-1-1-数据源协议" class="headerlink" title="4.1.1 数据源协议"></a>4.1.1 数据源协议</h4><ol><li><p>模块数据源协议: 主要向框架提供某个模块包含的组件信息、相关的布局信息、以及组件填充数据的内容等等</p> <figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="built_in">NSObject</span>&lt;LVTSectionDataSource&gt; LVTSectionData;</span><br><span class="line">  </span><br><span class="line"><span class="class"><span class="keyword">@protocol</span> <span class="title">LVTSectionDataSource</span> &lt;<span class="title">NSObject</span>&gt;</span></span><br><span class="line"> </span><br><span class="line">- (LVTemplateClass)headerClass;</span><br><span class="line">- (LVTemplateClass)cellClassAtIndex:(<span class="built_in">NSUInteger</span>)index;</span><br><span class="line">- ...</span><br><span class="line"></span><br><span class="line">- (<span class="built_in">BOOL</span>)hidden;</span><br><span class="line">- (<span class="built_in">NSUInteger</span>)numOfItems;</span><br><span class="line">- (<span class="built_in">UIEdgeInsets</span>)sectionInset;</span><br><span class="line">- (<span class="built_in">CGFloat</span>)itemSpace;</span><br><span class="line">- (<span class="built_in">CGFloat</span>)lineSpace;</span><br><span class="line">- (<span class="built_in">CGSize</span>)itemSizeAtIndex:(<span class="built_in">NSUInteger</span>)index withContainerSize:(<span class="built_in">CGSize</span>)size;</span><br><span class="line">- ...</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">nullable</span> LVTItemModel *)itemModelAtIndex:(<span class="built_in">NSUInteger</span>)index;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)requestSectionCustomData;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure></li><li><p>频道页数据源协议: 主要向框架提供整个频道拥有的模块总数, 以及各模块的局部数据源</p> <figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@protocol</span> <span class="title">LVTPageDataSource</span> &lt;<span class="title">NSObject</span>&gt;</span></span><br><span class="line"> </span><br><span class="line">- (<span class="built_in">NSUInteger</span>)numberOfSections;</span><br><span class="line">- (LVTSectionData *)sectionDataAt:(<span class="built_in">NSUInteger</span>)section;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)fetchPageDataWithCompletedBlock:(<span class="keyword">void</span> (^)(<span class="built_in">NSError</span> _Nullable *error))completedBlk;</span><br><span class="line">   </span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure></li></ol><h4 id="4-1-2-模块组件管理"><a href="#4-1-2-模块组件管理" class="headerlink" title="4.1.2 模块组件管理"></a>4.1.2 模块组件管理</h4><p>对于模块内的任意组件, 都有对应一个标识ID. 我们通过一个配置文件来维护标识与组件的对应关系. 联合开发时, 每开发好一个新的组件, 就只用修改配置文件.</p><p>配置的JSON结构大致如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">// 部分举例</span><br><span class="line">&#123;</span><br><span class="line">  &quot;header&quot;: &#123;</span><br><span class="line">    &quot;header1&quot;: &quot;LVTXXXHeader&quot;, // value为具体类名</span><br><span class="line">    &quot;header2&quot;: &quot;LVTXXXHeader&quot;,</span><br><span class="line">    ...</span><br><span class="line">  &#125;,</span><br><span class="line">  &quot;cell&quot;: &#123;</span><br><span class="line">    &quot;cell1&quot;: &quot;LVTXXXCell&quot;,</span><br><span class="line">    ...</span><br><span class="line">  &#125;,</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过ID我们可获得一个具体的类名, 再使用反射获得类对象以供框架创建组件实例. </p><p>在iOS上我们通过一个ClassMapper来专门维护对应关系, 如下图.</p><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/4_1_2_a.png" alt=""></p><p>上图类名仅为更好的表达Mapper的职责, 实际ClassMapper返回的类对象会使用泛型来进行解耦, ClassMapper中也不会引入任何组件的头文件.</p><h4 id="4-1-3-数据流向"><a href="#4-1-3-数据流向" class="headerlink" title="4.1.3 数据流向"></a>4.1.3 数据流向</h4><p>从原始数据到呈现到屏幕上的每个模块组件, 数据流向如下图所示:</p><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/4_1_3_a.png" alt=""></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">上图各元素代表:</span><br><span class="line">LVTPageDataSource为遵循 频道数据源协议 的对象</span><br><span class="line">LVTSectionData为遵循 模块数据源协议 的对象</span><br><span class="line">ClassMapper为管理对应关系的对象</span><br><span class="line">LVTCellXXX、LVTHeaderXXX为组件等</span><br></pre></td></tr></table></figure><h3 id="4-2-模块组件"><a href="#4-2-模块组件" class="headerlink" title="4.2 模块组件"></a>4.2 模块组件</h3><p>组件是模块化框架中复用的基础元素. </p><h4 id="4-2-1-组件协议"><a href="#4-2-1-组件协议" class="headerlink" title="4.2.1 组件协议"></a>4.2.1 组件协议</h4><p>模块组件分为可复用与不可复用两类, 分别对应以下协议:</p><ol><li><p>复用组件协议: 提供组件用于复用队列的复用Id、用于布局的元素大小等</p> <figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> Class&lt;LVTReuseItemProtocol&gt; LVTemplateClass;</span><br><span class="line"><span class="keyword">typedef</span> <span class="built_in">UICollectionViewCell</span>&lt;LVTReuseItemProtocol&gt; LVTemplateCell;</span><br><span class="line"><span class="keyword">typedef</span> <span class="built_in">UICollectionReusableView</span>&lt;LVTReuseItemProtocol&gt; LVTemplateReuseView;</span><br><span class="line"><span class="keyword">typedef</span> <span class="built_in">WKWebView</span>&lt;LVTReuseItemProtocol&gt; LVTemplateWebView;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@protocol</span> <span class="title">LVTReuseItemProtocol</span> &lt;<span class="title">NSObject</span>&gt;</span></span><br><span class="line"></span><br><span class="line">+ (<span class="built_in">NSString</span> *)tIdentifier;</span><br><span class="line">+ (<span class="built_in">CGSize</span>)itemSizeWithModel:(LVTItemModel *)model andContainerSize:(<span class="built_in">CGSize</span>)size;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)configItemWithModel:(LVTItemModel *)model;</span><br><span class="line">- (<span class="keyword">void</span>)setEventCenter:(<span class="keyword">id</span>&lt;LVTEventCenterProtocol&gt;)center;</span><br><span class="line">- (<span class="keyword">void</span>)setCacheUtil:(LVTCacheUtil *)util;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)itemPrepareForReuse;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">@end</span></span><br></pre></td></tr></table></figure></li><li><p>不可复用的悬浮组件协议: 提供视图高度, 悬浮定位信息等</p><figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> Class&lt;LVTFloatViewProtocol&gt; LVTFloatViewClass;</span><br><span class="line">   </span><br><span class="line"><span class="class"><span class="keyword">@protocol</span> <span class="title">LVTFloatViewProtocol</span> &lt;<span class="title">NSObject</span>&gt;</span></span><br><span class="line">  </span><br><span class="line">+ (<span class="built_in">CGFloat</span>)topInSection;</span><br><span class="line">+ (<span class="built_in">CGFloat</span>)viewHeight;</span><br><span class="line">   </span><br><span class="line">- (<span class="keyword">void</span>)configItemWithModel:(LVTItemModel *)model;</span><br><span class="line">- ...</span><br><span class="line">   </span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure></li></ol><p>数据填充等公共方法可抽象到另一个协议中, 再进行继承</p><h4 id="4-2-2-模块组件数据模型"><a href="#4-2-2-模块组件数据模型" class="headerlink" title="4.2.2 模块组件数据模型"></a>4.2.2 模块组件数据模型</h4><p>用于填充模块组件的数据模型类型不一, 框架也不与具体模型产生瓜葛. 通过协议规范数据模型得有的属性即可.</p><p>数据模型协议:</p><figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="built_in">NSObject</span>&lt;LVTItemModelProtocol&gt; LVTItemModel;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@protocol</span> <span class="title">LVTItemModelProtocol</span> &lt;<span class="title">NSObject</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">assign</span>) <span class="built_in">BOOL</span> isFolded;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">assign</span>) <span class="built_in">CGSize</span> itemSize;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">assign</span>) <span class="built_in">CGSize</span> foldedItemSize;</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>我们在前边协议中看到的LVTItemModel即代表了遵循该协议的数据模型</p><h3 id="4-3-频道代理对象协议"><a href="#4-3-频道代理对象协议" class="headerlink" title="4.3 频道代理对象协议"></a>4.3 频道代理对象协议</h3><p>以上我们说的那些模块在框架中的位置都是可调整的. 对于频道中位置固定的内容, 比如导航栏, 容器页头, 页脚等元素, 会交给一个频道的 <code>代理对象</code> 来处理. </p><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/4_3_a.png" alt=""></p><p>除了固定内容的管理, 还有一些与框架无关联的业务功能, 比如点位获取、站点切换等功能, 也会放到代理对象里边实现, 但不在协议里边体现.</p><p>具体代理协议如下:</p><figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@protocol</span> <span class="title">LVTPageDelegate</span> &lt;<span class="title">NSObject</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">weak</span>) LVTEventCenter *eventCenter;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">weak</span>) LVTLayoutQuery *layoutQuery;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">readonly</span>) <span class="built_in">CGFloat</span> containerViewTopInset;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">readonly</span>) <span class="built_in">BOOL</span> hidesBottomBarWhenPushed;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">readonly</span>) <span class="built_in">BOOL</span> showsLoadingIndicator;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">strong</span>) MJRefreshHeader *header;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">strong</span>) MJRefreshFooter *footer;</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)setupPageUI;</span><br><span class="line">- (<span class="keyword">void</span>)configPageWithModel:(<span class="keyword">id</span>)model;</span><br><span class="line">- ...</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>代理类型的管理, 与模块管理一致, 共用配置文件, 代理类对象的获取同样通过ClassMapper.</p><p>另外, 严格意义上讲, 把这个delegate命名为strategy会更加合适, 它的使用体现是 <code>策略模式</code>. 不同的代理有着不同的实现, 某个频道运行时, 也可能会动态的切换代理对象. </p><p>比如, 某次下拉刷新后, 下发的代理ID变了, 即对应的类对象变了, 就会创建新的 <code>策略对象</code> 来替换, 从而产生了不一样的UI或行为表现.</p><h3 id="4-4-对象通信"><a href="#4-4-对象通信" class="headerlink" title="4.4 对象通信"></a>4.4 对象通信</h3><p>模块之间, 模块与框架间存在相互通讯的需求. 比如在某些模块组件需要知道框架存在的生命周期事件, 以作出对应的操作. </p><p>对象间的常见通讯方式有:</p><ol><li>命令模式或Target-Action</li><li>代理模式或回调Callback</li><li>观察者模式</li></ol><p>考虑到模块间通讯可以1对多, 而前面两种皆为1对1通讯, 所以我们选择基于ReactiveCocoa或RxJava库, 遵循观察者模式来实现一个囊括所有跨模块事件的共享对象, 以进行集中式管理. 以下称之为 <code>事件中心</code>.</p><p>具体来说, 就是把有通信需求模块的相关事件集, 以空方法的形式统统添加到事件中心的共享对象上暴露出来(方法实现为空, 但并非抽象类). 各模块则根据自己的需求, 选择性的订阅共享对象上的事件.</p><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/4_4_a.png" alt=""></p><p>模块通讯方式则为直接调用共享事件中心上已添加好的事件方法, 如下:</p><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/4_4_b.png" alt=""></p><p>在 <code>4.2</code> 模块组件一节中的两个协议里, 都可见定义了设置事件中心的方法以供框架赋值, 以供组件访问.</p><h3 id="4-5-交互图"><a href="#4-5-交互图" class="headerlink" title="4.5 交互图"></a>4.5 交互图</h3><p>整个框架核心元素间的交互如下:</p><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/4_5_a.png" alt=""></p><p>上图没有包括具体的交互细节, 补上两张时序图:</p><ul><li><p>某频道页首屏展示的时序图(忽略本地缓存等各种情况):</p><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/4_5_b.png" alt=""></p><p>注: 模块的数据源(SectionData)会向ClassMapper获取具体的类对象, 具体可见数据源协议</p></li><li><p>某个框架事件(比如切换界面、滑动等)通过事件中心传递给订阅者的时序图:</p><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/4_5_c.png" alt=""></p><p>事件传递为同步操作, 哪个线程调用哪个线程触发, 订阅者接收事件的顺序由订阅时的先后顺序决定</p></li></ul><h3 id="4-6-架构一览"><a href="#4-6-架构一览" class="headerlink" title="4.6 架构一览"></a>4.6 架构一览</h3><p>上张简版架构图以示频道页模块化后上述提到的元素分别位于哪一层. </p><p><img src="/2018/05/10/驴妈妈客户端频道页模块化设计思路及实践/4_6_a.png" alt=""></p><table><thead><tr><th>层次</th><th>“个性”命名</th><th>说明</th><th>包含模块化元素</th></tr></thead><tbody><tr><td>4</td><td>塔顶(召唤师峡谷)</td><td>对外是某个”产品”. 对内是某个”载体”.</td><td>无 </td></tr><tr><td>3</td><td>纯业务层(外塔)</td><td>与具体业务密切相关的一层, 比如具体某个界面</td><td>模块化通用的控制器VC、遵循PageDataSource的数据源类</td></tr><tr><td>2</td><td>模块化”组件”层(中塔)</td><td>虚拟出来的一层, 只为更直观. 实际也属于上边的纯业务层.</td><td>包括遵循PageDelegate协议、模块组件协议、模块DataSource协议的所有类. 是统一规范的大合集</td></tr><tr><td>1</td><td>业务功能层(内塔)</td><td>依旧与业务相关的一层. 但属于公共的业务功能</td><td>模块化定义的接口, 遵循接口的VC基类、”组件”基类, ClassMapper, EventCenter, 其他辅助组件开发的类等</td></tr><tr><td>0</td><td>基础功能层(水晶)</td><td>与业务无关的一层, 换个项目也能用, 具有开源性</td><td>通用基础组件等 </td></tr></tbody></table><h2 id="五、小结"><a href="#五、小结" class="headerlink" title="五、小结"></a>五、小结</h2><p>以上便为驴妈妈频道页模块化的大致思路, 思路不复杂, 主要细节繁多, 就不一一展开. </p><p>无论何种实现方案, 在灵活满足业务需求的前提下, 同时保证技术上的拓展性, 未来再不断”打怪升级”, 都不失为一个较优解.</p><hr><p>原文作者: 傅翔</p><p>原文链接: <a href="https://mp.weixin.qq.com/s/J5YhTk5gyTt7Ie5803PeQg" target="_blank" rel="noopener">https://mp.weixin.qq.com/s/J5YhTk5gyTt7Ie5803PeQg</a></p><p>许可协议: 本文采用<a href="http://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">知识共享署名-非商业性使用 4.0 国际许可协议</a>进行许可. 非商业转载请注明作者及上述原文链接</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;一、引言&quot;&gt;&lt;a href=&quot;#一、引言&quot; class=&quot;headerlink&quot; title=&quot;一、引言&quot;&gt;&lt;/a&gt;一、引言&lt;/h2&gt;&lt;p&gt;为了满足运营同学动态配置&lt;strong&gt;频道页的内容排版&lt;/strong&gt;, 以及产品同学&lt;strong&gt;一次开发, 各频道复用&lt;/strong&gt;的需求, 要开发一个框架来满足以下两点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内容灵活排版: 某个频道页展示的内容及其顺序, 甚至说一个新的频道页, 皆可由运营同学在cms后台直接配置&lt;/li&gt;
&lt;li&gt;模块全局复用: 一个承载内容的 &lt;code&gt;模块&lt;/code&gt; 开发完后, 可在全部频道页配置使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;页面模块化的好处:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;方便运营同学在线上cms后台直接创建新的界面或动态调整界面(导航栏、页头脚、内容元素等). 缩短内容上线周期&lt;/li&gt;
&lt;li&gt;在我们不同的业务代码组件化后, 是相互隔离的. 不同的业务线开发好的业务组件难以复用(数据模型、命名、方法定义等都不统一). 而且并非所有业务组件都能封装成通用型基础组件下沉到基础组件库中. 开发这个框架, 就可以同时创建一个统一规范的业务组件库&lt;/li&gt;
&lt;/ol&gt;
    
    </summary>
    
      <category term="小结" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/"/>
    
      <category term="iOS" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/iOS/"/>
    
      <category term="2018" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/iOS/2018/"/>
    
    
      <category term="模块化" scheme="https://shawnfoo.github.io/tags/%E6%A8%A1%E5%9D%97%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>直播应用送礼动画特效实现</title>
    <link href="https://shawnfoo.github.io/2017/07/20/%E7%9B%B4%E6%92%AD%E5%BA%94%E7%94%A8%E9%80%81%E7%A4%BC%E5%8A%A8%E7%94%BB%E7%89%B9%E6%95%88%E5%AE%9E%E7%8E%B0/"/>
    <id>https://shawnfoo.github.io/2017/07/20/直播应用送礼动画特效实现/</id>
    <published>2017-07-19T16:00:00.000Z</published>
    <updated>2021-05-24T16:20:26.288Z</updated>
    
    <content type="html"><![CDATA[<p>送礼物作为观众打赏支持主播的一种方式, 也是直播应用的一大收入来源, 每个直播平台都包含送礼这一功能, 并且都把礼物动画效果做的特别炫酷. 如此的动画效果再搭配美女或帅哥主播的一句”谢谢某某某送的大飞机~”, 是不是想想都有点小激动, 感觉瞬间成为了全场的焦点? </p><p>本文主要叙述的就是大礼物动效的实现.</p><p>先放上按序列帧播放方案实现的动画引擎<a href="https://github.com/ShawnFoo/FXAnimationEngine" target="_blank" rel="noopener">FXAnimationEngine</a>, Demo中实现了直播间礼物队列、礼物配置、礼物列表, 另外还分别用动画引擎与原生Core Animation去播放序列帧动画以做比较.</p><a id="more"></a><p>然后国际惯例, 上两张图</p><p><img src="/2017/07/20/直播应用送礼动画特效实现/mhcb.gif" alt="梦幻城堡"></p><p><img src="/2017/07/20/直播应用送礼动画特效实现/angle.gif" alt="天使"></p><hr><h2 id="一、直播应用礼物动画的常见方案"><a href="#一、直播应用礼物动画的常见方案" class="headerlink" title="一、直播应用礼物动画的常见方案"></a>一、直播应用礼物动画的常见方案</h2><p>仅个人了解, 实现iOS侧动画配置化常见方案有如下几种:</p><table><thead><tr><th style="text-align:center">iOS方案</th><th style="text-align:center">优点</th><th style="text-align:center">缺点</th></tr></thead><tbody><tr><td style="text-align:center">Core Animation(此处不计CAKeyFrameAnimation)</td><td style="text-align:center">效果流畅逼真</td><td style="text-align:center">安卓需重新实现; 配置化成本高, 需自定义模型、协议、转换方法等(iOS侧已有现成工具, 某几家直播公司想必也有自己的动画配置化工具); 不解决动态配置问题, 则只能随包更新.</td></tr><tr><td style="text-align:center">序列帧播放(CAKeyframeAnimation、CADisplaylink、ImageView等)</td><td style="text-align:center">设计哥工具可直接导出动画序列帧图片, 简单易用; 多平台兼容</td><td style="text-align:center">效果略差; 图片帧数多易导致资源大</td></tr><tr><td style="text-align:center">Cocos2d-x</td><td style="text-align:center">效果好; 多平台兼容</td><td style="text-align:center">学习成本; 相应动画制作工具; 必须引入Cocos2d库;</td></tr><tr><td style="text-align:center">Lottie</td><td style="text-align:center">横跨三端, iOS, Android, React Native. 设计师可以完全按照自己的想法设计. 无需考虑实现这一块.</td><td style="text-align:center">内存占用? 作者本人尚未使用过, 不敢妄自评论</td></tr></tbody></table><p>可以看出, 序列帧播放方案是其中最简单易行的一个. 在我看来, <strong>花椒直播</strong>用的即是这套方案, 他们每一个动画, 都会对应一个配置文件config.ini及对应该动画的所有序列帧图片.</p><p>感兴趣的朋友可以移至最后一部分<code>礼物资源的下载策略、资源目录结构等</code>相关内容, 更建议尝试去探索一下花椒、映客等主流直播应用的bundle目录以及document中的资源.</p><h2 id="二、序列帧播放方案实践"><a href="#二、序列帧播放方案实践" class="headerlink" title="二、序列帧播放方案实践"></a>二、序列帧播放方案实践</h2><h3 id="2-1-实现方式"><a href="#2-1-实现方式" class="headerlink" title="2.1 实现方式"></a>2.1 实现方式</h3><p>序列帧播放动画一方案的具体实现必须能够满足以下需求:</p><ol><li>图片展示: CALayer、UIImageView</li><li>按时间间隔逐帧播放: CAKeyframeAnimation、UIImageView、定时器类(CADisplayLink、NSTimer、dispatch_source_t)+切换关键帧逻辑</li><li>提供所有序列帧播放完的事件: CAAnimationDelegate、CATransaction CompletionBlock、定时器类+回调触发逻辑</li></ol><p>组合方式很多, 比如: CALayer+CAKeyframeAnimation+delegate, UIImageView+定时器, CALayer+定时器类等等.</p><p>我们先选定这一套组合进行实践: <code>CALayer+CAKeyframeAnimation+delegate</code></p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 伪代码</span></span><br><span class="line">- (<span class="keyword">void</span>)startAnimation &#123;</span><br><span class="line">    <span class="built_in">UIImage</span> *frame = [<span class="built_in">UIImage</span> imageWithContentsOfFile:...];</span><br><span class="line">    <span class="built_in">NSArray</span>&lt;<span class="built_in">UIImage</span> *&gt; *frames = @[(<span class="keyword">id</span>)frame.CGImage, ...];</span><br><span class="line">    <span class="built_in">CAKeyframeAnimation</span> *keyframeAnim = ...;</span><br><span class="line">    keyframeAnim.contents = frames;</span><br><span class="line">    ...</span><br><span class="line">    keyframeAnim.delegate = <span class="keyword">self</span>;</span><br><span class="line">    [xxx.layer addAnimation:keyframeAnim forKey:<span class="string">@"xxx"</span>];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)animationDidStop:(<span class="built_in">CAAnimation</span> *)anim finished:(<span class="built_in">BOOL</span>)flag &#123;</span><br><span class="line">    <span class="comment">// 触发动画播放结束(全部播放完、中途结束)回调</span></span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果此处你已经下载了Demo, 可以打开Debug Navigator(<code>cmd+6</code>)简单查看内存增长或者留意Xcode Instrument-Allocations中<code>VM:ImageIO_PNG_Data</code>一项, 就会看到有内存增长波峰. 而且序列帧图片越多, 波峰越明显.</p><p>那么其他方案是否出现了相同的问题呢? 是的, 其他方案一样会如此, 换成UIImageView自带的animationImages来做序列帧播放或是其他组合方式, 也出现内存激增的情况.</p><h3 id="2-2-了解图片加载"><a href="#2-2-了解图片加载" class="headerlink" title="2.2 了解图片加载"></a>2.2 了解图片加载</h3><p>在我们搞清楚是什么导致内存激增前, 我们先了解一下图片从磁盘加载, 到写入内存, 最后显示到屏幕上分别都发生了什么. 大致分为如下步骤:</p><ol><li>为磁盘中的图片创建映射</li><li>IO操作读取图片数据流</li><li>图片解码位图拷贝, 写入内存</li><li>硬件绘制渲染到屏幕</li></ol><h4 id="2-2-1-映射文件"><a href="#2-2-1-映射文件" class="headerlink" title="2.2.1 映射文件"></a>2.2.1 映射文件</h4><p>当我们通过<code>[UIImage imageWithContentsOfFile:]</code>从磁盘加载图片数据流, 实际上只是为此图片创建了一个文件映射数据, 图片文件既没有真正被加载到内存, 更没有被解码成<a href="https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_images/dq_images.html#//apple_ref/doc/uid/TP30001066-CH212-SW3" target="_blank" rel="noopener">位图</a>的形式可供Core Animation传递给底层硬件进行渲染, 故此时内存并不会明显增加, 也不会出现因为解码操作导致CPU使用增加的情形. 但从网络下载图片数据不包含在内.</p><p>简单提及一下映射文件:</p><blockquote><p>A mapped file uses virtual memory techniques to avoid copying<br>pages of the file into memory until they are actually needed.</p></blockquote><p>直译就是一个映射文件借助虚拟内存技术来避免当他们还没有真正使用到时就被拷贝到内存中. </p><p>下面来一组对照验证一下:</p><p>对照组一</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)test1 &#123;</span><br><span class="line">    <span class="built_in">UIImage</span> *frame = [<span class="built_in">UIImage</span> imageWithContentsOfFile:filePath];</span><br><span class="line">    <span class="comment">// 确保超出局部作用域后, 依旧保持对这个Image对象的强引用</span></span><br><span class="line">    <span class="keyword">self</span>.frame = frame;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 待上方函数执行完后, 再查看内存使用情况</span></span><br></pre></td></tr></table></figure><p>对照组二</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)test2 &#123;</span><br><span class="line">    <span class="built_in">UIImage</span> *frame = [<span class="built_in">UIImage</span> imageWithContentsOfFile:filePath];</span><br><span class="line">    <span class="keyword">self</span>.imageview.image = frame;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们可以发现对照组二的内存占用明显比对照组一要多. 即通过<code>imageWithContentsOfFile:</code>创建的UIImage对象后, 内存并没有明显增长, 等我们将该UIImage对象赋值给UIImageView的image属性后的某个时刻, 内存才出现明显增长.</p><p>此处再留几个问题:</p><ol><li>我们都知道<code>imageWithName:</code>方法加载的图片, 会被系统缓存, 那么第一次通过该方法进行如上两个对照组的实验, 结果如何呢?</li><li>通过<code>imageWithName:</code>方法第2、3..n次加载同名图片时, 加载的图片数据流会不会再次被解码? 期间CPU占用有没有增加?</li><li>尝试把创建的UIImage对象桥接赋值给CALayer的contents属性, 结果如何?</li></ol><h4 id="2-2-2-浅谈CALayer的隐式动画及事务"><a href="#2-2-2-浅谈CALayer的隐式动画及事务" class="headerlink" title="2.2.2 浅谈CALayer的隐式动画及事务"></a>2.2.2 浅谈CALayer的隐式动画及事务</h4><p>从上一节中, 我们发现当给UIImageView的image属性或CALayer的contents属性赋值Image对象后的某一刻, 内存和CPU占用才会出现明显变化. 那是因为每一次Runloop循环, Core Animation都会在其开始创建一个动画事务, 在本次Runloop结束时才去执行所有添加到该事务里的所有动画操作. 此刻图片才被解码加载入内存, 图片数据会被解码为渲染可用的<a href="https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_images/dq_images.html#//apple_ref/doc/uid/TP30001066-CH212-SW3" target="_blank" rel="noopener">bitmap</a>数据. 一些相关细节可看我另一篇分享.</p><p><a href="https://shawnfoo.github.io/2017/06/05/%E6%B5%85%E8%B0%88CALayer%E7%9A%84%E9%9A%90%E5%BC%8F%E5%8A%A8%E7%94%BB%E5%8F%8A%E4%BA%8B%E5%8A%A1/">浅谈CALayer隐式动画及事务</a></p><h3 id="2-3-解决内存激增问题"><a href="#2-3-解决内存激增问题" class="headerlink" title="2.3 解决内存激增问题"></a>2.3 解决内存激增问题</h3><p>当前我们面临的问题是无论采用何种实现方案, 在执行序列帧动画时, 所有图片都会被解码成为位图并载入内存中. </p><h4 id="2-3-1-解压后的图片所占内存大小"><a href="#2-3-1-解压后的图片所占内存大小" class="headerlink" title="2.3.1 解压后的图片所占内存大小"></a>2.3.1 解压后的图片所占内存大小</h4><p>图片解码后的格式为<a href="https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_images/dq_images.html#//apple_ref/doc/uid/TP30001066-CH212-SW3" target="_blank" rel="noopener">位图</a>形式. 位图是由一组像素(pixel)组成的, 每一个像素就代表图片中的一个点. 比如常见的JPEG, 以及PNG格式的图片文件都是位图图片. </p><p>我们还需要知道, JPEG和PNG图片实际上都是一种编码/压缩后的位图格式, 它们是不能直接用来图片渲染的, 所以得先对其压缩的数据进行解码/解压缩操作.</p><p>那么一张解压后的位图其所占内存大小怎么计算呢? </p><p>此处假设我们有一张32位的PNG格式图片, 其像素格式为RGBA四部分组成, 每部分占8位, 该图片尺寸为160px * 320px. </p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">32位的图片意味着其每个像素占32位, 即4个字节.</span><br><span class="line">又根据图片尺寸计算出总像素数量为 160*320 个像素.</span><br><span class="line">所以该图片解码后所占内存大小就为 像素总数 * 单位像素的字节数</span><br><span class="line">即 (160*320) * 4 / 1024 = 200 KB.</span><br></pre></td></tr></table></figure><p>所以可想而知, 假设一个序列帧动画有80张图片, 200 * 80 / 1024 = 15.625 mb, 就会占用15mb的内存. 序列帧图片越多, 占用内存越大!</p><h4 id="2-3-2-解决方案"><a href="#2-3-2-解决方案" class="headerlink" title="2.3.2 解决方案"></a>2.3.2 解决方案</h4><p>那么有什么方法可以避免呢? 可否<strong>每次播放到哪一帧时就去加载那一帧的图片, 即每次仅加载一张图片到内存中. 这样当播放到下一张图片时, 上一张图片已无任何引用, 系统自然会对其进行释放.</strong></p><p>这就是最简单可行的一套方案. 但是我们无法靠CAAnimation及其派生类CAKeyframeAnimation来实现这一方案, 因为所有的图片都会解码导致占用大量的内存. </p><p>但我们可以通过CADisplayLink来实现该方案, 选CADisplayLink的原因是它比NSTimer精度要高很多, 正常情况下CADisplayLink的回调会在屏幕每次刷新时触发, 即一般1/60秒触发一次, 适合用于做UI的重绘, 因此可以通过它来周期性的替换关键帧图片, 从而达到播放动画的效果. 那么具体怎么做呢?</p><p><strong>在CADisplayLink的回调中获取两次屏幕刷新的间隔时间, 通过不断的累加间隔时间来判断总的时间是否已经满足下一帧的播放时刻, 如果大于下一帧的播放时刻就可以替换为下一帧图片了, 直至最后一张关键帧也播放完成.</strong></p><p>举个例子, 我们要在1秒内播放完一个含有5张关键帧图片的动画, 每张图片的停留时间、切换时间如下图2.3.2.a所示. 所以第0秒的时候就开始展示第一张关键帧, 直到1.0秒这一刻时, 动画播放结束.</p><p><img src="/2017/07/20/直播应用送礼动画特效实现/2_3_2_a.png" alt="图 2.3.2.a"></p><p>此外, 如果还需要进一步优化, 我们可以加入图片异步解码、图片预加载逻辑等方案. </p><ul><li><p>异步图片解码, 图片解码是一项比较耗时、比较占CPU的操作, 对于未解码的图片, 系统一般会在主线程对其进行解码, 所以可以通过在异步线程进行图片强制解压缩, 从而不占用UI线程. 关于图片解码的详情, 强烈推荐<a href="http://www.cocoachina.com/ios/20170227/18784.html" target="_blank" rel="noopener">谈谈 iOS 中图片的解压缩</a>.</p></li><li><p>图片预加载, 这个就是为了进一步节省上下文切换时间, 即前后两张图片切换的时间. 就是要做到当上一帧图片播放完时, 我们不用等下一张图片解码完成后再进行图片的切换, 而是可以直接从已解码图片的缓存队列中取出直接进行切换. 预加载我个人觉得其实主要就是阈值的最优选择, 可参考<a href="https://zhuanlan.zhihu.com/p/23418800" target="_blank" rel="noopener">预加载与智能预加载</a>一文.</p></li><li><p><a href="https://stackoverflow.com/questions/23790837/what-is-byte-alignment-cache-line-alignment-for-core-animation-why-it-matters" target="_blank" rel="noopener">字节对齐(byte alignment)对Core Animation性能的影响</a></p></li></ul><h2 id="三、序列帧动画引擎源代码及Demo"><a href="#三、序列帧动画引擎源代码及Demo" class="headerlink" title="三、序列帧动画引擎源代码及Demo"></a>三、序列帧动画引擎源代码及Demo</h2><p><a href="https://github.com/ShawnFoo/FXAnimationEngine" target="_blank" rel="noopener">FXAnimationEngine - Github跳转</a></p><p>针对该Demo近期会另起一文特别介绍, 此处占坑, 等待跳转链接</p><h2 id="四、礼物资源下载策略及资源目录结构"><a href="#四、礼物资源下载策略及资源目录结构" class="headerlink" title="四、礼物资源下载策略及资源目录结构"></a>四、礼物资源下载策略及资源目录结构</h2><h3 id="4-1-礼物资源下载策略"><a href="#4-1-礼物资源下载策略" class="headerlink" title="4.1 礼物资源下载策略"></a>4.1 礼物资源下载策略</h3><h4 id="4-1-1-两种方式比较"><a href="#4-1-1-两种方式比较" class="headerlink" title="4.1.1 两种方式比较"></a>4.1.1 两种方式比较</h4><table><thead><tr><th style="text-align:center">方式</th><th style="text-align:center">基本思路</th><th style="text-align:center">优点</th><th style="text-align:center">缺点</th></tr></thead><tbody><tr><td style="text-align:center">整包更新</td><td style="text-align:center">所有的动画资源按目录结构进行压缩, 客户端通过比较资源包版本号发现有更新后, 仅需下载一个资源包压缩文件, 并进行解压替换即可</td><td style="text-align:center">简单易实现, 客户端每次仅需下载一个资源包</td><td style="text-align:center">随着资源包逐渐增大, 下载及解压时间也会延长, 从而直接影响用户体验; 即使是仅是资源中的某个图片发生改变, 客户端都要重新下载整个资源包, 容错率低且浪费流量</td></tr><tr><td style="text-align:center">增量更新</td><td style="text-align:center">每个动画资源单独压缩并上传CDN, 若客户端发现资源版本号有变化, 再对服务器下发的资源列表跟本地资源列表求差集运算从而得出增量, 单个动画资源的下载地址或者md5可作为唯一标识进行比较. 得出增量后, 客户端再对每个增量资源包进行下载, 每下载完一个即可”投入使用”</td><td style="text-align:center">不怕资源变更频繁; 仅需下载有新增或有变更的资源包, 更节省时间以及流量;</td><td style="text-align:center">逻辑略复杂于整包更新, 比如下载中途用户把应杀掉, 下次需要找出未更新完的增量资源并继续下载</td></tr></tbody></table><h4 id="4-1-2-资源更新流程"><a href="#4-1-2-资源更新流程" class="headerlink" title="4.1.2 资源更新流程"></a>4.1.2 资源更新流程</h4><p><strong>因对上家公司的代码保密, 此处不上具体代码</strong></p><p>我们在上一小节中提及的两种更新方式, 它们主要的不同的就在于”资源更新”这一步骤</p><p><em>图 4.1.2.a 整包更新的流程图</em><br><img src="/2017/07/20/直播应用送礼动画特效实现/4_1_2_a.png" alt="整包更新流程图.png"></p><p><em>图 4.1.2.b 增量更新的流程图</em></p><p><img src="/2017/07/20/直播应用送礼动画特效实现/4_1_2_b.png" alt="增量更新流程图.png"></p><p>不知道各位发现两个流程共同之处没? 它们都需要检测资源版本号大小, 包括游戏补丁、热更补丁这一步骤都必不可少. 相比于补丁类的, 资源更新不用太考虑灰度发布、回滚机制等问题, 但还是依旧需要注意资源核对, 内部测试, 以及日志监控等保障, 我记得在前任公司就遇到了有的地区下载下来的资源包有问题, 所以不管是CDN的问题或资源本身有问题, 前端都需要为最坏的情况做好打算, 这才是万全之策.</p><p>引用我上家公司, 我老大兼mentor, 达文哥, 告诫的一句箴言</p><blockquote><p>不要相信后台下发的数据都是正确的</p></blockquote><p>大概意思如此, 原句没背下来😂, <strong>这句话绝非不是指后台同学不行, 或者甩锅给后台</strong>, 而是要<code>prepare for the worst</code>. </p><p>前后端测试都是一家人, 遇到问题我们先看看是不是自己问题, 不要相互甩锅..本是同根生相煎何太急, 如果有问题就一块搓一顿, 一顿不行就再来一顿</p><h3 id="4-2-资源目录结构设计"><a href="#4-2-资源目录结构设计" class="headerlink" title="4.2 资源目录结构设计"></a>4.2 资源目录结构设计</h3><p>不管哪个直播平台, 每个礼物都会对应一个逻辑id, 我们可以通过礼物的id作为该礼物的资源目录名, 然后在该目录内在去划分不同类型的图片子目录, 如下所示</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">- 10000 // 一级目录, 礼物id</span><br><span class="line">  -- gift// 二级目录, 小礼物序列帧图片</span><br><span class="line">  -- giftlist    // 二级目录, 礼物列表序列帧图片 </span><br><span class="line">  -- giftanim    // 二级目录, 大动画序列帧图片</span><br></pre></td></tr></table></figure><p>这只是其中的一种设计, 也有的平台会采用如下形式, 所以主要还是看需求而定</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">- gift</span><br><span class="line">  -- 10000</span><br><span class="line">- giftlist</span><br><span class="line">  -- 10000</span><br><span class="line">- giftanim</span><br><span class="line">  -- 10000</span><br></pre></td></tr></table></figure><p>此外, 有的平台还会采用id_version, 即礼物id+礼物版本的形式来命名, 这样可以方便配置使后台可以灵活下发给前端具体要去播放哪个动画的某个版本了</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">- 10000_11  // id为10000, 版本为11的礼物资源目录</span><br><span class="line">- 10000_12</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;送礼物作为观众打赏支持主播的一种方式, 也是直播应用的一大收入来源, 每个直播平台都包含送礼这一功能, 并且都把礼物动画效果做的特别炫酷. 如此的动画效果再搭配美女或帅哥主播的一句”谢谢某某某送的大飞机~”, 是不是想想都有点小激动, 感觉瞬间成为了全场的焦点? &lt;/p&gt;
&lt;p&gt;本文主要叙述的就是大礼物动效的实现.&lt;/p&gt;
&lt;p&gt;先放上按序列帧播放方案实现的动画引擎&lt;a href=&quot;https://github.com/ShawnFoo/FXAnimationEngine&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FXAnimationEngine&lt;/a&gt;, Demo中实现了直播间礼物队列、礼物配置、礼物列表, 另外还分别用动画引擎与原生Core Animation去播放序列帧动画以做比较.&lt;/p&gt;
    
    </summary>
    
      <category term="小结" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/"/>
    
      <category term="iOS" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/iOS/"/>
    
      <category term="2017" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/iOS/2017/"/>
    
    
      <category term="Animation" scheme="https://shawnfoo.github.io/tags/Animation/"/>
    
      <category term="CADisplayLink" scheme="https://shawnfoo.github.io/tags/CADisplayLink/"/>
    
  </entry>
  
  <entry>
    <title>浅谈CALayer的隐式动画及事务</title>
    <link href="https://shawnfoo.github.io/2017/06/05/%E6%B5%85%E8%B0%88CALayer%E7%9A%84%E9%9A%90%E5%BC%8F%E5%8A%A8%E7%94%BB%E5%8F%8A%E4%BA%8B%E5%8A%A1/"/>
    <id>https://shawnfoo.github.io/2017/06/05/浅谈CALayer的隐式动画及事务/</id>
    <published>2017-06-04T16:00:00.000Z</published>
    <updated>2021-05-13T06:46:03.453Z</updated>
    
    <content type="html"><![CDATA[<h3 id="一、前言"><a href="#一、前言" class="headerlink" title="一、前言"></a>一、前言</h3><p>本文是为了后续<a href="">直播App送礼大动画实战演练</a>做铺垫, 浅谈CALayer的隐式动画及事务. </p><p>全文主要涉及以下问题:</p><ul><li>CALayer图层的定义</li><li>何为隐式动画?</li><li>所有CALayer都有隐私动画吗? UIView的backing layer呢? </li><li>哪些对象遵循CAAction协议</li><li>动画事务是什么?</li></ul><a id="more"></a><h3 id="二、-CALayer"><a href="#二、-CALayer" class="headerlink" title="二、 CALayer"></a>二、 CALayer</h3><p>CALayer图层, 是数据模型, 数据对象. 对于iOS平台, 一个UIView视图在展示之前, 系统都会为其创建一个支持图层(backing layer), 其中就储存了View外貌样式的表现内容, 比如图层通过contents属性来管理<a href="https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_images/dq_images.html#//apple_ref/doc/uid/TP30001066-CH212-SW3" target="_blank" rel="noopener">bitmap位图</a>, 从而充当位图的容器.</p><p>Backing layer的delegate(CALayerDelegate)就是该图层所属的view对象.</p><h3 id="三、-隐式动画"><a href="#三、-隐式动画" class="headerlink" title="三、 隐式动画"></a>三、 隐式动画</h3><p>我们平常都肯定使用过显式动画, 但我们比较少见到的隐式动画(implicit animation)又是什么. </p><p>从Core Animation Guide官方指南中, 并没有找到隐式动画的明确定义, 仅提及了以下相关内容:</p><ol><li>直接更改图层CALayer的属性就会触发隐式动画, 但是<strong>修改UIView对象支持图层(backing layer)的动画属性是不会发生隐式动画的</strong>, 因为UIView默认禁止了backing layer的隐式动画, 所以对backing layer属性的修改在UIView上的反应是直接变化, 没有平滑过度的动画效果</li><li>隐式动画会使用当前动画事务的参数默认值来执行动画</li><li>隐式动画会直接更改了layer模型中的值, 而显式动画不会(在动画执行完后, 会根据layer模型的属性进行”还原”, 所以我们在添加动画后需要手动修改layer的属性来确保动画完成时, 图层能够”还原”到动画结束时的位置! 只有presentationLayer中的值会随着动画执行不断改变.)</li><li>隐式动画执行过程中无法<strong>直接</strong>被移除, 而显式动画可以通过实例方法removeAnimationForKey:或removeAllAnimations来直接移除</li><li>Core Animation通过 遵循CAAction协议的对象 来实现隐式动画</li></ol><p>此处来一发示例, 帮助我们理解一下上方第1点:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> 现有UIView对象 viewA, 以及一个CALayer对象 layerB, 我们把 layerB 添加到 </span></span><br><span class="line"><span class="comment"> viewA 的 layer(这个就叫backing layer) 上.</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 对backing layer进行属性修改, 不会发生隐式动画, 而是由之前的颜色直接变成红色</span></span><br><span class="line">viewA.layer.backgroundColor = [<span class="built_in">UIColor</span> redColor].CGColor;</span><br><span class="line"></span><br><span class="line"><span class="comment">// layerB作为viewA.layer的子图层, 会发生隐式动画, layerB的颜色会由原先的颜色平滑过渡到蓝色</span></span><br><span class="line">layerB.backgroundColor = [<span class="built_in">UIColor</span> blueColor].CGColor;</span><br></pre></td></tr></table></figure><p>至此, 想必我们已经清楚CALayer何时会发生隐式动画了, 但后半句中说UIView的backing layer不会有隐式动画又是何解? </p><p>这里需要关联第5点中提到的遵循CAAction协议的对象</p><p>实际上, CAAnimation及其派生类, 如CABasicAnimation、CASpringAnimation, 以及CATransition都遵循并实现了 CAAction 协议中的方法. 当CALayer的动画属性改变时就会去查找匹配的CAAniamtion对象来执行动画. 其<a href="https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/ReactingtoLayerChanges/ReactingtoLayerChanges.html#//apple_ref/doc/uid/TP40004514-CH7-SW2" target="_blank" rel="noopener">查找流程</a>用代码形式表示如下:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">1. 若返回遵循 CAAction 协议对象, 则用其执行动画(runActionForKey:object:arguments:)</span></span><br><span class="line"><span class="comment">2. 若该方法返回nil, 不执行隐式动画, 直接更新属性</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line">- (<span class="keyword">id</span>&lt;<span class="built_in">CAAction</span>&gt;)searchActionForLayer:(<span class="built_in">CALayer</span> *)layer forKey:(<span class="built_in">NSString</span> *)key &#123;</span><br><span class="line">    <span class="keyword">id</span>&lt;<span class="built_in">CAAction</span>&gt; action = <span class="literal">nil</span>;</span><br><span class="line">    <span class="comment">// 先问代理该key对应的Action, 如果返回不为nil, 则已找到; 如果返回nil, 则继续执行查找逻辑; 如果返回NSNull对象则停止剩余查找逻辑, 最终返回NSNull</span></span><br><span class="line">    <span class="keyword">if</span> ([layer.delegate respondsToSelector:<span class="keyword">@selector</span>(actionForLayer:forKey:)]) &#123;</span><br><span class="line">        <span class="keyword">id</span>&lt;<span class="built_in">CAAction</span>&gt; action = [layer.delegate actionForLayer:layer forKey:key];</span><br><span class="line">        <span class="keyword">if</span> ([<span class="built_in">NSNull</span> null] == action) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (action) &#123;</span><br><span class="line">        <span class="keyword">return</span> action;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (!action) &#123;</span><br><span class="line">        action = [<span class="keyword">self</span> actionForKey:key <span class="keyword">in</span>:layer.actions];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (!action) &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="built_in">NSDictionary</span>&lt;<span class="built_in">NSString</span>*, <span class="keyword">id</span>&lt;<span class="built_in">CAAction</span>&gt;&gt; *actions <span class="keyword">in</span> layer.style) &#123;</span><br><span class="line">            <span class="keyword">if</span> ((action = [<span class="keyword">self</span> actionForKey:key <span class="keyword">in</span>:actions])) &#123;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (!action) &#123;</span><br><span class="line">        action = [[layer <span class="keyword">class</span>] defaultActionForKey:key];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> action;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">id</span>&lt;<span class="built_in">CAAction</span>&gt;)actionForKey:(<span class="built_in">NSString</span> *)key <span class="keyword">in</span>:(<span class="built_in">NSDictionary</span>&lt;<span class="built_in">NSString</span>*, <span class="keyword">id</span>&lt;<span class="built_in">CAAction</span>&gt;&gt; *)actions &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="built_in">NSString</span> *actionKey <span class="keyword">in</span> actions.allKeys) &#123;</span><br><span class="line">        <span class="keyword">if</span> ([actionKey isEqualToString:key]) &#123;</span><br><span class="line">            <span class="keyword">return</span> actions[actionKey];</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>UIView作为backing layer的delegate(as CALayerDelegate), 实现了-actionForLayer:forKey方法; 当不处于动画block范围内时, 该方法返回nil; 否则, 返回对应的动画对象. 关联上方函数注释<code>若该方法返回nil, 不执行隐式动画, 直接更新属性</code>, 这就是为什么UIView的backing layer不会有隐式动画.</p><h3 id="四、-动画事务"><a href="#四、-动画事务" class="headerlink" title="四、 动画事务"></a>四、 动画事务</h3><p>动画事务, 也类似于数据库事务, 用于组合某个逻辑里边的一系列操作. Core Animation会自动为我们某个图层的单个或多个显式动画、隐式动画创建隐式事务. 当然, 我们也可以通过 CATransaction 的类方法, 来显式创建事务.</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 显式事务</span></span><br><span class="line">[<span class="built_in">CATransaction</span> begin];</span><br><span class="line">[<span class="built_in">CATransaction</span> setValue:@(<span class="number">0.3</span>) forKey:kCATransactionAnimationDuration];</span><br><span class="line">layer.opacity = <span class="number">0.0</span>;</span><br><span class="line">[<span class="built_in">CATransaction</span> commit];</span><br><span class="line"></span><br><span class="line"><span class="comment">// UIView提供用来做动画的类方法等价于上方显式创建动画, 因为这些类方法内部就调用了CATransaction的+begin, +commit方法</span></span><br><span class="line">[<span class="built_in">UIView</span> animateWithDuration:<span class="number">0.3</span> animations:^&#123;</span><br><span class="line">    layer.opacity = <span class="number">0.0</span>;</span><br><span class="line">&#125;];</span><br></pre></td></tr></table></figure><p>当runloop开始一次新的循环时, Core Animation就会开启一个事务, 在本次循环中的所有动画操作, 包括我们显示创建的事务都会被嵌套其中, 直至本次runloop循环结束之时, 再一块提交进行动画. 比如下方代码:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// layerA、layerB、layerC均为寄宿/单独图层(hosted layer/standalone layer)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// --- runloop 新的循环开始 ---</span></span><br><span class="line">[<span class="built_in">CATransaction</span> begin]; <span class="comment">// Core Animation 开始的新事务</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// -- 本次循环中, 我们涉及的动画操作 start -- </span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 修改单独图层属性, 将以最外层CATransaction的动画参数发起隐式动画, 动画时间为当前所在事务的动画时间, 即默认的0.25秒</span></span><br><span class="line">layerA.backgroundColor = [<span class="built_in">UIColor</span> redColor].CGColor; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建显式事务, 嵌套事务</span></span><br><span class="line">[<span class="built_in">CATransaction</span> begin];</span><br><span class="line">[<span class="built_in">CATransaction</span> setValue:@(<span class="number">1</span>) forKey:kCATransactionAnimationDuration];</span><br><span class="line"><span class="comment">// 当前事务中的隐式动画, 执行时间为1秒</span></span><br><span class="line">layerB.position = <span class="built_in">CGPoint</span>(<span class="number">110</span>, <span class="number">119</span>);</span><br><span class="line">[<span class="built_in">CATransaction</span> commit];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 实际也相当于嵌套事务</span></span><br><span class="line">[<span class="built_in">UIView</span> animateWithDuration:<span class="number">0.5</span> animations:^&#123;</span><br><span class="line">    layerC.opacity = <span class="number">0.0</span>;</span><br><span class="line">&#125;];</span><br><span class="line"></span><br><span class="line"><span class="comment">// -- 本次循环中, 我们涉及的动画操作 end -- </span></span><br><span class="line"></span><br><span class="line">[<span class="built_in">CATransaction</span> commit]; <span class="comment">// Core Animation 提交所有事务</span></span><br><span class="line"><span class="comment">// --- runloop 循环结束 ---</span></span><br></pre></td></tr></table></figure><p>利用事务, 禁止动画发生:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">[<span class="built_in">CATransaction</span> begin];</span><br><span class="line"><span class="comment">// 设置 kCATransactionDisableActions 为 false</span></span><br><span class="line">[<span class="built_in">CATransaction</span> setValue:@(<span class="literal">false</span>)</span><br><span class="line">                 forKey:kCATransactionDisableActions];</span><br><span class="line"></span><br><span class="line"><span class="comment">// UIView Animation显示动画失效</span></span><br><span class="line">[<span class="built_in">UIView</span> animateWithDuration:<span class="number">0.33</span></span><br><span class="line">                 animations:^&#123;</span><br><span class="line">                 <span class="comment">// 单独的图层layerA</span></span><br><span class="line">                    layerA.backgroundColor = [<span class="built_in">UIColor</span> redColor].CGColor;</span><br><span class="line">                 &#125;];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 单独的图层layerB, 隐式动画失效</span></span><br><span class="line">layerB.opacity = <span class="number">0.0</span>;</span><br><span class="line"></span><br><span class="line">[<span class="built_in">CATransaction</span> commit];</span><br></pre></td></tr></table></figure><h3 id="五、推荐资料"><a href="#五、推荐资料" class="headerlink" title="五、推荐资料"></a>五、推荐资料</h3><p>本文提及内容仅为Core Animation的冰山一角, 如果想深入了解iOS动画, 可以尝试阅读一下下方资料, 定会有一番收获!</p><p><a href="https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40004514" target="_blank" rel="noopener">Core Animation Guide</a><br><a href="https://book.douban.com/subject/25716177/" target="_blank" rel="noopener">iOS Core Animation</a><br><a href="http://calayer.com/core-animation/2016/05/17/catransaction-in-depth.html" target="_blank" rel="noopener">CATransaction In Depth</a></p>]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;一、前言&quot;&gt;&lt;a href=&quot;#一、前言&quot; class=&quot;headerlink&quot; title=&quot;一、前言&quot;&gt;&lt;/a&gt;一、前言&lt;/h3&gt;&lt;p&gt;本文是为了后续&lt;a href=&quot;&quot;&gt;直播App送礼大动画实战演练&lt;/a&gt;做铺垫, 浅谈CALayer的隐式动画及事务. &lt;/p&gt;
&lt;p&gt;全文主要涉及以下问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CALayer图层的定义&lt;/li&gt;
&lt;li&gt;何为隐式动画?&lt;/li&gt;
&lt;li&gt;所有CALayer都有隐私动画吗? UIView的backing layer呢? &lt;/li&gt;
&lt;li&gt;哪些对象遵循CAAction协议&lt;/li&gt;
&lt;li&gt;动画事务是什么?&lt;/li&gt;
&lt;/ul&gt;
    
    </summary>
    
      <category term="小结" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/"/>
    
      <category term="iOS" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/iOS/"/>
    
      <category term="2017" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/iOS/2017/"/>
    
    
      <category term="Animation" scheme="https://shawnfoo.github.io/tags/Animation/"/>
    
  </entry>
  
  <entry>
    <title>FXDanmaku弹幕库介绍</title>
    <link href="https://shawnfoo.github.io/2017/02/26/FXDanmaku%E5%BC%B9%E5%B9%95%E5%BA%93%E4%BB%8B%E7%BB%8D/"/>
    <id>https://shawnfoo.github.io/2017/02/26/FXDanmaku弹幕库介绍/</id>
    <published>2017-02-25T16:00:00.000Z</published>
    <updated>2021-05-13T06:46:03.452Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>去年, 2016年, 一大波直播平台在移动端涌出, 直播慢慢步入了人们的视角. 网上如今能够看到各式各样的直播, 如秀场直播、游戏直播、体育直播、娱乐直播等等. </p><p>在各种类型的直播中, 弹幕在PC、移动端都几乎成为了标配, 今天在这里主要介绍一下个人开源的iOS弹幕, 以及提前为<code>实现一款弹幕库涉及的相关技术分享</code>的相关篇章占坑, 虽不细至于手把手教如何实现, 但关键点都会有所涉及且不仅限于实现弹幕, 如iOS中用pthread实现生产者消费者模型、响应正在执行动画对象的点击事件、实现某类对象复用的ReuseQueue、使用GCD封装实现可取消未执行代码块的OperationQueue等等, <em>对这些更感兴趣的朋友麻烦直接滑至最后一段.</em></p><a id="more"></a><p>欢迎各位大神指点一二. </p><h4 id="广告"><a href="#广告" class="headerlink" title="广告"></a>广告</h4><p>统计了各渠道的一周浏览记录, 以及github浏览次数、评论留言数, 感觉弹幕估计是有点过时了..感兴趣的朋友比较少, 所以<strong>弹幕相关技术分享打算暂时一缓</strong>. 准备开源并分享一下可能更多人感兴趣的序列帧动画引擎, Demo会通过分别Core Animation以及个人FXAnimationEngine来实现花椒礼物动画的效果(资源花椒ipa中提取), 比较内存占用, 动画被系统打断情况下的表现, 图片解码相关知识等. 此外, 还会分享一下礼物资源热更的方案.</p><h2 id="Github"><a href="#Github" class="headerlink" title="Github"></a>Github</h2><p><a href="https://github.com/ShawnFoo/FXDanmaku" target="_blank" rel="noopener">Talk is cheap, I’ll show you the code</a>.</p><p>请大力点击上方超链接⬆️⬆️⬆️⬆️⬆️</p><h2 id="特性"><a href="#特性" class="headerlink" title="特性"></a>特性</h2><ol><li>除了UI操作, 其他操作都以代码块交给异步队列处理了.(使用GCD提交的代码块, 最终会由XNU kernel根据CPU使用情况创建新的线程去执行或分配给其他线程执行)</li><li>遵循 生产者消费者模式, 通过pthread去阻塞队列而非使用timer或异步队列开启runloop空转</li><li>定义了包含 弹幕块点击、将出现、已消失事件的delegate</li><li>提供 注册复用 自定义弹幕块 的方法</li><li>各种自定义参数, 如弹幕块移速, 弹幕库插入方向(从上, 从下, 随机), 弹幕库移动方向(左到右, 右到左), 重置弹道位移百分比系数(防前后弹幕块碰撞)、弹幕队列容量控制</li><li>简单易用, 控制方法就三个 start(同时也是恢复), pause, stop. 另外大部分方法都是线程安全的</li><li>轻易适配设备方向旋转</li><li>设置单行配置即可作为 跑马灯、直播间公告 使用</li></ol><h2 id="预览图"><a href="#预览图" class="headerlink" title="预览图"></a>预览图</h2><p><img src="http://upload-images.jianshu.io/upload_images/303892-2d797e38efceb2f4.gif?imageMogr2/auto-orient/strip" alt=""><br><img src="http://upload-images.jianshu.io/upload_images/303892-824590dfc5af0cf6.gif?imageMogr2/auto-orient/strip" alt=""></p><h2 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h2><p>弹幕设置</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Configuration</span></span><br><span class="line">FXDanmakuConfiguration *config = [FXDanmakuConfiguration defaultConfiguration];</span><br><span class="line">config.rowHeight = [DemoDanmakuItem itemHeight];</span><br><span class="line">config.dataQueueCapacity = <span class="number">500</span>;</span><br><span class="line">config.itemMinVelocity = <span class="number">80</span>;  <span class="comment">// set random velocity between 80 and 120 pt/s</span></span><br><span class="line">config.itemMaxVelocity = <span class="number">120</span>;</span><br><span class="line"><span class="keyword">self</span>.danmaku.configuration = config;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Delegate</span></span><br><span class="line"><span class="keyword">self</span>.danmaku.delegate = <span class="keyword">self</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Reuse</span></span><br><span class="line">[<span class="keyword">self</span>.danmaku registerNib:[<span class="built_in">UINib</span> nibWithNibName:<span class="built_in">NSStringFromClass</span>([DemoDanmakuItem <span class="keyword">class</span>]) bundle:<span class="literal">nil</span>]</span><br><span class="line">   forItemReuseIdentifier:[DemoDanmakuItem reuseIdentifier]];</span><br><span class="line">[<span class="keyword">self</span>.danmaku registerClass:[DemoBulletinItem <span class="keyword">class</span>] </span><br><span class="line">     forItemReuseIdentifier:[DemoBulletinItem reuseIdentifier]];</span><br></pre></td></tr></table></figure><p>数据添加</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// add data for danmaku view to present</span></span><br><span class="line">DemoDanmakuItemData *data = [DemoDanmakuItemData data];</span><br><span class="line">[<span class="keyword">self</span>.danmaku addData:data];</span><br><span class="line"></span><br><span class="line"><span class="comment">// start running</span></span><br><span class="line"><span class="keyword">if</span> (!<span class="keyword">self</span>.danmaku.isRunning) &#123;</span><br><span class="line">    [<span class="keyword">self</span>.danmaku start];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代理事件</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)danmaku:(FXDanmaku *)danmaku didClickItem:(FXDanmakuItem *)item withData:(DemoDanmakuItemData *)data &#123;</span><br><span class="line">    <span class="comment">// 此处 处理点击</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)danmaku:(FXDanmaku *)danmaku willDisplayItem:(FXDanmakuItem *)item withData:(FXDanmakuItemData *)data &#123;</span><br><span class="line">    <span class="comment">// 此处 处理弹幕块将要出现/展示</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)danmaku:(FXDanmaku *)danmaku didEndDisplayingItem:(FXDanmakuItem *)item withData:(FXDanmakuItemData *)data &#123;</span><br><span class="line">    <span class="comment">// 此处 处理弹幕块完全离开视线，结束展示</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>更多详情 麻烦参照 <a href="https://github.com/ShawnFoo/FXDanmaku" target="_blank" rel="noopener">Gitbub Demo project</a> <code>FXDanmakuDemo.xcworkspace</code>. </p><h2 id="有关弹幕库使用问题答疑"><a href="#有关弹幕库使用问题答疑" class="headerlink" title="有关弹幕库使用问题答疑"></a>有关弹幕库使用问题答疑</h2><h4 id="1-rowHeight、estimatedRowSpace-and-rowSpace-三者之间的关系"><a href="#1-rowHeight、estimatedRowSpace-and-rowSpace-三者之间的关系" class="headerlink" title="1. rowHeight、estimatedRowSpace and rowSpace 三者之间的关系"></a>1. rowHeight、estimatedRowSpace and rowSpace 三者之间的关系</h4><p><img src="http://upload-images.jianshu.io/upload_images/303892-7c9d2ff6d846ccc3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""></p><h4 id="2-如何使用nib创建自定义弹幕块"><a href="#2-如何使用nib创建自定义弹幕块" class="headerlink" title="2. 如何使用nib创建自定义弹幕块"></a>2. 如何使用nib创建自定义弹幕块</h4><p><img src="http://upload-images.jianshu.io/upload_images/303892-a9224a1153c59bac.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""><br><img src="http://upload-images.jianshu.io/upload_images/303892-66430222e3e71551.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt=""></p><h4 id="3-如何适配设备屏幕旋转"><a href="#3-如何适配设备屏幕旋转" class="headerlink" title="3. 如何适配设备屏幕旋转"></a>3. 如何适配设备屏幕旋转</h4><p>如果你的弹幕View 在横竖屏状态下 高度不一样, 比如竖屏高200pt, 横屏约束却是100pt, 那么需要在对应的controller.m文件中加入以下代码(否则 当你的弹幕块使用AutoLayout进行布局时, 横竖屏切换后, 由于视图的frame会变化多次，导致正在展示的弹幕块 出现布局约束冲突的报错)</p><p><em>iOS8+</em></p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)viewWillTransitionToSize:(<span class="built_in">CGSize</span>)size withTransitionCoordinator:(<span class="keyword">id</span>&lt;<span class="built_in">UIViewControllerTransitionCoordinator</span>&gt;)coordinator &#123;</span><br><span class="line">    [<span class="keyword">self</span>.danmaku pause];</span><br><span class="line">    [<span class="keyword">self</span>.danmaku cleanScreen];</span><br><span class="line">    </span><br><span class="line">    [coordinator animateAlongsideTransition:<span class="literal">nil</span></span><br><span class="line">                             completion:^(<span class="keyword">id</span>&lt;<span class="built_in">UIViewControllerTransitionCoordinatorContext</span>&gt;  _Nonnull context) &#123;</span><br><span class="line">                                     <span class="comment">// resume danmaku after orientation did change</span></span><br><span class="line">                                     [<span class="keyword">self</span>.danmaku start];</span><br><span class="line">                                 &#125;];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><em>系统版本小于iOS8</em></p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)willRotateToInterfaceOrientation:(<span class="built_in">UIInterfaceOrientation</span>)toInterfaceOrientation duration:(<span class="built_in">NSTimeInterval</span>)duration &#123;</span><br><span class="line">    [<span class="keyword">self</span>.danmaku pause];</span><br><span class="line">    [<span class="keyword">self</span>.danmaku cleanScreen];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)didRotateFromInterfaceOrientation:(<span class="built_in">UIInterfaceOrientation</span>)fromInterfaceOrientation &#123;</span><br><span class="line">    [<span class="keyword">self</span>.danmaku start];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><h4 id="Cocoapods-iOS7"><a href="#Cocoapods-iOS7" class="headerlink" title="Cocoapods(iOS7+)"></a>Cocoapods(iOS7+)</h4><ol><li><p>Podfile中 视情况对应添加以下内容</p> <figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">platform :ios, &apos;xxx&apos;</span><br><span class="line">target &apos;xxx&apos; do</span><br><span class="line">  pod &apos;FXDanmaku&apos;</span><br><span class="line">end</span><br></pre></td></tr></table></figure></li><li><p><code>pod install</code></p></li></ol><h4 id="Manually-iOS7"><a href="#Manually-iOS7" class="headerlink" title="Manually(iOS7+)"></a>Manually(iOS7+)</h4><p>直接拖动 <code>FXDanmaku</code> 文件夹 到你的项目 对应结构下</p><h2 id="介绍结尾"><a href="#介绍结尾" class="headerlink" title="介绍结尾"></a>介绍结尾</h2><p>欢迎各位 提出宝贵的issues, 更多功能建议, 或者改进之处等等. 同时若各位想要了解弹幕库具体实现的其他相关点, 也可在评论区留言.</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;去年, 2016年, 一大波直播平台在移动端涌出, 直播慢慢步入了人们的视角. 网上如今能够看到各式各样的直播, 如秀场直播、游戏直播、体育直播、娱乐直播等等. &lt;/p&gt;
&lt;p&gt;在各种类型的直播中, 弹幕在PC、移动端都几乎成为了标配, 今天在这里主要介绍一下个人开源的iOS弹幕, 以及提前为&lt;code&gt;实现一款弹幕库涉及的相关技术分享&lt;/code&gt;的相关篇章占坑, 虽不细至于手把手教如何实现, 但关键点都会有所涉及且不仅限于实现弹幕, 如iOS中用pthread实现生产者消费者模型、响应正在执行动画对象的点击事件、实现某类对象复用的ReuseQueue、使用GCD封装实现可取消未执行代码块的OperationQueue等等, &lt;em&gt;对这些更感兴趣的朋友麻烦直接滑至最后一段.&lt;/em&gt;&lt;/p&gt;
    
    </summary>
    
      <category term="开源" scheme="https://shawnfoo.github.io/categories/%E5%BC%80%E6%BA%90/"/>
    
      <category term="iOS" scheme="https://shawnfoo.github.io/categories/%E5%BC%80%E6%BA%90/iOS/"/>
    
      <category term="2017" scheme="https://shawnfoo.github.io/categories/%E5%BC%80%E6%BA%90/iOS/2017/"/>
    
    
      <category term="弹幕" scheme="https://shawnfoo.github.io/tags/%E5%BC%B9%E5%B9%95/"/>
    
  </entry>
  
  <entry>
    <title>Weak-Strong Dance解析</title>
    <link href="https://shawnfoo.github.io/2016/04/26/Weak-Strong-Dance%E8%A7%A3%E6%9E%90/"/>
    <id>https://shawnfoo.github.io/2016/04/26/Weak-Strong-Dance解析/</id>
    <published>2016-04-25T16:00:00.000Z</published>
    <updated>2021-05-13T06:46:03.452Z</updated>
    
    <content type="html"><![CDATA[<p>我们在使用Block时常常看到<code>Weak-Strong Dance</code>的用法, 很多的文章以及<a href="https://developer.apple.com/library/mac/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html#//apple_ref/doc/uid/TP40011226-CH1-SW4" target="_blank" rel="noopener">官方文档</a>都举例了这样做的原因. 但是还尚未发现有对strong进行讲解的. 下面就举个栗子具体分析下<strong>为什么加strong</strong>以及<strong>何时起作用</strong></p><p>首先放上两个类似ReactiveCocoa中定义weakify和strongify的宏, 以便下文用到</p><blockquote><p>#define WeakObj(o) autoreleasepool{} __weak typeof(o) weak##o = o<br>#define StrongObj(o) autoreleasepool{} __strong typeof(o) o = weak##o</p></blockquote><a id="more"></a><hr><h4 id="一、weak的作用-代码-注解-简单跳过"><a href="#一、weak的作用-代码-注解-简单跳过" class="headerlink" title="一、weak的作用(代码+注解 简单跳过)"></a>一、weak的作用(代码+注解 简单跳过)</h4><p>防止被block捕获(会导致引用计数加1), 打破循环引用(retain cycle)</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// DeallocMonitor继承NSObject, 仅重写其dealloc方法, 并在其中打印其被释放日志</span></span><br><span class="line">DeallocMonitor *object1 = [DeallocMonitor new];</span><br><span class="line">DeallocMonitor *object2 = [DeallocMonitor new];</span><br><span class="line">@WeakObj(object2);<span class="comment">//__weak typeof(object2) weakobject2 = object2;</span></span><br><span class="line">dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<span class="number">5</span> * <span class="built_in">NSEC_PER_SEC</span>)), dispatch_get_global_queue(<span class="number">0</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line">    <span class="comment">// object1被block捕获, 引用计数加1, 外部作用域结束时仍未被释放, 直至该Block执行完毕才被释放</span></span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"5s已到, %@该被释放勒"</span>, object1);</span><br><span class="line">    <span class="comment">// weakobject2被weak 修饰, 其指向的object2对象的引用计数不会增加, 当外部作用域结束时就已被释放</span></span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"5s已到, %@早已被释放, 此处为null"</span>, weakobject2);</span><br><span class="line">&#125;);</span><br><span class="line"><span class="comment">// 外部作用域结束</span></span><br></pre></td></tr></table></figure><hr><h4 id="二、为何要加strong-其何时才起作用？"><a href="#二、为何要加strong-其何时才起作用？" class="headerlink" title="二、为何要加strong, 其何时才起作用？"></a>二、为何要加strong, 其何时才起作用？</h4><p>加strong的原因想必大家都知道是为了<strong>防止block执行过程中 <code>__weak typeof(object) weakObject</code>指向的对象突然被释放了</strong>, 这就会导致block中的代码运行结果出现意想不到的结果(比如一些代码执行有效, 其余代码执行无效; 弱引用的对象因为为nil而导致的crash等.)</p><h5 id="2-1-即使加了strong-也不能保证weakObject指向的对象不会被释放"><a href="#2-1-即使加了strong-也不能保证weakObject指向的对象不会被释放" class="headerlink" title="2.1 即使加了strong, 也不能保证weakObject指向的对象不会被释放"></a>2.1 即使加了strong, 也不能保证weakObject指向的对象不会被释放</h5><p>只能确保在block执行期间, weakObject指向的对象有效(不会被释放)</p><p>下面这段代码就是在block中用strong申明的对象强引用一次weakObject, 但修饰对象在block执行前就已经被释放的栗子</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// DeallocMonitor继承NSObject, 仅重写其dealloc方法, 并在其中打印其被释放日志</span></span><br><span class="line">DeallocMonitor *object = [DeallocMonitor new];</span><br><span class="line">@WeakObj(object);<span class="comment">// weakobject</span></span><br><span class="line">dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<span class="number">5</span> * <span class="built_in">NSEC_PER_SEC</span>)), dispatch_get_global_queue(<span class="number">0</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line">    <span class="comment">// 该strongObj的申明仅在block执行时才见效, 而外部作用域一结束object就已经被释放了, 所以然并卵</span></span><br><span class="line">    @StrongObj(object);</span><br><span class="line">    <span class="comment">/* weakobject用 weak修饰, 故其引用计数不变, </span></span><br><span class="line"><span class="comment">上边的宏本意是申明一个新的object局域变量对weakobject指向的原object进行强引用..</span></span><br><span class="line"><span class="comment">按理 原object引用计数应该会加1, 可是它还没等到被强引用时就已经挂掉了</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"5s已到, %@然后早已被释放, 此处为null"</span>, object);</span><br><span class="line">&#125;);</span><br><span class="line"><span class="comment">// 外部作用域结束</span></span><br></pre></td></tr></table></figure><h5 id="2-2-Block内部申明的强引用指针变量指向weakObject仅在block执行时才有效"><a href="#2-2-Block内部申明的强引用指针变量指向weakObject仅在block执行时才有效" class="headerlink" title="2.2 Block内部申明的强引用指针变量指向weakObject仅在block执行时才有效"></a>2.2 Block内部申明的强引用指针变量指向weakObject仅在block执行时才有效</h5><p>定义该Block的时strongObj宏还尚未使原对象引用计数加1! 那么strongObj宏生效时的表现是什么样子的呢? 继续上代码</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 该段代码主要是打了一个时间差, 以模拟strong申明起作用的情形</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// DeallocMonitor继承NSObject, 仅重写其dealloc方法, 并在其中打印其被释放日志</span></span><br><span class="line">DeallocMonitor *object = [DeallocMonitor new];</span><br><span class="line"><span class="comment">// 保证外部作用域结束的2.5秒(无限接近..)内object不会被释放</span></span><br><span class="line">dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<span class="number">2.5</span> * <span class="built_in">NSEC_PER_SEC</span>)), dispatch_get_global_queue(<span class="number">0</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"果断强引用object: %@\n 还能再多坚持2.5s"</span>, object);</span><br><span class="line">&#125;);</span><br><span class="line">@WeakObj(object);<span class="comment">// weakobject</span></span><br><span class="line">dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<span class="number">2</span> * <span class="built_in">NSEC_PER_SEC</span>)), dispatch_get_global_queue(<span class="number">0</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line">    @StrongObj(object);<span class="comment">// __strong typeof(object) object = weakobject</span></span><br><span class="line">    sleep(<span class="number">3</span>);<span class="comment">// 卡个3s</span></span><br><span class="line">    <span class="comment">// 此处就不会像上一段代码那样, 强引用一个为nil的object, 故weakobject指向的对象引用计数加1, 直到该block运行完, 才会被释放</span></span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"5s已到, %@打印完这个日志就飞升了"</span>, object);</span><br><span class="line">&#125;);</span><br><span class="line"><span class="comment">// 外部作用域结束</span></span><br></pre></td></tr></table></figure><h5 id="2-3-有多少个嵌套block就应该申明多少对weak-strong"><a href="#2-3-有多少个嵌套block就应该申明多少对weak-strong" class="headerlink" title="2.3 有多少个嵌套block就应该申明多少对weak-strong"></a>2.3 有多少个嵌套block就应该申明多少对weak-strong</h5><p>假定我们在最外层block使用的一对weak-strong, 且外层block内还有一个block(没有用weak-strong)引用到了strongObj宏申明的局域变量object, 并假设原对象在外层block开始运行前一直存活, 这就会导致内层block捕获到局域变量object并使其指向对象的引用计数加1, 因为内层block捕获到了外层block中申明的object(强引用), 就跟外层block会捕获到外部强引用变量指向的对象一样一样的</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">DeallocMonitor *object = [DeallocMonitor new];</span><br><span class="line">@WeakObj(object);</span><br><span class="line">dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<span class="number">2</span> * <span class="built_in">NSEC_PER_SEC</span>)), dispatch_get_global_queue(<span class="number">0</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line">    @StrongObj(object);<span class="comment">// 因为block运行时, weakObject指向对象依旧存在, 故该强引用使其引用计数加1</span></span><br><span class="line">    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<span class="number">2</span> * <span class="built_in">NSEC_PER_SEC</span>)), dispatch_get_global_queue(<span class="number">0</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line">        <span class="comment">// 这一层block 发现上边的object是强引用, 导致捕获到其指向对象, 使其引用计数在该内层block尚未执行时就加1了</span></span><br><span class="line">        <span class="built_in">NSLog</span>(<span class="string">@"打印完这个日志, %@才被释放"</span>, object);</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"%@外层block结束, 引用计数减一"</span>, object);</span><br><span class="line">&#125;);</span><br><span class="line">sleep(<span class="number">3</span>);</span><br><span class="line"><span class="comment">// 外部作用域所在线程小歇一会, 确保object存活3s, 作用域结束</span></span><br></pre></td></tr></table></figure><p>所以嵌套block时 万万要小心, 不要漏写了. 另外weak-strong要成对出现, 不然少一个strong, 都有可能为此付出代价</p><h5 id="2-4-遗漏补缺"><a href="#2-4-遗漏补缺" class="headerlink" title="2.4 遗漏补缺"></a>2.4 遗漏补缺</h5><ol><li>在block中对外部weakObject进行强引用(strong修饰)的结果是使weakObject指向的原对象的引用计数加1, 因为weakObject指针指向的是原对象在堆中的存储地址</li><li>block 不会对弱引用指针变量指向的对象进行捕获</li></ol><h5 id="2-5-block的相关知识-个人推荐书籍章节"><a href="#2-5-block的相关知识-个人推荐书籍章节" class="headerlink" title="2.5 block的相关知识, 个人推荐书籍章节"></a>2.5 block的相关知识, 个人推荐书籍章节</h5><ul><li>Effective-ObjectiveC(Item 37: Understand Blocks)</li><li>Pro Multithreading and Memory Management for iOS and OS X(Blocks Implementation)</li></ul><h4 id="三、题外篇-内存泄露检测工具-妈妈再也不用担心内存泄露"><a href="#三、题外篇-内存泄露检测工具-妈妈再也不用担心内存泄露" class="headerlink" title="三、题外篇(内存泄露检测工具-妈妈再也不用担心内存泄露)"></a>三、题外篇(内存泄露检测工具-妈妈再也不用担心内存泄露)</h4><p>对于ReactiveCocoa以及各种嵌套Block的常用玩家..想必仅靠Xcode的Instrument去检测memory leak问题是绝对不够的,  个人卖瓜推荐一个检测内存泄露的小工具类:<br><a href="https://github.com/ShawnFoo/FXCustomTabBarController/blob/master/FXCustomTabBarController/FXDeallocMonitor.m" target="_blank" rel="noopener">FXDeallocMonitor</a><br>拷贝FXDeallocMonitor.h、FXDeallocMonitor.m文件到项目中, 根据头文件中的方法调用就行, 简单易用😄</p><p>对于需求更高者, 推荐近期facebook开源的<a href="https://github.com/facebook/FBAllocationTracker" target="_blank" rel="noopener">FBAllocationTracker</a></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;我们在使用Block时常常看到&lt;code&gt;Weak-Strong Dance&lt;/code&gt;的用法, 很多的文章以及&lt;a href=&quot;https://developer.apple.com/library/mac/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html#//apple_ref/doc/uid/TP40011226-CH1-SW4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;官方文档&lt;/a&gt;都举例了这样做的原因. 但是还尚未发现有对strong进行讲解的. 下面就举个栗子具体分析下&lt;strong&gt;为什么加strong&lt;/strong&gt;以及&lt;strong&gt;何时起作用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先放上两个类似ReactiveCocoa中定义weakify和strongify的宏, 以便下文用到&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;#define WeakObj(o) autoreleasepool{} __weak typeof(o) weak##o = o&lt;br&gt;#define StrongObj(o) autoreleasepool{} __strong typeof(o) o = weak##o&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="小结" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/"/>
    
      <category term="iOS" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/iOS/"/>
    
      <category term="2016" scheme="https://shawnfoo.github.io/categories/%E5%B0%8F%E7%BB%93/iOS/2016/"/>
    
    
      <category term="block" scheme="https://shawnfoo.github.io/tags/block/"/>
    
      <category term="修饰符" scheme="https://shawnfoo.github.io/tags/%E4%BF%AE%E9%A5%B0%E7%AC%A6/"/>
    
  </entry>
  
  <entry>
    <title>旧博客园博客地址</title>
    <link href="https://shawnfoo.github.io/2014/12/22/%E6%97%A7%E5%8D%9A%E5%AE%A2%E5%9B%AD%E5%8D%9A%E5%AE%A2%E5%9C%B0%E5%9D%80/"/>
    <id>https://shawnfoo.github.io/2014/12/22/旧博客园博客地址/</id>
    <published>2014-12-21T16:00:00.000Z</published>
    <updated>2021-05-13T06:46:03.451Z</updated>
    
    <content type="html"><![CDATA[<p>贴下14年末创建的博客园博客地址, 现在看回去感觉好水, 这也是一种进步的体现吧, 不做迁移了. </p><p><a href="http://www.cnblogs.com/fu4904/" target="_blank" rel="noopener">http://www.cnblogs.com/fu4904/</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;贴下14年末创建的博客园博客地址, 现在看回去感觉好水, 这也是一种进步的体现吧, 不做迁移了. &lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.cnblogs.com/fu4904/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://w
      
    
    </summary>
    
      <category term="记录" scheme="https://shawnfoo.github.io/categories/%E8%AE%B0%E5%BD%95/"/>
    
      <category term="2014" scheme="https://shawnfoo.github.io/categories/%E8%AE%B0%E5%BD%95/2014/"/>
    
    
      <category term="入门" scheme="https://shawnfoo.github.io/tags/%E5%85%A5%E9%97%A8/"/>
    
  </entry>
  
</feed>
