<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Ming&#39;s pages</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://zesming.com/"/>
  <updated>2019-02-24T04:26:13.087Z</updated>
  <id>https://zesming.com/</id>
  
  <author>
    <name>zesming</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>了解一下 Mesh 网络</title>
    <link href="https://zesming.com/learn-mesh/"/>
    <id>https://zesming.com/learn-mesh/</id>
    <published>2019-02-23T09:20:22.000Z</published>
    <updated>2019-02-24T04:26:13.087Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一切还要从上一篇文章说起"><a href="#一切还要从上一篇文章说起" class="headerlink" title="一切还要从上一篇文章说起"></a>一切还要从上一篇文章说起</h2><p>在去年的初夏，我写了一篇博文，分享了一下我家的网络搭建方案。说来惭愧，本来这篇文章应该是在上篇博文之后不久就写出来的，结果因为各种事儿（<del>懒</del>）一直拖到了现在。</p><p>那为什么要写 Mesh 呢？上次文章在朋友圈中嘚瑟后，没多久被某大神点评了下，并建议了解下 Mesh。</p><a id="more"></a><p><img src="http://images-13569712.mtmss.com/posts/learn_mesh/learn_mesh_1.jpg" alt="1"></p><p>还别说，这一块之前真的没有怎么了解过，Google 一番后发现这个其实就是<a href="https://zh.wikipedia.org/wiki/%E7%BD%91%E7%8A%B6%E7%BD%91%E7%BB%9C" target="_blank" rel="noopener">网状网络</a>，有着悠久的历史，并且应该是在某本教科书上还出现过它的身影。</p><p><img src="http://images-13569712.mtmss.com/posts/learn_mesh/learn_mesh_2.jpg" alt="2"></p><p>其中 <code>(e)网状拓扑结构</code> 应该说的就是它。</p><p>然后我竟无意间发现，家中的主路由器——华硕 AC88u，竟然支持 Mesh 的网络搭建，华硕官方称之为 <a href="https://www.asus.com/microsite/AiMesh/en/index.html" target="_blank" rel="noopener">AiMesh</a>。既然这东西就在身边，那我就顺藤摸瓜看看这究竟是个什么高科技。</p><h2 id="先了解下-Mesh-是什么"><a href="#先了解下-Mesh-是什么" class="headerlink" title="先了解下 Mesh 是什么"></a>先了解下 Mesh 是什么</h2><p>定义直接从维基百科上抠出来吧。</p><blockquote><p><strong>网状网络</strong>（<a href="https://zh.wikipedia.org/wiki/%E8%8B%B1%E6%96%87" target="_blank" rel="noopener">英文</a>：Mesh Network）是一种在<a href="https://zh.wikipedia.org/w/index.php?title=%E7%B6%B2%E8%B7%AF%E7%AF%80%E9%BB%9E&amp;action=edit&amp;redlink=1" target="_blank" rel="noopener">网络节点</a>间透过动态<a href="https://zh.wikipedia.org/wiki/%E8%B7%AF%E7%94%B1" target="_blank" rel="noopener">路由</a>的方式来进行资料与控制指令的传送。这种网络可以保持每个节点间的连线完整，当网络拓扑中有某节点失效或无法服务时，这种架构允许使用“跳跃”的方式形成新的路由后将讯息送达传输目的地。</p></blockquote><p>简单点来说，就是一种拓扑结构，而现在常见的无线 Mesh 具有<strong>高性价比</strong>（因为节点间可以通过无线互连，减少了对有线的依赖），<strong>可拓展性强</strong>（添加设备简单配置即可），<strong>部署快捷</strong>（同样是因为无线互连，比较方便），<strong>应用场景广</strong>（除了常见的人员密集场所，甚至可以用到轨道交通中），和<strong>高可靠性</strong>（Mesh 中各 AP 之间是全连接，有一个挂掉了可以自动连接到别的 AP）的优点。</p><p>下图是一个比较简单的 Mesh 网络。</p><p><img src="http://images-13569712.mtmss.com/posts/learn_mesh/learn_mesh_3.jpg" alt="3"></p><p>这个网络中有多个 Mesh AP，负责接入服务还有 Mesh 服务，中间还有个负责 Mesh AP 和 AC 之间通信的 Mesh Portal Point，如果 Mesh AP 多的话，可能还需要加一个负责补充 Mesh AP 的 Mesh Point，它只负责和 Mesh AP 之间通信，不直接和客户端通信。</p><h3 id="自我修复"><a href="#自我修复" class="headerlink" title="自我修复"></a>自我修复</h3><p>上述提到的众多优点中，高可靠性是 Mesh 网络中比较重要的一个特性。</p><p>我们现在常见的网络拓扑结构中，大多数在出现单点故障时，都会影响网络的稳定性，例如树状拓扑结构：</p><p><img src="http://images-13569712.mtmss.com/posts/learn_mesh/learn_mesh_4.jpg" alt="4"></p><p>上图中左侧的路由器故障使得下游的 AP 断连，从而导致客户端设备无法正常访问网络了。那么同样的问题对于 Mesh 网络是怎么处理的呢？我们还拿上面的 Mesh 网络拓扑图来展示：</p><p><img src="http://images-13569712.mtmss.com/posts/learn_mesh/learn_mesh_5.jpg" alt="5"></p><p>我们假设中间的 MPP 设备出现了故障，那么这个网络依然能够保持可用性。其实，如果在显示网络环境中，出现了比图示更严重的故障，导致后方的设备也掉线了，那么基于 Mesh Wi-Fi的自我修复，则可预防网络环境瘫痪的情境发生。其实就是运用 Mesh Wi-Fi 网状网络中每个接入点都能相互联机、沟通的特性。当网络中的某个节点故障时，Mesh Wi-Fi网络会绕过故障节点，让功能正常的节点相互连结、运作，并且同时自动寻找封包传输时的最佳路径，保持网络内的联机沟通都能正常运作。</p><h3 id="无线漫游"><a href="#无线漫游" class="headerlink" title="无线漫游"></a>无线漫游</h3><p>Mesh 网络有了自我修复的特性，能够保证网络的高可靠性，除此之外，我其实更关心的是它的无线漫游特性。</p><p>上篇博文我提到了我家因为有个非常长的走廊，导致需要两个无线路由器才可以勉强覆盖到生活和休息区。两个无线 AP 就会导致设备如果从一个屋子移动到另一个屋的时候，会有一次信号源的切换，然后就会断一下网，通常浏览网页，聊天等不需要一直保持连接的操作，不会有特别大的感受，但是当在屋子里边走边语音，视频，玩游戏的时候，就会感受特别明显，肯定要断线重连。</p><p><img src="https://images-13569712.mtmss.com/posts/home_network_setup/2.JPG" alt="6"></p><p>这就很不爽啊，那么无线漫游正好能解决这样的问题。</p><p>所谓无线漫游，是指设备信号从一个 AP 切换到另一个 AP 的时候所需的时间控制在毫秒级，且能通过 AC 保证无需重连。</p><p>一般在设备切换信号源之前，通过 802.11k – Neighbor Report 标准，收集邻近的 AP 信息列表，然后信号弱的时候可以自动挑选 AP 切换过去，同时还需要通过 802.11v – Wireless network management 标准，保证挑选到的 AP 是信号良好的，另外，在切换之前，设备也会利用 802.11r – Fast Transition Roaming 标准，使用一种名为“快速基本服务设置转换 (FT)”的功能更快地进行鉴定。FT 适用于预共享密钥 (PSK) 和 802.1X 鉴定方法。利用上述三个标准，可以保证在进行信号切换的时候，基本无痛无感。</p><p>苹果大大有一篇 <a href="https://support.apple.com/zh-cn/HT202628" target="_blank" rel="noopener">《在 iOS 上通过 802.11k、802.11r 和 802.11v 实现 Wi-Fi 网络漫游》</a>，详细介绍了 iOS 设备在这三个标准下的工作原理，感兴趣的同学可以看看。</p><h2 id="Mesh-改造"><a href="#Mesh-改造" class="headerlink" title="Mesh 改造"></a>Mesh 改造</h2><p>因为 Mesh 属于一个网络结构，并不是一个标准，所以想要组建 Mesh 网络还需要一整套同一厂商的设备，例如家用的华硕的 AiMesh，Linksys 的 Mesh 路由，以及各种大厂提供的一整套商用解决方案。</p><p>拿我们家的网络情况来举例子，下图是原来的网络拓扑图</p><p><img src="https://images-13569712.mtmss.com/posts/home_network_setup/3.png" alt="7"></p><p>还有我们家原来的路由器摆放位置</p><p><img src="http://images-13569712.mtmss.com/posts/learn_mesh/learn_mesh_8.jpg" alt="8"></p><p>因为次卧的设备接入的是有线，并没有设立 AP，所以从客厅到主卧的途中会有那么一点是“客厅不爱，主卧不管”的位置。而这个位置，正好是在次卧的飘窗附近，这里又正好是我的书桌。</p><p>房屋的结构肯定没法动了，AP 的位置也不可能有大的变化，因为弱电强电的接口已经留好了，那么我就需要在次卧加个 AP。</p><p>三个 AP 要是没有漫游那就可有的受了，既然主路由支持 AiMesh，那就全都换成华硕的路由器不就行了。于是我把次卧的斐讯 K3 换成了华硕 AC86u，负责主卧和阳台的无线覆盖，次卧新购一台华硕的 Blue Cave，不大，放在桌面上也挺好看，负责次卧书桌（因为靠近窗户，楼下以及路对面的公交车站也有无线信号）的无线覆盖。</p><p><img src="http://images-13569712.mtmss.com/posts/learn_mesh/learn_mesh_8.jpg" alt="9"></p><p>接下来就是调整 AP 的接入了，基于 Mesh 的网络拓扑图就成了如下图所示</p><p><img src="http://images-13569712.mtmss.com/posts/learn_mesh/learn_mesh_10.jpg" alt="10"></p><p>AC88u 作为主路由，接入外网，然后通过交换机和两个副路由进行 LAN to WAN 的有线连接，通过 AiMesh 实现有线回程，同时，这仨之间还可以通过 WiFi 实现无线回程，当有线出现问题的时候可以切换到无线保证稳定，尽管家用网络中这种情况基本不太可能发生。</p><p>因为华硕的 Mesh 是支持有线回程的，对于双频路由器来说，有线比无线更加稳定可靠，因为无线需要有一个频段一直保持通讯，通常优先 5Ghz，虽说带宽方面基本够用，但在极端数据传输的情况下，仍然会有影响。</p><p>具体的效果因为没有专门进行数据分析，就不贴了，实际感受就是抱着 iPad 满屋跑和老爹老妈打着 FaceTime 也没有掉线，并且没有出现过信号不佳的情况了。</p><h2 id="Mesh-更广泛的作用"><a href="#Mesh-更广泛的作用" class="headerlink" title="Mesh 更广泛的作用"></a>Mesh 更广泛的作用</h2><p>从上面提到的特性来看，Mesh 非常适合在覆盖面积大，网络质量要求高的环境下使用，那么常见的学校，医院等人口密集场所，甚至是港口，轨道交通等需要高可靠性网络的地方，都可以使用。</p><p>例如 H3C 所提供的地铁无线 Mesh 部署方案</p><blockquote><p>地铁是现代城市里不可缺少的交通工具。在一个地铁系统里，控制信息和多媒体信息需要实时的传递给快速移动的列车，从而有效的控制列车的运行，并且为乘客提供多种网络服务。</p><p>如下图所示，一个地铁无线 Mesh 网络里，车载 MP 和轨旁 MP 均采用 Fit MP，受 AC 集中管理，多个轨道 MP 沿轨道进行部署。车载 MP 不停扫描新的轨旁 MP，并选择信号质量最好的两个轨旁 MP 与之建立 Mesh 连接，主 Mesh 连接用于车载 MP 与轨旁有线网络之间的数据传输，从连接用于 Mesh 连接的切换备份。</p></blockquote><p><img src="http://images-13569712.mtmss.com/posts/learn_mesh/learn_mesh_11.png" alt="11"></p><p>还例如美军已经使用无线网状网络去连接在战场上的军用电脑，这可以让军队知道每一个军人的位置，这样就可以加强军队协同行动的能力。</p><p>那么在城市建设中，安装可以接入 Mesh 网络的电子水表或电表，利用这些水表或电表可以一个接着一个传送他们的读数到水电公司，如此就不需要额外抄表人员或是另外安装电缆线传送数据。</p><p>利用 Mesh 这种强大的拓展性，还可以脑洞更多应用场景，随着无线设备的功能提升，给用户带来的体验性上的提升也会越来越高。</p><h2 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h2><p>可能有人会说，搞这么多路由器或者专门买一套网络设备组建 Mesh 真的有必要么？</p><p>一个好的技术，或者说产品，是应该让你不用再去为之而操心，正所谓“真正的科技是让你感受不到科技的存在”，那么从这一点来讲，一些小的投入所带来的体验上的巨大提升就是值得的。</p><p>不过对于这些技术的选型其实都是因人而异的，那么从我家里的情况来看，目前这个还是挺适合我的，挑选一个适合自己的解决方案也是很重要的。</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;del&gt;懒&lt;/del&gt;）一直拖到了现在。&lt;/p&gt;
