<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.zhoulujue.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.zhoulujue.com/" rel="alternate" type="text/html" /><updated>2026-08-21T09:33:21+00:00</updated><id>https://blog.zhoulujue.com/feed.xml</id><title type="html">Michael周</title><subtitle>The only limit to our realization of tomorrow will be our doubts of today.</subtitle><entry><title type="html">去 Google 参加 Rising Star Meetup</title><link href="https://blog.zhoulujue.com/Google-Rising-Star-Meetup/" rel="alternate" type="text/html" title="去 Google 参加 Rising Star Meetup" /><published>2019-12-31T00:00:00+00:00</published><updated>2019-12-31T00:00:00+00:00</updated><id>https://blog.zhoulujue.com/Google-Rising-Star-Meetup</id><content type="html" xml:base="https://blog.zhoulujue.com/Google-Rising-Star-Meetup/"><![CDATA[<p><img src="/images/google-meetup-group.jpg" alt="Try finding me in the photo" /></p>

<p>17号收到谷歌 Maya 同学的邮件邀请去参加会议，我全然忘了什么时候被推荐去参加 Google 的组织的 Rising Star 线下聚会。其主要目的是将一些创业公司召集起来，鼓励大家做出海应用、同步 I/O 上的技术、产品等等。相当于是一个谷歌与创业公司的一个双向沟通。</p>

<p>下午12:30出发，目的地在一个名为“融科资讯中心”的大厦里，进去后感觉很高大上，电梯间也有很明显的 Google 风格的一些装饰，突然感觉是曾相识，我才想起来以前在搜狗上班的时候，同方大厦的那几层也是这样的装修风格。也不知道是谁抄袭谁哈~</p>

<p><img src="/images/15270918175606.jpg" alt="Guide" />
<img src="/images/15270918353294.jpg" alt="Agenda" /></p>

<hr />

<p>茶歇时遇到了一个做游戏出海的人，叫徐双。（后来翻出名片才知道公司名字叫 EnjoyGame）这个人很热情，主动问起我是做什么的，我告诉她我是下厨房的人后，她欣喜的掏出手机打开她的下厨房 App 告诉我她买了一些电子书，并且经常在下厨房上找菜谱。
探讨了下厨房的一些商业模式，盈利点等等，由于我涉及的比较少，我只能说个大概，但是说到下厨房产品本身相关的东西，我能讲的头头是道，像首页是怎么演进的我们对用户行为的一些思考和判断，于是前排的人也转过身来听我讲，自己在发光的感觉真的很不错。</p>

<p>我也向徐双同学请教了一下出海应用的现状，发现真是闷声挣大钱的一个行业！原来国内做出海游戏已经非常挣钱了，而且套路很多，听下来大体是：做爆款 -&gt; 买流量 -&gt; 盯转化 -&gt; 收割流量 -&gt; 做下一个…这样循环往复。原来 Google Play /AdMob 也提供了成熟的流量购买方式，她们还会把 Google 的数据和其他广告平台进行对比，介于很多术语不懂，所以后面很多东西我其实都没有听太懂…</p>

<hr />

<p>原来认识新的朋友没有我想象的那样耗费认知力，感觉不错，可能要保持这样的朋友关系可能需要更多的付出而我不太擅长吧。</p>

<p>很惊喜的是，遇到了之前的朋友陈卓，他今天来做 Android P 的 session，上次遇见他还是在上海的 Google 开发者大会，彼时他太忙没有聊太多，今天准备叙叙旧结果在茶歇时发现他已经早走了。也算是遗憾吧。想起刚认识，他还是售前工程师，现在已经成了<code class="language-plaintext highlighter-rouge">技术推广工程师</code>，<strong>每天学习最新的技术并向全世界推广，应该是最幸福的工程师了吧！</strong></p>

<p>在不同的场合都和 Google 中国的员工有过一些交流，今天也听了很多 Speaker 的 session, 总体感觉是这些人好像和我们都是一样，没有那么的“高大上”，大家都是在很务实地完成一些事情，执行力很高。包括之前去亚马逊去面试的时候，也是相同的感觉，可能优秀的公司都是有务实这个优点吧。</p>

<p>第一次有了，Google 的中国员工“不过如此，我也可以”的想法，不知道是好是坏哈哈哈。</p>

<p><img src="/images/15270918735953.jpg" alt="" /></p>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">从源码了解Flutter的渲染基础：Widget/Element/RenderObject</title><link href="https://blog.zhoulujue.com/Widget-Element-RenderObject/" rel="alternate" type="text/html" title="从源码了解Flutter的渲染基础：Widget/Element/RenderObject" /><published>2019-06-28T00:00:00+00:00</published><updated>2019-06-28T00:00:00+00:00</updated><id>https://blog.zhoulujue.com/Widget-Element-RenderObject</id><content type="html" xml:base="https://blog.zhoulujue.com/Widget-Element-RenderObject/"><![CDATA[<p>如果你要使用Flutter开始构建一个页面，那么你可能从Widget开始一层一层搭建。</p>

<p>但是你曾经想过这些Widget是如何绘制到屏幕上的吗？很多同学应该会熟悉Flutter的三个树形结构：</p>
<ul>
  <li>Widget Tree</li>
  <li>Element Tree</li>
  <li>RenderObject Tree</li>
</ul>

<p>站在Flutter程序员的角度，打交道最多的应该是Widget了，那么下面我带领大家从Flutter源码读起来看一下一个Widget是如何绘制到屏幕上的。</p>

<h2 id="opacity">Opacity</h2>
<p>首先我们来回忆一个熟悉的Widget：<code class="language-plaintext highlighter-rouge">Opacity</code>。</p>

<p>这个Widget一般用来给一个布局加上透明度，它只有一个参数 <code class="language-plaintext highlighter-rouge">opacity</code>，1.0表示完全不透明，0.0表示完全透明。</p>

<p>在源码里可以看到 Opacity 的继承关系：</p>

<blockquote>
  <p>Opacity -&gt; SingleChildRenderObjectWidget -&gt; RenderObject -&gt; Widget</p>
</blockquote>

<p>我们比较一下最简单的 StatelessWidget/StatefulWidget :</p>

<blockquote>
  <p>StatelessWidget -&gt; Widget</p>
</blockquote>

<blockquote>
  <p>StatefulWidget -&gt; Widget</p>
</blockquote>

<p>可以看到<code class="language-plaintext highlighter-rouge">Opacity</code>多了<code class="language-plaintext highlighter-rouge">SingleChildRenderObjectWidget</code>和<code class="language-plaintext highlighter-rouge">RenderObject</code>这层关系，那这层关系到底是干什么的呢？我们接着往下看源码。</p>

<h3 id="createrenderobject-方法">createRenderObject 方法</h3>
<p>通过看<code class="language-plaintext highlighter-rouge">RenderObject</code>的代码，我们知道只要继承了 <code class="language-plaintext highlighter-rouge">RenderObject</code>就需要实现<code class="language-plaintext highlighter-rouge">createRenderObject</code>方法。</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@override</span>
  <span class="n">RenderOpacity</span> <span class="n">createRenderObject</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">RenderOpacity</span><span class="p">(</span>
      <span class="nl">opacity:</span> <span class="n">opacity</span><span class="p">,</span>
      <span class="nl">alwaysIncludeSemantics:</span> <span class="n">alwaysIncludeSemantics</span><span class="p">,</span>
    <span class="p">);</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>可以发现其实最终返回了一个<code class="language-plaintext highlighter-rouge">RenderOpacity</code></p>

<h3 id="renderopacity">RenderOpacity</h3>
<p>大概看一下<code class="language-plaintext highlighter-rouge">RenderOpacity</code>的代码，可以知道这里实现了最后的所谓的“绘制”也就是<code class="language-plaintext highlighter-rouge">paint</code>的代码：</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">paint</span><span class="p">(</span><span class="n">PaintingContext</span> <span class="n">context</span><span class="p">,</span> <span class="n">Offset</span> <span class="n">offset</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">child</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">_alpha</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">_alpha</span> <span class="o">==</span> <span class="mi">255</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">context</span><span class="o">.</span><span class="na">paintChild</span><span class="p">(</span><span class="n">child</span><span class="p">,</span> <span class="n">offset</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="k">assert</span><span class="p">(</span><span class="n">needsCompositing</span><span class="p">);</span>
      <span class="n">context</span><span class="o">.</span><span class="na">pushOpacity</span><span class="p">(</span><span class="n">offset</span><span class="p">,</span> <span class="n">_alpha</span><span class="p">,</span> <span class="k">super</span><span class="o">.</span><span class="na">paint</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div></div>
<p>这里的<code class="language-plaintext highlighter-rouge">PaintingContext</code>实际上是个Canvas，所以最重要的代码就是<code class="language-plaintext highlighter-rouge">context.pushOpacity</code>这个方法的调用，最终用上了 <code class="language-plaintext highlighter-rouge">_alpha</code>，而这个<code class="language-plaintext highlighter-rouge">_alpha</code>由最上层<code class="language-plaintext highlighter-rouge">Opacity</code>Widget的唯一参数<code class="language-plaintext highlighter-rouge">opacity</code>计算而来。</p>

<p>所以我们来总结一下这里面的关系：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Opacity</code>直接继承<code class="language-plaintext highlighter-rouge">SingleChildRenderObjectWidget</code></li>
  <li><code class="language-plaintext highlighter-rouge">Widget</code><strong>不直接参与绘制</strong>，仅仅存储绘制需要的信息</li>
  <li><code class="language-plaintext highlighter-rouge">RenderOpacity</code> 在这里承担了实际的 layout/render 逻辑</li>
</ul>

<h3 id="小结">小结</h3>
<p>我们日常所用的Widget仅仅是一些配置，RenderObject做了实际的 layout/render 等工作。这一点和Android的 <code class="language-plaintext highlighter-rouge">View</code>、iOS的 <code class="language-plaintext highlighter-rouge">UIView</code> 有本质的区别，请注意。</p>

<p>在Flutter中，只要是<code class="language-plaintext highlighter-rouge">build()</code>方法被调用了，就会创建一堆Widget，这个是OK的，上面说过Widget仅仅是配置文件，所以即使频繁地 <strong>创建/重建</strong> 它们，是不会带来界面的刷新的。</p>

<p>回想一下，如果你要在Flutter里创建一个动画，一般来说，需要在指定的Widget之上套上一层XXXTransition，然后动画开始后用<code class="language-plaintext highlighter-rouge">animationController</code>来不断地让Widget刷新重建，但是整个页面不会因此重新刷新。</p>

<p>所以，Widget的重建和页面真正的重新渲染没有直接的联系，那么页面什么时候才会重新渲染呢，我们接着看源码。</p>

<h2 id="element">Element</h2>
<p>上面我们以<code class="language-plaintext highlighter-rouge">Opacity</code>为例，从源码中看出了它的继承关系，并且推导出一些结论，这里面都没有涉及到<code class="language-plaintext highlighter-rouge">Element</code>，但实际上Element也十分重要。那什么是Element呢？源码里的注释如下：</p>

<blockquote>
  <p>An instantiation of a Widget at a particular location in the tree.</p>
</blockquote>

<p>我们知道，Widget是<strong>immutable</strong>的，也就是说一旦有了变化，Widget是反复重建的，你在代码里写的Widget的构造函数会被重新执行。那对应的，肯定得有一个东西来消化掉这些变化中的不变，来做cache。</p>

<p>让程序员不去保存Widgets是好的，因为他不会因为管理无数的Widget而且烦恼，一旦有变化就当整个页面都在变化，Widget的每一帧对应了一个State。这样，一个指定的state就能精确描述一个Widget该怎么展示。也就是说，程序员只要管理好状态，那么就能管理好整个页面变化的逻辑。</p>

<p>当一个Widget首次被创建的时候，那么这个Widget会过<code class="language-plaintext highlighter-rouge">Widget.createElement</code> inflate成一个<code class="language-plaintext highlighter-rouge">element</code>，挂在 element tree 上。</p>

<p>此后，当State发生变化，则Widget重建，但Element只会updates。</p>

<p>也就是说我们能大概有一个以下的一个流程：</p>

<p>程序员写Widget -&gt; Widget形成Element -&gt; Element构建 RenderObject -&gt; RenderObject描述Canvas绘制 -&gt; 交给FlutterEngine 光栅化<strong>Rasterize</strong></p>

<p>回到我们今天的主角<code class="language-plaintext highlighter-rouge">Opacity</code>，从上面的分析，我们知道Opacity也是Widget，所以理应有对应的Element，但是上面看到为啥直接Widget就自己返回一个<code class="language-plaintext highlighter-rouge">RenderOpacity</code>呢？看起来是Widget自己创建了RenderObject。那么它的element是什么时候创建的呢？看源码：</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">abstract</span> <span class="kd">class</span> <span class="nc">SingleChildRenderObjectWidget</span> <span class="kd">extends</span> <span class="n">RenderObjectWidget</span> <span class="p">{</span> 

<span class="p">...</span>

<span class="nd">@override</span>
<span class="n">SingleChildRenderObjectElement</span> <span class="nf">createElement</span><span class="p">()</span> <span class="o">=</span><span class="p">&gt;</span> <span class="k">new</span> <span class="n">SingleChildRenderObjectElement</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>

<span class="p">...</span>

<span class="p">}</span>
</code></pre></div></div>
<p>Opacity继承自<code class="language-plaintext highlighter-rouge">SingleChildRenderObjectWidget</code>，
这里面的<code class="language-plaintext highlighter-rouge">SingleChildRenderObjectElement</code>，实际上是只有一个child的Element。当第Widget第一次创建的时候，<code class="language-plaintext highlighter-rouge">createElement</code>会被调用，返回一个<code class="language-plaintext highlighter-rouge">SingleChildRenderObjectElement</code></p>

