<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://git4pl.github.io</id>
    <title>PL Notes</title>
    <updated>2022-01-21T13:13:51.634Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://git4pl.github.io"/>
    <link rel="self" href="https://git4pl.github.io/atom.xml"/>
    <subtitle>积跬步至千里，积小流成江海</subtitle>
    <logo>https://git4pl.github.io/images/avatar.png</logo>
    <icon>https://git4pl.github.io/favicon.ico</icon>
    <rights>All rights reserved 2022, PL Notes</rights>
    <entry>
        <title type="html"><![CDATA[GC 算法初探]]></title>
        <id>https://git4pl.github.io/post/1pWhbE2ss/</id>
        <link href="https://git4pl.github.io/post/1pWhbE2ss/">
        </link>
        <updated>2022-01-21T13:00:47.000Z</updated>
        <content type="html"><![CDATA[<h2 id="一-为什么需要gc">一、为什么需要GC</h2>
<p>Java 程序员都知道对象初始化的重要性，我们要使用一个对象，必须先为其分配内存空间进行初始化，而使用完了对象后，我们很少关注要如何处理那些对象，可能会在明确对象不再使用的地方将对象设置为 null。而系统内存空间是有限的，操作系统允许一个应用程序占用的最大内存也是有限制的，当程序员“不羁放纵爱自由”地创建了很多对象后，JVM 垃圾回收器默默地在后台维护和清理了不需要的对象占用的内存空间。</p>
<p>垃圾回收器是一种动态存储分配器，它自动释放程序不再需要的已分配的内存块，这些块称为“垃圾”，也就是不再被引用的对象。自动回收垃圾的过程则称为垃圾回收（Garbage Collection），简称 GC。在一个支持垃圾收集的语言中，程序显式地申请内存，但从不需要显式地释放它们，垃圾收集器会定期识别垃圾块，并将垃圾块放回空闲链表中。GC 技术帮助程序员实现自动管理内存，程序员可以将注意力集中在业务逻辑，从繁重的内存管理中解放出来，而像 C 语言的 malloc 是一个不带 GC 功能的分配器，程序员显式调用 malloc 分配内存后需要显式调用 free 来释放它，否则会出现内存泄露、悬空指针等内存问题。</p>
<h2 id="二-gc的算法分类">二、GC的算法分类</h2>
<h3 id="引用计数法">引用计数法</h3>
<p>每个对象都有一个引用计数器，当对象被引用一次则计数器加1，当对象引用失效一次则计数器减1，对于引用数为0的对象意味着是垃圾对象，可以被GC回收。</p>
<h3 id="可达性算法">可达性算法</h3>
<p>从GC roots开始搜索，整个连通图中的对象都是活对象，对于GC Roots无法到达的对象便成了垃圾回收的对象，随时可被GC回收。</p>
<h3 id="分代收集理论">分代收集理论</h3>
<ul>
<li>当前商业虚拟机的垃圾收集器，大多数都遵循了“分代收集”(Generational Collection)的理论进行设计，分代收集名为理论，实质是一套符合大多数程序运行实际情况的经验法则，它建立在两个分代假说之上：
<ol>
<li>弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的</li>
<li>强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。</li>
<li>根据以上两条分代假说可推理出：<strong>跨代引用假说</strong> 跨代引用相对于同代引用来说仅占极少数</li>
</ol>
</li>
<li>对应的可以在Java堆划分出不同的区域，垃圾收集器每次只回收其中某一个或者某些部分的区域 ——因而就有了“Minor GC”，“Major GC”，“Full GC”这样的回收类型的划分;也就能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因此发展出了“标记-复制算法”“标记-清除算法”，“标记-整理算法”等针对性的垃圾收集算法；</li>
</ul>
<pre><code>Minor GC/Young GC：针对新生代的垃圾收集
Major GC/Old GC：针对老年代的垃圾收集
MixedGC：针对于新生代与部分老年代的垃圾收集
Full GC：针对于整个Java堆和方法区(MetaSpace元空间) 的垃圾收集
</code></pre>
<figure data-type="image" tabindex="1"><img src="https://git4pl.github.io/post-images/1642770233010.webp" alt="" loading="lazy"></figure>
<h2 id="三-经典-gc-算法详解">三、经典 GC 算法详解</h2>
<h3 id="1-copy算法">1、copy算法</h3>
<ul>
<li>1963年Marvin Minsky提出：这个算法的基本思想是把某个空间里的活跃对象复制到其他空间，把原来的空间全部清空，这就相当于是把活跃的对象从一个空间搬到新的空间。因为这种复制具有方向性，所以我们把原空间称为From空间（分配空间），把新的目标空间称为 To 空间（幸存者空间）</li>
<li>最基础copy算法可以将堆平分两份，但太浪费了，GC年轻代默认按照8:1:1分配；</li>
</ul>
<h4 id="对象分配">对象分配</h4>
<ul>
<li>碰撞指针分配对象：对象之间没有任何空隙，不会产生内存碎片；</li>
</ul>
<figure data-type="image" tabindex="2"><img src="https://git4pl.github.io/post-images/1642770263167.png" alt="" loading="lazy"></figure>
<h4 id="对象回收">对象回收</h4>
<ul>
<li>可达性分析算法需要找到GCroot，能作为GCront的对象有：
<ol>
<li>虚拟机栈中的引用的对象</li>
<li>类静态属性引用的对象</li>
<li>常量引用的对象</li>
<li>本地方法栈中的JNI（native方法）引用的对象</li>
</ol>
</li>
<li>可达性基本思想：由以上对象引用链构成一张图，如果从根出发，开始对图进行遍历，能够遍历到就是活跃对象，否则为垃圾对象；</li>
<li>如何将对象之间引用关系抽象成图呢？我们看JVM中java对象在内存中构成：<a href="https://juejin.cn/post/6844904054561193992">Oop-Klass模型</a></li>
</ul>
<figure data-type="image" tabindex="3"><img src="https://git4pl.github.io/post-images/1642770286688.png" alt="" loading="lazy"></figure>
<ul>
<li>根据klass快速获取哪些位置存放为引用字段，若为引用，且引用不为null,就表示当前对象引用了其他对象。这样从根引用出发就可以构建一张图；</li>
<li>一般对图遍历有两种算法： <strong>深度优先遍历（DFS）</strong> 和 <strong>广度优先遍历（BFS）</strong></li>
</ul>
<h4 id="dfs在copy中实现">DFS在Copy中实现</h4>
<ol>
<li>比如有如下引用关系A,B,C都是活跃对象，我们如何进行深度优先遍历Copy呢？</li>
</ol>
<figure data-type="image" tabindex="4"><img src="https://git4pl.github.io/post-images/1642770318693.png" alt="" loading="lazy"></figure>
<ol>
<li>
<p>遍历过程中，A拷贝到To space,然后C又拷贝过去，此时空间引用时将是这样的！<br>
<img src="https://git4pl.github.io/post-images/1642770340431.png" alt="" loading="lazy"></p>
</li>
<li>
<p>若当拷贝B后再将C拷贝一份，则To中又两个C了，如何解决呢？</p>
<ul>
<li>使用forwarding指针：在每个对象头部引入一个新的field,即forwarding，正常状态下，其值为null,如果一个对象被拷贝，就把它的新地址设到From空间对象的forwarding指针中；<br>
<img src="https://git4pl.github.io/post-images/1642770356913.png" alt="" loading="lazy"></li>
</ul>
</li>
</ol>
<h4 id="bfs与dfs在copy算法中对比">BFS与DFS在copy算法中对比</h4>
<ul>
<li>举例对比方能解释明晰：</li>
</ul>
<pre><code>class A {
    public B b1;
    public B b2;
    public A() {
        b1 = new B();
        b2 = new B();
    }
}
class B {
    public C c1;
    public C c2;
    public B() {
        c1 = new C();
        c2 = new C();
    }
}
class C {
}
</code></pre>
<ol>
<li>
<p>对于以上对象，在From空间布局如下所示：<br>
<img src="https://git4pl.github.io/post-images/1642770390005.png" alt="" loading="lazy"></p>
</li>
<li>
<p>我们开始从A用DFS遍历，拷贝至TO空间<br>
<img src="https://git4pl.github.io/post-images/1642770404885.png" alt="" loading="lazy"></p>
</li>
<li>
<p>对A的属性扩展，先访问属性b1引用的对象，即把b1指向对象拷贝至TO空间<br>
<img src="https://git4pl.github.io/post-images/1642770418634.png" alt="" loading="lazy"></p>
</li>
<li>
<p>接着我们拷贝c1所引用的C对象,这部完成以后开始退栈（DFS依靠栈，BFS依靠队列实现）<br>
<img src="https://git4pl.github.io/post-images/1642770430765.png" alt="" loading="lazy"></p>
</li>
<li>
<p>依次退栈，直到完成copy后布局如下：<br>
<img src="https://git4pl.github.io/post-images/1642770447843.png" alt="" loading="lazy"></p>
</li>
</ol>
<ul>
<li>经过以上步骤，不知你发现什么没有： 对比起始于结束箭头，会发现变简约好多啊，箭头代表了引用关系，说明具有引用关系的对象在新空间中距离更近了！这有啥用呢？</li>
</ul>
<h4 id="dfs总结">DFS总结</h4>
<ul>
<li><strong>优点：</strong> 编程中如果访问一个对象，马上就会访问它的属性，而又因为CPU缓存机制，在读取某个对象时，又很大概率会把它后面的对象一起读进来。而经过DFS遍历后，拥有引用关系极大可能会在同一个缓存中，即读 A 对象的时候，把它后面的 B 和 C 对象都能加载进缓存，那么，a.b1.c1 这种写法就可以立即命中缓存；</li>
<li><strong>缺点：</strong> 需要维护一个额外的辅助数据结构栈；</li>
</ul>
<h4 id="bfs总结">BFS总结</h4>
<ul>
<li>
<p>你可以对应的使用BFS遍历上方举例，它的优缺点刚好与深度优先搜索相反；</p>
</li>
<li>
<p>优点：无需额外空间，其所需的队列，巧妙地隐藏在了 To 空间中；</p>
</li>
<li>
<p>缺点：有引用关系的对象之间的距离就会比较远，这将不利于业务线程运行期的缓存命中；</p>
</li>
<li>
<p>深度优先搜索的非递归写法需要占用额外的空间，但有利于提高业务线程运行期的缓存命中率。而广度优先搜索则与其相反，它不利于运行期的缓存命中，但算法的执行效率更高。所以 JDK6 以前的 JVM 使用了广度优先的非递归遍历，而在 JDK8 以后，已经把广度优先算法改为深度优先了，尽管这样做需要额外引用一个独立的栈。</p>
</li>
</ul>
<h3 id="2-cms算法">2、CMS算法</h3>
<ul>
<li>CMS（Concurrent Mark Sweep）是一款里程碑式的垃圾收集器，为什么这么说呢？
<ul>
<li>在它之前，GC线程和用户线程是无法同时工作的，即使是Parallel Scavenge，也不过是GC时开启多个线程并行回收而已，GC的整个过程依然要暂停用户线程，即Stop The World。这带来的后果就是Java程序运行一段时间就会卡顿一会，降低应用的响应速度，这对于运行在服务端的程序是不能被接收的。</li>
</ul>
</li>
<li>CMS过程图大家都很熟悉，但是对于他如何实现并发你真的了解吗？<br>
<img src="https://git4pl.github.io/post-images/1642770475452.png" alt="" loading="lazy"><br>
<img src="https://git4pl.github.io/post-images/1642770491459.png" alt="" loading="lazy"></li>
</ul>
<h4 id="三色标记">三色标记</h4>
<ul>
<li>Serial、ParNew等垃圾收集器会简单粗暴的等所有线程进行安全点后STW；而CMS、G1等垃圾收集器采取了并发标记的策略；
<ul>
<li>在并发标记的过程中，对象间的引用关系也一直在发生改变：如原本与GCRoots存在间接引用的节点在并发标记过程中断连了，而这会产生并发的问题，如何解决？使用三色标记算法进行分析;</li>
</ul>
</li>
<li>三色标记，即通过不同的颜色标记处于不同标记状态的对象<br>
<img src="https://git4pl.github.io/post-images/1642770507423.png" alt="" loading="lazy"></li>
</ul>
<h4 id="漏标">漏标</h4>
<ul>
<li>对于一个本来不应该被回收的对象，却被标记成白色；</li>
</ul>
<pre><code>public class ThreeColorRemark {

    private static A a ;
    public static void main(String[] args) {
        creatClassA();
        //并发标记
        a.d = a.b.d ;
        a.b.d = null ;
    }
    private static void creatClassA() {
        a = new A();
        return; //安全点
    }
}
class A{
    B b = new B();
    D d = null;
}
class B{
    C c = new C();
    D d = new D();
}
class C{}
class D{}
</code></pre>
<figure data-type="image" tabindex="5"><img src="./assets/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8B%E9%93%BE%E8%A1%A8%E7%9A%84%E5%BA%94%E7%94%A8/three-color-missing.drawio.svg" alt="" loading="lazy"></figure>
<ul>
<li>对于以上黑色指向了未被标记的白色，将会导致白色对象不会再被扫描到而漏标，运行时会导致空指针；CMS时如何避免呢？<br>
<img src="https://git4pl.github.io/post-images/1642770538141.png" alt="" loading="lazy"></li>
</ul>
<h4 id="增量更新">增量更新</h4>
<ul>
<li>通过破坏第一条规则实现：依靠写屏障<br>
<img src="https://git4pl.github.io/post-images/1642770556218.jpg" alt="" loading="lazy"></li>
</ul>
<h4 id="写屏障write-barrier">写屏障write barrier</h4>
<ul>
<li>给某个对象的成员变量赋值前后加入一些处理，类似于AOP. <a href="http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/oop.inline.hpp">C++源码</a></li>
</ul>
<pre><code>/**
 * @param field ：某对象的属性 a.b.d
 * @param new_value :新值 如null
 */
void oop_field_store(oop* field , oop new_value){
    pre_write_barrier(field);  //写屏障 --- 写前屏障（G1使用）
    *field = new_value ; //赋值操作
    post_write_barrier(field , value); //写屏障---写后屏障（CMS使用）
}

//c++ 源码
template &lt;class T&gt; inline void oop_store(volatile T* p, oop v) {
  update_barrier_set_pre((T*)p, v);   // cast away volatile  写前屏障
  // Used by release_obj_field_put, so use release_store_ptr.
  oopDesc::release_encode_store_heap_oop(p, v);
  // When using CMS we must mark the card corresponding to p as dirty
  // with release sematics to prevent that CMS sees the dirty card but
  // not the new value v at p due to reordering of the two
  // stores. Note that CMS has a concurrent precleaning phase, where
  // it reads the card table while the Java threads are running.
  update_barrier_set((void*)p, v, true /* release */);    // cast away type 写后屏障
}

inline void update_barrier_set(void* p, oop v, bool release = false) {
  assert(oopDesc::bs() != NULL, &quot;Uninitialized bs in oop!&quot;);
  oopDesc::bs()-&gt;write_ref_field(p, v, release);
}

template &lt;class T&gt; inline void update_barrier_set_pre(T* p, oop v) {
  oopDesc::bs()-&gt;write_ref_field_pre(p, v);
}
</code></pre>
<h4 id="写屏障实现增量更新">写屏障实现增量更新</h4>
<ul>
<li>当对象A的成员变量的引用发生变化时，比如新增引用（a.d = D）,利用写后屏障将A新的成员变量引用对象D记录下来（先赋值，在收集引用）</li>
</ul>
<pre><code>//CMS 增量更新使用
void post_write_barrier(oop* field , oop new_value){
    remark_set.add(new_value); //记录新引用的对象
}
</code></pre>
<h4 id="写屏障实现satb">写屏障实现SATB</h4>
<ul>
<li>当对象B的成员变量的引用发生变化时，比如新增引用（b.d = null）,利用写前屏障将B旧的成员变量引用对象D记录下来（先收集引用，后赋值）</li>
</ul>
<pre><code>//G1 SATB 使用
void pre_write_barrier(oop* field){
    oop old_value = *field; //获取旧值
    remark_set.add(old_value); //记录原来引用的对象
}
</code></pre>
<ul>
<li>有兴趣同学可以查看<a href="https://android.googlesource.com/platform/libcore/+/871832c718f9e5c91134f9b935360cae17075278/hotspot/src/share/vm/gc/g1/g1SATBCardTableModRefBS.cpp">Hotspot实现类</a>： 存放到子Thread的内存队列中，避免对当前写操作产生影响；</li>
</ul>
<pre><code>void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
  // Nulls should have been already filtered.
  assert(pre_val-&gt;is_oop(true), &quot;Error&quot;);

  if (!JavaThread::satb_mark_queue_set().is_active()) return;
  Thread* thr = Thread::current();
  if (thr-&gt;is_Java_thread()) {
    JavaThread* jt = (JavaThread*)thr;
    jt-&gt;satb_mark_queue().enqueue(pre_val);  //JavaThread中satb队列中入队原来旧值
  } else {
    MutexLockerEx x(Shared_SATB_Q_lock, Mutex::_no_safepoint_check_flag);
    JavaThread::satb_mark_queue_set().shared_satb_queue()-&gt;enqueue(pre_val);
  }
}
</code></pre>
<h4 id="跨代引用问题">跨代引用问题</h4>
<ul>
<li>对于跨代引用，比如minor GC/younger GC 遇到老年代引用年轻代对象，如果每次都扫描老年代效率太低了，如何提效呢？</li>
</ul>
<h5 id="记录集">记录集</h5>
<ul>
<li>
<p>在新生代中可以开辟一块空间，引入记录集（Remember Set）的数据结构（记录从非收集区到收集区的指针集合），由于以上分析跨代引用相对比较少，因此记录集的数据有限，不会特别大；</p>
</li>
<li>
<p>分类：</p>
<ol>
<li>
<p>字长精度：每个记录精确到一个机器字长(就是处理器的寻址位数，如常见的32位或64位，这个 精度决定了机器访问物理内存地址的指针长度)，该字包含跨代指针。</p>
</li>
<li>
<p>对象精度:每个记录精确到对象，该对象里有字段含跨代指针。</p>
</li>
<li>
<p>卡精度:每个记录精确到一块内存区域，该内存区域有对象含有跨代指针。</p>
</li>
</ol>
<ul>
<li>HotSpot JVM，使用了卡标记（Card Marking）技术来解决老年代到新生代的引用问题。具体是，使用卡表（Card Table）和写屏障（Write Barrier）来进行标记并加快对GC Roots的扫描</li>
</ul>
</li>
</ul>
<h5 id="卡表card-table">卡表Card table</h5>
<ul>
<li>记忆集是一种抽象的数据结构，卡表是其实现形式，对于上方字长，对象等会导致随着对象的增多，记录集会变得很大，同时也不易维护；如何优化呢？使用压缩；<br>
<img src="https://git4pl.github.io/post-images/1642770601001.png" alt="" loading="lazy"></li>
<li>如何维护卡表呢？通过<strong>写屏障</strong>： 当对象的属性进行写操作时，跨代引用就有可能出现。所以，我们在这个时候可以检查是否存在跨代引用；</li>
<li>判断条件分成两种：
<ol>
<li>当前老年代属性赋值时引用了年轻代对象</li>
<li>原本在年轻代对象minor GC后晋升至老年代，此时遍历从这个对象出发的所有引用判断是否有年轻代对象</li>
</ol>
</li>
<li>若满足以上两个条件之一，就将当前card table中对应的byte标记为1即dirty脏；但是对性能有两个影响
<ol>
<li>无条件写屏障带来的性能开销：虚拟机会为所有的赋值操作生成相应的指令，每次只要对引用进行了赋值操作，就会判断是否需要更新卡表，从而产生额外的开销，不过这个开销与MinorGC时扫描整个老年代的代价要低的多；</li>
<li>高并发下虚共享带来的性能开销，伪共享； CPU缓存行一般为64字节，对应卡页为64*512bytes = 32kb，不同线程对对象引用的更新操作，恰好位于同一个32KB区域内，则将会带来同步性能问题</li>
</ol>
</li>
<li>优化：先检查当前卡表标记，只有当该卡表项未被标记过才将其标记为dirty<br>
<img src="https://git4pl.github.io/post-images/1642770613418.png" alt="" loading="lazy"></li>
</ul>
<h3 id="3-g1算法">3、G1算法</h3>
<p><em>1. 既然已经有了几个强大的GC，为什么还要发布G1 GC？</em></p>
<p>随着应用程序的业务逻辑越来越庞大、复杂，用户访问也越来越多，没有 GC 就不能保证应用程序正常运行，而经常 STW 的 GC 方案又跟不上实际的应用需求，所以需要不断尝试对 GC 进行优化。G1 GC 的目标就是为了适应现在不断扩大的内存和不断增加的处理器数量，进一步降低 GC 暂停时间，同时兼顾良好的吞吐量。</p>
<p><em>2. G1 GC 是什么？</em></p>
<p>官网上对G1 GC 的描述如下：</p>
<pre><code>The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is designed for applications that:

* Can operate concurrently with applications threads like the CMS collector.
* Compact free space without lengthy GC induced pause times.
* Need more predictable GC pause durations.
* Do not want to sacrifice a lot of throughput performance.
* Do not require a much larger Java heap.
</code></pre>
<p>G1是一种服务器端的垃圾收集器，应用在多处理器和大容量内存环境中，在实现高吞吐量的同时，尽可能的满足垃圾收集对暂停时间的要求。它是专门针对以下应用场景而设计的:</p>
<ul>
<li>像CMS收集器一样，能与应用程序线程并发执行</li>
<li>整理空闲空间更快</li>
<li>需要GC停顿时间更好预测</li>
<li>不希望牺牲大量的吞吐性能</li>
<li>不需要更大的Java Heap</li>
</ul>
<p><em>3. G1 的支持版本是什么？</em></p>
<p>在JDK1.7版本中正式启用 G1，移除了 Experimental 的标识，JDK 9以后 G1 是默认的垃圾回收器，取代了 CMS 回收器以及 Parallel + Parallel 0ld 组合。G1 被Oracle官方称为“全功能的垃圾收集器”。</p>
<p><em>4. G1 中的重要概念和原理有哪些？</em></p>
<p>在 G1 的实现中引入了一些新的概念和算法，来实现 GC 的高吞吐量、低内存碎片、可预测的停顿时间等目标。</p>
<h4 id="region">Region</h4>
<p>传统的 GC 将内存空间划分为新生代、老年代和永久代（JDK 8 中去除了永久代，引入了元空间），各代的内存地址是连续的。</p>
<figure data-type="image" tabindex="6"><img src="https://git4pl.github.io/post-images/1642770647033.png" alt="" loading="lazy"></figure>
<p>G1 也是分代的垃圾回收算法，不过 G1 的老年代和年轻代的存储地址是不连续的，每一代都使用了n个不连续的大小相同的Region，每个Region占有一块连续的虚拟内存地址。这就是 G1 引入的分区的概念，Region 的类型有 Eden、Survivor、Old、Humongous 四种，而且每个 Region 都可以单独进行管理。</p>
<figure data-type="image" tabindex="7"><img src="https://git4pl.github.io/post-images/1642770663479.png" alt="" loading="lazy"></figure>
<p>其中 Humongous 是用来存放大对象的，如果一个对象的大小大于一个 Region 的 50%（默认值），那么我们就认为这个对象是一个大对象。为了防止大对象的频繁拷贝，我们可以将大对象直接放到 Humongous 中。</p>
<p>H-obj有如下几个特征：</p>
<ul>
<li>H-obj直接分配到了老年代，防止了大对象的反复拷贝移动</li>
<li>H-obj在全局并发标记阶段的cleanup 和 full GC阶段回收</li>
<li>在分配H-obj之前先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话，就启动global concurrent marking，为的是提早回收，防止 evacuation failures 和 full GC。</li>
</ul>
<p>一个Region的大小可以通过参数-XX:G1HeapRegionSize设定，取值范围从1M到32M，且是2的指数。如果不设定，那么G1会根据Heap大小自动决定。</p>
<h4 id="satb">SATB</h4>
<p>SATB 的英文全称是 Snapshot-At-The-Beginning，其字面意思是 GC 开始时活着的对象的一个快照。根据前面介绍的三色标记算法，发生白对象漏标有两个前提条件：</p>
<ol>
<li>Mutator赋予一个黑对象该白对象的引用</li>
<li>Mutator删除了所有从灰对象到该白对象的直接或者间接引用</li>
</ol>
<p>对于第一个条件，在并发标记阶段，如果该白对象是new出来的，并没有被灰对象持有，那么它会不会被漏标呢？</p>
<figure data-type="image" tabindex="8"><img src="https://git4pl.github.io/post-images/1642770680636.png" alt="" loading="lazy"></figure>
<p>每个Region中有两个标记位图：next和 prev，next 是本次标记的标 记位图，而 prev 是上次标记的标记位图，保存了上次标记的结果。上图中的 bottom 表示 Region 内众多对象的末尾，top 表示开头。另外有两个top-at-mark-start（TAMS）指针，分别为prevTAMS和nextTAMS。nextTAMS 保存了本次标记开始时的 top，而 prevTAMS 保存了上次标记开始时的 top。在并发标记阶段新创建的对象会在 top 之后的区域分配空间，它将直接被标记为黑色，这是一种隐式的标记。所以并发标记阶段新生成的对象不会被漏标。</p>
<p>而对于在GC时已经存在的白对象，如果它是活着的，它必然会被另一个对象引用，即条件二中的灰对象。如果灰对象到白对象的直接引用或者间接引用被替换了，或者删除了，那个白对象就会被漏标，从而导致被回收掉，这是非常严重的错误。</p>
<p>在 CMS GC 算法中解决白对象漏标问题采用了写屏障技术，当 B 对象对 C 对象的引用消失时，C 对象将会被标记为灰色。这个动作的效率是比较低的，如果都放在写屏障中做，会极大地影响程序性能，因为写屏障的逻辑是由业务线程执行的。</p>
<p>为了解决写屏障的性能问题，G1 将“C 对象标记为灰色”这件事情往后推迟了。业务线程只需要把 C 对象记录到一个本地队列中就可以了。每个业务线程都有一个这样的线程本地队列，它的名字是 SATB 队列。SATB 专用写屏障的伪代码如下所示：</p>
<pre><code>1: def satb_write_barrier(field, newobj): 
2:      if $gc_phase == GC_CONCURRENT_MARK: 
3:          oldobj = *field 
4:          if oldobj != Null: 
5:              enqueue($current_thread.stab_local_queue, oldobj) 
6:
7:          *field = newobj
</code></pre>
<p>参数 field 表示被写入对象的域，参数 newobj 表示被写入域的值。第 2 行的 GC_CONCURRENT_MARK 用来表示并发标记阶段的标志位（flag）。第 4 行会检查当前是否处于并发标记阶段且被写入之前 field 域的值是不是 Null。如果检查通过，则在第 5 行将 oldobj 添加到 $current_thread.stab_local_queue 中。然后，在第 7 行进行 实际的写入操作。这个算法没有对 oldobj 进行任何标记处理。</p>
<p>上述SATB 写屏障的实现考虑了多线程环境的执行，第 5 行 $current_thread.stab_local_queue 是 mutator 各自持有的 线程本地队列，而非全局的队列，因此在执行 enqueue() 时不用担心 线程之间会发生资源竞争。 如下图每个线程有自己的本地 SATB 队列，当本地队列满了之后，就把它交给 SATB 队列集合，然后再领取一个空队列当做线程的本地 SATB 队列。GC 线程则会将 SATB 队列集合中的对象标记为灰色，至于什么时候标记，并不需要业务线程关心。</p>
<figure data-type="image" tabindex="9"><img src="https://git4pl.github.io/post-images/1642770697863.png" alt="" loading="lazy"></figure>
<p>在并发标记阶段，GC 线程会定期检查 SATB 队列集合的大小。如果发 现其中有队列，则会对队列中的全部对象进行标记和扫描。前面已经讲 过，SATB 专用写屏障并不检查目标对象是否被标记，因此队列中可能存在已经被标记的对象，已经被标记的对象也不会再次被标记和扫描。</p>
<h4 id="collection-set">Collection Set</h4>
<p>G1 的垃圾回收模式有两种：分别是 young GC 和 mixed GC。</p>
<ul>
<li>young GC：只回收年轻代的 Region</li>
<li>mixed GC：回收全部的年轻代 Region，并回收部分老年代的 Region。</li>
</ul>
<p>无论是 young GC 还是 mixed GC，都会回收全部的年轻代，mixed GC 回收的老年代 Region 是需要进行决策的（Humongous 在回收时也是当做老年代的 Region 处理的）。mixed GC 中选取的老年代对象 Region 的集合称之为回收集合（Collection Set，CSet），那么决定老年代 Region 是否被回收的因素具体有哪些呢？</p>
<p>CSet 的选取要素有以下两点：</p>
<ol>
<li>该 Region 的垃圾占比。垃圾占比越高的 Region，被放入 CSet 的优先级就越高，这就是垃圾优先策略（Garbage First），也是 G1 GC 名称的由来。</li>
<li>建议的暂停时间。建议的暂停时间由 -XX:MaxGCPauseMillis 指定，G1 会根据这个值来选择合适数量的老年代 Region。</li>
</ol>
<p>MaxGCPauseMillis 默认是 200ms，一般不需要进行调整，如果需要停顿时间更短可以对它进行设置，但是 MaxGCPauseMillis 设置的越小，选取的老年代 Region 就会越少，如果 GC 压力居高不下，就会触发 G1 的 Full GC。</p>
<h4 id="remember-set-和-card-table">Remember Set 和 Card Table</h4>
<p>在 CMS GC 中也用到了 Remember Set（RSet） 和 Card Table（卡表）这两种记录集数据结构，它们是用于辅助 GC 的结构，是一种空间换时间的方式。</p>
<p>逻辑上每个 Region 都有一个对应的 RSet，RSet 中记录了其他 Region 中的对象引用了本 Region 的对象的关系，即谁引用了我，这种记录集被称为 point-in 类型，而 Card Table 则记录我引用了谁的对象，被称为 point-out 类型。</p>
<p>卡表是由元素大小为 1B 的数组实现的，卡表里的元素称为卡片。堆中大小适当的一段存储空间会对应卡表中的 1 个元素（卡片）。比如在某个版本 JDK 中，这个大小被定为 512B，当堆的大小是 1GB 时，可以计算出卡表的大小就是 2MB。G1 的 RSet 是在卡表的基础上实现的：每个 Region 会记录下别的 Region 有指向自己的指针，并标记这些指针分别在哪些 Card 的范围内。 这个RSet其实是一个哈希表，Key是别的 Region 的起始地址，Value 是一个集合，里面的元素是卡表的 Index。</p>
<figure data-type="image" tabindex="10"><img src="https://git4pl.github.io/post-images/1642770715844.png" alt="" loading="lazy"></figure>
<p>上面图中表示了RSet、Card和Region的关系，每个Region被分成了多个Card，在不同Region中的Card会相互引用，Region1 和 Region3 中的 Card 中的对象引用了 Region2 中的 Card 中的对象，蓝色实线表示的就是 point-out 的关系，而在 Region2 的 RSet 中，记录了 Region1 和 Region3 的 Card，即红色虚线表示的关系，这就是point-in。</p>
<h4 id="g1-如何维护跨区引用">G1 如何维护跨区引用</h4>
<p>每个 Region 的专属 RSet 中记录了其他 Region 的对象对本 Region 中对象的引用关系，那么有哪些引用关系需要加入 RSet 中呢？</p>
<ol>
<li>同一个 Region 中的对象之间的相互引用是不必维护的，因为不存在跨 Region 的问题；</li>
<li>由年轻代 Region 出发到其他 Region 的，无论目标是年轻代还是老年代，这一类引用也都不用维护。因为结合 young GC 和 mixed GC 的策略可以知道，无论是什么回收模式，年轻代的全部 Region 都会被清理，这就意味着一定会对年轻代的所有对象进行遍历；</li>
<li>从 CSet 集合的 Region 出发指向其他 Region 的，也不需要维护，理由和第 2 点是一样的。</li>
</ol>
<p>因此，RSet 需要维护的引用关系只有两种：</p>
<ul>
<li>非 CSet 老年代 Region 到年轻代 Region 的引用</li>
<li>非 CSet 老年代 Region 到 CSet 老年代 Region 的引用</li>
</ul>
<p>RSet 中记录的也是 Card，根据上面的条件记录到 RSet 中的 Card 称为 Dirty Card。和 SATB 相似，业务线程也不是直接将 dirty card 放到 RSet 中的，而是在业务线程中引入一个叫做 dirty card queue（DCQ）的队列，在写屏障中，业务线程只需要将 dirty card 放入 DCQ 中，而不做非常细致的检查。然后，在 GC 线程中有一类叫 Refine 线程，它们会从 DCQ 中找到这种 dirty card，然后再去做更精细的检查，只有确实不属于上面所描述的三种情况的跨区引用，才真正放到专属 RSet 中去。</p>
<h4 id="停顿预测模型">停顿预测模型</h4>
<p>G1 GC是一个响应时间优先的 GC 算法，它与 CMS 最大的不同是，用户可以设置整个 GC 过程的期望停顿时间，通过参数 <em>-XX:MaxGCPauseMillis</em> 指定一个 G1 收集过程目标停顿时间，默认值200ms，它不是硬性条件，只是期望值。那么G1怎么满足用户的期望呢？就需要这个停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量，从而尽量满足用户设定的目标停顿时间。停顿预测模型是根据衰减标准偏差实现的：</p>
<pre><code class="language-C++">//  share/vm/gc_implementation/g1/g1CollectorPolicy.hpp
double get_new_prediction(TruncatedSeq* seq) {
    return MAX2(seq-&gt;davg() + sigma() * seq-&gt;dsd(),
                seq-&gt;davg() * confidence_factor(seq-&gt;num()));
}
</code></pre>
<p>在这个预测计算公式中：davg 表示衰减均值，sigma()返回一个系数，表示信赖度，dsd 表示衰减标准偏差，confidence_factor 表示可信度相关系数。而方法的参数 TruncateSeq，表示一个截断的序列，它只跟踪了序列中的最新的 n 个元素。在G1 GC过程中，每个可测量的步骤花费的时间都会记录到TruncateSeq（继承了AbsSeq）中，用来计算衰减均值、衰减变量，衰减标准偏差等。</p>
<h2 id="思考">思考</h2>
<ol>
<li>为何CMS使用增量更新，G1使用SATB?</li>
</ol>
<ul>
<li>我的理解：SATB相对增量更新效率会高(当然SATB会造成更多的浮动垃圾)，因为不需要在重新标记阶段再次深度扫描被删除引用对象，而CMS对增量引用的根对象会做深度扫描，G1因为很多对象都位于不同的region，CMS就一块老年代区域，重新深度扫描对象的话G1的代价会比CMS高，所以G1选择SATB不深度扫描对象，只是简单标记，等到下一轮GC再深度扫描。
<ul>
<li>原始快照只是简单把要可能消失的对象标记为黑色对象，这样有可能会产生浮动垃圾，而增量更新会把新增的引用关系都重新扫描一遍，在重新标记阶段不会产生浮动垃圾；</li>
<li>原始快照速度快，增量更新速度慢；</li>
</ul>
</li>
</ul>
<ol start="2">
<li>如何衡量一个 GC 算法的好坏，以及如何选择 GC 算法？</li>
</ol>
<h3 id="refers-to">Refers To</h3>
<p><a href="https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html">Getting Started with the G1 Garbage Collector</a><br>
<a href="https://www.infoq.com/articles/tuning-tips-G1-GC/">Tips for Tuning the Garbage First Garbage Collector</a></p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[浅析Flutter中的生命周期]]></title>
        <id>https://git4pl.github.io/post/heQX1rtz6/</id>
        <link href="https://git4pl.github.io/post/heQX1rtz6/">
        </link>
        <updated>2021-08-26T13:30:11.000Z</updated>
        <content type="html"><![CDATA[<h2 id="背景">背景</h2>
<p>作为 Android 开发者，我们对 Activity、Service 等系统组件的生命周期非常熟悉，在日常开发中也会利用组件的生命周期特点开发业务逻辑。</p>
<p>Flutter 中有“一切皆Widget”的说法，Widget 是 Flutter 功能的抽象描述，是视图的配置信息，也是数据的映射。Android 开发中的 View、Layout、Activity、Application等，对应到 Flutter 中都是 Widget。</p>
<p>Flutter 的 Widget 有 StatelessWidget 和 StatefulWidget 两种类型，StatelessWidget 用于处理静态的、无状态变化的视图展示，而 StatefulWidget 应对有交互、需要动态变化视觉效果的场景。</p>
<p>StatelessWidget 通过父 Widget 初始化时传入的静态配置就能完全控制其静态展示，而 StatefulWidget 需要借助于 State 对象，在特定阶段来处理用户交互或内部数据的变化，然后更新到 UI 上。这些特定的阶段涵盖了一个 Widget 从初始化到卸载的全过程，即生命周期。Flutter 中的生命周期即指的 StatefulWidget 的生命周期，并通过 State 对象来体现，StatelessWidget 的生命周期只有 build 过程，且只会执行一次。</p>
<h2 id="生命周期方法">生命周期方法</h2>
<p>Flutter中的生命周期方法主要包括以下内容：</p>
<ul>
<li>createState<br>
该函数为 StatefulWidget 中创建 State 的方法，当 StatefulWidget 被调用时会立即执行 createState 。</li>
</ul>
<pre><code>class MyPage extends StatefulWidget {
  @override
  _MyPageState createState() =&gt; _MyScreenState();
}
</code></pre>
<ul>
<li>initState<br>
该函数为 State 初始化调用，因此可以在此期间执行 State 各变量的初始赋值，同时也可以在此期间与服务端交互，获取服务端数据后调用 setState 来设置 State。</li>
</ul>
<pre><code>int a;
@override
void initState() {
  a = 0;
  super.initState();
}
</code></pre>
<ul>
<li>didChangeDependencies<br>
用来专门处理 State 对象依赖关系的变化，这里说的 State 为全局 State ，例如语言或者主题等，会在 initState() 方法调用结束后被调用。</li>
</ul>
<pre><code>@override
void didChangeDependencies() {
super.didChangeDependencies();
}
</code></pre>
<ul>
<li>build<br>
主要是构建视图，返回需要渲染的 Widget ，由于 build 会被调用多次，因此在该函数中只能做返回 Widget 相关逻辑，避免因为执行多次导致状态异常。</li>
</ul>
<pre><code>@override
Widget build(BuildContext context, MyButtonState state) {
  return Container(color:Colors.red);
}
</code></pre>
<ul>
<li>
<p>reassemble<br>
主要是提供开发阶段使用，在 debug 模式下，每次热重载都会调用该函数，因此在 debug 阶段可以在此期间增加一些 debug 代码，来检查代码问题。</p>
</li>
<li>
<p>didUpdateWidget<br>
该函数主要是在组件重新构建，比如说热重载，父组件发生 build 的情况下，子组件该方法才会被调用，其次该方法调用之后一定会再调用本组件中的 build 方法。</p>
</li>
</ul>
<pre><code>@override
void didUpdateWidget(MyHomePage oldWidget) {
  super.didUpdateWidget(oldWidget)
}
</code></pre>
<ul>
<li>deactivate<br>
在组件被移除节点后会被调用，如果该组件被移除节点，然后未被插入到其他节点时，则会继续调用 dispose 永久移除。</li>
</ul>
<pre><code>@override
void deactivate() {
  super.deactivate();
}
</code></pre>
<ul>
<li>dispose<br>
永久移除组件，并释放组件资源。</li>
</ul>
<pre><code>@override
dispose() {
  super.dispose();
}
</code></pre>
<h2 id="widget生命周期流程">Widget生命周期流程</h2>
<p>Widget 的生命周期主要体现在 State 对象的回调方法中，主要执行流程如下图：<br>
<img src="https://git4pl.github.io/post-images/1629984803865.png" alt="" loading="lazy"></p>
<p>上述过程可以分为3个主要阶段：创建 Widget、更新 Widget 和销毁 Widget。</p>
<h3 id="创建">创建</h3>
<p>State 的初始化流程是从系统调用 StatefulWidget 的 createWidget方 法开始的，在 createWidget 方法中会调用 State 类的构造方法，接着依次执行State类生命周期方法：initState -&gt; didChangeDependencies -&gt; build，然后完成页面渲染。</p>
<p>构造方法是 State 生命周期的起点，Flutter 通过调用 StatefulWidget.createState() 创建一个 State，父 Widget 可以通过构造方法传入一些初始化 UI 的数据。当 State 对象被插入到视图树时会调用 initState 方法，这个方法在 State 对象的生命周期中只会被调用一次，我们可以在这里做一些初始化工作。didChangeDependencies 方法是用来专门处理 State 对象依赖关系变化的，它会在 initState 执行结束后立即被调用。build 方法用于构建UI视图，经过前面的步骤，当前 State 对象已经准备好了，在 build 方法中根据父 Widget 传递的初始化数据以及 State 的当前状态，创建一个 Widget 返回。</p>
<h3 id="更新">更新</h3>
<p>当一个Widget收到其父 Widget 的重构请求，或者由于用户的交互触发内部 State 对象改变并调用 setState 方法，或者当前 Widget 被插入到新的视图树中，或者在开发过程中发生热重载，将触发 Widget 的 State 更新，更新的场景比较复杂，每次更新都会调用 build 方法。State 更新主要由3个方法触发：setState、didchangeDependencies 与 didUpdateWidget。</p>
<p>父 Widget 根据 runtimeType 和 Widget.key 确定需要更新的视图树中某个位置的 Widget，当 Widget 的配置发生变化或热重载时，会调用 didUpdateWidget 方法。当 State 内部状态改变时，可以在当前 State 中直接调用 setState 方法更新状态数据，setState 的回调是立即执行的，所以不能是 async 的。当 State 对象的依赖关系发生变化，比如系统语言或应用主题等全局的状态发生改变后，Framework 会通知 State 执行 didChangeDependencies 方法。</p>
<h3 id="销毁">销毁</h3>
<p>State 对象的销毁过程比较简单，包括移除和销毁 Widget 两个场景，系统会分别调用 deactivate 和 dispose 方法。</p>
<p>当组件的可见状态发生变化时，deactivate 函数会被调用，这时 State 会被暂时从视图树中移除。值得注意的是，页面切换时，由于 State 对象在视图树中的位置发生了变化，需要先暂时移除后再重新添加，重新触发组件构建，因此这个函数也会被调用。</p>
<p>当 State 被永久地从视图树中移除时，Flutter 会调用 dispose 函数。而一旦到这个阶段，组件就要被销毁了，所以我们可以在这里进行最终的资源释放、移除监听、清理环境等等。</p>
<figure data-type="image" tabindex="1"><img src="https://git4pl.github.io/post-images/1629985000809.png" alt="" loading="lazy"></figure>
<h2 id="app生命周期">App生命周期</h2>
<p>State 的生命周期定义了视图从加载到构建以及更新和销毁的全过程，根据其回调机制，我们可以在合适的回调方法中根据 Widget 的状态做相应的逻辑响应。在原生 Android 开发中，我们经常会处理 App 从后台进入前台、从前台切到后台，或者是 UI 绘制完成等时机的一些状态逻辑，通过重写 Activity 的生命周期回调方法或注册应用程序的相关通知，监听 App 的生命周期并做相应的处理。在 Flutter 中我们可以监听 WidgetsBindingObserver 的回调方法来实现同样的需求。</p>
<pre><code>abstract class WidgetsBindingObserver {
  //页面pop
  Future&lt;bool&gt; didPopRoute() =&gt; Future&lt;bool&gt;.value(false);
  //页面push
  Future&lt;bool&gt; didPushRoute(String route) =&gt; Future&lt;bool&gt;.value(false);
  //系统窗口相关改变回调，如旋转
  void didChangeMetrics() { }
  //文本缩放系数变化
  void didChangeTextScaleFactor() { }
  //系统亮度变化
  void didChangePlatformBrightness() { }
  //本地化语言变化
  void didChangeLocales(List&lt;Locale&gt; locale) { }
  //App生命周期变化
  void didChangeAppLifecycleState(AppLifecycleState state) { }
  //内存警告回调
  void didHaveMemoryPressure() { }
  //Accessibility相关特性回调
  void didChangeAccessibilityFeatures() {}
}
</code></pre>
<p>可以看出 WidgetsBindingObserver 类提供了很多回调方法，比如屏幕旋转、屏幕亮度变化、语言变化、内存警告等都可以通过监听这个类实现回调。通过给 WidgetsBinding 的单例对象设置监听器，就可以监听对应的回调方法。</p>
<h3 id="生命周期回调">生命周期回调</h3>
<p>didChangeAppLifecycleState 回调函数中，有一个参数类型为 AppLifecycleState 的枚举类，这个枚举类是 Flutter 对 App 生命周期状态的封装。它包括 resumed、inactive、paused 和 detached 4个状态，常用的是前3个。</p>
<ul>
<li>resumed：App可见，并能响应用户的输入</li>
<li>inactive：App处于不活动状态，无法处理用户响应；此时App可能仍可见，但在Android中被其他的Activity或Window抢占了焦点，在iOS中类似被其他页面获取了焦点</li>
<li>paused：App不可见并不能响应用户的输入，但是在后台继续活动中</li>
</ul>
<p>当切换Flutter应用到前后台时，App的状态变化如下：</p>
<ol>
<li>从前台退回后台：AppLifecycleState.resumed -&gt; AppLifecycleState.inactive -&gt; AppLifecycleState.paused</li>
<li>从后台切入前台：AppLifecycleState.paused -&gt; AppLifecycleState.inactive -&gt; AppLifecycleState.resumed</li>
</ol>
<h3 id="帧绘制回调">帧绘制回调</h3>
<p>除了需要监听 App 的生命周期回调做相应的处理之外，有时候我们还需要在组件渲染之后做一些与显示安全相关的操作。在Flutter中同样使用WidgetsBinding来监听帧绘制回调，提供了单次 Frame 绘制回调，以及实时 Frame 绘制回调两种机制：</p>
<ul>
<li>单次 Frame 绘制回调，会在当前 Frame 绘制完成后进行回调，并且只会回调一次，如果要再次监听则需要再设置一次</li>
</ul>
<pre><code>WidgetsBinding.instance.addPostFrameCallback((_) {
  print(&quot;单次Frame绘制回调&quot;); //只回调一次
});
</code></pre>
<ul>
<li>实时 Frame 绘制回调，则通过 addPersistentFrameCallback 实现。这个函数会在每次绘制 Frame 结束后进行回调，可以用做 FPS 监测。</li>
</ul>
<pre><code>WidgetsBinding.instance.addPersistentFrameCallback((_) {
  print(&quot;实时Frame绘制回调&quot;); //每帧都回调
});
</code></pre>
<h3 id="页面切换监听">页面切换监听</h3>
<p>Flutter提供了可以监听App中页面间切换的接口类RouteAware，用以在合适的时候对页面中控件进行动画暂停或者资源释放。</p>
<pre><code>/// An interface for objects that are aware of their current [Route].
///
/// This is used with [RouteObserver] to make a widget aware of changes to the
/// [Navigator]'s session history.
abstract class RouteAware {
  /// Called when the top route has been popped off, and the current route
  /// shows up.
  void didPopNext() { }

  /// Called when the current route has been pushed.
  void didPush() { }

  /// Called when the current route has been popped off.
  void didPop() { }

  /// Called when a new route has been pushed, and the current route is no
  /// longer visible.
  void didPushNext() { }
}
</code></pre>
<p>假如有3个页面分别是A、B、C，跳转逻辑由A-&gt;B-&gt;C，而RouteAware使用with混淆在B中。</p>
<ul>
<li>didPopNext：在C页面关闭后，B页面调起该方法；</li>
<li>didPush: 当由A打开B页面时，B页面调起该方法；</li>
<li>didPop: 当B页面关闭时，B页面调起该方法；</li>
<li>didPushNext: 当从B页面打开C页面时，该方法被调起。</li>
</ul>
<p><strong>使用方法</strong></p>
<ol>
<li>使用 with 关键字在 State 类中使用，使用前在 MaterialApp 中定义一个 RouteObserver 对象</li>
</ol>
<pre><code>final RouteObserver&lt;Route&lt;dynamic&gt;&gt; routeObserver = RouteObserver();

class MyApp extends StatelessWidget {
    Widget _buildMaterialApp() =&gt; MaterialApp(
      initialRoute: '/',
      navigatorObservers: [routeObserver], //添加路由观察者
      onGenerateRoute: _onGenerateRoute);
}
</code></pre>
<ol start="2">
<li>在B页面中混淆 RouteAware，并注册 RouteObserver，记得在 dispose 方法中取消注册</li>
</ol>
<pre><code>class B extends StatefulWidget {
     B({Key key}) : super(key: key);
}

class BState extends State&lt;B&gt;  with RouteAware {

  @override
  void didChangeDependencies() {
    routeObserver.subscribe(this, ModalRoute.of(context)); //订阅
    super.didChangeDependencies();
  }

  @override
  void didPush() {
    debugPrint(&quot;didPush&quot;);
    super.didPush();
  }

  @override
  void didPop() {
    debugPrint(&quot;didPop&quot;);
    super.didPop();
  }

  @override
  void didPopNext() {
    debugPrint(&quot;didPopNext&quot;);
    super.didPopNext();
  }
  
@override
  void didPushNext() {
    debugPrint(&quot;didPushNext&quot;);
    super.didPushNext();
  }

  @override
  void dispose() {
    routeObserver.unsubscribe(this); //取消订阅
    super.dispose();
  }
}
</code></pre>
<h2 id="总结">总结</h2>
<ol>
<li>Flutter Widget 生命周期的实际承载者是State类，其主要过程分为创建（插入视图树）、更新（在视图树中发生状态变化）和销毁（从视图树中移除）3个阶段；</li>
<li>StatelessWidget 用于不需要维护状态的场景，只会执行一次 build 方法，StatefulWidget 的 build 方法会被多次执行，触发函数是 setState、didChangeDependencies、didUpdateWidget；</li>
<li>Flutter App 生命周期可以通过 WidgetsBinding 注册监听 WidgetsBindingObserver 类的回调方法来获取；</li>
<li>帧绘制回调可以通过 WidgetsBinding 注册 FrameCallback 的回调方法获取，包括单次 Frame 绘制回调和实时 Frame 绘制回调；</li>
<li>监听页面间切换使用 RouteAware。</li>
</ol>
<h2 id="refers-to">Refers To</h2>
<p><a href="https://api.flutter.dev/flutter/widgets/State-class.html">State<T extends StatefulWidget> class</a><br>
<a href="https://www.jianshu.com/p/06dadf6ed3a2">Flutter widget生命周期详解</a><br>
<a href="https://www.jianshu.com/p/3edfa7d694e7">Flutter - 监视页面的切换</a></p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[探究Jetpack Compose实现原理]]></title>
        <id>https://git4pl.github.io/post/T_YU0mYZa/</id>
        <link href="https://git4pl.github.io/post/T_YU0mYZa/">
        </link>
        <updated>2021-05-07T09:24:45.000Z</updated>
        <content type="html"><![CDATA[<h3 id="整体设计原理">整体设计原理</h3>
<p>我们熟悉的 Android View 框架以 View 树的形式描述 UI，Jetpack Compose 构建 UI 也是树的形式，它在 UI 层级中的基本元素是 LayoutNode 节点。因为 Compose 是声明式 UI，需要将配置和绘制分离，同时设计算法对节点进行复用，以实现高性能。Jetpack Compose UI 框架的主要技术点大概如下：</p>
<ul>
<li>@Composable 注解函数实现 UI 的声明和配置</li>
<li>基于注解和 Composer 实现 LayoutNode 的缓存及复用，同时实现对属性变化的监听</li>
<li>LayoutNode 主要实现 UI 布局和绘制</li>
</ul>
<h3 id="compose-recompose-原理">Compose &amp; Recompose 原理</h3>
<p>使用 Jetpack Compose 构建 UI 最明显的一个特点是通过 @Composable 注解函数来声明 UI 内容，这里使用注解函数声明 UI 不能说 Compose 框架是一个注解处理器，Compose 应用了 Kotlin 编译器的新特性 —— IR extension（intermediate representation的缩写，意为中间语言），在 Kotlin 编译器的类型检测和代码生成阶段添加一些代码逻辑。@Composable 注解和 Kotlin 的 suspend 关键字类似，可以适用于函数、Lambda 或者函数类型，它会导致被注解的元素类型改变，未被注解的函数类型与注解后的相同函数类型互不兼容。</p>
<pre><code>@Composable
fun Greeting(name: String) {
    Text(text = &quot;Hello $name!&quot;)
}
</code></pre>
<p>将上面这段代码反编译后得到如下的代码内容，编译器在参数列表中增加了两个参数，在代码逻辑中也增加了很多条件判断和代码块，可见编译器做了大量的工作来实现简洁的声明式 API 调用。<br>
<img src="https://git4pl.github.io/post-images/1620379910698.png" alt="" loading="lazy"></p>
<p>从反编译的代码可以看到，编译器加入了一个叫做 Composer 的参数，这个参数就是在 Composable 函数中传递的上下文调用对象。Composer 对象将 Compose 框架里的 Composition 和 Recomposition 过程贯穿起来了，这里涉及到 Compose 框架的核心绘制机制。</p>
<p>Composer 可以理解为与 Android View 框架里的 Context 类似的对象，负责存储所有跟 Compose UI 有关的状态和生命周期，它会从 UI 的根节点一直往下传，连接所有调用到的 Composable 函数，所以 Composer 知道上一次渲染的状态，而且这些状态都有对应的位置。</p>
<p>Composer 的实现包含了一个与 Gap Buffer（间隙缓冲区）密切相关的数据结构 SlotTable，像是我们熟悉的 HashMap 一样，一个 key 对应到一个状态，这里的 key 就是上面反编译代码新增的第二个参数 $changed，它是这个函数的调用点所代表的源码位置的哈希值。</p>
<h4 id="间隙缓冲区是如何工作的">间隙缓冲区是如何工作的？</h4>
<p>这里我们简单理解一下间隙缓冲区的工作原理。间隙缓冲区代表了一个包含了当前索引的集合，在内存里用数组来实现，这个数组的大小会比它代表的数据集合要大一些，没有被使用的空间被称为 Gap（间隙）。</p>
<p>一个正在执行的 Composable UI 层级在这个数据结构中可能如下所示。在这个SlotTable 数据结构中存在 CompositionData 和 CompositionGroup 两种概念的数据，分别表示单个的 UI 控件或者是一组控件。<br>
<img src="https://git4pl.github.io/post-images/1620379978423.png" alt="" loading="lazy"></p>
<p>假设已经完成了层级结构的执行，在某个时候，我们需要更新 UI，这会重新组合一些 UI 元素，这叫做重组（Recomposition）。重组的时候将索引值重置回数组开头并再次进行遍历，遍历过程中查看数据，然后可能什么也不做，或者更新数据值。</p>
<figure data-type="image" tabindex="1"><img src="https://git4pl.github.io/post-images/1620380008478.pn" alt="" loading="lazy"></figure>
<p>如果要改变 UI 结构，并希望插入一些新的控件，这时会在 SlotTable 中进行一次插入操作，将间隙插入到需要的位置，可以理解为将操作位置后面的原有数据平移到间隙后面，这样就可以在新插入的间隙处添加新的数据。</p>
<figure data-type="image" tabindex="2"><img src="https://git4pl.github.io/post-images/1620380040341.png" alt="" loading="lazy"></figure>
<p>我们再通过下面这段 Compose 代码及其反编译代码理解 Compose 编译器为我们做的事，并理解 SlotTable 数据结构的工作机制和性能问题。</p>
<pre><code>@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count += 1 }) {
        Text(text = &quot;Count: $count&quot;, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp))
    }
}
</code></pre>
<figure data-type="image" tabindex="3"><img src="https://git4pl.github.io/post-images/1620380073540.png" alt="" loading="lazy"></figure>
<p>可以看到编译器在函数体中插入了一些额外的调用，并且这些调用是 Composer 对象的 startXXXGroup 和 endXXXGroup 方法的成对出现，在 Composer 接口中定义了 Replaceable、Movable、Restart 和 Default 这几种 Group，这些 group 适用于不同逻辑控制条件，每个 group 本身是树形结构，类似 HashMap 的 SlotTable 数据结构可以高效地操作和复用 slot。</p>
<p>在 SlotTable 的操作性能方面，除了需要移动间隙时，其他的操作包括 get、move、insert 和 delete 都是常数级的时间复杂度，只有当 UI 结构发生改变时需要移动间隙，移动间隙的时间复杂度是 O(n)。通常我们定义好的 UI 内容不会频繁地改变，只是提供给 UI 展示的数据和 UI 属性会发生变化，所以 SlotTable 的操作性能是可接受的。</p>
<h4 id="位置记忆化是什么">位置记忆化是什么？</h4>
<p>Compose 中的这种类缓存的数据结构可以在 UI 中任意实现控制流，编译器管理 UI，它是基于位置记忆化实现的。</p>
<p>什么是位置记忆化呢？前文已经介绍了 Composable 函数在编译后编译器给添加了一个 Composer 的参数，它包含了当前组件在树形结构中的位置以及将要访问哪些节点的上下文信息。编译器的目标是保留这样的数据模型，并且要复用 UI 在上一次执行过程中创建的节点，不用在每次执行时都要创建新的节点，也就是希望缓存每个节点。假设要缓存每个节点，那么每次执行函数时，就需要以相同的顺序查看缓存。从上面反编译的代码中可以看出，Composer 对象调用的 group 方法中第一个参数是一个数值，从 Composer 的源码注释可以知道那个数值是基于代码位置的一个 key，也就是表明所调用的函数在代码文件中的行信息，所以这个 key 在同一个代码文件中不会重复。在 Composer 中用一个带有 Gap Buffer 的数组来缓存代表函数位置的 key，每次执行同一个 Composable 函数时，它的数据模型可以被复用，只要 UI 结构不改变，就只需要更新数据就可以了。</p>
<h4 id="kotlin-语言特性对-compose-的支持">Kotlin 语言特性对 Compose 的支持</h4>
<p>Jetpack Compose 完全采用 Kotlin 实现，Kotlin 提供的一些语言特性使得编写高质量的 Compose 代码变得很容易，理解了 Kotlin 的这些语言特性也能很好地理解 Compose 代码背后的运行机制。</p>
<ul>
<li>
<p>默认参数<br>
Kotlin 函数参数可以指定默认值，调用方没有明确传递相应的值时，系统会使用默认值，这减少了对函数的重载。默认参数结合命名参数使代码读起来更清晰，参数具有自描述性，也更容易理解代码。</p>
</li>
<li>
<p>高阶函数和 Lambda 表达式<br>
Compose 中的函数大量应用 Kotlin 高阶函数，高阶函数与 Lambda 表达式自然配对，如果只需要该函数在一处调用，那么可以直接在函数被调用处用 Lambda 表达式。特别地，如果需要的高阶函数调用位置是函数的最后一个参数，可以使用尾随 Lambda，直接将表达式部分放到函数的圆括号后面，用大括号包起来。</p>
</li>
<li>
<p>范围和接收器<br>
有些方法和属性仅在某一范围内可用，限定的范围可让你在需要的地方提供相关功能，避免意外地在不当之处使用该功能。</p>
</li>
<li>
<p>委托属性<br>
Kotlin 支持属性委托，这些属性就像字段一样被调用，但是它们的值是通过对表达式求值动态确定的。</p>
</li>
<li>
<p>解构数据类<br>
对于数据类，可以使用解构声明来轻松地访问数据。</p>
</li>
</ul>
<p>其他被应用的特性包括单例对象、类型安全构建器和协程等。</p>
<h4 id="重组的实现原理">重组的实现原理</h4>
<p>当 Composable 函数的输入变更时再次调用该函数的过程就是重组，Composable 函数重组是如何触发的呢？还是以上面的 Counter 函数为例，编译器在函数末尾调用 endGroup 方法返回一个 ScopeUpdateScope 对象，当对象不为空的时候调用 updateScope 方法，将在需要时重新调用当前的 Composable 函数。</p>
<p>当 Compose 根据新输入重组时，它仅仅调用可能已经更改的函数或 Lambda，会跳过其余的函数或 Lambda，这样 Compose 可以高效地重组。</p>
<h3 id="关于-compose-的注意事项">关于 Compose 的注意事项</h3>
<p>在使用 Compose 编程时有许多注意事项，大概就像我们用 Android View 框架开发自定义 UI 组件时那样，不能在 onDraw 方法中执行耗时的任务，也不要在 onDraw 方法中创建对象等等，使用 Compose 编程的最佳做法是使 Composable 函数保持快速执行、幂等且没有副作用，因为 Compose 具有以下特点：</p>
<ul>
<li>
<p>Composable 函数可以按任何顺序执行<br>
在有 Composable 函数的代码中，Composable 函数可能不会按其在代码中出现的顺序执行，Compose 可以识别出某些 UI 元素的优先级高于其他 UI 元素，因而首先绘制这些元素。</p>
</li>
<li>
<p>Composable 函数可以并行执行<br>
Compose 可以通过并行运行 Composable 函数来优化重组，这样 Compose 就可以利用多个核心，并以较低的优先级运行 Composable 函数，Composable 函数可能会在后台线程池中执行。所以 Composable 函数都不应有副作用。</p>
</li>
<li>
<p>重组会跳过尽可能多的 Composable 函数和 Lambda<br>
如果界面上的某些部分没有变化，Compose 会尽力只重组需要更新的部分。</p>
</li>
<li>
<p>重组是乐观操作<br>
Compose 预计会在参数再次更改之前完成重组，如果某个参数在重组完成之前更改了，Compose 可能会取消重组，然后使用新参数重新开始。</p>
</li>
<li>
<p>Composable 函数可以非常频繁地运行<br>
有时可能会针对界面动画的每一帧运行一个 Composable 函数，如果该函数执行耗时操作，就会导致界面卡顿。</p>
</li>
</ul>
<h3 id="与android-view交互">与Android View交互</h3>
<p>Jetpack Compose 是一套全新的 UI 构建框架，但它也能配合现有的 View 框架构建 UI，官方建议构建新的应用最好选择 Compose 实现整个界面，当然需要等 Compose 正式发布后，对于现有的应用可以使用 Compose 和 View 结合逐步修改界面的构建方式。</p>
<p>Compose 与 Android View 的结合使用主要有两种方式：</p>
<ul>
<li>在Android View 中使用 Compose</li>
<li>在Compose 中使用 Android View</li>
</ul>
<p>如果要将 Compose 界面内容加入到现有的 View 布局中，可以使用 ComposeView 组件，调用其 setContent 方法，在该方法中可以直接调用 Composable 函数。ComposeView 是一个 Android View，可以像其他 View 组件一样放到 XML 布局中，然后用 id 获取 ComposeView，并调用 setContent() 以使用 Compose。如果整个 UI 布局是使用 Compose 构建的，可以直接在 fragment 中添加 ComposeView，这样可以完全避免使用 XML 布局文件。</p>
<p>另一方面，如果要在 Compose 构建的界面中添加 Android View 控件或层级结构，可以使用 AndroidView 可组合项。系统会向 AndroidView 传递一个返回 View 的 Lambda，同时提供了在布局加载完成后被调用的 update 回调，每当在该回调中读取的 State 发生变化时，AndroidView 都会进行重组。</p>
<h3 id="refers-to">Refers To</h3>
<p><a href="http://intelligiblebabble.com/compose-from-first-principles/">Compose From First Principles</a><br>
<a href="https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd">Under the hood of Jetpack Compose</a><br>
<a href="https://slacker.ro/2021/03/08/%e5%88%9d%e6%8e%a2-jetpack-compose%e2%80%8a-%e2%80%8a%e6%b8%b2%e6%9f%93%e6%a9%9f%e5%88%b6%ef%bc%88rendering%ef%bc%89/">初探 Jetpack Compose — 渲染機制（Rendering）</a></p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[初探Jetpack Compose]]></title>
        <id>https://git4pl.github.io/post/uDFS_pXuN/</id>
        <link href="https://git4pl.github.io/post/uDFS_pXuN/">
        </link>
        <updated>2021-04-11T02:37:18.000Z</updated>
        <content type="html"><![CDATA[<h2 id="jetpack-compose是什么">Jetpack Compose是什么？</h2>
<p>Jetpack Compose是Google在2019年5月的I/O大会上公布的Android最新的UI框架，官网上介绍它可以简化并加快 Android 上的界面开发，使用更少的代码、强大的工具和直观的 Kotlin API，快速让应用生动而精彩。经过两年多的开发，Jetpack Compose目前是Beta版本。<br>
<img src="https://git4pl.github.io/post-images/1618108739331.png" alt="" loading="lazy"></p>
<p>从2008年谷歌正式发布Android系统后，一直到2019年推出Jetpack Compose，Android系统在长达10多年的发展过程中，谷歌针对不同的问题都做过了很多调整，但是在UI构建方面，除了陆续推出RecyclerView、CardView、ConstraintLayout等高级UI控件外，Android系统最初的那一套View System一直沿用到今天，UI构建体系几乎没有做任何改变，View.java 文件已经有超过30000行代码了。谷歌官方也清楚View系统越来越庞大，并且有一些问题，但很为难的是不能轻易修改现有的View类，因为有很多应用可能依赖了这些API。<br>
<img src="https://git4pl.github.io/post-images/1618108783701.png" alt="" loading="lazy"></p>
<p>Jetpack Compose直接抛弃了View和ViewGroup那一套东西，从渲染机制、布局机制、触摸算法到 UI 的具体写法都是全新的方式，它受到React、Litho、Vue.js 和 Flutter的启发，完全采用Kotlin开发，实现声明式UI。</p>
<h2 id="compose解决什么问题">Compose解决什么问题？</h2>
<p>我们知道软件工程中有个原则是高内聚低耦合，高内聚是指一个软件模块由相关性很强的代码组成，只负责一项任务，即遵循单一职责原则；低耦合指一个完整的系统中模块与模块之间尽可能相互独立，每个模块独立完成特定的子功能，模块间通信的接口尽可能少而简单。编写代码时我们会创建包含多个单元的模块，&quot;耦合&quot; 便是不同模块中单元之间的依赖关系，它反映了一个模块中的各部分是如何影响另一个模块的各个部分的；&quot;内聚&quot; 则表示的是一个模块中各个单元之间的关系，它指示了模块中各个单元相互组合的合理程度。<br>
<img src="https://git4pl.github.io/post-images/1618108797869.png" alt="" loading="lazy"></p>
<p>当我们用Android View框架实现app的UI时我们需要 view model + 布局 共同完成，view model提供数据给布局，布局文件渲染出UI效果。通常view model会使用像 Kotlin或Java 这样的编程语言实现，而布局则使用 XML，由于两种实现语言的差异，即使view model与xml布局联系紧密，它们之前也有明显的分隔线，它们的联系属于模块间的耦合关系。</p>
<p>实际上这里隐藏了很多依赖关系，我们通过使用一些API访问布局中的UI控件，比如findViewById(ResId)、button.setText(String)、container.addChild(View)、img.setImageBitmap(Bitmap)等，使用这些API需要了解xml布局如何定义并创建view model和布局的关联，由于xml布局文件通常没有很好的可复用性，随着app的规模逐渐增长，这些依赖关系将越积越多。</p>
<p>当我们处理紧耦合的模块时，对一个地方的代码改动，便意味对其他的模块作出许多其他的改动，手动地修改代码增加了犯错的可能性。更糟的是有些耦合常常是隐式的，以至于看起来毫无关联的修改，却会造成了意料之外的错误发生。大多数现代应用会动态展示 UI，并且会在执行过程中不断演变。结果导致应用不仅要验证布局 XML 是否静态地满足了这些依赖关系，而且还需要保证在应用的生命周期内满足这些依赖。如果一个元素在运行时离开了视图层级，一些依赖关系可能会被破坏，并导致NullPointerException等问题。<br>
<img src="https://git4pl.github.io/post-images/1618108938524.png" alt="" loading="lazy"></p>
<p>这就引出了一个问题: 如果我们用和实现view model相同的语言定义布局与UI结构会怎样？如果我们选用 Kotlin 来做这件事会怎样？</p>
<p>Jetpack Compose完全采用Kotlin语言开发，你可以运用底层语言的全部灵活特性。由于我们可以使用相同的实现语言，一些隐式的依赖关系可能会变得更加明显，我们也可以重构代码并将其移动至那些可以使它们减少耦合和增加内聚的位置。<br>
<img src="https://git4pl.github.io/post-images/1618108960929.png" alt="" loading="lazy"></p>
<p>你可能会以为这是建议将逻辑与 UI 混合起来，实际上无论如何组织架构，你的应用中都将出现与 UI 相关联的逻辑。Compose 框架本身并不会改变这一点，不过框架可以为你提供一些工具，从而帮你更简单地实现关注点分离: 这一工具便是 Composable 函数。它就像你已经很熟悉的普通函数一样，你在使用普通函数时所掌握的重构和编写可靠、可维护、干净代码的技能都可以运用到 Composable 函数上。</p>
<p>Android View 框架的耦合性还体现在 View 组件的继承实现方式上，前文已说明 View 类拥有将近3万行代码，而所有 UI 控件都继承自该类，导致一些本身功能很简单的控件通过继承获得了很多不需要的属性和行为，所以在软件设计原则中有组合优于继承的说法。Compose 框架使用一组接受数据并生成 UI 元素的 Composable 函数来构建 UI，后文将详细说明 Compose 的使用组合的优势。</p>
<h2 id="compose相比xml布局有啥优势">Compose相比XML布局有啥优势？</h2>
<p>Jetpack Compose库除了解决构建UI时的代码耦合问题，借助统一的Kotlin语言，用更少的代码、强大的工具和直观的API快速构建Android app，Compose相比XML布局还有什么优势呢？</p>
<h3 id="声明式ui-vs-命令式ui">声明式UI vs 命令式UI</h3>
<p>传统的 Android View 框架是面向对象的命令式UI工具，它通过加载xml布局文件，实例化一棵View控件树来初始化UI，每个控件都维护了自己的内部状态，并提供 getter/setter 方法，app逻辑层代码通过调用View控件暴露的方法来获取View的状态或者更新View状态。UI的更新依赖于逻辑层调用控件对象的 API，像是接受命令一样。</p>
<p>Jetpack Compose是声明式的UI框架，UI控件是相对无状态的，不对外提供 getter/setter 方法，实际上声明式UI控件不是对象，通过更新参数调用同一个 Composable 函数来更新 UI。在 Compose 框架中一切都是函数，并且都是顶层函数，Composable 函数响应快速，具有幂等性且没有副作用，幂等性是指使用同一参数多次调用此函数时它的行为方式相同。用一组函数来声明 UI，一个 Composable 函数可以嵌套另一个 Composable 函数，并且只能被 Composable 函数调用。</p>
<p>这里用一个例子说明命令式 UI 和声明式 UI 代码的区别。假设有一个带有未读消息图标的电子邮件应用。如果没有消息，应用会绘制一个空信封；如果有一些消息，我们会在信封中绘制一些纸张；而如果有 100 条消息，我们就把图标绘制成好像在着火的样子......<br>
<img src="https://git4pl.github.io/post-images/1618108980548.png" alt="" loading="lazy"></p>
<p>使用命令式实现，我们可能会写出下面这样更新消息数量的逻辑函数：</p>
<pre><code>fun updateCount(count: Int) {
  if (count &gt; 0 &amp;&amp; !hasBadge()) {
    addBadge()
  } else if (count == 0 &amp;&amp; hasBadge()) {
    removeBadge()
  }
  if (count &gt; 99 &amp;&amp; !hasFire()) {
    addFire()
    setBadgeText(&quot;99+&quot;)
  } else if (count &lt;= 99 &amp;&amp; hasFire()) {
    removeFire()
  }
  if (count &gt; 0 &amp;&amp; !hasPaper()) {
   addPaper()
  } else if (count == 0 &amp;&amp; hasPaper()) {
   removePaper()
  }
  if (count &lt;= 99) {
    setBadgeText(&quot;$count&quot;)
  }
}
</code></pre>
<p>如果用声明式来实现这一逻辑和UI则会像下面这样：</p>
<pre><code>@Composable
fun BadgedEnvelope(count: Int) {
  Envelope(fire=count &gt; 99, paper=count &gt; 0) {
    if (count &gt; 0) {
      Badge(text=&quot;$count&quot;)
    }
  }
}
</code></pre>
<p>这里我们定义集中状态：</p>
<ul>
<li>当数量大于99时，显示火焰；</li>
<li>当数量大于0时，显示纸张；</li>
<li>当数量大于0时，绘制数量气泡。</li>
</ul>
<p>这便是声明式 API 的含义。我们编写代码来按我们的想法描述 UI，而不是如何转换到对应的状态。这里的关键是，编写像这样的声明式代码时，您不需要关注您的 UI 在先前是什么状态，而只需要指定当前应当处于的状态。框架控制着如何从一个状态转到其他状态，所以我们不再需要考虑它。</p>
<h3 id="组合-vs-继承">组合 vs 继承</h3>
<p>Android View 系统中的 UI 都是用 View 和 ViewGroup 对象的层级结构进行构建的，View 对象用于绘制用户可见的具体 UI 元素，ViewGroup 则是不可见的容器，用于布局 View 对象的排列方式；所有的 UI 元素都直接或间接地继承自 View 类，比如 EditText 继承自 TextView，而 TextView 又继承于 View，ViewGroup 类也继承于 View，自定义的容器类继承于 ViewGroup。<br>
<img src="https://git4pl.github.io/post-images/1618109033249.png" alt="" loading="lazy"></p>
<p>Jetpack Compose 使用组合而不是继承的方式构建 UI 元素，由于 Composable 函数都是顶层函数，Text、Image、CustomComposables 和所有 UI 组件都是 Composable 函数，它们通过组合其他函数一起构建 UI，Compose 还提供了一系列现成可用的布局来帮助排列 UI 元素，类似 View 框架中的 ViewGroup 容器。<br>
<img src="https://git4pl.github.io/post-images/1618109011415.png" alt="" loading="lazy"></p>
<p>我们知道软件设计中有一条原则是“多用组合，少用继承”，在《阿里巴巴Java开发手册》中也推荐谨慎使用继承的方式进行扩展，优先使用组合的方式实现。继承层次过深、继承关系过于复杂时会影响到代码的可读性和可维护性。</p>
<h3 id="单向数据流">单向数据流</h3>
<p>Jetpack Compose 参考了其他现代 UI 框架比如 Flutter 和 React 的思想，采取单一向下数据流和单一向上事件流的方式构建或重组 UI。简单来说，就是由父组件向子组件传递数据，子组件通过数据构建 UI，当子组件发送交互事件时，通过Lambda 方法将行为的发生交与父组件处理，父组件处理后修改数据，再通过单一向下数据流的原则通知子组件变化。</p>
<p>这是一种单向数据流的设计模式，这种模式下状态从有状态可组合项向下传递，而事件从无状态可组合项向上流动。<br>
<img src="https://git4pl.github.io/post-images/1618109087054.png" alt="" loading="lazy"></p>
<p>Compose 中的状态提升是一种将状态移至可组合项的调用方以使可组合项无状态的模式。Jetpack Compose 中的常规状态提升模式是将状态变量替换为两个参数：</p>
<ul>
<li>value: T：要显示的当前值</li>
<li>onValueChange: (T) -&gt; Unit：请求更改值的事件，其中 T 是建议的新值。</li>
</ul>
<pre><code>@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf(&quot;&quot;) }
    HelloContent(name) { name = it }
}