&lt;p&gt;那为什么要写 Mesh 呢？上次文章在朋友圈中嘚瑟后，没多久被某大神点评了下，并建议了解下 Mesh。&lt;/p&gt;
    
    </summary>
    
      <category term="Network" scheme="https://zesming.com/categories/Network/"/>
    
    
      <category term="家庭" scheme="https://zesming.com/tags/%E5%AE%B6%E5%BA%AD/"/>
    
      <category term="网络" scheme="https://zesming.com/tags/%E7%BD%91%E7%BB%9C/"/>
    
      <category term="无线漫游" scheme="https://zesming.com/tags/%E6%97%A0%E7%BA%BF%E6%BC%AB%E6%B8%B8/"/>
    
  </entry>
  
  <entry>
    <title>网线必须有——家用网络搭建心得</title>
    <link href="https://zesming.com/home-network-setup/"/>
    <id>https://zesming.com/home-network-setup/</id>
    <published>2018-05-12T12:01:26.000Z</published>
    <updated>2018-05-19T10:37:55.688Z</updated>
    
    <content type="html"><![CDATA[<h2 id="为什么要说这个"><a href="#为什么要说这个" class="headerlink" title="为什么要说这个"></a>为什么要说这个</h2><p>从上一次更新文章到现在将近一年的时间了，在此期间，我经历了人生中的一次大事——装修，其中最让我有成就感的，除了一整套智能家居的组合，就是家庭网络的搭建了，而后者，也是家里所有互联设备日后协同工作的基础。</p><p>提及到装修，就少不了要谈论到家里弱电的设计，很多人，包括很多程序员，甚至有部分网络工程师，都会觉得把这个交给家装设计师或者电工就行了，还有朋友觉得，现在无线设备这么先进，家里一根入户线接一个无线路由器就足矣，但事实上，在日后的生活中上网的极致体验可不是一个路由器就能满足得了的。</p><p>从长远考虑，我还是决定在这一块上亲自出马，除了一部分走线交给电工，其余都由我来。</p><a id="more"></a><h2 id="设计"><a href="#设计" class="headerlink" title="设计"></a>设计</h2><p>设计整体的网络布局，肯定要根据整个房屋格局来，先上一份家里的格局图：</p><p><img src="https://images-13569712.mtmss.com/posts/home_network_setup/1.JPG" alt="格局图"></p><p>找了好久只找到了一张渣质量的图了，凑合看吧。从上图可以看到，我们家是个狭长了格局，如果想要房子内哪里都有无线信号，面对这么奇葩的结构，就算是企业级无线路由器肯定也是有些吃力的。</p><p>再结合每个房间未来将要承载的功能和设备，大致有了以下的设计：</p><p><img src="https://images-13569712.mtmss.com/posts/home_network_setup/2.JPG" alt="设计图"></p><p>还是那张渣质量图。主要使用网络的点位都已经在图上标出来了，而这些地方是肯定要有有线端口的。</p><p>得益于 ZF 近年来的提速降费政策，帝都联通每年都会给用户的带宽翻一倍，并且最低资费的带宽已经 100M 了，去年双十一搞活动还出了几天的 1000M 资费，加上家中会有高速内网通信的需要（因为考虑要上 NAS），能达到千兆的六类线是必须的了，考虑过全屋上光纤，好处就是未来带宽升级空间大，并且光纤便宜，走线也方便不少。</p><p>不过为了支持光转电，家里的很多节点设备都需要换成专用的，而在目前以电为主的时代，这样搭建麻烦不少，其实最主要的还是家没多大点地儿，整体工程上全光性价比不太高。</p><h3 id="网线必须有"><a href="#网线必须有" class="headerlink" title="网线必须有"></a>网线必须有</h3><p>就像我一开始说的那样，很多朋友在新装修的时候都觉得弱电无所谓，随便走走就行了，反正现在无线路由器也这么发达了。但是作为一个不喜欢凑合的人（……上图凑合是因为真的找不到清楚的了🤦🏻‍♂️），是无法忍受无线的不稳定性。因为无线路由再怎么发达，无线信号也是会受到家里的墙，家具，管线，以及别人家的无线信号的影响的，2.4GHz 速度慢，5GHz 衰减快，总不能为了体验抱个设备蹲在 AP 旁吧？而每个房间放一个无线 AP 又不太现实，就算是麻烦点用电力猫每个屋放一个，无线信号干扰不说，能达到满速传输也不太现实，搭建和维护成本也相对较高。</p><p>所以我一开始的设计原则就是，主打有线，无线辅助，毕竟手机这些移动设备和智能家居的设备还是没法接网线的。</p><h3 id="网线服务节点"><a href="#网线服务节点" class="headerlink" title="网线服务节点"></a>网线服务节点</h3><p><code>① 弱电箱</code> 这个没什么办法，和电工在家里找了一圈，也就那个位置比较合适了，因为还可以被沙发挡一下，相对好看点，并且距离电视柜这个重要设备聚集地还近点。交换机本来想上16口的呢，后来一看其实也用不了那么多，就换成8口的，弱电箱尺寸也能小点。</p><p><code>② 电视柜</code> 在一开始的时候就是要把这里当成家里面的影音数据中心（听着很高大上），因为回家除了在睡觉和在书房 <del>写码</del>（玩游戏），就是在客厅活动了。加上家里小，没有专用的书房，我是肯定不能放 NAS 这样噪声狂魔，所以把这些东西放客厅是最佳选择，主路由放这里也能满足活动区的无线需要。</p><p><code>③ 书桌</code> 这里必须要有有线端口，并且两个设备都要配置，为啥？当玩网游稳定不丢包，下载传输数据打满千兆的时候，就知道稳定的网络环境是一个多么爽的事情。搬进来后还发现，这里被各种承重墙外墙包成了一个 WiFi 死角，真是庆幸当时拉了网线过来。</p><p><code>④ 主卧电视柜</code> 这里用一个之前撸的免费的斐讯 K3 连接网线当无线 AP，看结构图里面一堵一堵承重墙就能明白为啥不用无线中继了。它主要负责俩卧室的无线信号和主卧未来可能会有的电视，结果使用的时候发现这家伙属于信号强盗，但凡是连接上它的信号，就算是蹲在客厅 AC88U 这样一个企业路由的旁边，使用的信号仍然是它发出来的，不过基本不可用，没什么速度。后来上网搜索了下发现是这家伙的 <a href="https://zh.wikipedia.org/wiki/%E6%B3%A2%E6%9D%9F%E8%B5%8B%E5%BD%A2" target="_blank" rel="noopener">Beamforming</a> 的功能造成的，因为 K3 使用的是 Intel 的芯片，貌似在这个功能上是有 bug 的（Intel 在无线通信这块和 Broadcom 差的真不是一点半点）。在关了这个功能后，K3 就可以乖乖的只服务自己辖区内的设备了。</p><h3 id="网络拓扑图"><a href="#网络拓扑图" class="headerlink" title="网络拓扑图"></a>网络拓扑图</h3><p>先上图：</p><p><img src="https://images-13569712.mtmss.com/posts/home_network_setup/3.png" alt="网络拓扑图"></p><h3 id="客厅"><a href="#客厅" class="headerlink" title="客厅"></a>客厅</h3><p>首先，我把光猫改成桥接模式，通过路由器来拨号，这样做的目的是可以让路由器成为一级网络节点，从而直接拥有公网 IP，进行端口转发，远程访问等操作。所以，光猫从<code>① 弱电箱</code>里面把光转电后，走线到<code>② 电视柜</code>，然后连接主路由的 WAN，进行上网拨号，同时联通还送了我一个 IPTV 机顶盒（现在基本上都不会放在机顶了吧），而北京联通的 IPTV 是走的单独的协议，所以还必须从<code>① 弱电箱</code>中光猫特定的 LAN 口接出来一根线，走到<code>② 电视柜</code>，连接机顶盒，通过机顶盒来拨号看电视。</p><p>提到 IPTV 需要单独线路的事情，其实可以偷懒使用单线复用方案，也就是和主路由共用从光猫分出来的那根线，然后通过 VLAN 各种绑定，使机顶盒可以通过路由器来连接拨号看电视，具体的一些资料可以搜索下 <a href="http://koolshare.cn/portal.php" target="_blank" rel="noopener">KoolShare</a> 上的帖子来了解。</p><p>除了上述的链路连接，因为家里面其余房间的网线需要通过交换机交换，但交换机我是选择放在弱电箱里的，所以从<code>② 电视柜</code>到<code>① 弱电箱</code>还有一条线路，连接主路由 LAN 口和交换机，用于转接内网线路。</p><p>剩下的移动设备，智能设备都通过无线路由器的 WiFi 连接。</p><h3 id="主卧"><a href="#主卧" class="headerlink" title="主卧"></a>主卧</h3><p>主要就是一个无线路由器改造成的无线 AP，用于拓展距离客厅最远的主卧无线信号。定制家具的时候专门设计了一个能放得下 K3 的电视柜（K3 是竖着放的）。</p><h3 id="次卧"><a href="#次卧" class="headerlink" title="次卧"></a>次卧</h3><p>两个电脑都使用网线上网，MBP 设置网线为首选上网方式，WiFi 打开用于 Handoff 和 AirDrop 等操作，得亏 MBP 的无线网卡还算是不挑信号，在这个 WiFi 死角也勉强能连得上主卧 AP 的 5GHz，用于上述功能已经够了。</p><h2 id="设备购买和安装"><a href="#设备购买和安装" class="headerlink" title="设备购买和安装"></a>设备购买和安装</h2><p>上述设计完成后，保险起见，我咨询了一个当网络工程师的哥们，一是 Review 一下我的设计（Design Review 🙄），二来呢，让他给我后期的设备购买和安装出一些专业性的指导和建议。</p><h3 id="设备购买"><a href="#设备购买" class="headerlink" title="设备购买"></a>设备购买</h3><h4 id="路由器"><a href="#路由器" class="headerlink" title="路由器"></a>路由器</h4><p>华硕 AC88U，17年年初买的，华硕中比较不错的无线路由器了，配合 Merlin 固件能干好多事情，所以它就当主路由吧，主要负责拨号，DDNS，端口转发，梯子和部分服务。</p><p>斐讯 K3，免费撸的斐讯最高端的路由器，但是固件一直被批评功能欠佳，拓展性不好，有后门，刷个第三方的吧，又没有人家 Merlin 做得好，索性就别纠结了，安心当个 AP 算了。</p><h4 id="网线"><a href="#网线" class="headerlink" title="网线"></a>网线</h4><p>在 JD 上买的六类非屏蔽100米的整箱，这是在电工开完槽，走完线管后告诉我的长度，质量我觉得一些大牌子的都差不多，所以就看哪个顺眼随手挑的。</p><p>从墙上网线端口到设备之间的跳线，我选用的是成品六类，原因是六类跳线手工打造基本上很难合格，水晶头那块是个坎儿，成品跳线便宜质量还有保证，既省事又好看，何乐而不为呢？</p><h4 id="面板"><a href="#面板" class="headerlink" title="面板"></a>面板</h4><p>因为家里其他地方的面板是西门子的，所以这个我也直接选的<a href="https://item.jd.com/228640.html" target="_blank" rel="noopener">西门子</a>家的。西门子家的面板中是都含有六类模块的，但是我还是买了一堆单独的<a href="https://item.jd.com/5056217.html" target="_blank" rel="noopener">六类模块</a>，原因下面安装的时候我会提到。</p><h4 id="交换机"><a href="#交换机" class="headerlink" title="交换机"></a>交换机</h4><p>网工哥们给的购买建议是 <a href="https://item.jd.com/3660988.html" target="_blank" rel="noopener">TP-LINK</a> 的8口千兆交换机，尺寸也小，不占地儿，主要是便宜。</p><h4 id="联通赠送的光猫和机顶盒"><a href="#联通赠送的光猫和机顶盒" class="headerlink" title="联通赠送的光猫和机顶盒"></a>联通赠送的光猫和机顶盒</h4><h3 id="设备安装"><a href="#设备安装" class="headerlink" title="设备安装"></a>设备安装</h3><p>走线都交给了电工师傅们，接头面板这块，我选择自己来，因为就连师傅自己都承认，六类线他们搞不好。</p><p>这块可以简单介绍下，不同于百兆线，千兆线需要网线中四对八根线，也就是所有线用于通信，如果有一根没有连通，那么就会被网卡等设备降级识别为 100M 的，所以为了跑满千兆，安装这一块要求非常高。</p><p>在没有意识到这个问题之前，我还买了几个水晶头用于弱电箱里面的网线接头，结果在一顿忙活后，仍没有一根是能正确识别为千兆的，当然了，如果手法老道，一些高端网工是可以手工打制合格的六类水晶头的。后来了解到这个问题后，把弱电箱内所有的接头都换成模块，配合成品跳线连接光猫和交换机。</p><p><img src="https://images-13569712.mtmss.com/posts/home_network_setup/4.jpg" alt=""></p><p>这就是模块安装图，其实这个和面板里面那个是一样的，所以基本上卡好之后能保证八根线是都能连通的。</p><p><img src="https://images-13569712.mtmss.com/posts/home_network_setup/5.jpg" alt=""></p><p>然后配合成品跳线，完美达成千兆速率。</p><h2 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h2><p>用 macOS 的 Network Utility 测试下看看。</p><p><img src="https://images-13569712.mtmss.com/posts/home_network_setup/6.png" alt=""></p><p>继而用 Speedtest 跑下速度（500M 的资费套餐）。</p><p><img src="https://images-13569712.mtmss.com/posts/home_network_setup/7.png" alt=""></p><p>当然了，实际应用场景中，基本上是达不到这个下行峰值的，因为很多服务器就没有这么高的输出速率。</p><p>现在还没有采购 NAS，未来到位后，尝试用内网链路聚合看看能有什么效果。</p><p>大体来说，家里的网络还是比较符合我的预期，通过端口转发在外面控制家里的设备，访问搭建在树莓派上的一些服务，虽说受到 20M 的上行速率限制，但是也已经满足预期了。</p><p>路由器刷上 Merlin 固件后，覆盖全家的梯子，以及通过树莓派暴露给在外搭梯子时候使用的 KCPTUN 服务，日后再结合 NAS，基本上常用的操作都能满足了。</p><p>最后上一张我的<code>③ 书桌</code>的摆拍图：</p><p><img src="https://images-13569712.mtmss.com/posts/home_network_setup/IMG_3238.jpg" alt=""></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;/p&gt;
&lt;p&gt;从长远考虑，我还是决定在这一块上亲自出马，除了一部分走线交给电工，其余都由我来。&lt;/p&gt;
    
    </summary>
    
      <category term="Network" scheme="https://zesming.com/categories/Network/"/>
    
    
      <category term="家庭" scheme="https://zesming.com/tags/%E5%AE%B6%E5%BA%AD/"/>
    
      <category term="网络" scheme="https://zesming.com/tags/%E7%BD%91%E7%BB%9C/"/>
    
      <category term="树莓派" scheme="https://zesming.com/tags/%E6%A0%91%E8%8E%93%E6%B4%BE/"/>
    
      <category term="装修" scheme="https://zesming.com/tags/%E8%A3%85%E4%BF%AE/"/>
    
      <category term="六类" scheme="https://zesming.com/tags/%E5%85%AD%E7%B1%BB/"/>
    
  </entry>
  
  <entry>
    <title>Hyperloop，让发布简洁高效</title>
    <link href="https://zesming.com/hyperloop/"/>
    <id>https://zesming.com/hyperloop/</id>
    <published>2017-06-05T16:34:12.000Z</published>
    <updated>2018-05-13T11:42:43.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="Hyperloop-是什么？"><a href="#Hyperloop-是什么？" class="headerlink" title="Hyperloop 是什么？"></a>Hyperloop 是什么？</h3><p>Hyperloop 是服务于美团点评客户端的组件发版、持续集成、App 打包构建、资源调度等各个环节的发布调度系统。名称起源于美国 Elon Musk 构想的 <a href="https://hyperloop-one.com/" target="_blank" rel="noopener">Hyperloop 超级高铁</a>，象征着现代、简洁、高效。</p><p>Hyperloop 提供了一站式的平台，管理着美团点评 iOS 业务的超过 300 个组件和包括美团 iOS 客户端在内的4个App。接入 Hyperloop 系统后，开发同学可以通过 Hyperloop 来管理自己的项目，配置发版和打包所需要的步骤和检查项。开发完成时，用户只需要登录 Hyperloop 进行相关操作，Hyperloop 就会根据项目的配置去调用不同的步骤，上报每个步骤的状态，给出错误日志、状态通知等。</p><a id="more"></a><h3 id="为什么要有-Hyperloop？"><a href="#为什么要有-Hyperloop？" class="headerlink" title="为什么要有 Hyperloop？"></a>为什么要有 Hyperloop？</h3><p>说到发布，我们首先想到的就是持续集成和交付，而说到持续集成和交付，我们又会自然而然地想到 Jenkins。没错，我们之前的所有和发布工程相关的流程都是与 Jenkins 密切相关，其任务的调度、自动化的构建等功能深受我们的喜爱。</p><p>可是随着我们的业务爆发式的增长，加之对 Jenkins 的深入使用，一些问题逐渐暴露出来了：</p><h4 id="1-使用和维护成本加大"><a href="#1-使用和维护成本加大" class="headerlink" title="1. 使用和维护成本加大"></a>1. 使用和维护成本加大</h4><p>业务量的增多，导致参与到整个发布流程中的同学也越来越多，而因为 Jenkins 偏向于专业的配置和运维步骤，给我们普通的开发同学带来很多 Jenkins 的使用和维护上的问题，让我们发布工程的同学不得不花大量的时间来进行答疑和维护。</p><h4 id="2-Jenkins-Job-的数量增多"><a href="#2-Jenkins-Job-的数量增多" class="headerlink" title="2. Jenkins Job 的数量增多"></a>2. Jenkins Job 的数量增多</h4><p>Jenkins 中存在的 Job 数量也随着业务的扩张而变的十分庞大。</p><style>table th {    width: 100px;}</style><table><thead><tr><th style="text-align:center">一级目录数量</th><th style="text-align:center">总 Job 数量</th></tr></thead><tbody><tr><td style="text-align:center">74</td><td style="text-align:center">569</td></tr></tbody></table><p>为什么会有这么多的任务呢？因为我们发布流程会提供一些必要的功能，而每个业务方在使用这些功能的时候需要配置一些自己的业务参数，出于 Jenkins 的一些局限性，常规的做法就是复制一份示例 Job 然后改成自己的构建任务。</p><p><img src="/images/hyperloop-0.png" alt="Job 数量公式"></p><p>所以说这么多的业务方乘以这么多的业务，就造就了这么多的 Job……</p><p>这么多的 Job 带来了不小的问题，不管是新策略的推广，还是 Job 的维护都是 N 倍的工作量。</p><h4 id="3-构建数据难以统计"><a href="#3-构建数据难以统计" class="headerlink" title="3. 构建数据难以统计"></a>3. 构建数据难以统计</h4><p>这么多的 Job，结果基本上没有办法集中统计。此外，Jenkins 中数据分析和呈现能力偏弱，而数据对于我们现如今的开发是尤为重要的，不仅能够反应出我们的工作成果，还能及时反馈出我们工作中的问题。</p><p>有人会说，我们可以利用 Jenkins 插件来解决上述的问题。</p><p>的确，Jenkins 凭借着自己丰富的插件和较为成熟的社区，有着较高的拓展性，但是经过我们的调研和评估，发现由于整个系统的设计并不是一个集中式的管理系统，所以很多功能都局限于 Job 而非系统层面。</p><p>另外，由于对于数据的收集和存储能力偏弱，所以我们经常要考虑如何解决数据保存的问题。</p><p>如果能够完美的解决上述问题，从成本上来说，通过对 Jenkins 的二次开发的性价比相较于打造一个全新的系统就低了很多。</p><p>因此，我们认为一个拥有中央调度功能的系统可以为我们整个项目带来诸多便利，所以打造一个 Hyperloop 这样的系统的想法应运而生。</p><h3 id="如何去搭建-Hyperloop？"><a href="#如何去搭建-Hyperloop？" class="headerlink" title="如何去搭建 Hyperloop？"></a>如何去搭建 Hyperloop？</h3><h4 id="1-系统架构"><a href="#1-系统架构" class="headerlink" title="1. 系统架构"></a>1. 系统架构</h4><p>从技术角度划分，我们把 Hyperloop 划分为 Web 前端、后端和工具链。前端专注于良好的用户交互和清晰的数据展示，后端则负责数据收集分析、业务处理和任务调度，而工具链是负责具体的发版、集成等任务的执行。</p><p>后端接受来自前端用户触发的操作，反馈数据或者调用工具链执行相关任务，所有的任务数据流都流经后端，从而能够保证构建数据的完整性。</p><p><img src="/images/hyperloop-1.png" alt="前后工具链端架构图"></p><h4 id="2-业务模型"><a href="#2-业务模型" class="headerlink" title="2. 业务模型"></a>2. 业务模型</h4><h5 id="插件化检查方式"><a href="#插件化检查方式" class="headerlink" title="插件化检查方式"></a>插件化检查方式</h5><p>组件的发布和集成阶段都需要有准入，但实际上每个组件的准入步骤并不一定完全一致，并且随着业务的需要，准入步骤也会相应的调整。面对这样的需求，将准入步骤插件化则是一个比较灵活方式。</p><h5 id="能力"><a href="#能力" class="headerlink" title="能力"></a>能力</h5><p>工具链会先行开发准入步骤，这是一个可以接受参数的 fastlane 的 action。完成开发后，我们会在 Hyperloop 后台上线对应的能力。</p><p>组件在自己的设置界面可以看到 Hyperloop 中所有已上线的能力，有一些是必选的，则默认就已经选上了，其他非必选的能力，则有该组件配置权限的同学可以根据自己业务需要勾选。</p><p>有一些检查，例如 warning 数，是可以设置一个目标值的，在未来的某个时间点达到什么样的目标，之后每一次准入都会由后台动态计算本次需要达到的目标。</p><p>App 的打包集成准入则是通过打包模板的方式来配置准入步骤，相较于发版的简单方式，集成打包则需要有多种情况供用户选择。除了和组件一样，可以配置目标值，打包模板还可以灵活设置参数配置方式。</p><h5 id="策略"><a href="#策略" class="headerlink" title="策略"></a>策略</h5><p>将某个能力勾选之后，组件或者打包模板中就会生成对应的策略，不同于能力的是，策略中保存了组件或打包模板中业务方配置的参数。</p><p><img src="/images/hyperloop-6.png" alt="能力和策略关系图"></p><h5 id="步骤"><a href="#步骤" class="headerlink" title="步骤"></a>步骤</h5><p>每一次组件发版，每一次集成打包，都会生成一个版本，Hyperloop 后台会根据能力和策略生成本次执行时具体的步骤，和策略类似，不过步骤中参数则是更为具体的值。</p><p><img src="/images/hyperloop-7.png" alt="能力策略步骤关系图"></p><p>随着发版和集成任务被触发，工具链被调用后会请求 Hyperloop 后台，下发本次执行的步骤，工具链拿到后就会按照具体的步骤和参数来执行任务了。</p><p>这样插件化的模型设计，大大减少了工具链的开发成本，并且增加了整个准入步骤的灵活性，更加符合业务方自己的需求。</p><h4 id="3-功能实现"><a href="#3-功能实现" class="headerlink" title="3. 功能实现"></a>3. 功能实现</h4><h5 id="组件发版"><a href="#组件发版" class="headerlink" title="组件发版"></a>组件发版</h5><p>组件是支撑整个美团 iOS 客户端的基础，前面也提到了，现在有超过300个组件通过我们的这个系统来发布，而在整个开发周期中，又有超过700次的发布需求。而作为整个客户端的发布流程的起点，组件发版又有着尤为重要的作用，所以在整个系统搭建之初，组件发版是我们优先考虑的功能。</p><p>在第一阶段，我们实现了基本的组件发布能力，业务方已经可以通过 Hyperloop 来对自己的业务组件进行发版。</p><p><img src="/images/hyperloop-8.png" alt="组件发布基本流程图"></p><p>但是对于一个全新的发布调度系统，仅仅用于发版是远远不够的，既然我们可以做到数据的汇总分析，那么就可以通过在准入中添加一些特殊的能力，使项目中一些可以优化的指标得以实现，例如 warning 数的分析和限制。所以在完成了基本的发版准入后，我们又增加了一些可选的优化能力。</p><p><img src="/images/hyperloop-9.png" alt="组件发布可选能力图"></p><p>提到限制，可不仅仅是设置一个数值这么简单，Hyperloop 允许用户设置目标值和目标时间，动态地计算出每一次需要达到的数值，从而通过程序这种强制性的手段，来实现工程上面的优化。</p><p><img src="/images/hyperloop-4.png" alt="Warning 数限制折线图"></p><p>有了准入限制，只能说完成了这个功能的一大部分，数据的的展示有多种多样，既然我们拿到了组件发布时的全部数据，那就让它有一个很好的展示。所以第三阶段，我们又完善了整个组件发布的信息，以及添加了一些功能性的能力，例如 Changelog 的生成，以丰富整个发布过程，让用户能够更清楚地了解到这一次发布的状况。</p><p><img src="/images/hyperloop-10.png" alt="发版详情参数图"></p><p>如果发布失败了，我们会在出错的步骤中展示错误的原因和建议，以及出错的 log，方便大家调查出错原因。</p><h5 id="打包集成"><a href="#打包集成" class="headerlink" title="打包集成"></a>打包集成</h5><p>组件发布后，就去集成到美团 iOS 客户端的主工程里面，所以完成了发布功能后，我们就开始着手实现打包集成。</p><p>作为美团点评最大的 iOS 项目之一，美团 iOS 客户端不管是从业务量级，还是整个打包集成发布的流程，都是非常复杂的，参与 RD 人数也很多。当我们对全公司 iOS 项目分析后发现，如果实现了美团 iOS 客户端的打包集成，那么别的独立 App 也都能够适用。</p><p><img src="/images/hyperloop-11.png" alt="美团 iOS 客户端的打包类型图"></p><p>在这个功能的第一阶段，我们简单的适配了之前的集成流程，为了让整个发版集成流程尽早可以完整的连起来。满足了基本可用的条件后，我们参考组件发布的步调，开始完善整个集成的流程。第一步，就是增加准入限制。</p><p>如果说组件发版是整个美团 iOS 客户端搭建的第一步，那么集成就是第二步，也是至关重要的一步。作为集成来讲，准入尤为重要，可以说客户端的各项性能指标是否符合要求，基本上就看集成准入是否能够过滤掉不合格的组件。</p><p>所以我们会在基本的集成能力中加入例如包大小检查这样的增强型准入检查，而在集成包构建完成之后，会进行自动化测试，保证本次集成除了没有编译错误，还不会有运行时问题。</p><p><img src="/images/hyperloop-12.png" alt="包大小检查图"></p><p>当然了，作为二进制集成，我们还需要保证我们的组件间 APIs 调用正确，所以我们还会进行全源码编译，来确保没有调用不一致的问题。</p><p>同样，Hyperloop 在集成时所收集的信息，都会展示出来。不同于组件发布，集成后有诸多产物，各位 RD 可能会有下载需求，我们还会把所有的产物都打包上传到美团云上，提供下载链接。</p><p><img src="/images/hyperloop-13.png" alt="集成详情图"></p><p>我们虽说有内部的分发平台，但是高频次的集成带来的集成包数量也是非常多的，在集成详情界面提供可以扫一扫就能下载的二维码，方便 RD 和 QA 下载下来验证自己的需求。</p><p>除了集成，我们还有个很重要的功能就是打包。作为一些小一点的业务方，自己并没有什么集成的需求，而更多的是希望能够按照自己的需求来打一个包出来。例如用于验证功能的 feature test，用户每日构建的 daily build，或者说用于提交 iTC（iTunes Connect） 的 App Store build。</p><p>之前的做法是，业务方自己创建 Jenkins job，自己配置构建脚本，虽说业务方之间任务独立，但是基本上很大程度都是重复的。</p><p>Hyperloop 为各业务方提供通用的打包能力，业务方可以根据需求组成自己的打包模板，每一次打包的时候只需要根据自己的需求选择相应的打包模板即可。</p><p><img src="/images/hyperloop-5.png" alt="打包模板图"></p><p>虽说能力都有参数，但是在设置打包模板的时候可以固定配置一些参数，简化每一次的操作步骤，甚至像 App Store build 就可以做到完全零配置，点一下打包即可完成构建触发。</p><p>同样的，完成打包后，相应的界面也能看到本次构建的详细信息。</p><h5 id="去-Jenkins-化"><a href="#去-Jenkins-化" class="headerlink" title="去 Jenkins 化"></a>去 Jenkins 化</h5><p>从一开始我们就提到 Jenkins 对于我们普通的 iOS 开发者而言，不管是使用成本还是维护成本都是比较高的，而其他方面诸多限制致使我们想要搭建一个这样的系统。</p><p>虽说是因为要去 Jenkins，但是我们在一开始也只是降低了对 Jenkins 的依赖，但还是利用 Jenkins 来分配执行任务。</p><p><img src="/images/hyperloop-14.png" alt="Jenkins job 图"></p><p>而仔细审视一下，我们不难发现，我们对 Jenkins 的使用也仅仅是分配任务了，为了这一个简单的需求我们还要保留 Jenkins，维护两套系统，并且通过系统间 APIs 来通信，非常恶心。</p><p>比较巧合的是，后台和工具链都是 Ruby 栈的，这样来看，工具链完全可以成为后台模块中的一部分，通过消息队列就能达到我们想要的效果，并且直接对数据库的操作，统一的功能开发，也让整套逻辑变得十分优雅。</p><p><img src="/images/hyperloop-15.png" alt="新版工具链后台 slave 关系图"></p><p>这样来看，Jenkins 就彻底的从我们的视线中消失了。</p><h5 id="通知能力"><a href="#通知能力" class="headerlink" title="通知能力"></a>通知能力</h5><p>如何才能让用户知道整个流程的结果？如何才能更好地向用户反馈出现的问题？除了前端界面展示详细数据外，主动通知能力也是个非常重要的功能，毕竟大家不可能一直守在 Hyperloop 面前。</p><p>我们提供有内部 IM 工具（大象）的消息通知，重要的问题我们还有邮件通知。</p><p><img src="/images/hyperloop-16.png" alt="大象通知图"></p><p><img src="/images/hyperloop-17.png" alt="邮件通知图"></p><p>如果出现问题，用户会第一时间收到来自 Hyperloop 的消息，当然了，如果顺利结束，系统也会恭喜你顺利完成了此次构建。</p><p>除了常规的构建通知，一些重要事项的提醒我们也是有能力告知的，例如开发者证书是否要过期？ Provisioning Profile 是否要更新？Hyperloop 有什么需要通知大家的新闻等等……</p><p><img src="/images/hyperloop-18.png" alt="提醒设置图"></p><h3 id="能否解决我们的问题？"><a href="#能否解决我们的问题？" class="headerlink" title="能否解决我们的问题？"></a>能否解决我们的问题？</h3><p>说了这么多，Hyperloop 是否能够解决我们之前所遇到的问题呢？</p><h4 id="1-通过新版工具链和系统后台解决-Jenkins-所带来的问题"><a href="#1-通过新版工具链和系统后台解决-Jenkins-所带来的问题" class="headerlink" title="1. 通过新版工具链和系统后台解决 Jenkins 所带来的问题"></a>1. 通过新版工具链和系统后台解决 Jenkins 所带来的问题</h4><p>我们通过新的系统来取代 Jenkins，人性化的界面和交互流程，清晰的信息展示和问题反馈，基本上让用户能很轻松地完成自己的发布流程，大大降低了他们的学习操作成本。</p><h4 id="2-中央集中式系统取代分散式的管理方式"><a href="#2-中央集中式系统取代分散式的管理方式" class="headerlink" title="2. 中央集中式系统取代分散式的管理方式"></a>2. 中央集中式系统取代分散式的管理方式</h4><p>Hyperloop 是一个中央集中式的系统，所有的业务方可以根据自己的业务来使用我们提供的能力，减少了各自为政所带来的冗余，重复的任务配置，也方便我们流程维护和技术升级。不管是运维成本还是技术推动成本都极大地降低。</p><h4 id="3-着重凸显数据的重要性"><a href="#3-着重凸显数据的重要性" class="headerlink" title="3. 着重凸显数据的重要性"></a>3. 着重凸显数据的重要性</h4><p>不同于之前的流程，Hyperloop 有着强大的数据汇总和分析能力，由于对于整个流程的参与，所有的信息都存储在 Hyperloop 中，方便分析和展示。作为日常工作的重要指标，数据扮演着尤为重要的角色。</p><p>有了数据，我们就能从数据中发现问题，解决问题，从而优化我们的项目，也能从方向性上提升我们的工作效率。</p><h3 id="展望未来"><a href="#展望未来" class="headerlink" title="展望未来"></a>展望未来</h3><p>Hyperloop 虽说已经能够处理解决大多数发布流程相关的任务，但是未来依然任重而道远。</p><h4 id="1-监控统计"><a href="#1-监控统计" class="headerlink" title="1. 监控统计"></a>1. 监控统计</h4><p>虽说我们能够汇总和分析数据，但是很多方面的监控和统计力度仍然不够，例如静态分析问题，重复代码检查等等。只有不断丰富整个工程中的监控指标，才能逐渐暴露出隐藏在项目中的一些问题，解决并且优化整个项目。</p><h4 id="2-上下游关系"><a href="#2-上下游关系" class="headerlink" title="2. 上下游关系"></a>2. 上下游关系</h4><p>目前整个 Hyperloop 上下游中已经充斥着 Git 托管平台，美团云，iTC 以及 IM 等诸多系统，可是作为整个开发流程来讲，我们可能还需要和更多系统之间的通信，例如在构建完成后可以自动管理相应的任务，可以在一个阶段完成后自动产生一个统计报表输出到 wiki 中等等。通过上下游多系统联动，从而进一步提升我们的开发效率。</p><h4 id="3-多端支持"><a href="#3-多端支持" class="headerlink" title="3. 多端支持"></a>3. 多端支持</h4><p>Hyperloop 目前仅仅是支持 iOS 业务，但是整个发布工程可是不限于 iOS 的，所以我们未来希望能够让所有还在发布工程中挣扎的同学们都能搭乘上 Hyperloop，让开发专注于开发的本质，发布仅仅是点一个键的事情。</p><h4 id="4-流程优化"><a href="#4-流程优化" class="headerlink" title="4. 流程优化"></a>4. 流程优化</h4><p>上面也提到过，我们一些功能是延承自之前的流程，而作为一个全新的系统，我们完全可以用更高效的流程来提升构建效率，运用政治经济学中生产力和生产关系的理论，我们有了更强大的生产力，那也需要有与之相适应的生产关系才能保证和进一步提高生产力。流程，正是如此。</p><p>我们做了这么多，其实本质就是让 RD 专注于开发，减少无谓的时间消耗，而发布的事情就让系统化的东西来解决。同时又能通过集中式的系统来保证整个流程的管控力度和数据统计，从另一个方面反推我们的开发。这就是 Hyperloop 名字的意义，不仅仅是个 loop，还是个现代、简洁、高效的 loop。</p><h3 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h3><p>这篇文章最初是我发表在美团点评技术团队公众号上的（微信号：meituantech），大家可以扫一扫关注下，里面会经常发表一些美团点评技术团队的最新成果。</p><p><img src="/images/hyperloop-19.JPG" alt="meituantech"></p><p>加上今年在 @Swift2017 大会上分享的东西，美团 iOS 客户端整体的持续集成交付流程基本上已经给大家介绍完了。还是那句话，一个好的流程是要根据业务的发展进行迭代的，所以这里只是根据我们的业务介绍一些在发布流程上的思考和经验，仅供参考，如果有好的想法也欢迎和我讨论。</p>]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;Hyperloop-是什么？&quot;&gt;&lt;a href=&quot;#Hyperloop-是什么？&quot; class=&quot;headerlink&quot; title=&quot;Hyperloop 是什么？&quot;&gt;&lt;/a&gt;Hyperloop 是什么？&lt;/h3&gt;&lt;p&gt;Hyperloop 是服务于美团点评客户端的组件发版、持续集成、App 打包构建、资源调度等各个环节的发布调度系统。名称起源于美国 Elon Musk 构想的 &lt;a href=&quot;https://hyperloop-one.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hyperloop 超级高铁&lt;/a&gt;，象征着现代、简洁、高效。&lt;/p&gt;
&lt;p&gt;Hyperloop 提供了一站式的平台，管理着美团点评 iOS 业务的超过 300 个组件和包括美团 iOS 客户端在内的4个App。接入 Hyperloop 系统后，开发同学可以通过 Hyperloop 来管理自己的项目，配置发版和打包所需要的步骤和检查项。开发完成时，用户只需要登录 Hyperloop 进行相关操作，Hyperloop 就会根据项目的配置去调用不同的步骤，上报每个步骤的状态，给出错误日志、状态通知等。&lt;/p&gt;
    
    </summary>
    
      <category term="iOS" scheme="https://zesming.com/categories/iOS/"/>
    
    
      <category term="CI" scheme="https://zesming.com/tags/CI/"/>
    
      <category term="美团" scheme="https://zesming.com/tags/%E7%BE%8E%E5%9B%A2/"/>
    
      <category term="Hyperloop" scheme="https://zesming.com/tags/Hyperloop/"/>
    
      <category term="CD" scheme="https://zesming.com/tags/CD/"/>
    
      <category term="发布工程" scheme="https://zesming.com/tags/%E5%8F%91%E5%B8%83%E5%B7%A5%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>简单谈谈 APFS</title>
    <link href="https://zesming.com/apfs/"/>
    <id>https://zesming.com/apfs/</id>
    <published>2017-04-08T07:32:53.000Z</published>
    <updated>2018-05-13T11:38:44.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="何为-APFS？"><a href="#何为-APFS？" class="headerlink" title="何为 APFS？"></a>何为 APFS？</h3><p>2016 年 WWDC 的时候，苹果甩出了一个名为 APFS 的文件系统，就是 Apple File System 的缩写。刚听到苹果又推出了一个文件系统的时候，我其实并没有特别惊讶，甚至感觉有些习惯了，因为自打苹果用上了 HFS 后，每隔一段时间就会来一个变种，甚至说每一个设备可能都会有一种特殊的 HFS（据说 iOS 里面使用的 HFS 也是个独特的变种，连 macOS 的同学都不知道他们自己搞了一套），后来又有了 HFS+。</p><p>不过按照苹果的习惯，一旦一个技术为了适应苹果的产品而被改来改去，杂乱无章的时候，就意味着在不久的将来肯定会有个全新的技术来替代它，例如早些年用 clang 替换掉了 GCC，现在正在用 Swift 替换 Objective-C。</p><p>在 iOS 10.3 发布之前，我完全没有仔细地去了解过这个玩意儿，直到 iOS 10.3 beta 版出来后，大家惊呼 APFS 现身带来了更多的可用空间，我才意识到这个技术可能又是一个苹果新造的轮子。</p><p>果不其然，在和 HFS 抗争多年后，苹果终于发现这垃圾玩意已经没有改的价值了，所以另起炉灶，重新搞一套。传闻 APFS 项目中负责人为了保证不受其它已有文件系统的影响，不去看任何一个文件系统的实现原理和机制，从零开始，针对苹果现有的设备形态，打造的一个全新的文件系统。</p><p>在苹果官网 APFS 的介绍页，介绍了它的一些主要特性：</p><blockquote><p>Apple File System is a 64-bit file system supporting over 9 quintillion files on a single volume. This state-of-the-art file system features cloning for files and directories, snapshots, space sharing, fast directory sizing, atomic safe-save primitives, and improved filesystem fundamentals, as well as a unique copy-on-write design that uses I/O coalescing to deliver maximum performance while ensuring data reliability.</p></blockquote><p>简单来说，主要还是从效率和安全性方面做了很多改进。</p><a id="more"></a><h3 id="主要特性"><a href="#主要特性" class="headerlink" title="主要特性"></a>主要特性</h3><h4 id="Clones"><a href="#Clones" class="headerlink" title="Clones"></a>Clones</h4><p>通俗点来讲，就是平时我们拷贝文件。按照我们传统思维来讲，拷贝一个文件到另一个地方还需要在磁盘上占用相同空间，并且视文件大小还会有相应的拷贝时间（例如拷一个 1GB 左右的大姐姐影视作品到别的地方，可能就需要另外占用 1GB 空间，并且还需要差不多 1s 左右的拷贝时间）。</p><p>但是在 APFS 下，一个 clone 操作基本上是瞬间完成，不管大小，并且也不会额外占用磁盘空间。</p><p>那它是咋做的呢？其实有点像是我们平时写代码时用的浅拷贝，仅仅是创建了个副本指针，指向了相同的一块区域。</p><p><img src="/images/Clone_figure1_2x.png" alt="clone-1"></p><p>可是问题来了，我们都知道，在浅拷贝操作后，如果内容发生了变更，那么所有指向这块内存的变量都会跟着改变，而在文件系统中如果这么做，那和链接有什么区别？我们希望的就是保留变更啊。好吧，你们要不同，APFS 就仅仅保留了不同。</p><p><img src="/images/Clone_figure2_2x.png" alt="clone-2"></p><p>听起来有点和 Git 的一些做法类似啊。</p><h4 id="Snapshots"><a href="#Snapshots" class="headerlink" title="Snapshots"></a>Snapshots</h4><p>快照通常情况下是一种用于备份的方式，操作系统同时可以使用快照快速还原到某一个时间点。有别于传统的备份，快照不管是从时间还是从空间占用都不大。</p><p>APFS 的快照也类似，不过从介绍来看，和传统的快照类似，只记录 diff，所以说创建了多个快照也不会特别大。</p><p><img src="/images/Snapshot_figure1_2x.png" alt="snapshot-1"></p><p>得益于 clone 的思路，创建快照也是一个简单的“指针”创建和引用，由于速度快，创建快照可以在不打断用户操作的前提下对数据进行备份</p><p><img src="/images/Snapshot_figure2_2x.png" alt="snapshot-2"></p><p>不过不巧的是，由于 APFS 不支持硬链接，还不支持现阶段的 Time Machine，所以为啥只有 iOS 现在用上了 APFS 而 macOS 还没有（其实想用也是可以用的，只不过限制多多，想必苹果可能也没有做好软件层面的适配），想必有一部分是这个原因。</p><h4 id="Space-Sharing"><a href="#Space-Sharing" class="headerlink" title="Space Sharing"></a>Space Sharing</h4><p>这个技术就有点厉害了，回想下我们在用 Windows 的时候，如果不加控制或者在一开始给系统盘分配的空间不多的话，会在使用一段时间后收到系统警告，告诉你系统盘空间不足。</p><p>通常面对这样的情况，我们可能会尝试删一些文件或者软件啊，高端点的玩家可能还会调整下磁盘分区，再高端点的玩家可能直接就选择重装系统了。完后为了防止再出现这样的问题，可能就会不再系统盘装软件放文件之类的，但是这样来说可能有会产生其他奇奇怪怪不是特别好的问题，总之非常头疼。</p><p>虽说用 Mac 后就没有再分过区，但是 Space Sharing 这项技术解决的就是上述问题。</p><p>Space Sharing 允许多个文件系统在物理卷上共享相同的底层可用空间。与为每个文件系统预先分配固定数量的空间的刚性分区方案不同，APFS格式的卷大小可以在不重新分区的情况下增长和缩小。</p><p>APFS 容器中的每个卷都会给操作系统报告相同的可用磁盘空间，这相当于容器的总可用磁盘空间。例如，对于分区A（已用 10GB）和分区B（已用 20GB）的容量为 100GB 的 APFS 容器，显示的 分区A 和 B 的可用空间均为 70GB（100GB - 10GB - 20GB）。</p><p><img src="/images/space_sharing.png" alt="space_sharing-1"></p><h4 id="Encryption"><a href="#Encryption" class="headerlink" title="Encryption"></a>Encryption</h4><p>苹果在安全方面一向比较严格，想想 FBI 为了给一个 iPhone 5c 磁盘解密从而拿到里面的数据，威逼利诱苹果帮忙，结果苹果却以除了用户谁都解不开为由拒绝，尽管到后来还是被解开了，但是从历年来有关苹果设备出现的安全问题的情况来看，因为设备和技术原因的基本没有。</p><p>而随着 macOS 上 FileVault 的出现，全盘加密几乎成了 macOS 的标配。想想几年前还在用 Windows 的时候，换了块硬盘，拿着老硬盘放到移动硬盘盒里就能当移动硬盘使用继续读取原来的数据，虽说 Windows 也有全盘加密的技术，但是从用户体验的角度来讲，苹果无疑是让你在不知不觉中就被保护了起来，这倒让我想起了一句话：真正的科技是让你感受不到它的存在。</p><p>APFS 里面，安全是它的设计基础，而针对不同的设备，APFS 加密方式也不尽相同。APFS 为容器中的每个卷提供了以下三种加密模型：</p><ul><li>无加密</li><li>单键加密</li><li>多键加密</li></ul><p>其中多键加密最为安全（这不废话么），官网介绍说，哪怕在你的设备物理安全受到威胁并且获取到访问权限，也不能解开 APFS 保护下的磁盘数据。其实上面这句话我没怎么理解，但是猜测可能是类似 macOS 的登录密码被破解了，但是磁盘数据仍然不会被解密，具体的还是日后再研究吧。</p><p>得益于它加密功能的实现，我们以后想要删除数据就没必要反复覆盖磁盘数据了，直接删除掉密钥就 OK 啦，相较于前者，简单快速。</p><h4 id="Crash-Protection-amp-Atomic-Safe-Save"><a href="#Crash-Protection-amp-Atomic-Safe-Save" class="headerlink" title="Crash Protection &amp; Atomic Safe-Save"></a>Crash Protection &amp; Atomic Safe-Save</h4><p>作为文件系统，最重要的就是要保证数据的安全，而在我们平时日常使用过程中，难免会遇到一些突发情况，导致数据错误甚至丢失，所以文件系统作为操作系统和磁盘中间管理数据的系统，强大的数据安全保护性是一个很重要的功能。</p><p>APFS 利用 copy-on-write 这项技术，可以在数据真正完成更新后再写入，期间如果发生了异常中断，诸如 Crash，那么也不会影响原有数据。而这种原子性操作，对于用户而言，只有两种状态——完全更新和啥都没变，没有了中间状态就会减少出现异常情况的可能性。</p><p>从正常的使用来看，我们操作过程中可能出现的问题，APFS 已经做了保护，并且有别于其它文件系统的数据一致性复杂校验，APFS 原子性操作从某种程度上是提高了效率。</p><p>不过提到数据校验，我从一些文章中发现，苹果的校验机制挺有意思，只会校验元数据，而用户数据则不管，WWDC 上 APFS 工程师说到，之所以这么做，是因为元数据相较于用户数据更重要，但是校验成本低，并且所有苹果设备中都有 ECC 纠错保护，加上 NAND 的冗余数据检验纠错，已经足够了。其实最有意思的是，苹果工程师对自己的设备很有信心，认为他们的设备通常不会遇到数据错误，设备本身误码率已经相当低了。</p><p>至于这种信心从何而来我不得而知，不过从专门用于数据存储阵列的 ZFS 文件系统都可能会出现数据错误，并且它们的硬件可是上百万美元级别的，那么相较于 iPhone 和 Mac 中的 TLC 芯片，如何保证数据不出错？</p><h4 id="Performance"><a href="#Performance" class="headerlink" title="Performance"></a>Performance</h4><p>性能可能是 APFS 另一个重要特性，正是因为苹果认为 HFS 不能够适用于 SSD，才重新开发 APFS 并着重优化 SSD 的存储。</p><p>SSD 和 HDD 在本质上有着很大的差别，除了 SSD 不需要寻道以外，它的存储方式也是不一样的，例如为了保证 SSD 整体的寿命，闪存主控会把写入的数据均匀分散在各个块中，因为 SSD 内的闪存的寿命是由写入次数决定的。而和上层，也就是文件系统层的交互，则是通过一个类似于虚拟内存一样的，被称作 FTL 的东西负责处理的，它会在块地址和真正存储位置中间建立个映射关系。</p><p>除此之外，SSD 有别于 HDD 还有个点，就是空间回收，HDD 通常的做法就是，这块数据不要了，我就标记为这块是空闲空间，以后用到的话再覆盖掉就好了，没必要删，所以说如果我们想要恢复数据，是一件挺容易的事情。而 SSD 则没办法这样做，因为在 SSD 中，只有空数据块才可以直接执行写入操作，而非空的要先擦除掉才能写入。</p><p>这样看来，SSD 和 HDD 的区别仅仅是多了一步擦除操作而已，但实际上并非如此。在 SSD 中，数据存储的最小单位是页（page），一个页的大小一般是 4KB，若干个页面又被组合成块（block），一个块的大小一般是 512KB。由于硬件方面的限制，SSD 单独对某个页面进行读写的操作，但擦除操作却只能对整个块进行，也就是说，一旦擦除就必须一次性擦除整个块。想想看，如果操作系统要让 SSD 改写某个页面的数据，SSD 需要执行怎样的操作呢：</p><ol><li>将要改写的目标页面所在的整个块的数据读取到缓存。</li><li>在缓存中修改目标页面的数据。</li><li>对整个块执行擦除操作。</li><li>将缓存中的数据重新写入整个块中。</li></ol><p>一个简单的 4KB 操作可能就需要把整个 512KB 的块都操作一遍，虽说 SSD 读写速度快，但这样无谓的折腾也是挺影响效率的。也正是因为这样的原因，SSD 的主控中通常会提供一个 TRIM 的命令，操作系统在删除文件时可以向主控发送 TRIM 命令告诉它哪些数据不需要了，主控拿到命令后不着急删除，而是定期 GC 一下。</p><p>不同类型的主控所提供的 TRIM 也不太一样，APFS 虽说也支持了 TRIM，并且做出了一些改进，但是按照苹果的风格，应该还是先支持自家产品使用的 SSD。</p><p>另外，APFS 也通过 I/O QoS（服务质量）对不同的数据操作请求分优先级，将用户可以立刻感知到的操作优先级提高，后台任务就可以放放。听起来和 iOS 里面的 UI 操作类似，这种高用户体验的小设计苹果也是玩的很转啊。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>当然还有例如稀疏文件支持，快速目录大小调整等特性，但是也存在着我看来不是特别靠谱的地方，就像上面我们提到的数据校验那一块，看起来有些想当然。不过针对现阶段硬件进行的软件层面的优化，肯定会比 HFS 这种杂乱老旧并且臃肿的文件系统要好的吧。毕竟在这个世界上，能够和苹果比软硬件结合开发，应该也没谁了吧。</p><p>从 iOS 10.3 可以显而易见的看出优势，哪怕只是多了几个G的可用空间，也是棒棒哒！（不过你们难道没发现通过 iOS OTA 升级无损切换文件系统其实也是一件很厉害的事么？不过从刚才我们分析 copy-on-write 这项技术来看，应该也不会有啥问题。）</p><p>虽说 macOS 目前还是不能从 GUI 上找到什么 APFS 的痕迹，但是可以通过一些命令行方式创建一个 APFS 卷（分区）。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hdiutil create -fs APFS -size 1GB foo.sparseimage</span><br></pre></td></tr></table></figure><p>不过要注意的是，现在的 macOS 的 Time Mechine 并不兼容 APFS，FileVault 也是不兼容的，最主要的是，目前正式版本 macOS 10.12.4 是不支持 APFS 的分区当作启动分区的。</p><p>所以简单来说，体验一下 APFS 没啥问题，想日常使用，就算了，等 macOS 什么时候兼容 APFS，应该会是有很大程度的用户体验提升吧。</p><p>虽说苹果直接扔掉了 HFS 开发了 APFS，但是我感觉当前版本的 APFS 应该不会成为一个主流的文件系统，首先一点就是苹果还没有开源，至于到底开不开源不得而知。另外这种针对 SSD 专门开发的文件系统，是否对于 HDD 有支持，我们也不清楚。处于自家生态链的考虑，抛弃掉32位设备的支持，也会让它在一段时间内只会出现在苹果的设备上。</p><p>存在一些遗憾也在所难免，不过作为用户能够更好地体验苹果产品也是挺爽的。</p><p>附：<a href="https://developer.apple.com/library/content/documentation/FileManagement/Conceptual/APFS_Guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40016999-CH1-DontLinkElementID_18" target="_blank" rel="noopener">Apple File System Guide</a></p>]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;何为-APFS？&quot;&gt;&lt;a href=&quot;#何为-APFS？&quot; class=&quot;headerlink&quot; title=&quot;何为 APFS？&quot;&gt;&lt;/a&gt;何为 APFS？&lt;/h3&gt;&lt;p&gt;2016 年 WWDC 的时候，苹果甩出了一个名为 APFS 的文件系统，就是 Apple File System 的缩写。刚听到苹果又推出了一个文件系统的时候，我其实并没有特别惊讶，甚至感觉有些习惯了，因为自打苹果用上了 HFS 后，每隔一段时间就会来一个变种，甚至说每一个设备可能都会有一种特殊的 HFS（据说 iOS 里面使用的 HFS 也是个独特的变种，连 macOS 的同学都不知道他们自己搞了一套），后来又有了 HFS+。&lt;/p&gt;