<p>那它的 RenderObject 实际上是被这个 Element 创建的，看一下为什么：</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">SingleChildRenderObjectElement</span> <span class="kd">extends</span> <span class="n">RenderObjectElement</span> <span class="p">{</span>

    <span class="n">SingleChildRenderObjectElement</span><span class="p">(</span><span class="n">SingleChildRenderObjectWidget</span> <span class="n">widget</span><span class="p">)</span> <span class="o">:</span> <span class="k">super</span><span class="p">(</span><span class="n">widget</span><span class="p">);</span>

    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这个<code class="language-plaintext highlighter-rouge">SingleChildRenderObjectElement</code>构造函数接受了一个<code class="language-plaintext highlighter-rouge">SingleChildRenderObjectWidget</code>参数，它负责创建一个<code class="language-plaintext highlighter-rouge">RenderObject</code>。那么我们看这个 renderobject 实际在哪里创建的：</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">SingleChildRenderObjectElement</span> <span class="p">{</span>
    <span class="p">...</span>

    <span class="nd">@override</span>
  <span class="kt">void</span> <span class="n">mount</span><span class="p">(</span><span class="n">Element</span> <span class="n">parent</span><span class="p">,</span> <span class="kd">dynamic</span> <span class="n">newSlot</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">super</span><span class="o">.</span><span class="na">mount</span><span class="p">(</span><span class="n">parent</span><span class="p">,</span> <span class="n">newSlot</span><span class="p">);</span>
    <span class="n">_child</span> <span class="o">=</span> <span class="n">updateChild</span><span class="p">(</span><span class="n">_child</span><span class="p">,</span> <span class="n">widget</span><span class="o">.</span><span class="na">child</span><span class="p">,</span> <span class="kc">null</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>SingleChildRenderObjectElement里有个mount方法，看起来没啥，不过没事儿我们往父类里看：</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">RenderObjectElement</span> <span class="kd">extends</span> <span class="n">Element</span> <span class="p">{</span>

<span class="p">...</span>

<span class="nd">@override</span>
  <span class="kt">void</span> <span class="n">mount</span><span class="p">(</span><span class="n">Element</span> <span class="n">parent</span><span class="p">,</span> <span class="kd">dynamic</span> <span class="n">newSlot</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">super</span><span class="o">.</span><span class="na">mount</span><span class="p">(</span><span class="n">parent</span><span class="p">,</span> <span class="n">newSlot</span><span class="p">);</span>
    <span class="n">_renderObject</span> <span class="o">=</span> <span class="n">widget</span><span class="o">.</span><span class="na">createRenderObject</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
    <span class="k">assert</span><span class="p">(()</span> <span class="p">{</span> <span class="n">_debugUpdateRenderObjectOwner</span><span class="p">();</span> <span class="k">return</span> <span class="kc">true</span><span class="p">;</span> <span class="p">}());</span>
    <span class="k">assert</span><span class="p">(</span><span class="n">_slot</span> <span class="o">==</span> <span class="n">newSlot</span><span class="p">);</span>
    <span class="n">attachRenderObject</span><span class="p">(</span><span class="n">newSlot</span><span class="p">);</span>
    <span class="n">_dirty</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="p">...</span>
</code></pre></div></div>
<p>终于看到magic了，所以在<code class="language-plaintext highlighter-rouge">mount</code>方法里，element调用了<code class="language-plaintext highlighter-rouge">widget.createRenderObject</code>，并attach。</p>

<p>注意，这些都发生在<code class="language-plaintext highlighter-rouge">mount</code>方法里，意味着只有element mounted的时候才会执行，所以只有一次。</p>

<hr />

<p>参考文献：</p>

<p><a href="https://medium.com/flutter-community/flutter-what-are-widgets-renderobjects-and-elements-630a57d05208">1. Flutter, what are Widgets, RenderObjects and Elements?</a></p>

<p><a href="https://medium.com/flutter-community/the-layer-cake-widgets-elements-renderobjects-7644c3142401">2. The Layer Cake</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[如果你要使用Flutter开始构建一个页面，那么你可能从Widget开始一层一层搭建。]]></summary></entry><entry><title type="html">Flutter的Event Loop、Future及Isolate</title><link href="https://blog.zhoulujue.com/EventLoop-In-Flutter/" rel="alternate" type="text/html" title="Flutter的Event Loop、Future及Isolate" /><published>2019-06-23T00:00:00+00:00</published><updated>2019-06-23T00:00:00+00:00</updated><id>https://blog.zhoulujue.com/EventLoop-In-Flutter</id><content type="html" xml:base="https://blog.zhoulujue.com/EventLoop-In-Flutter/"><![CDATA[<p>本文主要介绍Flutter中Event Loop以及如何在Flutter中做parallel processing.</p>

<h2 id="event-loop">Event Loop</h2>

<blockquote>
  <p>First things first, everyone needs to bear in mind that Dart is Single Thread and Flutter relies on Dart.</p>
</blockquote>

<blockquote>
  <p>IMPORTANT
Dart executes one operation at a time, one after the other meaning that as long as one operation is executing, it cannot be interrupted by any other Dart code.</p>
</blockquote>

<p>跟AndroidVM类似，当你启动一个Flutter的App，那么就会系统就会启动一个DartVM
Flutter中的Dart VM启动后，那么一个新的Thread就会被创建，并且只会有一个线程，它运行在自己的Isolate中。</p>

<p>当这个Thread被创建后，DartVM会自动做以下3件事情：</p>
<ul>
  <li>初始化2个队列，一个叫“MicroTask”，一个叫“Event”，都是FIFO队列</li>
  <li>执行 main() 方法，一旦执行完毕就做下一步</li>
  <li>启动 Event Loop</li>
</ul>

<p>Event Loop就像一个 infinite loop，被内部时钟来调谐，每一个tick，如果没有其他Dart Code在执行，就会做如下的事情（伪代码）：</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">eventLoop</span><span class="p">(){</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">microTaskQueue</span><span class="o">.</span><span class="na">isNotEmpty</span><span class="p">){</span>
        <span class="n">fetchFirstMicroTaskFromQueue</span><span class="p">();</span>
        <span class="n">executeThisMicroTask</span><span class="p">();</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">eventQueue</span><span class="o">.</span><span class="na">isNotEmpty</span><span class="p">){</span>
        <span class="n">fetchFirstEventFromQueue</span><span class="p">();</span>
        <span class="n">executeThisEventRelatedCode</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="microtask-queue">MicroTask Queue</h3>
<p>MicroTask Queue是为了非常短暂的asynchronously的内部操作来设计的。在其他Dart代码运行完毕后，且在移交给Event Queue前。</p>

<p>举个例子，我们经常需要在close一个resource以后，dispose掉一些handle，下面的这个例子里，scheduleMicroTask 可以用来做 dispose 的事情：</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">MyResource</span> <span class="n">myResource</span><span class="p">;</span>

    <span class="p">...</span>

    <span class="kt">void</span> <span class="n">closeAndRelease</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">scheduleMicroTask</span><span class="p">(</span><span class="n">_dispose</span><span class="p">);</span>
        <span class="n">_close</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="kt">void</span> <span class="n">_close</span><span class="p">(){</span>
        <span class="c1">// The code to be run synchronously</span>
        <span class="c1">// to close the resource</span>
        <span class="p">...</span>
    <span class="p">}</span>

    <span class="kt">void</span> <span class="n">_dispose</span><span class="p">(){</span>
        <span class="c1">// The code which has to be run</span>
        <span class="c1">// right after the _close()</span>
        <span class="c1">// has completed</span>
    <span class="p">}</span>
</code></pre></div></div>
<p>这里，虽然<code class="language-plaintext highlighter-rouge">scheduleMicroTask(_dispose)</code>语句在<code class="language-plaintext highlighter-rouge">_close()</code>语句之前，但是由于上面说到的，“其他Dart代码执行完毕后”，所以<code class="language-plaintext highlighter-rouge">_close()</code>会先执行，然后执行 Event loop 的 microTask。</p>

<p>即使你已经知道 microTask 的执行时机，而且还学习了用<code class="language-plaintext highlighter-rouge">scheduleMicroTask</code>来使用 microTask，但是 microTask 也不是你常用的东西。就 Flutter 本身来说，整个 source code只引用了 <code class="language-plaintext highlighter-rouge">scheduleMicroTask()</code> 7次。</p>

<h3 id="event-queue">Event Queue</h3>
<p>Event Queue 主要用来处理当某些事件发生后，调用哪些操作，这些事件分为：</p>

<ul>
  <li>外部事件：
    <ul>
      <li>I/O</li>
      <li>gesture</li>
      <li>drawing</li>
      <li>timers</li>
      <li>streams</li>
    </ul>
  </li>
  <li>futures</li>
</ul>

<p>事实上，每当外部事件发生时，要执行的代码都是在 Event Queue里找到的。
只要当没有 MicroTask 需要run了，那么 Event Queue 就会从第一个事件开始处理</p>

<h3 id="futures">Futures</h3>
<p>当你创建了一个future的实例，实际上是做了以下几件事情：</p>
<ul>
  <li>一个 instance 被创建，放到一个内部的数组后重新排序，由dart管理</li>
  <li>需要后续被执行的代码，被直接push到 Event Queue里面</li>
  <li>future立即同步返回一个状态<code class="language-plaintext highlighter-rouge">incomplete</code></li>
  <li>如果有其他 <em>synchronous</em> 代码，会先执行这些 sychronous 代码</li>
</ul>

<p>Future和其他Event一样，会在EventQueue里被执行。
以下的例子用来说明Future和上面的Event执行过程一样</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">main</span><span class="p">(){</span>
    <span class="n">print</span><span class="p">(</span><span class="s">'Before the Future'</span><span class="p">);</span>
    <span class="n">Future</span><span class="p">((){</span>
        <span class="n">print</span><span class="p">(</span><span class="s">'Running the Future'</span><span class="p">);</span>
    <span class="p">})</span><span class="o">.</span><span class="na">then</span><span class="p">((</span><span class="n">_</span><span class="p">){</span>
        <span class="n">print</span><span class="p">(</span><span class="s">'Future is complete'</span><span class="p">);</span>
    <span class="p">});</span>
    <span class="n">print</span><span class="p">(</span><span class="s">'After the Future'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>执行后，会得到如下输出：</p>

<pre><code class="language-Shell">Before the Future
After the Future
Running the Future
Future is complete
</code></pre>

<p>我们来分步骤解释一下代码是如何执行的：</p>
<ol>
  <li>执行print(‘Before the Future’)</li>
  <li>将function “(){print(‘Running the Future’);}” 添加到 event queue</li>
  <li>执行print(‘After the Future’)</li>
  <li>Event Loop 取到第2步里说的代码，并且运行</li>
  <li>代码执行完毕后，它尝试找到 then 语句并运行</li>
</ol>

<blockquote>
  <p>A <strong>Future</strong> is <strong>NOT</strong> executed in parallel but following the regular sequence of events, handled by the <strong>Event Loop</strong></p>
</blockquote>

<h3 id="async-methods">Async Methods</h3>
<p>如果在任何一个方法的声明部分加上 <code class="language-plaintext highlighter-rouge">async</code> 后缀，那么你实际上在向dart表明：</p>

<ul>
  <li>该方法的结果是一个 future</li>
  <li>如果调用时遇到一个await，那么它会同步执行，会把它所在的代码上下文给pause住</li>
  <li>下一行代码会等待，直到上面的future(被await等的)结束</li>
</ul>

<h3 id="isolate">Isolate</h3>
<p>每个线程有自己的Isolate，你可以用<code class="language-plaintext highlighter-rouge">Isolate.spawn</code> 或者是 <code class="language-plaintext highlighter-rouge">compute</code> 来创建一个 Isolate
每个Isolate都有自己的Data，和Event loop
Isolate之间通过消息来进行沟通</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Isolate</span><span class="o">.</span><span class="na">spawn</span><span class="p">(</span>
  <span class="n">aFunctionToRun</span><span class="p">,</span>
  <span class="p">{</span><span class="s">'data'</span> <span class="o">:</span> <span class="s">'Here is some data.'</span><span class="p">},</span> 
<span class="p">);</span>
</code></pre></div></div>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">compute</span><span class="p">(</span>
  <span class="p">(</span><span class="n">paramas</span><span class="p">)</span> <span class="p">{</span>
    <span class="cm">/* do something */</span>
  <span class="p">},</span>
  <span class="p">{</span><span class="s">'data'</span> <span class="o">:</span> <span class="s">'Here is some data.'</span><span class="p">},</span>
<span class="p">);</span>  
</code></pre></div></div>
<p>当你在一个Isolate中，创建了一个新的Isolate，然后要和新的Isolate进行沟通，那么就需要<code class="language-plaintext highlighter-rouge">SendPort</code>和<code class="language-plaintext highlighter-rouge">ReceivePort</code>。
为了能沟通，两个Isolate必须要互相知晓对方的port：</p>
<ul>
  <li>本地 Isolate 通过 <code class="language-plaintext highlighter-rouge">SendPort</code> 来收/发消息，官方起的名字真的是有些让人困惑。</li>
  <li>当你创建一个Isolate时，就需要给 <code class="language-plaintext highlighter-rouge">spawn</code> 方法传递一个 <code class="language-plaintext highlighter-rouge">ReceivePort</code> 的实例，后续会用这个port来收/发消息，同时也会通过这个port把本地 Isolate的sendport返回</li>
</ul>

<p>找了一个例子，感受一下</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//</span>
<span class="c1">// The port of the new isolate</span>
<span class="c1">// this port will be used to further</span>
<span class="c1">// send messages to that isolate</span>
<span class="c1">//</span>
<span class="n">SendPort</span> <span class="n">newIsolateSendPort</span><span class="p">;</span>

<span class="c1">//</span>
<span class="c1">// Instance of the new Isolate</span>
<span class="c1">//</span>
<span class="n">Isolate</span> <span class="n">newIsolate</span><span class="p">;</span>

<span class="c1">//</span>
<span class="c1">// Method that launches a new isolate</span>
<span class="c1">// and proceeds with the initial</span>
<span class="c1">// hand-shaking</span>
<span class="c1">//</span>
<span class="kt">void</span> <span class="nf">callerCreateIsolate</span><span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
    <span class="c1">//</span>
    <span class="c1">// Local and temporary ReceivePort to retrieve</span>
    <span class="c1">// the new isolate's SendPort</span>
    <span class="c1">//</span>
    <span class="n">ReceivePort</span> <span class="n">receivePort</span> <span class="o">=</span> <span class="n">ReceivePort</span><span class="p">();</span>

    <span class="c1">//</span>
    <span class="c1">// Instantiate the new isolate</span>
    <span class="c1">//</span>
    <span class="n">newIsolate</span> <span class="o">=</span> <span class="k">await</span> <span class="n">Isolate</span><span class="o">.</span><span class="na">spawn</span><span class="p">(</span>
        <span class="n">callbackFunction</span><span class="p">,</span>
        <span class="n">receivePort</span><span class="o">.</span><span class="na">sendPort</span><span class="p">,</span>
    <span class="p">);</span>

    <span class="c1">//</span>
    <span class="c1">// Retrieve the port to be used for further</span>
    <span class="c1">// communication</span>
    <span class="c1">//</span>
    <span class="n">newIsolateSendPort</span> <span class="o">=</span> <span class="k">await</span> <span class="n">receivePort</span><span class="o">.</span><span class="na">first</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">//</span>
<span class="c1">// The entry point of the new isolate</span>
<span class="c1">//</span>
<span class="kd">static</span> <span class="kt">void</span> <span class="nf">callbackFunction</span><span class="p">(</span><span class="n">SendPort</span> <span class="n">callerSendPort</span><span class="p">){</span>
    <span class="c1">//</span>
    <span class="c1">// Instantiate a SendPort to receive message</span>
    <span class="c1">// from the caller</span>
    <span class="c1">//</span>
    <span class="n">ReceivePort</span> <span class="n">newIsolateReceivePort</span> <span class="o">=</span> <span class="n">ReceivePort</span><span class="p">();</span>

    <span class="c1">//</span>
    <span class="c1">// Provide the caller with the reference of THIS isolate's SendPort</span>
    <span class="c1">//</span>
    <span class="n">callerSendPort</span><span class="o">.</span><span class="na">send</span><span class="p">(</span><span class="n">newIsolateReceivePort</span><span class="o">.</span><span class="na">sendPort</span><span class="p">);</span>

    <span class="c1">//</span>
    <span class="c1">// Further processing</span>
    <span class="c1">//</span>
<span class="p">}</span>
</code></pre></div></div>

<p>两个 Isolate 都有了各自的port，那么它们就可以开始互发消息了:</p>

<p>本地Isolate向新Isolate发消息并回收结果：</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Future</span><span class="p">&lt;</span><span class="kt">String</span><span class="p">&gt;</span> <span class="n">sendReceive</span><span class="p">(</span><span class="kt">String</span> <span class="n">messageToBeSent</span><span class="p">)</span> <span class="kd">async</span> <span class="p">{</span>
    <span class="c1">//</span>
    <span class="c1">// We create a temporary port to receive the answer</span>
    <span class="c1">//</span>
    <span class="n">ReceivePort</span> <span class="n">port</span> <span class="o">=</span> <span class="n">ReceivePort</span><span class="p">();</span>

    <span class="c1">//</span>
    <span class="c1">// We send the message to the Isolate, and also</span>
    <span class="c1">// tell the isolate which port to use to provide</span>
    <span class="c1">// any answer</span>
    <span class="c1">//</span>
    <span class="n">newIsolateSendPort</span><span class="o">.</span><span class="na">send</span><span class="p">(</span>
        <span class="n">CrossIsolatesMessage</span><span class="p">&lt;</span><span class="kt">String</span><span class="p">&gt;(</span>
            <span class="nl">sender:</span> <span class="n">port</span><span class="o">.</span><span class="na">sendPort</span><span class="p">,</span>
            <span class="nl">message:</span> <span class="n">messageToBeSent</span><span class="p">,</span>
        <span class="p">)</span>
    <span class="p">);</span>

    <span class="c1">//</span>
    <span class="c1">// Wait for the answer and return it</span>
    <span class="c1">//</span>
    <span class="k">return</span> <span class="n">port</span><span class="o">.</span><span class="na">first</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>本地Isolate被动收消息，还记得上面的<code class="language-plaintext highlighter-rouge">spawn</code>方法吗，第一个参数是 <code class="language-plaintext highlighter-rouge">callbackFunction</code>这个方法就是用来收结果的：</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//</span>
<span class="c1">// Extension of the callback function to process incoming messages</span>
<span class="c1">//</span>
<span class="kd">static</span> <span class="kt">void</span> <span class="nf">callbackFunction</span><span class="p">(</span><span class="n">SendPort</span> <span class="n">callerSendPort</span><span class="p">){</span>
    <span class="c1">//</span>
    <span class="c1">// Instantiate a SendPort to receive message</span>
    <span class="c1">// from the caller</span>
    <span class="c1">//</span>
    <span class="n">ReceivePort</span> <span class="n">newIsolateReceivePort</span> <span class="o">=</span> <span class="n">ReceivePort</span><span class="p">();</span>

    <span class="c1">//</span>
    <span class="c1">// Provide the caller with the reference of THIS isolate's SendPort</span>
    <span class="c1">//</span>
    <span class="n">callerSendPort</span><span class="o">.</span><span class="na">send</span><span class="p">(</span><span class="n">newIsolateReceivePort</span><span class="o">.</span><span class="na">sendPort</span><span class="p">);</span>

    <span class="c1">//</span>
    <span class="c1">// Isolate main routine that listens to incoming messages,</span>
    <span class="c1">// processes it and provides an answer</span>
    <span class="c1">//</span>
    <span class="n">newIsolateReceivePort</span><span class="o">.</span><span class="na">listen</span><span class="p">((</span><span class="kd">dynamic</span> <span class="n">message</span><span class="p">){</span>
        <span class="n">CrossIsolatesMessage</span> <span class="n">incomingMessage</span> <span class="o">=</span> <span class="n">message</span> <span class="k">as</span> <span class="n">CrossIsolatesMessage</span><span class="p">;</span>

        <span class="c1">//</span>
        <span class="c1">// Process the message</span>
        <span class="c1">//</span>
        <span class="kt">String</span> <span class="n">newMessage</span> <span class="o">=</span> <span class="s">"complemented string "</span> <span class="o">+</span> <span class="n">incomingMessage</span><span class="o">.</span><span class="na">message</span><span class="p">;</span>

        <span class="c1">//</span>
        <span class="c1">// Sends the outcome of the processing</span>
        <span class="c1">//</span>
        <span class="n">incomingMessage</span><span class="o">.</span><span class="na">sender</span><span class="o">.</span><span class="na">send</span><span class="p">(</span><span class="n">newMessage</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">}</span>

<span class="c1">//</span>
<span class="c1">// Helper class</span>
<span class="c1">//</span>
<span class="kd">class</span> <span class="nc">CrossIsolatesMessage</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="p">{</span>
    <span class="kd">final</span> <span class="n">SendPort</span> <span class="n">sender</span><span class="p">;</span>
    <span class="kd">final</span> <span class="n">T</span> <span class="n">message</span><span class="p">;</span>

    <span class="n">CrossIsolatesMessage</span><span class="p">({</span>
        <span class="nd">@required</span> <span class="k">this</span><span class="o">.</span><span class="na">sender</span><span class="p">,</span>
        <span class="k">this</span><span class="o">.</span><span class="na">message</span><span class="p">,</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="isolate-的销毁">Isolate 的销毁</h4>
<p>如果创建的Isolate不再使用，那么最好是能将其release掉：</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">dispose</span><span class="p">(){</span>
    <span class="n">newIsolate</span><span class="o">?.</span><span class="na">kill</span><span class="p">(</span><span class="nl">priority:</span> <span class="n">Isolate</span><span class="o">.</span><span class="na">immediate</span><span class="p">);</span>
    <span class="n">newIsolate</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="single-listener-streams">Single-Listener Streams</h4>
<p>实际上Isolate之间的沟通是通过 “Single-Listener” Streams 来实现的。</p>

<h3 id="compute">compute</h3>
<p>上面说过创建 Isolate的方式，其中 compute 适合于创建以后执行任务，而且完成任务后你不希望有任何沟通。
compute是个function：</p>

<ul>
  <li>spawn 一个 Isolate</li>
  <li>运行一个callback，传递一些data，返回结果</li>
  <li>在callback执行完毕时，kill掉isolate</li>
</ul>

<h3 id="适合-isolate-的一些场景">适合 Isolate 的一些场景</h3>

<ul>
  <li>JSON 解析</li>
  <li>encryption 加解密</li>
  <li>图像处理，比如 cropping</li>
  <li>从网络加载图片</li>
</ul>

<p>如何挑选呢？</p>

<p>一般来说：</p>

<ul>
  <li>如果一个方法耗时几十毫秒，用 Future</li>
  <li>如果一个操作需要几百毫秒了，那么就用 Isolate</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[本文主要介绍Flutter中Event Loop以及如何在Flutter中做parallel processing.]]></summary></entry><entry><title type="html">Flutter中InheritedWidget使用的最佳实践</title><link href="https://blog.zhoulujue.com/InheritedWidget/" rel="alternate" type="text/html" title="Flutter中InheritedWidget使用的最佳实践" /><published>2019-02-23T00:00:00+00:00</published><updated>2019-02-23T00:00:00+00:00</updated><id>https://blog.zhoulujue.com/InheritedWidget</id><content type="html" xml:base="https://blog.zhoulujue.com/InheritedWidget/"><![CDATA[<p>本文主要介绍InheritedWidget的设计目的、用法以及推荐的最佳实践</p>

<h1 id="inheritedwidget">InheritedWidget</h1>

<p>Flutter的Widget层级可以做得非常深，在Widget层级间传递数据会让效率变得很低，也会多处很多bolierplate代码。</p>

<p>例如下面的<code class="language-plaintext highlighter-rouge">accountId</code>，<code class="language-plaintext highlighter-rouge">scopeId</code>，如果MyWidget肯本用不到它们，<code class="language-plaintext highlighter-rouge">accountId</code>, <code class="language-plaintext highlighter-rouge">scopeId</code>还是要作为MyWidget的final参数，因为它们要作为构造函数的参数接受参数值并向<code class="language-plaintext highlighter-rouge">MyOtherWidget</code>传递，先得非常冗余，因为MyWidget完全没有必要知道这两个参数。</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MyPage</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">accountId</span><span class="p">;</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">scopeId</span><span class="p">;</span>
  
  <span class="n">MyPage</span><span class="p">(</span><span class="k">this</span><span class="o">.</span><span class="na">accountId</span><span class="p">,</span> <span class="k">this</span><span class="o">.</span><span class="na">scopeId</span><span class="p">);</span>
  
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">new</span> <span class="n">MyWidget</span><span class="p">(</span><span class="n">accountId</span><span class="p">,</span> <span class="n">scopeId</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">MyWidget</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">accountId</span><span class="p">;</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">scopeId</span><span class="p">;</span>
  
  <span class="n">MyWidget</span><span class="p">(</span><span class="k">this</span><span class="o">.</span><span class="na">accountId</span><span class="p">,</span> <span class="k">this</span><span class="o">.</span><span class="na">scopeId</span><span class="p">);</span>
  
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// somewhere down the line</span>
    <span class="k">new</span> <span class="n">MyOtherWidget</span><span class="p">(</span><span class="n">accountId</span><span class="p">,</span> <span class="n">scopeId</span><span class="p">);</span>
    <span class="p">...</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">MyOtherWidget</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">accountId</span><span class="p">;</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">scopeId</span><span class="p">;</span>
  
  <span class="n">MyOtherWidget</span><span class="p">(</span><span class="k">this</span><span class="o">.</span><span class="na">accountId</span><span class="p">,</span> <span class="k">this</span><span class="o">.</span><span class="na">scopeId</span><span class="p">);</span>
  
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// rinse and repeat</span>
    <span class="p">...</span>
</code></pre></div></div>

<p>通过引进InheritedWidget，我们就能解决这个困扰。</p>

<p>InheritedWidget的思想和redux的思想差不多，所谓<code class="language-plaintext highlighter-rouge">accountId</code>，<code class="language-plaintext highlighter-rouge">scopeId</code>实际上是这些widget的状态，用户交互或其他事件会触发Action，Action产生新的State，Widget接受新的State生成新的RenderNode挂载到Widget Tree上，完成一次渲染。</p>

<p>那怎么让这些参数在不同层级之间传递呢？答案是将State往上放，也就是俗称<code class="language-plaintext highlighter-rouge">state up lifting</code></p>

<p>例如下面的代码就是通过插入一个<code class="language-plaintext highlighter-rouge">InheritedWidget</code>将<code class="language-plaintext highlighter-rouge">accountId</code>和<code class="language-plaintext highlighter-rouge">scopeId</code>存储在InheritedWidget描述的这一层级中，所有这一层级和下面的层级都能访问这两个参数。</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MyInheritedWidget</span> <span class="kd">extends</span> <span class="n">InheritedWidget</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">accountId</span><span class="p">;</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">scopeId</span><span class="p">;</span>

  <span class="n">MyInheritedWidget</span><span class="p">(</span><span class="n">accountId</span><span class="p">,</span> <span class="n">scopeId</span><span class="p">,</span> <span class="n">child</span><span class="p">)</span><span class="o">:</span> <span class="k">super</span><span class="p">(</span><span class="n">child</span><span class="p">);</span>
  
  <span class="nd">@override</span>
  <span class="kt">bool</span> <span class="n">updateShouldNotify</span><span class="p">(</span><span class="n">MyInheritedWidget</span> <span class="n">old</span><span class="p">)</span> <span class="o">=</span><span class="p">&gt;</span>
    <span class="n">accountId</span> <span class="o">!=</span> <span class="n">old</span><span class="o">.</span><span class="na">accountId</span> <span class="o">||</span> <span class="n">scopeId</span> <span class="o">!=</span> <span class="n">old</span><span class="o">.</span><span class="na">scopeId</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">MyPage</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">accountId</span><span class="p">;</span>
  <span class="kd">final</span> <span class="kt">int</span> <span class="n">scopeId</span><span class="p">;</span>
  
  <span class="n">MyPage</span><span class="p">(</span><span class="k">this</span><span class="o">.</span><span class="na">accountId</span><span class="p">,</span> <span class="k">this</span><span class="o">.</span><span class="na">scopeId</span><span class="p">);</span>
  
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">new</span> <span class="n">MyInheritedWidget</span><span class="p">(</span>
      <span class="n">accountId</span><span class="p">,</span>
      <span class="n">scopeId</span><span class="p">,</span>
      <span class="kd">const</span> <span class="n">MyWidget</span><span class="p">(),</span>
     <span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">MyWidget</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>

  <span class="kd">const</span> <span class="n">MyWidget</span><span class="p">();</span>
  
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// somewhere down the line</span>
    <span class="kd">const</span> <span class="n">MyOtherWidget</span><span class="p">();</span>
    <span class="p">...</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">MyOtherWidget</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>

  <span class="kd">const</span> <span class="n">MyOtherWidget</span><span class="p">();</span>
  
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">final</span> <span class="n">myInheritedWidget</span> <span class="o">=</span> <span class="n">MyInheritedWidget</span><span class="o">.</span><span class="na">of</span><span class="p">(</span><span class="n">context</span><span class="p">);</span>
    <span class="n">print</span><span class="p">(</span><span class="n">myInheritedWidget</span><span class="o">.</span><span class="na">scopeId</span><span class="p">);</span>
    <span class="n">print</span><span class="p">(</span><span class="n">myInheritedWidget</span><span class="o">.</span><span class="na">accountId</span><span class="p">);</span>
    <span class="p">...</span>
</code></pre></div></div>

<p>注意：</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">InheritedWidget</code>的<code class="language-plaintext highlighter-rouge">child</code>是const修饰的构造函数，这样做的目的是让child能缓存</li>
  <li><code class="language-plaintext highlighter-rouge">accountId</code>或者<code class="language-plaintext highlighter-rouge">scopeId</code>值更新的时候，MyInheritedWidget会被重新创建，但是它的child不一定会被创建，取决于是否用到了<code class="language-plaintext highlighter-rouge">accountId</code>或者<code class="language-plaintext highlighter-rouge">scopeId</code>，上面的这个例子中<code class="language-plaintext highlighter-rouge">MyOtherWidget</code>会被rebuild，但是<code class="language-plaintext highlighter-rouge">MyWidget</code>不会被rebuild</li>
  <li>如果tree被其他事件触发rebuild，例如orientation changes，<code class="language-plaintext highlighter-rouge">InheritedWidget</code>会被rebuild，但是child同样不一定被rebuild，因为<code class="language-plaintext highlighter-rouge">accountId</code>和<code class="language-plaintext highlighter-rouge">scopeId</code>没变。</li>
</ol>

<p>为了让InheritedWidget更加高效，推荐下面的最佳实践：</p>

<h2 id="keep-inherited-widgets-small">Keep inherited widgets small</h2>

<p>尽量让你的Context语义尽可能单一，这样Flutter渲染时才能更细粒度的判断哪些Widget需要rebuild哪些需要复用，否则就失去了<code class="language-plaintext highlighter-rouge">InheritedWidget</code>的意义。</p>

<p><strong>要这样</strong></p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">TeamContext</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">teamId</span><span class="p">;</span>
  <span class="kt">String</span> <span class="n">teamName</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nc">StudentContext</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">studentId</span><span class="p">;</span>
  <span class="kt">String</span> <span class="n">studentName</span><span class="p">;</span>
<span class="p">}</span>
 
<span class="kd">class</span> <span class="nc">ClassContext</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">classId</span><span class="p">;</span>
  <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>不要这样</strong></p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MyAppContext</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">teamId</span><span class="p">;</span>
  <span class="kt">String</span> <span class="n">teamName</span><span class="p">;</span>
  
  <span class="kt">int</span> <span class="n">studentId</span><span class="p">;</span>
  <span class="kt">String</span> <span class="n">studentName</span><span class="p">;</span>
  
  <span class="kt">int</span> <span class="n">classId</span><span class="p">;</span>
  <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="使用const方式来构造child">使用const方式来构造child</h2>

<p>如果没有const，<code class="language-plaintext highlighter-rouge">InheritedWidget</code>的整个child都不会缓存，选择性rebuild整个子tree就不会生效。</p>

<h2 id="管理好context的scope">管理好Context的Scope</h2>

<p>因为Flutter的整个App是一个Tree，所以<code class="language-plaintext highlighter-rouge">InheritedWidget</code>可以放在任意一个层级，这样显然不利于人脑来理解并管理，所以必须大家规约好，例如规定<code class="language-plaintext highlighter-rouge">InheritedWidget</code>只接受<code class="language-plaintext highlighter-rouge">Scaffold</code>作为child，这样所有的<code class="language-plaintext highlighter-rouge">InheritedWidget</code>最大粒度是Page，便于人脑理解及阅读。</p>

<h2 id="不要跨路由访问context">不要跨路由访问context</h2>

<p>Flutter眼里没有page不page的，所有东西就是widget，如果使用<code class="language-plaintext highlighter-rouge">Navigation.push</code>一个Widget请注意route的下一个widget没有继承上一个widget的InheritedWidget的Context，你需要明确将参数传递，而不是将InheriedWidget往上提。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[本文主要介绍InheritedWidget的设计目的、用法以及推荐的最佳实践]]></summary></entry><entry><title type="html">通过Google“样板间”iosched项目看DataBinding进展</title><link href="https://blog.zhoulujue.com/data-binding/" rel="alternate" type="text/html" title="通过Google“样板间”iosched项目看DataBinding进展" /><published>2019-02-22T00:00:00+00:00</published><updated>2019-02-22T00:00:00+00:00</updated><id>https://blog.zhoulujue.com/data-binding</id><content type="html" xml:base="https://blog.zhoulujue.com/data-binding/"><![CDATA[<p>自从若干年前Databinding被引进后，很多人感觉看到了未来，Android的代码写起来更像前端的代码组合方式了。</p>

<p>然而，因为没有类似 webpack 的打包工具，Databinding将Java代码放到xml的做法始终没有Html配合JavaScript那么顺畅。</p>

<p>XML在Android的历史上有很长的时间被视为resource（到现在还被放到res/目录下），好在 AndroidStudio3.0.0后Google引进了<code class="language-plaintext highlighter-rouge">aapt2</code>，看起来要终结这个现象。
我们通过Google的<code class="language-plaintext highlighter-rouge">iosched</code>项目来看一下<code class="language-plaintext highlighter-rouge">DataBinding</code>的推荐用法。</p>

<h2 id="something-about-aapt---android-asset-packaging-tool">Something about AAPT - Android Asset Packaging Tool.</h2>
<blockquote>

  <p>XMLs are also code.</p>

</blockquote>

<p>Aapt2 的设计方向：将 resources 视为代码，将错误提前。</p>

<p>早期的 aapt 在处理resources时，只是将 resource 做一下 indexing，生成 R.java，然后参与Gradle编译，runtime 加载</p>

<p>resource所以，以前经常出现的情况时，在XML里写了个错误的 property / attribute value，编译通过了，跑起来了，点到测试页面崩溃了。</p>

<p>如果你用过 aapt，它其实是没有 compile 这个命令的，Android Studio 或者 Apktool 打包时，都是用的 aapt package，也就是说，只有当 indexing 不了的时候才会出错。</p>

<p>现在 aapt2 提供了 compile 命令，也就是说 gradle 的 assembleReource 的 task 会执行 aapt2 的编译命令，如果你的XML有任何错误，编译时会提示你：例如，引用了不存在的 @string，@style，@drawable。</p>

<h2 id="namespace">namespace</h2>

<p>由于XML 的 namespace 没有严格区分，所以很难一眼看出哪个是 bind adapter 提供的 attribute 哪个不是所以使用时，一定要区分清楚，bind adapter 不要忘了 @{ }</p>

<p><img src="/images/3E735FF2-1B1E-4AA8-A668-AF5D8C1F78A0.png" alt="3698172989fb3f94f89e9a1a20272e1a" /></p>

<hr />

<h2 id="不支持在type里声明">不支持在type里声明&lt;&gt;</h2>

<p>目前IDE及插件不能解决，写起来不太优雅只能通过代码规范来约束</p>

<p><img src="/images/AA220FE0-44D4-4BEC-B2F8-62AAC7FF9B35.png" alt="8019f77473838855e3a8438e1c4f714f" /></p>

<hr />

<h2 id="binding-expression-的缺点">Binding Expression 的缺点</h2>

<ol>
  <li>漏写 @{}，aapt2 不会将这个 expression 识别为一个编译错误，只有当运行起来后才会报错，极有可能延迟错误的发生到 runtime。IDE对 binding expression 的支持是一个 limited support，也就是说不会有代码提示，错误提示等等，增加了犯错的几率。</li>
</ol>

<p><img src="/images/CF5F6F1C-DDB6-4C4E-B4D1-CAC2DAAAC9F3.png" alt="87760e92e1b85ad667205e77670c9f96" /></p>

<p>Bindging expression 还有一个不算缺点的缺点：被限制只能写简单的逻辑。</p>

<p>它的设计目的是为了保证只依赖 <data> 声明的内容，从而使view的依赖单一。</data></p>

<p>首先第一个问题就是<strong>事件</strong>怎么用 <data> 描述，官方推荐的解决方案：LiveData<SomeEventType> eventEmitter</SomeEventType></data></p>

<p>这样ViewModel里会多出来很多无用的 LiveData 即使这些 LiveData 没有装载任何Data，代码可读性不太高。</p>

<p>其实可以等效改为 typed EventBus 来解决，避免冗余的范型展开以及LiveData的状态管理</p>

<p><img src="/images/65741E63-FB7A-4C80-A301-6D1D08DF6C89.png" alt="96eba6d3ad6b4842a4c87ee1861c50d3" /></p>

<p>被限制只能写简单的逻辑，出发点是好的，但是随着业务的复杂化，attribute 的 value 很有可能不是简单的从 ViewModel 的 property/getter 获得，而是拥有了一些逻辑。</p>

<p>此时就要用到 BindingAdapter，通过在方法上添加 @BindingAdapter 来向某个 View 的 attribute 提供 value，也没问题。</p>

<p>问题是，这些方法你要放在哪里？官方推荐做法：放到一个同一个文件里。逻辑没有内聚：可以分门别类到不同的文件中来解决，最好放到 XXXView.java / XXXCell.java 里，如果需要复用，最好抽取 Base类，然后放到 Base类里</p>

<p>读代码时带来的困扰，无法F3和Find Usages：在XML里没有形成依赖，只能通过字符串查找。目前没有办法解决命名冲突：BindingAdapter的名称要用指定好的前缀来避免命名冲突</p>

<p><img src="/images/0CCCF47C-7A80-4AC7-8768-C0CB5CC2281C.png" alt="1e26e750273a8c7e441c927dfbdf1145" /></p>

<p>如果你的XML描述的 View 自治性很好，对外界的依赖很小，那么这种写法其实很好，因为xml中 <data> 对你的单依赖能很好的在 ViewModel 中内消化掉。但是如果你的XML描述的 View 对外界的依赖较高，要么添加更多 <data> 依赖，要么使用复杂的 binding adapter。违背了 DataBinding设计的初衷，所以请将你的XML描述的View的自治性设计的尽量好一些。</data></data></p>]]></content><author><name></name></author><summary type="html"><![CDATA[自从若干年前Databinding被引进后，很多人感觉看到了未来，Android的代码写起来更像前端的代码组合方式了。]]></summary></entry><entry><title type="html">2018的总结</title><link href="https://blog.zhoulujue.com/my-2018/" rel="alternate" type="text/html" title="2018的总结" /><published>2019-01-13T00:00:00+00:00</published><updated>2019-01-13T00:00:00+00:00</updated><id>https://blog.zhoulujue.com/my-2018</id><content type="html" xml:base="https://blog.zhoulujue.com/my-2018/"><![CDATA[<p>前几天和同事聊天，开玩笑说过去一年应该是我入行以来角色变化最多的一年。18年对我来说的确是值得记录一下，跳出了舒适圈，也学到了一些东西。</p>

<h2 id="看过的书">看过的书</h2>

<p>今年没看啥书，而且大量都是半途而废，感觉大量的碎片时间都被我浪费了，值得反思。</p>

<h4 id="python-机器学习及实践"><a href="https://book.douban.com/subject/26886337/">Python 机器学习及实践</a></h4>

<p>年初机器学习的大热，未能免俗。从京东买了一本，大概每天抽出一个小时左右阅读，大概两个星期看完。</p>

<p>因为目标是弄懂大概的知识体系及算法大概，很多Task都没有实践，只在<code class="language-plaintext highlighter-rouge">pycharm</code>玩了玩。算是达成目标吧。</p>

<h4 id="阅微草堂笔记"><a href="https://book.douban.com/subject/1279132/">阅微草堂笔记</a></h4>

<p>大概春天的时候，和老婆去丈母娘家，收拾屋子的时候偶然翻出了一些书，随手拿了一本。没想到拿起后，就一直看了下去，大概看到了十七卷，完成了80%吧，目前也没看完。记得夏天的时候，我去北戴河clubmed也带着，秋天去英国也带着。</p>

<p>总体来说喜欢里面的一些<code class="language-plaintext highlighter-rouge">志怪</code>故事，可能之前看了小泉八云的《奇谭·怪谈》对这些志怪类的小故事比较感兴趣。</p>

<h4 id="用户思维-badass---making-users-awesome"><a href="https://book.douban.com/subject/27129563/">用户思维+ （Badass - making users awesome）</a></h4>

<p>年初的时候，因为工作中需要担任产品的角色，所以想改变一下思维方式。
也是在京东购买的，大概用了一周左右的时间来完成阅读。内容不太多，如果时间允许的话基本上可以一口气读完。</p>

<p>主要收获是两个方面：</p>

<ol>
  <li>如何从用户角度出发看产品：成就用户</li>
  <li>如何更加高效引导用户：感性认知</li>
</ol>

<h4 id="火星救援"><a href="https://book.douban.com/subject/26586492/">火星救援</a></h4>

<p>同事推荐的书。虽然看过了电影，但是书里的语言幽默风趣，读的时候大脑再次加工比电影要丰富很多。</p>

<h4 id="重生"><a href="https://book.douban.com/subject/26910593/">重生</a></h4>

<p>在机场接人，等待的时候在书店看到了，一看是史蒂芬·金的就买了一本。之前读过一本史蒂芬·金的短片小说集，感觉情节设计非常好，也很适合改编为电影。那本书中有个故事叫《尸体》记忆犹新，虽然没有他一贯的那么惊悚，但是给人感觉非常舒缓，后来找到改编的电影《Stand by Me》看了，电影没什么印象，倒是同名主题曲《Stand by Me》非常喜欢。</p>

<p>扯远了，回到这本书，整个故事感觉在前面留下悬念后一直在铺垫，而且特别的长，从主角的小时候到长大成家，最后的“重生”才把谜底揭开。总体上是对信仰的一些探讨，get不到太多点。</p>

<h4 id="算法"><a href="https://book.douban.com/subject/19952400/">算法</a></h4>

<p>买来备查阅用，闲来无事复习了一些算法，不算完整看完过。</p>

<h4 id="大脑整理术"><a href="https://book.douban.com/subject/27079629/">大脑整理术</a></h4>

<p>夏天的时候感觉大脑不够使，所以读了读，有很多生物学的术语都记不太清了，倒是养成了一些对大脑有益的饮食习惯。</p>

<h4 id="闪回电影简史"><a href="https://book.douban.com/subject/10567711/">闪回：电影简史</a></h4>

<p>龙叔说电影上推荐的，就买来看了看，主要在公司中午休息的时候阅读。了解了电影的起源，看到就1940年代就作罢了。完成30%左右，没有看完。</p>

<h4 id="如何欣赏一部电影"><a href="https://book.douban.com/subject/30177151/">如何欣赏一部电影</a></h4>

<p>某个周末去了一趟三里屯，偶然逛了Page One书店，在书店里看到的书。当然还是回家在京东上买的。
像教材一样的书，完成30%，也没有看完。</p>

<h2 id="技术上">技术上</h2>

<p>Android方面除了参与了一些架构上的工作，就没有放太多精力，主要是在Review代码，或者阅读一些库的代码。诶，划水居多。</p>

<h3 id="代码量">代码量</h3>
<p><img src="/images/15473923019403.jpg" alt="Github commits" /></p>

<p>看图说话：比去年少了一些，主要因为年初到8月都在做产品相关的工作。</p>

<h4 id="flutter">Flutter</h4>
<p>一开始大概花了一天时间熟悉了一下语言，然后花了一周时间上手开始写。正好用来做一个新的小项目来试一下。</p>

<p>从提交第一行代码到提交AppStore算了一下大概花了21天，感觉对于简单项目来说Flutter是非常适合的。
因为刚开始写的时候，Flutter还是预览版，所以这期间当然也遇到了很多坑。以后再单开一个文章讨论一下Flutter吧。</p>

<p><img src="/images/731547394077.png" alt="这个那个App" /></p>

<h4 id="整理了之前一些技术分享">整理了之前一些技术分享</h4>

<p>Deeplink相关和FFmpeg相关都发了<a href="https://juejin.im/user/56e22f327664bf0051838031/posts">两篇专栏</a>到掘金上。</p>

<p>FFmpeg的貌似受大家关注更多，应该是18年短视频的火爆带起来了。而且因为这些专栏，认识了一些有趣的人，包括当时在<a href="http://maobing.tv/">猫饼</a>工作的<a href="https://github.com/zhenghuiy">光源</a>，交流了很多视频编辑的经验。</p>

<h4 id="参会">参会</h4>

<ol>
  <li>
    <p>5月去了Google，参加<a href="https://blog.zhoulujue.com/Google-Rising-Star-Meetup/">线下Meetup</a></p>
  </li>
  <li>
    <p>11月去了<code class="language-plaintext highlighter-rouge">安卓绿色联盟开发者大会</code>领了个绿色应用奖(大概是参与奖？)，也听了很多技术上的分享。
<img src="/images/15473923019403431.jpg" alt="安卓绿色联盟开发者大会" /></p>
  </li>
  <li>
    <p>12月又去了Google的线下Meetup，这次遇到很多出海应用的产品和技术，聊的很开心
<img src="/images/154739230194034.jpg" alt="Google rising star meetup" /></p>
  </li>
</ol>

<h2 id="生活上">生活上</h2>

<h3 id="旅行">旅行</h3>

<ol>
  <li>
    <p>年初元旦期间去了海南，去海滩躺了几天很舒服。乘坐环岛高铁时，拍下了一个匆匆的身影
<img src="/images/IMG_20180109_083617_788.jpg" alt="" /></p>
  </li>
  <li>
    <p>6月去了青海，开车绕了青海湖半圈
<img src="/images/20180617_061804-EFFECTS.jpg" alt="" /></p>
  </li>
  <li>
    <p>9月又去了一趟海坨山
<img src="/images/20180915_181940.jpg" alt="" /></p>
  </li>
  <li>
    <p>十一去了趟英国，主要在苏格兰高地附近撒野，下图是斯托尔老人岩。英国乡间开车真的太美了。
<img src="/images/DSC02103.JPG" alt="" /></p>
  </li>
</ol>

<h3 id="自省思考">自省思考</h3>

<p>因为今年在工作上作了若干个角色，开始思考自己的核心价值，意识到其实不管作什么我擅长的其实是<strong>快速学习</strong>，把不熟悉的事物分析清楚并尝试用自己的知识体系去理解，消化后内化为自己的一部分。</p>

<p>意识到自己在工作中的自我反馈。</p>

<p>之前作技术的时候，正反馈和负反馈来得都很快，一般以执行任务，解决问题为主。任务完成，问题解决，反馈就结束。</p>

<p>当输出决策的时候，这个反馈就会变得很长，决策要先形成需求，需求排队等待实现，实现完成等待上线，上线等到覆盖率饱和，这时候才会看到数据，数据给出正/负反馈。
一开始特别不适应，感觉工作起来好像没有成就感（其实是没有耐心或者不自信？），后来也就慢慢习惯了。</p>

<p>…</p>

<h3 id="很多第一次">很多第一次</h3>

<p>第一次和老婆去了一趟酒吧🍺</p>

<p>第一次骑马🐎</p>

<p>第一次作App实名备案，ICP实名备案📱</p>

<p>第一次听古典歌剧🎼</p>

<p>…</p>

<h3 id="最后">最后</h3>

<p>本来想在元旦假期的年终总结，让我拖到现在总算了解了。希望19年能够少一些拖延吧😂</p>

<p>19年愿自己仍然保持对技术的好奇与热情。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[前几天和同事聊天，开玩笑说过去一年应该是我入行以来角色变化最多的一年。18年对我来说的确是值得记录一下，跳出了舒适圈，也学到了一些东西。]]></summary></entry><entry><title type="html">在 Android VM 上 使用 Java8-12</title><link href="https://blog.zhoulujue.com/Using-Java8-12-on-Android-VM/" rel="alternate" type="text/html" title="在 Android VM 上 使用 Java8-12" /><published>2018-12-07T00:00:00+00:00</published><updated>2018-12-07T00:00:00+00:00</updated><id>https://blog.zhoulujue.com/Using-Java8-12-on-Android-VM</id><content type="html" xml:base="https://blog.zhoulujue.com/Using-Java8-12-on-Android-VM/"><![CDATA[<h4 id="tldr不是所有的java8-feature-都会被-d8-desugar">TLDR：“不是所有的Java8 Feature 都会被 D8 desugar”</h4>

<hr />

<p>在 <code class="language-plaintext highlighter-rouge">D8</code> 被引进作为替代 <code class="language-plaintext highlighter-rouge">dx</code> 的dex工具后，越来越多的人问我是不是可以在项目里用原生lambda来替换Retrolambda，或者问各种新特性在Android上的支持程度。</p>

<p>本文主要讨论在Android VM中能否用、以及如何用这些新语言特性。</p>

<h1 id="java8">Java8</h1>
<h3 id="tldr真正能natively用的特性">TLDR：真正能Natively用的特性</h3>

<ul>
  <li>Api Level 24及以上能用 interface methods (default methods or static methods) 和 Method reference</li>
  <li>Api Level 26及以上能用 lambda</li>
</ul>

<h3 id="lambda">lambda</h3>

<ul>
  <li>Q: 在低Api Level上为什么不能 natively 用呢？</li>
  <li>A: 是因为 Native lambda 用到了 VM 指令 invoke-dynamic ，这个指令在低版本上不支持</li>
</ul>

<hr />

<ul>
  <li>
    <p>Q: 在低Api Level上怎么用呢？</p>
  </li>
  <li>
    <p>A1: 用 Retrolambda</p>
  </li>
  <li>
    <p>A2: 或者用 D8 的 desugar:  将 lambda 函数体抽取出来，提成一个静态方法；创建一个内部类，构造函数的参数实现 context capture，内部类里生成一个实例方法调用外部类的静态方法，最后原来调用 lambda 的地方调用 new 内部类的实例方法</p>
  </li>
</ul>

<hr />

<ul>
  <li>
    <p>Q: 如果用了D8，且在D8的命令中指定 min-api 为26，是不是 lambda 就是 Native lambda 了呢？</p>
  </li>
  <li>
    <p>A: 不是！
是因为，Native lambda 采用的是 BootstrapMethod (也就是在 bytecode 第一次执行的时候，用来定义行为的，native lambda 生成的 invoke-dynamic method 就在这时定义)，BootstrapMethod 用到了 java/lang/invoke.* 一些类，可惜 Android runtime不提供这些类，所以还是不行，所以D8为了避免 runtime error ，还是 desugar 了 lambda</p>
  </li>
</ul>

<h3 id="localdatetime">LocalDateTime</h3>

<p>LocalDateTime 的新用法要小心，D8不会帮你 Desugar ，LocalDateTime.now()编写时不会报任何错误，但是跑起来就不一定了，在 26以下会报 ClassNotFound 错</p>

<p>em…. 那就用 ThreeTenBP 吧 😄</p>

<h3 id="interface-methods">Interface methods</h3>

<p>D8 会在编译后 Desugar 好，同样如果指定 –min-api 24 的话，D8 不会 desugar，使用 native interface methods</p>

<h1 id="java9">Java9</h1>

<ul>
  <li>Sad fact : Java9 目前还没有被引进 Android SDK.</li>
  <li>Fun fact : ART 在运行时用 bytecode instructions 支持了某些Java9特性，例如 private interface methods.</li>
</ul>

<p><strong>下面的新特性为了避免因为用得少而陌生，我先把例子写出来，然后再阐述是否可以在AndroidVM上使用。</strong></p>

<h3 id="concise-try-with-resources">Concise Try with Resources</h3>

<p>如果你尝试在 Java 中读取一个流，然后关闭一个流，你应该会写很多 try catch</p>

<p>Java9 中可以这样写了：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.io.*</span><span class="o">;</span>

<span class="kd">class</span> <span class="nc">Java9TryWithResources</span> <span class="o">{</span>
  <span class="nc">String</span> <span class="nf">effectivelyFinalTry</span><span class="o">(</span><span class="nc">BufferedReader</span> <span class="n">r</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">(</span><span class="n">r</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">return</span> <span class="n">r</span><span class="o">.</span><span class="na">readLine</span><span class="o">();</span>
    <span class="o">}</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>这个特性在 Java compiler 中实现，不需要特殊 VM 指令，所以 Android 里随便用</strong></p>

<h3 id="anonymous-diamond">Anonymous Diamond</h3>

<p>钻石语法 Java7 就有了，但是直到 Java9 才有匿名类的钻石语法，像这样：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.util.concurrent.*</span><span class="o">;</span>

<span class="kd">class</span> <span class="nc">Java9AnonymousDiamond</span> <span class="o">{</span>
  <span class="nc">Callable</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">anonymousDiamond</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">Callable</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">call</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Callable</span><span class="o">&lt;&gt;()</span> <span class="o">{</span>
      <span class="nd">@Override</span> <span class="kd">public</span> <span class="nc">String</span> <span class="nf">call</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="s">"Hey"</span><span class="o">;</span>
      <span class="o">}</span>
    <span class="o">};</span>
    <span class="k">return</span> <span class="n">call</span><span class="o">;</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><strong>同样，这个特性在 Java compiler 中实现，不需要特殊 VM 指令，所以 Android 里随便用</strong></p>

<h3 id="private-interface-methods">Private interface methods</h3>

<p>Java8 引入了 default methods &amp; static methods for interfaces, 但是如果想要复用代码且不暴露给外界，必然需要 private methods，所以 Java9 引入了这个特性。</p>

<p>上面说到，D8 在给 Java8 的 interface default methods &amp; static methods 的 desugar 和 non desugar 的情况，这里的 private methods 是一样的。</p>

<p>上面说过的这些特性虽然在Android SDK里包括，但是如果你 javac 后，再 D8，adb push 到 sdcard 上，用 <code class="language-plaintext highlighter-rouge">adb shell dalvikvm -cp /sdcard/path/to/classes.dex</code> 执行的话，会得到正确执行结果。</p>

<h3 id="string-concatenation">String concatenation</h3>

<p>Java9 之前的 String concat，都会在 bytecode 里转换成 StringBuilder，有多少次 concat 就有多少次append</p>

<p>Java9 的 concat 用了 invoke-dynamic 来委托给 <code class="language-plaintext highlighter-rouge">StringConcatFactory</code> 返回这些代码，也就是说，不管里怎么改 concat，实际上编译后的 bytecode 不会频繁发生改变，提高性能。</p>

<p>至此，我们知道 Java9 所有的 language feature 都被 D8 搞定了，虽然 Android SDK 没有引入任何 Java9的 class，但是仍然可以被 Google 魔改的 ART 和 D8 支持。</p>

<p>你只要用了 D8，且 -min-api 21 Java9 特性随便用，至于 Java8 么，呵呵，还要 -min-api 26 才能随便写，what a world!</p>

<h1 id="java10">Java10</h1>

<h4 id="tldr很多新特性都是在-java-compiler-中实现的所以和-runtime-的支持与否无关">TLDR：很多新特性都是在 Java compiler 中实现的，所以和 Runtime 的支持与否无关</h4>

<h3 id="local-variable-type-inference">Local variable type inference</h3>

<p>var: 局部变量的类型推断</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Main</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
    <span class="o">}</span>

    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">localVariableTypeInference</span><span class="o">()</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">url</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;();</span>
        <span class="k">return</span> <span class="n">url</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>同样的，本地变量类型推断仍然是在 java compiler 里做的，所以 Android 里随便用</p>

<h1 id="java11">Java11</h1>

<h3 id="type-inference-for-lambda-parameters">Type inference for lambda parameters</h3>

<p>对 Java10 的类型推断的一个优化，看如下的代码实例，新旧用法对比：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Main</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
    
    <span class="o">}</span>

    <span class="kt">void</span> <span class="nf">lambdaParameterTypeInference</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// old ways</span>
        <span class="nc">Function</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">normal</span> <span class="o">=</span> <span class="o">(</span><span class="nc">String</span> <span class="n">param</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">param</span> <span class="o">+</span> <span class="n">param</span><span class="o">;</span>
        <span class="c1">// now</span>
        <span class="nc">Function</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">function</span> <span class="o">=</span> <span class="o">(</span><span class="kt">var</span> <span class="n">param</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">param</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>虽然这个特性看起来不起眼，因为大家平时用lambda的时候都会隐藏类型声明。但是如果是你想加一些 annotation，那就变的很方便：例如，上述代码如果需要在param上加@Nonnull注解，你不需要去翻阅代码去确认 param 的类型，直接把上述方法改为：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Main</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
    
    <span class="o">}</span>

    <span class="kt">void</span> <span class="nf">lambdaParameterTypeInference</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// old ways</span>
        <span class="nc">Function</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">normal</span> <span class="o">=</span> <span class="o">(</span><span class="nc">String</span> <span class="n">param</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">param</span> <span class="o">+</span> <span class="n">param</span><span class="o">;</span>
        <span class="c1">// now</span>
        <span class="nc">Function</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">function</span> <span class="o">=</span> <span class="o">(</span><span class="nd">@NonNull</span> <span class="kt">var</span> <span class="n">param</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">param</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>同样的，这个特性仍然是在 java compiler 里做的，所以 Android 里随便用</p>

<h3 id="nestmates">Nestmates</h3>

<p>Java11 中Natively支持了嵌套类。
我们知道在以前在Java中编写嵌套的两个类，编译过后这两个类分别是两个class文件，VM里只是用命名规范来区别这两个类，例如如下Java类：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Outer</span> <span class="o">{</span>
  <span class="kd">private</span> <span class="nc">String</span> <span class="n">hello</span> <span class="o">=</span> <span class="s">"hello"</span><span class="o">;</span>
  <span class="kd">class</span> <span class="nc">Inner</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="nf">helloWorld</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">hello</span> <span class="o">+</span> <span class="s">"world!"</span><span class="o">;</span>
    <span class="o">}</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>编译后就会变为：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>javac <span class="k">*</span>.java

<span class="nv">$ </span><span class="nb">ls
</span>Outer.java  Outer.class  Outer<span class="nv">$Inner</span>.class
</code></pre></div></div>

<p>写过内部类的Handler的我们都知道，一旦内部类用了外部类的 method 或者 property 那么在编译时会 capture 外部类的实例，从而造成对外部类的强引用。
解决过OOM的人一定对此深恶痛绝，因为要解决这个问题要写很繁琐的 WeakReference。</p>

<p>在Java11中编译后的类会变为:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>javac <span class="k">*</span>.java

<span class="nv">$ </span>javap <span class="nt">-v</span> <span class="nt">-p</span> <span class="k">*</span>.class
class Outer <span class="o">{</span>
  private java.lang.String name<span class="p">;</span>
<span class="o">}</span>
NestMembers:
  Outer<span class="nv">$Inner</span>

class Outer<span class="nv">$Inner</span> <span class="o">{</span>
  final Outer this<span class="nv">$0</span><span class="p">;</span>

  Outer<span class="nv">$Inner</span><span class="o">(</span>Outer<span class="o">)</span><span class="p">;</span>
    …

  java.lang.String sayHi<span class="o">()</span><span class="p">;</span>
    …
<span class="o">}</span>
NestHost: class Outer
</code></pre></div></div>

<p>注意到<code class="language-plaintext highlighter-rouge">NestMembers</code>和<code class="language-plaintext highlighter-rouge">NestHost</code>属性，在VM中不再是通过命名来区别嵌套类了。这样，在VM级别 Outer 和 Inner 通过可见性关键词就可以互相访问 <code class="language-plaintext highlighter-rouge">package-private</code> and <code class="language-plaintext highlighter-rouge">private</code> 级别及以下的方法和属性。</p>

<p><strong>很遗憾，ART 不支持这个特性，所以得靠 D8 来 desugar，但是貌似D8没有对class文件中的<code class="language-plaintext highlighter-rouge">NestMembers</code>和<code class="language-plaintext highlighter-rouge">NestHost</code>进行支持，所以目前Android中不能用。</strong></p>

<h1 id="java12">Java12</h1>

<p>虽然Java12仍在预览版，不过我们可以通过 EA-build 来了解具体有哪些特性，到写这篇文章时：<code class="language-plaintext highlighter-rouge">expression switch</code> and <code class="language-plaintext highlighter-rouge">string literals</code></p>

<p>expression switch:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">switch</span> <span class="o">(</span><span class="n">s</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">case</span> <span class="s">"hello"</span><span class="o">,</span> <span class="s">"Hello"</span> <span class="o">-&gt;</span> <span class="mi">0</span><span class="o">;</span>
      <span class="k">case</span> <span class="s">"world"</span> <span class="o">-&gt;</span> <span class="mi">1</span><span class="o">;</span>
      <span class="k">default</span> <span class="o">-&gt;</span> <span class="n">s</span><span class="o">.</span><span class="na">length</span><span class="o">();</span>
 <span class="o">}</span>