@Composable
fun HelloContent(name: String, onNameChanged: (String) -&gt; Unit) {
    Column {
        Text(
            text = &quot;你好, $name！&quot;,
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChanged,
            label = { Text(&quot;Name&quot;) }
        )
    }
}
</code></pre>
<p>使用 Jetpack Compose 时遵循此模式可带来下面几项优势：</p>
<ul>
<li>可测试性：将状态与显示状态的界面分离开来，更容易单独测试这两者。</li>
<li>状态封装：因为状态只能在一个位置进行更新，并且可组合项的状态只有一个可信来源，所以不太可能由于状态不一致而产生错误。</li>
<li>界面一致性：通过使用可观察的状态容器，例如 LiveData 或 StateFlow，所有状态更新都会立即反映在界面中。</li>
</ul>
<h2 id="compose框架组成">Compose框架组成</h2>
<p>Jetpack Compose 库由 androidx 中6个 Maven 组构成，每个组都包含一套特定用途的功能。</p>
<h4 id="composeanimation">compose.animation</h4>
<p>在Jetpack Compose 应用中构建动画，丰富用户体验。</p>
<h4 id="composecompiler">compose.compiler</h4>
<p>借助 Kotlin 编译器插件，转换 @Composable 函数并启用优化功能。</p>
<h4 id="composefoundation">compose.foundation</h4>
<p>使用现成可用的构建块编写 Jetpack Compose 应用，还可扩展 foundation 以构建你自己的设计系统元素。</p>
<h4 id="composematerial">compose.material</h4>
<p>使用现成可用的 Material Design 组件构建 Jetpack Compose UI，这是更高层级的 Compose 入口点，旨在提供与 www.material.io 上描述的组件一致的组件。</p>
<h4 id="composeruntime">compose.runtime</h4>
<p>Compose 的编程模型和状态管理的基本构建模块，以及 Compose 编译器插件针对的核心运行时库。</p>
<h4 id="composeui">compose.ui</h4>
<p>与设备互动所需的 Compose UI 的基本组件，包括布局、绘图和输入。</p>
<h3 id="refers-to">Refers To：</h3>
<p><a href="https://juejin.cn/post/6885900954307133448">深入详解 Jetpack Compose | 优化 UI 构建</a><br>
<a href="https://medium.com/androiddevelopers/jetpack-compose-before-and-after-8b43ba0b7d4f">使用Compose前后</a><br>
<a href="https://blog.csdn.net/zhireshini233/article/details/114708862">Jetpack Compose漫谈</a><br>
<a href="https://juejin.cn/post/6844903982742126600">Jetpack Compose 最新进展</a></p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[关于背压的理解]]></title>
        <id>https://git4pl.github.io/post/imXjzp6FH/</id>
        <link href="https://git4pl.github.io/post/imXjzp6FH/">
        </link>
        <updated>2021-01-20T08:19:14.000Z</updated>
        <content type="html"><![CDATA[<h3 id="背压是什么">背压是什么？</h3>
<p>初次听到“背压”这个词感觉像是“天王盖地虎”这样的行话，不知道是什么，感觉很高深的样子。</p>
<p>网络上有很多关于背压的文章，但对背压这个词的解释各不相同。有的人把它看作一个需要避免的问题，或者程序的异常，描述语言类似于“...发生了背压”；有的人说背压是解决这种在异步场景中被观察者发送事件速度远快于观察者的处理速度的问题的策略；还有的人认为背压是具有解决这种问题的能力或机制，比如说某某库支持背压或不支持背压。</p>
<p>维基百科上对 Backpressure 的定义是：Backpressure is a resistance or force opposing the desired flow of fluid through pipes（背压是抵抗所需的流体通过管道流动的阻力或反作用力），借用到软件领域它的定义应该是：Backpressure is a resistance or force opposing the desired flow of data through software。</p>
<p>国内有位Android大咖<a href="https://www.zhihu.com/people/rengwuxian">扔物线</a>在知乎上的一篇关于背压的回答我比较赞同，他说 Backpressure 其实是一种现象：在数据流从上游生产者向下游消费者传输的过程中，上游生产速度大于下游消费速度，导致下游的 Buffer 溢出，这种现象就叫做 Backpressure 出现。响应式编程中的【背压（Backpressure）】概念来源于流体力学的比喻，也和其他工程领域的背压概念相似，比如在管道运输中，气流或液流由于管道突然变细、急弯等原因导致由某处出现了下游向上游的逆向压力，这种情况称为 Backpressure。背压是我们在软件开发中经常遇到并且有时不得不处理的问题，但这个词以及表达的现象并没有被正确地理解和认识。背压并不是一种机制，也不是一种策略，背压现象中关键是下游的 Buffer 溢出。</p>
<h3 id="背压的例子">背压的例子</h3>
<h4 id="读写文件">读写文件</h4>
<p>一般写文件比读文件要慢，假设一个磁盘驱动有效的读文件速度是150MB/s，而写文件速度是100MB/s，如果要以最快的速度将文件读入内存，同时又以最快的速度将其写回磁盘，则必须每秒缓冲50MB，这样内存将被不断被堆积，在输入文件被完全读取之前，内存中还有大量的数据没有被写入文件。假设要读入一个6GB的文件然后将它写入磁盘，在你读完全部文件时，还有2GB的内容等待写入。</p>
<pre><code>6 GB / 150 MB/s = 40 s
150 MB - 100 MB = 50 MB
50 MB x 40 = 2 GB !!!
</code></pre>
<p>这将浪费很多的内存，在一些系统上可能超出了可用内存，最终产生OutOfMemory Exception，这就是由于写文件速度慢于读文件速度产生了背压问题。</p>
<h4 id="服务器通信">服务器通信</h4>
<p>另一个关于背压问题的例子是服务器间的通信问题。在普遍采用的微服务架构中，各个服务器的职责一般是独立分散的，当一台服务器向另一台服务器发送请求的速度快于后者处理请求的速度时，通常会出现背压。<br>
如果服务器A向服务器B发送100个rps（每秒请求数），但服务器B只能处理75个rps，那么对于服务器B就有25个rps的富余。此时服务器B可能会落后，因为它需要处理这些请求，或者还需要与下游的其他服务器通信。这是由于服务器处理能力的差异，导致上下游通信的服务器在处理请求时产生了背压问题。</p>
<figure data-type="image" tabindex="1"><img src="https://git4pl.github.io/post-images/1611130854773.gif" alt="" loading="lazy"></figure>
<h4 id="渲染ui">渲染UI</h4>
<p>在渲染UI方面也经常发生背压的现象，当程序无法以所需的速度渲染时，就会发生背压，比如Android App的卡顿现象是由于应用程序的渲染速度没有达到60fps。</p>
<h3 id="如何解决背压">如何解决背压</h3>
<p>可以看出背压现象在我们的程序开发中普遍存在，而且各个场景下处理背压的思路都差不多。除了扩展可用的计算资源外，如何处理背压问题可以概括为三种可能的选择：</p>
<ul>
<li>控制生产者产生数据的速度以适应消费者的响应速度</li>
<li>缓存临时产生的大量数据</li>
<li>丢弃上游发出的事件（可能有多种策略）</li>
</ul>
<p><strong>控制</strong>生产者可能是处理背压问题的最优选择，如果这种方案可行的话，它只需要考虑设计好控制机制，而不会有内存和其他资源的开销。在数据消费端不需要额外的内存来缓存数据，也不需要顾虑丢弃数据。不幸的是控制生产者产生数据并不总是可行，比如数据的输入是用户操作，我们不能控制程序系统之外的数据生产者。</p>
<p><strong>缓存</strong>是大多数背压问题的解决方式。Backpressure 和 Buffer 是一对相生共存的概念，一方面只有设置了 Buffer，才有 Backpressure 出现；另一方面只要设置了 Buffer，一定存在出现 Backpressure 的风险。使用缓存时要考虑，缓冲区的增长速度有可能在相当长的一段时间内超过它的消耗速度吗？如果缓冲区是无界的，那么缓冲区可能是危险的，这意味着对缓冲区没有大小或时间限制。</p>
<p><strong>丢弃</strong>是最后一种策略，它也经常与缓存相结合。比如基于时间的采样，每秒丢弃10%的数据。丢弃的具体策略有多种。</p>
<h3 id="rxjava中如何解决背压">RxJava中如何解决背压？</h3>
<p>RxJava是一个使用可观察序列组成异步和基于事件的程序的库，在RxJava中会经常遇到一种情况就是被观察者发送消息太快以至于它的操作符或订阅者不能及时处理相关的消息，那么随之而来的问题就是如何处理这些不断累积的未处理的消息？</p>
<p>RxJava从1.x到3.x不同版本对这个问题的应对方式和支持力度不同，很多文章说RxJava 1.x不支持背压，根据这些文章的描述，我理解不支持背压说的是RxJava 1.x版本的Observable不能直接添加解决背压的策略，而RxJava 2.x以后版本的Flowable可以通过参数设置背压策略，其实RxJava 1.x版本提供了支持解决背压的其他操作符，比如onBackpressureBuffer、onBackpressureDrop和onBackpressureLatest。在使用Observable发射数据后不使用解决背压的操作符进行处理，或者在使用了背压策略后观察者仍不能及时处理上游的数据的情况下，可能会抛出MissingBackpressureException表示发生了背压问题。</p>
<p>Observable 数据流可分为两种类型：Cold 和 Hot Observable，并不是严格的概念区分，它们只是对于两类被观察的数据流的形象描述。</p>
<ul>
<li>Cold Observable：指的是被观察者发射数据的时机和频率由观察者决定，每个观察者都能接收到完整的事件序列，cold observable的示例包括数据库查询、文件检索和Web请求等</li>
<li>Hot Observable：指的是被观察者在被创建后就立即发射数据，观察者会在建立对被观察者的订阅关系时接收到数据，它不能干涉hot observable发射数据的行为，示例包括鼠标或键盘事件、系统事件等</li>
</ul>
<h4 id="recative-pull">Recative Pull</h4>
<p>用<em>控制</em> 的方式解决背压问题可以通过阻塞被观察者的线程来实现，这有个缺点就是违背了Rx的响应式和非阻塞的模式。如果被观察者生产数据和观察者接收数据都在同一个线程，这就天然地形成了通过阻塞方式来解决背压问题。RxJava 提供了一种响应式拉取（Reactive Pull）的方式实现异步线程阻塞以解决背压问题，通过实现Subscriber类来订阅Observable，在 subscriber 的 onStart()方法中调用Subscriber.request(n) 方法，其中n表示观察者在下次调用request方法前被观察者可发送的最大数据量。在subscriber 的 onNext()中处理完已发送的数据后，可以再次调用request()方法，以指示Observable再发射一些数据。</p>
<pre><code>someObservable.subscribe(new Subscriber&lt;T&gt;() {
    @Override
    public void onStart() {
      request(1);
    }

    @Override
    public void onCompleted() {
      // gracefully handle sequence-complete
    }

    @Override
    public void onError(Throwable e) {
      // gracefully handle error
    }

    @Override
    public void onNext(T n) {
      // do something with the emitted item &quot;n&quot;
      // request another item:
      request(1);
    }
});
</code></pre>
<h4 id="rxjava背压操作符">RxJava背压操作符</h4>
<p>Cold Observables非常适合采用响应式拉取方式解决背压，Cold Observable可能会被转化成Hot Observable，而Hot Observable不适合用响应式拉取的方式。RxJava 提供了一些流控策略的操作符解决背压，这些操作符的基本原理都是采用<em>缓存</em>或<em>丢弃</em>策略，来调节被观察者发射数据的频率。</p>
<h5 id="buffer">buffer</h5>
<p>buffer 操作符可以使观察者以固定的时间间隔周期性地接收到被观察者发送的数据，被观察者发射的数据不会被丢失。<br>
<img src="https://git4pl.github.io/post-images/1611130890101.png" alt="" loading="lazy"></p>
<h5 id="window">window</h5>
<p>window 与 buffer 类似，一个窗口类型可以让被观察者以固定的时间间隔发射可观察到的窗口，还可以选择在每次从可观察源收集特定数量的数据项时发出一个新窗口。<br>
<img src="https://git4pl.github.io/post-images/1611130927060.png" alt="" loading="lazy"></p>
<h5 id="samplethrottlelast">sample(throttleLast)</h5>
<p>sample 操作符可以从被观察者发射的数据序列中周期性地获取数据，并且取观察周期内最近发射的数据。<br>
<img src="https://git4pl.github.io/post-images/1611130939445.png" alt="" loading="lazy"></p>
<h5 id="throttlefirst">throttleFirst</h5>
<p>与 sample 类似周期地获取数据，但是取在一个观察周期后面发射的第一个数据。<br>
<img src="https://git4pl.github.io/post-images/1611130949480.png" alt="" loading="lazy"></p>
<h5 id="debounce">debounce</h5>
<p>debounce 操作符可以去除发射频率过快的数据，所以它获取的是被观察者发射的数据序列中没有后继者的那些数据。<br>
<img src="https://git4pl.github.io/post-images/1611130909637.png" alt="" loading="lazy"></p>
<h4 id="flowable">Flowable</h4>
<p>在RxJava 2.0 版本中加入了 Flowable，为了解决RxJava 1.x中 Observable 不直接支持背压的问题。Flowable 支持背压是通过在 create 方法中传入 BackpressureStrategy 指定解决背压的策略，并且提供了默认的缓存大小为128。BackpressureStrategy是一个枚举类型，具体策略的实现原理根据字面意思与上面对应的操作符类似。</p>
<pre><code>public enum BackpressureStrategy {
    //不指定背压策略
    MISSING,
    //出现背压就抛出异常
    ERROR,
    //指定无限大小的缓存池，此时不会出现异常，但无限制大量发送会发生OOM
    BUFFER,
    //如果缓存池满了就丢弃掉之后发出的事件
    DROP,
    //在DROP的基础上，强制将最后一条数据加入到缓存池中
    LATEST
}
</code></pre>
<h3 id="总结">总结</h3>
<p>解决背压的策略并不能消除过度生产或者消费不足的问题，它只是把问题转移到一个可以更好处理的操作链上。再次理解背压可以总结为：</p>
<ol>
<li>
<p>只要上游生产速度不会快到让系统崩溃或者应用场景不可接受，那么不用设置 Buffer，从而也就不用考虑 Backpressure发生。</p>
</li>
<li>
<p>只有上游生产速度可能会快到让系统崩溃，并且事件是可以丢弃的，才需要设置 Buffer。当 Buffer 有上限的时候，Backpressure 也就有可能出现；一旦 Backpressure 出现，只能选择丢弃，只是具体的丢弃策略可以根据需求而定（全部丢弃、只保留最新的一个而丢弃其余的等等，丢弃根本基本原则）。</p>
</li>
<li>
<p>如果上游生产速度可能会快到把系统搞崩溃，而事件也不可丢弃，这个时候可能修改程序的设计了：修改代码设计来规避风险，或者修改软件设计、通过让步的方式来从根源上避免问题发生。但这已经不是 Buffer 或者 Backpressure 能解决的问题了。</p>
</li>
</ol>
<h3 id="refers-to">Refers To</h3>
<p><a href="https://www.zhihu.com/question/49618581">如何形象的描述反应式编程中的背压(Backpressure)机制？</a><br>
<a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">Backpressure</a><br>
<a href="https://medium.com/@jayphelps/backpressure-explained-the-flow-of-data-through-software-2350b3e77ce7">Backpressure explained — the resisted flow of data through software</a><br>
<a href="https://proandroiddev.com/rxjava-backpressure-and-why-you-should-care-369c5242c9e6">RxJava Backpressure and why should you care?</a><br>
<a href="https://juejin.cn/post/6844903473016733710">关于 RxJava 背压</a></p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[我们一起走过2020]]></title>
        <id>https://git4pl.github.io/post/XT99my5Cu/</id>
        <link href="https://git4pl.github.io/post/XT99my5Cu/">
        </link>
        <updated>2021-01-19T11:28:59.000Z</updated>
        <content type="html"><![CDATA[<p>2020年终于过去了，这一年我们经历了很多事，有喜乐也有烦扰，从宏观环境到我们个人周遭都发生了很多事，在2020年的最后一天总结回顾一下这一年发生在我身边的大小故事吧。</p>
<h2 id="关于我个人">关于我个人</h2>
<p>关于我个人，在2020这一年里也是起起伏伏有喜有忧，对我个人而言最重要的就是家和事业。</p>
<p>1月份回到老家完成我的婚礼，完成婚礼就标志我正式有了自己的家，家里有了妻子有了儿子，他们是我最爱的人也是最爱我的人；10月份我回老家把老妈接到北京来住，她很开心，一直想跟我一起住，这一年是我和妈妈一起朝夕相处生活的时间最长的时候。有我老妈包括丈母娘来和我们小家一起住，我的这个家才圆满了，叫做一家老小其乐融融！</p>
<p>过完春节在全国疫情还不明朗的情况下，我独自从老家回到北京开始工作，2、3月份都是在家里远程办公，4月份疫情减轻了，回到公司办公。而这时我遭遇了职业生涯的第一次裁员，公司倒是按正规的补偿方案给予被裁员工补偿的，但是在全国乃至全世界疫情没有得到有效控制经济环境不好的情况下，丢掉工作是一件让人忧心的事，加之当时正值我孩子要出生的时候，的确给我不小的压力。遇到困难只有直面问题认真对待才不会被困难吓倒，我镇定下来在5月份一个月认真准备面试，积极寻找工作机会，终于在6月初入职了新的公司。</p>
<p>在经济大环境不好的条件下经历过裁员其实给了我不小的成长，懂得了一定要在职业上有圈子、有知识技能的沉淀，同时还要不断学习新技术。做技术的其实不用太担心工作机会缺少，只要练好基本功，再加上一些人脉资源和业务素养，在现代社会是有很多工作机会的；但对自己一定要有高标准，有对更好工作机会的追求热情，这就需要在某些领域技术有深度的积累。今年的特殊环境催生了大量对音视频领域的技术人才需求，而音视频也是经久不衰的技术领域，所以在第四季度我报了一个网络课程，全面深入地学习音视频开发的技术。原本计划今年考研究生，但在下半年由于工作时间比较紧，加上有音视频开发的学习课程，以及更明确地通过平时工作学习积累，导致考研准备不足，所以取消了今年的考研计划，后面再考虑考研的事情。</p>
<h2 id="关于我们家庭">关于我们家庭</h2>
<p>这一年我们成立了自己的小家庭，1月份在我哥的全力帮助下完成了我和老婆的婚礼，亲友们见证了我成家的盛典，这是很多亲人期盼的一天，遗憾的是父亲没有见证这一幕。</p>
<p>5月份的时候，老婆在北京顺利生产了我们的第一个孩子，儿子皓荣出生以及一直到现在的成长都很顺利，这也许是命运对我的福报吧，感激！儿子出生后，丈母娘来北京照顾他们母子俩3月，那时候正是我在家准备面试和刚入职新公司，感谢她的付出！</p>
<p>有了孩子的家庭才更有家的气氛，老婆在家带儿子，我在外上班，有了家的氛围，工作生活都有了劲头。9月初在我们原来租住的房子到期后，我们换了新的住处，独立租了一个两居室，房子离我工作的地方很近，开始两个月还可以每天骑着电瓶车回家吃午饭。10月初，我把我老妈从老家接到北京来住，父亲走了快10年了，她一直想在老家跟哥一家住。她很想跟我一起住住，因为我一直没有成家，在大城市生活不容易，所以一直不能满足她的愿望。现在我结婚成家了，老婆也很有孝心，老妈过来住三个月了，她很喜欢和我们住一起，有老有小的一家是很幸福的家庭！</p>
<h2 id="关于我的国家社会">关于我的国家社会</h2>
<p>2020年必定是被铭记的一年，从年初开始新冠肺炎疫情就给整个国家带来了前所未有的紧张感。一种不明来历的病毒传染很快，对人的生命造成极大的威胁，而在国际上西方国家又一直对我国存有政治和意识形态的偏见，借助这次病毒首先在我国境内大面积传播的问题，以美国为首的西方国家对我国进行无底线的挑衅和打击，在春节期间国内应对疫情的状态有相当多的困难。好在国家治理有方，有条不紊地进行了全面的抗疫，到了天气回暖后的4、5月份，国内疫情初步得到了控制，生产生活有序地逐步恢复展开，全社会逐渐恢复勃勃生机。不过疫情仍然没有被完全控制，由于国外政府管理不利加上国外人民对病毒的轻视，新冠肺炎在欧美国家相继持续爆发了，出现了他们在年初嘲笑和打击的中国面对疫情时的种种问题。7、8月分后，中国不同的地方也偶尔发生一些小规模的疫情爆发问题，不过在中国已经形成了防疫的成熟应对方案，疫情不会发生大范围的如年初那样的失控的状态。</p>
<p>这一年一直有疫情笼罩着全世界，就算在这个最需要人类共同携手解决人类问题的时刻，人类世界中也不能避免政治、经济和意识方面的斗争。美国政府一直对我国的科技企业穷追猛打，华为公司的新产品供货受阻，只好选择整体打包出售其手机产品业务。美国还不忘在中国的主权和内政问题上添油加醋扰乱是非，它们关闭中国的驻美使馆，在香港台湾问题上使乱子，当然日渐强大的中国在适当的时候给了它们正面的回击。</p>
<p>国家社会大事不是我们渺小的个人所能左右的，但我们身在其中，必须要对时事有所了解，对大势有所判断，这些可能间接甚至直接地影响到我们的工作和生活。</p>
<p>2020就此别过吧，2021充满希望，希望祖国山河锦绣、国泰民安，祝愿自己在已有的幸福生活上继续努力，获得更美好的生活！</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[记部落帖子分享微信小程序封面图片合成的一些坑]]></title>
        <id>https://git4pl.github.io/post/WmBbWgxWB/</id>
        <link href="https://git4pl.github.io/post/WmBbWgxWB/">
        </link>
        <updated>2020-08-22T01:07:08.000Z</updated>
        <content type="html"><![CDATA[<h3 id="项目背景">项目背景</h3>
<p>58部落帖子详情的分享功能里分享到微信小程序，原有的封面图是由Server端下发的图片，若帖子是纯文本内容则Server下发的是58部落的广告默认图，这样分享出去的微信小程序封面图形势比较简单。为了丰富小程序封面图的内容，激起收到分享的用户点击进入小程序查看帖子的兴趣，本次项目针对这个小程序封面图进行优化设计。</p>
<p>新版本的分享到小程序的封面图设计是这样的：用帖子的素材合成一副新的封面图，要求封面图的顶部展示用户头像、名称以及帖子的浏览量。根据帖子是否带有图片，合成的封面图有两种不同的样式：<br>
1、带图片的帖子，取帖子第一张图片，在用户信息下面展示图片内容<br>
2、纯文本的帖子，取帖子正文内容展示在用户信息下面，最多显示6行帖子内容<br>
<img src="https://git4pl.github.io/post-images/1598063903913.png" alt="cover with picture" loading="lazy"><br>
<img src="https://git4pl.github.io/post-images/1598063965619.png" alt="cover with text" loading="lazy"></p>
<h3 id="实现方案">实现方案</h3>
<p>由于封面图中的元素都是Server动态下发的，尤其是图片需要下载后才能处理。一开始想到用UI控件来创建这个布局，可以很方便地利用控件的属性实现UI效果的细节，但是要生成的封面图没有Activity或者其他ViewGroup容器来承载它的绘制，最后采用Canvas + Paint来动态绘制，并将生成的Bitmap保存为本地文件。在实现这个UI效果的过程中遇到了一些技术上的小坑，记录下来以备后续复用。</p>
<h4 id="绘制圆形头像">绘制圆形头像</h4>
<p>利用Canvas绘制圆形图是很容易的事，通常可以使用这三种API在Canvas上实现圆形图：BitmapShader、ClipPath和PorterDuffXfermode。</p>
<h5 id="bitmapshager-图片渲染方式">BitmapShager 图片渲染方式</h5>
<pre><code>public Bitmap getCirleBitmap(Bitmap bmp) {
    //获取bmp的宽高 小的一个做为圆的直径r
    int w = bmp.getWidth();
    int h = bmp.getHeight();
    int r = Math.min(w, h);
 
    //创建一个paint
    Paint paint = new Paint();
    paint.setAntiAlias(true);
 
    //新创建一个Bitmap对象newBitmap 宽高都是r
    Bitmap newBitmap = Bitmap.createBitmap(r, r, Bitmap.Config.ARGB_8888);
 
    //创建一个使用newBitmap的Canvas对象
    Canvas canvas = new Canvas(newBitmap);
 
    //创建一个BitmapShader对象 使用传递过来的原Bitmap对象bmp
    BitmapShader bitmapShader = new BitmapShader(bmp, Shader.TileMode.CLAMP，Shader.TileMode.CLAMP);
 
    //paint设置shader
    paint.setShader(bitmapShader);
 
    //canvas画一个圆 使用设置了shader的paint
    canvas.drawCircle(r / 2, r / 2, r / 2, paint);
   
    return newBitmap;
}
</code></pre>
<h5 id="clippath-裁剪区域">ClipPath 裁剪区域</h5>
<pre><code>public Bitmap getCirleBitmap(Bitmap bmp) {
    //获取bmp的宽高 小的一个做为圆的直径r
    int w = bmp.getWidth();
    int h = bmp.getHeight();
    int r = Math.min(w, h);
 
    //创建一个paint
    Paint paint = new Paint();
    paint.setAntiAlias(true);
 
    //新创建一个Bitmap对象newBitmap 宽高都是r
    Bitmap newBitmap = Bitmap.createBitmap(r, r, Bitmap.Config.ARGB_8888);
 
    //创建一个使用newBitmap的Canvas对象
    Canvas canvas = new Canvas(newBitmap);
 
    //创建一个Path对象，path添加一个圆 圆心半径均是r / 2， Path.Direction.CW顺时针方向
    Path path = new Path();
    path.addCircle(r / 2, r / 2, r / 2, Path.Direction.CW);
    //canvas绘制裁剪区域
    canvas.clipPath(path);   
    //canvas将图画到留下的圆形区域上
    canvas.drawBitmap(bmp, 0, 0, paint);
 
    return newBitmap;
}
</code></pre>
<h5 id="porterduffxfermode-图片混合模式">PorterDuffXfermode 图片混合模式</h5>
<pre><code>public Bitmap getCirleBitmap(Bitmap bmp) {
    //获取bmp的宽高 小的一个做为圆的直径r
    int w = bmp.getWidth();
    int h = bmp.getHeight();
    int r = Math.min(w, h);

    //创建一个paint
    Paint paint = new Paint();
    paint.setAntiAlias(true);

    //新创建一个Bitmap对象newBitmap 宽高都是r
    Bitmap newBitmap = Bitmap.createBitmap(r, r, Bitmap.Config.ARGB_8888);

    //创建一个使用newBitmap的Canvas对象
    Canvas canvas = new Canvas(newBitmap);

    //canvas画一个圆形
    canvas.drawCircle(r / 2, r / 2, r / 2, paint);

    //然后 paint要设置Xfermode 模式为SRC_IN 显示上层图像（后绘制的一个）的相交部分
    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

    //canvas调用drawBitmap直接将bmp对象画在画布上 
    //因为paint设置了Xfermode，所以最终只会显示这个bmp的一部分 
    //也就是bmp的和下层圆形相交的一部分圆形的内容
    canvas.drawBitmap(bmp, 0, 0, paint);

    return newBitmap;
}
</code></pre>
<p>虽然上面三种方式都能绘出圆形图，但在实际项目需求中有一些坑需要注意。<br>
使用BitmapShader要注意图像缩放的处理，可以计算好缩放比例，使用Matrix对象设置缩放比例，然后将Matrix对象设置到BitmapShader对象中。<br>
使用Canvas的clipPath方法设置path，然后调用drawBitmap绘制的图形有锯齿现象，即使在paint中设置了抗锯齿参数也不能消除。<br>
使用Xfermode的方式，在实际项目中如果直接用上面代码里的写法将不能得到预期的圆形图，需要使用saveLayer方法将绘制操作保存到新的图层，将图像合成的处理放到离屏缓存中进行。</p>
<h4 id="文字居中绘制">文字居中绘制</h4>
<p>项目需求中要求顶部的用户头像和用户名水平方向居中对齐，这两者要居中对齐可以确定一个基准参考线，比如都关于顶部栏纵向的中线上下对称，头像关于该中线对称很容易实现，而要实现文字关于该中线的对称需要理解Android绘制文字的原理以及drawText方法的参数意义。<br>
<img src="https://git4pl.github.io/post-images/1598087533894.png" alt="" loading="lazy"><br>
Android的文字绘制是按如上图所示的基线绘制的，与之对应的实现是Paint类中的FontMetrics类的定义。</p>
<pre><code>public static class FontMetrics {
    /**
        * The maximum distance above the baseline for the tallest glyph in
        * the font at a given text size.
        */
    public float   top;
    /**
        * The recommended distance above the baseline for singled spaced text.
        */
    public float   ascent;
    /**
        * The recommended distance below the baseline for singled spaced text.
        */
    public float   descent;
    /**
        * The maximum distance below the baseline for the lowest glyph in
        * the font at a given text size.
        */
    public float   bottom;
    /**
        * The recommended additional space to add between lines of text.
        */
    public float   leading;
}
</code></pre>
<p>drawText()方法的x、y指定要绘制文字的基准点，该基准点是要绘制的文字基准线上的left、center、right三点之一（如下图），具体是哪个由paint的setTextAlign()方法设置，默认是left。<br>
<img src="https://git4pl.github.io/post-images/1598088667357.png" alt="" loading="lazy"><br>
要让所画的文字刚好相对于我们选定的参考线上下居中，即让文字的中心点落在参考线上，我们需要计算调用drawText()方法的y坐标，即上图中基线的y坐标值。可以将问题转化为计算中心点相对于基线的距离，文字中心点相对于其基线的距离可表示为 (top+bottom)/2 - bottom，在屏幕坐标系中top的实际值相对于基线是负数，所以前述距离公式为 (-top+bottom)/2 - bottom，简化后为 -top/2 - bottom/2，将此值加上参考线的y坐标值即为我们所求的y坐标。另外要注意paint.getFontMetrics()这个方法一定要在设置字体大小或者样式等一系列会影响字体的方法后调用，不然获取到的top和bottom值不准。</p>
<pre><code>Paint paint = new Paint();
paint.setTextSize(textSize);
paint.setAntiAlias(true);
Paint.FontMetrics fm = paint.getFontMetrics();
float top = fm.top;
float bottom = fm.bottom;
float baseLineY = (headH &gt;&gt; 1) - top / 2 - bottom / 2;
canvas.drawText(username, x, baseLineY, paint);
</code></pre>
<h4 id="绘制多行文字">绘制多行文字</h4>
<p>Canvas.drawText()只能绘制单行的文字，不能换行。它既不能在View的边缘自动折行显示，也不能在换行符\n处换行。而StaticLayout 支持换行，它既可以为文字设置宽度上限来让文字自动换行，也会在 \n 处主动换行。<br>
StaticLayout通过构造函数参数设置文字显示的属性。StaticLayout 的构造方法是 StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad)，其中参数里：</p>
<pre><code>width 是文字区域的宽度，文字到达这个宽度后就会自动换行；
align 是文字的对齐方向；
spacingmult 是行间距的倍数，通常情况下填 1 就好；
spacingadd 是行间距的额外增加值，通常情况下填 0 就好；
includepad 是指是否在文字上下添加额外的空间，来避免某些过高的字符的绘制出现越界。
</code></pre>
<h4 id="图片裁剪压缩">图片裁剪压缩</h4>
<p>微信分享SDK对分享到微信小程序的封面图大小有限制，要求体积不超过128KB，对图片分辨率没有明确要求，但是在不同机型以及平台终端上展示分享出去的小程序链接里发现微信SDK对封面图的尺寸比例是有要求的。小程序在显示分享的封面图时，图片的宽高比为5:4，不符合这个比例的尺寸，微信客户端将会对其进行裁剪。</p>
<h3 id="thanks-to">Thanks To</h3>
<p><a href="https://blog.csdn.net/zly921112/article/details/50401976">android canvas drawText()文字居中</a><br>
<a href="https://www.jianshu.com/p/5136fbc0d301">StaticLayout支持文字绘制换行</a><br>
<a href="https://blog.csdn.net/TO_BE_RM/article/details/79859540">初识Android Bitmap压缩原理</a></p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[利用Canvas绘制圆形图]]></title>
        <id>https://git4pl.github.io/post/Vgyf9i2nb/</id>
        <link href="https://git4pl.github.io/post/Vgyf9i2nb/">
        </link>
        <updated>2020-08-16T10:35:52.000Z</updated>
        <content type="html"><![CDATA[<p>最近实现一个业务需求，要求将用户头像、用户名和正文内容合成为一张图片，以作为给微信小程序分享的封面图。通过一些ImageView控件是很容易实现圆形头像的，但这里需要创建一块画布，在Canvas上画出圆形头像，如果细节处理不好，可能达不到理想的效果。<br>
通常用Java代码绘制圆形图有三种方法，分别是使用BitmapShader、PorterDuffXfermode和ClipPath。这里记录一下这三种画圆形图的代码实现：</p>
<h4 id="bitmapshager-图片渲染方式">BitmapShager 图片渲染方式</h4>
<pre><code>public Bitmap getCirleBitmap(Bitmap bmp) {
    //获取bmp的宽高 小的一个做为圆的直径r
    int w = bmp.getWidth();
    int h = bmp.getHeight();
    int r = Math.min(w, h);
 
    //创建一个paint
    Paint paint = new Paint();
    paint.setAntiAlias(true);
 
    //新创建一个Bitmap对象newBitmap 宽高都是r
    Bitmap newBitmap = Bitmap.createBitmap(r, r, Bitmap.Config.ARGB_8888);
 
    //创建一个使用newBitmap的Canvas对象
    Canvas canvas = new Canvas(newBitmap);
 
    //创建一个BitmapShader对象 使用传递过来的原Bitmap对象bmp
    BitmapShader bitmapShader = new BitmapShader(bmp, Shader.TileMode.CLAMP，Shader.TileMode.CLAMP);
 
    //paint设置shader
    paint.setShader(bitmapShader);
 
    //canvas画一个圆 使用设置了shader的paint
    canvas.drawCircle(r / 2, r / 2, r / 2, paint);
   
    return newBitmap;
}
</code></pre>
<h4 id="porterduffxfermode-图片混合模式">PorterDuffXfermode 图片混合模式</h4>
<pre><code>    public Bitmap getCirleBitmap(Bitmap bmp) {
        //获取bmp的宽高 小的一个做为圆的直径r
        int w = bmp.getWidth();
        int h = bmp.getHeight();
        int r = Math.min(w, h);
 
        //创建一个paint
        Paint paint = new Paint();
        paint.setAntiAlias(true);
 
        //新创建一个Bitmap对象newBitmap 宽高都是r
        Bitmap newBitmap = Bitmap.createBitmap(r, r, Bitmap.Config.ARGB_8888);
 
        //创建一个使用newBitmap的Canvas对象
        Canvas canvas = new Canvas(newBitmap);
 
        //canvas画一个圆形
        canvas.drawCircle(r / 2, r / 2, r / 2, paint);
 
        //然后 paint要设置Xfermode 模式为SRC_IN 显示上层图像（后绘制的一个）的相交部分
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
 
        //canvas调用drawBitmap直接将bmp对象画在画布上 
        //因为paint设置了Xfermode，所以最终只会显示这个bmp的一部分 
        //也就是bmp的和下层圆形相交的一部分圆形的内容
        canvas.drawBitmap(bmp, 0, 0, paint);
 
        return newBitmap;
    }
</code></pre>
<h4 id="clippath-裁剪区域">ClipPath 裁剪区域</h4>
<pre><code>public Bitmap getCirleBitmap(Bitmap bmp) {
    //获取bmp的宽高 小的一个做为圆的直径r
    int w = bmp.getWidth();
    int h = bmp.getHeight();
    int r = Math.min(w, h);
 
    //创建一个paint
    Paint paint = new Paint();
    paint.setAntiAlias(true);
 
    //新创建一个Bitmap对象newBitmap 宽高都是r
    Bitmap newBitmap = Bitmap.createBitmap(r, r, Bitmap.Config.ARGB_8888);
 
    //创建一个使用newBitmap的Canvas对象
    Canvas canvas = new Canvas(newBitmap);
 
    //创建一个Path对象，path添加一个圆 圆心半径均是r / 2， Path.Direction.CW顺时针方向
    Path path = new Path();
    path.addCircle(r / 2, r / 2, r / 2, Path.Direction.CW);
    //canvas绘制裁剪区域
    canvas.clipPath(path);   
    //canvas将图画到留下的圆形区域上
    canvas.drawBitmap(bmp, 0, 0, paint);
 
    return newBitmap;
}
</code></pre>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[关于类加载的时机与过程]]></title>
        <id>https://git4pl.github.io/post/OH92heMH4/</id>
        <link href="https://git4pl.github.io/post/OH92heMH4/">
        </link>
        <updated>2020-07-19T02:54:31.000Z</updated>
        <content type="html"><![CDATA[<p>思考以下代码的输出结果：</p>
<pre><code>public class Singleton {
    private static Singleton instance = new Singleton();
    public static int count1;
    public static int count2 = 0;
    private Singleton() {
        count1 ++;
        count2 ++;
    }

    public static Singleton getInstance() {
        return instance;
    }
｝

public class Test {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println(&quot;count1 = &quot; + Singleton.count1);
        System.out.println(&quot;count2 = &quot; + Singleton.count2);
    }
}
</code></pre>
<blockquote>
<p>错误答案<br>
count1 = 1<br>
count2 = 1</p>
</blockquote>
<blockquote>
<p>正确答案<br>
count1 = 1<br>
count2 = 0</p>
</blockquote>
<p>这个问题就是牵涉到类的加载与过程，虚拟机定义了以下六种情况，如果类未被初始化，则会进行初始化：<br>
1、创建类的实例<br>
2、访问类的静态变量(除常量【被final修辞的静态变量】原因:常量一种特殊的变量，因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable)，编译器并不会生成字节码来从对象中载入域的值，而是直接把这个值插入到字节码中。这是一种很有用的优化，但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译。<br>
3、访问类的静态方法<br>
4、反射如(Class.forName(&quot;my.xyz.Test&quot;))<br>
5、当初始化一个类时，发现其父类还未初始化，则先出发父类的初始化<br>
6、虚拟机启动时，定义了main()方法的那个类先初始化</p>
<p>我们来分析以下上述代码的执行情况：</p>
<ol>
<li>main()方法 Test类初始化</li>
<li>main()方法第一句：访问Singleton的getInstance()静态方法 Singleton类初始化，此时按照代码执行顺序进行静态成员的初始化默认值<br>
instance = null<br>
count1 = 0<br>
count2 = 0</li>
<li>按照代码执行顺序为类的静态成员赋值：<br>
private static Singleton instance = new Singleton(); instance调用Singleton的构造方法,调用构造方法后 count1 = 1,count2 = 1<br>
public static int count1; count1没有进行赋值操作，所以count1 = 1<br>
public static int count2 = 0; count2进行赋值操作，所以count2 = 0</li>
<li>main()方法第二句：访问Singleton的count1变量，由于count1没有赋初始值，所以count1 = 1</li>
<li>main()方法第三句：访问Singleton的count2变量，由于count2赋了初始值 0，所以count2 = 0</li>
</ol>
<p>如果我们把Singleton代码执行顺序变化一下：</p>
<pre><code>public class Singleton {
    public static int count1;
    public static int count2 = 0;
    private static Singleton instance = new Singleton();

    private Singleton() {
        count1++;
        count2++;
    }

    public static Singleton getInstance() {
        return instance;
    }

}
</code></pre>
<p>此时输出结果就为:</p>
<blockquote>
<p>count1 = 1<br>
count2 = 1</p>
</blockquote>
<p>如果改为如下代码，那么运行情况又是怎样：</p>
<pre><code>public class Singleton {
    Singleton(){
        System.out.println(&quot;Singleton construct&quot;);
    }

    static {
        System.out.println(&quot;Singleton static block&quot;);
    }

    public static final int COUNT = 1;

}

public class Test {
    public static void main(String[] args) {
        System.out.println(&quot;count = &quot; + Singleton.COUNT);
    }
}
</code></pre>
<p>运行结果为：</p>
<blockquote>
<p>count = 1</p>
</blockquote>
<p>由于常量在编译阶段会存入相应类的常量池当中，所以在实际调用中Singleton.COUNT并没有直接引用到Singleton类，因此不会进行Singleton类的初始化，所以输出结果为 count = 1</p>
<h3 id="thanks-to">Thanks To</h3>
<p><a href="https://www.jianshu.com/p/27eb533b29f7">Kotlin:由object和companion object创建的单例模式引发的思考</a></p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[适配器模式笔记]]></title>
        <id>https://git4pl.github.io/post/NUANIynJT/</id>
        <link href="https://git4pl.github.io/post/NUANIynJT/">
        </link>
        <updated>2020-07-11T01:28:47.000Z</updated>
        <content type="html"><![CDATA[<h3 id="开篇小引">开篇小引</h3>
<p>在上大学时我做了一个小项目是关于人机交互的应用，我负责实现的部分是通过语音识别技术接收用户的问题，然后在数据库中检索预置的问题答案，并由我们的应用“说”出来对用户的问题进行解答。当时我就由这个应用的交互模式产生了一个小小的想法，我希望有一天可以实现一个很简单方便的东西能让中国的老奶奶和美国甚至更多其他国家的老奶奶坐在一起轻松愉快地聊天，完全不用担心各自语言的障碍，这涉及到语音识别和机器翻译等诸多技术，现在顶级的科技公司已经在各种细分的技术上有研究实践了。我的那个小想法其实与本篇博客的主题——适配器模式——有相似的实现思想。</p>
<h3 id="定义及特点">定义及特点</h3>
<p><strong>适配器模式将一个类的接口转换成客户期望的另一个接口，使得原本因接口不匹配而无法在一起工作的两个类能够在一起工作。</strong></p>
<p>适配器模式可以通过创建适配器进行接口转换，让不兼容的接口变得能兼容。这可以让客户从实现的接口解耦，如果在一段时间之后，我们想要改变接口，适配器可以将改变的部分封装起来，客户端就不必为了应对不同的接口而每次跟着修改。</p>
<p>适配器模式涉及到三种角色：<br>
●  目标接口（Target）——定义客户期望的与特定领域相关的接口<br>
●  适配者类（Adaptee）——定义一个已经存在的接口，是需要适配的类<br>
●  适配器类（Adapter）——通过包装一个需要适配的对象，把原接口转换成目标接口</p>
<h3 id="适用场景">适用场景</h3>
<p>以下情况可使用适配器模式：<br>
√  系统需要使用现有的类，而这些类的接口不符合系统的接口<br>
√  想要建立一个可以重用的类，用于与一些彼此之间没有太大关联的一些类，包括一些可能在将来引进的类一起工作<br>
√  两个类所做的事情相同或相似，但是具有不同接口的时候<br>
√  旧的系统开发的类已经实现了一些功能，但是客户端却只能以另外接口的形式访问，但我们不希望手动更改原有类的时候<br>
√  使用第三方组件，组件接口定义和自己定义的不同，不希望修改自己的接口，但是要使用第三方组件接口的功能</p>
<h3 id="实现方式">实现方式</h3>
<p>在GoF的设计模式中，适配器模式有两种可行的实现方式：<strong>类适配器</strong>和<strong>对象适配器</strong>，类适配器使用多重继承对一个接口与另一个接口进行匹配，如下图所示<br>
<img src="https://git4pl.github.io/post-images/1594431276853.png" alt="" loading="lazy"><br>
对象适配器依赖于对象组合，如下图所示<br>
<img src="https://git4pl.github.io/post-images/1594431299777.webp" alt="" loading="lazy"><br>
类适配器因为需要Adapter类同时继承自Target类和Adaptee类，所以只能在支持多重继承的语言中实现（Java虽然不支持多继承，但可以通过实现接口implements interface来实现类适配器，如下代码示例）；对象适配器模式充满着良好的OO设计原则：使用对象组合，以修改的接口包装被适配者，这样被适配者的任何子类，都可以搭配着适配器使用。</p>
<pre><code>public interface Target {
    /**
     * 这是适配者类Adaptee也有的方法
     */
    public void sampleOperation1(); 
    /**
     * 这是适配者类Adapteee没有的方法
     */
    public void sampleOperation2(); 
}

public class Adaptee {
    void sampleOperation1(){}
}

public class Adapter extends Adaptee implements Target {
    /**
     * 由于适配者类Adaptee没有方法sampleOperation2()
     * 因此适配器类Adapter补充上这个方法
     */
    @Override
    public void sampleOperation2() {
        //实现相关的代码
    }
}
</code></pre>
<p>类适配器和对象适配器有不同的权衡。<br>
类适配器：<br>
●  用一个具体的Adapter类对Adaptee和Target进行匹配，这使得Adapter不能和Adaptee的子类一起工作<br>
●  使得Adapter可以重定义Adaptee的部分行为，因为Adapter是Adaptee的子类<br>
●  仅仅引入了一个对象，并不需要额外的指针以间接得到Adaptee<br>
对象适配器：<br>
●  允许一个Adapter与多个Adaptee（即Adaptee本身以及它的所有子类）同时工作，Adapter也可以一次给所有的Adaptee添加功能<br>
●  重定义Adaptee的行为比较困难（这需要生成Adaptee的子类并且使得Adapter引用这个子类而不是引用Adaptee本身）</p>
<h3 id="适配器小结">适配器小结</h3>
<p>使用适配器的优点：<br>
1、通过适配器，客户端可以调用同一接口，因而对客户端来说是透明的。这样做更简单、更直接、更紧凑。<br>
2、复用了现存的类，解决了现存类和复用环境要求不一致的问题。<br>
3、将目标类和适配者类解耦，通过引入一个适配器类重用现有的适配者类，而无需修改原有代码。<br>
4、一个对象适配器可以把多个不同的适配者类适配到同一个目标，也就是说，同一个适配器可以把适配者类和它的子类都适配到目标接口。</p>
<p>使用适配器的缺点：<br>
过多的使用适配器，会让系统非常零乱，不易整体进行把握。</p>
<h3 id="thanks-to">Thanks To</h3>
<p><a href="http://www.cnblogs.com/wangjq/archive/2012/07/09/2582485.html">设计模式学习笔记-适配器模式</a><br>
<a href="http://www.cnblogs.com/java-my-life/archive/2012/04/13/2442795.html">《JAVA与模式》之适配器模式</a></p>
]]></content>
    </entry>
</feed>