&lt;p&gt;不过按照苹果的习惯，一旦一个技术为了适应苹果的产品而被改来改去，杂乱无章的时候，就意味着在不久的将来肯定会有个全新的技术来替代它，例如早些年用 clang 替换掉了 GCC，现在正在用 Swift 替换 Objective-C。&lt;/p&gt;
&lt;p&gt;在 iOS 10.3 发布之前，我完全没有仔细地去了解过这个玩意儿，直到 iOS 10.3 beta 版出来后，大家惊呼 APFS 现身带来了更多的可用空间，我才意识到这个技术可能又是一个苹果新造的轮子。&lt;/p&gt;
&lt;p&gt;果不其然，在和 HFS 抗争多年后，苹果终于发现这垃圾玩意已经没有改的价值了，所以另起炉灶，重新搞一套。传闻 APFS 项目中负责人为了保证不受其它已有文件系统的影响，不去看任何一个文件系统的实现原理和机制，从零开始，针对苹果现有的设备形态，打造的一个全新的文件系统。&lt;/p&gt;
&lt;p&gt;在苹果官网 APFS 的介绍页，介绍了它的一些主要特性：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Apple File System is a 64-bit file system supporting over 9 quintillion files on a single volume. This state-of-the-art file system features cloning for files and directories, snapshots, space sharing, fast directory sizing, atomic safe-save primitives, and improved filesystem fundamentals, as well as a unique copy-on-write design that uses I/O coalescing to deliver maximum performance while ensuring data reliability.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;简单来说，主要还是从效率和安全性方面做了很多改进。&lt;/p&gt;
    
    </summary>
    
      <category term="iOS" scheme="https://zesming.com/categories/iOS/"/>
    
      <category term="macOS" scheme="https://zesming.com/categories/macOS/"/>
    
    
      <category term="APFS" scheme="https://zesming.com/tags/APFS/"/>
    
      <category term="HFS" scheme="https://zesming.com/tags/HFS/"/>
    
      <category term="SSD" scheme="https://zesming.com/tags/SSD/"/>
    
      <category term="file system" scheme="https://zesming.com/tags/file-system/"/>
    
  </entry>
  
  <entry>
    <title>树莓派的初探</title>
    <link href="https://zesming.com/respberry-centos/"/>
    <id>https://zesming.com/respberry-centos/</id>
    <published>2017-03-25T06:24:01.000Z</published>
    <updated>2018-05-13T11:46:15.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="树莓派"><a href="#树莓派" class="headerlink" title="树莓派"></a>树莓派</h3><p>树莓派（Raspberry Pi），是一款基于 Linux 的单板机电脑。它由英国的树莓派基金会所开发，目的是以低价硬件及自由软件促进学校的基本计算机科学教育。</p><p>它使用的是基于 ARM 架构的博通芯片，和现在大多数智能路由器选用的硬件大体相当，不过最新款的树莓派3 Model B 则使用了 ARM 64 位的处理器，性能上已经能够超越路由器好多了。</p><a id="more"></a><h3 id="购入"><a href="#购入" class="headerlink" title="购入"></a>购入</h3><p>其实当时考虑的是想给我们家的梯子提个速，顺带看看有没有什么其他能玩的小东西，反正也不贵，所以就去马云家选购了一套 Respberry 3 Model B。</p><p>回来简单拼了一下，感觉还挺好看的，满满的极客塑料感……</p><p><img src="/images/respberry_1.JPG" alt=""></p><p><img src="/images/respberry_2.JPG" alt=""></p><p>由于树莓派的硬盘实际上就是个 MicroSD 卡，考虑到性能原因，官方推荐使用 Class 10 的卡，所以我又在奶茶东购入了一张 32G 的卡，如果说单纯装一个 Linux 的话，32G 足够了。</p><p>前前后后算下来，这一套机器花了300软妹币左右吧，综合考虑下来，性价比还是挺高的。</p><h3 id="ROM"><a href="#ROM" class="headerlink" title="ROM"></a>ROM</h3><p>官方推荐的是一个名为 Respbian 的 Linux 系统，这还算是个挺完备的系统，有图形化界面，各项功能驱动都挺齐全。不过对于我这种压根就没有想把它当电脑用的人来说，图形化界面有点多余。看了下其他支持 ARM 的 OS，还有 Ubuntu 和 CentOS。我对 Ubuntu 向来就没有什么感觉，可能就是因为 <code>apt-get</code> 比 <code>yum</code> 敲起来麻烦。所以只剩下 CentOS 了。</p><p>PS：其实支持树莓派的系统还有一些，甚至 Windows 10 的物联网版也是支持的，不过我真想不出来用 Windows 除了玩游戏还能干啥？</p><h3 id="装系统"><a href="#装系统" class="headerlink" title="装系统"></a>装系统</h3><p>到 CentOS 的官网，扒到一篇 <a href="https://wiki.centos.org/SpecialInterestGroup/AltArch/Arm32/RaspberryPi3" target="_blank" rel="noopener">wiki</a>，还专门介绍如何在树莓派上装 CentOS。不过……貌似只有32位的系统，64位那里写着我们熟悉的 <code>Coming Soon</code>。</p><p>唉，无所谓了，按照它的 Quick Start，我们找到它的 32bit image 下载地址，下载下来。</p><p>接下来，如果按照 wiki 上描述的步骤，看起来需要用到 <code>XZ Utils</code> 来搞，通过 wiki 上提供的命令将 CentOS 的镜像写入到卡上，还挺简单：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ dd <span class="keyword">if</span>=CentOS-Userland-7-armv7hl-Minimal-RaspberryPi3.img of=/dev/mmcblk0 bs=8192; sync</span><br></pre></td></tr></table></figure><p>如果你没有 Linux 系统，可以到 Windows 上下载一个名为 <code>Win32DiskImager</code> 的软件（看来有时候 Windows 还是能做一些有用的事情的）。先解压下载的压缩包，然后通过图形化界面把 CentOS 的镜像写入到卡上。</p><p><img src="/images/respberry_3.JPG" alt=""></p><p><img src="/images/respberry_4.JPG" alt=""></p><p>尽管官方介绍说，1.2A 的电源就可以，但是为了正常稳定使用，还是建议使用 2.5A 的电源，因为一旦火力全开，挂上各种传感器，整个电力需求还是蛮大的。</p><p>把写好的卡插进机器，连接上有线网络（因为一开始 Wi-Fi 还没有安装驱动），链接上电源启动机器，大概 5s 就会启动好了，完后可能就需要去路由器里面看分配的 IP 多少，通过 IP 登录到机器上（Windows 下需要个软件，其他系统直接以 root 用户 SSH 就好了，密码就是 <code>centos</code>）。</p><p>另外有网友提到，内存卡写入系统后被重新分区，导致很大一部分空闲分区浪费，需要输入如下命令将空闲分区重新分配：</p><figure class="highlight bash"><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">$ touch /.rootfs-repartition</span><br><span class="line">$ systemctl reboot</span><br></pre></td></tr></table></figure><p>另外需要提一点，因为 CentOS 默认没有安装 Wi-Fi 的相关模块，如果说想要使用 Wi-Fi 功能的话，需要执行以下两行命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl --location https://github.com/RPi-Distro/firmware-nonfree/raw/master/brcm80211/brcm/brcmfmac43430-sdio.bin &gt; /usr/lib/firmware/brcm/brcmfmac43430-sdio.bin</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl --location https://github.com/RPi-Distro/firmware-nonfree/raw/master/brcm80211/brcm/brcmfmac43430-sdio.txt &gt; /usr/lib/firmware/brcm/brcmfmac43430-sdio.txt</span><br></pre></td></tr></table></figure><p>完成后重启系统。</p><h3 id="看下系统信息"><a href="#看下系统信息" class="headerlink" title="看下系统信息"></a>看下系统信息</h3><p>登录上来后，第一件事就想看看硬件信息以确定我买的是不是正品（多年以来和 JS 斗争养成的习惯）。</p><p>首先看看 CPU：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ lscpu</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><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Architecture:          armv7l</span><br><span class="line">Byte Order:            Little Endian</span><br><span class="line">CPU(s):                4</span><br><span class="line">On-line CPU(s) list:   0-3</span><br><span class="line">Thread(s) per core:    1</span><br><span class="line">Core(s) per socket:    4</span><br><span class="line">座：                 1</span><br><span class="line">型号名称：        ARMv7 Processor rev 4 (v7l)</span><br></pre></td></tr></table></figure><p>大哥，说好的 ARM64 呢？怎么变成 ARMv7l 了？</p><p>又看了眼 <code>cpuinfo</code></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><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line">processor: 0</span><br><span class="line">model name: ARMv7 Processor rev 4 (v7l)</span><br><span class="line">BogoMIPS: 38.40</span><br><span class="line">Features: half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32 </span><br><span class="line">CPU implementer: 0x41</span><br><span class="line">CPU architecture: 7</span><br><span class="line">CPU variant: 0x0</span><br><span class="line">CPU part: 0xd03</span><br><span class="line">CPU revision: 4</span><br><span class="line"></span><br><span class="line">processor: 1</span><br><span class="line">model name: ARMv7 Processor rev 4 (v7l)</span><br><span class="line">BogoMIPS: 38.40</span><br><span class="line">Features: half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32 </span><br><span class="line">CPU implementer: 0x41</span><br><span class="line">CPU architecture: 7</span><br><span class="line">CPU variant: 0x0</span><br><span class="line">CPU part: 0xd03</span><br><span class="line">CPU revision: 4</span><br><span class="line"></span><br><span class="line">processor: 2</span><br><span class="line">model name: ARMv7 Processor rev 4 (v7l)</span><br><span class="line">BogoMIPS: 38.40</span><br><span class="line">Features: half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32 </span><br><span class="line">CPU implementer: 0x41</span><br><span class="line">CPU architecture: 7</span><br><span class="line">CPU variant: 0x0</span><br><span class="line">CPU part: 0xd03</span><br><span class="line">CPU revision: 4</span><br><span class="line"></span><br><span class="line">processor: 3</span><br><span class="line">model name: ARMv7 Processor rev 4 (v7l)</span><br><span class="line">BogoMIPS: 38.40</span><br><span class="line">Features: half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32 </span><br><span class="line">CPU implementer: 0x41</span><br><span class="line">CPU architecture: 7</span><br><span class="line">CPU variant: 0x0</span><br><span class="line">CPU part: 0xd03</span><br><span class="line">CPU revision: 4</span><br><span class="line"></span><br><span class="line">Hardware: BCM2835</span><br><span class="line">Revision: a22082</span><br><span class="line">Serial: 00000000a9898d83</span><br></pre></td></tr></table></figure><p>奇怪，最后的 <code>Hardware</code> 竟然变成了树莓派一代的 SoC，不过下面的 <code>Revision</code> 则是三代 B 的系列号。</p><p>去网上搜搜，看到一个网友提到了这样一段话。</p><blockquote><p>可能是因为为了兼容旧版本的软件和操作系统，在树莓派3上依然使用了 ARMv7 的32位指令集，即使芯片支持 ARMv8 指令集，但由于考虑到兼容性，现在还没有推出64位的系统和软件来运行，所以固件依然保留32位。</p></blockquote><p>所以说，很有可能是因为 CentOS 目前并没有推出64位的系统，而目前32位的系统则是简单使用了之前的内核。</p><p>综合来看，有 Wi-Fi 有蓝牙，系统里面也有树莓派3的各种痕迹，单纯从 CPU 的信息来判定感觉有些太片面。所以就先凑合着吧，记得最初我的第一台笔记本也是用了多年32位的 Windows。</p><h3 id="安装一些软件"><a href="#安装一些软件" class="headerlink" title="安装一些软件"></a>安装一些软件</h3><p>没了图形化界面，命令行界面必须要好看啊，zsh 和 <a href="http://ohmyz.sh/" target="_blank" rel="noopener">oh-my-zsh</a> 是必备的，这个简单，直接 <code>yum</code> 加上官方安装方式，几行命令就搞定。</p><p>其次，<a href="http://hisham.hm/htop/" target="_blank" rel="noopener">htop</a>，这个漂亮的任务管理器还是平时比较喜欢使用的工具之一，因为有时候通过这个工具能够一眼看到很多进程系统信息。不过目前貌似没有 ARM 版本的二进制直接提供使用，只能尝试通过源码本地编译来安装。可能是因为没有适配的原因吧，在树莓派上使用时不时会崩溃。</p><p><a href="https://github.com/xtaci/kcptun" target="_blank" rel="noopener">kcptun</a>，这个工具是我买树莓派的最初目的，虽说家里智能路由器也是各种性能不错，但是一旦上 kcptun 后，经常就会 CPU 打满，最终被系统杀掉进程。所以最初的打算就是外挂一个树莓派专门跑 kcptun，能够保证性能。最后的结果是，开了4线程来跑，树莓派的系统负荷依然很低。现在就先不细说了，过段时间可以单独说一下这个工具。</p><h3 id="未来的使用计划"><a href="#未来的使用计划" class="headerlink" title="未来的使用计划"></a>未来的使用计划</h3><p>买完这家伙后，工作上就开始各种忙了，紧接着就是过年，过完年后又忙着杂七杂八的事情，一直到这段时间才有空过来看看这个被遗忘的家伙。</p><p>因为单一跑个 kcptun 实在是有点浪费，本来打算搭建个服务，上几个传感器为我监控家里面的温度湿度空气质量啥的，后来发现小米的米家里面都有，还比自己买便宜，而且有个米家的 app 也省得自己搭建服务了，索性就放弃了这个想法。这段时间又看到可以利用树莓派把米家的设备桥接到 iOS 里面的 HomeKit 上，所以准备抽空把这个捣鼓下。</p><p>当然了，如果性能还充足，我准备用 Rails 建个服务，跑一些自动化任务或者简单的家庭网络管理后台，这样配合 DDNS 就能在外面掌控家里的情况了，虽说 Ruby 可能会有一些性能上的问题，但是这点小的任务加上对树莓派硬件的大致了解来看，问题应该不大。</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;树莓派（Raspberry Pi），是一款基于 Linux 的单板机电脑。它由英国的树莓派基金会所开发，目的是以低价硬件及自由软件促进学校的基本计算机科学教育。&lt;/p&gt;