</code></pre></div></div>
<p>string literals:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">String</span> <span class="n">script</span> <span class="o">=</span> <span class="err">`</span><span class="n">function</span> <span class="nf">hello</span><span class="o">()</span> <span class="o">{</span>
                    <span class="n">print</span><span class="o">(</span><span class="err">'</span><span class="s">"Hello World"</span><span class="err">'</span><span class="o">);</span>
                 <span class="o">}</span>
				
                 <span class="n">hello</span><span class="o">();</span>
                <span class="err">`</span><span class="o">;</span>
</code></pre></div></div>
<p>同样，上述特性都是 Java compiler 支持的，所以 Android 里随便用</p>]]></content><author><name></name></author><summary type="html"><![CDATA[TLDR：“不是所有的Java8 Feature 都会被 D8 desugar”]]></summary></entry><entry><title type="html">Android 上玩转 DeepLink：如何最大程度的向 App 引流</title><link href="https://blog.zhoulujue.com/DeepLink-On-Android/" rel="alternate" type="text/html" title="Android 上玩转 DeepLink：如何最大程度的向 App 引流" /><published>2018-04-10T00:00:00+00:00</published><updated>2018-04-10T00:00:00+00:00</updated><id>https://blog.zhoulujue.com/DeepLink-On-Android</id><content type="html" xml:base="https://blog.zhoulujue.com/DeepLink-On-Android/"><![CDATA[<p>如果你的产品向用户提供网页服务，像 Web 页面或者为移动设备设计的 Html5 页面，那么我猜你
一定会鼓励用户将这些内容分享到其他平台，或者通过信息邮件分享。</p>

<p>一般来说产品经理会用各种机制来鼓励用户主动完成分享，有的产品会对完成分享的用户奖励，
比如积分、优惠券等。<em>分享</em> 的实质是基于用户关系的传播，让更多人接触到你的产品。这些看到
分享链接或者页面的人，如果产生一次点击，你需要尽一切可能把他转化成你的用户。提高点击链接
的效果，也就提高了产品的 <strong>分享转化率</strong>。</p>

<p>所以本文主要解决的问题其实是<strong>如何在 Android 上尽可能提高分享转化率</strong>。</p>

<h1 id="基础设施-url-路由">基础设施: URL 路由</h1>

<p>这是后续步骤的基础，没有这个基础，后面说道的很多事情没有办法完成。
URL路由指的是你的 App 里的产品页面都需要能用户 URL 跳转。Github 上有非常多非常优秀的
URL 路由，像阿里巴巴技术团队的<a href="https://github.com/alibaba/ARouter">ARouter</a>。
你只需要简单配置，加上注解，就可以很快的搭建自己的 URL 路由框架。</p>

<p>下面我们简单介绍一下基本原理。</p>

<p>举个例子，一个新闻 App 提供 <em>新闻详情页</em>、<em>新闻专题页</em>、<em>新闻讨论页</em> 这个3个功能模块。
我们先假设我们要处理的 App 的包名为 <code class="language-plaintext highlighter-rouge">com.zhoulujue.news</code>, 所以这些功能模块的连接
看起来应该是这样：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>指向id=123456的新闻详情页：http://news.zhoulujue.com/article/123456/
指向id=123457的新闻专题页：http://news.zhoulujue.com/story/123457/
指向id=123456的新闻讨论页：http://news.zhoulujue.com/article/123456/comments/
</code></pre></div></div>

<p>再假设这些页面的类名分别为：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>新闻详情页：ArticleActivity
新闻专题页：StoryActivity
新闻讨论页：CommentsActivity
</code></pre></div></div>

<p>所以我们需要一个管理中心，完成两件事情：</p>

<ol>
  <li>将外界传递进来的 URL，分发给各个 Activity 来处理；</li>
  <li>管理 URL 路径和 Activity 的对应关系。</li>
</ol>

<p>为了统一入口，我们创建一个入口 Activity: <code class="language-plaintext highlighter-rouge">RouterActivty</code>，它用来向系统声明 App 能
打开哪些链接，同时接受外界传递过来的 URL。首先我们在 Manifest 里声明它：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;activity</span>
    <span class="na">android:name=</span><span class="s">".RouterActivty"</span>
    <span class="na">android:theme=</span><span class="s">"@android:style/Theme.Translucent.NoTitleBar"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;intent-filter</span> <span class="na">android:autoVerify=</span><span class="s">"true"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;action</span> <span class="na">android:name=</span><span class="s">"android.intent.action.VIEW"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;category</span> <span class="na">android:name=</span><span class="s">"android.intent.category.DEFAULT"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;category</span> <span class="na">android:name=</span><span class="s">"android.intent.category.BROWSABLE"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;data</span>
            <span class="na">android:host=</span><span class="s">"news.zhoulujue.com"</span>
            <span class="na">android:pathPattern=</span><span class="s">"/.*"</span>
            <span class="na">android:scheme=</span><span class="s">"http"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;data</span>
            <span class="na">android:host=</span><span class="s">"news.zhoulujue.com"</span>
            <span class="na">android:pathPattern=</span><span class="s">"/.*"</span>
            <span class="na">android:scheme=</span><span class="s">"https"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/intent-filter&gt;</span>
<span class="nt">&lt;/activity&gt;</span>
</code></pre></div></div>

<p>上面的声明表示，RouterActivty 可以打开所有域名为news.zhoulujue.com 的 https/http
链接。
这个 RouterActivty 在收到 http://news.zhoulujue.com/article/123456/ 后，需要
负责将 /article/123456/ 解析出来，根据 <strong>对应关系</strong> 找到<code class="language-plaintext highlighter-rouge">ArticleActivity</code>，唤起它并且
把<code class="language-plaintext highlighter-rouge">123456</code>这个 id 作为参数传递给<code class="language-plaintext highlighter-rouge">ArticleActivity</code>。</p>

<p>常见的 Router 框架通过在 Activity 的类名上添加注解来管理对应关系：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Route</span><span class="o">(</span><span class="n">path</span> <span class="o">=</span> <span class="s">"/acticel/"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ArticleActivity</span> <span class="n">extend</span> <span class="nc">Activity</span> <span class="o">{</span>
    <span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>

<p>实际上它在处理这个注解的时候生成了一个建造者模式里的 builder，然后向 <strong>管理中心</strong> 注册，说
自己(ArticleActivity)能处理<code class="language-plaintext highlighter-rouge">/acticel/xxx</code>的子域名。</p>

<h1 id="scheme-的选择很重要url-scheme-唤醒">Scheme 的选择很重要：URL Scheme 唤醒</h1>
<p>上面简述原理的时候说道了 Manifest 的声明，我们只声明了 <code class="language-plaintext highlighter-rouge">android:scheme="http"</code>
和 <code class="language-plaintext highlighter-rouge">android:scheme="http"</code> , 但是实际上很多 App 还会用特定 <code class="language-plaintext highlighter-rouge">scheme</code> 的方式来唤起
App，例如在 iOS 早期没有 UniversalLink 的时候，大家这样来唤起。</p>

<p>像淘宝就会用 <code class="language-plaintext highlighter-rouge">tbopen</code>的 scheme，例如 <code class="language-plaintext highlighter-rouge">tbopen://item.taobao.com/item.htm?id=xxxx</code>，当你在网页点击链接以后，页面会创建一个隐藏的 iframe，用它来打开自定义 scheme 的 URL，浏览器无法响应时，向系统发送一个 Action 为 <code class="language-plaintext highlighter-rouge">android.intent.action.VIEW</code>、Data 为 <code class="language-plaintext highlighter-rouge">tbopen://item.taobao.com/item.htm?id=xxxx</code> 的 Intent，如果 App 已经按照上述章节改造，那么系统将唤起 RouterActivity 并将 Intent 传递过去。</p>

<p>所以问题就来了：<strong>如何选取一个 URL Scheme 使得“浏览器无法响应”</strong>，所以你的scheme
最好满足以下两个条件：</p>

<ol>
  <li>区别于其他应用：唯一性</li>
  <li>区别于浏览器已经能处理的 scheme：特殊性</li>
</ol>

<p>在我们上述假设的新闻 App 里，我们可以定义 scheme 为 <code class="language-plaintext highlighter-rouge">zljnews</code>，那么在 URL Scheme
发送的 URL 将会是这样：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>指向id=123456的新闻详情页：zljnews://news.zhoulujue.com/article/123456/
指向id=123457的新闻专题页：zljnews://news.zhoulujue.com/story/123457/
指向id=123456的新闻讨论页：zljnews://news.zhoulujue.com/article/123456/comments/
</code></pre></div></div>

<p>为了避免某些应用会预处理 scheme 和 host，我们还需要将 URL Scheme 的 Host 也做相应
更改：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>指向id=123456的新闻详情页：zljnews://zljnews/article/123456/
指向id=123457的新闻专题页：zljnews://zljnews/story/123457/
指向id=123456的新闻讨论页：zljnews://zljnews/article/123456/comments/
</code></pre></div></div>

<p>这样的我们的 Manifest 里 RouterActivity 的声明要改为：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;activity</span>
    <span class="na">android:name=</span><span class="s">".RouterActivty"</span>
    <span class="na">android:theme=</span><span class="s">"@android:style/Theme.Translucent.NTitleBar"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;intent-filter</span> <span class="na">android:autoVerify=</span><span class="s">"true"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;action</span> <span class="na">android:name=</span><span class="s">"android.intent.action.VIEW"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;category</span> <span class="na">android:name=</span><span class="s">"android.intent.category.DEFAULT"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;category</span> <span class="na">android:name=</span><span class="s">"android.intent.category.BROWSABLE"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;data</span>
            <span class="na">android:host=</span><span class="s">"news.zhoulujue.com"</span>
            <span class="na">android:pathPattern=</span><span class="s">"/.*"</span>
            <span class="na">android:scheme=</span><span class="s">"http"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;data</span>
            <span class="na">android:host=</span><span class="s">"news.zhoulujue.com"</span>
            <span class="na">android:pathPattern=</span><span class="s">"/.*"</span>
            <span class="na">android:scheme=</span><span class="s">"https"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/intent-filter&gt;</span>
    <span class="nt">&lt;intent-filter&gt;</span>
        <span class="nt">&lt;action</span> <span class="na">android:name=</span><span class="s">"android.intent.action.VIEW"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;category</span> <span class="na">android:name=</span><span class="s">"android.intent.category.DEFAULT"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;category</span> <span class="na">android:name=</span><span class="s">"android.intent.category.BROWSABLE"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;data</span> <span class="na">android:scheme=</span><span class="s">"zljnews"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;data</span> <span class="na">android:host=</span><span class="s">"zljnews"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;data</span> <span class="na">android:pathPattern=</span><span class="s">"/.*"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/intent-filter&gt;</span>
<span class="nt">&lt;/activity&gt;</span>
</code></pre></div></div>

<h1 id="app-links-与-universal-links来自官方的方式">App Links 与 Universal Links，来自官方的方式</h1>
<p>我们假设一个用例：用户在印象笔记里写了一篇笔记，笔记里有一个链接：
<code class="language-plaintext highlighter-rouge">http://news.zhoulujue.com/article/123456/</code>。
那么问题来了：用户点击以后，将会发生什么？</p>

<p>答案是：很大的可能是系统弹出一个对话框，列出若干个 App，问你想用哪一个打开。</p>

<p><img src="/images/15262934775428.jpg" alt="选择App列表" /></p>

<p>这样体验其实不够好，因为用户路径变长了，<strong>转化率</strong> 将下降。所以我们应该尽可能去掉这个
对话框，其实上述章节说到了一个方法：将 <code class="language-plaintext highlighter-rouge">http://news.zhoulujue.com/article/123456/</code>
改为 <code class="language-plaintext highlighter-rouge">zljnews://zljnews/article/123456/</code>，原理是我们选取了看起来”唯一性”的 scheme，
但是如果用户没有安装你的 App，这个体验就相当糟糕了，用户在点击以后将没有任何反应。</p>

<p>此时就需要 AppLinks 和 UniversalLinks 了，一言以蔽之，就是域名持有者向系统证明自己
拥有 <code class="language-plaintext highlighter-rouge">news.zhoulujue.com</code> 这个域名并且 App 属于自己，这样系统就会直接将 App 唤起
并把 intent 传递给 App。</p>

<p>如何配置 AppLinks 就不在赘述了，参考<a href="https://developer.android.com/training/app-links/index.html">官方的教程</a>。</p>

<h1 id="app-links-实现的另一种方式">App Links 实现的另一种方式</h1>
<p>Facebook 在2014年的F8开发者大会上公布了 AppLinks 协议，在Android 的 AppLinks之前(Google I/O 15)，
也是一种可行的“链接跳转 App”的方式。
这里也不在赘述细节，可以参考 Facebook 官方的介绍来实现，也特别简单。<a href="https://developers.facebook.com/docs/applinks">Facebook AppLinks</a></p>

<p><img src="/images/app_links_bolt.png" alt="Facebook Bolts On Android" /></p>

<h1 id="非自己的代码怎么办">非自己的代码怎么办</h1>
<p>上面说了很多在网页中唤醒 App 的方式，但是这些都是建立在我们可以改页面 JS 等代码的前提下，
如果页面由第三方提供，举个例子，由广告主提供，表现方式是广告主提供一个落地页放在你的 App 里，
推动第三方去按照你的要求去改动他们的代码，可能比较困难，但是如果只是修改一下跳转链接就可以达到
唤起 App 的效果，这样性价比就比较高了。这个时候就需要 chrome 推荐的 intent scheme 了：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"intent://zljnews/recipe/100390954#Intent;scheme=zljnews;package=com.zhoulujue.news;end"</span><span class="nt">&gt;</span> Intent scheme <span class="nt">&lt;/a&gt;</span>
</code></pre></div></div>

<p>如代码所示，scheme填写的是我们上面假设的 scheme：<code class="language-plaintext highlighter-rouge">zljnews</code>，保持一致。
package 填写 App 包名：<code class="language-plaintext highlighter-rouge">com.zhoulujue.news</code>，参考<a href="https://developer.chrome.com/multidevice/android/intents">Chrome官方 Intent 编写规范</a></p>

<h1 id="微信里怎么办">微信里怎么办</h1>
<p>总所周知，微信是限制唤起 App 的行为的，坊间流传着各种微信唤起的 hack，但总是不知道什么时候就被封禁了，这里介绍
微信官方的 <em>正规</em> 搞法：微下载链接：</p>

<p><img src="/images/wei_xia_zai.jpeg" alt="微信微下载" /></p>

<p>如上图，知乎就使用了微下载来向知乎的 App 导流，这种方式 Android iOS 都是通用的，具体实现方式参考腾讯微信官方的<a href="http://wiki.open.qq.com/index.php?title=mobile/%E5%BA%94%E7%94%A8%E5%AE%9D%E5%BE%AE%E4%B8%8B%E8%BD%BD">文档</a>。</p>

<h1 id="优化1从网页到-app-的无缝体验">优化1：从网页到 App 的无缝体验</h1>
<p>假设一个场景，用户访问 <code class="language-plaintext highlighter-rouge">http://news.zhoulujue.com</code> 阅读新闻时，被推荐下载了 App，此时安装完毕后打开 App后，最好
的体验当然是帮用户打开他没有看完新闻，直接跳转到刚刚在网页版阅读的文章。
最佳实践是：在用户点击下载时，把当前页面的 URL 写到 APK 文件的 ZIP 文件头里，待用户下载安装完毕后，启动时去读取这个
URL，然后结合上面说到过的 Router，路由到新闻详情页。下面跟我来一步一步实现吧。</p>

<h2 id="在网页上下载apk时将路径写如-apk-的-zip-文件头里">在网页上下载APK时：将路径写如 APK 的 ZIP 文件头里</h2>

<p>将下面的 Java 代码保存为 <code class="language-plaintext highlighter-rouge">WriteAPK.java</code> 并用 javac 编译好。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.io.ByteArrayOutputStream</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.io.File</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.io.IOException</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.io.RandomAccessFile</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.nio.ByteBuffer</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.nio.ByteOrder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.zip.ZipFile</span><span class="o">;</span>

<span class="cm">/**
 * Created by michael on 16/9/8.
 */</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WriteApk</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">args</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">args</span><span class="o">[</span><span class="n">i</span><span class="o">]);</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">args</span><span class="o">.</span><span class="na">length</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Wrong parameters! Usage : WriteApk path comment\n"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="nc">String</span> <span class="n">path</span> <span class="o">=</span> <span class="n">args</span><span class="o">[</span><span class="mi">0</span><span class="o">];</span>
        <span class="nc">String</span> <span class="n">comment</span> <span class="o">=</span> <span class="n">args</span><span class="o">[</span><span class="mi">1</span><span class="o">];</span>
        <span class="n">writeApk</span><span class="o">(</span><span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">path</span><span class="o">),</span> <span class="n">comment</span><span class="o">);</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Complete! File lies in "</span> <span class="o">+</span> <span class="n">path</span><span class="o">);</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">ZipFile</span> <span class="n">zipFile</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ZipFile</span><span class="o">(</span><span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">path</span><span class="o">));</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Zip file comment = "</span> <span class="o">+</span> <span class="n">zipFile</span><span class="o">.</span><span class="na">getComment</span><span class="o">());</span>
        <span class="o">}</span> <span class="k">catch</span><span class="o">(</span><span class="nc">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Zip file comment read failed!"</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">writeApk</span><span class="o">(</span><span class="nc">File</span> <span class="n">file</span><span class="o">,</span> <span class="nc">String</span> <span class="n">comment</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">ZipFile</span> <span class="n">zipFile</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
        <span class="nc">ByteArrayOutputStream</span> <span class="n">outputStream</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
        <span class="nc">RandomAccessFile</span> <span class="n">accessFile</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="n">zipFile</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ZipFile</span><span class="o">(</span><span class="n">file</span><span class="o">);</span>
            <span class="nc">String</span> <span class="n">zipComment</span> <span class="o">=</span> <span class="n">zipFile</span><span class="o">.</span><span class="na">getComment</span><span class="o">();</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">zipComment</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="k">return</span><span class="o">;</span>
            <span class="o">}</span>

            <span class="kt">byte</span><span class="o">[]</span> <span class="n">byteComment</span> <span class="o">=</span> <span class="n">comment</span><span class="o">.</span><span class="na">getBytes</span><span class="o">();</span>
            <span class="n">outputStream</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ByteArrayOutputStream</span><span class="o">();</span>

            <span class="n">outputStream</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="n">byteComment</span><span class="o">);</span>
            <span class="n">outputStream</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="n">short2Stream</span><span class="o">((</span><span class="kt">short</span><span class="o">)</span> <span class="n">byteComment</span><span class="o">.</span><span class="na">length</span><span class="o">));</span>

            <span class="kt">byte</span><span class="o">[]</span> <span class="n">data</span> <span class="o">=</span> <span class="n">outputStream</span><span class="o">.</span><span class="na">toByteArray</span><span class="o">();</span>

            <span class="n">accessFile</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RandomAccessFile</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="s">"rw"</span><span class="o">);</span>
            <span class="n">accessFile</span><span class="o">.</span><span class="na">seek</span><span class="o">(</span><span class="n">file</span><span class="o">.</span><span class="na">length</span><span class="o">()</span> <span class="o">-</span> <span class="mi">2</span><span class="o">);</span>
            <span class="n">accessFile</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="n">short2Stream</span><span class="o">((</span><span class="kt">short</span><span class="o">)</span> <span class="n">data</span><span class="o">.</span><span class="na">length</span><span class="o">));</span>
            <span class="n">accessFile</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="n">data</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
        <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">zipFile</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">zipFile</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
                <span class="o">}</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">outputStream</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">outputStream</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
                <span class="o">}</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">accessFile</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">accessFile</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
                <span class="o">}</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>

            <span class="o">}</span>

        <span class="o">}</span>
    <span class="o">}</span>

    <span class="cm">/**
     * 字节数组转换成short（小端序）
     */</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kt">byte</span><span class="o">[]</span> <span class="nf">short2Stream</span><span class="o">(</span><span class="kt">short</span> <span class="n">data</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">ByteBuffer</span> <span class="n">buffer</span> <span class="o">=</span> <span class="nc">ByteBuffer</span><span class="o">.</span><span class="na">allocate</span><span class="o">(</span><span class="mi">2</span><span class="o">);</span>
        <span class="n">buffer</span><span class="o">.</span><span class="na">order</span><span class="o">(</span><span class="nc">ByteOrder</span><span class="o">.</span><span class="na">LITTLE_ENDIAN</span><span class="o">);</span>
        <span class="n">buffer</span><span class="o">.</span><span class="na">putShort</span><span class="o">(</span><span class="n">data</span><span class="o">);</span>
        <span class="n">buffer</span><span class="o">.</span><span class="na">flip</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">buffer</span><span class="o">.</span><span class="na">array</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>然后使用下面的命令对 APK 写入 URL：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$java</span> WriteAPK /path/to/your/APK http://news.zhoulujue.com/article/12345/
</code></pre></div></div>

<h2 id="用户首次打开时读取-url-并打开">用户首次打开时：读取 URL 并打开</h2>
<p>在 App 首次打开的时候读取 ZIP 文件头里你写入的 URL，读取代码如下：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">static</span> <span class="nc">String</span> <span class="nf">getUnfinishedURL</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">//获取缓存的 APK 文件</span>
    <span class="nc">File</span> <span class="n">file</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getPackageCodePath</span><span class="o">());</span>
    <span class="kt">byte</span><span class="o">[]</span> <span class="n">bytes</span><span class="o">;</span>
    <span class="nc">RandomAccessFile</span> <span class="n">accessFile</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
    <span class="c1">// 从指定的位置找到 WriteAPK.java 写入的信息</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">accessFile</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RandomAccessFile</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="s">"r"</span><span class="o">);</span>
        <span class="kt">long</span> <span class="n">index</span> <span class="o">=</span> <span class="n">accessFile</span><span class="o">.</span><span class="na">length</span><span class="o">();</span>
        <span class="n">bytes</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">byte</span><span class="o">[</span><span class="mi">2</span><span class="o">];</span>
        <span class="n">index</span> <span class="o">=</span> <span class="n">index</span> <span class="o">-</span> <span class="n">bytes</span><span class="o">.</span><span class="na">length</span><span class="o">;</span>
        <span class="n">accessFile</span><span class="o">.</span><span class="na">seek</span><span class="o">(</span><span class="n">index</span><span class="o">);</span>
        <span class="n">accessFile</span><span class="o">.</span><span class="na">readFully</span><span class="o">(</span><span class="n">bytes</span><span class="o">);</span>
        <span class="kt">int</span> <span class="n">contentLength</span> <span class="o">=</span> <span class="n">stream2Short</span><span class="o">(</span><span class="n">bytes</span><span class="o">,</span> <span class="mi">0</span><span class="o">);</span>
        <span class="n">bytes</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">byte</span><span class="o">[</span><span class="n">contentLength</span><span class="o">];</span>
        <span class="n">index</span> <span class="o">=</span> <span class="n">index</span> <span class="o">-</span> <span class="n">bytes</span><span class="o">.</span><span class="na">length</span><span class="o">;</span>
        <span class="n">accessFile</span><span class="o">.</span><span class="na">seek</span><span class="o">(</span><span class="n">index</span><span class="o">);</span>
        <span class="n">accessFile</span><span class="o">.</span><span class="na">readFully</span><span class="o">(</span><span class="n">bytes</span><span class="o">);</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">String</span><span class="o">(</span><span class="n">bytes</span><span class="o">,</span> <span class="s">"utf-8"</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
    <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">accessFile</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="n">accessFile</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">IOException</span> <span class="n">ignored</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">ignored</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>    
<span class="o">}</span>
</code></pre></div></div>

<p>接着只要将<code class="language-plaintext highlighter-rouge">getUnfinishedURL</code>返回值交给 Router 去处理，从而将用户导向没有阅读完毕的新闻详情页。</p>

<h2 id="优化2有控制的允许流量的导出">优化2：有控制的允许流量的导出</h2>
<p>上面的内容都是在讲如何尽可能地把用户导进 App 里来，从另外一个角度，为了提高用户转化率我们要降低用户的<strong>跳出率</strong>，也就是说尽量避免用户从我们的 App 里被带跑了。</p>

<p>很多情况下，如果我们运营一个 UGC 的社区，我们无法控制用户创建内容的时候会填写哪些 URL，当然作为一个开放的平台我们肯定希望用户能够更高地利用各种工具将他们所专注的任务完成。</p>

<p>但是如果平台出现了一些人不受限制的发广告，或者利用你的平台运营竞争对手的产品，这种方式对成长中的产品打击有可能将是毁灭性的。</p>

<p>最佳实践：在服务器维护一个白名单，这个白名单中被允许的域名将被允许唤醒，否则拦截。</p>

<p>而这个拦截最好的方式是在WebView里，因为大多数跳转代码都在 URL 指向的落地页里。所以我们需要这样定义<code class="language-plaintext highlighter-rouge">WebView</code>的<code class="language-plaintext highlighter-rouge">WebViewClient</code></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ControlledWebViewClient</span> <span class="kd">extends</span> <span class="nc">WebViewClient</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">shouldOverrideUrlLoading</span><span class="o">(</span><span class="nc">WebView</span> <span class="n">view</span><span class="o">,</span> <span class="nc">String</span> <span class="n">url</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Context</span> <span class="n">context</span> <span class="o">=</span>  <span class="n">view</span><span class="o">.</span><span class="na">getContext</span><span class="o">();</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">String</span> <span class="n">host</span> <span class="o">=</span> <span class="nc">Uri</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="n">url</span><span class="o">.</span><span class="na">getOriginalUrl</span><span class="o">()).</span><span class="na">getHost</span><span class="o">();</span>
            <span class="k">if</span> <span class="o">(!</span><span class="n">isHostInWhiteList</span><span class="o">(</span><span class="n">host</span><span class="o">))</span> <span class="o">{</span>
                <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
            <span class="o">}</span>
            
            <span class="nc">String</span> <span class="n">scheme</span> <span class="o">=</span> <span class="nc">Uri</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="n">url</span><span class="o">).</span><span class="na">getScheme</span><span class="o">();</span>
            <span class="k">if</span> <span class="o">(!</span><span class="nc">TextUtils</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">(</span><span class="n">scheme</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">scheme</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="s">"http"</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">scheme</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="s">"https"</span><span class="o">))</span> <span class="o">{</span>
                <span class="nc">Intent</span> <span class="n">intent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Intent</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">ACTION_VIEW</span><span class="o">,</span> <span class="nc">Uri</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="n">url</span><span class="o">));</span>
                <span class="n">intent</span><span class="o">.</span><span class="na">addFlags</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">FLAG_ACTIVITY_SINGLE_TOP</span><span class="o">);</span>
                <span class="n">intent</span><span class="o">.</span><span class="na">addFlags</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">FLAG_ACTIVITY_NEW_TASK</span><span class="o">);</span>
                <span class="n">context</span><span class="o">.</span><span class="na">getApplicationContext</span><span class="o">().</span><span class="na">startActivity</span><span class="o">(</span><span class="n">intent</span><span class="o">);</span>
                <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
            <span class="o">}</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Throwable</span> <span class="n">t</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">t</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">isHostInWhiteList</span><span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 查询白名单，是否在白名单里</span>
        <span class="o">...</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>为了尽可能获取正确的 Host，请注意在上面第7行代码里，使用的是<code class="language-plaintext highlighter-rouge">url.getOriginalUrl()</code></p>

<hr />

<p>好了，App 里面利用链接跳来跳去的事情基本上就讲完了，希望对你有帮助。如果你还有什么建议，可以通过扫描下面的二维码联系我，或者在下面留言哦~</p>]]></content><author><name></name></author><summary type="html"><![CDATA[如果你的产品向用户提供网页服务，像 Web 页面或者为移动设备设计的 Html5 页面，那么我猜你 一定会鼓励用户将这些内容分享到其他平台，或者通过信息邮件分享。]]></summary></entry><entry><title type="html">编译并裁剪 FFmpeg 在 Android 上做视频编辑</title><link href="https://blog.zhoulujue.com/edit-local-video-using-customized-ffmpeg-so/" rel="alternate" type="text/html" title="编译并裁剪 FFmpeg 在 Android 上做视频编辑" /><published>2017-09-01T00:00:00+00:00</published><updated>2017-09-01T00:00:00+00:00</updated><id>https://blog.zhoulujue.com/edit-local-video-using-customized-ffmpeg-so</id><content type="html" xml:base="https://blog.zhoulujue.com/edit-local-video-using-customized-ffmpeg-so/"><![CDATA[<p>众所周知，Android 对涉及底层硬件的 API 控制力都比较弱，从其难用的 Camera/Camera2、MediaCodec 等 API 就可见一斑。</p>

<p>最近项目中有需要对视频进行编辑的需求，总体分析有如下技术上需要实现的点：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 需要支持视频尺寸裁剪，给出左上角和右下角的坐标后裁剪两个点描述的区域；

2. 需要支持帧预览，裁剪前需要向用户展示时间线上的预览图；

3. 需要支持截取视频，给出开始时间和结束时间后截取这两个时间点之间的视频段落。
</code></pre></div></div>

<hr />

<h2 id="mediacodec-方案">MediaCodec 方案</h2>

<p>首先，按照 Android 官方的文档推荐，当然首推 MediaCodec。</p>

<p><img src="/images/ffmpeg1.png" alt="MediaCodec 编解码" /></p>

<ol>
  <li>
    <p><strong>MediaCodec 尺寸裁减</strong></p>

    <p>首先用 inputBuffers 读取帧数据到 outputBuffers，如果需要使用 MediaCodec 裁减尺寸，按照上图 MediaCodec 的流程以及官方的文档，需要在处理 output buffer 时将每一帧的数据处理为 bitmap 然后根据左上角的坐标和右下角的坐标对图像进行裁减 <a href="https://developer.android.com/reference/android/graphics/Bitmap.html#createBitmap%28android.graphics.Bitmap,%20int,%20int,%20int,%20int%29">Bitmap.createBitmap</a>
实际上这样裁减的过程还是在利用 CPU 来进行裁减</p>
  </li>
  <li>
    <p><strong>MediaCodec 取帧</strong></p>

    <p>使用<a href="https://developer.android.com/reference/android/media/MediaMetadataRetriever.html">MediaMetadataRetriever</a></p>
  </li>
  <li>
    <p><strong>MediaCodec 截取</strong></p>

    <p>截取实际上在第一步的 output 就可以做了，因为 outputbuffer 里每一帧的数据就有时间戳信息，<a href="https://developer.android.com/reference/android/media/MediaCodec.BufferInfo.html#presentationTimeUs">MediaCodec.BufferInfo.presentationTimeUs</a></p>
  </li>
</ol>

<hr />

<h2 id="mediacodec-的问题">MediaCodec 的问题</h2>

<p>怎么样，看起来这套方案还是不错的，但是实际操作下来有几个严重的问题：</p>

<ol>
  <li>首先不是所有设备的 DSP 芯片都支持你需要的 codec 对应的编码器，而且编码器支持特性相当有限：
具体参考<a href="https://github.com/WeMobileDev/article/blob/master/%E5%BE%AE%E4%BF%A1Android%E8%A7%86%E9%A2%91%E7%BC%96%E7%A0%81%E7%88%AC%E8%BF%87%E7%9A%84%E9%82%A3%E4%BA%9B%E5%9D%91.md">微信团队对 MediaCodec 编码器的研究</a>
    <blockquote>
      <p>如果使用MediaCodec来编码H264视频流，对于H264格式来说，会有一些针对压缩率以及码率相关的视频质量设置，典型的诸如Profile(baseline, main, high)，Profile Level, Bitrate mode(CBR, CQ, VBR)，合理配置这些参数可以让我们在同等的码率下，获得更高的压缩率，从而提升视频的质量，Android也提供了对应的API进行设置，可以设置到MediaFormat中这些设置项:</p>

      <p><code class="language-plaintext highlighter-rouge">MediaFormat.KEY_BITRATE_MODE</code></p>

      <p><code class="language-plaintext highlighter-rouge">MediaFormat.KEY_PROFILE</code></p>

      <p><code class="language-plaintext highlighter-rouge">MediaFormat.KEY_LEVEL</code></p>

      <p>但问题是，对于Profile，Level, Bitrate mode这些设置，在大部分手机上都是不支持的，即使是设置了最终也不会生效，例如设置了Profile为high，最后出来的视频依然还会是Baseline….</p>
    </blockquote>
  </li>
  <li>其次，MediaMetadataRetriever 实测也不太好用，在某些机型上会出现取不到帧的情况。
于是决定弃用 MediaCodec 转投如日中天的 FFmpeg。</li>
</ol>

<hr />

<h2 id="ffmpeg">FFmpeg</h2>

<p>FFmpeg 由于其丰富的 codec 插件，详细的文档说明，并且与其调试复杂量大的编解码代码(是的，用 MediaCodec 实现起来十分啰嗦和繁琐)还是不如调试一行 ffmpeg 命令来的简单。</p>

<p>利用 FFmpeg 做视频编辑大家一般都会去参考这个 <a href="https://github.com/WritingMinds/ffmpeg-android-java">repo</a> ，但是他的 asset 里面的 ffmpeg 大小高达 18MB，即使压缩进 APK 包里也会达到 9MB。对 APK 大小敏感的开发者肯定颇有微词。
ffmpeg-android-java 的原理很简单，交叉编译好可执行的 ffmpeg 二进制文件放到 asset 里，安装后释放二进制文件到 /data/data/ 里，用 Shell command 的形式去执行这个文件，好处是没有任何依赖(依赖全打进二进制了)，稳定可靠(不需要动态加载)。
坏处就很明显了，因为是二进制文件，所以 size 会很大。</p>

<p>于是，果断放弃这种方式，转而编译 ffmpeg 的 so 库，动态加载然后执行命令。听起来不错，对不对？动态库的大小肯定比 ffmpeg-android-java 的 executable 要小多了，而且自己编译 ffmpeg 还能对其进行裁减。</p>

<hr />

<h2 id="交叉编译-ffmpeg-及-x264">交叉编译 FFmpeg 及 x264</h2>

<p>相信很多开发者都会使用 ijkplayer，ijkplayer 底层也用到了 ffmpeg，ijk使用的是 so 库的形式，libffmpeg.so。所以最理想的状态是，重新编译一个公共的 libffmpeg.so，这个 libffmpeg.so 即有 ijk 需要的 decoders 和视频编辑模块需要的 encoders。但是一旦 ijk 或者 ffmpeg 有升级就会很麻烦，因为得重新编译一次 ffmpeg，而且还得 fork ijkplayer，然后每当 ijk 更新的时候将 ijkplayer master 合并到你 fork 分支，视频播放又是很常用的模块，很难做到“无痛”升级。</p>

<p>如果不动 ijk 的 ffmpeg，单独为视频编辑模块编译一个 ffmpeg.so ，与视频播放模块隔离开，这样就可以无痛升级 ijk 依赖 ffmpeg 的视频播放库了。但是，问题来了，如果存在两个 ffmpeg 的话不可避免的会存在冗余。所以编译视频编辑模块的 ffmpeg 时，要裁剪他的 encoders 和 decoders 尽量做到两个 ffmpeg 模块是正交的就 ok了。</p>

<p>交叉编译 FFmpeg 的过程就不赘述，网上有太多教程，这里简单记录一下编译的步骤：</p>

<ol>
  <li>
    <p>同步 x264 的 <a href="https://github.com/yixia/x264">repo</a>，这里我选择的是 YIXIA INC 的 mirror.</p>
  </li>
  <li>
    <p>编写编译脚本：</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c">#!/bin/bash</span>

 <span class="k">if</span> <span class="o">[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$ANDROID_NDK</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
 	</span><span class="nb">echo</span> <span class="s2">"You must define ANDROID_NDK before starting."</span>
 	<span class="nb">echo</span> <span class="s2">"They must point to your NDK directories.</span><span class="se">\n</span><span class="s2">"</span>
 	<span class="nb">exit </span>1
 <span class="k">fi</span>

 <span class="c"># Detect OS</span>
 <span class="nv">OS</span><span class="o">=</span><span class="sb">`</span><span class="nb">uname</span><span class="sb">`</span>
 <span class="nv">HOST_ARCH</span><span class="o">=</span><span class="sb">`</span><span class="nb">uname</span> <span class="nt">-m</span><span class="sb">`</span>
 <span class="nb">export </span><span class="nv">CCACHE</span><span class="o">=</span><span class="p">;</span> <span class="nb">type </span>ccache <span class="o">&gt;</span>/dev/null 2&gt;&amp;1 <span class="o">&amp;&amp;</span> <span class="nb">export </span><span class="nv">CCACHE</span><span class="o">=</span>ccache
 <span class="k">if</span> <span class="o">[</span> <span class="nv">$OS</span> <span class="o">==</span> <span class="s1">'Linux'</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
 	</span><span class="nb">export </span><span class="nv">HOST_SYSTEM</span><span class="o">=</span>linux-<span class="nv">$HOST_ARCH</span>
 <span class="k">elif</span> <span class="o">[</span> <span class="nv">$OS</span> <span class="o">==</span> <span class="s1">'Darwin'</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
 	</span><span class="nb">export </span><span class="nv">HOST_SYSTEM</span><span class="o">=</span>darwin-<span class="nv">$HOST_ARCH</span>
 <span class="k">fi

 </span><span class="nv">NDK</span><span class="o">=</span>/Users/xxx/Library/Android/sdk/ndk-bundle

 <span class="nv">SOURCE</span><span class="o">=</span><span class="sb">`</span><span class="nb">pwd</span><span class="sb">`</span>
 <span class="nv">PREFIX</span><span class="o">=</span><span class="nv">$SOURCE</span>/build/android
 <span class="nv">TOOLCHAIN</span><span class="o">=</span><span class="nv">$NDK</span>/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64
 <span class="nv">SYSROOT</span><span class="o">=</span><span class="nv">$NDK</span>/platforms/android-16/arch-arm/
 <span class="nv">ADDI_CFLAGS</span><span class="o">=</span><span class="s2">"-marm"</span>
 <span class="c">#EXTRA_CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=neon -D__ARM_ARCH_7__ -D__ARM_ARCH_7A__"</span>
 <span class="c">#EXTRA_LDFLAGS="-nostdlib"</span>

 ./configure  <span class="nt">--prefix</span><span class="o">=</span><span class="nv">$PREFIX</span> <span class="se">\</span>
 	<span class="nt">--cross-prefix</span><span class="o">=</span><span class="nv">$TOOLCHAIN</span>/bin/arm-linux-androideabi- <span class="se">\</span>
 	<span class="nt">--enable-pic</span> <span class="se">\</span>
 	<span class="nt">--enable-shared</span> <span class="se">\</span>
 	<span class="nt">--enable-static</span> <span class="se">\</span>
 	<span class="nt">--enable-strip</span> <span class="se">\</span>
 	<span class="nt">--disable-cli</span> <span class="se">\</span>
 	<span class="nt">--host</span><span class="o">=</span>arm-linux <span class="se">\</span>
 	<span class="nt">--sysroot</span><span class="o">=</span><span class="nv">$SYSROOT</span> <span class="se">\</span>
 	<span class="nt">--extra-cflags</span><span class="o">=</span><span class="s2">"-Os -fpic </span><span class="nv">$ADDI_CFLAGS</span><span class="s2"> </span><span class="nv">$EXTRA_CFLAGS</span><span class="s2">"</span> <span class="se">\</span>
 	<span class="nt">--extra-ldflags</span><span class="o">=</span><span class="s2">"</span><span class="nv">$ADDI_LDFLAGS</span><span class="s2"> </span><span class="nv">$EXTRA_LDFLAGS</span><span class="s2">"</span>

 make clean
 make <span class="nv">STRIP</span><span class="o">=</span> <span class="nt">-j4</span> <span class="nb">install</span> <span class="o">||</span> <span class="nb">exit </span>1
</code></pre></div>    </div>

    <p><img src="/images/ffmpeg2.png" alt="x264编译脚本" /></p>
  </li>
  <li>
    <p>找到x264 repo 的根目录下的 configure 文件，找到 <code class="language-plaintext highlighter-rouge">echo "SONAME=libx264.so.$API" &gt;&gt; config.mak</code> 改为 <code class="language-plaintext highlighter-rouge">echo "SONAME=libx264-$API.so" &gt;&gt; config.mak</code></p>
  </li>
  <li>
    <p>执行编译脚本进行编译，结果在会在 <code class="language-plaintext highlighter-rouge">build/</code> 文件夹下</p>
  </li>
  <li>
    <p>接下来编译 FFmpeg, 先同步 ffmpeg 的 <a href="https://github.com/FFmpeg/FFmpeg">repo</a></p>
  </li>
  <li>
    <p>编写编译脚本：</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> #!/bin/bash
 export TMPDIR=/Users/xxx/ffmpegbuilddir/temp/
 NDK=/Users/xxx/Library/Android/sdk/ndk-bundle
 SYSROOT=$NDK/platforms/android-16/arch-arm/
 TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64

 CPU=arm
 PREFIX=/Users/xxx/ffmpegbuilddir/ffmpeg-install-dir/arm/
 ADDI_CFLAGS="-marm"

 # 加入x264编译库
 EXTRA_DIR=./../path/to/your/x264/repo/build/android
 EXTRA_CFLAGS="-I./${EXTRA_DIR}/include"
 EXTRA_LDFLAGS="-L./${EXTRA_DIR}/lib"

 function build_one
 {
 ./configure \
 --prefix=$PREFIX \
 --enable-gpl \
 --enable-libx264 \
 --enable-shared \
 --enable-filter=crop \
 --enable-filter=rotate \
 --enable-filter=scale \
 --disable-encoders \
 --enable-encoder=mpeg4 \
 --enable-encoder=aac \
 --enable-encoder=png \
 --enable-encoder=libx264 \
 --enable-encoder=gif \
 --disable-decoders \
 --enable-decoder=mpeg4 \
 --enable-decoder=h264 \
 --enable-decoder=aac \
 --enable-decoder=gif \
 --enable-parser=h264 \
 --disable-static \
 --disable-doc \
 --disable-ffmpeg \
 --disable-ffplay \
 --disable-ffprobe \
 --disable-ffserver \
 --disable-doc \
 --disable-symver \
 --enable-small \
 --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
 --target-os=linux \
 --arch=arm \
 --enable-cross-compile \
 --sysroot=$SYSROOT \
 --extra-cflags="-Os -fpic $ADDI_CFLAGS $EXTRA_CFLAGS" \
 --extra-ldflags="$ADDI_LDFLAGS $EXTRA_LDFLAGS" \
 $ADDITIONAL_CONFIGURE_FLAG
 make clean
 make
 make install

 }

 build_one
 say "Your building has been completed!"
</code></pre></div>    </div>

    <p><img src="/images/ffmpeg3.png" alt="FFmpeg shared lib 编译脚本" /></p>
  </li>
  <li>
    <p>执行编译脚本，编译结果会在 /Users/xxx/ffmpegbuilddir/ffmpeg-install-dir/arm/ 目录下</p>
  </li>
  <li>
    <p>到此，你已经拥有了能在 arm 平台上 load 的 so 文件</p>
  </li>
</ol>

<hr />

<h2 id="编写-jni-来调用-ffmpeg">编写 jni 来调用 ffmpeg</h2>

<p>在上面的编译脚本中，我们考虑到 so 的输出大小，configure 中有这么一行 <code class="language-plaintext highlighter-rouge">--disable-ffmpeg</code>，意为不编译 ffmpeg 的可执行文件，这样我们就没有 ffmpeg 的执行入口，相当于没有 <code class="language-plaintext highlighter-rouge">main()</code>函数。所以，我们需要为这些 so 文件编写一个命令执行的入口，这方面也有超多的教程，过程就不深究了，同样这里也只记录一下编译步骤：</p>

<ol>
  <li>
    <p>在你的 Android Studio 工程里新建一个目录，例如： jni/</p>
  </li>
  <li>
    <p>将 ffmpeg repo 中的 ffmpeg.c、ffmpeg.h、FFmpegNativeHelper.c、cmdutils.c、ffmpeg_opt.c、ffmpeg_filter.c、show_func_wrapper.c 拷贝到 jni</p>
  </li>
  <li>
    <p>编写 makefile:</p>

    <div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">ifeq</span> <span class="nv">($(APP_ABI), x86)</span>
 <span class="nv">LIB_NAME_PLUS</span> <span class="o">:=</span> x86
 <span class="k">else</span>
 <span class="nv">LIB_NAME_PLUS</span> <span class="o">:=</span> armeabi
 <span class="k">endif</span>

 <span class="nv">LOCAL_PATH</span><span class="o">:=</span> <span class="nf">$(</span><span class="nb">call</span> my-dir<span class="nf">)</span>

 <span class="k">include</span><span class="sx"> $(CLEAR_VARS)</span>
 <span class="nv">LOCAL_MODULE</span> <span class="o">:=</span> x264-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span>
 <span class="nv">LOCAL_SRC_FILES</span> <span class="o">:=</span> prebuilt/<span class="nv">$(LIB_NAME_PLUS)</span>/libx264-148.so
 <span class="k">include</span><span class="sx"> $(PREBUILT_SHARED_LIBRARY)</span>

 <span class="k">include</span><span class="sx"> $(CLEAR_VARS)</span>
 <span class="nv">LOCAL_MODULE</span><span class="o">:=</span> avcodec-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span>
 <span class="nv">LOCAL_SRC_FILES</span><span class="o">:=</span> prebuilt/<span class="nv">$(LIB_NAME_PLUS)</span>/libavcodec-57.so
 <span class="k">include</span><span class="sx"> $(PREBUILT_SHARED_LIBRARY)</span>

 <span class="k">include</span><span class="sx"> $(CLEAR_VARS)</span>
 <span class="nv">LOCAL_MODULE</span><span class="o">:=</span> avdevice-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span>
 <span class="nv">LOCAL_SRC_FILES</span><span class="o">:=</span> prebuilt/<span class="nv">$(LIB_NAME_PLUS)</span>/libavdevice-57.so
 <span class="k">include</span><span class="sx"> $(PREBUILT_SHARED_LIBRARY)</span>

 <span class="k">include</span><span class="sx"> $(CLEAR_VARS)</span>
 <span class="nv">LOCAL_MODULE</span><span class="o">:=</span> avfilter-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span>
 <span class="nv">LOCAL_SRC_FILES</span><span class="o">:=</span> prebuilt/<span class="nv">$(LIB_NAME_PLUS)</span>/libavfilter-6.so
 <span class="k">include</span><span class="sx"> $(PREBUILT_SHARED_LIBRARY)</span>

 <span class="k">include</span><span class="sx"> $(CLEAR_VARS)</span>
 <span class="nv">LOCAL_MODULE</span><span class="o">:=</span> avformat-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span>
 <span class="nv">LOCAL_SRC_FILES</span><span class="o">:=</span> prebuilt/<span class="nv">$(LIB_NAME_PLUS)</span>/libavformat-57.so
 <span class="k">include</span><span class="sx"> $(PREBUILT_SHARED_LIBRARY)</span>

 <span class="k">include</span><span class="sx"> $(CLEAR_VARS)</span>
 <span class="nv">LOCAL_MODULE</span> <span class="o">:=</span>  avutil-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span>
 <span class="nv">LOCAL_SRC_FILES</span> <span class="o">:=</span> prebuilt/<span class="nv">$(LIB_NAME_PLUS)</span>/libavutil-55.so
 <span class="k">include</span><span class="sx"> $(PREBUILT_SHARED_LIBRARY)</span>

 <span class="k">include</span><span class="sx"> $(CLEAR_VARS)</span>
 <span class="nv">LOCAL_MODULE</span> <span class="o">:=</span> swresample-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span>
 <span class="nv">LOCAL_SRC_FILES</span> <span class="o">:=</span> prebuilt/<span class="nv">$(LIB_NAME_PLUS)</span>/libswresample-2.so
 <span class="k">include</span><span class="sx"> $(PREBUILT_SHARED_LIBRARY)</span>

 <span class="k">include</span><span class="sx"> $(CLEAR_VARS)</span>
 <span class="nv">LOCAL_MODULE</span> <span class="o">:=</span> swscale-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span>
 <span class="nv">LOCAL_SRC_FILES</span> <span class="o">:=</span> prebuilt/<span class="nv">$(LIB_NAME_PLUS)</span>/libswscale-4.so
 <span class="k">include</span><span class="sx"> $(PREBUILT_SHARED_LIBRARY)</span>

 <span class="k">include</span><span class="sx"> $(CLEAR_VARS)</span>
 <span class="nv">LOCAL_MODULE</span> <span class="o">:=</span> postproc-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span>
 <span class="nv">LOCAL_SRC_FILES</span> <span class="o">:=</span> prebuilt/<span class="nv">$(LIB_NAME_PLUS)</span>/libpostproc-54.so
 <span class="k">include</span><span class="sx"> $(PREBUILT_SHARED_LIBRARY)</span>

 <span class="k">include</span><span class="sx"> $(CLEAR_VARS)</span>

 <span class="nv">LOCAL_MODULE</span> <span class="o">:=</span> libffmpegjni

 <span class="k">ifeq</span> <span class="nv">($(APP_ABI), x86)</span>
 <span class="nv">TARGET_ARCH</span><span class="o">:=</span>x86
 <span class="nv">TARGET_ARCH_ABI</span><span class="o">:=</span>x86
 <span class="k">else</span>
 <span class="nv">LOCAL_ARM_MODE</span> <span class="o">:=</span> arm
 <span class="k">endif</span>

 <span class="nv">LOCAL_SRC_FILES</span> <span class="o">:=</span> FFmpegNativeHelper.c <span class="se">\</span>
                    cmdutils.c <span class="se">\</span>
                    ffmpeg_opt.c <span class="se">\</span>
                    ffmpeg_filter.c <span class="se">\</span>
                    show_func_wrapper.c

 <span class="nv">LOCAL_LDLIBS</span> <span class="o">:=</span> <span class="nt">-L</span><span class="nv">$(SYSROOT)</span>/usr/lib <span class="nt">-llog</span> <span class="nt">-lz</span>

 <span class="nv">LOCAL_SHARED_LIBRARIES</span><span class="o">:=</span> avcodec-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          avdevice-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          avfilter-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          avformat-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          avutil-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          swresample-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          swscale-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          postproc-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          x264-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span>

 <span class="nv">LOCAL_C_INCLUDES</span> <span class="o">+=</span> <span class="nt">-L</span><span class="nv">$(SYSROOT)</span>/usr/include
 <span class="nv">LOCAL_C_INCLUDES</span> <span class="o">+=</span> <span class="nv">$(LOCAL_PATH)</span>/include

 <span class="k">ifeq</span> <span class="nv">($(APP_ABI), x86)</span>
 <span class="nv">LOCAL_CFLAGS</span> <span class="o">:=</span> <span class="nt">-DUSE_X86_CONFIG</span>
 <span class="k">else</span>
 <span class="nv">LOCAL_CFLAGS</span> <span class="o">:=</span> <span class="nt">-DUSE_ARM_CONFIG</span>
 <span class="k">endif</span>

 <span class="k">include</span><span class="sx"> $(BUILD_SHARED_LIBRARY)</span>
</code></pre></div>    </div>

    <p><img src="/images/ffmpeg3.png" alt="Jni 目录结构" /></p>
  </li>
  <li>
    <p>编写 java 代码，声明 Java native method</p>

    <p><img src="/images/ffmpeg4.png" alt="Java 代码" /></p>
  </li>
  <li>
    <p>修改 ffmpeg.c 文件，绑定 jni 方法名与 ffmpeg.c 的方法名</p>

    <p><img src="/images/ffmpeg5.png" alt="绑定方法名称" /></p>
  </li>
  <li>
    <p>在 jni 目录下执行 <code class="language-plaintext highlighter-rouge">ndk-build APP_ABI=armeabi</code></p>

    <p><img src="/images/ffmpeg6.png" alt="编译 libffmpegjni.so " /></p>
  </li>
  <li>
    <p>在 libs/armeabi 目录下得到 libffmpegjni.so</p>
  </li>
  <li>
    <p>到这里，你已经拥有了可以动态 load 的 so 库，并且可以执行 ffmpeg command 了！</p>
  </li>
</ol>

<hr />

<h2 id="集成-ffmpegmediametadataretriever">集成 FFmpegMediaMetadataRetriever</h2>

<p>相信很多开发者对这个库都不会陌生<a href="https://github.com/wseemann/FFmpegMediaMetadataRetriever">FFmpegMediaMetadataRetriever</a>，正如上面所说，原生的 MediaMetadataRetriever 不太好用，这个开源库被我们用来取预览帧：给出时间点，返回 bitmap。</p>

<p>然而，这个库引进来后，聪明的你应该发现了他也编译了一个 ffmpeg 放在了 aar 中，大小约为4MB。</p>

<p>其实，上面步骤走完后，你应该立即想到“可以直接复用已经编译好的 ffmpeg”，安装包立即节约4MB！</p>

<p>同样的这里也只记录步骤：</p>

<ol>
  <li>
    <p>将 FFmpegMediaMetadataRetriever repo 下 <code class="language-plaintext highlighter-rouge">FFmpegMediaMetadataRetriever/gradle/fmmr-library/library/src/main/jni/metadata</code> 的 .c 、.h、.cpp 文件都拷贝到上述的 jni 文件夹中</p>
  </li>
  <li>
    <p>打开上面章节我们编写的 makefile，添加如下代码：</p>

    <div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">include</span><span class="sx"> $(CLEAR_VARS)</span>
 <span class="nv">LOCAL_MODULE</span>  <span class="o">:=</span> ffmpeg_mediametadataretriever_jni

 <span class="k">ifeq</span> <span class="nv">($(APP_ABI), x86)</span>
 <span class="nv">TARGET_ARCH</span><span class="o">:=</span>x86
 <span class="nv">TARGET_ARCH_ABI</span><span class="o">:=</span>x86
 <span class="k">else</span>
 <span class="nv">LOCAL_ARM_MODE</span> <span class="o">:=</span> arm
 <span class="k">endif</span>

 <span class="nv">LOCAL_SRC_FILES</span>  <span class="o">:=</span>  wseemann_media_MediaMetadataRetriever.cpp <span class="se">\</span>
                      mediametadataretriever.cpp <span class="se">\</span>
                      ffmpeg_mediametadataretriever.c <span class="se">\</span>
                      ffmpeg_utils.c

 <span class="nv">LOCAL_LDLIBS</span> <span class="o">:=</span> <span class="nt">-L</span><span class="nv">$(SYSROOT)</span>/usr/lib <span class="nt">-llog</span> <span class="nt">-lz</span>
 <span class="nv">LOCAL_LDLIBS</span> <span class="o">+=</span> <span class="nt">-landroid</span>
 <span class="nv">LOCAL_LDLIBS</span> <span class="o">+=</span> <span class="nt">-ljnigraphics</span>

 <span class="nv">LOCAL_SHARED_LIBRARIES</span><span class="o">:=</span> avcodec-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          avdevice-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          avfilter-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          avformat-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          avutil-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          swresample-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          swscale-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          postproc-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span> <span class="se">\</span>
                          x264-prebuilt-<span class="nv">$(LIB_NAME_PLUS)</span>

 <span class="nv">LOCAL_C_INCLUDES</span> <span class="o">+=</span> <span class="nt">-L</span><span class="nv">$(SYSROOT)</span>/usr/include
 <span class="nv">LOCAL_C_INCLUDES</span> <span class="o">+=</span> <span class="nv">$(LOCAL_PATH)</span>/include

 <span class="k">ifeq</span> <span class="nv">($(APP_ABI), x86)</span>
 <span class="nv">LOCAL_CFLAGS</span> <span class="o">:=</span> <span class="nt">-DUSE_X86_CONFIG</span>
 <span class="k">else</span>
 <span class="nv">LOCAL_CFLAGS</span> <span class="o">:=</span> <span class="nt">-DUSE_ARM_CONFIG</span>
 <span class="k">endif</span>

 <span class="k">include</span><span class="sx"> $(BUILD_SHARED_LIBRARY)</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>重新执行 <code class="language-plaintext highlighter-rouge">ndk-build APP_ABI=armeabi</code> ，将在 <code class="language-plaintext highlighter-rouge">libs/armeabi</code> 下得到 lib ffmpeg_mediametadataretriever_jni.so</p>

    <p><img src="/images/ffmpeg7.png" alt="ffmpeg_mediametadataretriever_jni 编译结果" /></p>
  </li>
  <li>
    <p>将 FFmpegMediaMetadataRetriever repo 中 的 Java 类<code class="language-plaintext highlighter-rouge">FFmpegMediaMetadataRetriever.java</code>拷贝到你的项目中，注意要改一下 so load 的过程：</p>

    <p><img src="/images/ffmpeg8.png" alt="修改 FFmpegMediaMetadataRetriever.java " /></p>
  </li>
  <li>
    <p>到这里，你已经或得了可以运行的 FFmpegMediaMetadataRetriever，并且复用了用于视频编辑模块的 ffmpeg</p>
  </li>
</ol>

<hr />

<h2 id="后续">后续</h2>

<p>如果你需要任何帮助，可以参考我的开源库<a href="https://github.com/zhoulujue/ffmpeg-commands-executor-library">zhoulujue/ffmpeg-commands-executor-library</a>， fork 的 <a href="https://github.com/dxjia/ffmpeg-commands-executor-library">dxjia/ffmpeg-commands-executor-library</a> 仓库。</p>

<p>自己完全控制 ffmpeg 有一个很大的好处，就是可以根据需求的变化来调整所引入的 ffmpeg codec 插件。
例如，需要增加对 gif 编辑的支持，只需要添加一个 encoder 和 decoder 就 OK 了。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[众所周知，Android 对涉及底层硬件的 API 控制力都比较弱，从其难用的 Camera/Camera2、MediaCodec 等 API 就可见一斑。]]></summary></entry><entry><title type="html">Android 动态检查是否拥有某项权限的最佳实践</title><link href="https://blog.zhoulujue.com/Android-Check-Runtime-Permission/" rel="alternate" type="text/html" title="Android 动态检查是否拥有某项权限的最佳实践" /><published>2016-10-09T00:00:00+00:00</published><updated>2016-10-09T00:00:00+00:00</updated><id>https://blog.zhoulujue.com/Android-Check-Runtime-Permission</id><content type="html" xml:base="https://blog.zhoulujue.com/Android-Check-Runtime-Permission/"><![CDATA[<p>API Level 23 的 ContextCompact.checkSelfPermission() 在 小于 23 时，永远返回 PERMISSION_GRANTED</p>

<p>在 API Level = 23 时，ContextCompact.checkSelfPermission() 才返回正确的值</p>

<p>如果 targetAPI Level 在 23 以前，但是又要支持23，使用 PermissionChecker.checkSelfPermission() 是最靠谱的，</p>

<p>这个 PermissionChecker support-v4 lib 里新出的API</p>

<p>需要 在build.gradle 里</p>

<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">compile</span> <span class="err">‘</span><span class="n">com</span><span class="o">.</span><span class="na">android</span><span class="o">.</span><span class="na">support</span><span class="o">:</span><span class="n">support</span><span class="o">-</span><span class="nl">v4:</span><span class="mf">23.1</span><span class="o">.</span><span class="mi">1</span><span class="err">’</span>
</code></pre></div></div>

<p>注意版本号，版本号较低的没有这个API</p>

<p>首先检查是否在安装时申请并授予了指定权限，如果没有再查询AppOps是否是在安装后被用户使用AppOps revoke 了权限。</p>

<p>如果没有记错的话，这个 AppOps 是android官方在4.3的时候引进的一个权限管理模块，可能后来觉得不好就给干掉了</p>

<p>然而现在在23上使用 PermissionChecker 检查的AppOps的结果和 实际的 “Runtime Permission”的权限授予结果一致，说明android在6.0上搞的所谓新权限机制”Runtime Permission” 本来就是复活了的 AppOps</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * Checks whether your app has a given permission and whether the app op
 * that corresponds to this permission is allowed.
 *
 * @param context Context for accessing resources.
 * @param permission The permission to check.
 * @return The permission check result which is either {@link #PERMISSION_GRANTED}
 *     or {@link #PERMISSION_DENIED} or {@link #PERMISSION_DENIED_APP_OP}.
 */</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">checkSelfPermission</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">,</span>
        <span class="nd">@NonNull</span> <span class="nc">String</span> <span class="n">permission</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nf">checkPermission</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">permission</span><span class="o">,</span> <span class="n">android</span><span class="o">.</span><span class="na">os</span><span class="o">.</span><span class="na">Process</span><span class="o">.</span><span class="na">myPid</span><span class="o">(),</span>
            <span class="n">android</span><span class="o">.</span><span class="na">os</span><span class="o">.</span><span class="na">Process</span><span class="o">.</span><span class="na">myUid</span><span class="o">(),</span> <span class="n">context</span><span class="o">.</span><span class="na">getPackageName</span><span class="o">());</span>
<span class="o">}</span>
<span class="cm">/**
 * Checks whether a given package in a UID and PID has a given permission
 * and whether the app op that corresponds to this permission is allowed.
 *
 * @param context Context for accessing resources.
 * @param permission The permission to check.
 * @param pid The process id for which to check.
 * @param uid The uid for which to check.
 * @param packageName The package name for which to check. If null the
 *     the first package for the calling UID will be used.
 * @return The permission check result which is either {@link #PERMISSION_GRANTED}
 *     or {@link #PERMISSION_DENIED} or {@link #PERMISSION_DENIED_APP_OP}.
 */</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">int</span> <span class="nf">checkPermission</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nd">@NonNull</span> <span class="nc">String</span> <span class="n">permission</span><span class="o">,</span>
        <span class="kt">int</span> <span class="n">pid</span><span class="o">,</span> <span class="kt">int</span> <span class="n">uid</span><span class="o">,</span> <span class="nc">String</span> <span class="n">packageName</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">checkPermission</span><span class="o">(</span><span class="n">permission</span><span class="o">,</span> <span class="n">pid</span><span class="o">,</span> <span class="n">uid</span><span class="o">)</span> <span class="o">==</span> <span class="nc">PackageManager</span><span class="o">.</span><span class="na">PERMISSION_DENIED</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="no">PERMISSION_DENIED</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="nc">String</span> <span class="n">op</span> <span class="o">=</span> <span class="nc">AppOpsManagerCompat</span><span class="o">.</span><span class="na">permissionToOp</span><span class="o">(</span><span class="n">permission</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">op</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="no">PERMISSION_GRANTED</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">packageName</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span><span class="o">[]</span> <span class="n">packageNames</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getPackageManager</span><span class="o">().</span><span class="na">getPackagesForUid</span><span class="o">(</span><span class="n">uid</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">packageNames</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">packageNames</span><span class="o">.</span><span class="na">length</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="no">PERMISSION_DENIED</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="n">packageName</span> <span class="o">=</span> <span class="n">packageNames</span><span class="o">[</span><span class="mi">0</span><span class="o">];</span>
    <span class="o">}</span>
    <span class="k">if</span> <span class="o">(</span><span class="nc">AppOpsManagerCompat</span><span class="o">.</span><span class="na">noteProxyOp</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">op</span><span class="o">,</span> <span class="n">packageName</span><span class="o">)</span>
            <span class="o">!=</span> <span class="nc">AppOpsManagerCompat</span><span class="o">.</span><span class="na">MODE_ALLOWED</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="no">PERMISSION_DENIED_APP_OP</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="no">PERMISSION_GRANTED</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>]]></content><author><name></name></author><summary type="html"><![CDATA[API Level 23 的 ContextCompact.checkSelfPermission() 在 小于 23 时，永远返回 PERMISSION_GRANTED]]></summary></entry></feed>