&lt;p&gt;它使用的是基于 ARM 架构的博通芯片，和现在大多数智能路由器选用的硬件大体相当，不过最新款的树莓派3 Model B 则使用了 ARM 64 位的处理器，性能上已经能够超越路由器好多了。&lt;/p&gt;
    
    </summary>
    
      <category term="Linux" scheme="https://zesming.com/categories/Linux/"/>
    
    
      <category term="Raspberry Pi" scheme="https://zesming.com/tags/Raspberry-Pi/"/>
    
      <category term="CentOS" scheme="https://zesming.com/tags/CentOS/"/>
    
      <category term="Raspbian" scheme="https://zesming.com/tags/Raspbian/"/>
    
      <category term="Ubuntu" scheme="https://zesming.com/tags/Ubuntu/"/>
    
      <category term="Windows" scheme="https://zesming.com/tags/Windows/"/>
    
      <category term="arm64" scheme="https://zesming.com/tags/arm64/"/>
    
      <category term="armv7" scheme="https://zesming.com/tags/armv7/"/>
    
  </entry>
  
  <entry>
    <title>简谈 OC 中陌生而又熟悉的 super</title>
    <link href="https://zesming.com/oc-super/"/>
    <id>https://zesming.com/oc-super/</id>
    <published>2017-03-11T08:34:16.000Z</published>
    <updated>2018-05-13T11:44:43.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h3><p>平时一直和 <code>@臧成威</code> 等小伙伴做我们美团 iOS 客户端的发布工程相关事情，感觉和 iOS 越来越没啥关系了，有一天晚上吃饭，我们几个同学讨论要不然让成威在平时抽空给我们讲述一些 iOS 中好玩的东西，我们总结下来也算是一种技术积累和提升。</p><p>于是在我们的威逼利诱下，成威答应了这个事情。</p><p>此篇文章是在听了成威某一天的分享后，我抽空稍加调研总结出来的。</p><h3 id="熟悉的-super"><a href="#熟悉的-super" class="headerlink" title="熟悉的 super"></a>熟悉的 <code>super</code></h3><p>我们平时利用 OC 开发 iOS 的时候，会经常用到 <code>super</code> 这一关键字，比如我们在创建一个新的 <code>ViewController</code> 后会首先看到的：</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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)viewDidLoad &#123;</span><br><span class="line">    [<span class="keyword">super</span> viewDidLoad];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>或者是我们在一个类的初始化器 (<code>initializer</code>) 里面，我们需要这样写：</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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">instancetype</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">        <span class="comment">// init something</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></pre></td></tr></table></figure><p>有时候我们调用一些其他的 <code>CocoaTouch</code> 方法，也需要我们手动调用 <code>super</code> 的方法，例如：</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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)updateConstraints &#123;</span><br><span class="line"><span class="comment">// do something to setup views constraints</span></span><br><span class="line"></span><br><span class="line">[<span class="keyword">super</span> updateConstraints];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上，都是我们平时比较常见的一些带有 <code>super</code> 的用法，看起来还是比较正常的，也比较能够清楚地知道这些用法的含义。</p><p>接下来我们看一个不太常见的代码。</p><a id="more"></a><h3 id="陌生的-super"><a href="#陌生的-super" class="headerlink" title="陌生的 super"></a>陌生的 <code>super</code></h3><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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">OCTChild</span> : <span class="title">OCTFather</span></span></span><br><span class="line"></span><br><span class="line">- (<span class="keyword">instancetype</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">        <span class="built_in">NSLog</span>(<span class="string">@"%@"</span>, <span class="built_in">NSStringFromClass</span>([<span class="keyword">self</span> <span class="keyword">class</span>]));</span><br><span class="line">        <span class="built_in">NSLog</span>(<span class="string">@"%@"</span>, <span class="built_in">NSStringFromClass</span>([<span class="keyword">super</span> <span class="keyword">class</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="keyword">@end</span></span><br></pre></td></tr></table></figure><p>那么在 <code>OCTChild</code> 被 <code>- init</code> 的时候，它会输出什么？</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">2017-02-25 16:38:07.064 OC Test Project[4633:537030] OCTChild</span><br><span class="line">2017-02-25 16:38:07.064 OC Test Project[4633:537030] OCTChild</span><br></pre></td></tr></table></figure><p>看到这里，有的同学可能就笑了，这不是个常见的 iOS 面试题么？有什么陌生的？</p><p>的确，这个代码之前在我们网红 <code>孙源 @sunnyxx</code> 的博文中也出现过，当时他也简单分析了出现上述代码执行结果的原因。</p><h3 id="super-到底什个什么鬼？"><a href="#super-到底什个什么鬼？" class="headerlink" title="super 到底什个什么鬼？"></a>super 到底什个什么鬼？</h3><p>官方文档中提到</p><blockquote><p>There’s another important keyword available to you in Objective-C, called <code>super</code>. Sending a message to <code>super</code> is a way to call through to a method implementation defined by a superclass further up the inheritance chain. The most common use of <code>super</code> is when overriding a method.</p></blockquote><p>用我们中国人的话来说，即为 <code>super</code> 是一个关键字，给 <code>super</code> 发消息实际上是一种调用继承链上父类实现的方法的方式，通常用于子类需要重写父类方法又想保留父类行为的时候。</p><p>上面我们提到的 <code>- viewDidLoad</code> 里面的调用，实际上是想在子类初始化自己之前首先执行父类的一些初始化操作。</p><p>而 <code>- init</code> 则是利用父类（甚至是祖父类）的初始化器先创建一个原始的自己，再进行定制化操作，保证 OOP 中的一些继承特性。</p><p>最后的 <code>- updateConstraints</code> 我们可以理解为，在完成自己组件的 <code>autolayout</code>约束设定后，需要调用父类的更新设定以保证约束生效。</p><p>这没问题，我记住了，我们就是用 <code>super</code> 来调用父类的方法嘛，看起来还是比较容易理解的。</p><p>可是，上面那个打印 <code>[super class]</code> 的代码为啥看起来不想这么回事啊！？</p><h3 id="这并不是我的本意！"><a href="#这并不是我的本意！" class="headerlink" title="这并不是我的本意！"></a>这并不是我的本意！</h3><p>我们再来看一个和 <code>[super class]</code> 类似的代码：</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><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">OCTFather</span></span></span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)selfIntroduce &#123;</span><br><span class="line">    [<span class="keyword">self</span> saySomething];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)saySomething &#123;</span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"%s"</span>, <span class="string">"I am a father!"</span>);</span><br><span class="line">&#125;</span><br><span class="line"></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">OCTChild</span> : <span class="title">OCTFather</span></span></span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)selfIntroduce &#123;</span><br><span class="line">    [<span class="keyword">self</span> saySomething];</span><br><span class="line">    [<span class="keyword">super</span> selfIntroduce];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)saySomething &#123;</span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"%s"</span>, <span class="string">"I am a child!"</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>这里我们执行一下 <code>OCTChild</code> 的实例方法 <code>- shoutOut</code></p><figure class="highlight objectivec"><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">OCTChild *child = [[OCTChild alloc] init];</span><br><span class="line">[child selfIntroduce];</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></pre></td><td class="code"><pre><span class="line">2017-02-25 17:46:22.014 OC Test Project[6099:782572] I am a child!</span><br><span class="line">2017-02-25 17:46:22.014 OC Test Project[6099:782572] I am a child!</span><br></pre></td></tr></table></figure><p>按照上面我们说的 <code>super</code> 的用法，我们是不是该这样理解？</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">路人甲：&quot;这位小朋友，来做个自我介绍！“</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"></span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>这当爹的傻了吧？</p><p>不过我们回顾下上面 <code>[super class]</code> 的那坨代码，可以看出这两个都得出了一个反常理的结果！</p><p>这并不是我的本意！</p><h3 id="runtime-问题吧？"><a href="#runtime-问题吧？" class="headerlink" title="runtime 问题吧？"></a>runtime 问题吧？</h3><p>虽说上面的两个问题有些不符合常理，但是面对这样的结果，我们还是要有个突破口来找寻答案。</p><p>众所周知，<code>Objective-C</code> 是一个动态语言，所谓的方法调用实际上是在运行时的一个消息发送。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">id <span class="title">objc_msgSend</span><span class="params">(id theReceiver, SEL theSelector, ...)</span></span></span><br></pre></td></tr></table></figure><p>那么，<code>super</code> 的话，receiver 应该是自己的父类吧？可是这样的话，应该是个父类的实例，可是什么时候生成的实例呢？每调用一次都要生成一个实例，什么时候销毁的呢？整个生命周期是啥样的呢？</p><p>感觉跑偏了，不要瞎想了，我们用 <code>clang</code> 看一下咋回事应该是比较直接的。</p><p>利用 <code>clang</code> 重写的 <code>C++</code> 代码，我们找到了这样一些东西：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">void</span> _I_OCTChild_selfIntroduce(OCTChild * self, SEL _cmd) &#123;</span><br><span class="line">    ((<span class="keyword">void</span> (*)(id, SEL))(<span class="keyword">void</span> *)objc_msgSend)((id)self, sel_registerName(<span class="string">"saySomething"</span>));</span><br><span class="line">    ((<span class="keyword">void</span> (*)(__rw_objc_super *, SEL))(<span class="keyword">void</span> *)objc_msgSendSuper)((__rw_objc_super)&#123;(id)self, (id)class_getSuperclass(objc_getClass(<span class="string">"OCTChild"</span>))&#125;, sel_registerName(<span class="string">"selfIntroduce"</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这就是上面我们提到的 <code>OCTChild</code> 中的 <code>- selfIntroduce</code> 方法，方法块中第一行的 <code>[self saySomething]</code> 已经被转成了我们比较熟知的样子：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">((<span class="keyword">void</span> (*)(id, SEL))(<span class="keyword">void</span> *)objc_msgSend)((id)self, sel_registerName(<span class="string">"saySomething"</span>));</span><br></pre></td></tr></table></figure><p>下面那句好像看起来和上面不一样。这个 <code>objc_msgSendSuper</code> 应该就是 <code>super</code> 调用方法时候真正的实现。</p><p>我们不难看出，这个函数有两个参数：</p><ol><li><code>(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass(&quot;OCTChild&quot;))}</code></li><li><code>sel_registerName(&quot;selfIntroduce&quot;)</code></li></ol><p>而第一个参数实际上是个名为 <code>objc_super</code> 的 <code>struct</code>：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/// Specifies the superclass of an instance. </span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">objc_super</span> &#123;</span></span><br><span class="line">    <span class="comment">/// Specifies an instance of a class.</span></span><br><span class="line">    __unsafe_unretained id receiver;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/// Specifies the particular superclass of the instance to message. </span></span><br><span class="line">    __unsafe_unretained Class super_class;</span><br><span class="line">    <span class="comment">/* super_class is the first class to search */</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>所以，大体上我们可以确定，在编译期的时候，我们的这个 <code>struct</code> 就已经确定了里面 <code>super_class</code> 的值（虽说 <code>objc_getClass()</code> 是个运行时函数，但是入参是个固定的类名哦）。</p><p>回顾下 <code>self</code> 的本质，实际上是一个隐式参数，表示用来接收消息的自己类的实例对象。</p><p>而 <code>super</code> 实际上则是个指令符号（就是个 flag），编译器在看到我们写的 <code>[super someMethod]</code> 的时候，会编译成刚才我们看到的 <code>objc_msgSendSuper</code>。</p><p>综上所述，<code>super</code> 相关的方法调用方式实际上在编译期间就已经确定了。</p><h3 id="真相只有一个！"><a href="#真相只有一个！" class="headerlink" title="真相只有一个！"></a>真相只有一个！</h3><p>原理搞清楚了，我们再回来看下刚才的问题，为啥调用父类的 <code>- selfIntroduce</code> 方法还是会喊出“我是儿子”这个话？</p><p>按照刚才我们的研究步骤，我们一步一步来重现。</p><p>1 一开始的 <code>child</code> 对象调用自己的实例方法 <code>- selfIntroduce</code>，实际上底层调用是这样子的：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">objc_msgSend(self, @selector(selfIntrduce))</span><br></pre></td></tr></table></figure><p>2 然后执行方法实现中 <code>[self saySomething]</code> 和 <code>[super selfIntroduce]</code></p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">objc_msgSend(self, @selector(selfIntrduce))</span><br><span class="line">objc_msgSendSuper(&#123;self, class_getSuperclass(objc_getClass(<span class="string">"OCTChild"</span>))&#125;,@selector(selfIntroduce))</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">注意：这里的 self 为我们一开始生成的 OCTChild 的实例对象 child</span><br></pre></td></tr></table></figure><p>3 <code>[self saySomething]</code> 执行，打印出 <code>OCTChild</code> 中的实现 <code>I am a child!</code></p><p>4 <code>[super selfIntroduce]</code> 执行，查找 <code>OCTChild</code> 父类中是否有 <code>- selfIntroduce</code> 方法，按照继承链，找到它的第一个父类 <code>OCTFather</code>，然后调用其实现，执行 <code>[self saySomething]</code></p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">objc_msgSend(self, @selector(selfIntrduce))</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">注意：这里的 self 仍为我们一开始生成的 OCTChild 的实例对象 child，它是通过 objc_msgSendSuper 里面的 objc_super 这一 struct 带过来的。</span><br></pre></td></tr></table></figure><p>5 <code>[self saySomething]</code> 执行，打印出 <code>OCTChild</code> 中的实现 <code>I am a child!</code></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><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="class"><span class="keyword">@implementation</span> <span class="title">OCTFather</span></span></span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)selfIntroduce &#123;</span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"%@: %s"</span>, <span class="keyword">self</span>, <span class="string">"Begin to introduce myself at superclass!"</span>);</span><br><span class="line">    [<span class="keyword">self</span> saySomething];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)saySomething &#123;</span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"%s"</span>, <span class="string">"I am a father!"</span>);</span><br><span class="line">&#125;</span><br><span class="line"></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">OCTChild</span> : <span class="title">OCTFather</span></span></span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)selfIntroduce &#123;</span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"%@: %s"</span>, <span class="keyword">self</span>, <span class="string">"Begin to introduce myself!"</span>);</span><br><span class="line">    [<span class="keyword">self</span> saySomething];</span><br><span class="line">    [<span class="keyword">super</span> selfIntroduce];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)saySomething &#123;</span><br><span class="line">    <span class="built_in">NSLog</span>(<span class="string">@"%s"</span>, <span class="string">"I am a child!"</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>其实就是加了两个在自我介绍开始时的 log</p><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></pre></td><td class="code"><pre><span class="line">2017-02-25 21:35:34.293 OC Test Project[9496:1418664] &lt;OCTChild: 0x6100000182c0&gt;: Begin to introduce myself!</span><br><span class="line">2017-02-25 21:35:34.294 OC Test Project[9496:1418664] I am a child!</span><br><span class="line">2017-02-25 21:35:34.294 OC Test Project[9496:1418664] &lt;OCTChild: 0x6100000182c0&gt;: Begin to introduce myself at superclass!</span><br><span class="line">2017-02-25 21:35:34.294 OC Test Project[9496:1418664] I am a child!</span><br></pre></td></tr></table></figure><p>简单总结下我们之前的所有分析：</p><ol><li><code>super</code> 不同于 <code>self</code>，它不是个对象，而是个 flag</li><li>用于 <code>objc_msgSendSuper</code> 的结构体 <code>objc_super</code> 是编译时确定的，里面包含了当前类的父类信息</li><li><code>[super someMethod]</code> 会去利用结构体中的父类信息，从这个父类开始顺着继承链向上查找，直到找到第一个实现 <code>- someMethod</code> 这个方法的类</li><li>找到方法后，利用结构体中的 receiver，也就是一开始触发这个方法调用的实例，调用这个方法实现。</li></ol><p>所以说，<code>[super class]</code> 经过这么一大圈的转换，实际上变成了 <code>[self class]</code> 了。</p><p>而我们刚才的那个自我介绍的例子，也是这样实现方式的体现，不过由于碰巧和父类有同样的实现逻辑，并且还覆写了同样的方法，最后才有这样的奇怪结果。</p><p>想象着还挺危险的，不过也可以利用这样的黑魔法去玩一些高端的实现。</p><h3 id="为什么不在运行时确定？"><a href="#为什么不在运行时确定？" class="headerlink" title="为什么不在运行时确定？"></a>为什么不在运行时确定？</h3><p>既然 OC 是一个动态语言，那么为什么 <code>super</code> 不能和 <code>self</code> 一样运行时来查找所属关系呢？</p><p>这个问题问得好，我们先来看下 <code>self</code> 在运行时是怎么玩的？</p><figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">NSLog</span>(<span class="string">@"%@"</span>, <span class="keyword">self</span>);</span><br></pre></td></tr></table></figure><p>得到的答案很明确，<code>self</code> 是自己这个类的实例对象。</p><p>很好，我们就按照这个思路假设 <code>super</code> 就是一个 “<code>self</code> 的父类”。</p><p>那么就简单的重新回顾下我们之前的那个例子：</p><p><code>self</code> 是一个 <code>OCTChild</code> 的实例对象，那么 <code>super</code> 是这个对象的父类。</p><p><img src="/images/super-1.png" alt="super-1"></p><p>看上去没啥问题，那么我们现在如果加一个 <code>OCTGrandFather</code> 类，使 <code>OCTFather</code> 继承这个类，那结果呢？</p><p><code>self</code> 还是 <code>OCTChild</code> 的一个实例对象，只不过在 <code>OCTFather</code> 中增加一个 <code>super</code> 的方法调用，按照我们的期望，应该是会找到 <code>OCTGrandFather</code> 这个类。</p><p>但实际上呢？</p><p><img src="/images/super-2.png" alt="super-2"></p><p>如图所示，一开始调用没啥问题，刚才我们也验证过了，但是当 <code>OCTFather</code> 调用 <code>super</code> 方法的时候，由于是运行时确定 <code>super</code> 为 “<code>self</code> 的父类”，那么它就又会回到 <code>self</code> 所表示的 <code>OCTChild</code> 的实例对象这里来，然后重新向上找父类。</p><p>等于说，如果是运行时确定的，那么 <code>super</code> 仅仅只能找到 <code>self</code> 自己的父类，再往上就会回来往复循环。</p><p>正是因为这个原因，所以要在编译期就确定 <code>super</code> 的从属关系而不是在运行时去做。</p><p>同理，如果你想要打印一个 <code>super</code> 或者添加一个带有 <code>super</code> 方法调用的运行时方法，编译器就会立刻告诉你我不认识这个 <code>super</code>。</p><p><img src="/images/super-3.png" alt="super-3"></p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p><code>super</code> 这个东西虽说平时用的很多，并且在 Xcode 的语法高亮上和 <code>self</code> 一样，但是本质上却是和 <code>self</code> 有着天壤之别。通过各种手段去认识了解 <code>super</code>，可以尽可能的减少我们平时因为使用不当造成的奇奇怪怪的问题，也可以加深我们对这个动态语言的理解～</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;code&gt;@臧成威&lt;/code&gt; 等小伙伴做我们美团 iOS 客户端的发布工程相关事情，感觉和 iOS 越来越没啥关系了，有一天晚上吃饭，我们几个同学讨论要不然让成威在平时抽空给我们讲述一些 iOS 中好玩的东西，我们总结下来也算是一种技术积累和提升。&lt;/p&gt;
&lt;p&gt;于是在我们的威逼利诱下，成威答应了这个事情。&lt;/p&gt;
&lt;p&gt;此篇文章是在听了成威某一天的分享后，我抽空稍加调研总结出来的。&lt;/p&gt;
&lt;h3 id=&quot;熟悉的-super&quot;&gt;&lt;a href=&quot;#熟悉的-super&quot; class=&quot;headerlink&quot; title=&quot;熟悉的 super&quot;&gt;&lt;/a&gt;熟悉的 &lt;code&gt;super&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;我们平时利用 OC 开发 iOS 的时候，会经常用到 &lt;code&gt;super&lt;/code&gt; 这一关键字，比如我们在创建一个新的 &lt;code&gt;ViewController&lt;/code&gt; 后会首先看到的：&lt;/p&gt;
&lt;figure class=&quot;highlight objectivec&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;- (&lt;span class=&quot;keyword&quot;&gt;void&lt;/span&gt;)viewDidLoad &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    [&lt;span class=&quot;keyword&quot;&gt;super&lt;/span&gt; viewDidLoad];&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;或者是我们在一个类的初始化器 (&lt;code&gt;initializer&lt;/code&gt;) 里面，我们需要这样写：&lt;/p&gt;
&lt;figure class=&quot;highlight objectivec&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;- (&lt;span class=&quot;keyword&quot;&gt;instancetype&lt;/span&gt;)init &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;self&lt;/span&gt; = [&lt;span class=&quot;keyword&quot;&gt;super&lt;/span&gt; init];&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;keyword&quot;&gt;self&lt;/span&gt;) &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// init something&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;self&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;有时候我们调用一些其他的 &lt;code&gt;CocoaTouch&lt;/code&gt; 方法，也需要我们手动调用 &lt;code&gt;super&lt;/code&gt; 的方法，例如：&lt;/p&gt;
&lt;figure class=&quot;highlight objectivec&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;- (&lt;span class=&quot;keyword&quot;&gt;void&lt;/span&gt;)updateConstraints &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;comment&quot;&gt;// do something to setup views constraints&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	[&lt;span class=&quot;keyword&quot;&gt;super&lt;/span&gt; updateConstraints];&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;以上，都是我们平时比较常见的一些带有 &lt;code&gt;super&lt;/code&gt; 的用法，看起来还是比较正常的，也比较能够清楚地知道这些用法的含义。&lt;/p&gt;
&lt;p&gt;接下来我们看一个不太常见的代码。&lt;/p&gt;
    
    </summary>
    
      <category term="iOS" scheme="https://zesming.com/categories/iOS/"/>
    
    
      <category term="Objective-C" scheme="https://zesming.com/tags/Objective-C/"/>
    
      <category term="super" scheme="https://zesming.com/tags/super/"/>
    
      <category term="self" scheme="https://zesming.com/tags/self/"/>
    
      <category term="object" scheme="https://zesming.com/tags/object/"/>
    
  </entry>
  
  <entry>
    <title>利用重签名实现真机自动化测试</title>
    <link href="https://zesming.com/resign-uiautomation/"/>
    <id>https://zesming.com/resign-uiautomation/</id>
    <published>2016-07-17T03:40:00.000Z</published>
    <updated>2018-05-13T11:46:03.000Z</updated>
    
    <content type="html"><![CDATA[<p>不少 app 可能都有自动化测试的需求，尤其是规模起来后，大批量自动化测试可以减少很多机械化的人工测试成本，也能及时的查出一些比较关键的问题，可是基于 Xcode 的模拟器独占的特性，利用 Mac 集群测试不是一个比较合适的方案，另外通过 Debug 打出的包也不一定是和线上相一致的，所以利用 Release 包 + 多台 iOS 设备进行的自动化测试相对比较靠谱。</p><a id="more"></a><p>只不过，利用 Xcode 中 <code>Instruments</code> 的 <code>UIAutomation</code> 却有一个比较致命的限制：</p><blockquote class="blockquote-center"><p>The app must be signed with a development identity (e.g. iOS Developer).</p></blockquote><p>这就很尴尬了，怎么办？既然他说只能用 development 证书来签名，那我们第一个想到的可能就是重签。</p><h3 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h3><p>我们可以通过对 <code>.ipa</code> 分析来看看到底是哪里控制着 <code>Instruments</code> 的访问权限的</p><p>一般来说，App 的权限和设定都放在了包里面的描述文件中，也就是 <code>xxx.app</code> 里面的 <code>embedded.mobileprovision</code></p><p>果不其然，里面有一个 <code>get-task-allow</code> 的 key，它的值是 <code>false</code>，通过查证，果然是用于控制其他进程的访问权限的，并且正如上面返回的信息，这个值只有通过 development 证书来签名的时候才会是 <code>true</code>。</p><h3 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h3><p>其实步骤比较简单</p><ol><li>自己重新生成用于签名的 <code>Provisioning Profile</code>，如果 App 里面有多个 target，还需要一一对应生成多个，<code>Bundle ID</code> 一定要设置好并且有别于原来的，并且不能一样，<code>extension</code> 的一定要是所依附 App 下的 ID，例如主 ID 为 <code>com.ming.app</code>，那么 <code>extension</code> 一定要是 <code>com.ming.app.xxx</code></li><li>将 <code>Entitlements</code> 提取出来，并且用新的 PP 文件替换掉原有的</li><li>更改 App 的 <code>Bundle ID</code> 为刚刚生成的 PP 文件所对应的，依旧，如果有多个 target ，还是要一一对应的替换掉</li><li>去掉原有的签名，重新签名</li><li>打包，安装</li></ol><p>哈哈，是不是看完后感觉好抽象，有些同学已经打开了 Google 准备开始搜索具体怎么做了吧？</p><p>好在我们有个大杀器—— <a href="https://github.com/fastlane/fastlane/tree/master/sigh" target="_blank" rel="noopener">fastlane 下的 sigh 工具</a></p><p>这是鼎鼎大名的 iOS 开发工具链 Fastlane 旗下的一个小工具，用于管理开发者账号下面的证书，PP 以及修复 App 的 PP 文件，签名和重签名……总之就是个神器。我们在这里只用到它里面的 <a href="https://github.com/fastlane/fastlane/tree/master/sigh#resign" target="_blank" rel="noopener">resign</a></p><p>首先我们做下准备工作</p><ol><li>因为它是个基于 <code>Ruby</code> 的工具链，所以，你需要有个 <code>Ruby</code> 环境（Xcode 现在已经提供 <code>Ruby 2.0.0</code>，不过作为 iOS 开发，想必你已经有更高版本的 <code>Ruby</code> 了吧？）</li><li>安装它 <code>sudo gem install sigh</code> ，当然了，为了能正常的安装，你还要确保你的 <code>Xcode Command Line Tools</code> 已经安装好了：<code>xcode-select --install</code></li><li>然后将你准备重签名的 App 和你刚才生成的 PP 文件放到一起，然后执行 <code>sigh resign ./path/app.ipa --signing_identity &quot;iPhone Developer: XXX&quot; -p com.you.BundleID=my.mobileprovision</code> ，记着替换掉里面的信息为自己的，如果有多个 target ，只需要在后面追加 <code>-p</code> 就行了。</li><li>回车，搞定</li></ol><h3 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h3><p>生成 <code>.ipa</code> 后，就是要安装到手机上了，比较简单的方法就是通过 Xcode</p><p>连上 iOS 设备，打开 Xcode，在顶部菜单栏找到 <code>Window</code> 下面的 <code>Devices</code>，选中你的设备，会在右侧有个非 App Store 的 App 列表，下面有个 + 号，点击它就能安装</p><p><img src="/images/resign_1.png" alt="Xcode_install"></p><p>另外还有个通过 iTunes 的安装方式，大同小异，和正常的同步操作一致，就不赘述了</p><h3 id="自动化前的验证"><a href="#自动化前的验证" class="headerlink" title="自动化前的验证"></a>自动化前的验证</h3><p>安装完后可以到设置中的开发者里面看看 UIAutomation 是否打开了</p><p><img src="/images/resign_2.png" alt="Settings"></p><p>然后去启动下 App 看看能用么，iOS 9以上的系统还会让你去描述文件中认证下</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>这个不仅解决了 Release 包真机自动化测试的问题，也给我们提供了一个比较好的重签名思路</p><p>用这个工具还能做好多事情，好奇的同学可以去研究下</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;不少 app 可能都有自动化测试的需求，尤其是规模起来后，大批量自动化测试可以减少很多机械化的人工测试成本，也能及时的查出一些比较关键的问题，可是基于 Xcode 的模拟器独占的特性，利用 Mac 集群测试不是一个比较合适的方案，另外通过 Debug 打出的包也不一定是和线上相一致的，所以利用 Release 包 + 多台 iOS 设备进行的自动化测试相对比较靠谱。&lt;/p&gt;
    
    </summary>
    
      <category term="iOS" scheme="https://zesming.com/categories/iOS/"/>
    
    
      <category term="automation" scheme="https://zesming.com/tags/automation/"/>
    
      <category term="archive" scheme="https://zesming.com/tags/archive/"/>
    
      <category term="xcodebuild" scheme="https://zesming.com/tags/xcodebuild/"/>
    
      <category term="iOS" scheme="https://zesming.com/tags/iOS/"/>
    
      <category term="sign" scheme="https://zesming.com/tags/sign/"/>
    
      <category term="resign" scheme="https://zesming.com/tags/resign/"/>
    
      <category term="fastlane" scheme="https://zesming.com/tags/fastlane/"/>
    
      <category term="ipa" scheme="https://zesming.com/tags/ipa/"/>
    
  </entry>
  
  <entry>
    <title>美团 app 之 3D Touch 初探</title>
    <link href="https://zesming.com/3d-touch/"/>
    <id>https://zesming.com/3d-touch/</id>
    <published>2016-04-17T06:48:55.000Z</published>
    <updated>2018-05-19T03:56:12.551Z</updated>
    
    <content type="html"><![CDATA[<h3 id="3D-Touch"><a href="#3D-Touch" class="headerlink" title="3D Touch"></a>3D Touch</h3><p>在现在的 Multi-Touch 技术下，我们见的最多的就是点，滑和缩放操作，这些操作都是在二维的维度下。而 3D Touch 则是可以给操作提供了一个新的维度——可以感受用户按压的力度。</p><a id="more"></a><h3 id="适用场景"><a href="#适用场景" class="headerlink" title="适用场景"></a>适用场景</h3><p>目前官方提供了三个可以适用 3D Touch 的场景：</p><h4 id="Pressure-Sensitivity"><a href="#Pressure-Sensitivity" class="headerlink" title="Pressure Sensitivity"></a>Pressure Sensitivity</h4><p>可以用于绘画的 App，通过不同的力度来画出不同的线条。</p><p><img src="/images/3D_touch_1.jpg" alt="Pressure Sensitivity"></p><h4 id="Peek-and-Pop"><a href="#Peek-and-Pop" class="headerlink" title="Peek and Pop"></a>Peek and Pop</h4><p>一个快速预览的功能。</p><p><img src="/images/3D_touch_2.jpg" alt="Peek and Pop"></p><h4 id="Quick-Action"><a href="#Quick-Action" class="headerlink" title="Quick Action"></a>Quick Action</h4><p>主屏幕上的 App 快捷菜单，提供一个快接入口，简化一些操作。</p><p><img src="/images/3D_touch_3.jpg" alt="Quick Action"></p><p>目前来看，后两者更适合于我们的App。</p><p>关于更多 3D Touch 的介绍，详见：<a href="https://developer.apple.com/ios/3d-touch/" target="_blank" rel="noopener">https://developer.apple.com/ios/3d-touch/</a> 或者 <a href="http://www.apple.com/cn/iphone-6s/3d-touch/" target="_blank" rel="noopener">http://www.apple.com/cn/iphone-6s/3d-touch/</a></p><h3 id="静态的-Quick-Action"><a href="#静态的-Quick-Action" class="headerlink" title="静态的 Quick Action"></a>静态的 Quick Action</h3><p>考虑到适配工作和当前业务的使用情况，我们的 PM 首先选择了在主屏幕上添加一个 Quick Action 这样一个快捷菜单，嗯……如下图所示</p><p><img src="/images/3D_touch_4.jpg" alt="美团 Quick Action"></p><p>这是三个静态的菜单选项，何为静态的？就是从 App 安装后一直是长这个样子的，它是声明在 Info.plist 里面的 <a href="https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/iPhoneOSKeys.html#//apple_ref/doc/uid/TP40009252-SW36" target="_blank" rel="noopener"><strong>UIApplicationShortcutItems</strong></a> 数组中的。</p><p><img src="/images/3D_touch_5.png" alt="Info.plist"></p><p>我这里只用到了三个key：</p><ul><li><strong><code>UIApplicationShortcutItemType</code></strong>，有点像是这个 item 的标识符，它会通过 <code>launchOption</code> 传到 <code>appDelegate</code> 里面然后供开发者判断 app 是通过哪个 item 启动的，以便处理自己的逻辑</li><li><strong><code>UIApplicationShortcutItemTitle</code></strong>，顾名思义了，这个就是用于设置快捷菜单标题的</li><li><strong><code>UIApplicationShortcutItemIconFile</code></strong>，这个是用于设置快捷菜单的 icon 的，不过是使用工程中的图像文件来设置的，这是一个 35 * 35 点，单色的图片。</li></ul><p>其中前两个 key 是必须要声明的。这里还有其他的一堆 key 用于 item 的定制：</p><ul><li><strong><code>UIApplicationShortcutItemSubtitle</code></strong>，这是用来设置副标题的，不是必要的，如果设置了这个将会在标题下方第二行出现一行副标题。另外，如果没有副标题的话，标题过长会自动折行到第二行。</li><li><strong><code>UIApplicationShortcutItemIconType</code></strong>，这个是用来设置图标的，不过这个是使用系统提供的一堆图标，这个key其实是声明在 <a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationShortcutIcon_Class/index.html#//apple_ref/c/tdef/UIApplicationShortcutIconType" target="_blank" rel="noopener"><strong>UIApplicationShortcutItemIcon</strong></a> 中的一个枚举，想用的话可以从这里面找，不是很多。另外，如果你又通过 <code>UIApplicationShortcutItemIconFile</code> 来定制了自己的 icon，那么系统将会自动忽略这个 key 的设置而使用来自 file 的 icon 设置。</li><li><strong><code>UIApplicationShortcutItemUserInfo</code></strong>，苹果说这个可以利用这个 key 让开发者提供一些类似 app 版本信息的东西，用于 app 升级后但是还没有被用户启动，但是用户又通过 Quick Action 来启动了 app，可能会存在的问题。这个时候，这个 key 所提供的类似版本信息的东西就可能会派上用场。但是我还没有发现它的使用场景。</li></ul><p>当你在 <code>Info.plist</code> 里面设置好这些东西后，你就能在支持 3D Touch 的设备上通过用力按压 app 的 icon 呼出这个快捷菜单了，并且你还可以通过这个菜单来启动 app，只是没有做任何的处理，现在从这个菜单启动 app 和从 icon 启动没啥两样。接下来我们还要对从菜单启动 app 这个逻辑进行单独的处理。同样我们还是先说静态的菜单选项：</p><p>剩下的工作我们就要在 <code>appDelegate</code> 这里面来做了。首先我们要先认识下新增的一个回调：<a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/index.html#//apple_ref/occ/intfm/UIApplicationDelegate/application:performActionForShortcutItem:completionHandler:" target="_blank" rel="noopener"><strong>-application:performActionForShortcutItem:completionHandler:</strong></a></p><p>这个回调会在 app 通过快捷菜单启动或者从后台唤醒的时候被调用，但是，如果你在 <code>-application:didFinishLaunchingWithOptions:</code> 或者 <code>-application:willFinishLaunchingWithOptions:</code> 返回的是 <code>false</code>，这个回调就不会被调用。苹果官方给的 Demo 里面是这样写的：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">application</span><span class="params">(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?)</span></span> -&gt; <span class="type">Bool</span> &#123;</span><br><span class="line">    <span class="comment">// Override point for customization after application launch.</span></span><br><span class="line">    <span class="keyword">var</span> shouldPerformAdditionalDelegateHandling = <span class="literal">true</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// If a shortcut was launched, display its information and take the appropriate action</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">let</span> shortcutItem = launchOptions?[<span class="type">UIApplicationLaunchOptionsShortcutItemKey</span>] <span class="keyword">as</span>? <span class="type">UIApplicationShortcutItem</span> &#123;</span><br><span class="line">        launchedShortcutItem = shortcutItem</span><br><span class="line"></span><br><span class="line">        <span class="comment">// This will block "performActionForShortcutItem:completionHandler" from being called.</span></span><br><span class="line">        shouldPerformAdditionalDelegateHandling = <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Launch Code</span></span><br><span class="line">    ......</span><br><span class="line">    <span class="keyword">return</span> shouldPerformAdditionalDelegateHandling</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个代码的意思是，我在程序启动的时候看下是不是通过快捷菜单启动的，如果是的话，就不让 <code>performActionForShortcutItem:completionHandler</code> 这个回调被调用。具体的原因没有说，但是通过这个回调来看，会有影响程序启动的可能性，所以为了保证程序能够顺畅的启动，先把这里面传过来的 <code>UIApplicationShortcutItem</code> 存下来，在下面程序启动完后另外找个机会再处理接下来的逻辑，什么时候呢？</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">applicationDidBecomeActive</span><span class="params">(application: UIApplication)</span></span> &#123;</span><br><span class="line">    <span class="keyword">guard</span> <span class="keyword">let</span> shortcut = launchedShortcutItem <span class="keyword">else</span> &#123; <span class="keyword">return</span> &#125;</span><br><span class="line"></span><br><span class="line">    handleShortCutItem(shortcut)</span><br><span class="line"></span><br><span class="line">    launchedShortcutItem = <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>-applicationDidBecomeActive</code> 这个回调中，通过判断是否有 <code>launchedShortcutItem</code>，手动调起用于处理逻辑的方法。如果是从后台唤醒的话，就不需要这么麻烦了，直接通过系统正常调用 <code>performActionForShortcutItem:completionHandler</code> 这个回调就好啦：</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">application</span><span class="params">(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -&gt; Void)</span></span> &#123;</span><br><span class="line">    <span class="keyword">let</span> handledShortCutItem = handleShortCutItem(shortcutItem)</span><br><span class="line"></span><br><span class="line">    completionHandler(handledShortCutItem)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所以以防重复调用，这里在首次启动完后，需要在 <code>-applicationDidBecomeActive</code> 将这个手动存储的 property 置为 <code>nil</code>。</p><p>大体上一个快捷菜单的启动流程就是这样了。可是真正在我们的工程中可能还有一些问题：</p><ol><li>我们的启动很复杂，可能有各种启动画面</li><li>需要跳转好多界面啊，还需要各种参数，前置界面，bulabula…</li><li>需要登录怎么办？我们从哪个界面发起新页面的跳转？用户取消后返回到哪里？</li><li>……</li></ol><p>首先，通过快捷菜单启动 app 的话，我们自定义的启动画面会让这整个过程变得很复杂，因为系统的整个启动流程很快，但是可能真正到跳转方法被执行的时候，我们冗长的启动画面还没有结束，首页还没有创建，结果…… Push Failed！</p><p>为了保证整个跳转过程能够在启动画面完成，并且首页创建（或者说我们自己写的 <code>tabbarController</code> 创建好）后执行，我们就需要观察这个至关重要的 <code>tabbarController</code>：</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">void</span>)applicationDidBecomeActive:(<span class="built_in">UIApplication</span> *)application</span><br><span class="line">&#123;</span><br><span class="line"></span><br><span class="line">    ......</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ([[[<span class="built_in">UIDevice</span> currentDevice] systemVersion] compare:<span class="string">@"9.0"</span> options:<span class="built_in">NSNumericSearch</span>] != <span class="built_in">NSOrderedAscending</span>) &#123;</span><br><span class="line">        <span class="comment">// 用于处理来自 ShortcutItem 的唤醒动作，我们的 App 一开始有一大堆的界面广告之类的，调用早的话，tabbarController 还没创建，就没办法定位发动跳转的界面了</span></span><br><span class="line">        <span class="comment">// 所以还是稍等下，等 tabbarController 创建好了我们再继续处理跳转逻辑</span></span><br><span class="line">        <span class="comment">// 另外，这里仅仅处理首次启动时来自 ShortcutItem 的跳转逻辑，如果是从后台唤醒的话，请使用 -application:performActionForShortcutItem: completionHandler:这个回调</span></span><br><span class="line">        @weakify(<span class="keyword">self</span>);</span><br><span class="line">        [[RACObserve(<span class="keyword">self</span>, tabbarController) deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(MTGroupTabbarController *tabbarController) &#123;</span><br><span class="line">            @strongify(<span class="keyword">self</span>);</span><br><span class="line">            <span class="keyword">if</span> (<span class="keyword">self</span>.launchedShortcutItem &amp;&amp; tabbarController) &#123;</span><br><span class="line">                [<span class="keyword">self</span> handleShortCutItem:<span class="keyword">self</span>.launchedShortcutItem];</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 这里在首次启动完后，将这个 property 置 nil ，以防从后台唤醒的时候重复调用上面的方法</span></span><br><span class="line">                <span class="keyword">self</span>.launchedShortcutItem = <span class="literal">nil</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>至于 <code>-application:performActionForShortcutItem:completionHandler:</code> 这个回调，就比较简单了，教科书般的写法就好了：</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="comment">// 这个回调会在程序通过ShortcutItem启动的被调用</span></span><br><span class="line"><span class="comment">// 但是如果 -application:didFinishLaunchingWithOptions:</span></span><br><span class="line"><span class="comment">// 或者 -application:willFinishLaunchingWithOptions: 返回的是NO，这个回调就不会被调用。</span></span><br><span class="line"><span class="comment">// 如果程序通过 ShortcutItem 启动的话，为了不影响 app 的正常启动</span></span><br><span class="line"><span class="comment">// 在上面说的那两个回调中存一下 ShortcutItem 后返回 NO，然后在 -applicationDidBecomeActive: 中处理跳转逻辑</span></span><br><span class="line"><span class="comment">// 正常情况下建议这个回调是用于程序通过 ShortcutItem 从后台被唤醒的时候调用</span></span><br><span class="line">- (<span class="keyword">void</span>)application:(<span class="built_in">UIApplication</span> *)application performActionForShortcutItem:(<span class="built_in">UIApplicationShortcutItem</span> *)shortcutItem completionHandler:(<span class="keyword">void</span> (^)(<span class="built_in">BOOL</span>))completionHandler</span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">BOOL</span> handledShortcutItem = [<span class="keyword">self</span> handleShortCutItem:shortcutItem];</span><br><span class="line">    completionHandler(handledShortcutItem);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当然了，为了不影响我们正常的启动（尽管已经很慢了），我们还需要在 <code>-application:didFinishLaunchingWithOptions:</code> 里面做一下处理：</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></pre></td><td class="code"><pre><span class="line">- (<span class="built_in">BOOL</span>)application:(<span class="built_in">UIApplication</span> *)application didFinishLaunchingWithOptions:(<span class="built_in">NSDictionary</span> *)launchOptions</span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">BOOL</span> shouldPerformAdditionalDelegateHandling = <span class="literal">YES</span>;</span><br><span class="line"></span><br><span class="line">    ......</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ([[[<span class="built_in">UIDevice</span> currentDevice] systemVersion] compare:<span class="string">@"9.0"</span> options:<span class="built_in">NSNumericSearch</span>] != <span class="built_in">NSOrderedAscending</span>) &#123;</span><br><span class="line">        <span class="comment">// 判断是否是从 ShortcutItem 启动的 app</span></span><br><span class="line">        <span class="keyword">if</span> ([launchOptions[<span class="built_in">UIApplicationLaunchOptionsShortcutItemKey</span>] isKindOfClass:[<span class="built_in">UIApplicationShortcutItem</span> <span class="keyword">class</span>]]) &#123;</span><br><span class="line">            <span class="comment">// 将用于启动的 ShortcutItem 存下来后面用</span></span><br><span class="line">            <span class="keyword">self</span>.launchedShortcutItem = launchOptions[<span class="built_in">UIApplicationLaunchOptionsShortcutItemKey</span>];</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 为了不再调用 -application:performActionForShortcutItem:completionHandler:</span></span><br><span class="line">            shouldPerformAdditionalDelegateHandling = <span class="literal">NO</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> shouldPerformAdditionalDelegateHandling;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最重要的还是处理来自快捷菜单的启动（或唤醒）逻辑，说白了这个快捷菜单就是个快捷入口，处理起来也很简单，就是个跳转。例如用户选择了『美团券』，app 启动完后就需要进入到美团券列表页。有了上面的 <code>tabbarController</code> 的保证，我们就能够利用这个东西做一些事情：</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></pre></td><td class="code"><pre><span class="line">......</span><br><span class="line"></span><br><span class="line"><span class="comment">// 切换到首页</span></span><br><span class="line">MTGroupTabbarController *tabBarController = <span class="keyword">self</span>.tabbarController;</span><br><span class="line">tabBarController.selectedIndex = <span class="number">0</span>;</span><br><span class="line">MTNavigationController *destinationViewController = (MTNavigationController *)tabBarController.selectedViewController;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 做一个 popToRootViewController 的操作</span></span><br><span class="line"><span class="built_in">UIViewController</span> *homePageViewController = [destinationViewController.viewControllers firstObject];</span><br><span class="line">homePageViewController.navigationController.viewControllers = @[homePageViewController];</span><br><span class="line"></span><br><span class="line">......</span><br></pre></td></tr></table></figure><p>发起跳转前，先做个 tab 的切换，保证是通过首页跳转的。</p><p>然后我们就要判断传入的快捷菜单选项是什么了，这里就用到了上面提到的 <code>UIApplicationShortcutItemType</code> 这个字段来判断了：</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></pre></td><td class="code"><pre><span class="line">......</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> ([shortcutItem.type isEqualToString:<span class="string">@"Search"</span>]) &#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>跳转这一块我们有现成的基于 URL 跳转的机制，能够实现通过 URL 跳转到指定的 <code>viewController</code>，我们就利用自己的库来处理。在这里就不过多叙述。</p><p>如果处理完后，还要告诉外面通过快捷菜单启动的逻辑完成了，就需要个 <code>boolean</code>，所以整个方法大致这个样子：</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><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 以下 string 声明在 Info.plist 中 UIApplicationShortcutItems 内</span></span><br><span class="line"><span class="keyword">static</span> <span class="built_in">NSString</span> *<span class="keyword">const</span> METShortcutSearchItemType = <span class="string">@"Search"</span>;</span><br><span class="line"><span class="keyword">static</span> <span class="built_in">NSString</span> *<span class="keyword">const</span> METShortcutCouponItemType = <span class="string">@"Coupon"</span>;</span><br><span class="line"><span class="keyword">static</span> <span class="built_in">NSString</span> *<span class="keyword">const</span> METShortcutScanItemType = <span class="string">@"Scan"</span>;</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"><span class="comment">// 处理 Quick Action 的跳转操作的方法</span></span><br><span class="line">- (<span class="built_in">BOOL</span>)handleShortCutItem:(<span class="built_in">UIApplicationShortcutItem</span> *)shortcutItem</span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">BOOL</span> handled = <span class="literal">NO</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!shortcutItem) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NO</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">    MTGroupTabbarController *tabBarController = <span class="keyword">self</span>.tabbarController;</span><br><span class="line">    tabBarController.selectedIndex = <span class="number">0</span>;</span><br><span class="line">    MTNavigationController *destinationViewController = (MTNavigationController *)tabBarController.selectedViewController;</span><br><span class="line">    <span class="built_in">UIViewController</span> *homePageViewController = destinationViewController.topViewController;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ([shortcutItem.type isEqualToString:METShortcutSearchItemType]) &#123;</span><br><span class="line">        handled = <span class="literal">YES</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><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> ([shortcutItem.type isEqualToString:METShortcutCouponItemType]) &#123;</span><br><span class="line">        handled = <span class="literal">YES</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><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> ([shortcutItem.type isEqualToString:METShortcutScanItemType]) &#123;</span><br><span class="line">        handled = <span class="literal">YES</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><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// do nothing</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> handled;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>一个静态的快捷菜单启动的流程差不多就是这个样子了。</p><h3 id="动态的-Quick-Action"><a href="#动态的-Quick-Action" class="headerlink" title="动态的 Quick Action"></a>动态的 Quick Action</h3><p>静态的快捷菜单在 app 被安装后就会创建，哪怕你一次都没有启动过。</p><p>看起来是没问题的，但是我们的 app 在用户首次启动的时候会有漫长的引导界面还有一个城市选择界面，如果用户通过快捷菜单首次启动 app 的话，就会和引导界面还有城市选择界面冲突，并且实际上用户还没有启动 app 就展现这三个功能性的快捷菜单逻辑上也有点欠妥。</p><p>参考 EverNote 等一些 app 的做法之后，决定不用静态的快捷菜单，而是等到用户真正启动过一次 app 之后再动态生成，那就需要用到动态生成的快捷菜单。</p><p>具体判断逻辑跳转方法都不需要调整，唯一需要做的就是把声明在 <code>Info.plist</code> 的菜单项删掉，改成在 <code>-application:didFinishLaunchingWithOptions:</code> 里面动态生成：</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><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></pre></td><td class="code"><pre><span class="line">- (<span class="built_in">BOOL</span>)application:(<span class="built_in">UIApplication</span> *)application didFinishLaunchingWithOptions:(<span class="built_in">NSDictionary</span> *)launchOptions &#123;</span><br><span class="line">    <span class="comment">// Override point for customization after application launch.</span></span><br><span class="line"></span><br><span class="line">    <span class="built_in">BOOL</span> shouldPerformAdditionalDelegateHandling = <span class="literal">YES</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 判断是否是从 ShortcutItem 启动的 app</span></span><br><span class="line">    <span class="keyword">if</span> ([launchOptions[<span class="built_in">UIApplicationLaunchOptionsShortcutItemKey</span>] isKindOfClass:[<span class="built_in">UIApplicationShortcutItem</span> <span class="keyword">class</span>]]) &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 将用于启动的 ShortcutItem 存下来后面用</span></span><br><span class="line">        <span class="keyword">self</span>.launchedShortcutItem = launchOptions[<span class="built_in">UIApplicationLaunchOptionsShortcutItemKey</span>];</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 为了不再调用 -application:performActionForShortcutItem:completionHandler:</span></span><br><span class="line">        shouldPerformAdditionalDelegateHandling = <span class="literal">NO</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (application.shortcutItems.count == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="built_in">UIMutableApplicationShortcutItem</span> *searchItem = [[<span class="built_in">UIMutableApplicationShortcutItem</span> alloc] initWithType:<span class="string">@"Search"</span> localizedTitle:<span class="string">@"搜索"</span> localizedSubtitle:<span class="literal">nil</span> icon:[<span class="built_in">UIApplicationShortcutIcon</span> iconWithTemplateImageName:<span class="string">@"icon_shortcut_search"</span>] userInfo:<span class="literal">nil</span>];</span><br><span class="line"></span><br><span class="line">        <span class="built_in">UIMutableApplicationShortcutItem</span> *couponItem = [[<span class="built_in">UIMutableApplicationShortcutItem</span> alloc] initWithType:<span class="string">@"Coupon"</span> localizedTitle:<span class="string">@"美团券"</span> localizedSubtitle:<span class="literal">nil</span> icon:[<span class="built_in">UIApplicationShortcutIcon</span> iconWithTemplateImageName:<span class="string">@"icon_shortcut_coupon"</span>] userInfo:<span class="literal">nil</span>];</span><br><span class="line"></span><br><span class="line">        <span class="built_in">UIMutableApplicationShortcutItem</span> *scanItem = [[<span class="built_in">UIMutableApplicationShortcutItem</span> alloc] initWithType:<span class="string">@"Scan"</span> localizedTitle:<span class="string">@"扫一扫"</span> localizedSubtitle:<span class="literal">nil</span> icon:[<span class="built_in">UIApplicationShortcutIcon</span> iconWithTemplateImageName:<span class="string">@"icon_shortcut_scan"</span>] userInfo:<span class="literal">nil</span>];</span><br><span class="line"></span><br><span class="line">        application.shortcutItems = @[searchItem, couponItem, scanItem];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> shouldPerformAdditionalDelegateHandling;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里面利用到了 <a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationShortcutItem_class/index.html#//apple_ref/occ/cl/UIApplicationShortcutItem" target="_blank" rel="noopener"><strong>UIApplicationShortcutItem</strong></a> 这个类，生成 <code>UIMutableApplicationShortcutItem</code> 并添加到 <code>UIApplication</code> 里面的 <code>shortcutItems</code> 这个属性中。可以看一下这个指定初始化器：</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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">instancetype</span>)initWithType:(<span class="built_in">NSString</span> *)type</span><br><span class="line">              localizedTitle:(<span class="built_in">NSString</span> *)localizedTitle</span><br><span class="line">           localizedSubtitle:(<span class="keyword">nullable</span> <span class="built_in">NSString</span> *)localizedSubtitle</span><br><span class="line">                        icon:(<span class="keyword">nullable</span> <span class="built_in">UIApplicationShortcutIcon</span> *)icon</span><br><span class="line">                    userInfo:(<span class="keyword">nullable</span> <span class="built_in">NSDictionary</span> *)userInfo <span class="built_in">NS_DESIGNATED_INITIALIZER</span>;</span><br></pre></td></tr></table></figure><p>从上面的参数来看，和 <code>Info.plist</code> 里面设置的一样， <code>type</code> 和 <code>title</code> 是必须的，剩下的都是可选择的。</p><p>这些设置好后，直接赋值到 <code>shortcutItems</code> 这个属性中就好了。</p><p>小坑：为了防止有些用户没有完成首次启动 app 的城市选择设置，就把 app 杀掉，下次进来还会有可能冲突，索性我们就等用户完成所有的首次启动设置后，再创建这个菜单。</p><p>由于在我们的 app 中，如果没有选择城市，<code>userDefaults</code> 里面就不会存储城市信息，那就监听用于存储城市的 <code>userDefaults</code> 里面的 <code>Dictionary</code> 好了：</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></pre></td><td class="code"><pre><span class="line">......</span><br><span class="line"></span><br><span class="line">[[[[<span class="built_in">NSUserDefaults</span> standardUserDefaults] rac_channelTerminalForKey:kMTUserDefaultCityKey] deliverOn:[RACScheduler scheduler]] subscribeNext:^(<span class="built_in">NSDictionary</span> *cityInfo) &#123;</span><br><span class="line">    <span class="keyword">if</span> (cityInfo) &#123;</span><br><span class="line">        <span class="built_in">UIMutableApplicationShortcutItem</span> *searchItem = [[<span class="built_in">UIMutableApplicationShortcutItem</span> alloc] initWithType:METShortcutSearchItemType localizedTitle:<span class="string">@"搜索"</span> localizedSubtitle:<span class="literal">nil</span> icon:[<span class="built_in">UIApplicationShortcutIcon</span> iconWithTemplateImageName:<span class="string">@"icon_shortcut_search"</span>] userInfo:<span class="literal">nil</span>];</span><br><span class="line"></span><br><span class="line">        <span class="built_in">UIMutableApplicationShortcutItem</span> *couponItem = [[<span class="built_in">UIMutableApplicationShortcutItem</span> alloc] initWithType:METShortcutCouponItemType localizedTitle:<span class="string">@"美团券"</span> localizedSubtitle:<span class="literal">nil</span> icon:[<span class="built_in">UIApplicationShortcutIcon</span> iconWithTemplateImageName:<span class="string">@"icon_shortcut_coupon"</span>] userInfo:<span class="literal">nil</span>];</span><br><span class="line"></span><br><span class="line">        <span class="built_in">UIMutableApplicationShortcutItem</span> *scanItem = [[<span class="built_in">UIMutableApplicationShortcutItem</span> alloc] initWithType:METShortcutScanItemType localizedTitle:<span class="string">@"扫一扫"</span> localizedSubtitle:<span class="literal">nil</span> icon:[<span class="built_in">UIApplicationShortcutIcon</span> iconWithTemplateImageName:<span class="string">@"icon_shortcut_scan"</span>] userInfo:<span class="literal">nil</span>];</span><br><span class="line"></span><br><span class="line">        application.shortcutItems = @[searchItem, couponItem, scanItem];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;];</span><br><span class="line"></span><br><span class="line">......</span><br></pre></td></tr></table></figure><p>这样，一个动态生成的 Quick Action 菜单就生成了。</p><p>我们上面提到了，每一个 <code>item</code> 的 <code>type</code> 就像是一个唯一标示符，我们还可以利用这个 <code>type</code> 动态改变这个 <code>item</code> 的内容。然后再设置给这个 <code>shortcutItems</code> 这个属性就好了。<br>动态的 <code>item</code> 差不多就是这个样子了。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>其实如果我们的 app 中并没有这么多的启动逻辑，用静态的 Quick Action 应该是最佳选择，而如果说非要用静态实现，也到不难，在处理接受到 Quick Action 事件的回调中，对 <code>userDefaults</code> 进行监听，如果首次启动完成了我们再调跳转方法。</p><p>动态的 Quick Action 则给我们提供了很多可能，例如动态配置一个活动入口啥的。</p><p>随着支持 3D Touch 的设备越来越多，苹果也应该会给 3D Touch 加入更多的可操作性，到那时候，我们可能还会有更多针对 3D Touch 的探索和适配，这只是个开始。</p>]]></content>
    
    <summary type="html">
    
      &lt;h3 id=&quot;3D-Touch&quot;&gt;&lt;a href=&quot;#3D-Touch&quot; class=&quot;headerlink&quot; title=&quot;3D Touch&quot;&gt;&lt;/a&gt;3D Touch&lt;/h3&gt;&lt;p&gt;在现在的 Multi-Touch 技术下，我们见的最多的就是点，滑和缩放操作，这些操作都是在二维的维度下。而 3D Touch 则是可以给操作提供了一个新的维度——可以感受用户按压的力度。&lt;/p&gt;
    
    </summary>
    
      <category term="iOS" scheme="https://zesming.com/categories/iOS/"/>
    
    
      <category term="美团" scheme="https://zesming.com/tags/%E7%BE%8E%E5%9B%A2/"/>
    
      <category term="3D Touch" scheme="https://zesming.com/tags/3D-Touch/"/>
    
      <category term="Force Touch" scheme="https://zesming.com/tags/Force-Touch/"/>
    
  </entry>
  
  <entry>
    <title>利用 Build Configuration 来进行一些定制化的配置</title>
    <link href="https://zesming.com/build-configuration/"/>
    <id>https://zesming.com/build-configuration/</id>
    <published>2016-04-17T05:19:38.000Z</published>
    <updated>2018-05-13T11:41:01.000Z</updated>
    
    <content type="html"><![CDATA[<p>一般来说，大厂的 App 有两个证书进行签名打包，除了 App Store 上架用的，还有个用于内部测试，用的是企业证书。为了和正式包加以区分，与之相对应就会有两种不同的 App IDs 。通常来说，可能直接点的手法就是在打包之前手动更新 <code>Info.plist</code> 文件里的 <code>Bundle Identifier</code> 。</p><p>这样的做法可行，但是由于需要在打包的 Job 中进行更改，会有很多修改语句，如果 app 是一个有很多 target 的庞大工程，那么在打包脚本中就会出现这样的现象：</p><a id="more"></a><p><img src="/images/build_configuration_1.png" alt="打包脚本"></p><p>如果想改动，那就会是一件非常蛋疼的事情……</p><h3 id="利用-Build-Configuration-做统一配置"><a href="#利用-Build-Configuration-做统一配置" class="headerlink" title="利用 Build Configuration 做统一配置"></a>利用 Build Configuration 做统一配置</h3><p>在 Xcode 的 <code>Build Settings -&gt; Packing</code> 中，有个 <code>Product Bundle Identifier</code> 的设置项，展开后有一些以 <code>Build Configuration</code> 为 key 的子项：</p><p><img src="/images/build_configuration_2.png" alt="build settings"></p><p>将我们不同 Configuration 下的 Bundle ID 设置好，不过还需要在 <code>Info.plist</code> 里面 <code>CFBundleIdentifier</code>（<code>Bundle Identifier</code>） 改成用这个参数：</p><p><img src="/images/build_configuration_3.png" alt="info.plist"></p><p>这样在 <code>xcodebuild</code> 时，会根据 <code>-configuration</code> 这个参数选择对应的 ID。</p><p>如果有别的 ID 也需要变动呢？这一个参数恐怕就不够用了，那就用 <code>User-Defined Setting</code> 吧。</p><p>在 <code>Build Settings</code> 上，点 <code>+</code> 号，<code>Add User-Defined Setting</code>：</p><p><img src="/images/build_configuration_4.png" alt="uiser_defined_settings"></p><p>然后就会在下方的 <code>User-Defined</code> 多出一个空的设置项，展开后和 <code>Product Bundle Identifier</code> 一样，有一堆以 <code>Build Configuration</code> 为 key 的子项，剩下的事情参考上面的步骤，就是把你想设置上的 ID 设置好，更新到你想要的用到的地方。</p><p>不一定是 Bundle ID，其他的一些打包配置也可以通过这个进行设置。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;一般来说，大厂的 App 有两个证书进行签名打包，除了 App Store 上架用的，还有个用于内部测试，用的是企业证书。为了和正式包加以区分，与之相对应就会有两种不同的 App IDs 。通常来说，可能直接点的手法就是在打包之前手动更新 &lt;code&gt;Info.plist&lt;/code&gt; 文件里的 &lt;code&gt;Bundle Identifier&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;这样的做法可行，但是由于需要在打包的 Job 中进行更改，会有很多修改语句，如果 app 是一个有很多 target 的庞大工程，那么在打包脚本中就会出现这样的现象：&lt;/p&gt;
    
    </summary>
    
      <category term="iOS" scheme="https://zesming.com/categories/iOS/"/>
    
    
      <category term="Xcode" scheme="https://zesming.com/tags/Xcode/"/>
    
      <category term="build configuration" scheme="https://zesming.com/tags/build-configuration/"/>
    
      <category term="archive" scheme="https://zesming.com/tags/archive/"/>
    
  </entry>
  
  <entry>
    <title>用 Ansible 玩转批量运维</title>
    <link href="https://zesming.com/ansible-playbook/"/>
    <id>https://zesming.com/ansible-playbook/</id>
    <published>2016-03-26T05:45:18.000Z</published>
    <updated>2018-05-19T03:55:44.650Z</updated>
    
    <content type="html"><![CDATA[<p>Ansible 是个什么东西呢？官方的 title 是 “Ansible is Simple IT Automation” ——简单的自动化IT工具。这个工具的目标有这么几项：让我们自动化部署 APP；自动化管理配置项；自动化的持续交付；自动化的（AWS）云服务管理。</p><p>所有的这几个目标本质上来说都是在一个台或者几台服务器上，执行一系列的命令而已。</p><p>Ansible 基于 paramiko 开发的。这个 paramiko 是什么呢？它是一个纯 Python 实现的 ssh 协议库。因此 ansible 还有一个特点就是不需要在远程主机上安装 client/agents，因为它们是基于 ssh来和远程主机通讯的。</p><a id="more"></a><h3 id="用-Ansible-的必要性"><a href="#用-Ansible-的必要性" class="headerlink" title="用 Ansible 的必要性"></a>用 Ansible 的必要性</h3><p>假如目前我们已经有超过 10 台的 Jenkins slave 机器，但是 slave 的环境配置却是需要统一，所以想要维护多台 slaves 就是一项需要做很多重复性机械性工作的体力活。</p><p>既然 Ansible 是一个能够在多台目标机器上批量执行命令的工具，那么用它来统一维护 slaves 无疑是一个省时省力的方式。</p><h3 id="怎么安装呢？"><a href="#怎么安装呢？" class="headerlink" title="怎么安装呢？"></a>怎么安装呢？</h3><p>既然它是 Python 实现的 ssh 协议库，那么就用 Python package Manager——pip 进行安装就好了，如果没有装过 pip，那就先需要安装这个</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo easy_install pip</span><br></pre></td></tr></table></figure><p>安装 pip 后，我们就能安装 Ansible 了</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo pip install ansible</span><br></pre></td></tr></table></figure><p>官方还特别温馨的提示了这句话</p><blockquote class="blockquote-center"><p>If you are installing on OS X Mavericks, you may encounter some noise from your compiler. A workaround is to do the following:<br>$ sudo CFLAGS=-Qunused-arguments CPPFLAGS=-Qunused-arguments pip install ansible</p></blockquote><p>更多详细内容，可以参阅 <a href="http://docs.ansible.com/ansible/intro_installation.html" target="_blank" rel="noopener">官方文档</a></p><p>如果在 OS X El Capitan 下安装 <code>pycrypto</code> 遇到</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">configure: error: C compiler cannot create executables</span><br></pre></td></tr></table></figure><p>请参考此篇文章：<a href="http://pod.hatenablog.com/entry/2016/01/06/153144" target="_blank" rel="noopener">El Captainでmacportsでinstallしたpython3.4上でpycryptoのinstallに失敗する場合</a></p><p>需要注意的一点，因为这是通过 ssh 来实现的，所使用的 remote_user 要能够 ssh 无密码登录到所有机器，so，我们可能还需要把想要操作的 slave 的机器（一般是本机）的公钥传到 slave 上，这个比较简单：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ssh-copy-id [user@]hostname</span><br></pre></td></tr></table></figure><h3 id="快速上手"><a href="#快速上手" class="headerlink" title="快速上手"></a>快速上手</h3><p>ansible 执行的时候会按照以下顺序查找配置项:</p><figure class="highlight bash"><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">* ANSIBLE_CONFIG <span class="comment"># 环境变量</span></span><br><span class="line">* ansible.cfg <span class="comment"># 当前目录下</span></span><br><span class="line">* .ansible.cfg <span class="comment"># 用户家目录下</span></span><br><span class="line">* /etc/ansible/ansible.cfg</span><br></pre></td></tr></table></figure><p>还有一个重要的配置是 hosts 的配置，所有的远程主机需要在 hosts 中配置，可以分组。当然 hosts 也可以执行是指定。先来一个简单的例子，目录下新建一个 hosts 文件:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># hosts</span></span><br><span class="line">[slave]</span><br><span class="line">slave1.ci.ios.com ansible_ssh_user=jenkins</span><br></pre></td></tr></table></figure><p>然后在终端执行</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ansible -i ./hosts all -a <span class="string">'who'</span></span><br></pre></td></tr></table></figure><p>你就会得到如下的反馈</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 输出结果</span></span><br><span class="line">slave1.ci.ios.com | success | rc=0 &gt;&gt;</span><br><span class="line">jenkins  console  Sep  6 13:10</span><br><span class="line">jenkins  ttys000  Sep  8 14:51  (你的 IP)</span><br></pre></td></tr></table></figure><p>这是一条 <code>ad-hoc</code> 命令，也就是临时执行命令，<code>ad-hoc</code> 是 ansible 里的一个概念, 在上面命令中就是 <code>-a</code> ，下面会提到。命令中的 <code>all</code> 是值 <code>host</code> 中的所有服务器，当然也可以通过 <code>$ ansible -i ~/hosts slave -a &#39;who&#39;</code> 这样根据组名指定服务器。</p><p>再说到 <code>ansible.cfg</code> 的配置，默认 ansible 执行时会从该配置中加载 hosts 配置，因此可以通过修改 <code>.ansible.cfg</code> 来指定默认的 hosts 文件地址：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># .ansible.cfg</span></span><br><span class="line">[defaults]</span><br><span class="line">hostfile=/Projects/jenkins-ansible/hosts <span class="comment"># 这就是我hosts文件的路径</span></span><br></pre></td></tr></table></figure><p>然后再执行的话，就不需要-i命令了</p><h3 id="ad-hoc-命令"><a href="#ad-hoc-命令" class="headerlink" title="ad-hoc 命令"></a>ad-hoc 命令</h3><p>上面你看到的就是个简单的 <code>ad-hoc</code> 命令，当你想临时执行一个小命令的时候，例如看下 slave 上的 IP ，重启一下， ping 一下 slave 是否能 pong 回来（打个乒乓），这个命令就最适合啦。</p><p>那么这个 <code>ad-hoc</code> 命令怎么用呢？上面已经简单的示范了下。在 ansible 中还有一个 Module（模块）的概念，这个模块可以理解为一个库，所有的命令都需要通过模块来执行，比如上面的那个命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ansible -i ./hosts all -a <span class="string">'who'</span></span><br></pre></td></tr></table></figure><p>其实是调用了默认的 <code>command</code> 模块</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ansible -i ./hosts all -m <span class="built_in">command</span> -a <span class="string">'who'</span></span><br></pre></td></tr></table></figure><p>除了 <code>command</code> 模块还有其他很多模块，比如就像刚才我说的你就想 ping 下这个 slave 是不是还联通可以通过 <code>ping</code> 模块：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ansible -i ./hosts all -m ping</span><br></pre></td></tr></table></figure><p>然后就会看到……</p><figure class="highlight bash"><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">slave1.ci.ios.com | success &gt;&gt; &#123;</span><br><span class="line">    <span class="string">"changed"</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="string">"ping"</span>: <span class="string">"pong"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其实还有以下几个有用的参数</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">-u username  <span class="comment"># 指定ssh连接的用户名</span></span><br><span class="line">-f 10        <span class="comment"># 指定并发数</span></span><br><span class="line">--sudo [-K]  <span class="comment"># 如果需要root权限执行的话，-K参数是用来输入root密码的</span></span><br></pre></td></tr></table></figure><p>你可以通过各种模块来批量完成某个包的安装，或者其他什么需要的操作。 更多模块可以看 <a href="http://docs.ansible.com/ansible/modules.html" target="_blank" rel="noopener">官网文档</a>。</p><h3 id="一次要执行很多任务？Playbook出场！"><a href="#一次要执行很多任务？Playbook出场！" class="headerlink" title="一次要执行很多任务？Playbook出场！"></a>一次要执行很多任务？Playbook出场！</h3><p>如果你想装一个包，配置N多个环境，升级下库，拉一下代码等众多任务，那么一条一条 <code>ad-hoc</code> 命令能把你累死，怎么办？</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ansible-playbook</span><br></pre></td></tr></table></figure><p>playbook，顾名思义，就是个剧本，你写好，它们照着演。先写个简单的剧情让它们试试镜？</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># test.yml</span></span><br><span class="line"><span class="attr">- hosts:</span> <span class="string">slave</span></span><br><span class="line"><span class="attr">  user:</span> <span class="string">jenkins</span></span><br><span class="line"><span class="attr">  tasks:</span></span><br><span class="line"><span class="attr">    - name:</span> <span class="string">update</span> <span class="string">homebrew</span></span><br><span class="line"><span class="attr">      homebrew:</span> <span class="string">update_homebrew=yes</span></span><br></pre></td></tr></table></figure><p>上面的剧情就是让 slave 升级下 homebrew ，剧本写的好奇怪是么？那可能是你第一次见到 <code>YAML</code> 格式的文件，啥是 <code>YAML</code> ？<a href="https://zh.wikipedia.org/wiki/YAML" target="_blank" rel="noopener">戳这里！</a></p><p>简单解释下上面的 playbook ，hosts 后面根据 slave 是从 hosts 中读取的，<code>tasks</code> 是是关键词，指明了要执行哪些任务；下面的 <code>name</code> 是任务的名称，<code>homebrew</code> 是前面提到的 <code>module</code>(模块)，后面跟的是命令。（点我看下 <a href="http://docs.ansible.com/ansible/homebrew_module.html" target="_blank" rel="noopener">官方文档</a> 理解更清楚）</p><p>然后，保存下，执行</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ansible-playbook -i ./hosts test.yml</span><br></pre></td></tr></table></figure><p>哎呀，好戏开场了！</p><figure class="highlight bash"><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">PLAY [slave] ******************************************************************</span><br><span class="line"></span><br><span class="line">GATHERING FACTS ***************************************************************</span><br><span class="line">ok: [slave1.ci.ios.com]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">TASK: [allow debugging] *******************************************************</span><br><span class="line">changed: [slave1.ci.ios.com]</span><br><span class="line"></span><br><span class="line">PLAY RECAP ********************************************************************</span><br><span class="line">slave1.ci.ios.com : ok=2    changed=1    unreachable=0    failed=0</span><br></pre></td></tr></table></figure><p>这就是个简单的 playbook ，结合 <code>hosts</code> 和 <code>Module</code> ，可以演绎出各种惊天动地的凄美爱情剧目（play），有关详细的 playbook的官方介绍，可以 <a href="http://docs.ansible.com/ansible/playbooks.html" target="_blank" rel="noopener">戳这里</a></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Ansible 是个什么东西呢？官方的 title 是 “Ansible is Simple IT Automation” ——简单的自动化IT工具。这个工具的目标有这么几项：让我们自动化部署 APP；自动化管理配置项；自动化的持续交付；自动化的（AWS）云服务管理。&lt;/p&gt;
&lt;p&gt;所有的这几个目标本质上来说都是在一个台或者几台服务器上，执行一系列的命令而已。&lt;/p&gt;
&lt;p&gt;Ansible 基于 paramiko 开发的。这个 paramiko 是什么呢？它是一个纯 Python 实现的 ssh 协议库。因此 ansible 还有一个特点就是不需要在远程主机上安装 client/agents，因为它们是基于 ssh来和远程主机通讯的。&lt;/p&gt;
    
    </summary>
    
      <category term="Linux" scheme="https://zesming.com/categories/Linux/"/>
    
      <category term="macOS" scheme="https://zesming.com/categories/macOS/"/>
    
    
      <category term="automation" scheme="https://zesming.com/tags/automation/"/>
    
      <category term="OM" scheme="https://zesming.com/tags/OM/"/>
    
      <category term="ansible" scheme="https://zesming.com/tags/ansible/"/>
    
  </entry>
  
  <entry>
    <title>简单的 iOS 性能监测工具</title>
    <link href="https://zesming.com/ios-performance-tools/"/>
    <id>https://zesming.com/ios-performance-tools/</id>
    <published>2016-03-26T03:46:27.000Z</published>
    <updated>2018-05-19T03:55:05.283Z</updated>
    
    <content type="html"><![CDATA[<p>由于现在iOS这一块缺少性能测试，导致 app 性能优化方面工作并没有明确的工作目标。所以需要个必要的测试工具来数据化 app 的性能状况。</p><p>需要一个工具可以检测 app 的内存占用，CPU使用情况，FPS还有启动时间（可以通过 <code>UIAutomation</code> 来实现）<br>由于需要检测生产环境下各种友商 app，用于对比性能，所以决定用越狱后tweak来实现性能的检测。</p><a id="more"></a><h3 id="Tweak-的一些实现"><a href="#Tweak-的一些实现" class="headerlink" title="Tweak 的一些实现"></a>Tweak 的一些实现</h3><p>（示例代码：<a href="https://github.com/zesming/SysInfoRecorder" target="_blank" rel="noopener">Github</a>）</p><p>tweak的主要功能是检测app启动后每一秒的内存使用量，CPU使用率，FPS。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// get current RAM usage</span></span><br><span class="line"><span class="keyword">double</span> usedMemory()</span><br><span class="line">&#123;</span><br><span class="line">  task_basic_info_data_t taskInfo;</span><br><span class="line">  mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT;</span><br><span class="line">  kern_return_t kernReturn = task_info(mach_task_self(),</span><br><span class="line">                                       TASK_BASIC_INFO, (task_info_t)&amp;taskInfo, &amp;infoCount);</span><br><span class="line">  <span class="keyword">if</span>(kernReturn != KERN_SUCCESS) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="built_in">NSNotFound</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> taskInfo.resident_size / <span class="number">1024.0</span> / <span class="number">1024.0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>CPU使用率检测：</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><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// get current CPU usage</span></span><br><span class="line"><span class="keyword">float</span> cpu_usage()</span><br><span class="line">&#123;</span><br><span class="line">  kern_return_t kr;</span><br><span class="line">  task_info_data_t tinfo;</span><br><span class="line">  mach_msg_type_number_t task_info_count;</span><br><span class="line"></span><br><span class="line">  task_info_count = TASK_INFO_MAX;</span><br><span class="line">  kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)tinfo, &amp;task_info_count);</span><br><span class="line">  <span class="keyword">if</span> (kr != KERN_SUCCESS) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  task_basic_info_t basic_info;</span><br><span class="line">  thread_array_t thread_list;</span><br><span class="line">  mach_msg_type_number_t thread_count;</span><br><span class="line"></span><br><span class="line">  thread_info_data_t thinfo;</span><br><span class="line">  mach_msg_type_number_t thread_info_count;</span><br><span class="line"></span><br><span class="line">  thread_basic_info_t basic_info_th;</span><br><span class="line">  uint32_t stat_thread = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  basic_info = (task_basic_info_t)tinfo;</span><br><span class="line"></span><br><span class="line">  kr = task_threads(mach_task_self(), &amp;thread_list, &amp;thread_count);</span><br><span class="line">  <span class="keyword">if</span> (kr != KERN_SUCCESS) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (thread_count &gt; <span class="number">0</span>)</span><br><span class="line">      stat_thread += thread_count;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">long</span> tot_sec = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">long</span> tot_usec = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">float</span> tot_cpu = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">int</span> j;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (j = <span class="number">0</span>; j &lt; thread_count; j++)</span><br><span class="line">  &#123;</span><br><span class="line">      thread_info_count = THREAD_INFO_MAX;</span><br><span class="line">      kr = thread_info(thread_list[j], THREAD_BASIC_INFO,</span><br><span class="line">                       (thread_info_t)thinfo, &amp;thread_info_count);</span><br><span class="line">      <span class="keyword">if</span> (kr != KERN_SUCCESS) &#123;</span><br><span class="line">          <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      basic_info_th = (thread_basic_info_t)thinfo;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (!(basic_info_th-&gt;flags &amp; TH_FLAGS_IDLE)) &#123;</span><br><span class="line">          tot_sec = tot_sec + basic_info_th-&gt;user_time.seconds + basic_info_th-&gt;system_time.seconds;</span><br><span class="line">          tot_usec = tot_usec + basic_info_th-&gt;system_time.microseconds + basic_info_th-&gt;system_time.microseconds;</span><br><span class="line">          tot_cpu = tot_cpu + basic_info_th-&gt;cpu_usage / (<span class="keyword">float</span>)TH_USAGE_SCALE * <span class="number">100.0</span>;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  kr = vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * <span class="keyword">sizeof</span>(thread_t));</span><br><span class="line">  assert(kr == KERN_SUCCESS);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> tot_cpu;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>FPS 监测：</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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)displayLinkTick:(<span class="built_in">CADisplayLink</span> *)displayLink</span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">static</span> <span class="built_in">CFTimeInterval</span> lastTimeInterval = <span class="number">0.0</span>;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">CFTimeInterval</span> cuTime = <span class="built_in">CACurrentMediaTime</span>();</span><br><span class="line">  <span class="keyword">if</span> (!lastTimeInterval) &#123;</span><br><span class="line">      lastTimeInterval = cuTime;</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="built_in">CFTimeInterval</span> newInterval = cuTime - lastTimeInterval;</span><br><span class="line">  lastTimeInterval = cuTime;</span><br><span class="line">  <span class="keyword">if</span> (newInterval &gt; <span class="number">1.0</span> || newInterval &lt;= <span class="number">0</span>) &#123;</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">  currentFPS = round(<span class="number">1.0</span>/ newInterval);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>由于想要做到通用性，所以选择在 <code>UIApplication</code> 的 <code>delegate</code> 里面的 <code>-application:didFinishLaunchingWithOptions:</code> 来启动检测，由于是 <code>delegate</code>，正常的 Logos 语法没法准确 hook ，于是决定用运行时 <code>Method Swizzling</code> （参考：<a href="http://blog.csdn.net/yiyaaixuexi/article/details/9374411" target="_blank" rel="noopener">念茜 Objective-C的hook方案（一）:  Method Swizzling</a>）</p><p>自定义IMP用于替换之前的 <code>-application:didFinishLaunchingWithOptions:</code></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">id</span> myDidFinishLaunchingWithOptions_imp(<span class="keyword">id</span> <span class="keyword">self</span>, SEL cmd, <span class="built_in">UIApplication</span> *application, <span class="built_in">NSDictionary</span> *launchOptions)</span><br><span class="line">&#123;</span><br><span class="line">  <span class="built_in">BOOL</span> ret = [<span class="keyword">self</span> myApplication:application didFinishLaunchingWithOptions:launchOptions];</span><br><span class="line">  <span class="comment">// remove old log file</span></span><br><span class="line">  <span class="built_in">NSString</span> *path = [<span class="built_in">NSString</span> stringWithFormat:<span class="string">@"/tmp/%@.plist"</span>, [<span class="built_in">NSBundle</span> mainBundle].bundleIdentifier];</span><br><span class="line">  <span class="keyword">if</span> ([[<span class="built_in">NSFileManager</span> defaultManager] fileExistsAtPath:path]) &#123;</span><br><span class="line">      [[<span class="built_in">NSFileManager</span> defaultManager] removeItemAtPath:path error:<span class="literal">nil</span>];</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// creat a container for recording the log</span></span><br><span class="line">  logList = [[<span class="built_in">NSMutableArray</span> alloc] init];</span><br><span class="line"></span><br><span class="line">  <span class="comment">// get current FPS</span></span><br><span class="line">  <span class="built_in">CADisplayLink</span> *displayLink = [<span class="built_in">CADisplayLink</span> displayLinkWithTarget:<span class="keyword">self</span> selector:<span class="keyword">@selector</span>(displayLinkTick:)];</span><br><span class="line">  [displayLink addToRunLoop:[<span class="built_in">NSRunLoop</span> currentRunLoop] forMode:<span class="built_in">NSRunLoopCommonModes</span>];</span><br><span class="line"></span><br><span class="line">  <span class="built_in">NSTimer</span> *timer = [<span class="built_in">NSTimer</span> scheduledTimerWithTimeInterval:<span class="number">1</span> target:<span class="keyword">self</span> selector:<span class="keyword">@selector</span>(onTimer) userInfo:<span class="literal">nil</span> repeats:<span class="literal">YES</span>];</span><br><span class="line">  [timer fire];</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> @(ret);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后在App进入到后台的时候，进行检测记录的文件写入，替换原来的 <code>-applicationDidEnterBackground:</code></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">id</span> myApplicationDidEnterBackground_imp(<span class="keyword">id</span> <span class="keyword">self</span>, SEL cmd, <span class="built_in">UIApplication</span> *application, <span class="built_in">NSDictionary</span> *launchOptions)</span><br><span class="line">&#123;</span><br><span class="line">  [<span class="keyword">self</span> myApplicationDidEnterBackground:application];</span><br><span class="line"></span><br><span class="line">  [<span class="keyword">self</span> writeLogsToFile];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)setDelegate:(<span class="keyword">id</span>)delegate</span><br><span class="line">&#123;</span><br><span class="line">  %orig;</span><br><span class="line">  Method method1 = class_getInstanceMethod([delegate <span class="keyword">class</span>], <span class="keyword">@selector</span>(application:didFinishLaunchingWithOptions:));</span><br><span class="line">  class_addMethod([delegate <span class="keyword">class</span>], <span class="keyword">@selector</span>(myApplication:didFinishLaunchingWithOptions:), (IMP)myDidFinishLaunchingWithOptions_imp, method_getTypeEncoding(method1));</span><br><span class="line">  Method method2 = class_getInstanceMethod([delegate <span class="keyword">class</span>], <span class="keyword">@selector</span>(myApplication:didFinishLaunchingWithOptions:));</span><br><span class="line">  method_exchangeImplementations(method1, method2);</span><br><span class="line"></span><br><span class="line">  Method method3 = class_getInstanceMethod([delegate <span class="keyword">class</span>], <span class="keyword">@selector</span>(applicationDidEnterBackground:));</span><br><span class="line">  class_addMethod([delegate <span class="keyword">class</span>], <span class="keyword">@selector</span>(myApplicationDidEnterBackground:), (IMP)myApplicationDidEnterBackground_imp, method_getTypeEncoding(method3));</span><br><span class="line">  Method method4 = class_getInstanceMethod([delegate <span class="keyword">class</span>], <span class="keyword">@selector</span>(myApplicationDidEnterBackground:));</span><br><span class="line">  method_exchangeImplementations(method3, method4);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Tweak-的使用"><a href="#Tweak-的使用" class="headerlink" title="Tweak 的使用"></a>Tweak 的使用</h3><p>和其他的越狱工具大体相当，在 <code>.plist</code> 文件里声明好你想要 hook 的 app（也就是你想要监测的）。利用 Theos 可以把你写好的工具生成一个 dylib ，然后通过已越狱的手机就可以运行了。当然了，如果你要想生成一些报告，还需要在工具中实现。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;由于现在iOS这一块缺少性能测试，导致 app 性能优化方面工作并没有明确的工作目标。所以需要个必要的测试工具来数据化 app 的性能状况。&lt;/p&gt;
&lt;p&gt;需要一个工具可以检测 app 的内存占用，CPU使用情况，FPS还有启动时间（可以通过 &lt;code&gt;UIAutomation&lt;/code&gt; 来实现）&lt;br&gt;由于需要检测生产环境下各种友商 app，用于对比性能，所以决定用越狱后tweak来实现性能的检测。&lt;/p&gt;
    
    </summary>
    
      <category term="iOS" scheme="https://zesming.com/categories/iOS/"/>
    
    
      <category term="iOS" scheme="https://zesming.com/tags/iOS/"/>
    
      <category term="tweak" scheme="https://zesming.com/tags/tweak/"/>
    
      <category term="performance" scheme="https://zesming.com/tags/performance/"/>
    
  </entry>
  
  <entry>
    <title>Update CocoaPods</title>
    <link href="https://zesming.com/update-cocoapods/"/>
    <id>https://zesming.com/update-cocoapods/</id>
    <published>2016-03-20T13:53:14.000Z</published>
    <updated>2018-05-19T03:55:24.763Z</updated>
    
    <content type="html"><![CDATA[<h2 id="CocoaPods-版本升级"><a href="#CocoaPods-版本升级" class="headerlink" title="CocoaPods 版本升级"></a>CocoaPods 版本升级</h2><p>CocoaPods 每一次升级都会带来不小的改动，经常保持最新版本是一种好习惯。除了通过 Gemfile 来更新，我们平时还可以用传统的 <code>gem install</code> 的方式来操作。</p><a id="more"></a><p>如果想升级，执行</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ sudo gem update --system</span><br><span class="line">$ sudo gem install cocoapods // 安装cocoapods</span><br><span class="line">$ pod setup</span><br></pre></td></tr></table></figure><p>如果上面 install 卡住了，尝试换个源</p><figure class="highlight bash"><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">$ gem sources --remove https://rubygems.org/</span><br><span class="line">$ gem sources -a https://ruby.taobao.org/ // 或者是其他的源</span><br><span class="line">$ gem sources -l</span><br><span class="line"></span><br><span class="line">*** CURRENT SOURCES ***</span><br><span class="line">https://ruby.taobao.org/</span><br></pre></td></tr></table></figure><p>天朝的网络有时候会大姨妈，你会发现最后一步卡住了很长时间，要么耐心等待，要么就试试下面的命令</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ pod setup --verbose // 就是能让你看到它在干啥，并不是在那里傻等着，还是锁屏吃饭去吧！</span><br></pre></td></tr></table></figure><p>经过漫长等待，如果完成的话，应该就是最新版本了~</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ pod --version</span><br></pre></td></tr></table></figure><p>Done</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;CocoaPods-版本升级&quot;&gt;&lt;a href=&quot;#CocoaPods-版本升级&quot; class=&quot;headerlink&quot; title=&quot;CocoaPods 版本升级&quot;&gt;&lt;/a&gt;CocoaPods 版本升级&lt;/h2&gt;&lt;p&gt;CocoaPods 每一次升级都会带来不小的改动，经常保持最新版本是一种好习惯。除了通过 Gemfile 来更新，我们平时还可以用传统的 &lt;code&gt;gem install&lt;/code&gt; 的方式来操作。&lt;/p&gt;
    
    </summary>
    
      <category term="iOS" scheme="https://zesming.com/categories/iOS/"/>
    
    
      <category term="CocoaPods" scheme="https://zesming.com/tags/CocoaPods/"/>
    
  </entry>
  
  <entry>
    <title>Command-Line Testing</title>
    <link href="https://zesming.com/command-line-testing/"/>
    <id>https://zesming.com/command-line-testing/</id>
    <published>2016-03-20T12:05:01.000Z</published>
    <updated>2018-05-13T11:40:53.000Z</updated>
    
    <content type="html"><![CDATA[<p>一般来说，我们平时测试都是在 Xcode 里面利用可视化界面或者 <code>cmd+U</code> 进行的，可是自动化脚本对这样的方式支持可不是那么的友好，例如利用 CI 的自动化测试，最好的办法就是利用 Xcode 提供的 Command Line 工具。</p><a id="more"></a><h2 id="xcodebuild"><a href="#xcodebuild" class="headerlink" title="xcodebuild"></a>xcodebuild</h2><blockquote class="blockquote-center"><p>xcodebuild builds one or more targets contained in an Xcode project, or builds a scheme contained in an Xcode workspace or Xcode project.</p></blockquote><p>Xcode 提供的一个基于 CLT 的一个命令，用于编译测试打包分析你的工程。</p><h3 id="build-actions"><a href="#build-actions" class="headerlink" title="build actions"></a>build actions</h3><p>在这个命令中，提供了一大坨 build actions ，基本和 GUI 里面的功能一致。</p><figure class="highlight bash"><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">build action ...</span><br><span class="line">           Specify a build action (or actions) to perform on the target.</span><br><span class="line">           Available build actions are:</span><br><span class="line"></span><br><span class="line">           build       Build the target <span class="keyword">in</span> the build root (SYMROOT).  This is</span><br><span class="line">                       the default build action.</span><br><span class="line"></span><br><span class="line">           analyze     Build and analyze a target or scheme from the build</span><br><span class="line">                       root (SYMROOT).  This requires specifying a scheme.</span><br><span class="line"></span><br><span class="line">           archive     Archive a scheme from the build root (SYMROOT).  This</span><br><span class="line">                       requires specifying a scheme.</span><br><span class="line"></span><br><span class="line">           <span class="built_in">test</span>        Test a scheme from the build root (SYMROOT).  This</span><br><span class="line">                       requires specifying a scheme and optionally a destina-</span><br><span class="line">                       tion.</span><br><span class="line"></span><br><span class="line">           installsrc  Copy the <span class="built_in">source</span> of the project to the <span class="built_in">source</span> root</span><br><span class="line">                       (SRCROOT).</span><br><span class="line"></span><br><span class="line">           install     Build the target and install it into the target<span class="string">'s</span></span><br><span class="line"><span class="string">                       installation directory in the distribution root</span></span><br><span class="line"><span class="string">                       (DSTROOT).</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">           clean       Remove build products and intermediate files from the</span></span><br><span class="line"><span class="string">                       build root (SYMROOT).</span></span><br></pre></td></tr></table></figure><h3 id="用于跑测试的-test-命令"><a href="#用于跑测试的-test-命令" class="headerlink" title="用于跑测试的 test 命令"></a>用于跑测试的 test 命令</h3><p>一般来说，这里面的参数也是和图形化界面里面的一样，选择 build action ，选择 <code>project</code> 或者 <code>workspace</code> ，选择 <code>scheme</code> 还有目标测试平台设备，例如</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ xcodebuild <span class="built_in">test</span> -project MyAppProject.xcodeproj -scheme MyApp -destination <span class="string">'platform=OS X,arch=x86_64'</span></span><br></pre></td></tr></table></figure><h3 id="选择运行测试设备"><a href="#选择运行测试设备" class="headerlink" title="选择运行测试设备"></a>选择运行测试设备</h3><p>当然了，我们一般都是用模拟器啦~</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ xcodebuild <span class="built_in">test</span> -project MyAppProject.xcodeproj -scheme MyApp -destination <span class="string">'platform=iOS Simulator,name=iPhone,0S=7.0'</span></span><br></pre></td></tr></table></figure><p>如果你想用真机测试，当然可以</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ xcodebuild <span class="built_in">test</span> -project MyAppProject.xcodeproj -scheme MyApp -destination <span class="string">'platform=iOS,name=Development iPod touch'</span></span><br></pre></td></tr></table></figure><p>如果你突发奇想，想要三个目标设备一块跑，当然可以！只需要填写三个 <code>-destination</code> 即可。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ xcodebuild <span class="built_in">test</span> -project MyAppProject.xcodeproj -scheme MyApp -destination <span class="string">'platform=OS X,arch=x86_64'</span> -destination <span class="string">'platform=iOS,name=Development iPod touch'</span> -destination <span class="string">'platform=iOS Simulator,name=iPhone,0S=7.0'</span></span><br></pre></td></tr></table></figure><h3 id="命令返回值"><a href="#命令返回值" class="headerlink" title="命令返回值"></a>命令返回值</h3><p>和大多数命令一样，执行测试完成后，会有个返回值，当然了，如果失败了……</p><blockquote class="blockquote-center"><p>If any tests fail, xcodebuild returns a nonzero exit code.</p></blockquote><h3 id="想要了解更多"><a href="#想要了解更多" class="headerlink" title="想要了解更多"></a>想要了解更多</h3><p>这只是个大致的介绍，更多细节请联系 Apple 牛逼的工程师们（明显不靠谱），或者</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ man xcodebuild</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;一般来说，我们平时测试都是在 Xcode 里面利用可视化界面或者 &lt;code&gt;cmd+U&lt;/code&gt; 进行的，可是自动化脚本对这样的方式支持可不是那么的友好，例如利用 CI 的自动化测试，最好的办法就是利用 Xcode 提供的 Command Line 工具。&lt;/p&gt;
    
    </summary>
    
      <category term="iOS" scheme="https://zesming.com/categories/iOS/"/>
    
      <category term="macOS" scheme="https://zesming.com/categories/macOS/"/>
    
    
      <category term="Unit test" scheme="https://zesming.com/tags/Unit-test/"/>
    
      <category term="Command line test" scheme="https://zesming.com/tags/Command-line-test/"/>
    
      <category term="xcodebuild" scheme="https://zesming.com/tags/xcodebuild/"/>
    
      <category term="CI" scheme="https://zesming.com/tags/CI/"/>
    
  </entry>
  
</feed>
